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,Nullableenable,ImplicitUsingsenable, file-scoped namespaces,recordtypes. Matchsrc/MemBus.Core/MemBus.Core.csprojexactly. - JSON:
System.Text.Json,JsonNamingPolicy.CamelCase,DefaultIgnoreCondition = WhenWritingNull— identical to theJsonOptsinsrc/MemBus.Core/MemBusClient.cs. Expose those options as a shared staticFleetJson.Optionsin the new package so every serializer uses one config. - REUSE, do not duplicate, existing
MemBus.Coretypes:Harness(enum +.Trust()),Provenance. Add a project reference MemBus.Contracts → MemBus.Core. The envelope's provenance/trust/dedupe fields AREProvenance— 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.MachineNameat publish.Harness Harness(required) — reuse MemBus.Core.Harness enum.string? Modelstring? 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.slnxclean, zero warnings.dotnet testall 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.