Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/claude-agent-sdk-harness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@bradygaster/squad-sdk": minor
"@bradygaster/squad-cli": minor
---

Experimental `claude-agent-sdk` execution harness (`ClaudeAgentSdkHarness`, `@bradygaster/squad-sdk/backend`): runs conversation turns on the Claude Agent SDK, with broker approvals through `canUseTool` (hash-bound, one at a time, a deny ends the turn) and no repository or user allow rules in play. `@anthropic-ai/claude-agent-sdk` is an optional peer dependency; `squad state-mcp --experimental-backend` registers the harness only when it is installed (`SQUAD_BACKEND_HARNESSES=none` turns it off). Installs without it are unchanged. Also adds `copilot-sdk` (`CopilotSdkHarness`) on `@github/copilot-sdk`, opt-in on the bridge via `SQUAD_BACKEND_HARNESSES=copilot-sdk`, a shared approval queue (`TurnApprovals`, conversation-scoped; approval records hold a redacted summary and the hash, never the tool input) and a shared turn lifecycle (`TurnHarness`: atomic acceptance, cancellation that survives startup, the next turn waiting for an interrupt to finish). Harness state (accepted deliveries, session ids) survives restarts; Claude needs an API credential (never a claude.ai login) and keeps settings deny rules; Copilot correlates events to its own turn and recovers dropped sessions; turns run in the worktree-aware repository root.
20 changes: 15 additions & 5 deletions docs/specification/client-backend-decoupling-v0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
**Implementation status:** The contract in this draft is implemented as the
experimental `@bradygaster/squad-sdk/backend` subpath (`LocalSquadBackend`,
`DurableHttpSquadBackend`, `DefaultHarnessRegistry`) and bridged over MCP by
`squad state-mcp --experimental-backend`. No execution harness ships with it;
the MCP bridge records transcripts and events only.
`squad state-mcp --experimental-backend`. Two execution harnesses ship:
`claude-agent-sdk` (`ClaudeAgentSdkHarness`), registered by the MCP bridge by
default when the optional `@anthropic-ai/claude-agent-sdk` peer is installed,
and `copilot-sdk` (`CopilotSdkHarness`), registered only when listed in
`SQUAD_BACKEND_HARNESSES`. Without a registered harness the bridge records
transcripts and events only.

**Known deviations:** Approval hashes are opaque harness-supplied strings
(not broker-computed digests), lease epochs are client-asserted rather than
Expand Down Expand Up @@ -415,8 +419,9 @@ The same contract is projected through three backend families.
`LocalSquadBackend` runs the broker in process and stores each conversation as
one JSON snapshot under `.squad/backend/conversations/<id>.json`, serialized
across processes by `.squad/backend/locks/<id>.lock`. Harnesses are plugged in
through a `HarnessRegistry`; the SDK ships the registry and the harness
interface but no harness implementation.
through a `HarnessRegistry`; the SDK ships the registry, the harness
interface and two implementations, `claude-agent-sdk` (Claude Agent SDK) and
`copilot-sdk` (GitHub Copilot SDK).

### 10.2 MCP bridge

Expand Down Expand Up @@ -454,7 +459,12 @@ replayed to another origin.
- Lease epochs are client-asserted (see section 9).
- Cursors are unsigned `base64url("c1:<conversationId>:<sequence>")` values,
validated only for the conversation id and numeric form.
- No execution harness ships with the CLI or SDK.
- `claude-agent-sdk` needs the optional `@anthropic-ai/claude-agent-sdk` peer
and Anthropic API, Bedrock or Vertex credentials; `copilot-sdk` needs a
signed-in Copilot user and is opt-in on the MCP bridge.
- Both harnesses keep a pending approval in process memory, so a harness
restart loses it; the broker's record must then be cancelled and the session
resumed.

## 11. Protocol versioning and compatibility policy

Expand Down
74 changes: 70 additions & 4 deletions docs/specification/client-harness-adapters-v0.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,75 @@ registry lists harnesses during capability negotiation.
`decision: 'approve' | 'deny'`, `reason?`, `epoch?`); the `hash` is the opaque
string the harness itself supplied with its `approval-requested` event.

No `HarnessAdapter` implementation ships in the SDK or CLI today. The
registry, the SPI and the broker-side delivery guarantees exist and are tested
with in-memory harnesses; the `squad state-mcp` bridge runs without any
harness and refuses `squad_backend_send_message` accordingly.
Two `HarnessAdapter`s ship, in `packages/squad-sdk/src/backend/harnesses/`.
Both route approvals through one shared queue (`TurnApprovals`): requests are
hash-bound (sha256 of the canonical action), offered one at a time because the
broker holds one pending approval per turn, and a deny ends the turn.
Approvals are scoped to their conversation, since runtime tool-call ids are
not globally unique. The broker's approval record carries only a short,
redacted action summary and the hash: the tool input itself is never sent as
`payload`, so it stays out of backend state. The hash covers the full input.
Redaction masks credentials (named values, JSON keys, `Authorization`
schemes, credential flags, `-u user:password`, URL passwords, known token
formats) but never masks executable syntax: a value containing shell
expansion, substitution, chaining or redirection stays visible, so the
approver always sees what would run.

Both also share one turn lifecycle (`TurnHarness`). Concurrent deliveries of
an operation share one acceptance, and a failed start forgets it so the retry
runs. A run is registered before its runtime starts, so a cancel during
startup or session creation is kept and the turn never reaches the runtime.
`cancel` acknowledges once the stop is requested; the conversation's next turn
starts only after the interrupt has finished, so late events from an aborted
turn are never attributed to the next one. An interrupt that fails stays
retryable, and the harness stops reusing the runtime session it may still be
using. With a state directory (`stateDir`; `squad state-mcp` always passes one)
accepted deliveries and each conversation's runtime session id survive a
restart: a redelivered turn is not run again, and the next turn resumes the
session instead of starting without context.

