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.
- package/CLAUDE.md +11 -15
- package/README.md +10 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/outbox.d.ts +2 -24
- package/dist/activities/outbox.js +7 -32
- package/dist/adapters/pi/adapter.js +0 -18
- package/dist/cli.js +1 -1
- package/dist/config.d.ts +0 -20
- package/dist/config.js +0 -20
- package/dist/daemon.js +1 -35
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/auth.js +2 -2
- package/dist/http/inner-loop-routes.d.ts +0 -24
- package/dist/http/inner-loop-routes.js +0 -35
- package/dist/http/server.d.ts +0 -23
- package/dist/http/server.js +7 -54
- package/dist/pi/cue-pump.d.ts +81 -8
- package/dist/pi/cue-pump.js +109 -4
- package/dist/pi/extension.d.ts +4 -21
- package/dist/pi/extension.js +16 -110
- package/dist/pi/headless.d.ts +28 -32
- package/dist/pi/headless.js +14 -87
- package/dist/pi/inner-loop-client.d.ts +0 -9
- package/dist/pi/inner-loop-client.js +1 -16
- package/dist/pi/inner-loop-publisher.d.ts +1 -33
- package/dist/pi/install.d.ts +3 -3
- package/dist/pi/install.js +3 -3
- package/dist/pi/mission-control/actions.d.ts +1 -5
- package/dist/pi/mission-control/actions.js +4 -17
- package/dist/pi/mission-control/extension.d.ts +0 -2
- package/dist/pi/mission-control/extension.js +1 -25
- package/dist/pi/mission-control/render.js +0 -5
- package/dist/pi/pi-types.d.ts +4 -3
- package/dist/spawn.d.ts +4 -14
- package/dist/spawn.js +3 -11
- package/dist/tools/recruit.js +0 -38
- package/dist/types.d.ts +0 -90
- package/dist/worker.d.ts +1 -2
- package/dist/worker.js +2 -2
- package/dist/workflows/session.js +1 -15
- package/dist/workflows/signals.d.ts +1 -3
- package/package.json +1 -1
- package/workflow-bundle.js +2 -16
- package/dist/http/gate-audit.d.ts +0 -12
- package/dist/http/gate-audit.js +0 -95
- package/dist/http/gate-registry.d.ts +0 -279
- package/dist/http/gate-registry.js +0 -253
- package/dist/http/gate-routes.d.ts +0 -48
- package/dist/http/gate-routes.js +0 -102
- package/dist/pi/gate-client.d.ts +0 -71
- package/dist/pi/gate-client.js +0 -177
- package/dist/pi/reset-pump.d.ts +0 -85
- package/dist/pi/reset-pump.js +0 -135
- package/dist/security/tool-capability.d.ts +0 -60
- 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
|
|
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
|
|
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)
|
|
125
|
-
│ │ ├── actions.ts # Daemon HTTP write-surface client (T2 + T3 bearer): cue/pause/play/restart/destroy
|
|
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).
|
|
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
|
|
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,
|
|
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'`.
|
|
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
|
|
package/dashboard/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tempo-dashboard",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "1.7.0-beta.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
496
|
-
//
|
|
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
|
|
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}"
|
|
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
|
-
// #
|
|
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
|
|
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})`);
|
package/dist/http/auth.d.ts
CHANGED
|
@@ -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 (
|
|
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
|
|
226
|
-
const ADMIN_UNSET_HINT = 'Set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only) to enable writes /
|
|
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
|
/**
|