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
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InnerLoopRegistry = exports.InnerSubscription = exports.INNER_SUB_QUEUE_MAX = void 0;
|
|
4
|
+
/** Per-subscriber bounded queue depth before drop-oldest engages. */
|
|
5
|
+
exports.INNER_SUB_QUEUE_MAX = 256;
|
|
6
|
+
/**
|
|
7
|
+
* One connected `/inner` SSE subscriber. Async-iterable: the SSE handler does
|
|
8
|
+
* `for await (const frame of sub) { write(frame) }`. Bounded queue with
|
|
9
|
+
* drop-oldest; a `compacted{dropped,sinceTs}` marker is injected before the next
|
|
10
|
+
* real frame whenever drops have occurred since the last delivery.
|
|
11
|
+
*
|
|
12
|
+
* No ring / replay / seq — this is an ephemeral best-effort tail (MD-F): a
|
|
13
|
+
* disconnect loses in-flight deltas, by design.
|
|
14
|
+
*/
|
|
15
|
+
class InnerSubscription {
|
|
16
|
+
now;
|
|
17
|
+
queue = [];
|
|
18
|
+
dropped = 0;
|
|
19
|
+
droppedSinceTs = 0;
|
|
20
|
+
waiter = null;
|
|
21
|
+
closed = false;
|
|
22
|
+
constructor(now = Date.now) {
|
|
23
|
+
this.now = now;
|
|
24
|
+
}
|
|
25
|
+
/** Enqueue a frame, dropping the oldest if the queue is full. Source → sub. */
|
|
26
|
+
push(frame) {
|
|
27
|
+
if (this.closed)
|
|
28
|
+
return;
|
|
29
|
+
if (this.queue.length >= exports.INNER_SUB_QUEUE_MAX) {
|
|
30
|
+
this.queue.shift(); // drop oldest
|
|
31
|
+
if (this.dropped === 0)
|
|
32
|
+
this.droppedSinceTs = this.now();
|
|
33
|
+
this.dropped++;
|
|
34
|
+
}
|
|
35
|
+
this.queue.push(frame);
|
|
36
|
+
// A full queue means no waiter is parked (a waiter only parks on an empty
|
|
37
|
+
// queue), so a drop never races a pending take — drain just wakes a waiter
|
|
38
|
+
// when one exists (queue was empty, now has one item).
|
|
39
|
+
if (this.waiter) {
|
|
40
|
+
const next = this.take();
|
|
41
|
+
if (next) {
|
|
42
|
+
const w = this.waiter;
|
|
43
|
+
this.waiter = null;
|
|
44
|
+
w({ value: next, done: false });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Pull the next deliverable, injecting a `compacted` marker first if drops are pending. */
|
|
49
|
+
take() {
|
|
50
|
+
if (this.dropped > 0) {
|
|
51
|
+
const marker = { type: 'compacted', dropped: this.dropped, sinceTs: this.droppedSinceTs };
|
|
52
|
+
this.dropped = 0;
|
|
53
|
+
return marker;
|
|
54
|
+
}
|
|
55
|
+
return this.queue.shift() ?? null;
|
|
56
|
+
}
|
|
57
|
+
next() {
|
|
58
|
+
if (this.closed)
|
|
59
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
60
|
+
const item = this.take();
|
|
61
|
+
if (item)
|
|
62
|
+
return Promise.resolve({ value: item, done: false });
|
|
63
|
+
return new Promise((resolve) => { this.waiter = resolve; });
|
|
64
|
+
}
|
|
65
|
+
/** Terminate the stream — wakes a parked consumer with `done: true`. Idempotent. */
|
|
66
|
+
close() {
|
|
67
|
+
if (this.closed)
|
|
68
|
+
return;
|
|
69
|
+
this.closed = true;
|
|
70
|
+
if (this.waiter) {
|
|
71
|
+
const w = this.waiter;
|
|
72
|
+
this.waiter = null;
|
|
73
|
+
w({ value: undefined, done: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return() {
|
|
77
|
+
this.close();
|
|
78
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
79
|
+
}
|
|
80
|
+
[Symbol.asyncIterator]() {
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.InnerSubscription = InnerSubscription;
|
|
85
|
+
/**
|
|
86
|
+
* Per-daemon registry of inner-loop subscribers, keyed by the player's fixed
|
|
87
|
+
* session `workflowId`. Implements eng's {@link InnerLoopSink} interface
|
|
88
|
+
* (`publish` + `subscriberCount`) so the publisher's thin client and this sink
|
|
89
|
+
* share one contract.
|
|
90
|
+
*/
|
|
91
|
+
class InnerLoopRegistry {
|
|
92
|
+
now;
|
|
93
|
+
subs = new Map();
|
|
94
|
+
constructor(now = Date.now) {
|
|
95
|
+
this.now = now;
|
|
96
|
+
}
|
|
97
|
+
/** Open a new SSE subscription for a player. Caller drains it, then `unsubscribe`. */
|
|
98
|
+
subscribe(workflowId) {
|
|
99
|
+
const sub = new InnerSubscription(this.now);
|
|
100
|
+
let set = this.subs.get(workflowId);
|
|
101
|
+
if (!set) {
|
|
102
|
+
set = new Set();
|
|
103
|
+
this.subs.set(workflowId, set);
|
|
104
|
+
}
|
|
105
|
+
set.add(sub);
|
|
106
|
+
return sub;
|
|
107
|
+
}
|
|
108
|
+
/** Remove + close one subscription (on SSE disconnect). Prunes the empty player set. */
|
|
109
|
+
unsubscribe(workflowId, sub) {
|
|
110
|
+
const set = this.subs.get(workflowId);
|
|
111
|
+
if (!set)
|
|
112
|
+
return;
|
|
113
|
+
set.delete(sub);
|
|
114
|
+
sub.close();
|
|
115
|
+
if (set.size === 0)
|
|
116
|
+
this.subs.delete(workflowId);
|
|
117
|
+
}
|
|
118
|
+
/** Fan a frame out to every live subscriber for the player. Source → subs. */
|
|
119
|
+
publish(workflowId, frame) {
|
|
120
|
+
const set = this.subs.get(workflowId);
|
|
121
|
+
if (!set)
|
|
122
|
+
return;
|
|
123
|
+
for (const sub of set)
|
|
124
|
+
sub.push(frame);
|
|
125
|
+
}
|
|
126
|
+
/** Live subscriber count — the publisher's presence gate reads this (via `/inner/presence`). */
|
|
127
|
+
subscriberCount(workflowId) {
|
|
128
|
+
return this.subs.get(workflowId)?.size ?? 0;
|
|
129
|
+
}
|
|
130
|
+
/** Close every subscriber for a player (player gone → streams end with `:closed`). */
|
|
131
|
+
closePlayer(workflowId) {
|
|
132
|
+
const set = this.subs.get(workflowId);
|
|
133
|
+
if (!set)
|
|
134
|
+
return;
|
|
135
|
+
for (const sub of set)
|
|
136
|
+
sub.close();
|
|
137
|
+
this.subs.delete(workflowId);
|
|
138
|
+
}
|
|
139
|
+
/** Total live inner-tail subscribers across all players (diagnostics). */
|
|
140
|
+
totalSubscriberCount() {
|
|
141
|
+
let n = 0;
|
|
142
|
+
for (const set of this.subs.values())
|
|
143
|
+
n += set.size;
|
|
144
|
+
return n;
|
|
145
|
+
}
|
|
146
|
+
/** Close everything (daemon shutdown). */
|
|
147
|
+
close() {
|
|
148
|
+
for (const set of this.subs.values()) {
|
|
149
|
+
for (const sub of set)
|
|
150
|
+
sub.close();
|
|
151
|
+
}
|
|
152
|
+
this.subs.clear();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.InnerLoopRegistry = InnerLoopRegistry;
|
package/dist/http/server.d.ts
CHANGED
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
import * as http from 'http';
|
|
19
19
|
import type { TempoClient } from '../client/interface';
|
|
20
20
|
import type { AggregateRunner } from './aggregate';
|
|
21
|
+
import type { InnerLoopRegistry } from './inner-loop';
|
|
22
|
+
import type { IngestTokenRegistry } from './ingest-registry';
|
|
23
|
+
import type { GateRegistry } from './gate-registry';
|
|
21
24
|
import { type CorsConfig } from './cors';
|
|
22
25
|
import { ConnectionCap } from './sse-handler';
|
|
23
26
|
/** Default bind addr per SSE-PROTOCOL.md §1. */
|
|
@@ -59,10 +62,14 @@ export interface HttpServerOptions {
|
|
|
59
62
|
*/
|
|
60
63
|
portFilePath?: string;
|
|
61
64
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
65
|
+
* @deprecated 3e — back-compat alias for {@link readToken}. A single injected
|
|
66
|
+
* bearer is treated as the READ token (T1). Prefer `readToken`/`adminToken`.
|
|
64
67
|
*/
|
|
65
68
|
httpToken?: string;
|
|
69
|
+
/** 3e — inject the read-tier (T1) token directly (tests). Overrides config/env. */
|
|
70
|
+
readToken?: string;
|
|
71
|
+
/** 3e — inject the admin (T1+T2+T3) token directly (tests). Overrides env. */
|
|
72
|
+
adminToken?: string;
|
|
66
73
|
/**
|
|
67
74
|
* Test seam — lets unit tests stub `process.uptime`-style readings.
|
|
68
75
|
*/
|
|
@@ -80,6 +87,25 @@ export interface HttpServerOptions {
|
|
|
80
87
|
* low value in tests to exercise the 503 path.
|
|
81
88
|
*/
|
|
82
89
|
maxSseConnections?: number;
|
|
90
|
+
/**
|
|
91
|
+
* 3c Tier-2 — the inner-loop fine-tail registry (off-wire SSE sink) and the
|
|
92
|
+
* ingest-token registry (source-plane auth). When both are provided the
|
|
93
|
+
* `/v1/players/:e/:p/inner` egress + `/inner/ingest` + `/inner/presence`
|
|
94
|
+
* ingress routes light up; absent → those routes 404/503. The daemon
|
|
95
|
+
* constructs + shares these (the outbox mints ingest tokens into the same
|
|
96
|
+
* `ingestTokens` instance the server validates against).
|
|
97
|
+
*/
|
|
98
|
+
innerLoop?: InnerLoopRegistry;
|
|
99
|
+
ingestTokens?: IngestTokenRegistry;
|
|
100
|
+
/**
|
|
101
|
+
* 3d MD-G — the operator-gate registry. When provided (with `ingestTokens`)
|
|
102
|
+
* the `/v1/players/:e/:p/gate-arm` + `/gate-disarm` + `/gate/:requestId`
|
|
103
|
+
* operator routes (requireTier(3)) and the `/gate/:requestId/resolution`
|
|
104
|
+
* subprocess-poll route (ingest-token) light up; absent → those routes 404.
|
|
105
|
+
* The daemon constructs + shares this with the worker (auto-disarm on
|
|
106
|
+
* detach/destroy) — same singleton pattern as the inner-loop registries.
|
|
107
|
+
*/
|
|
108
|
+
gate?: GateRegistry;
|
|
83
109
|
}
|
|
84
110
|
export interface HttpServerHandle {
|
|
85
111
|
/** The actual port the server is listening on (after `.listen()` resolves). */
|
|
@@ -106,13 +132,22 @@ interface HandleContext {
|
|
|
106
132
|
version: string;
|
|
107
133
|
bindAddr: string;
|
|
108
134
|
corsConfig: CorsConfig;
|
|
109
|
-
|
|
135
|
+
/** 3e RBAC read-tier token (T1), or null. */
|
|
136
|
+
readToken: string | null;
|
|
137
|
+
/** 3e RBAC admin token (T1+T2+T3) — env-var-only, or null when unset. */
|
|
138
|
+
adminToken: string | null;
|
|
110
139
|
startedAt: number;
|
|
111
140
|
subscriberCount: () => number;
|
|
112
141
|
/** Present when PR-2 streaming is wired; null on PR-1-only deployments. */
|
|
113
142
|
aggregate: AggregateRunner | null;
|
|
114
143
|
/** Process-wide SSE subscriber cap (§7.3). */
|
|
115
144
|
sseConnectionCap: ConnectionCap;
|
|
145
|
+
/** 3c Tier-2 inner-loop fine-tail sink (off-wire) — null when unwired. */
|
|
146
|
+
innerLoop: InnerLoopRegistry | null;
|
|
147
|
+
/** 3c Tier-2 ingest-token registry (source-plane auth) — null when unwired. */
|
|
148
|
+
ingestTokens: IngestTokenRegistry | null;
|
|
149
|
+
/** 3d MD-G operator-gate registry — null when unwired. */
|
|
150
|
+
gate: GateRegistry | null;
|
|
116
151
|
}
|
|
117
152
|
/**
|
|
118
153
|
* Top-level request dispatcher — exported for unit tests that want to
|
package/dist/http/server.js
CHANGED
|
@@ -56,6 +56,8 @@ exports.handle = handle;
|
|
|
56
56
|
const http = __importStar(require("http"));
|
|
57
57
|
const config_1 = require("../config");
|
|
58
58
|
const auth_1 = require("./auth");
|
|
59
|
+
const inner_loop_routes_1 = require("./inner-loop-routes");
|
|
60
|
+
const gate_routes_1 = require("./gate-routes");
|
|
59
61
|
const cors_1 = require("./cors");
|
|
60
62
|
const dashboard_1 = require("./dashboard");
|
|
61
63
|
const dashboard_pair_1 = require("./dashboard-pair");
|
|
@@ -98,10 +100,46 @@ async function startHttpServer(opts) {
|
|
|
98
100
|
// generation now so the daemon doesn't crash mid-request when the first
|
|
99
101
|
// bearer-required call shows up.
|
|
100
102
|
const bindIsLoopback = (0, auth_1.isLoopbackBindAddr)(bindAddr);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
// 3e RBAC token resolution. Back-compat: a single `httpToken` option (or a
|
|
104
|
+
// legacy config.json `httpToken`) is adopted as the READ token (T1); the ADMIN
|
|
105
|
+
// token is env-var-only. Explicit `readToken`/`adminToken` options override
|
|
106
|
+
// (used by tests). loopback bind ⇒ no bearer required ⇒ tokens may be null.
|
|
107
|
+
let readToken;
|
|
108
|
+
let legacyMigrated = false;
|
|
109
|
+
if (opts.readToken !== undefined) {
|
|
110
|
+
readToken = opts.readToken;
|
|
111
|
+
}
|
|
112
|
+
else if (opts.httpToken !== undefined) {
|
|
113
|
+
// Back-compat: a single injected bearer is treated as the READ token (T1).
|
|
114
|
+
readToken = opts.httpToken;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const loaded = (0, auth_1.loadReadToken)({ bearerRequired: !bindIsLoopback });
|
|
118
|
+
readToken = loaded.token;
|
|
119
|
+
legacyMigrated = loaded.legacy;
|
|
120
|
+
}
|
|
121
|
+
const adminToken = opts.adminToken ?? (0, auth_1.loadAdminToken)();
|
|
122
|
+
if (!bindIsLoopback && !readToken) {
|
|
103
123
|
throw new Error('Bearer token required for non-loopback bind but none configured. ' +
|
|
104
|
-
'Set
|
|
124
|
+
'Set AGENT_TEMPO_HTTP_READ_TOKEN (or readToken in ~/.agent-tempo/config.json), ' +
|
|
125
|
+
'or unset AGENT_TEMPO_HTTP_BIND.');
|
|
126
|
+
}
|
|
127
|
+
// 3e MD-E — one-time startup warnings (non-blocking).
|
|
128
|
+
//
|
|
129
|
+
// (1) Legacy migration: a pre-3e single `httpToken` was adopted as the READ
|
|
130
|
+
// token (T1) and no admin token is configured, so writes / operator gate /
|
|
131
|
+
// inner-tail (all Tier ≥ 2) will 503 until an admin token is set.
|
|
132
|
+
if (legacyMigrated && adminToken === null) {
|
|
133
|
+
log('NOTICE: adopted legacy config.json `httpToken` as the read-tier token. ' +
|
|
134
|
+
'Writes, the operator gate, and the inner-tail are admin-only and will return ' +
|
|
135
|
+
'503 until you set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only).');
|
|
136
|
+
}
|
|
137
|
+
// (2) Plaintext-bearer exposure: binding to a non-loopback address serves the
|
|
138
|
+
// bearer token over cleartext HTTP. Suppressible, never blocking.
|
|
139
|
+
if (!bindIsLoopback && process.env[config_1.ENV.TLS_ACKNOWLEDGED] !== '1') {
|
|
140
|
+
log(`WARNING: binding to non-loopback ${bindAddr} serves the bearer token over ` +
|
|
141
|
+
'plaintext HTTP. Terminate TLS at a reverse proxy, or tunnel via SSH/Tailscale. ' +
|
|
142
|
+
`Set ${config_1.ENV.TLS_ACKNOWLEDGED}=1 to acknowledge and suppress this warning.`);
|
|
105
143
|
}
|
|
106
144
|
const startedAt = opts.startedAtMs ?? Date.now();
|
|
107
145
|
// §7.3 process-wide cap. Defaults to 100 per spec; env var override.
|
|
@@ -122,11 +160,15 @@ async function startHttpServer(opts) {
|
|
|
122
160
|
version: opts.version,
|
|
123
161
|
bindAddr,
|
|
124
162
|
corsConfig,
|
|
125
|
-
|
|
163
|
+
readToken,
|
|
164
|
+
adminToken,
|
|
126
165
|
startedAt,
|
|
127
166
|
subscriberCount,
|
|
128
167
|
aggregate: opts.aggregate ?? null,
|
|
129
168
|
sseConnectionCap,
|
|
169
|
+
innerLoop: opts.innerLoop ?? null,
|
|
170
|
+
ingestTokens: opts.ingestTokens ?? null,
|
|
171
|
+
gate: opts.gate ?? null,
|
|
130
172
|
}).catch((err) => {
|
|
131
173
|
log('unhandled handler error:', err instanceof Error ? err.message : err);
|
|
132
174
|
if (!res.headersSent) {
|
|
@@ -233,12 +275,53 @@ async function handle(req, res, ctx) {
|
|
|
233
275
|
if (method === 'GET' && pairConsumeMatch) {
|
|
234
276
|
return (0, dashboard_pair_1.handlePairConsume)(req, res, pairConsumeMatch[1]);
|
|
235
277
|
}
|
|
236
|
-
//
|
|
278
|
+
// 3c Tier-2 INGRESS (publisher → daemon). Matched BEFORE the outer bearer
|
|
279
|
+
// gate: these use their OWN source-plane auth (loopback `socket.remoteAddress`
|
|
280
|
+
// + `X-Ingest-Token` vs the URL workflowId), so a localhost Pi subprocess
|
|
281
|
+
// reaches them regardless of the daemon's bind address. Only live when the
|
|
282
|
+
// daemon wired the registries; else they fall through to the 404/405 path.
|
|
283
|
+
if (ctx.innerLoop && ctx.ingestTokens) {
|
|
284
|
+
const innerDeps = { innerLoop: ctx.innerLoop, ingestTokens: ctx.ingestTokens, ...(ctx.gate ? { gate: ctx.gate } : {}) };
|
|
285
|
+
const ingestMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/inner\/ingest$/);
|
|
286
|
+
if (ingestMatch) {
|
|
287
|
+
if (method !== 'POST') {
|
|
288
|
+
return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'POST' });
|
|
289
|
+
}
|
|
290
|
+
return (0, inner_loop_routes_1.handleInnerIngest)(req, res, innerDeps, decodeURIComponent(ingestMatch[1]), decodeURIComponent(ingestMatch[2]));
|
|
291
|
+
}
|
|
292
|
+
const presenceMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/inner\/presence$/);
|
|
293
|
+
if (presenceMatch) {
|
|
294
|
+
if (method !== 'GET') {
|
|
295
|
+
return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET' });
|
|
296
|
+
}
|
|
297
|
+
return (0, inner_loop_routes_1.handleInnerPresence)(req, res, innerDeps, decodeURIComponent(presenceMatch[1]), decodeURIComponent(presenceMatch[2]));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// 3d MD-G INGRESS (Pi subprocess → daemon poll). Same source-plane auth as the
|
|
301
|
+
// inner-loop ingest (loopback `socket.remoteAddress` + `X-Ingest-Token` vs the
|
|
302
|
+
// URL workflowId), matched BEFORE the bearer gate. Live only when the daemon
|
|
303
|
+
// wired the gate + ingest registries.
|
|
304
|
+
if (ctx.gate && ctx.ingestTokens) {
|
|
305
|
+
const gateDeps = { gate: ctx.gate, ingestTokens: ctx.ingestTokens };
|
|
306
|
+
const resolutionMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/gate\/([^/]+)\/resolution$/);
|
|
307
|
+
if (resolutionMatch) {
|
|
308
|
+
if (method !== 'GET') {
|
|
309
|
+
return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET' });
|
|
310
|
+
}
|
|
311
|
+
return (0, gate_routes_1.handleGateResolution)(req, res, gateDeps, decodeURIComponent(resolutionMatch[1]), decodeURIComponent(resolutionMatch[2]), decodeURIComponent(resolutionMatch[3]));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Layer 2 — shared AUTHENTICATION + Origin/DNS-rebind defense (architect's
|
|
315
|
+
// decomposition). bearerRequired() carries the Origin-rebind logic; a request
|
|
316
|
+
// in bearer mode must present a token granting at LEAST a tier (read or admin).
|
|
317
|
+
// This is the single shared upstream pass — the per-route TIER authorization
|
|
318
|
+
// (Layer 3, `gateTier(N)` / inline `requireTier(3)`) refines it below: reads → T1,
|
|
319
|
+
// writes/pair-mint → T2 (admin), gate/inner → T3 (admin).
|
|
237
320
|
const originHeader = headerString(req.headers.origin);
|
|
238
321
|
const reqBearer = (0, auth_1.bearerRequired)(ctx.bindAddr, originHeader);
|
|
239
322
|
if (reqBearer) {
|
|
240
323
|
const provided = (0, auth_1.extractBearerToken)(headerString(req.headers.authorization));
|
|
241
|
-
if (!provided ||
|
|
324
|
+
if (!provided || (0, auth_1.tierForToken)(provided, ctx) === 0) {
|
|
242
325
|
writeCorsHeaders(res, originHeader, ctx, reqBearer);
|
|
243
326
|
return (0, responses_1.errorResponse)(res, 401, { error: 'unauthorized' });
|
|
244
327
|
}
|
|
@@ -252,6 +335,46 @@ async function handle(req, res, ctx) {
|
|
|
252
335
|
res.setHeader('Access-Control-Allow-Origin', cors.echo);
|
|
253
336
|
res.setHeader('Vary', 'Origin');
|
|
254
337
|
}
|
|
338
|
+
// Layer 3 — per-route AUTHORIZATION (3e MD-E). The tier-guard input is
|
|
339
|
+
// resolved ONCE here off the shared L2 pass (bindAddr + Origin + the two
|
|
340
|
+
// RBAC tokens) and reused by every `gateTier(N)` call below — reads require
|
|
341
|
+
// T1, the write/pair-mint surface requires T2 (admin). The grandfathered T3
|
|
342
|
+
// gate/inner sites (3c/3d) keep their own inline `requireTier(3)` by design.
|
|
343
|
+
//
|
|
344
|
+
// This is defense-in-depth ON TOP of the L2 token-validity floor above (which
|
|
345
|
+
// already rejects an unrecognized bearer with 401): the explicit per-route
|
|
346
|
+
// guard keeps each surface protected at its declared tier even if the L2 floor
|
|
347
|
+
// is later relaxed, and makes the required tier self-documenting + greppable.
|
|
348
|
+
const tierInput = {
|
|
349
|
+
bindAddr: ctx.bindAddr,
|
|
350
|
+
originHeader,
|
|
351
|
+
authHeader: headerString(req.headers.authorization),
|
|
352
|
+
readToken: ctx.readToken,
|
|
353
|
+
adminToken: ctx.adminToken,
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* Write a tier-denial response, surfacing requireTier's actionable `detail`
|
|
357
|
+
* hint on 403 (insufficient-tier) / 503 (admin-unset) when present (3e ruling
|
|
358
|
+
* #3). The hint is operator guidance — e.g. "set AGENT_TEMPO_HTTP_ADMIN_TOKEN"
|
|
359
|
+
* — NOT a sensitive leak (security-confirmed). Shared by `gateTier` and the
|
|
360
|
+
* inline T3 gate/inner sites so every tier denial carries the same body shape.
|
|
361
|
+
*/
|
|
362
|
+
const denyTier = (r) => {
|
|
363
|
+
(0, responses_1.errorResponse)(res, r.status, 'detail' in r ? { error: r.error, detail: r.detail } : { error: r.error });
|
|
364
|
+
};
|
|
365
|
+
/**
|
|
366
|
+
* Apply a per-route tier guard against the L2-resolved input. On failure it
|
|
367
|
+
* writes the 401/403/503 response and returns `false` (caller returns); on
|
|
368
|
+
* success returns `true`. Loopback requests short-circuit to PASS inside
|
|
369
|
+
* {@link requireTier} (local-trust → full tier).
|
|
370
|
+
*/
|
|
371
|
+
const gateTier = (n) => {
|
|
372
|
+
const g = (0, auth_1.requireTier)(n, tierInput);
|
|
373
|
+
if (g.ok)
|
|
374
|
+
return true;
|
|
375
|
+
denyTier(g);
|
|
376
|
+
return false;
|
|
377
|
+
};
|
|
255
378
|
// Write surface (PR-7a of #340) — POST `/v1/ensembles/:ensemble/<action>`
|
|
256
379
|
// Match BEFORE the GET-only method gate; everything else (POST to a
|
|
257
380
|
// read endpoint, GET to a write endpoint) flows into the 405 fallback
|
|
@@ -261,6 +384,10 @@ async function handle(req, res, ctx) {
|
|
|
261
384
|
const ensemble = decodeURIComponent(writeMatch[1]);
|
|
262
385
|
const action = writeMatch[2];
|
|
263
386
|
if ((0, writes_1.isWriteAction)(action)) {
|
|
387
|
+
// L3 — the mutate surface is admin-only (Tier 2). Gate before the method
|
|
388
|
+
// check so an under-privileged caller can't probe the write verbs via 405.
|
|
389
|
+
if (!gateTier(2))
|
|
390
|
+
return;
|
|
264
391
|
if (method !== 'POST') {
|
|
265
392
|
return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'POST, OPTIONS' });
|
|
266
393
|
}
|
|
@@ -274,6 +401,8 @@ async function handle(req, res, ctx) {
|
|
|
274
401
|
// alongside the writeMatch above so it's reached before the GET-only
|
|
275
402
|
// gate; the GET on the same path (list ensembles) is handled below.
|
|
276
403
|
if (pathname === '/v1/ensembles' && method === 'POST') {
|
|
404
|
+
if (!gateTier(2))
|
|
405
|
+
return; // L3 — create-ensemble is a write (Tier 2).
|
|
277
406
|
return (0, catalog_1.handleCreateEnsemble)(req, res, ctx.client);
|
|
278
407
|
}
|
|
279
408
|
// POST `/dashboard/api/pair` — mint a pairing for cross-device QR (PR-8
|
|
@@ -281,9 +410,43 @@ async function handle(req, res, ctx) {
|
|
|
281
410
|
// proves authority before issuing a token; the token's GET-side consume
|
|
282
411
|
// is the carve-out above the auth gate.
|
|
283
412
|
if (method === 'POST' && pathname === '/dashboard/api/pair') {
|
|
413
|
+
// L3 — minting a cross-device pairing token is an admin operation (Tier 2):
|
|
414
|
+
// it grants a bearer-equivalent capability, so it must require the admin token.
|
|
415
|
+
if (!gateTier(2))
|
|
416
|
+
return;
|
|
284
417
|
const provided = (0, auth_1.extractBearerToken)(headerString(req.headers.authorization));
|
|
285
418
|
return (0, dashboard_pair_1.handlePairCreate)(req, res, provided);
|
|
286
419
|
}
|
|
420
|
+
// 3d MD-G OPERATOR plane (operator/dashboard → daemon) — POST routes, so they
|
|
421
|
+
// sit BEFORE the GET-only method gate below (alongside the other POST routes).
|
|
422
|
+
// `requireTier(3)` — only an admin-token holder may arm/disarm or decide
|
|
423
|
+
// (MD-E highest tier). Live only when the daemon wired the gate registries.
|
|
424
|
+
if (ctx.gate && ctx.ingestTokens && method === 'POST') {
|
|
425
|
+
const gateDeps = { gate: ctx.gate, ingestTokens: ctx.ingestTokens };
|
|
426
|
+
const armMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/gate-(arm|disarm)$/);
|
|
427
|
+
const decideMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/gate\/([^/]+)$/);
|
|
428
|
+
if (armMatch || decideMatch) {
|
|
429
|
+
const tier = (0, auth_1.requireTier)(3, {
|
|
430
|
+
bindAddr: ctx.bindAddr,
|
|
431
|
+
originHeader,
|
|
432
|
+
authHeader: headerString(req.headers.authorization),
|
|
433
|
+
readToken: ctx.readToken,
|
|
434
|
+
adminToken: ctx.adminToken,
|
|
435
|
+
});
|
|
436
|
+
if (!tier.ok) {
|
|
437
|
+
denyTier(tier);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (armMatch) {
|
|
441
|
+
const [, e, p, verb] = armMatch;
|
|
442
|
+
return verb === 'arm'
|
|
443
|
+
? (0, gate_routes_1.handleGateArm)(req, res, gateDeps, decodeURIComponent(e), decodeURIComponent(p))
|
|
444
|
+
: (0, gate_routes_1.handleGateDisarm)(req, res, gateDeps, decodeURIComponent(e), decodeURIComponent(p));
|
|
445
|
+
}
|
|
446
|
+
// decideMatch — POST /gate/:requestId { decision }
|
|
447
|
+
return (0, gate_routes_1.handleGateDecide)(req, res, gateDeps, decodeURIComponent(decideMatch[1]), decodeURIComponent(decideMatch[2]), decodeURIComponent(decideMatch[3]));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
287
450
|
// Method gate — read endpoints are GET-only. Both POST paths above
|
|
288
451
|
// (PR-7a writes, PR-8 pair-mint) handle their own method matching;
|
|
289
452
|
// everything else falls through here.
|
|
@@ -295,18 +458,26 @@ async function handle(req, res, ctx) {
|
|
|
295
458
|
// `/v1/*` API uses. The pre-auth pair-token carve-out above is the
|
|
296
459
|
// single exception that bootstraps cross-device pairing.
|
|
297
460
|
if (pathname === '/dashboard' || pathname.startsWith('/dashboard/')) {
|
|
461
|
+
if (!gateTier(1))
|
|
462
|
+
return; // L3 — the dashboard SPA is read-tier (Tier 1).
|
|
298
463
|
return (0, dashboard_1.handleDashboardStatic)(req, res, pathname);
|
|
299
464
|
}
|
|
300
465
|
if (pathname === '/v1/ensembles') {
|
|
466
|
+
if (!gateTier(1))
|
|
467
|
+
return; // L3 — read (Tier 1).
|
|
301
468
|
return handleListEnsembles(res, ctx);
|
|
302
469
|
}
|
|
303
470
|
if (pathname === '/v1/hosts') {
|
|
471
|
+
if (!gateTier(1))
|
|
472
|
+
return; // L3 — read (Tier 1).
|
|
304
473
|
return handleHosts(res, ctx);
|
|
305
474
|
}
|
|
306
475
|
// #579 — cluster-wide cross-host orphan listing for the dashboard.
|
|
307
476
|
// Same bearer + CORS gate as `/v1/hosts`; optional `?ensemble=<name>`
|
|
308
477
|
// narrows to one ensemble.
|
|
309
478
|
if (pathname === '/v1/orphans') {
|
|
479
|
+
if (!gateTier(1))
|
|
480
|
+
return; // L3 — read (Tier 1).
|
|
310
481
|
const ensembleFilter = url.searchParams.get('ensemble') ?? undefined;
|
|
311
482
|
return (0, orphans_1.handleOrphans)(res, {
|
|
312
483
|
client: ctx.client,
|
|
@@ -318,14 +489,20 @@ async function handle(req, res, ctx) {
|
|
|
318
489
|
// Catalog reads (issue #400) — `listAgentTypes` / `listLineups`
|
|
319
490
|
// touch local fs only, no Temporal calls; cheap to serve per-request.
|
|
320
491
|
if (pathname === '/v1/agent-types') {
|
|
492
|
+
if (!gateTier(1))
|
|
493
|
+
return; // L3 — read (Tier 1).
|
|
321
494
|
return (0, catalog_1.handleListAgentTypes)(res);
|
|
322
495
|
}
|
|
323
496
|
if (pathname === '/v1/lineups') {
|
|
497
|
+
if (!gateTier(1))
|
|
498
|
+
return; // L3 — read (Tier 1).
|
|
324
499
|
return (0, catalog_1.handleListLineups)(res);
|
|
325
500
|
}
|
|
326
501
|
// /v1/state/:ensemble — single capture group.
|
|
327
502
|
const stateMatch = pathname.match(/^\/v1\/state\/([^/]+)$/);
|
|
328
503
|
if (stateMatch) {
|
|
504
|
+
if (!gateTier(1))
|
|
505
|
+
return; // L3 — read (Tier 1); covers the fixture path too.
|
|
329
506
|
const ensemble = decodeURIComponent(stateMatch[1]);
|
|
330
507
|
// Fixture mode (PR-3 of #340) — `?fixture=<name>` short-circuits the
|
|
331
508
|
// live snapshot with canned data. Sits behind the bearer-auth gate.
|
|
@@ -340,6 +517,8 @@ async function handle(req, res, ctx) {
|
|
|
340
517
|
// signals "feature exists, not yet wired" rather than "ensemble
|
|
341
518
|
// doesn't exist".
|
|
342
519
|
if (pathname === '/v1/events') {
|
|
520
|
+
if (!gateTier(1))
|
|
521
|
+
return; // L3 — read/observe stream (Tier 1).
|
|
343
522
|
if (!ctx.aggregate) {
|
|
344
523
|
return (0, responses_1.errorResponse)(res, 503, { error: 'streaming-not-implemented' }, { 'Retry-After': '60' });
|
|
345
524
|
}
|
|
@@ -352,6 +531,8 @@ async function handle(req, res, ctx) {
|
|
|
352
531
|
}
|
|
353
532
|
const evtMatch = pathname.match(/^\/v1\/events\/([^/]+)$/);
|
|
354
533
|
if (evtMatch) {
|
|
534
|
+
if (!gateTier(1))
|
|
535
|
+
return; // L3 — read/observe stream (Tier 1); covers fixture too.
|
|
355
536
|
const ensemble = decodeURIComponent(evtMatch[1]);
|
|
356
537
|
// Fixture mode (PR-3 of #340) — `?fixture=<name>` short-circuits both
|
|
357
538
|
// the existence check and the aggregate poll loop with a canned event
|
|
@@ -378,6 +559,30 @@ async function handle(req, res, ctx) {
|
|
|
378
559
|
cap: ctx.sseConnectionCap,
|
|
379
560
|
});
|
|
380
561
|
}
|
|
562
|
+
// 3c Tier-2 EGRESS — operator/widget inner-loop SSE fine tail. After the outer
|
|
563
|
+
// bearer gate (so it's already authenticated); the explicit `requireTier(3)`
|
|
564
|
+
// marks the tier for 3e (today the outer bearer already satisfied it — 3e
|
|
565
|
+
// relaxes the outer gate to a read token and this guard demands the admin
|
|
566
|
+
// token, no call-site change). View-agnostic: bearer-keyed, NO Origin
|
|
567
|
+
// requirement, plain `event:`/`data:` framing (fetch + Node-client consumable).
|
|
568
|
+
const innerSseMatch = pathname.match(/^\/v1\/players\/([^/]+)\/([^/]+)\/inner$/);
|
|
569
|
+
if (innerSseMatch) {
|
|
570
|
+
if (!ctx.innerLoop || !ctx.ingestTokens) {
|
|
571
|
+
return (0, responses_1.errorResponse)(res, 503, { error: 'streaming-not-implemented' }, { 'Retry-After': '60' });
|
|
572
|
+
}
|
|
573
|
+
const tier = (0, auth_1.requireTier)(3, {
|
|
574
|
+
bindAddr: ctx.bindAddr,
|
|
575
|
+
originHeader,
|
|
576
|
+
authHeader: headerString(req.headers.authorization),
|
|
577
|
+
readToken: ctx.readToken,
|
|
578
|
+
adminToken: ctx.adminToken,
|
|
579
|
+
});
|
|
580
|
+
if (!tier.ok) {
|
|
581
|
+
denyTier(tier);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
return (0, inner_loop_routes_1.handleInnerSse)(req, res, { innerLoop: ctx.innerLoop, ingestTokens: ctx.ingestTokens }, decodeURIComponent(innerSseMatch[1]), decodeURIComponent(innerSseMatch[2]));
|
|
585
|
+
}
|
|
381
586
|
return (0, responses_1.errorResponse)(res, 404, { error: 'not-found' });
|
|
382
587
|
}
|
|
383
588
|
/** Pull a single string from a possibly-array header. */
|
package/dist/http/snapshot.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface PlayerWireMeta {
|
|
|
51
51
|
expiresAt: number | null;
|
|
52
52
|
leaseMs: number | null;
|
|
53
53
|
};
|
|
54
|
+
/** 3c Tier-1 — coarse activity (currentTool + context usage), merged onto the summary. */
|
|
55
|
+
coarse?: {
|
|
56
|
+
currentTool: string | null;
|
|
57
|
+
contextTokens?: number;
|
|
58
|
+
contextPercent?: number;
|
|
59
|
+
};
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
62
|
* Project a `MaestroPlayerInfo` into the wire-stable `PlayerSummaryV1`.
|
package/dist/http/snapshot.js
CHANGED
|
@@ -99,6 +99,12 @@ function toPlayerSummaryV1(p, wireMeta = null) {
|
|
|
99
99
|
...(wireMeta?.runId !== undefined ? { runId: wireMeta.runId } : {}),
|
|
100
100
|
...(wireMeta?.messaging !== undefined ? { messaging: wireMeta.messaging } : {}),
|
|
101
101
|
...(wireMeta?.lease !== undefined ? { lease: wireMeta.lease } : {}),
|
|
102
|
+
// 3c Tier-1 — coarse activity merged onto the summary so the aggregate
|
|
103
|
+
// poll/diff can emit player.activity. currentTool is always present on the
|
|
104
|
+
// coarse object (null = idle); context fields are conditionally included.
|
|
105
|
+
...(wireMeta?.coarse?.currentTool !== undefined ? { currentTool: wireMeta.coarse.currentTool } : {}),
|
|
106
|
+
...(wireMeta?.coarse?.contextTokens !== undefined ? { contextTokens: wireMeta.coarse.contextTokens } : {}),
|
|
107
|
+
...(wireMeta?.coarse?.contextPercent !== undefined ? { contextPercent: wireMeta.coarse.contextPercent } : {}),
|
|
102
108
|
};
|
|
103
109
|
}
|
|
104
110
|
/**
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cue pump — pulls cues queued on the session workflow and injects them into
|
|
3
|
+
* the LIVE Pi session via `sendCustomMessage`, then acks them.
|
|
4
|
+
*
|
|
5
|
+
* Pi has no reverse-RPC into a running session from Temporal, so (like the
|
|
6
|
+
* existing adapters) we poll `pendingMessages` and ack via `markDelivered`.
|
|
7
|
+
*
|
|
8
|
+
* Injection follows D10 cue-delivery semantics:
|
|
9
|
+
* - **deliverAs** — operator cue (`msg.isMaestro`, a human steering from the
|
|
10
|
+
* Maestro dashboard) → `'steer'` (interrupt the in-flight turn so the
|
|
11
|
+
* override lands immediately); peer cue → `'followUp'` (queue behind the
|
|
12
|
+
* current turn rather than interrupting a peer's work).
|
|
13
|
+
* - **triggerTurn — always `true`.** Researcher-confirmed: Pi's `followUp`
|
|
14
|
+
* does NOT self-wake an idle agent, so an unconditional `triggerTurn` is
|
|
15
|
+
* REQUIRED to avoid #18-style silent cue loss when no human is driving. It
|
|
16
|
+
* is a no-op when a turn is already running (the message just queues), so we
|
|
17
|
+
* don't need to race-check the idle state — set it unconditionally.
|
|
18
|
+
*
|
|
19
|
+
* Adapted from Pi's `examples/extensions/file-trigger.ts`.
|
|
20
|
+
*/
|
|
21
|
+
import type { Message } from '../types';
|
|
22
|
+
import type { PiAgentSession } from './pi-types';
|
|
23
|
+
/** Source of pending cues + ack — satisfied by `PiWorkflowClient`. */
|
|
24
|
+
export interface CueSource {
|
|
25
|
+
fetchPending(): Promise<Message[]>;
|
|
26
|
+
ackDelivered(messageIds: string[]): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolves the CURRENT live Pi session at injection time. Re-acquired on every
|
|
30
|
+
* tick rather than captured once, so a session switch (D11) never injects into
|
|
31
|
+
* a stale session. Returns `null` when no session is attached.
|
|
32
|
+
*/
|
|
33
|
+
export type SessionResolver = () => PiAgentSession | null;
|
|
34
|
+
export interface CuePumpOptions {
|
|
35
|
+
source: CueSource;
|
|
36
|
+
resolveSession: SessionResolver;
|
|
37
|
+
/** Poll interval (ms). */
|
|
38
|
+
intervalMs?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare class CuePump {
|
|
41
|
+
private readonly source;
|
|
42
|
+
private readonly resolveSession;
|
|
43
|
+
private readonly intervalMs;
|
|
44
|
+
private timer;
|
|
45
|
+
private draining;
|
|
46
|
+
constructor(opts: CuePumpOptions);
|
|
47
|
+
start(): void;
|
|
48
|
+
stop(): void;
|
|
49
|
+
/**
|
|
50
|
+
* One poll cycle: fetch pending cues, inject each into the live session, ack
|
|
51
|
+
* the ones successfully injected. Re-entrancy guarded so a slow tick never
|
|
52
|
+
* overlaps the next interval.
|
|
53
|
+
*/
|
|
54
|
+
tick(): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Inject one cue into the live session (D10 — see file header). Operator cues
|
|
57
|
+
* `steer` (same-turn priority); peer cues `followUp` (queue). `triggerTurn` is
|
|
58
|
+
* always set: a no-op mid-turn, the required cold-idle wake otherwise.
|
|
59
|
+
*/
|
|
60
|
+
private injectCue;
|
|
61
|
+
}
|