`claude-agent-sdk` (`ClaudeAgentSdkHarness`) runs each admitted turn as a
Claude Agent SDK `query()`, resuming the conversation's session, and maps
`canUseTool` onto `approval-requested`: `approvalId` is the tool-use id, `hash`
is a sha256 of the canonical tool name and input, parallel tool calls are
offered one at a time (the broker holds one pending approval per turn), and a
deny ends the turn. It runs with `permissionMode: 'default'` and
`settingSources: []`, so no repository or user allow rule approves a call the
broker never sees; the `permissions.deny` rules from user, project and local
settings are carried over as `disallowedTools`, so they still apply. The real
SDK starts only with an API credential (`ANTHROPIC_API_KEY`,
`ANTHROPIC_AUTH_TOKEN`, or Bedrock, Vertex or Foundry settings), never a saved
claude.ai login. Claude Code still runs some read-only commands (such as
`echo`) without calling `canUseTool`, so those never reach the broker; anything
that writes does (live-verified 2026-11-10: a `touch` was offered, denied, and
not run). The SDK is an optional peer dependency; `squad state-mcp
--experimental-backend` registers the harness only when it is installed and
such a credential is configured (`SQUAD_BACKEND_HARNESSES=none` turns it off),
and otherwise refuses
`squad_backend_send_message`.

`copilot-sdk` (`CopilotSdkHarness`) keeps one Copilot session per conversation
(created on the first turn, resumed by id afterwards). A turn is `send()`
followed by `session.idle` (completed) or `session.error` (failed); only
main-agent `assistant.message` content enters the transcript. A turn reacts
only to events after its own `user.message` (matched to the id `send()`
returns), so an idle or answer from an earlier, aborted turn can't complete or
join it. A session whose send failed, whose interrupt failed or whose turn
timed out (`turnTimeoutMs`, default 30 minutes) is dropped and resumed fresh.
`onPermissionRequest` goes through the same queue, with `toolCallId` as the
`approvalId` (`#2`, `#3` … when one tool call asks for several permissions); approve answers `approve-once`, and deny answers `reject` and
aborts the turn. The harness never answers with a remembered approval, but the
Copilot runtime can keep "approve permanently" decisions (URL prompts) in its
config directory; the `configDirectory` option points sessions at a
harness-only directory so approvals made in the user's own Copilot CLI don't
apply; the MCP bridge always sets one. Because `@github/copilot-sdk` is always installed, the MCP
bridge registers this harness only when `SQUAD_BACKEND_HARNESSES` lists
`copilot-sdk`.

The shared contract suite (`test/harness-contract.ts`) runs the same
assertions against both harnesses (§15 item 1). Both run in the
worktree-aware repository root, so a linked worktree's turns edit that
worktree even when its squad state resolves to the main checkout.

`describe` and `projectWorkspace` on `ClientAdapter`
let the broker reason about host capabilities and file projection without
Expand Down Expand Up @@ -623,6 +688,7 @@ module and keep host projections in one sibling folder per host family.
| backend conversation contracts (experimental) | `packages/squad-sdk/src/backend/` (`@bradygaster/squad-sdk/backend`) |
| client and harness adapter SPI | `packages/squad-sdk/src/client/adapter-contract.ts` |
| host projection implementations | `packages/squad-sdk/src/client/adapters/` |
| execution harness implementations (experimental) | `packages/squad-sdk/src/backend/harnesses/` |
| public client exports | `packages/squad-sdk/src/client/index.ts` (`@bradygaster/squad-sdk/client`) |
| deprecated alias | `@bradygaster/squad-sdk/clients` via `src/clients/index.ts` |

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/concepts/squad-2-0-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The control plane lives in `@bradygaster/squad-sdk/backend` and owns conversatio
- **Versioned hash-bound approvals** (`v1` requested → `v2` decided) where `respondApproval` verifies the immutable payload `hash`;
- **Canonical health and error envelopes** (`BackendError`, `/health/live`, `/health/ready`).

In `squad state-mcp`, the 11 `squad_backend_*` MCP conversation tools are **opt-in**: pass `--experimental-backend` or set `SQUAD_EXPERIMENTAL_BACKEND=1`. `SQUAD_BACKEND_URL` alone does not enable them. The bridge ships no execution harness: `squad_backend_health` / `squad_backend_negotiate` report `executionAvailable: false`, and `squad_backend_send_message` refuses instead of queueing (`unavailable` when no harness is registered; auto-selects a single registered in-process harness when `harness` is omitted, or refuses with `invalid-request` when multiple harnesses are registered), so over MCP the bundled backend is a transcript and event-log surface.
In `squad state-mcp`, the 11 `squad_backend_*` MCP conversation tools are **opt-in**: pass `--experimental-backend` or set `SQUAD_EXPERIMENTAL_BACKEND=1`. `SQUAD_BACKEND_URL` alone does not enable them. The bridge registers the `claude-agent-sdk` harness when the optional `@anthropic-ai/claude-agent-sdk` peer is installed, and the `copilot-sdk` harness only when `SQUAD_BACKEND_HARNESSES` lists it (`SQUAD_BACKEND_HARNESSES=none` turns both off); without a harness `squad_backend_health` / `squad_backend_negotiate` report `executionAvailable: false`, and `squad_backend_send_message` refuses instead of queueing (`unavailable` when no harness is registered; auto-selects a single registered in-process harness when `harness` is omitted, or refuses with `invalid-request` when multiple harnesses are registered), so over MCP the bundled backend is a transcript and event-log surface.

### Plane 3: Execution Plane (`HarnessAdapter` SPI)

Expand Down
Loading
Loading