agent-tempo 1.7.0-beta.7 → 1.7.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CLAUDE.md +11 -15
  2. package/README.md +10 -1
  3. package/dashboard/package.json +1 -1
  4. package/dist/activities/outbox.d.ts +2 -24
  5. package/dist/activities/outbox.js +7 -32
  6. package/dist/adapters/pi/adapter.js +0 -18
  7. package/dist/cli.js +1 -1
  8. package/dist/config.d.ts +0 -20
  9. package/dist/config.js +0 -20
  10. package/dist/daemon.js +1 -35
  11. package/dist/http/auth.d.ts +1 -1
  12. package/dist/http/auth.js +2 -2
  13. package/dist/http/inner-loop-routes.d.ts +0 -24
  14. package/dist/http/inner-loop-routes.js +0 -35
  15. package/dist/http/server.d.ts +0 -23
  16. package/dist/http/server.js +7 -54
  17. package/dist/pi/cue-pump.d.ts +81 -8
  18. package/dist/pi/cue-pump.js +109 -4
  19. package/dist/pi/extension.d.ts +4 -21
  20. package/dist/pi/extension.js +16 -110
  21. package/dist/pi/headless.d.ts +28 -32
  22. package/dist/pi/headless.js +14 -87
  23. package/dist/pi/inner-loop-client.d.ts +0 -9
  24. package/dist/pi/inner-loop-client.js +1 -16
  25. package/dist/pi/inner-loop-publisher.d.ts +1 -33
  26. package/dist/pi/install.d.ts +3 -3
  27. package/dist/pi/install.js +3 -3
  28. package/dist/pi/mission-control/actions.d.ts +1 -5
  29. package/dist/pi/mission-control/actions.js +4 -17
  30. package/dist/pi/mission-control/extension.d.ts +0 -2
  31. package/dist/pi/mission-control/extension.js +1 -25
  32. package/dist/pi/mission-control/render.js +0 -5
  33. package/dist/pi/pi-types.d.ts +4 -3
  34. package/dist/spawn.d.ts +4 -14
  35. package/dist/spawn.js +3 -11
  36. package/dist/tools/recruit.js +0 -38
  37. package/dist/types.d.ts +0 -90
  38. package/dist/worker.d.ts +1 -2
  39. package/dist/worker.js +2 -2
  40. package/dist/workflows/session.js +1 -15
  41. package/dist/workflows/signals.d.ts +1 -3
  42. package/package.json +1 -1
  43. package/workflow-bundle.js +2 -16
  44. package/dist/http/gate-audit.d.ts +0 -12
  45. package/dist/http/gate-audit.js +0 -95
  46. package/dist/http/gate-registry.d.ts +0 -279
  47. package/dist/http/gate-registry.js +0 -253
  48. package/dist/http/gate-routes.d.ts +0 -48
  49. package/dist/http/gate-routes.js +0 -102
  50. package/dist/pi/gate-client.d.ts +0 -71
  51. package/dist/pi/gate-client.js +0 -177
  52. package/dist/pi/reset-pump.d.ts +0 -85
  53. package/dist/pi/reset-pump.js +0 -135
  54. package/dist/security/tool-capability.d.ts +0 -60
  55. package/dist/security/tool-capability.js +0 -157
package/CLAUDE.md CHANGED
@@ -21,10 +21,12 @@ src/
21
21
  ├── daemon.ts # Daemon entry point — runs Temporal workers as a detached background process
22
22
  ├── cli/
23
23
  │ ├── commands.ts # CLI command implementations (up, start, conduct, status, stop, …)
24
+ │ ├── command-center-command.ts # command-center subcommand — crash-proof; launches Pi mission-control board; sets AGENT_TEMPO_MISSION_CONTROL=1 (#729)
24
25
  │ ├── config-command.ts # config subcommand (interactive + set/show) — crash-proof for show/set
25
26
  │ ├── daemon.ts # Daemon management utilities (start, stop, status, heartbeat, isDaemonRunning)
26
27
  │ ├── daemon-command.ts # daemon subcommand handler — crash-proof, no Temporal deps
27
28
  │ ├── dashboard-command.ts # dashboard subcommand — crash-proof; opens the web dashboard, optionally minting a QR-code pairing token (#340)
29
+ │ ├── ensure-infra.ts # shared infra bootstrap (`ensureInfra()`) — brings up Temporal, SAs, daemon; shared by `up` and the Pi extension `/ensemble-up` (#700 P1)
28
30
  │ ├── dev-banner.ts # [DEV MODE] banner formatter (ADR 0014 §5.4) — gate 4 production-safety line
29
31
  │ ├── dev-mode-bootstrap.ts # pre-import side-effect: promotes top-level `--dev` flag to `CLAUDE_TEMPO_DEV_MODE=1` before any other module loads
30
32
  │ ├── dev-verbs.ts # dev-mode scriptable CLI verbs (#432) — shell-scriptable wrappers over MCP tools for E2E validation; stripped from production surface
@@ -35,6 +37,7 @@ src/
35
37
  │ ├── output.ts # Shared CLI output formatting helpers
36
38
  │ ├── preflight.ts # Environment preflight checks
37
39
  │ ├── removed-verbs.ts # lookup table for the 10 CLI verbs removed in #288 — dispatches migration hints before loading Temporal surface
40
+ │ ├── resolve-ensemble.ts # canonical ensemble-name resolver — `--ensemble` flag > positional > env > 'default' (#685)
38
41
  │ ├── sa-preflight.ts # search-attribute preflight — REQUIRED_SEARCH_ATTRIBUTES list (single source of truth), registerSearchAttribute, verifySearchAttributes, assertSearchAttributesOrExit
39
42
  │ ├── scenarios-command.ts # scenarios subcommand (dev mode only) — list/show shipped YAML scenario library (ADR 0014 §4.8)
40
43
  │ ├── startup.ts # auto-provisioning bootstrap state machine (#289) — six-step idempotent sequence used by bare `agent-tempo` invocation
@@ -82,9 +85,6 @@ src/
82
85
  │ ├── inner-loop.ts # InnerLoopRegistry + InnerSubscription — daemon-local fine-tail sink (3c MD-F); drop-oldest bounded queue (256) + `compacted{dropped,sinceTs}` marker; NOT on Temporal/bus, ephemeral no-replay
83
86
  │ ├── ingest-registry.ts # IngestTokenRegistry — per-player ingest token (mint-on-pi-spawn / revoke-on-destroy / revokeAll-on-shutdown); timing-safe validation; cross-player-spoof guard
84
87
  │ ├── inner-loop-routes.ts # 3 inner-loop HTTP routes: POST /inner/ingest + GET /inner/presence (INGRESS, loopback + X-Ingest-Token, uniform 403); GET /inner (EGRESS operator SSE, requireTier(3))
85
- │ ├── gate-registry.ts # GateRegistry (3d MD-G) — per-player armed-gate + pending-request store; 45s lazy auto-allow (R3 locked); arm/disarm/decide/getResolution; injected auditSink + publishToInner callback
86
- │ ├── gate-routes.ts # Gate HTTP routes: POST /gate-arm + /gate-disarm + /gate/:requestId (OPERATOR, Tier 3); GET /gate/:requestId/resolution (SOURCE, loopback + X-Ingest-Token); uniform 403 no-leak
87
- │ ├── gate-audit.ts # createGateAuditSink — append-only JSONL at ~/.agent-tempo/gate-audit/<ensemble>/<workflowId>.jsonl; sync write (R5 durable-before-return); whitelisted path segments; swallows I/O errors
88
88
  │ ├── auth.ts # 3e MD-E RBAC: two-token model (readToken T1 / adminToken T1+T2+T3 env-var-only); loadRbacTokens; requireTier(tier, input) → TierGuardResult; tierForToken; loadReadToken (env>config>legacy httpToken>auto-gen); loadAdminToken (env-only); TLS/legacy startup warnings in startHttpServer
