Skip to content

P1 — MemBus.Contracts (fleet.v1 envelope) — build spec

Council-ratified fleet comms+command bus. This is the FIRST buildable unit of bus-P1: the shared contract package. It unblocks peer session WT-60fa (fleet-telegram / worktree-mail). NATS transport (MemBus.Nats) and compose come as later P1 sub-steps — NOT in this task.

Hard constraints

  • C#, net10.0, Nullable enable, ImplicitUsings enable, file-scoped namespaces, record types. Match src/MemBus.Core/MemBus.Core.csproj exactly.
  • JSON: System.Text.Json, JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = WhenWritingNull — identical to the JsonOpts in src/MemBus.Core/MemBusClient.cs. Expose those options as a shared static FleetJson.Options in the new package so every serializer uses one config.
  • REUSE, do not duplicate, existing MemBus.Core types: Harness (enum + .Trust()), Provenance. Add a project reference MemBus.Contracts → MemBus.Core. The envelope's provenance/trust/dedupe fields ARE Provenance — do not re-declare origin_harness/origin_machine/trust_level/dedupe_key.
  • No NATS dependency in this package. Pure POCO contracts + serialization + validation. Transport-agnostic.

New project

src/MemBus.Contracts/MemBus.Contracts.csproj, added to MemBus.slnx under /src/. Namespace MemBus.Contracts.

Types

FleetSender (record)

  • string SessionId (required) — harness-native session id (Claude transcript uuid / codex rollout id / grok session). ALWAYS present. This is the crypto identity anchor.
  • string? WorktreeId — WT-xxxx, populated ONLY when cwd→worktree-identity resolves; null for unregistered harnesses.
  • string Pc (required) — Environment.MachineName at publish.
  • Harness Harness (required) — reuse MemBus.Core.Harness enum.
  • string? Model
  • string? Repo, string? Branch — WorktreeResolver git context.

FleetTarget (record)

  • string? Pc (null = broadcast within scope), string? WorktreeId.

TrustLevel (enum): Data, Command, Dangerous. Serialize lowercase (JsonStringEnumConverter + camelCase).

CommandClass (enum): Safe, BoundedWrite, Network, Dangerous. Server-side classification only — NEVER a client-supplied field on the envelope.

FleetEnvelope (record) — the wire type

Fields (camelCase on the wire): - string Schema = "fleet.v1" (const default) - string Id (uuid), string? CorrelationId, string? CausationId - DateTimeOffset TimestampUtc - string Nonce (base64, caller-supplied random ≥16 bytes) - FleetSender Sender (required) - FleetTarget? Target - string Kind (required) — e.g. "ChatMessage", "CommandRequested", "CommandResult", "Presence", "CouncilRequested", "ApprovalRequired", "ApprovalDecision". - IReadOnlyList<string> Capabilities (default empty) - bool Taint (default false) - TrustLevel TrustLevel - string? Signature (base64 Ed25519 over the canonical signing string — see below; null until MemBus.Security signs it at P2) - Provenance? Provenance — reuse MemBus.Core.Provenance (maps origin_harness/machine/trust/ttl/dedupe/schema). Optional on chat, required on audit. - JsonElement? Payload — discriminated command/result records live here.

Add dedupe_key = Id convention: expose string DedupeKey => CorrelationId ?? Id;

Signing canonical string (define now, sign at P2)

static string SigningString(FleetEnvelope e)$"{e.Id}|{e.TimestampUtc:O}|{e.Sender.SessionId}|{e.Kind}|{bodyHash}" where bodyHash = base64(SHA256(payload utf8 bytes, or empty)). Signing key is anchored on Sender.SessionId, NOT WorktreeId. Document this in an XML comment.

Subjects (static class FleetSubjects)

Const/format-helpers, versioned fleet.v1.: - ChatWt(wtId) = fleet.v1.chat.wt.{wtId}; ChatBroadcast = fleet.v1.chat.broadcast - CmdRequest(pc) = fleet.v1.cmd.request.{pc}; CmdResult(correlationId) = fleet.v1.cmd.result.{correlationId} - Presence(pc, wtId) = fleet.v1.presence.{pc}.{wtId} - CouncilRequest(councilId), CouncilResult(correlationId) - AuditAppend = fleet.v1.audit.append - ApprovalRequired = fleet.v1.approval.required; ApprovalDecision(id) = fleet.v1.approval.decision.{id}

Command payload records (skeletons, safe-class only for now)

Discriminated by a Type string. Base abstract record FleetCommand(string Type). Concrete safe-class: - RepoStatus(string Repo) — Type "repo.status" - RunTests(string Repo, string? Filter) — Type "run.tests" Result: record FleetCommandResult(bool Ok, int? ExitCode, string? StdoutRef, string? Error, long DurationMs). Approval: record ApprovalRequired(string CommandId, string CommandSummary, string RiskReason, IReadOnlyList<string> Capabilities), record ApprovalDecision(string CommandId, bool Approved, string Approver, DateTimeOffset DecidedAtUtc). (No dangerous/bounded-write/network command records yet — P4.)

Tests

tests/MemBus.Tests (existing project). Add: - Round-trip: serialize each record with FleetJson.Options, deserialize, assert equality. Assert wire is camelCase, nulls omitted, enums lowercase. - TrustLevel/CommandClass serialize to lowercase strings. - DedupeKey returns CorrelationId when set, else Id. - SigningString is stable + uses SessionId (not WorktreeId): two envelopes differing only in WorktreeId produce the SAME signing string. - Envelope with null Signature/Target/Payload serializes without those keys.

Gates

  • dotnet build MemBus.slnx clean, zero warnings.
  • dotnet test all green.
  • Do NOT add auto-exec, NATS, or any transport. Contracts + tests only.
  • Do NOT touch MemBus.Core except adding the project reference target (Contracts references Core, not the reverse).

Deliverable

New src/MemBus.Contracts/**, slnx updated, tests added, build+test green. Report the file list and test count.