Repository navigation
feat(backend): add experimental /backend conversation and harness contract [3/4] - #2253
Conversation
🟠 Impact Analysis — PR #2253Risk tier: 🟠 HIGH 📊 Summary
🎯 Risk Factors
📦 Modules Affecteddocs (5 files)
root (4 files)
squad-cli (4 files)
squad-sdk (7 files)
tests (6 files)
|
🛫 PR Readiness Check
PR Scope: 📦🔧 Mixed (product + infrastructure)
|
| Status | Check | Details |
|---|---|---|
| ❌ | Single commit | 6 commits — consider squashing before review |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with tamirdresher-microsoft-stack-2-casting-repair |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | Changeset file found |
| ✅ | Scope clean | |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | 1 active Copilot thread(s) resolved (8 outdated skipped) |
| ✅ | CI passing | All checks passing |
Files Changed (26 files, +8725 −14)
| File | +/− |
|---|---|
.changeset/squad-2-0-multi-host-clients.md |
+5 −1 |
docs/proposals/squad-2.0-security-and-isolation.md |
+104 −0 |
docs/specification/README.md |
+6 −0 |
docs/specification/adr-001-client-control-execution-boundary.md |
+73 −0 |
docs/specification/client-backend-decoupling-v0.2.md |
+537 −0 |
docs/specification/client-harness-adapters-v0.2.md |
+559 −0 |
packages/squad-cli/src/cli/commands/state-mcp.ts |
+46 −8 |
packages/squad-cli/src/cli/core/backend-mcp.ts |
+359 −0 |
packages/squad-cli/src/cli/core/command-help.ts |
+5 −1 |
packages/squad-cli/src/cli/core/upgrade.ts |
+44 −3 |
packages/squad-sdk/package.json |
+4 −0 |
packages/squad-sdk/src/backend/durable-http-backend.ts |
+264 −0 |
packages/squad-sdk/src/backend/harness-registry.ts |
+47 −0 |
packages/squad-sdk/src/backend/index.ts |
+40 −0 |
packages/squad-sdk/src/backend/local-backend.ts |
+945 −0 |
packages/squad-sdk/src/backend/state-backend-store.ts |
+192 −0 |
packages/squad-sdk/src/backend/types.ts |
+435 −0 |
test-fixtures/spec/suite-v0.1/index.json |
+38 −0 |
test-fixtures/spec/suite-v0.2/capability-matrix.json |
+155 −0 |
test-fixtures/spec/suite-v0.2/golden-trace.json |
+63 −0 |
test/backend.test.ts |
+1772 −0 |
test/cli/upgrade.test.ts |
+1 −1 |
test/specification-suite.test.ts |
+2 −0 |
test/squad-20-all-scenarios.e2e.test.ts |
+2056 −0 |
test/squad-20-real-clients-stdio.e2e.test.ts |
+487 −0 |
test/squad-20-security-contracts.test.ts |
+486 −0 |
Total: +8725 −14
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
🏗️ Architectural Review
Automated architectural review — informational only. |
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved execution races, shared-storage locking gaps, and approval-delivery problems can duplicate work or lose conversation state.
9 open issues
Optional harness target can silently queue unexecutable turns · New Omitted event limit bypasses the 500-event cap · New Response read failures are incorrectly reported as healthy · New Duplicate dispatch can execute one turn twice · New Queued approval delivery can block cancellation · New Missing harness identity misroutes events across harnesses · New Overwriting pending approvals strands the first request · New Worktree-specific locks allow shared state overwrite races · New Tests inherit ambient remote backend credentials · New
What changed in this PR
Adds an experimental conversation backend layer that separates shared conversation control from host execution.
Changes:
- Introduces
/backendcontracts, local persistence, HTTP transport, and harness registration. - Adds explicitly enabled MCP conversation tools and CLI help.
- Adds contract tests, protocol fixtures, specifications, and improved casting-repair diagnostics.
| File | Description |
|---|---|
test/squad-20-security-contracts.test.ts |
Tests security and isolation contracts. |
test/squad-20-real-clients-stdio.e2e.test.ts |
Exercises projected clients through real MCP processes. |
test/specification-suite.test.ts |
Registers new requirements documents. |
test/cli/upgrade.test.ts |
Checks actionable casting-repair errors. |
test-fixtures/spec/suite-v0.2/golden-trace.json |
Defines recovery and cancellation trace. |
test-fixtures/spec/suite-v0.2/capability-matrix.json |
Defines capability negotiation baselines. |
test-fixtures/spec/suite-v0.1/index.json |
Registers draft profiles. |
packages/squad-sdk/src/backend/types.ts |
Defines experimental backend and harness contracts. |
packages/squad-sdk/src/backend/state-backend-store.ts |
Adapts configured storage with locking. |
packages/squad-sdk/src/backend/local-backend.ts |
Implements persisted conversation control. |
packages/squad-sdk/src/backend/index.ts |
Exports backend APIs and factory. |
packages/squad-sdk/src/backend/harness-registry.ts |
Registers harnesses and validates extensions. |
packages/squad-sdk/src/backend/durable-http-backend.ts |
Implements remote transport and retries. |
packages/squad-sdk/package.json |
Exposes the /backend subpath. |
packages/squad-cli/src/cli/core/upgrade.ts |
Ignores backend state and improves repair diagnostics. |
packages/squad-cli/src/cli/core/command-help.ts |
Documents experimental opt-in. |
packages/squad-cli/src/cli/core/backend-mcp.ts |
Implements backend MCP tools. |
packages/squad-cli/src/cli/commands/state-mcp.ts |
Wires opt-in tools into the server. |
docs/specification/README.md |
Indexes new profiles and dependencies. |
docs/specification/client-harness-adapters-v0.2.md |
Documents adapter boundaries. |
docs/specification/client-backend-decoupling-v0.2.md |
Documents conversation semantics and limitations. |
docs/specification/adr-001-client-control-execution-boundary.md |
Records architectural boundaries. |
docs/proposals/squad-2.0-security-and-isolation.md |
Describes implemented protections and remaining limitations. |
.changeset/squad-2-0-multi-host-clients.md |
Records backend release details. |
Review effort: Balanced
- Auto-select a single registered in-process harness in `squad_backend_send_message` when `harness` is omitted, and reject an omitted `harness` when multiple harnesses are registered instead of silently queueing; keep remote routing separate
- Default an omitted `limit` in `squad_backend_read_events` to `MAX_READ_EVENTS_LIMIT` (500)
- Read the response body outside the JSON parse `try/catch` in `DurableHttpSquadBackend.probe` so body timeouts and stream errors fail health checks
- Coordinate in-flight `adapter.submit` dispatches by `(conversationId, runId, seq)` and pass a deterministic `operationId` (`${conversationId}:${runId}:${seq}`) for crash-retry deduplication
- Remove queued approval deliveries for a turn when committing cancellation (`markCancelled` / terminal state) so `flushDeliveries` never retries or blocks on an obsolete approval
- Thread the publishing `harnessId` through `ensureSubscribed`, `applyEvent`, and `applyEventLocked` (including synchronous subscription replay) and verify it matches the turn's routed harness
- Reject a second distinct `approval-requested` event while a turn already has a `pendingApproval`
- Derive `createStateBackendStore` lock identity from the canonical shared storage (`.git` common dir across linked worktrees, resolved via `realpathSync.native`) and the state ref (`refs/heads/<branch>` or `refs/notes/<ref>`), unwrapping `TwoLayerBackend`
- Clear ambient `SQUAD_BACKEND_URL`, `SQUAD_BACKEND_TOKEN`, and `SQUAD_EXPERIMENTAL_BACKEND` before applying explicit test environment values in `McpStdioClient`
Refs #2239, #2253
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ff02830-3b62-4a0a-8752-bc54a0bc3639
…tract (Layer 3) Carves Layer 3 of #2239 targeting tamirdresher-microsoft-stack-2-casting-repair and applies all Layer 3 review fixes: - Export the experimental conversation & harness contract from `@bradygaster/squad-sdk/backend` only (`BACKEND_CONTRACT_STAGE = 'experimental'`) without re-exporting from the root `@bradygaster/squad-sdk` barrel - Gate `squad_backend_*` MCP tools in `squad state-mcp` behind explicit opt-in (`--experimental-backend`, `SQUAD_EXPERIMENTAL_BACKEND=1`, or `SQUAD_BACKEND_URL`) - Fix `createFileStateStore` lock contention `catch` block so persistent `EACCES`/`EPERM` errors respect `lockTimeoutMs` and sleep between polls instead of spinning forever, and reject symlinked `.squad` or store root directories via `lstatSync` - Track pending harness submissions in `LocalSquadBackend.sendMessage` so a failed or interrupted `harness.submit` is recovered on a same-`(runId, seq)` retry and returns the original admission cursor - Implement real cross-process `wx` file locking with stale-lock recovery and symlink guards in `createStateBackendStore` - Add per-attempt `requestTimeoutMs` (default 15_000 ms) via `AbortSignal.timeout` in `DurableHttpSquadBackend` Refs #2239 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 5ff02830-3b62-4a0a-8752-bc54a0bc3639
…rden durable-http, locks and MCP opt-in - H5: rewrite test/squad-20-security-contracts.test.ts to exercise product code (createFileStateStore/resolveInsideRoot guards, mergeMcpServer/installClients, LocalSquadBackend idempotency/epochs/approvals/cursors, backend-mcp, DurableHttp); delete self-referential reference guards; proposal no longer claims an "executable contract" or CI-blocking gates - H6: proposal, ADR and spec now describe the shipped behavior: opaque harness-supplied approval hash echoed back, cooperative client-asserted epochs (explicitly not a security boundary), unsigned cursors; stronger properties moved to "Future work / not implemented" - H7: replace spec TypeScript contracts with the real signatures from backend/types.ts (SquadBackend, HarnessAdapter, RunState, ...); golden-trace fixture uses real BackendEnvelope.type names and the e2e runner no longer needs a translation table - M10: squad_backend_send_message returns BackendError 'unavailable' when no harness is registered instead of queueing forever; health/negotiate expose harnesses: [] and executionAvailable: false; tool descriptions and specs state the bridge is transcript/event-log only - M11: git-ignore .squad/backend/ in the upgrade scaffold (no merge=union) - M12: SQUAD_BACKEND_URL alone no longer enables the tools; DurableHttp validates baseUrl (https, or http to loopback), uses redirect: 'error' - M13: health probes get AbortSignal.timeout; file-store lock acquisition is async (lockAsync) for backend methods; read() only treats ENOENT as missing; corrupt conversation JSON surfaces as BackendError with the path; temp file is fsync'd before rename; spec documents whole-file snapshot limitation - M14: createStateBackendStore requires a deterministic lock root (no mkdtemp fallback) and no longer prunes lock directories after release - Low: MCP arg validation (role/parts/protocols/limit cap 500), generic message for non-contract errors, Windows reserved device names rejected as conversation ids, misplaced comment fixed, real-binary tests gated behind SQUAD_REAL_CLI_TESTS=1, flaky opt-in test timeout raised Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 92888fd8-c715-4d1d-a60f-893a7a157841
…; keep configured state backend out of .squad/backend; fail-closed casting repair reports instead of crashing
- e2e expectation updates for the audited stack behavior: Codex agent is
`.codex/agents/squad.toml`; `.gitignore` projections are `# squad:claude`
plain lines (exact restoration on removal still asserted);
`normalizeHandwrittenCastingRegistry` callers use the new
`NormalizeHandwrittenResult` shape; repair archives originals under
`handwritten-archive-<sha256>/` instead of `.handwritten.bak`; a
manifest-only recommit bumps the revision by one and keeps agents/history;
team.md backfill resolves ids from charter folders (`belanna`, not
`b-elanna`) and `parseTeamRoster` members carry `charterId`; a roster with
no provable agent folder fails closed with the dropped row named
- role-keyed fixture now uses object entries (`tester: { name }`) since bare
strings are rejected by design; a dedicated case asserts the bare-string
rejection reason
- createStateBackendStore: a configured non-local state backend (git-notes,
orphan, or any backend without a `root`) no longer gets its lock root from
the team `.squad` dir. Locks live in a deterministic machine-local
directory derived from the backend identity (`cwd` or the new `lockAnchor`
option); `createMcpBackend` passes `lockAnchor` instead of `squadDir`, so
`.squad/backend/` is never created when a non-local backend is configured
- squad upgrade: when the handwritten-registry repair fails closed and the
pair is not readable as a managed pair, `ensureCastingDefaults` and the
pre-preflight repair in `runUpgrade` throw an actionable error (reason,
dropped entries, how to fix) instead of the generic "exactly one
authoritative file exists" / preflight error
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 92888fd8-c715-4d1d-a60f-893a7a157841
…ction code Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 92888fd8-c715-4d1d-a60f-893a7a157841
…y its generic error with the handwritten-repair reason The fail-closed handwritten-registry path pre-empted preflightCastingRegistryPair, so legacy`entries` registries (and --dry-run) got the new error instead of the legacy migration/refusal they had before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 92888fd8-c715-4d1d-a60f-893a7a157841
- Auto-select a single registered in-process harness in `squad_backend_send_message` when `harness` is omitted, and reject an omitted `harness` when multiple harnesses are registered instead of silently queueing; keep remote routing separate
- Default an omitted `limit` in `squad_backend_read_events` to `MAX_READ_EVENTS_LIMIT` (500)
- Read the response body outside the JSON parse `try/catch` in `DurableHttpSquadBackend.probe` so body timeouts and stream errors fail health checks
- Coordinate in-flight `adapter.submit` dispatches by `(conversationId, runId, seq)` and pass a deterministic `operationId` (`${conversationId}:${runId}:${seq}`) for crash-retry deduplication
- Remove queued approval deliveries for a turn when committing cancellation (`markCancelled` / terminal state) so `flushDeliveries` never retries or blocks on an obsolete approval
- Thread the publishing `harnessId` through `ensureSubscribed`, `applyEvent`, and `applyEventLocked` (including synchronous subscription replay) and verify it matches the turn's routed harness
- Reject a second distinct `approval-requested` event while a turn already has a `pendingApproval`
- Derive `createStateBackendStore` lock identity from the canonical shared storage (`.git` common dir across linked worktrees, resolved via `realpathSync.native`) and the state ref (`refs/heads/<branch>` or `refs/notes/<ref>`), unwrapping `TwoLayerBackend`
- Clear ambient `SQUAD_BACKEND_URL`, `SQUAD_BACKEND_TOKEN`, and `SQUAD_EXPERIMENTAL_BACKEND` before applying explicit test environment values in `McpStdioClient`
Refs #2239, #2253
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5ff02830-3b62-4a0a-8752-bc54a0bc3639
7b8d410 to
442bcea
Compare

Summary (Stack Layer 3 of 4 — splits #2239)
Adds the experimental
@bradygaster/squad-sdk/backendconversation/control plane (SquadBackend/IConversationBackend,HarnessAdapterSPI,LocalSquadBackend,DurableHttpSquadBackend,createStateBackendStore) and opt-insquad_backend_*MCP tools.Review fixes included in Layer 3
@bradygaster/squad-sdk/backendonly (not re-exported from the root SDK barrel) and annotated@experimental(BACKEND_CONTRACT_STAGE = 'experimental').squad_backend_*tools insquad state-mcpbehind explicit opt-in (--experimental-backend,SQUAD_EXPERIMENTAL_BACKEND=1, orSQUAD_BACKEND_URL).local-backend.tslock()so permission/read errors insidecatchalways enforcedeadline+sleepSync(10)(never spinning forever), added.squadrootlstatchecks, and added lost-submission retry recovery for turns stuck inrunning.createStateBackendStore(state-backend-store.ts) for non-local state backends (orphan/two-layer).AbortSignal.timeout(requestTimeoutMs)(default15_000ms) toDurableHttpSquadBackend.