agent-tempo 1.2.0 → 1.4.0
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 +253 -219
- package/LICENSE +21 -21
- package/README.md +293 -289
- package/assets/icon-dark.svg +9 -9
- package/assets/icon.svg +9 -9
- package/assets/logo-dark.svg +11 -11
- package/assets/logo-light.svg +11 -11
- package/dashboard/README.md +91 -91
- package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
- package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
- package/dashboard/dist/index.html +20 -20
- package/dashboard/package.json +47 -47
- package/dist/activities/outbox.d.ts +30 -1
- package/dist/activities/outbox.js +96 -3
- package/dist/adapters/base.js +5 -0
- package/dist/adapters/copilot/adapter.js +12 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/adapters/index.js +7 -0
- package/dist/adapters/pi/adapter.d.ts +2 -0
- package/dist/adapters/pi/adapter.js +43 -0
- package/dist/adapters/pi/index.d.ts +16 -0
- package/dist/adapters/pi/index.js +10 -0
- package/dist/cli/global-wrapper.d.ts +19 -0
- package/dist/cli/global-wrapper.js +169 -0
- package/dist/cli/help-text.js +97 -97
- package/dist/cli/startup.js +11 -0
- package/dist/cli/upgrade-command.js +81 -81
- package/dist/cli.js +12 -0
- package/dist/client/core.js +9 -2
- package/dist/client/interface.d.ts +6 -0
- package/dist/config.d.ts +79 -0
- package/dist/config.js +74 -0
- package/dist/daemon.js +37 -1
- package/dist/http/aggregate.d.ts +22 -1
- package/dist/http/aggregate.js +41 -0
- package/dist/http/auth.d.ts +94 -8
- package/dist/http/auth.js +93 -9
- package/dist/http/body.d.ts +4 -1
- package/dist/http/body.js +6 -3
- package/dist/http/event-bus.js +1 -0
- package/dist/http/event-types.d.ts +34 -2
- package/dist/http/event-types.js +1 -0
- package/dist/http/gate-audit.d.ts +12 -0
- package/dist/http/gate-audit.js +95 -0
- package/dist/http/gate-registry.d.ts +167 -0
- package/dist/http/gate-registry.js +163 -0
- package/dist/http/gate-routes.d.ts +48 -0
- package/dist/http/gate-routes.js +102 -0
- package/dist/http/ingest-registry.d.ts +30 -0
- package/dist/http/ingest-registry.js +108 -0
- package/dist/http/inner-loop-routes.d.ts +66 -0
- package/dist/http/inner-loop-routes.js +182 -0
- package/dist/http/inner-loop.d.ts +92 -0
- package/dist/http/inner-loop.js +155 -0
- package/dist/http/server.d.ts +38 -3
- package/dist/http/server.js +211 -6
- package/dist/http/snapshot.d.ts +6 -0
- package/dist/http/snapshot.js +6 -0
- package/dist/pi/cue-pump.d.ts +61 -0
- package/dist/pi/cue-pump.js +95 -0
- package/dist/pi/extension.d.ts +45 -0
- package/dist/pi/extension.js +407 -0
- package/dist/pi/gate-client.d.ts +54 -0
- package/dist/pi/gate-client.js +136 -0
- package/dist/pi/headless.d.ts +85 -0
- package/dist/pi/headless.js +224 -0
- package/dist/pi/index.d.ts +28 -0
- package/dist/pi/index.js +43 -0
- package/dist/pi/inner-loop-client.d.ts +67 -0
- package/dist/pi/inner-loop-client.js +164 -0
- package/dist/pi/inner-loop-publisher.d.ts +187 -0
- package/dist/pi/inner-loop-publisher.js +236 -0
- package/dist/pi/lazy-proxy.d.ts +37 -0
- package/dist/pi/lazy-proxy.js +55 -0
- package/dist/pi/mission-control/actions.d.ts +48 -0
- package/dist/pi/mission-control/actions.js +98 -0
- package/dist/pi/mission-control/board.d.ts +53 -0
- package/dist/pi/mission-control/board.js +104 -0
- package/dist/pi/mission-control/extension.d.ts +44 -0
- package/dist/pi/mission-control/extension.js +251 -0
- package/dist/pi/mission-control/index.d.ts +15 -0
- package/dist/pi/mission-control/index.js +32 -0
- package/dist/pi/mission-control/inner-tail.d.ts +48 -0
- package/dist/pi/mission-control/inner-tail.js +76 -0
- package/dist/pi/mission-control/pi-ui.d.ts +43 -0
- package/dist/pi/mission-control/pi-ui.js +10 -0
- package/dist/pi/mission-control/render.d.ts +6 -0
- package/dist/pi/mission-control/render.js +95 -0
- package/dist/pi/phase-driver.d.ts +74 -0
- package/dist/pi/phase-driver.js +122 -0
- package/dist/pi/pi-types.d.ts +208 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +80 -0
- package/dist/pi/probe.js +154 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +51 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/tool-capability.d.ts +60 -0
- package/dist/pi/tool-capability.js +156 -0
- package/dist/pi/workflow-client.d.ts +158 -0
- package/dist/pi/workflow-client.js +289 -0
- package/dist/pi/zod-to-typebox.d.ts +74 -0
- package/dist/pi/zod-to-typebox.js +191 -0
- package/dist/scripts/verify-daemon-isolation-guard.js +24 -24
- package/dist/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/server.js +4 -0
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +84 -12
- package/dist/tools/agent-types.d.ts +2 -2
- package/dist/tools/agent-types.js +22 -17
- package/dist/tools/attachment-info.d.ts +2 -2
- package/dist/tools/attachment-info.js +38 -33
- package/dist/tools/broadcast.d.ts +2 -2
- package/dist/tools/broadcast.js +69 -64
- package/dist/tools/cancel-stage.d.ts +2 -2
- package/dist/tools/cancel-stage.js +20 -15
- package/dist/tools/clear-state.d.ts +2 -2
- package/dist/tools/clear-state.js +25 -20
- package/dist/tools/coat-check-evict.d.ts +2 -2
- package/dist/tools/coat-check-evict.js +30 -25
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +39 -34
- package/dist/tools/coat-check-list.d.ts +2 -2
- package/dist/tools/coat-check-list.js +48 -43
- package/dist/tools/coat-check-put.d.ts +2 -2
- package/dist/tools/coat-check-put.js +41 -36
- package/dist/tools/cue.d.ts +2 -2
- package/dist/tools/cue.js +57 -52
- package/dist/tools/descriptor.d.ts +72 -0
- package/dist/tools/descriptor.js +39 -0
- package/dist/tools/destroy.d.ts +2 -2
- package/dist/tools/destroy.js +153 -148
- package/dist/tools/ensemble.d.ts +2 -2
- package/dist/tools/ensemble.js +71 -66
- package/dist/tools/evaluate-gate.d.ts +2 -2
- package/dist/tools/evaluate-gate.js +33 -27
- package/dist/tools/fetch-state.d.ts +2 -2
- package/dist/tools/fetch-state.js +43 -38
- package/dist/tools/gates.d.ts +2 -2
- package/dist/tools/gates.js +39 -34
- package/dist/tools/hosts.d.ts +2 -2
- package/dist/tools/hosts.js +25 -20
- package/dist/tools/listen.d.ts +2 -2
- package/dist/tools/listen.js +23 -18
- package/dist/tools/load-lineup.d.ts +2 -2
- package/dist/tools/load-lineup.js +324 -319
- package/dist/tools/migrate.d.ts +2 -2
- package/dist/tools/migrate.js +45 -40
- package/dist/tools/pause.d.ts +2 -2
- package/dist/tools/pause.js +34 -29
- package/dist/tools/play.d.ts +2 -2
- package/dist/tools/play.js +53 -48
- package/dist/tools/quality-gate.d.ts +2 -2
- package/dist/tools/quality-gate.js +26 -21
- package/dist/tools/recall.d.ts +2 -2
- package/dist/tools/recall.js +32 -27
- package/dist/tools/recruit.d.ts +2 -2
- package/dist/tools/recruit.js +325 -256
- package/dist/tools/release.d.ts +2 -2
- package/dist/tools/release.js +85 -80
- package/dist/tools/report.d.ts +2 -2
- package/dist/tools/report.js +28 -23
- package/dist/tools/reset.d.ts +3 -0
- package/dist/tools/reset.js +51 -0
- package/dist/tools/restart.d.ts +2 -2
- package/dist/tools/restart.js +51 -46
- package/dist/tools/restore.d.ts +2 -2
- package/dist/tools/restore.js +76 -71
- package/dist/tools/save-lineup.d.ts +2 -2
- package/dist/tools/save-lineup.js +32 -27
- package/dist/tools/save-state.d.ts +2 -2
- package/dist/tools/save-state.js +43 -38
- package/dist/tools/schedule.d.ts +2 -2
- package/dist/tools/schedule.js +133 -128
- package/dist/tools/schedules.d.ts +2 -2
- package/dist/tools/schedules.js +41 -36
- package/dist/tools/set-ensemble-description.d.ts +2 -2
- package/dist/tools/set-ensemble-description.js +26 -21
- package/dist/tools/set-name.d.ts +2 -2
- package/dist/tools/set-name.js +38 -33
- package/dist/tools/set-part.d.ts +2 -2
- package/dist/tools/set-part.js +20 -15
- package/dist/tools/shutdown.d.ts +2 -2
- package/dist/tools/shutdown.js +39 -34
- package/dist/tools/stage.d.ts +2 -2
- package/dist/tools/stage.js +28 -23
- package/dist/tools/stages.d.ts +2 -2
- package/dist/tools/stages.js +36 -31
- package/dist/tools/unschedule.d.ts +2 -2
- package/dist/tools/unschedule.js +30 -25
- package/dist/tools/who-am-i.d.ts +2 -2
- package/dist/tools/who-am-i.js +36 -31
- package/dist/tools/worktree.d.ts +2 -2
- package/dist/tools/worktree.js +134 -129
- package/dist/tui/index.js +6 -6
- package/dist/types.d.ts +47 -2
- package/dist/types.js +1 -1
- package/dist/utils/default-part.js +1 -0
- package/dist/utils/grpc-shutdown-guard.d.ts +52 -0
- package/dist/utils/grpc-shutdown-guard.js +88 -0
- package/dist/utils/sdk-probe.d.ts +23 -0
- package/dist/utils/sdk-probe.js +46 -7
- package/dist/worker.d.ts +3 -1
- package/dist/worker.js +6 -2
- package/dist/workflows/session.js +70 -2
- package/dist/workflows/signals.d.ts +32 -2
- package/dist/workflows/signals.js +25 -2
- package/examples/agents/tempo-composer.md +56 -56
- package/examples/agents/tempo-conductor.md +117 -117
- package/examples/agents/tempo-critic.md +73 -73
- package/examples/agents/tempo-improv.md +74 -74
- package/examples/agents/tempo-liner.md +75 -75
- package/examples/agents/tempo-roadie.md +61 -61
- package/examples/agents/tempo-soloist.md +71 -71
- package/examples/agents/tempo-tuner.md +94 -94
- package/examples/ensembles/tempo-big-band.yaml +146 -146
- package/examples/ensembles/tempo-dev-team.yaml +58 -58
- package/examples/ensembles/tempo-headless-jam.yaml +77 -77
- package/examples/ensembles/tempo-jam-session.yaml +41 -41
- package/examples/ensembles/tempo-mock-jam.yaml +79 -79
- package/examples/ensembles/tempo-review-squad.yaml +32 -32
- package/package.json +176 -173
- package/packaging/launchd/com.agent.tempo.plist +46 -46
- package/packaging/systemd/agent-tempo.service +32 -32
- package/packaging/windows/install-task.ps1 +71 -71
- package/scenarios/conductor-recruit-mock.yaml +33 -33
- package/scenarios/echo-roundtrip.yaml +15 -15
- package/scenarios/multi-player-handoff.yaml +38 -38
- package/scenarios/recruit-cascade.yaml +38 -38
- package/scenarios/two-player-conversation.yaml +33 -33
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/activities/claude-stop.d.ts +0 -21
- package/dist/activities/claude-stop.js +0 -94
- package/dist/channel.d.ts +0 -3
- package/dist/channel.js +0 -48
- package/dist/copilot-bridge.d.ts +0 -22
- package/dist/copilot-bridge.js +0 -565
- package/dist/scripts/258-spotcheck.js +0 -303
- package/dist/tools/detach.d.ts +0 -4
- package/dist/tools/detach.js +0 -45
- package/dist/tools/encore.d.ts +0 -4
- package/dist/tools/encore.js +0 -31
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
- package/dist/tools/pause-ensemble.d.ts +0 -4
- package/dist/tools/pause-ensemble.js +0 -58
- package/dist/tools/resume-ensemble.d.ts +0 -4
- package/dist/tools/resume-ensemble.js +0 -79
- package/dist/tools/stop.d.ts +0 -4
- package/dist/tools/stop.js +0 -29
- package/dist/tui/client.d.ts +0 -6
- package/dist/tui/client.js +0 -9
- package/dist/tui/components/ActivityLog.d.ts +0 -16
- package/dist/tui/components/ActivityLog.js +0 -36
- package/dist/tui/components/CommandOverlay.d.ts +0 -15
- package/dist/tui/components/CommandOverlay.js +0 -34
- package/dist/tui/components/ConductorChat.d.ts +0 -16
- package/dist/tui/components/ConductorChat.js +0 -32
- package/dist/tui/components/EnsembleListView.d.ts +0 -14
- package/dist/tui/components/EnsembleListView.js +0 -32
- package/dist/tui/components/EnsemblePanel.d.ts +0 -12
- package/dist/tui/components/EnsemblePanel.js +0 -40
- package/dist/tui/components/InputBar.d.ts +0 -13
- package/dist/tui/components/InputBar.js +0 -58
- package/dist/tui/components/ScheduleOverlay.d.ts +0 -13
- package/dist/tui/components/ScheduleOverlay.js +0 -113
- package/dist/tui/components/TopBar.d.ts +0 -12
- package/dist/tui/components/TopBar.js +0 -15
- package/dist/tui/core-api.d.ts +0 -26
- package/dist/tui/core-api.js +0 -67
- package/dist/tui/hooks/useEnsembleDiscovery.d.ts +0 -3
- package/dist/tui/hooks/useEnsembleDiscovery.js +0 -30
- package/dist/tui/hooks/useMaestroPoller.d.ts +0 -3
- package/dist/tui/hooks/useMaestroPoller.js +0 -36
- package/dist/tui/hooks/useSendCommand.d.ts +0 -7
- package/dist/tui/hooks/useSendCommand.js +0 -29
- package/dist/utils/bg-preflight.d.ts +0 -25
- package/dist/utils/bg-preflight.js +0 -154
package/dist/config.d.ts
CHANGED
|
@@ -48,6 +48,37 @@ export declare const ENV: {
|
|
|
48
48
|
* sandboxed contexts. Mutually exclusive with {@link PERMISSION_MODE}.
|
|
49
49
|
*/
|
|
50
50
|
readonly DANGEROUSLY_SKIP_PERMISSIONS: "AGENT_TEMPO_DANGEROUSLY_SKIP_PERMISSIONS";
|
|
51
|
+
/**
|
|
52
|
+
* Phase 3a — headless Pi runtime model selector. Pi takes a `provider/model`
|
|
53
|
+
* string (e.g. `anthropic/claude-opus-4-7`); absent → Pi's own default
|
|
54
|
+
* provider/model (the 3a anthropic-default path). Recruit `model` arg →
|
|
55
|
+
* this env → Pi default.
|
|
56
|
+
*/
|
|
57
|
+
readonly PI_MODEL: "AGENT_TEMPO_PI_MODEL";
|
|
58
|
+
/**
|
|
59
|
+
* Phase 3a — headless Pi restart-resume. The daemon reads `metadata.sessionId`
|
|
60
|
+
* (the Pi conversation id the player was in when it died) and passes it here;
|
|
61
|
+
* the headless entry resumes via Pi `continueSession(<id>)`. Absent on a fresh
|
|
62
|
+
* recruit → a new Pi session.
|
|
63
|
+
*/
|
|
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
|
+
* 3c Tier-2 ingest auth. The daemon mints a per-player ingest token (scoped to
|
|
75
|
+
* the session workflowId) BEFORE spawning a headless Pi player and threads it
|
|
76
|
+
* into the subprocess env here. The player's inner-loop publisher presents it
|
|
77
|
+
* on `POST /inner/ingest` + `GET /inner/presence` (loopback), where the daemon
|
|
78
|
+
* validates it against the URL-derived workflowId (cross-player-spoof guard).
|
|
79
|
+
* Absent → the publisher's HTTP client is a no-op (no fine-tail forwarding).
|
|
80
|
+
*/
|
|
81
|
+
readonly INGEST_TOKEN: "AGENT_TEMPO_INGEST_TOKEN";
|
|
51
82
|
/**
|
|
52
83
|
* v0.25 PR-D attachment resume plumbing. When `restart` / `migrate`
|
|
53
84
|
* enqueues a spawn outbox entry, the workflow passes the pre-claimed
|
|
@@ -72,6 +103,16 @@ export declare const ENV: {
|
|
|
72
103
|
readonly DAEMON_PORT: "AGENT_TEMPO_DAEMON_PORT";
|
|
73
104
|
readonly CORS_ORIGINS: "AGENT_TEMPO_CORS_ORIGINS";
|
|
74
105
|
readonly SSE_MAX_CONNECTIONS: "AGENT_TEMPO_SSE_MAX_CONNECTIONS";
|
|
106
|
+
/**
|
|
107
|
+
* 3e RBAC (MD-E). Two-token model: the READ token (T1 — observe) may live in
|
|
108
|
+
* env or config.json and auto-generates; the ADMIN token (T1+T2+T3 — mutate +
|
|
109
|
+
* supervisory gate/inner) is ENV-VAR-ONLY (never config.json/disk, never
|
|
110
|
+
* auto-generated). `TLS_ACKNOWLEDGED=1` suppresses the non-loopback-bind
|
|
111
|
+
* plaintext-HTTP startup warning.
|
|
112
|
+
*/
|
|
113
|
+
readonly HTTP_READ_TOKEN: "AGENT_TEMPO_HTTP_READ_TOKEN";
|
|
114
|
+
readonly HTTP_ADMIN_TOKEN: "AGENT_TEMPO_HTTP_ADMIN_TOKEN";
|
|
115
|
+
readonly TLS_ACKNOWLEDGED: "AGENT_TEMPO_TLS_ACKNOWLEDGED";
|
|
75
116
|
/**
|
|
76
117
|
* Dev profile gate (ADR 0014 §5.2). One source of truth — every layer
|
|
77
118
|
* (paths, namespace, port, task queue, banner, registry gating) consults
|
|
@@ -115,8 +156,19 @@ export interface PersistedConfig {
|
|
|
115
156
|
* a request with a non-loopback `Origin`) and no token is set:
|
|
116
157
|
* `crypto.randomBytes(32).toString('base64url')`, 0600 on POSIX.
|
|
117
158
|
* Rotation = delete this field; next daemon boot regenerates.
|
|
159
|
+
*
|
|
160
|
+
* 3e: this LEGACY single token is migrated to the READ tier (T1) — a daemon
|
|
161
|
+
* with only `httpToken` set keeps read access and emits a one-time startup
|
|
162
|
+
* warning to set an admin token for writes/gate/inner. Prefer `readToken`.
|
|
118
163
|
*/
|
|
119
164
|
httpToken?: string;
|
|
165
|
+
/**
|
|
166
|
+
* 3e RBAC — the READ-tier (T1) bearer token. Env `AGENT_TEMPO_HTTP_READ_TOKEN`
|
|
167
|
+
* takes precedence over this; auto-generated here on first bearer-mode boot if
|
|
168
|
+
* neither is set. The ADMIN token is deliberately ABSENT from this file (it is
|
|
169
|
+
* env-var-only, never persisted).
|
|
170
|
+
*/
|
|
171
|
+
readToken?: string;
|
|
120
172
|
}
|
|
121
173
|
/**
|
|
122
174
|
* Dev profile defaults — one switch (`--dev` top-level flag, or
|
|
@@ -278,6 +330,33 @@ export declare function parseTemporalYaml(content: string): PersistedConfig;
|
|
|
278
330
|
* for empty/unset values so callers can use it as a source-aware default.
|
|
279
331
|
*/
|
|
280
332
|
export declare function parseAgent(value: string | undefined, source: ConfigSource): AgentType;
|
|
333
|
+
/**
|
|
334
|
+
* Result of {@link parsePiProviderModel}: the parsed parts, OR an `{ error }`
|
|
335
|
+
* describing why the selector is malformed. Non-throwing by design — a pure
|
|
336
|
+
* mapper returning a discriminated union (the recruit wiring branches
|
|
337
|
+
* `if ('error' in r) return fail(r.error)`, no try/catch).
|
|
338
|
+
*/
|
|
339
|
+
export type ProviderModel = {
|
|
340
|
+
provider: string;
|
|
341
|
+
model: string;
|
|
342
|
+
} | {
|
|
343
|
+
error: string;
|
|
344
|
+
};
|
|
345
|
+
/**
|
|
346
|
+
* Parse a Pi provider/model selector (e.g. `"github-copilot/gpt-4o"`) into its
|
|
347
|
+
* `{ provider, model }` parts for Pi's `createAgentSession` model option.
|
|
348
|
+
*
|
|
349
|
+
* Provider-agnostic: the segment before the FIRST `/` is the provider id,
|
|
350
|
+
* passed through VERBATIM (Copilot's pi-ai provider id is literally
|
|
351
|
+
* `github-copilot` — no normalization needed); everything after is the model
|
|
352
|
+
* id, which may itself contain `/` (e.g. `openrouter/anthropic/claude`).
|
|
353
|
+
*
|
|
354
|
+
* Fail-loud (no silent default): returns `{ error }` — never a fallback model —
|
|
355
|
+
* when the selector has no `/`, an empty provider, or an empty model. A bare
|
|
356
|
+
* provider with no model is rejected here; omitting the recruit `model` arg
|
|
357
|
+
* ENTIRELY is a different path (Pi's own default), handled upstream, not here.
|
|
358
|
+
*/
|
|
359
|
+
export declare function parsePiProviderModel(model: string): ProviderModel;
|
|
281
360
|
/** CLI flag overrides — passed down from the arg parser. */
|
|
282
361
|
export interface CliOverrides {
|
|
283
362
|
temporalAddress?: string;
|
package/dist/config.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.saveConfigFile = saveConfigFile;
|
|
|
11
11
|
exports.loadTemporalCliConfig = loadTemporalCliConfig;
|
|
12
12
|
exports.parseTemporalYaml = parseTemporalYaml;
|
|
13
13
|
exports.parseAgent = parseAgent;
|
|
14
|
+
exports.parsePiProviderModel = parsePiProviderModel;
|
|
14
15
|
exports.getConfig = getConfig;
|
|
15
16
|
exports.getConfigWithSources = getConfigWithSources;
|
|
16
17
|
exports.hostTaskQueue = hostTaskQueue;
|
|
@@ -78,6 +79,37 @@ exports.ENV = {
|
|
|
78
79
|
* sandboxed contexts. Mutually exclusive with {@link PERMISSION_MODE}.
|
|
79
80
|
*/
|
|
80
81
|
DANGEROUSLY_SKIP_PERMISSIONS: 'AGENT_TEMPO_DANGEROUSLY_SKIP_PERMISSIONS',
|
|
82
|
+
/**
|
|
83
|
+
* Phase 3a — headless Pi runtime model selector. Pi takes a `provider/model`
|
|
84
|
+
* string (e.g. `anthropic/claude-opus-4-7`); absent → Pi's own default
|
|
85
|
+
* provider/model (the 3a anthropic-default path). Recruit `model` arg →
|
|
86
|
+
* this env → Pi default.
|
|
87
|
+
*/
|
|
88
|
+
PI_MODEL: 'AGENT_TEMPO_PI_MODEL',
|
|
89
|
+
/**
|
|
90
|
+
* Phase 3a — headless Pi restart-resume. The daemon reads `metadata.sessionId`
|
|
91
|
+
* (the Pi conversation id the player was in when it died) and passes it here;
|
|
92
|
+
* the headless entry resumes via Pi `continueSession(<id>)`. Absent on a fresh
|
|
93
|
+
* recruit → a new Pi session.
|
|
94
|
+
*/
|
|
95
|
+
PI_CONTINUE_SESSION: 'AGENT_TEMPO_PI_CONTINUE_SESSION',
|
|
96
|
+
/**
|
|
97
|
+
* Phase 3a / MD-C — headless Pi tool-access policy. One of
|
|
98
|
+
* `restricted` (default; Bash/shell/exec HARD-BLOCKED) | `standard` (scoped
|
|
99
|
+
* Bash) | `full` (unsandboxed; admin-gated at recruit). Read by the Pi
|
|
100
|
+
* extension's `tool_call` gate (mode='headless' only). Mirrors
|
|
101
|
+
* {@link PERMISSION_MODE}'s threading.
|
|
102
|
+
*/
|
|
103
|
+
TOOL_ACCESS: 'AGENT_TEMPO_TOOL_ACCESS',
|
|
104
|
+
/**
|
|
105
|
+
* 3c Tier-2 ingest auth. The daemon mints a per-player ingest token (scoped to
|
|
106
|
+
* the session workflowId) BEFORE spawning a headless Pi player and threads it
|
|
107
|
+
* into the subprocess env here. The player's inner-loop publisher presents it
|
|
108
|
+
* on `POST /inner/ingest` + `GET /inner/presence` (loopback), where the daemon
|
|
109
|
+
* validates it against the URL-derived workflowId (cross-player-spoof guard).
|
|
110
|
+
* Absent → the publisher's HTTP client is a no-op (no fine-tail forwarding).
|
|
111
|
+
*/
|
|
112
|
+
INGEST_TOKEN: 'AGENT_TEMPO_INGEST_TOKEN',
|
|
81
113
|
/**
|
|
82
114
|
* v0.25 PR-D attachment resume plumbing. When `restart` / `migrate`
|
|
83
115
|
* enqueues a spawn outbox entry, the workflow passes the pre-claimed
|
|
@@ -102,6 +134,16 @@ exports.ENV = {
|
|
|
102
134
|
DAEMON_PORT: 'AGENT_TEMPO_DAEMON_PORT',
|
|
103
135
|
CORS_ORIGINS: 'AGENT_TEMPO_CORS_ORIGINS',
|
|
104
136
|
SSE_MAX_CONNECTIONS: 'AGENT_TEMPO_SSE_MAX_CONNECTIONS',
|
|
137
|
+
/**
|
|
138
|
+
* 3e RBAC (MD-E). Two-token model: the READ token (T1 — observe) may live in
|
|
139
|
+
* env or config.json and auto-generates; the ADMIN token (T1+T2+T3 — mutate +
|
|
140
|
+
* supervisory gate/inner) is ENV-VAR-ONLY (never config.json/disk, never
|
|
141
|
+
* auto-generated). `TLS_ACKNOWLEDGED=1` suppresses the non-loopback-bind
|
|
142
|
+
* plaintext-HTTP startup warning.
|
|
143
|
+
*/
|
|
144
|
+
HTTP_READ_TOKEN: 'AGENT_TEMPO_HTTP_READ_TOKEN',
|
|
145
|
+
HTTP_ADMIN_TOKEN: 'AGENT_TEMPO_HTTP_ADMIN_TOKEN',
|
|
146
|
+
TLS_ACKNOWLEDGED: 'AGENT_TEMPO_TLS_ACKNOWLEDGED',
|
|
105
147
|
/**
|
|
106
148
|
* Dev profile gate (ADR 0014 §5.2). One source of truth — every layer
|
|
107
149
|
* (paths, namespace, port, task queue, banner, registry gating) consults
|
|
@@ -420,6 +462,38 @@ function parseAgent(value, source) {
|
|
|
420
462
|
}
|
|
421
463
|
return value;
|
|
422
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Parse a Pi provider/model selector (e.g. `"github-copilot/gpt-4o"`) into its
|
|
467
|
+
* `{ provider, model }` parts for Pi's `createAgentSession` model option.
|
|
468
|
+
*
|
|
469
|
+
* Provider-agnostic: the segment before the FIRST `/` is the provider id,
|
|
470
|
+
* passed through VERBATIM (Copilot's pi-ai provider id is literally
|
|
471
|
+
* `github-copilot` — no normalization needed); everything after is the model
|
|
472
|
+
* id, which may itself contain `/` (e.g. `openrouter/anthropic/claude`).
|
|
473
|
+
*
|
|
474
|
+
* Fail-loud (no silent default): returns `{ error }` — never a fallback model —
|
|
475
|
+
* when the selector has no `/`, an empty provider, or an empty model. A bare
|
|
476
|
+
* provider with no model is rejected here; omitting the recruit `model` arg
|
|
477
|
+
* ENTIRELY is a different path (Pi's own default), handled upstream, not here.
|
|
478
|
+
*/
|
|
479
|
+
function parsePiProviderModel(model) {
|
|
480
|
+
const raw = model.trim();
|
|
481
|
+
const slash = raw.indexOf('/');
|
|
482
|
+
if (slash < 0) {
|
|
483
|
+
return {
|
|
484
|
+
error: `model "${model}" must be a "provider/model" selector (e.g. "github-copilot/gpt-4o") — no "/" found.`,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const provider = raw.slice(0, slash).trim();
|
|
488
|
+
const modelId = raw.slice(slash + 1).trim();
|
|
489
|
+
if (!provider) {
|
|
490
|
+
return { error: `model "${model}" has an empty provider before "/" — expected e.g. "github-copilot/gpt-4o".` };
|
|
491
|
+
}
|
|
492
|
+
if (!modelId) {
|
|
493
|
+
return { error: `model "${model}" has an empty model after "/" — specify a model, e.g. "github-copilot/gpt-4o".` };
|
|
494
|
+
}
|
|
495
|
+
return { provider, model: modelId };
|
|
496
|
+
}
|
|
423
497
|
/**
|
|
424
498
|
* Resolve `defaultAgent` through the standard precedence chain and validate
|
|
425
499
|
* against the {@link AgentType} union. Each step passes its own source tag
|
package/dist/daemon.js
CHANGED
|
@@ -66,6 +66,11 @@ const config_1 = require("./config");
|
|
|
66
66
|
const dev_banner_1 = require("./cli/dev-banner");
|
|
67
67
|
const worker_1 = require("./worker");
|
|
68
68
|
const connection_1 = require("./connection");
|
|
69
|
+
const inner_loop_1 = require("./http/inner-loop");
|
|
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
|
+
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
69
74
|
const daemon_1 = require("./cli/daemon");
|
|
70
75
|
const client_3 = require("./client");
|
|
71
76
|
const orphans_1 = require("./reconcile/orphans");
|
|
@@ -684,6 +689,10 @@ function startCleanupLoop(client, daemonConfig, hostname = os.hostname()) {
|
|
|
684
689
|
};
|
|
685
690
|
}
|
|
686
691
|
async function main() {
|
|
692
|
+
// Neutralize the Temporal/grpc-js "Channel has been shut down" retry-after-
|
|
693
|
+
// close race so a stray retry timer can't kill the long-lived daemon. See
|
|
694
|
+
// src/utils/grpc-shutdown-guard.ts.
|
|
695
|
+
(0, grpc_shutdown_guard_1.installGrpcShutdownGuard)();
|
|
687
696
|
// ADR 0014 §5.4 / gate 4 — dev daemon log self-identifies. Banner fires
|
|
688
697
|
// first so it lands at the top of `~/.agent-tempo-dev/daemon.log` for
|
|
689
698
|
// grep-friendly identification regardless of subsequent log volume.
|
|
@@ -823,6 +832,19 @@ async function main() {
|
|
|
823
832
|
// #94/#95 PR-2 — aggregate poll loop + per-ensemble buses. Owned by
|
|
824
833
|
// the daemon process; `close()` drains every per-ensemble bus.
|
|
825
834
|
let aggregateRunner = null;
|
|
835
|
+
// 3c Tier-2 — daemon-owned singletons shared between the Temporal worker
|
|
836
|
+
// (outbox pi-spawn mints / destroy revokes) and the HTTP server (/inner SSE
|
|
837
|
+
// + /inner/ingest validation). Both the worker and startHttpServer run in
|
|
838
|
+
// THIS process, so one instance each suffices. Constructed eagerly (no I/O)
|
|
839
|
+
// so the shutdown handler — declared just below — can drain them.
|
|
840
|
+
const innerLoop = new inner_loop_1.InnerLoopRegistry();
|
|
841
|
+
const ingestTokens = new ingest_registry_1.IngestTokenRegistry();
|
|
842
|
+
// 3d MD-G — the operator-gate registry, same daemon-owned-singleton pattern.
|
|
843
|
+
// Audit sink = the append-only JSONL writer; publishToInner = innerLoop.publish
|
|
844
|
+
// (the DI that emits gate_resolved on the player's /inner stream without a
|
|
845
|
+
// GateRegistry↔inner-loop circular import).
|
|
846
|
+
const gate = new gate_registry_1.GateRegistry((0, gate_audit_1.createGateAuditSink)(), Date.now, undefined, // default 45s auto-allow
|
|
847
|
+
(workflowId, frame) => innerLoop.publish(workflowId, frame));
|
|
826
848
|
const shutdown = () => {
|
|
827
849
|
if (shuttingDown)
|
|
828
850
|
return;
|
|
@@ -856,6 +878,12 @@ async function main() {
|
|
|
856
878
|
// sockets — preventing wasted work in the drain window.
|
|
857
879
|
aggregateRunner?.close();
|
|
858
880
|
httpServerHandle?.close().catch((err) => log('http close error (non-fatal):', err instanceof Error ? err.message : err));
|
|
881
|
+
// 3c Tier-2 — clear-all on shutdown: drop every minted ingest token (no
|
|
882
|
+
// dead token outlives the daemon) and close every open /inner subscriber
|
|
883
|
+
// (streams end cleanly rather than dangling).
|
|
884
|
+
ingestTokens.revokeAll();
|
|
885
|
+
innerLoop.close();
|
|
886
|
+
gate.clear();
|
|
859
887
|
sharedWorker?.shutdown();
|
|
860
888
|
hostWorker?.shutdown();
|
|
861
889
|
};
|
|
@@ -863,7 +891,7 @@ async function main() {
|
|
|
863
891
|
process.on('SIGINT', shutdown);
|
|
864
892
|
// Create workers (signal handlers already active via mutable refs)
|
|
865
893
|
log(`Connecting to Temporal at ${config.temporalAddress} (namespace: ${config.temporalNamespace})`);
|
|
866
|
-
const workers = await (0, worker_1.createWorkers)(config);
|
|
894
|
+
const workers = await (0, worker_1.createWorkers)(config, ingestTokens, gate);
|
|
867
895
|
sharedWorker = workers.sharedWorker;
|
|
868
896
|
hostWorker = workers.hostWorker;
|
|
869
897
|
log('Workers created — processing tasks');
|
|
@@ -948,6 +976,14 @@ async function main() {
|
|
|
948
976
|
taskQueue: config.taskQueue,
|
|
949
977
|
version: daemonVersion(),
|
|
950
978
|
aggregate: aggregateRunner,
|
|
979
|
+
// 3c Tier-2 — same singletons the worker's outbox activities use, so
|
|
980
|
+
// the operator /inner SSE reads the registry the publisher POSTs into
|
|
981
|
+
// and /inner/ingest validates against the tokens the spawn path minted.
|
|
982
|
+
innerLoop,
|
|
983
|
+
ingestTokens,
|
|
984
|
+
// 3d MD-G — the same gate registry the worker's outbox auto-disarms on
|
|
985
|
+
// detach/destroy; the HTTP server serves arm/disarm/decide + resolution.
|
|
986
|
+
gate,
|
|
951
987
|
});
|
|
952
988
|
log(`HTTP listening on http://${httpServerHandle.bindAddr}:${httpServerHandle.port}`);
|
|
953
989
|
log(`Aggregate poll loop running (bootEpoch=${bootEpoch})`);
|
package/dist/http/aggregate.d.ts
CHANGED
|
@@ -41,6 +41,21 @@ export declare class TickSkipped extends Error {
|
|
|
41
41
|
readonly name = "TickSkipped";
|
|
42
42
|
constructor(reason: string);
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* 3c Tier-1 coarse activity snapshot for a single player — the bits the
|
|
46
|
+
* aggregate diffs to decide whether to emit `player.activity`. `currentTool`
|
|
47
|
+
* is normalized to `null` when idle/absent; the context fields are `undefined`
|
|
48
|
+
* when Pi can't report usage (e.g. right after compaction).
|
|
49
|
+
*/
|
|
50
|
+
export interface PlayerCoarse {
|
|
51
|
+
currentTool: string | null;
|
|
52
|
+
contextTokens?: number;
|
|
53
|
+
contextPercent?: number;
|
|
54
|
+
}
|
|
55
|
+
/** Extract the coarse-activity tuple from a player summary, normalizing idle → null. */
|
|
56
|
+
export declare function coarseOf(p: PlayerSummaryV1): PlayerCoarse;
|
|
57
|
+
/** True when two coarse snapshots differ in any field. */
|
|
58
|
+
export declare function coarseChanged(a: PlayerCoarse | undefined, b: PlayerCoarse): boolean;
|
|
44
59
|
/** Per-ensemble tracking state across ticks. */
|
|
45
60
|
interface EnsembleTrack {
|
|
46
61
|
bus: EnsembleEventBus;
|
|
@@ -56,6 +71,12 @@ interface EnsembleTrack {
|
|
|
56
71
|
consecutiveFailures: number;
|
|
57
72
|
/** Last seen player phase, keyed by playerId. */
|
|
58
73
|
playerPhases: Map<string, string | undefined>;
|
|
74
|
+
/**
|
|
75
|
+
* 3c Tier-1 — last-seen coarse activity per playerId (currentTool + context
|
|
76
|
+
* usage). Diffed each poll to emit `player.activity` on change. Seeded on
|
|
77
|
+
* `player.added`, dropped on `player.removed` — lockstep with `playerPhases`.
|
|
78
|
+
*/
|
|
79
|
+
playerCoarse: Map<string, PlayerCoarse>;
|
|
59
80
|
/**
|
|
60
81
|
* Adapter family per playerId — used to faithfully reconstruct the
|
|
61
82
|
* prior `AggregateEnsembleSnapshot` view at tick boundaries (see #535).
|
|
@@ -175,7 +196,7 @@ export interface DiffEvent {
|
|
|
175
196
|
type: import('./event-types').SseEventKind;
|
|
176
197
|
payload: unknown;
|
|
177
198
|
}
|
|
178
|
-
export declare function diffEnsembleSnapshot(prev: AggregateEnsembleSnapshot | null, next: AggregateEnsembleSnapshot, track: Pick<EnsembleTrack, 'playerPhases' | 'playerAgentTypes' | 'flags' | 'schedulesHash' | 'chatIds' | 'chatIdOrder'>, capturedAt: string): DiffEvent[];
|
|
199
|
+
export declare function diffEnsembleSnapshot(prev: AggregateEnsembleSnapshot | null, next: AggregateEnsembleSnapshot, track: Pick<EnsembleTrack, 'playerPhases' | 'playerCoarse' | 'playerAgentTypes' | 'flags' | 'schedulesHash' | 'chatIds' | 'chatIdOrder'>, capturedAt: string): DiffEvent[];
|
|
179
200
|
/**
|
|
180
201
|
* Diff host profiles map → per-host events. Pure function.
|
|
181
202
|
*/
|
package/dist/http/aggregate.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.AggregateRunner = exports.TickSkipped = exports.MAX_CONSECUTIVE_FAILURES = exports.AGGREGATE_LIST_DEADLINE_MS = exports.DEFAULT_POLL_INTERVAL_MS = void 0;
|
|
4
|
+
exports.coarseOf = coarseOf;
|
|
5
|
+
exports.coarseChanged = coarseChanged;
|
|
4
6
|
exports.canonicalize = canonicalize;
|
|
5
7
|
exports.hashOf = hashOf;
|
|
6
8
|
exports.diffEnsembleSnapshot = diffEnsembleSnapshot;
|
|
@@ -84,6 +86,21 @@ class TickSkipped extends Error {
|
|
|
84
86
|
constructor(reason) { super(reason); }
|
|
85
87
|
}
|
|
86
88
|
exports.TickSkipped = TickSkipped;
|
|
89
|
+
/** Extract the coarse-activity tuple from a player summary, normalizing idle → null. */
|
|
90
|
+
function coarseOf(p) {
|
|
91
|
+
return {
|
|
92
|
+
currentTool: p.currentTool ?? null,
|
|
93
|
+
...(p.contextTokens !== undefined ? { contextTokens: p.contextTokens } : {}),
|
|
94
|
+
...(p.contextPercent !== undefined ? { contextPercent: p.contextPercent } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** True when two coarse snapshots differ in any field. */
|
|
98
|
+
function coarseChanged(a, b) {
|
|
99
|
+
return (!a ||
|
|
100
|
+
a.currentTool !== b.currentTool ||
|
|
101
|
+
a.contextTokens !== b.contextTokens ||
|
|
102
|
+
a.contextPercent !== b.contextPercent);
|
|
103
|
+
}
|
|
87
104
|
/**
|
|
88
105
|
* Stable JSON canonicalization — keys sorted, no extraneous whitespace.
|
|
89
106
|
* Used for SHA-256 diff suppression so reordered key emits don't
|
|
@@ -118,6 +135,9 @@ function diffEnsembleSnapshot(prev, next, track, capturedAt) {
|
|
|
118
135
|
if (!prevPlayers.has(p.playerId)) {
|
|
119
136
|
events.push({ type: 'player.added', payload: p });
|
|
120
137
|
track.playerPhases.set(p.playerId, p.phase);
|
|
138
|
+
// 3c — seed coarse so the first real change (not the add itself, whose
|
|
139
|
+
// payload already carries currentTool/context) emits player.activity.
|
|
140
|
+
track.playerCoarse.set(p.playerId, coarseOf(p));
|
|
121
141
|
// #535 — record the adapter family so the prior reconstruction at
|
|
122
142
|
// the next tick (aggregate.ts ~L600) carries the real agentType
|
|
123
143
|
// instead of a hardcoded `'claude'` stand-in. Treated as immutable
|
|
@@ -140,6 +160,25 @@ function diffEnsembleSnapshot(prev, next, track, capturedAt) {
|
|
|
140
160
|
});
|
|
141
161
|
track.playerPhases.set(p.playerId, p.phase);
|
|
142
162
|
}
|
|
163
|
+
// player.activity (3c Tier-1) — emit when coarse currentTool/context
|
|
164
|
+
// changes between polls. Distinct from phase_changed (phase vs activity).
|
|
165
|
+
// Source freshness ~30s (heartbeat metadata piggyback); the live, fine
|
|
166
|
+
// tail is the off-wire /inner side-channel.
|
|
167
|
+
const nextCoarse = coarseOf(p);
|
|
168
|
+
if (coarseChanged(track.playerCoarse.get(p.playerId), nextCoarse)) {
|
|
169
|
+
events.push({
|
|
170
|
+
type: 'player.activity',
|
|
171
|
+
payload: {
|
|
172
|
+
playerId: p.playerId,
|
|
173
|
+
ensemble,
|
|
174
|
+
currentTool: nextCoarse.currentTool,
|
|
175
|
+
...(nextCoarse.contextTokens !== undefined ? { contextTokens: nextCoarse.contextTokens } : {}),
|
|
176
|
+
...(nextCoarse.contextPercent !== undefined ? { contextPercent: nextCoarse.contextPercent } : {}),
|
|
177
|
+
at: capturedAt,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
track.playerCoarse.set(p.playerId, nextCoarse);
|
|
181
|
+
}
|
|
143
182
|
}
|
|
144
183
|
// player.removed — iterate prev.
|
|
145
184
|
if (prev) {
|
|
@@ -159,6 +198,7 @@ function diffEnsembleSnapshot(prev, next, track, capturedAt) {
|
|
|
159
198
|
},
|
|
160
199
|
});
|
|
161
200
|
track.playerPhases.delete(p.playerId);
|
|
201
|
+
track.playerCoarse.delete(p.playerId);
|
|
162
202
|
track.playerAgentTypes.delete(p.playerId);
|
|
163
203
|
}
|
|
164
204
|
}
|
|
@@ -377,6 +417,7 @@ class AggregateRunner {
|
|
|
377
417
|
bus,
|
|
378
418
|
consecutiveFailures: 0,
|
|
379
419
|
playerPhases: new Map(),
|
|
420
|
+
playerCoarse: new Map(),
|
|
380
421
|
playerAgentTypes: new Map(),
|
|
381
422
|
flags: null,
|
|
382
423
|
schedulesHash: null,
|
package/dist/http/auth.d.ts
CHANGED
|
@@ -51,17 +51,103 @@ export declare function extractBearerToken(authHeader: string | undefined): stri
|
|
|
51
51
|
*/
|
|
52
52
|
export declare function tokensMatch(received: string, expected: string): boolean;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* `httpToken`, generate one (`crypto.randomBytes(32).toString('base64url')`)
|
|
58
|
-
* and persist it via `saveConfigFile` (which sets 0600 on POSIX).
|
|
59
|
-
* - When `bearerRequired` is false, return whatever is in the config
|
|
60
|
-
* without generating — operators may still want a token saved for
|
|
61
|
-
* future use, and we shouldn't write secrets the user didn't request.
|
|
54
|
+
* @deprecated 3e — superseded by {@link loadRbacTokens} (read + admin split).
|
|
55
|
+
* Kept only until every caller migrates; do NOT add new callers. Resolves the
|
|
56
|
+
* legacy single `httpToken` (env-less) for back-compat shims.
|
|
62
57
|
*/
|
|
63
58
|
export declare function loadOrGenerateHttpToken(opts: {
|
|
64
59
|
bearerRequired: boolean;
|
|
65
60
|
load?: () => PersistedConfig;
|
|
66
61
|
save?: (cfg: PersistedConfig) => void;
|
|
67
62
|
}): string | null;
|
|
63
|
+
/** Resolved RBAC tokens for the daemon HTTP surface (3e). */
|
|
64
|
+
export interface RbacTokens {
|
|
65
|
+
/** T1 read token (env > config.json > auto-gen), or `null` when none + not required. */
|
|
66
|
+
readToken: string | null;
|
|
67
|
+
/** T1+T2+T3 admin token — ENV-VAR-ONLY, or `null` when unset (→ 503 on T≥2). */
|
|
68
|
+
adminToken: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* True when a LEGACY `httpToken` (no `readToken`) was adopted as the read token.
|
|
71
|
+
* The daemon emits a one-time startup warning so the operator sets an admin token.
|
|
72
|
+
*/
|
|
73
|
+
legacyMigrated: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** Admin token — ENV-VAR-ONLY (never config.json/disk, never auto-generated). */
|
|
76
|
+
export declare function loadAdminToken(env?: NodeJS.ProcessEnv): string | null;
|
|
77
|
+
/**
|
|
78
|
+
* Read token (T1). Priority: env `AGENT_TEMPO_HTTP_READ_TOKEN` > config.json
|
|
79
|
+
* `readToken` > LEGACY config.json `httpToken` (adopted → `legacy:true`) >
|
|
80
|
+
* auto-generate when bearer mode is required (persisted as `readToken`).
|
|
81
|
+
*/
|
|
82
|
+
export declare function loadReadToken(opts: {
|
|
83
|
+
bearerRequired: boolean;
|
|
84
|
+
env?: NodeJS.ProcessEnv;
|
|
85
|
+
load?: () => PersistedConfig;
|
|
86
|
+
save?: (cfg: PersistedConfig) => void;
|
|
87
|
+
}): {
|
|
88
|
+
token: string | null;
|
|
89
|
+
legacy: boolean;
|
|
90
|
+
};
|
|
91
|
+
/** Load both RBAC tokens. The daemon calls this once at startup. */
|
|
92
|
+
export declare function loadRbacTokens(opts: {
|
|
93
|
+
bearerRequired: boolean;
|
|
94
|
+
env?: NodeJS.ProcessEnv;
|
|
95
|
+
load?: () => PersistedConfig;
|
|
96
|
+
save?: (cfg: PersistedConfig) => void;
|
|
97
|
+
}): RbacTokens;
|
|
98
|
+
/** Access tiers (MD-E): 1 = read/observe, 2 = write/mutate, 3 = supervisory (gate/inner). */
|
|
99
|
+
export type Tier = 1 | 2 | 3;
|
|
100
|
+
/**
|
|
101
|
+
* Granted tier for a presented bearer, given the RBAC tokens (timing-safe).
|
|
102
|
+
* Admin grants the FULL ladder (3 ⊇ 2 ⊇ 1); read grants T1 only; no match → 0.
|
|
103
|
+
* There is no T2-only token — T2 and T3 are both "admin required".
|
|
104
|
+
*/
|
|
105
|
+
export declare function tierForToken(presented: string, tokens: {
|
|
106
|
+
readToken: string | null;
|
|
107
|
+
adminToken: string | null;
|
|
108
|
+
}): 0 | 1 | 3;
|
|
109
|
+
export interface TierGuardInput {
|
|
110
|
+
/** Daemon bind address — decides loopback trust. */
|
|
111
|
+
bindAddr: string;
|
|
112
|
+
/** Request `Origin` header (may be absent for a non-browser client). */
|
|
113
|
+
originHeader: string | undefined;
|
|
114
|
+
/** Request `Authorization` header. */
|
|
115
|
+
authHeader: string | undefined;
|
|
116
|
+
/** Read-tier token, or `null`. */
|
|
117
|
+
readToken: string | null;
|
|
118
|
+
/** Admin token, or `null` when unset (→ 503 on T≥2). */
|
|
119
|
+
adminToken: string | null;
|
|
120
|
+
}
|
|
121
|
+
export type TierGuardResult = {
|
|
122
|
+
ok: true;
|
|
123
|
+
} | {
|
|
124
|
+
ok: false;
|
|
125
|
+
status: 401;
|
|
126
|
+
error: 'unauthorized';
|
|
127
|
+
} | {
|
|
128
|
+
ok: false;
|
|
129
|
+
status: 403;
|
|
130
|
+
error: 'insufficient-tier';
|
|
131
|
+
detail: string;
|
|
132
|
+
} | {
|
|
133
|
+
ok: false;
|
|
134
|
+
status: 503;
|
|
135
|
+
error: 'admin-token-not-configured';
|
|
136
|
+
detail: string;
|
|
137
|
+
};
|
|
138
|
+
/**
|
|
139
|
+
* Authorization guard (3e MD-E). Assumes the shared upstream pass already settled
|
|
140
|
+
* AUTHENTICATION + CORS + the DNS-rebind/Origin defense (architect's Layer 2);
|
|
141
|
+
* this is Layer-3 authZ — it ONLY decides tier ≥ N and emits 503/403/401.
|
|
142
|
+
*
|
|
143
|
+
* Matrix (bearer mode):
|
|
144
|
+
* - loopback (`!bearerRequired`) → PASS all tiers (local-trust short-circuit).
|
|
145
|
+
* - N ≥ 2 AND adminToken unset → 503 admin-token-not-configured.
|
|
146
|
+
* - no/invalid bearer (granted 0) → 401 unauthorized.
|
|
147
|
+
* - granted < N (read token on T≥2) → 403 insufficient-tier (+ migration hint).
|
|
148
|
+
* - granted ≥ N (admin, or read on T1) → PASS.
|
|
149
|
+
*
|
|
150
|
+
* Keyed off the `Authorization` bearer only (no `Origin`/cookie requirement) so a
|
|
151
|
+
* headless Node client passes once its token validates. View-agnostic by design.
|
|
152
|
+
*/
|
|
153
|
+
export declare function requireTier(tier: Tier, input: TierGuardInput): TierGuardResult;
|
package/dist/http/auth.js
CHANGED
|
@@ -39,6 +39,11 @@ exports.bearerRequired = bearerRequired;
|
|
|
39
39
|
exports.extractBearerToken = extractBearerToken;
|
|
40
40
|
exports.tokensMatch = tokensMatch;
|
|
41
41
|
exports.loadOrGenerateHttpToken = loadOrGenerateHttpToken;
|
|
42
|
+
exports.loadAdminToken = loadAdminToken;
|
|
43
|
+
exports.loadReadToken = loadReadToken;
|
|
44
|
+
exports.loadRbacTokens = loadRbacTokens;
|
|
45
|
+
exports.tierForToken = tierForToken;
|
|
46
|
+
exports.requireTier = requireTier;
|
|
42
47
|
/**
|
|
43
48
|
* Authentication for the daemon HTTP surface (SSE-PROTOCOL.md §3).
|
|
44
49
|
*
|
|
@@ -152,14 +157,9 @@ function tokensMatch(received, expected) {
|
|
|
152
157
|
return crypto.timingSafeEqual(a, b);
|
|
153
158
|
}
|
|
154
159
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* `httpToken`, generate one (`crypto.randomBytes(32).toString('base64url')`)
|
|
159
|
-
* and persist it via `saveConfigFile` (which sets 0600 on POSIX).
|
|
160
|
-
* - When `bearerRequired` is false, return whatever is in the config
|
|
161
|
-
* without generating — operators may still want a token saved for
|
|
162
|
-
* future use, and we shouldn't write secrets the user didn't request.
|
|
160
|
+
* @deprecated 3e — superseded by {@link loadRbacTokens} (read + admin split).
|
|
161
|
+
* Kept only until every caller migrates; do NOT add new callers. Resolves the
|
|
162
|
+
* legacy single `httpToken` (env-less) for back-compat shims.
|
|
163
163
|
*/
|
|
164
164
|
function loadOrGenerateHttpToken(opts) {
|
|
165
165
|
const load = opts.load ?? config_1.loadConfigFile;
|
|
@@ -170,8 +170,92 @@ function loadOrGenerateHttpToken(opts) {
|
|
|
170
170
|
}
|
|
171
171
|
if (!opts.bearerRequired)
|
|
172
172
|
return null;
|
|
173
|
-
// Auto-generate. base64url chars are safe inside Authorization values.
|
|
174
173
|
const token = crypto.randomBytes(32).toString('base64url');
|
|
175
174
|
save({ ...cfg, httpToken: token });
|
|
176
175
|
return token;
|
|
177
176
|
}
|
|
177
|
+
/** Admin token — ENV-VAR-ONLY (never config.json/disk, never auto-generated). */
|
|
178
|
+
function loadAdminToken(env = process.env) {
|
|
179
|
+
const t = env[config_1.ENV.HTTP_ADMIN_TOKEN];
|
|
180
|
+
return t && t.length > 0 ? t : null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Read token (T1). Priority: env `AGENT_TEMPO_HTTP_READ_TOKEN` > config.json
|
|
184
|
+
* `readToken` > LEGACY config.json `httpToken` (adopted → `legacy:true`) >
|
|
185
|
+
* auto-generate when bearer mode is required (persisted as `readToken`).
|
|
186
|
+
*/
|
|
187
|
+
function loadReadToken(opts) {
|
|
188
|
+
const env = opts.env ?? process.env;
|
|
189
|
+
const load = opts.load ?? config_1.loadConfigFile;
|
|
190
|
+
const save = opts.save ?? config_1.saveConfigFile;
|
|
191
|
+
const envTok = env[config_1.ENV.HTTP_READ_TOKEN];
|
|
192
|
+
if (envTok && envTok.length > 0)
|
|
193
|
+
return { token: envTok, legacy: false };
|
|
194
|
+
const cfg = load();
|
|
195
|
+
if (cfg.readToken && cfg.readToken.length > 0)
|
|
196
|
+
return { token: cfg.readToken, legacy: false };
|
|
197
|
+
// LEGACY: a pre-3e single `httpToken` becomes the READ token (T1) — NOT admin.
|
|
198
|
+
if (cfg.httpToken && cfg.httpToken.length > 0)
|
|
199
|
+
return { token: cfg.httpToken, legacy: true };
|
|
200
|
+
if (!opts.bearerRequired)
|
|
201
|
+
return { token: null, legacy: false };
|
|
202
|
+
const token = crypto.randomBytes(32).toString('base64url');
|
|
203
|
+
save({ ...cfg, readToken: token });
|
|
204
|
+
return { token, legacy: false };
|
|
205
|
+
}
|
|
206
|
+
/** Load both RBAC tokens. The daemon calls this once at startup. */
|
|
207
|
+
function loadRbacTokens(opts) {
|
|
208
|
+
const { token: readToken, legacy } = loadReadToken(opts);
|
|
209
|
+
const adminToken = loadAdminToken(opts.env);
|
|
210
|
+
return { readToken, adminToken, legacyMigrated: legacy };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Granted tier for a presented bearer, given the RBAC tokens (timing-safe).
|
|
214
|
+
* Admin grants the FULL ladder (3 ⊇ 2 ⊇ 1); read grants T1 only; no match → 0.
|
|
215
|
+
* There is no T2-only token — T2 and T3 are both "admin required".
|
|
216
|
+
*/
|
|
217
|
+
function tierForToken(presented, tokens) {
|
|
218
|
+
if (tokens.adminToken && tokensMatch(presented, tokens.adminToken))
|
|
219
|
+
return 3;
|
|
220
|
+
if (tokens.readToken && tokensMatch(presented, tokens.readToken))
|
|
221
|
+
return 1;
|
|
222
|
+
return 0;
|
|
223
|
+
}
|
|
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.';
|
|
227
|
+
/**
|
|
228
|
+
* Authorization guard (3e MD-E). Assumes the shared upstream pass already settled
|
|
229
|
+
* AUTHENTICATION + CORS + the DNS-rebind/Origin defense (architect's Layer 2);
|
|
230
|
+
* this is Layer-3 authZ — it ONLY decides tier ≥ N and emits 503/403/401.
|
|
231
|
+
*
|
|
232
|
+
* Matrix (bearer mode):
|
|
233
|
+
* - loopback (`!bearerRequired`) → PASS all tiers (local-trust short-circuit).
|
|
234
|
+
* - N ≥ 2 AND adminToken unset → 503 admin-token-not-configured.
|
|
235
|
+
* - no/invalid bearer (granted 0) → 401 unauthorized.
|
|
236
|
+
* - granted < N (read token on T≥2) → 403 insufficient-tier (+ migration hint).
|
|
237
|
+
* - granted ≥ N (admin, or read on T1) → PASS.
|
|
238
|
+
*
|
|
239
|
+
* Keyed off the `Authorization` bearer only (no `Origin`/cookie requirement) so a
|
|
240
|
+
* headless Node client passes once its token validates. View-agnostic by design.
|
|
241
|
+
*/
|
|
242
|
+
function requireTier(tier, input) {
|
|
243
|
+
// Loopback trust: local clients (TUI / CLI / future Pi widget) get full access.
|
|
244
|
+
if (!bearerRequired(input.bindAddr, input.originHeader))
|
|
245
|
+
return { ok: true };
|
|
246
|
+
// A tier that needs admin is UNAVAILABLE when no admin token is configured —
|
|
247
|
+
// the honest answer is 503, regardless of what the caller presents.
|
|
248
|
+
if (tier >= 2 && input.adminToken === null) {
|
|
249
|
+
return { ok: false, status: 503, error: 'admin-token-not-configured', detail: ADMIN_UNSET_HINT };
|
|
250
|
+
}
|
|
251
|
+
const provided = extractBearerToken(input.authHeader);
|
|
252
|
+
if (!provided)
|
|
253
|
+
return { ok: false, status: 401, error: 'unauthorized' };
|
|
254
|
+
const granted = tierForToken(provided, input);
|
|
255
|
+
if (granted === 0)
|
|
256
|
+
return { ok: false, status: 401, error: 'unauthorized' };
|
|
257
|
+
if (granted < tier) {
|
|
258
|
+
return { ok: false, status: 403, error: 'insufficient-tier', detail: INSUFFICIENT_TIER_HINT };
|
|
259
|
+
}
|
|
260
|
+
return { ok: true };
|
|
261
|
+
}
|
package/dist/http/body.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ import type { IncomingMessage, ServerResponse } from 'http';
|
|
|
13
13
|
import { type AgentType } from '../types';
|
|
14
14
|
/** Hard cap on incoming JSON body size (1 MiB). */
|
|
15
15
|
export declare const WRITE_BODY_MAX: number;
|
|
16
|
+
/** 3c Tier-2 ingest cap (32 KiB) — the DOS backstop for `/inner/ingest`; the
|
|
17
|
+
* source already ~2KB-truncates summaries, so real frames are far smaller. */
|
|
18
|
+
export declare const INGEST_BODY_MAX: number;
|
|
16
19
|
export declare const BODY_TOO_LARGE: unique symbol;
|
|
17
20
|
export declare const BODY_INVALID_JSON: unique symbol;
|
|
18
21
|
export type ReadJsonBodyResult = Record<string, unknown> | typeof BODY_TOO_LARGE | typeof BODY_INVALID_JSON;
|
|
@@ -26,7 +29,7 @@ export type ReadJsonBodyResult = Record<string, unknown> | typeof BODY_TOO_LARGE
|
|
|
26
29
|
* handler ends. Explicit `req.destroy()` would race the response
|
|
27
30
|
* write — left alone.
|
|
28
31
|
*/
|
|
29
|
-
export declare function readJsonBody(req: IncomingMessage): Promise<ReadJsonBodyResult>;
|
|
32
|
+
export declare function readJsonBody(req: IncomingMessage, maxBytes?: number): Promise<ReadJsonBodyResult>;
|
|
30
33
|
/**
|
|
31
34
|
* Pluck a string field from a parsed JSON body. Returns `undefined`
|
|
32
35
|
* for absent or non-string values; with `requireNonEmpty: true`, also
|