89
89
  │ ├── cors.ts / responses.ts / event-id.ts / port-file.ts / index.ts
90
90
  ├── reconcile/
@@ -108,21 +108,18 @@ src/
108
108
  │ └── descriptor.ts # Transport-neutral tool descriptor (TempoToolDescriptor) + renderToMcp; per-tool `build*Tool` factories live in each tool file (MD-B, Phase 1)
109
109
  ├── pi/ # Pi-native integration — a Pi session as a first-class player over the Temporal core
110
110
  │ ├── extension.ts # `export default function(pi)` — interactive runtime entry. Holds the MODULE-SCOPE singleton `Map<workflowId, PiPlayerRuntime>` that survives Pi's per-switch instance rebuild (rebind, not re-claim); full tool surface via renderToPi; Option-C reason-discriminated teardown
111
- │ ├── phase-driver.ts / workflow-client.ts / cue-pump.ts # Pi-event→attachment-phase machine, thin client-side WorkflowClient (lease/heartbeat 90/30, handle getter), cue pump (D10 steer/followUp)
111
+ │ ├── phase-driver.ts / workflow-client.ts / cue-pump.ts # Pi-event→attachment-phase machine, thin client-side WorkflowClient (lease/heartbeat 90/30, handle getter), cue pump (D10 steer/followUp; one 1s loop with a second pendingReset intake — S3 merge, D14 clean-wipe + /tempo-reset operator notice)
112
112
  │ ├── lazy-proxy.ts # D11 createLazyProxy — Client/WorkflowHandle proxy resolving the live module-scope target per call (survives instance rebuild)
113
113
  │ ├── headless.ts # Headless Pi runtime (Phase 3a) — boots Pi's createAgentSession with inline extension; `noExtensions: true` closes S2 exec-tool bypass; SIGTERM/SIGINT shutdown → reliable detach + dispose
114
114
  │ ├── render-tools.ts # renderToPi — registers the shared tool descriptors on Pi's ExtensionAPI (TypeBox params via the converter)
115
115
  │ ├── zod-to-typebox.ts # zod→TypeBox tool-schema converter (fail-loud on unsupported constructs; Phase 1 / D1)
116
116
  │ ├── inner-loop-publisher.ts # InnerLoopPublisher (3c MD-F) — single Pi-source observer; Tier-1 coarse via heartbeat piggyback (currentTool + context pressure), Tier-2 fine presence-gated; source coalescing (100ms/2KB) + 2KB truncation
117
- │ ├── inner-loop-client.ts # InnerLoopHttpClient — production InnerLoopRegistry impl: thin loopback-HTTP calls (publish→POST ingest, subscriberCount→cached presence GET); no-ops without AGENT_TEMPO_INGEST_TOKEN; 3d: also caches gateArmed from presence response
118
- │ ├── gate-client.ts # GateClient (3d MD-G) — Pi-subprocess poll client for operator gate resolution; awaitDecision(requestId) polls GET /gate/:id/resolution until resolved or timeout; fail-open (allow on timeout/error/auto-allow)
119
- │ ├── reset-pump.ts # ResetPump (3d D14) — polls pendingReset query every 1s; on result calls session.newSession() (clean-wipe); sends system notice; acks via ackReset signal
120
- │ ├── tool-capability.ts # classify(toolName) → ToolCapability ('exec' | 'high-blast' | 'low-risk'); EXEC_TOOLS denylist (F1 locked: bash/shell/exec/sh/powershell/pwsh/cmd/…); unknown → 'high-blast' (fail-safe)
117
+ │ ├── inner-loop-client.ts # InnerLoopHttpClient — production InnerLoopRegistry impl: thin loopback-HTTP calls (publish→POST ingest, subscriberCount→cached presence GET); no-ops without AGENT_TEMPO_INGEST_TOKEN
121
118
  │ ├── mission-control/ # 3f — observer-only Pi extension that turns one interactive Pi TUI into an ensemble mission-control board + operator controller. HTTP-driven (NOT MCP tools). Never claims attachment or registers as a player.
122
- │ │ ├── extension.ts # Controller (testable command handlers) + Pi extension lifecycle: session_start opens coarse SSE + ~200ms render tick + registers /players /tail /cue /pause /play /restart /destroy /reset /arm /gate; session_shutdown tears down SSE + widget
119
+ │ │ ├── extension.ts # Controller (testable command handlers) + Pi extension lifecycle: session_start opens coarse SSE + ~200ms render tick + registers /players /tail /cue /pause /play /restart /destroy /reset; session_shutdown tears down SSE + widget
123
120
  │ │ ├── board.ts # Pure BoardModel + reducers: applyTempoEvent (coarse /events SSE) + applyInnerFrame (selected-player inner tail); revision counter drives render throttle
124
- │ │ ├── render.ts # Pure BoardModel → string[] renderer; phase glyphs, part, currentTool, context%, selected-player tail (12 lines), inner.gate_pending/gate_resolved frames
125
- │ │ ├── actions.ts # Daemon HTTP write-surface client (T2 + T3 bearer): cue/pause/play/restart/destroy + gate arm/disarm/decide; reads AGENT_TEMPO_HTTP_ADMIN_TOKEN
121
+ │ │ ├── render.ts # Pure BoardModel → string[] renderer; phase glyphs, part, currentTool, context%, selected-player tail (12 lines)
122
+ │ │ ├── actions.ts # Daemon HTTP write-surface client (T2 + T3 bearer): cue/pause/play/restart/destroy; reads AGENT_TEMPO_HTTP_ADMIN_TOKEN
126
123
  │ │ ├── inner-tail.ts # Operator-egress /inner SSE consumer (T3 bearer): pure parseInnerSse + injectable stream pump; cross-host note: baseUrl is the preferredHost seam
127
124
  │ │ ├── pi-ui.ts # Local structural slice of Pi's ctx.ui / registerCommand / registerShortcut API (self-contained; tsc green without optional Pi dep)
128
125
  │ │ └── index.ts # Barrel — re-exports Controller, BoardModel, renderBoard, MissionControlActions, and public types
@@ -211,17 +208,16 @@ daemon worker notes, `npx ts-node` dev runner).
211
208
  - **Claude API adapter** (`agent: 'claude-api'`, #131): Headless adapter that drives sessions via the Anthropic Messages API (`@anthropic-ai/sdk`) — no terminal, no Claude Code CLI. Requires `ANTHROPIC_API_KEY` env var and the `@anthropic-ai/sdk` optional dependency. Default model `claude-opus-4-7` (overridable via `model` recruit arg or `CLAUDE_TEMPO_API_MODEL` env). Claude-API players have access to agent-tempo MCP tools (cue, report, recall, ensemble, …) but not file-edit/shell/web tools. See `src/adapters/claude-api/`.
212
209
  - **OpenCode adapter** (`agent: 'opencode'`, #449): Headless multi-provider adapter that drives sessions via [SST OpenCode](https://opencode.ai) as a managed subprocess — supports Anthropic, OpenAI, Bedrock, Vertex, Ollama, and ~70 other providers via OpenCode's `provider/model` selector. Requires OpenCode CLI (`npm install -g opencode-ai`) and the `@opencode-ai/sdk` optional dependency. Recruit with `model: 'provider/name'` (e.g. `'anthropic/claude-opus-4-7'`). Tool bridging is MCP-native — OpenCode spawns `dist/server.js` as its own stdio MCP child. Session state is persisted server-side by OpenCode; the adapter stashes the session id on workflow metadata for reconnect across `opencode serve` restarts. See `src/adapters/opencode/`.
213
210
  - **Claude Code headless adapter** (`agent: 'claude-code-headless'`, #520): Headless adapter that drives sessions via the official `claude` CLI as a per-turn `claude -p --output-format stream-json` subprocess. The whole point: turns bill against the host's existing Claude Code subscription extra-usage credits (Pro / Max plans) rather than a Console workspace API key — the only ToS-clean way for a third-party tool to tap that pool. Requires the `claude` binary on PATH AND a logged-in Claude Code session (`claude auth login`); recruit pre-flight rejects with an actionable error otherwise. Tool surface is the union of full Claude Code built-ins (Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch) and the agent-tempo MCP surface — registered via inline `--mcp-config` so `claude` spawns `dist/server.js` as its own MCP child (no in-process bridge). Recruit knobs: `permissionMode` (default `'acceptEdits'`) or `dangerouslySkipPermissions: true` (mutually exclusive). Sessions resume across restart via the existing `sessionId` metadata field — the same UUID is shared with the interactive `claude-code` adapter (per-cwd JSONL is per-cwd, not per-adapter). See `src/adapters/claude-code-headless/` and `examples/ensembles/tempo-headless-jam.yaml`.
214
- - **Pi adapter** (`agent: 'pi'`, #632 / #666): Two modes. **(1) Interactive conductor** (#666): `agent-tempo up --agent pi --ensemble <name>` launches `pi` in a real terminal with the agent-tempo extension auto-loaded (`pi -e dist/pi/extension.js`); the Pi session self-bootstraps its Temporal workflow and attaches as a conductor/player — no separate recruiter step. From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor. Requires `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model). The `AGENT_TEMPO_*` env is auto-wired by `up`; power users can invoke the extension directly with `pi -e dist/pi/extension.js`. `--model provider/model` selector (e.g. `'github-copilot/gpt-4o'`) is a fast-follow. **(2) Headless player** (Phase 3a): `recruit` with `agent: 'pi'` — no terminal, no BaseAttachment; runs `createAgentSession` with an in-memory `SessionManager`; the module-scope singleton owns claim/heartbeat/tools/cue pump (MD-D). MD-C tool-access policy: `toolAccess: 'restricted'` (defaultBash/shell/exec HARD-BLOCKED) | `'standard'` (scoped Bash) | `'full'` (unsandboxed; requires `force: true`). `noExtensions: true` closes the S2 exec-tool-bypass gap. See `src/adapters/pi/` and `src/pi/headless.ts`.
215
- - **Mission-control / Command-center** (3f, #700 P2): An interactive Pi TUI that is both a live ensemble board + operator controller and an LLM planner. Board side: HTTP-drives the daemon — coarse ensemble view via `/v1/events/:ensemble` SSE + fine per-player tail via `/inner` (T3); operator controls (cue/pause/play/restart/destroy + gate arm/disarm/decide) POST to the daemon write surface using `AGENT_TEMPO_HTTP_ADMIN_TOKEN`. Planner side: registers LLM tools (`ask` / `handoff` / `cue` / `recruit` / `observe_board`) via `MissionControlActions` — HTTP-backed, distinct from the player extension's `renderToPi` MCP surface. **Never claims attachment or registers as a player** — invisible to the ensemble. Launch with **`agent-tempo command-center [ensemble]`** (aliases: `cc`, `board`) — sets `AGENT_TEMPO_MISSION_CONTROL=1`. **Role-gated and mutually exclusive** with the player extension (`resolvePiRole` discriminator: explicit `AGENT_TEMPO_PI_ROLE` → `PLAYER_NAME` present → `AGENT_TEMPO_MISSION_CONTROL` → `none`): a bare `pi` keeps both extensions dormant (plain coding session); a player session (`up --agent pi` / `recruit`) keeps the board dormant. Power users invoking `pi -e dist/pi/extension.js` directly without `PLAYER_NAME` also resolve to `none` (dormant) — set `AGENT_TEMPO_PI_ROLE=player` to force player mode. See `src/pi/mission-control/`.
211
+ - **Pi adapter** (`agent: 'pi'`, #632 / #666): Two modes. **(1) Interactive conductor** (#666): `agent-tempo up --agent pi --ensemble <name>` launches `pi` in a real terminal with the agent-tempo extension auto-loaded (`pi -e dist/pi/extension.js`); the Pi session self-bootstraps its Temporal workflow and attaches as a conductor/player — no separate recruiter step. From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor. Requires `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model). The `AGENT_TEMPO_*` env is auto-wired by `up`; power users can invoke the extension directly with `pi -e dist/pi/extension.js`. `--model provider/model` selector (e.g. `'github-copilot/gpt-4o'`) is a fast-follow. **(2) Headless player** (Phase 3a): `recruit` with `agent: 'pi'` — no terminal, no BaseAttachment; runs `createAgentSession` with an in-memory `SessionManager`; the module-scope singleton owns claim/heartbeat/tools/cue pump (MD-D). Pi players run the full tool surface incl. shell with no permission layer, like the other adapters (the former `toolAccess`/`guardrailPolicy` knobs were removed). `noExtensions: true` stays: it blocks third-party disk/package extensions from loading into a recruited player (supply-chain hygiene). See `src/adapters/pi/` and `src/pi/headless.ts`.
212
+ - **Mission-control / Command-center** (3f, #700 P2): An interactive Pi TUI that is both a live ensemble board + operator controller and an LLM planner. Board side: HTTP-drives the daemon — coarse ensemble view via `/v1/events/:ensemble` SSE + fine per-player tail via `/inner` (T3); operator controls (cue/pause/play/restart/destroy) POST to the daemon write surface using `AGENT_TEMPO_HTTP_ADMIN_TOKEN`. Planner side: registers LLM tools (`ask` / `handoff` / `cue` / `recruit` / `observe_board`) via `MissionControlActions` — HTTP-backed, distinct from the player extension's `renderToPi` MCP surface. **Never claims attachment or registers as a player** — invisible to the ensemble. Launch with **`agent-tempo command-center [ensemble]`** (aliases: `cc`, `board`) — sets `AGENT_TEMPO_MISSION_CONTROL=1`. **Loopback auto-token** (#736): a local (loopback) daemon grants full trust without `AGENT_TEMPO_HTTP_ADMIN_TOKEN`; only a remote daemon requires explicit token configuration. **Role-gated and mutually exclusive** with the player extension (`resolvePiRole` discriminator: explicit `AGENT_TEMPO_PI_ROLE` → `PLAYER_NAME` present → `AGENT_TEMPO_MISSION_CONTROL` → `none`): a bare `pi` keeps both extensions dormant (plain coding session); a player session (`up --agent pi` / `recruit`) keeps the board dormant. Power users invoking `pi -e dist/pi/extension.js` directly without `PLAYER_NAME` also resolve to `none` (dormant) — set `AGENT_TEMPO_PI_ROLE=player` to force player mode. See `src/pi/mission-control/`.
216
213
  - **Command-center planner** (#700 P2): An inbox-less interactive Pi session (the operator's planning seat) that routes questions to players via correlated `cue` tags (`[Q <questionId>]`). Players answer with the `respond` MCP tool, which parks the answer on the per-ensemble maestro Q&A mailbox (TTL 1h, 20-slot cap). The planner is woken by an `answer` SSE event when the answer lands (`docs/SSE-PROTOCOL.md` §6). `/handoff` cues hand active work to a conductor (a registered player with a Temporal inbox). See `docs/concepts.md` for the Q&A mechanics.
217
- - **Guardrail policy** (`guardrailPolicy` recruit arg, #700 P2): Per-player posture for the operator gate on headless Pi players. Four values — `'autonomous'` (default; no gate), `'monitored'` (gate engaged; fail-open: auto-allow after 45 s), `'supervised'` (gate engaged; fail-closed: auto-deny after 300 s; **client-cooperative in P2, not tamper-proof**; daemon-side enforcement is P2.1 #44), `'observe-only'` (non-`low-risk` tools hard-blocked outright). Persisted on `SessionMetadata`; re-sourced at each `(re)attach` from `getMetadata`. See `docs/concepts.md` for operator gate mechanics.
218
214
  - **Mock adapter** (`agent: 'mock'`, dev mode only): Four modes: `echo` (echoes input), `scripted` (replays YAML scenario rules), `silent` (drains messages without replying — heartbeat-stale validation), `chaos` (probabilistic fail/crash injection via seeded PRNG). Only registered when `isDevMode()` is true; stripped from the npm tarball by `prepack`. See `src/adapters/mock/`.
219
215
  - **Saveable state** (#334, ADR 0011): Per-player curated state slots — the player itself decides what context survives a restart. Three MCP tools: `save_state` (owner-only write, max 4 slots × 32 KiB), `fetch_state` (read self or peer; audit identity recorded on each entry's `savedBy`), `clear_state` (owner-only). `restart` accepts `loadFromState: true | 'someKey'` to seed the new session from a saved-state slot instead of (or, with `transcript: 'replay'`, alongside) transcript replay. Saved-state delivery uses `from: 'self-restart'` as a stable system identity. Empty-slot fallback: graceful — falls through to transcript replay with a log line. See [docs/design/334-player-saveable-state.md](docs/design/334-player-saveable-state.md).
220
216
  - **Coat-check** (#318, ADR 0008): Per-ensemble transient content store on Maestro state. Solves the 100 KB cue body cap — stash a large artifact with `coat_check_put` (returns a ticket id) and attach the ticket to a `cue` via `attachmentTicket`; the recipient calls `coat_check_get` to pull the full body. Four MCP tools: `coat_check_put` (any player; max 32 KiB per entry, 20 slots per ensemble, TTL 7d default), `coat_check_get` (any player; bumps fetch-audit counters), `coat_check_list` (read-only survey; headers only, content omitted), `coat_check_evict` (owner or conductor). Saturation rejects with `CoatCheckSlotsFull` (no LRU eviction). See `src/tools/coat-check-*.ts` and [docs/adr/0008-coat-check-pattern.md](docs/adr/0008-coat-check-pattern.md).
221
217
  - **Lineup examples**: Six pre-built ensemble YAML files in `examples/ensembles/` — `tempo-big-band`, `tempo-dev-team`, `tempo-review-squad`, `tempo-jam-session`, `tempo-mock-jam` (dev-mode all-mock ensemble), `tempo-headless-jam` (#520 — all-`claude-code-headless` subscription-billed ensemble). Load with `agent-tempo up --lineup <name>` or the `load_lineup` tool.
222
218
  - **GitHub App identity** (`agent-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `agent-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). Every bot-authored comment/PR body must include the AI attribution footer documented in [docs/github-app.md](docs/github-app.md).
223
219
 
224
- See [docs/concepts.md](docs/concepts.md) for the full glossary (Adapter, Attachment phases, Restart, Detach/Destroy, Migrate, Broadcast, Recall, Schedule, Lineup, Quality Gate, Worktree, Stage, Hold/Release, Pause/Resume, Maestro, TempoClient, Guardrail policy, Command-center planner, and more).
220
+ See [docs/concepts.md](docs/concepts.md) for the full glossary (Adapter, Attachment phases, Restart, Detach/Destroy, Migrate, Broadcast, Recall, Schedule, Lineup, Quality Gate, Worktree, Stage, Hold/Release, Pause/Resume, Maestro, TempoClient, Command-center planner, and more).
225
221
 
226
222
  ## Commit Convention
227
223
 
package/README.md CHANGED
@@ -277,7 +277,16 @@ From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor
277
277
 
278
278
  **Prerequisites:** `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model).
279
279
 
280
- **Headless Pi players** — recruit as a background agent slot using `agent: 'pi'`. Pass `guardrailPolicy: 'monitored' | 'supervised' | 'observe-only'` to control the operator gate (default `'autonomous'` no gate). See [docs/concepts.md](docs/concepts.md#command-center-and-player-supervision) for the gate mechanics and the supervised caveat.
280
+ **Headless Pi players** — recruit as a background agent slot using `agent: 'pi'`. Pi players run the full tool surface including shell — without an approval layer, exactly like the other adapters. Observe them live via the mission-control board (coarse SSE + fine `/inner` tail); control via `cue`/`pause`/`restart`/`destroy`/`reset`. See [docs/concepts.md](docs/concepts.md#command-center-and-player-supervision).
281
+
282
+ **Mission-control board** — operator-only view that observes the ensemble and sends operator actions (cue, pause, restart, destroy) without joining as a player:
283
+
284
+ ```bash
285
+ agent-tempo install-pi # install Pi extensions (once per machine)
286
+ agent-tempo command-center # launch the board (aliases: cc, board)
287
+ ```
288
+
289
+ The admin token is injected automatically for loopback daemons — no manual token setup required locally (#736).
281
290
 
282
291
  📖 [Pi integration reference → docs/design/pi-hardening-h1-h2-h3.md](docs/design/pi-hardening-h1-h2-h3.md)
283
292
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tempo-dashboard",
3
3
  "private": true,
4
- "version": "1.7.0-beta.7",
4
+ "version": "1.7.0-beta.8",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -1,9 +1,8 @@
1
1
  import { Client } from '@temporalio/client';
2
2
  import { Config } from '../config';
3
- import { AgentType, AttachmentPhase, MockMode, DetachReason, GuardrailPolicy } from '../types';
3
+ import { AgentType, AttachmentPhase, MockMode, DetachReason } from '../types';
4
4
  import type { ClaudeCodeHeadlessPermissionMode } from '../adapters/claude-code-headless/types';
5
5
  import type { IngestTokenRegistry } from '../http/ingest-registry';
6
- import type { GateRegistry } from '../http/gate-registry';
7
6
  import { type HardTerminateInput, type HardTerminateResult } from './hard-terminate';
8
7
  export interface DeliverCueInput {
9
8
  ensemble: string;
@@ -60,12 +59,6 @@ export interface StartRecruitedSessionInput {
60
59
  * can recover the original choice. Ignored when `agent !== 'claude-api'`.
61
60
  */
62
61
  model?: string;
63
- /**
64
- * #700 (P2 / G) — guardrail posture. Persisted onto
65
- * `SessionMetadata.guardrailPolicy` so restart recovers it. Ignored when
66
- * `agent !== 'pi'`. Absent ⇒ autonomous.
67
- */
68
- guardrailPolicy?: GuardrailPolicy;
69
62
  }
70
63
  export interface ReleasePlayerInput {
71
64
  ensemble: string;
@@ -174,21 +167,6 @@ export interface SpawnProcessInput {
174
167
  * exclusive with {@link permissionMode}.
175
168
  */
176
169
  dangerouslySkipPermissions?: boolean;
177
- /**
178
- * Phase 3a / MD-C — headless Pi tool-class policy. Forwarded as
179
- * `AGENT_TEMPO_TOOL_ACCESS`. Only meaningful when `agent === 'pi'`. One of
180
- * `'restricted'` (default; Bash hard-blocked) | `'standard'` | `'full'`.
181
- * Inline literal — kept off the workflow-sandbox import path (see the
182
- * RecruitOutboxEntry note in src/types.ts).
183
- */
184
- toolAccess?: 'restricted' | 'standard' | 'full';
185
- /**
186
- * #700 (P2 / G) — headless Pi guardrail posture. Forwarded as
187
- * `AGENT_TEMPO_GUARDRAIL_POLICY`. Sourced from durable
188
- * `SessionMetadata.guardrailPolicy` on (re)spawn. Only meaningful when
189
- * `agent === 'pi'`. Absent ⇒ autonomous (the extension default).
190
- */
191
- guardrailPolicy?: GuardrailPolicy;
192
170
  }
193
171
  export interface OutboxActivityResult {
194
172
  success: boolean;
@@ -240,4 +218,4 @@ export interface OutboxActivities {
240
218
  * claim — phase is legitimately live) → never skipped. Pure + exported for tests.
241
219
  */
242
220
  export declare function shouldSkipDuplicateSpawn(attachmentId: string | undefined, phase: AttachmentPhase): boolean;
243
- export declare function createOutboxActivities(client: Client, config: Config, ingestTokens?: IngestTokenRegistry, gate?: GateRegistry): OutboxActivities;
221
+ export declare function createOutboxActivities(client: Client, config: Config, ingestTokens?: IngestTokenRegistry): OutboxActivities;
@@ -156,7 +156,7 @@ function shouldSkipDuplicateSpawn(attachmentId, phase) {
156
156
  return false; // restart/migrate handoff — must spawn
157
157
  return phase === 'attached' || phase === 'processing' || phase === 'awaiting';
158
158
  }
159
- function createOutboxActivities(client, config, ingestTokens, gate) {
159
+ function createOutboxActivities(client, config, ingestTokens) {
160
160
  return {
161
161
  async deliverCue(input) {
162
162
  const { ensemble, fromPlayerId, targetPlayerId, message, broadcastId, attachmentTicket } = input;
@@ -233,7 +233,7 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
233
233
  }
234
234
  },
235
235
  async startRecruitedSession(input) {
236
- const { ensemble, targetName, workDir, isConductor, initialMessage, fromPlayerId, agent, systemPrompt, taskQueue, agentDefinition, agentDefinitionDescription, held, model, guardrailPolicy } = input;
236
+ const { ensemble, targetName, workDir, isConductor, initialMessage, fromPlayerId, agent, systemPrompt, taskQueue, agentDefinition, agentDefinitionDescription, held, model } = input;
237
237
  try {
238
238
  const workflowId = isConductor
239
239
  ? (0, config_1.conductorWorkflowId)(ensemble)
@@ -267,11 +267,6 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
267
267
  // (different value shapes — bare vs `provider/model`) — the spawn
268
268
  // path inspects `metadata.agentType` to know which env var to set.
269
269
  ...((agent === 'claude-api' || agent === 'opencode' || agent === 'pi') && model ? { model } : {}),
270
- // #700 (P2 / G) — persist the guardrail posture on DURABLE metadata so
271
- // arm-at-boot reads the real posture on every (re)attach; a restart
272
- // can't silently downgrade a supervised player. Only when explicitly
273
- // set (absent ⇒ autonomous). Pi-only for P2.
274
- ...(agent === 'pi' && guardrailPolicy ? { guardrailPolicy } : {}),
275
270
  ...(agentDefinition ? { playerType: agentDefinition } : {}),
276
271
  ...(agentDefinitionDescription ? { playerTypeDescription: agentDefinitionDescription } : {}),
277
272
  recruitedBy: fromPlayerId,
@@ -329,7 +324,7 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
329
324
  }
330
325
  },
331
326
  async spawnProcess(input) {
332
- const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, sessionId, allowedTools, claudeBin, attachmentId, attachmentRunId, adapterId, mockMode, mockScenario, model, permissionMode, dangerouslySkipPermissions, toolAccess, guardrailPolicy } = input;
327
+ const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, sessionId, allowedTools, claudeBin, attachmentId, attachmentRunId, adapterId, mockMode, mockScenario, model, permissionMode, dangerouslySkipPermissions } = input;
333
328
  // Read secrets from the worker's config closure — never from workflow state
334
329
  const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
335
330
  // #676 FIX-3 — double-dispatch backstop (ACTIVITY-level; no workflow/bundle
@@ -492,11 +487,10 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
492
487
  else if (agent === 'pi') {
493
488
  // Phase 3a — headless Pi runtime. Injects the src/pi extension into
494
489
  // Pi's createAgentSession; the module-scope singleton owns the
495
- // lifecycle (claim/heartbeat/tools/cue-pump). Tool access is governed
496
- // by the MD-C `toolAccess` policy (restricted hard-blocks Bash via the
497
- // extension's tool_call gate), NOT per-tool allowlists.
490
+ // lifecycle (claim/heartbeat/tools/cue-pump). Pi players run the full
491
+ // Pi tool surface per-tool allowlists are not supported.
498
492
  if (allowedTools && allowedTools.length > 0) {
499
- log(`Warning: allowedTools [${allowedTools.join(', ')}] specified for pi agent "${targetName}" — Pi tool access is governed by toolAccess (MD-C), skipping allowedTools`);
493
+ log(`Warning: allowedTools [${allowedTools.join(', ')}] specified for pi agent "${targetName}" — Pi players run the full tool surface, skipping allowedTools`);
500
494
  }
501
495
  // 3c Tier-2 — mint a per-player ingest token scoped to this player's
502
496
  // session workflowId so the headless Pi subprocess can authenticate its
@@ -504,13 +498,6 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
504
498
  // REPLACES) means a restart re-mints and naturally revokes the stale
505
499
  // token. Injected into the subprocess env as AGENT_TEMPO_INGEST_TOKEN.
506
500
  const ingestToken = ingestTokens?.mint((0, config_1.sessionWorkflowId)(ensemble, targetName));
507
- // #712 — record the DURABLE guardrail policy on the daemon gate at spawn
508
- // (beside the ingest-token mint, same daemon process + lifecycle) so the
509
- // gate's failMode cross-check is daemon-authoritative from the FIRST
510
- // engagement — no lazy metadata query on the common path. Absent ⇒
511
- // autonomous (the extension default). Mint REPLACES on restart; setPolicy
512
- // likewise re-stamps the current durable posture (no silent downgrade).
513
- gate?.setPolicy((0, config_1.sessionWorkflowId)(ensemble, targetName), guardrailPolicy ?? 'autonomous');
514
501
  const { pid } = (0, spawn_1.spawnPiHeadless)({
515
502
  name: targetName,
516
503
  ensemble,
@@ -525,14 +512,12 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
525
512
  // Restart-resume: continue the prior Pi conversation only on a
526
513
  // restart (resume=true); a fresh recruit starts a new Pi session.
527
514
  continueSessionId: resume ? sessionId : undefined,
528
- toolAccess,
529
- guardrailPolicy,
530
515
  attachmentId,
531
516
  attachmentRunId,
532
517
  adapterId,
533
518
  ingestToken,
534
519
  });
535
- log(`Spawned pi headless adapter (pid ${pid}) in ${workDir} as "${targetName}" (toolAccess=${toolAccess ?? 'restricted'})${guardrailPolicy ? ` (guardrailPolicy=${guardrailPolicy})` : ''}${model ? ` (model=${model})` : ''}${resume && sessionId ? ` (continue=${sessionId})` : ''}${attachmentId ? ` (attachmentId=${attachmentId})` : ''}`);
520
+ log(`Spawned pi headless adapter (pid ${pid}) in ${workDir} as "${targetName}"${model ? ` (model=${model})` : ''}${resume && sessionId ? ` (continue=${sessionId})` : ''}${attachmentId ? ` (attachmentId=${attachmentId})` : ''}`);
536
521
  }
537
522
  else {
538
523
  // Resolve agent flags: --agent (native) > --system-prompt (shipped/legacy)
@@ -654,9 +639,6 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
654
639
  // the player goes away. Mirrors the destroy-side revoke; idempotent and a
655
640
  // no-op for non-Pi players (they never minted one). Re-attach re-mints.
656
641
  ingestTokens?.revoke((0, config_1.sessionWorkflowId)(ensemble, targetPlayerId));
657
- // 3d MD-G — auto-disarm the gate on detach (the operator's gate posture
658
- // shouldn't survive the player going away; re-attach re-arms). Idempotent.
659
- gate?.clearPlayer((0, config_1.sessionWorkflowId)(ensemble, targetPlayerId));
660
642
  log(`Detach signaled for "${targetPlayerId}" (deadline=${deadlineMs}ms)`);
661
643
  return { success: true };
662
644
  }
@@ -698,9 +680,6 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
698
680
  // revokeIngestToken(workflowId) on detach — deferred; residual surface
699
681
  // negligible (dead holder + loopback-only + single-token replacement).
700
682
  ingestTokens?.revoke((0, config_1.sessionWorkflowId)(ensemble, targetPlayerId));
701
- // 3d MD-G — auto-disarm: drop the gate's armed-state + any pending
702
- // requests for the destroyed player (idempotent; no-op for non-Pi).
703
- gate?.clearPlayer((0, config_1.sessionWorkflowId)(ensemble, targetPlayerId));
704
683
  if (notifyConductor) {
705
684
  try {
706
685
  const condId = (0, config_1.conductorWorkflowId)(ensemble);
@@ -965,10 +944,6 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
965
944
  // metadata.model field landed (which fall back to the env / default
966
945
  // chain inside the adapter).
967
946
  ...(metadata.model !== undefined ? { model: metadata.model } : {}),
968
- // #700 (P2 / G) — guardrail posture carried across restart, read from
969
- // DURABLE SessionMetadata so the restarted player re-arms the SAME
970
- // posture (no silent downgrade). Absent ⇒ autonomous.
971
- ...(metadata.guardrailPolicy !== undefined ? { guardrailPolicy: metadata.guardrailPolicy } : {}),
972
947
  ...(resolved ? {
973
948
  agentDefinition: resolved.name,
974
949
  agentDefinitionPath: resolved.path,
@@ -21,27 +21,9 @@ const log = (...args) => {
21
21
  // eslint-disable-next-line no-console
22
22
  console.error('[agent-tempo:pi]', ...args);
23
23
  };
24
- /** Normalize the env tool-access value to the MD-C policy (default 'restricted'). */
25
- function readToolAccess() {
26
- const raw = process.env[config_1.ENV.TOOL_ACCESS];
27
- return raw === 'standard' || raw === 'full' ? raw : 'restricted';
28
- }
29
- /**
30
- * #700 (P2 / G) — normalize the env guardrail posture. Unknown / absent ⇒
31
- * undefined (the extension defaults to 'autonomous'). The durable source is
32
- * SessionMetadata; this env is re-derived from it on every (re)spawn.
33
- */
34
- function readGuardrailPolicy() {
35
- const raw = process.env[config_1.ENV.GUARDRAIL_POLICY];
36
- return raw === 'autonomous' || raw === 'monitored' || raw === 'supervised' || raw === 'observe-only'
37
- ? raw
38
- : undefined;
39
- }
40
24
  /** Parse the spawn-provided env into options and run. Exported for testing. */
41
25
  async function main() {
42
26
  await (0, headless_1.runHeadlessPi)({
43
- toolAccess: readToolAccess(),
44
- guardrailPolicy: readGuardrailPolicy(),
45
27
  model: process.env[config_1.ENV.PI_MODEL] || undefined,
46
28
  continueSessionId: process.env[config_1.ENV.PI_CONTINUE_SESSION] || undefined,
47
29
  });
package/dist/cli.js CHANGED
@@ -638,7 +638,7 @@ async function main() {
638
638
  const { installPiExtensions } = await Promise.resolve().then(() => __importStar(require('./pi/install')));
639
639
  const result = installPiExtensions({ project: args.project });
640
640
  out.success(`Pi extensions installed → ${result.settingsPath}`);
641
- // #52 — show pruned stale/old-version entries so an upgrade is legible.
641
+ // #738 — show pruned stale/old-version entries so an upgrade is legible.
642
642
  for (const p of result.removed)
643
643
  out.log(` ${out.yellow('-')} ${p} ${out.dim('(removed stale/old-version entry)')}`);
644
644
  for (const p of result.added)
package/dist/config.d.ts CHANGED
@@ -62,26 +62,6 @@ export declare const ENV: {
62
62
  * recruit → a new Pi session.
63
63
  */
64
64
  readonly PI_CONTINUE_SESSION: "AGENT_TEMPO_PI_CONTINUE_SESSION";
65
- /**
66
- * Phase 3a / MD-C — headless Pi tool-access policy. One of
67
- * `restricted` (default; Bash/shell/exec HARD-BLOCKED) | `standard` (scoped
68
- * Bash) | `full` (unsandboxed; admin-gated at recruit). Read by the Pi
69
- * extension's `tool_call` gate (mode='headless' only). Mirrors
70
- * {@link PERMISSION_MODE}'s threading.
71
- */
72
- readonly TOOL_ACCESS: "AGENT_TEMPO_TOOL_ACCESS";
73
- /**
74
- * #700 (P2 / G) — headless Pi guardrail posture. One of `autonomous`
75
- * (default; no gate) | `monitored` (operator-armed, fail-OPEN) | `supervised`
76
- * (fail-CLOSED, self-arming) | `observe-only` (no-act). Read by the Pi
77
- * extension's `tool_call` gate (mode='headless'). The DURABLE source of truth
78
- * is {@link SessionMetadata.guardrailPolicy}; this env var is just the per-boot
79
- * transport — the (re)spawn always re-derives it from metadata, so a restart
80
- * can never silently downgrade a supervised player (mirrors how `model` is
81
- * re-threaded from durable metadata across restart, NOT the ephemeral
82
- * `TOOL_ACCESS` env-only path).
83
- */
84
- readonly GUARDRAIL_POLICY: "AGENT_TEMPO_GUARDRAIL_POLICY";
85
65
  /**
86
66
  * 3c Tier-2 ingest auth. The daemon mints a per-player ingest token (scoped to
87
67
  * the session workflowId) BEFORE spawning a headless Pi player and threads it
package/dist/config.js CHANGED
@@ -91,26 +91,6 @@ exports.ENV = {
91
91
  * recruit → a new Pi session.
92
92
  */
93
93
  PI_CONTINUE_SESSION: 'AGENT_TEMPO_PI_CONTINUE_SESSION',
94
- /**
95
- * Phase 3a / MD-C — headless Pi tool-access policy. One of
96
- * `restricted` (default; Bash/shell/exec HARD-BLOCKED) | `standard` (scoped
97
- * Bash) | `full` (unsandboxed; admin-gated at recruit). Read by the Pi
98
- * extension's `tool_call` gate (mode='headless' only). Mirrors
99
- * {@link PERMISSION_MODE}'s threading.
100
- */
101
- TOOL_ACCESS: 'AGENT_TEMPO_TOOL_ACCESS',
102
- /**
103
- * #700 (P2 / G) — headless Pi guardrail posture. One of `autonomous`
104
- * (default; no gate) | `monitored` (operator-armed, fail-OPEN) | `supervised`
105
- * (fail-CLOSED, self-arming) | `observe-only` (no-act). Read by the Pi
106
- * extension's `tool_call` gate (mode='headless'). The DURABLE source of truth
107
- * is {@link SessionMetadata.guardrailPolicy}; this env var is just the per-boot
108
- * transport — the (re)spawn always re-derives it from metadata, so a restart
109
- * can never silently downgrade a supervised player (mirrors how `model` is
110
- * re-threaded from durable metadata across restart, NOT the ephemeral
111
- * `TOOL_ACCESS` env-only path).
112
- */
113
- GUARDRAIL_POLICY: 'AGENT_TEMPO_GUARDRAIL_POLICY',
114
94
  /**
115
95
  * 3c Tier-2 ingest auth. The daemon mints a per-player ingest token (scoped to
116
96
  * the session workflowId) BEFORE spawning a headless Pi player and threads it
package/dist/daemon.js CHANGED
@@ -68,14 +68,10 @@ const worker_1 = require("./worker");
68
68
  const connection_1 = require("./connection");
69
69
  const inner_loop_1 = require("./http/inner-loop");
70
70
  const ingest_registry_1 = require("./http/ingest-registry");
71
- const gate_registry_1 = require("./http/gate-registry");
72
- const gate_audit_1 = require("./http/gate-audit");
73
71
  const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
74
72
  const daemon_1 = require("./cli/daemon");
75
73
  const client_3 = require("./client");
76
74
  const orphans_1 = require("./reconcile/orphans");
77
- const query_timeout_1 = require("./utils/query-timeout");
78
- const signals_1 = require("./workflows/signals");
79
75
  const agent_types_1 = require("./ensemble/agent-types");
80
76
  const pre_flight_1 = require("./adapters/claude-code-headless/pre-flight");
81
77
  const daemon_adapter_versions_1 = require("./daemon-adapter-versions");
@@ -841,12 +837,6 @@ async function main() {
841
837
  // so the shutdown handler — declared just below — can drain them.
842
838
  const innerLoop = new inner_loop_1.InnerLoopRegistry();
843
839
  const ingestTokens = new ingest_registry_1.IngestTokenRegistry();
844
- // 3d MD-G — the operator-gate registry, same daemon-owned-singleton pattern.
845
- // Audit sink = the append-only JSONL writer; publishToInner = innerLoop.publish
846
- // (the DI that emits gate_resolved on the player's /inner stream without a
847
- // GateRegistry↔inner-loop circular import).
848
- const gate = new gate_registry_1.GateRegistry((0, gate_audit_1.createGateAuditSink)(), Date.now, undefined, // default 45s auto-allow
849
- (workflowId, frame) => innerLoop.publish(workflowId, frame));
850
840
  const shutdown = () => {
851
841
  if (shuttingDown)
852
842
  return;
@@ -885,7 +875,6 @@ async function main() {
885
875
  // (streams end cleanly rather than dangling).
886
876
  ingestTokens.revokeAll();
887
877
  innerLoop.close();
888
- gate.clear();
889
878
  sharedWorker?.shutdown();
890
879
  hostWorker?.shutdown();
891
880
  };
@@ -893,7 +882,7 @@ async function main() {
893
882
  process.on('SIGINT', shutdown);
894
883
  // Create workers (signal handlers already active via mutable refs)
895
884
  log(`Connecting to Temporal at ${config.temporalAddress} (namespace: ${config.temporalNamespace})`);
896
- const workers = await (0, worker_1.createWorkers)(config, ingestTokens, gate);
885
+ const workers = await (0, worker_1.createWorkers)(config, ingestTokens);
897
886
  sharedWorker = workers.sharedWorker;
898
887
  hostWorker = workers.hostWorker;
899
888
  log('Workers created — processing tasks');
@@ -972,24 +961,6 @@ async function main() {
972
961
  const bootEpoch = Date.now();
973
962
  aggregateRunner = new AggregateRunner({ client: httpClient, bootEpoch });
974
963
  aggregateRunner.start();
975
- // #712 — BOUNDED durable-policy resolver for the gate failMode cross-check.
976
- // The gate is normally policy-populated at spawn (sync, no query); this is
977
- // the rare-cache-miss fallback the ingest gate_pending path awaits. It MUST
978
- // be bounded (utils/query-timeout) so it can never hang gate engagement. A
979
- // successful query with an absent metadata field ⇒ 'autonomous' (a real,
980
- // open posture). Timeout / error / workflow-gone ⇒ undefined ⇒ the route
981
- // leaves the policy unresolved ⇒ open() enforces 'closed' (NO-FAIL-OPEN).
982
- const reconcileClientForPolicy = reconcileClient;
983
- const resolveGuardrailPolicy = async (workflowId) => {
984
- try {
985
- const handle = reconcileClientForPolicy.workflow.getHandle(workflowId);
986
- const md = await (0, query_timeout_1.queryHandleWithTimeout)(handle, signals_1.getMetadataQuery);
987
- return md?.guardrailPolicy ?? 'autonomous';
988
- }
989
- catch {
990
- return undefined; // timeout / error / gone → indeterminate → open() enforces closed
991
- }
992
- };
993
964
  httpServerHandle = await startHttpServer({
994
965
  client: httpClient,
995
966
  namespace: config.temporalNamespace,
@@ -1001,11 +972,6 @@ async function main() {
1001
972
  // and /inner/ingest validates against the tokens the spawn path minted.
1002
973
  innerLoop,
1003
974
  ingestTokens,
1004
- // 3d MD-G — the same gate registry the worker's outbox auto-disarms on
1005
- // detach/destroy; the HTTP server serves arm/disarm/decide + resolution.
1006
- gate,
1007
- // #712 — bounded durable-policy resolver for the failMode cross-check.
1008
- resolveGuardrailPolicy,
1009
975
  });
1010
976
  log(`HTTP listening on http://${httpServerHandle.bindAddr}:${httpServerHandle.port}`);
1011
977
  log(`Aggregate poll loop running (bootEpoch=${bootEpoch})`);
@@ -95,7 +95,7 @@ export declare function loadRbacTokens(opts: {
95
95
  load?: () => PersistedConfig;
96
96
  save?: (cfg: PersistedConfig) => void;
97
97
  }): RbacTokens;
98
- /** Access tiers (MD-E): 1 = read/observe, 2 = write/mutate, 3 = supervisory (gate/inner). */
98
+ /** Access tiers (MD-E): 1 = read/observe, 2 = write/mutate, 3 = supervisory (inner-tail). */
99
99
  export type Tier = 1 | 2 | 3;
100
100
  /**
101
101
  * Granted tier for a presented bearer, given the RBAC tokens (timing-safe).
package/dist/http/auth.js CHANGED
@@ -222,8 +222,8 @@ function tierForToken(presented, tokens) {
222
222
  return 0;
223
223
  }
224
224
  /** Migration hint surfaced in the 403 body so a read-token holder knows what's missing. */
225
- const INSUFFICIENT_TIER_HINT = 'This token is read-tier. Writes, the operator gate, and the inner-tail require the admin token (set AGENT_TEMPO_HTTP_ADMIN_TOKEN).';
226
- const ADMIN_UNSET_HINT = 'Set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only) to enable writes / gate / inner-tail.';
225
+ const INSUFFICIENT_TIER_HINT = 'This token is read-tier. Writes and the inner-tail require the admin token (set AGENT_TEMPO_HTTP_ADMIN_TOKEN).';
226
+ const ADMIN_UNSET_HINT = 'Set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only) to enable writes / inner-tail.';
227
227
  /**
228
228
  * Authorization guard (3e MD-E). Assumes the shared upstream pass already settled
229
229
  * AUTHENTICATION + CORS + the DNS-rebind/Origin defense (architect's Layer 2);
@@ -23,35 +23,11 @@
23
23
  import type { IncomingMessage, ServerResponse } from 'http';
24
24
  import type { InnerLoopRegistry } from './inner-loop';
25
25
  import type { IngestTokenRegistry } from './ingest-registry';
26
- import type { GateRegistry } from './gate-registry';
27
- import type { GuardrailPolicy } from '../types';
28
26
  /** Header carrying the per-player ingest token (the source-plane credential). */
29
27
  export declare const INGEST_TOKEN_HEADER = "x-ingest-token";
30
28
  export interface InnerLoopDeps {
31
29
  innerLoop: InnerLoopRegistry;
32
30
  ingestTokens: IngestTokenRegistry;
33
- /**
34
- * 3d MD-G — the operator-gate registry, when wired. Two narrow couplings:
35
- * - ingest: an `inner.gate_pending` frame ALSO registers the pending request
36
- * in the gate (`open()`) — atomic register-and-surface from one POST (the
37
- * "engagement IS registration" path; no separate open-route).
38
- * - presence: the response carries `gateArmed` so the polling subprocess
39
- * evaluates engagement (armed + present) from one fetch.
40
- * The coupling is one-directional (inner-routes → GateRegistry); the gate's
41
- * `gate_resolved` emission flows back via its injected publishToInner.
42
- */
43
- gate?: GateRegistry;
44
- /**
45
- * #712 — BOUNDED resolver for a player's DURABLE `guardrailPolicy` (daemon
46
- * wires it to a `getMetadataQuery` behind `utils/query-timeout`). Used by the
47
- * ingest gate_pending path ONLY on a {@link GateRegistry.getPolicy} cache-miss
48
- * (the common path is spawn/reconcile-populated → sync, no query). Returns the
49
- * resolved policy (absent metadata field ⇒ `'autonomous'`), or `undefined` when
50
- * the query TIMES OUT / ERRORS / the workflow is gone — the caller then leaves
51
- * the gate policy unresolved so `open()` enforces `closed` (NO-FAIL-OPEN). The
52
- * resolver MUST be bounded so it can never hang gate engagement.
53
- */
54
- resolveGuardrailPolicy?: (workflowId: string) => Promise<GuardrailPolicy | undefined>;
55
31
  }
56
32
  /** True when the request originates from the same host (loopback). */
57
33
  export declare function isLoopbackRemote(req: IncomingMessage): boolean;
@@ -78,36 +78,6 @@ async function handleInnerIngest(req, res, deps, ensemble, playerId) {
78
78
  // Trust the authenticated publisher's frame shape (it owns the schema +
79
79
  // truncation). Publish to local subscribers and ack with no body.
80
80
  deps.innerLoop.publish(workflowId, body);
81
- // 3d MD-G — a gate_pending frame ALSO registers the pending request in the
82
- // gate (atomic register-and-surface from one POST; the "engagement IS
83
- // registration" path, no separate open-route). Narrow, one-directional
84
- // coupling: inner-routes → GateRegistry.open(). Guarded on the type so it
85
- // never fires for ordinary inner frames. `open` is idempotent on requestId.
86
- if (type === 'inner.gate_pending' && deps.gate) {
87
- const f = body;
88
- if (typeof f.requestId === 'string' && typeof f.tool === 'string') {
89
- // #712 — ensure the gate knows the player's DURABLE guardrailPolicy BEFORE
90
- // registering, so open() enforces failMode from policy (daemon-authoritative),
91
- // not the frame's advisory claim. Fast path: already populated at spawn /
92
- // reconcile (sync, no query). Miss (post-restart pre-reconcile): a BOUNDED
93
- // getMetadataQuery; on success populate the gate. On timeout/error/gone the
94
- // resolver returns undefined → we leave it unresolved → open() enforces
95
- // 'closed' (NO-FAIL-OPEN).
96
- if (deps.resolveGuardrailPolicy && deps.gate.getPolicy(workflowId) === undefined) {
97
- const resolved = await deps.resolveGuardrailPolicy(workflowId);
98
- if (resolved !== undefined)
99
- deps.gate.setPolicy(workflowId, resolved);
100
- }
101
- deps.gate.open(workflowId, f.requestId, {
102
- tool: f.tool,
103
- argsSummary: typeof f.argsSummary === 'string' ? f.argsSummary : '',
104
- ensemble,
105
- // #712 — ADVISORY claim only (open() enforces from policy); retained so a
106
- // self-downgrade (frame 'open' vs policy-enforced 'closed') is auditable.
107
- failMode: f.failMode === 'closed' ? 'closed' : 'open',
108
- });
109
- }
110
- }
111
81
  res.writeHead(204);
112
82
  res.end();
113
83
  }
@@ -120,13 +90,8 @@ function handleInnerPresence(req, res, deps, ensemble, playerId) {
120
90
  const workflowId = gateIngress(req, res, deps, ensemble, playerId);
121
91
  if (workflowId === null)
122
92
  return;
123
- // 3d MD-G — fold `gateArmed` into the presence response so the polling
124
- // subprocess reads BOTH engagement inputs (operator-present + armed) from one
125
- // fetch (avoids a stale-armed / fresh-present mismatch). `false` when the gate
126
- // is unwired or unarmed.
127
93
  (0, responses_1.jsonResponse)(res, 200, {
128
94
  subscribers: deps.innerLoop.subscriberCount(workflowId),
129
- gateArmed: deps.gate?.isArmed(workflowId) ?? false,
130
95
  });
131
96
  }
132
97
  /**