agent-tempo 1.3.1 → 1.4.1
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 +39 -5
- package/README.md +6 -2
- 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 +1 -1
- package/dashboard/package.json +1 -1
- 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/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/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 +32 -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 +250 -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 +88 -0
- package/dist/pi/mission-control/board.js +141 -0
- package/dist/pi/mission-control/extension.d.ts +51 -0
- package/dist/pi/mission-control/extension.js +330 -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 +98 -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 +222 -0
- package/dist/pi/pi-types.js +21 -0
- package/dist/pi/probe.d.ts +99 -0
- package/dist/pi/probe.js +179 -0
- package/dist/pi/render-tools.d.ts +17 -0
- package/dist/pi/render-tools.js +56 -0
- package/dist/pi/reset-pump.d.ts +47 -0
- package/dist/pi/reset-pump.js +85 -0
- package/dist/pi/session-seed.d.ts +74 -0
- package/dist/pi/session-seed.js +103 -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/server-tools.d.ts +2 -0
- package/dist/server-tools.js +50 -46
- package/dist/spawn.d.ts +55 -0
- package/dist/spawn.js +72 -0
- 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 +29 -24
- package/dist/tools/coat-check-get.d.ts +2 -2
- package/dist/tools/coat-check-get.js +38 -33
- 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 +38 -33
- 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 +42 -37
- 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 +340 -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 +31 -26
- 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/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/package.json +4 -1
- package/workflow-bundle.js +97 -6
- package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
- package/dist/tools/helpers.d.ts +0 -21
- package/dist/tools/helpers.js +0 -25
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
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CuePump = void 0;
|
|
4
|
+
const DEFAULT_POLL_MS = 1_000;
|
|
5
|
+
const log = (...args) => {
|
|
6
|
+
// eslint-disable-next-line no-console
|
|
7
|
+
console.error('[agent-tempo:pi]', ...args);
|
|
8
|
+
};
|
|
9
|
+
class CuePump {
|
|
10
|
+
source;
|
|
11
|
+
resolveSession;
|
|
12
|
+
intervalMs;
|
|
13
|
+
timer = null;
|
|
14
|
+
draining = false;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
this.source = opts.source;
|
|
17
|
+
this.resolveSession = opts.resolveSession;
|
|
18
|
+
this.intervalMs = opts.intervalMs ?? DEFAULT_POLL_MS;
|
|
19
|
+
}
|
|
20
|
+
start() {
|
|
21
|
+
if (this.timer)
|
|
22
|
+
return;
|
|
23
|
+
this.timer = setInterval(() => {
|
|
24
|
+
this.tick().catch((err) => log('cue-pump tick failed:', err));
|
|
25
|
+
}, this.intervalMs);
|
|
26
|
+
if (typeof this.timer.unref === 'function')
|
|
27
|
+
this.timer.unref();
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
if (this.timer) {
|
|
31
|
+
clearInterval(this.timer);
|
|
32
|
+
this.timer = null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* One poll cycle: fetch pending cues, inject each into the live session, ack
|
|
37
|
+
* the ones successfully injected. Re-entrancy guarded so a slow tick never
|
|
38
|
+
* overlaps the next interval.
|
|
39
|
+
*/
|
|
40
|
+
async tick() {
|
|
41
|
+
if (this.draining)
|
|
42
|
+
return;
|
|
43
|
+
this.draining = true;
|
|
44
|
+
try {
|
|
45
|
+
const pending = await this.source.fetchPending();
|
|
46
|
+
if (pending.length === 0)
|
|
47
|
+
return;
|
|
48
|
+
const session = this.resolveSession();
|
|
49
|
+
if (!session) {
|
|
50
|
+
// No live session yet — leave cues queued; next tick retries.
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const delivered = [];
|
|
54
|
+
for (const msg of pending) {
|
|
55
|
+
try {
|
|
56
|
+
await this.injectCue(session, msg);
|
|
57
|
+
delivered.push(msg.id);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
log(`failed to inject cue ${msg.id}:`, err);
|
|
61
|
+
// Stop on first failure — preserve ordering; retry next tick.
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await this.source.ackDelivered(delivered);
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
this.draining = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Inject one cue into the live session (D10 — see file header). Operator cues
|
|
73
|
+
* `steer` (same-turn priority); peer cues `followUp` (queue). `triggerTurn` is
|
|
74
|
+
* always set: a no-op mid-turn, the required cold-idle wake otherwise.
|
|
75
|
+
*/
|
|
76
|
+
async injectCue(session, msg) {
|
|
77
|
+
const content = msg.from ? `[cue from ${msg.from}] ${msg.text}` : msg.text;
|
|
78
|
+
// LOAD-BEARING Pi-runtime invariant (D10) — confirmed sound through Pi 0.78.x
|
|
79
|
+
// (researcher-cited; a D6 "behaviors-to-revalidate-on-bump" item):
|
|
80
|
+
// peer cue = { deliverAs: 'followUp', triggerTurn: true } → QUEUES; drains
|
|
81
|
+
// when the agent goes idle, NEVER preempts a running turn. triggerTurn only
|
|
82
|
+
// wakes a cold-idle session (followUp alone won't start one); it is a no-op
|
|
83
|
+
// while a turn is in flight.
|
|
84
|
+
// operator cue = { deliverAs: 'steer', triggerTurn: true } → same-turn PRIORITY:
|
|
85
|
+
// injected after the current tool batch, before the next LLM call. NOT a hard
|
|
86
|
+
// mid-tool abort (only RPC abort / AbortSignal hard-interrupts a running tool).
|
|
87
|
+
// The guarantee this comment protects: a future Pi version MUST keep followUp
|
|
88
|
+
// non-interrupting AND triggerTurn a no-op-while-busy. If that regresses, peer
|
|
89
|
+
// cues silently become preemptions, defeating operator-vs-peer. Not unit-testable
|
|
90
|
+
// here (the session is mocked) — locked by researcher confirmation + the D6 Pi
|
|
91
|
+
// version floor (≥ #2860 + #5115) + a real-Pi mid-turn integration smoke.
|
|
92
|
+
await session.sendCustomMessage({ customType: 'cue', content, display: true }, { deliverAs: msg.isMaestro ? 'steer' : 'followUp', triggerTurn: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.CuePump = CuePump;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Client } from '@temporalio/client';
|
|
2
|
+
import { type Config } from '../config';
|
|
3
|
+
import type { ExtensionAPI, PiAgentSession } from './pi-types';
|
|
4
|
+
/** Runtime mode. Headless = recruited unsupervised player (MD-C gate active). */
|
|
5
|
+
export type PiExtensionMode = 'interactive' | 'headless';
|
|
6
|
+
export type PiToolAccess = 'restricted' | 'standard' | 'full';
|
|
7
|
+
export interface PiExtensionOptions {
|
|
8
|
+
/** Default `'interactive'`. Headless installs the MD-C tool_call gate. */
|
|
9
|
+
mode?: PiExtensionMode;
|
|
10
|
+
/** MD-C tool-class policy (headless only). Default `'restricted'`. */
|
|
11
|
+
toolAccess?: PiToolAccess;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build the Pi extension factory. `mode='headless'` installs the MD-C tool_call
|
|
15
|
+
* gate; `mode='interactive'` (default) does not (the human owns their machine).
|
|
16
|
+
*/
|
|
17
|
+
export declare function createPiExtension(options?: PiExtensionOptions): (pi: ExtensionAPI) => void;
|
|
18
|
+
/**
|
|
19
|
+
* RELIABLE detach for the headless exit sequence (Phase 3a). Headless owns its
|
|
20
|
+
* exit loop, so — unlike interactive's best-effort `quit` path — it can AWAIT a
|
|
21
|
+
* clean detach before disposing the SDK session. Ordering (architect ruling):
|
|
22
|
+
* stopHeartbeat → requestDetach → adapterExited (all inside `wf.detach`) → unmap.
|
|
23
|
+
* The caller then calls `session.dispose()`; the dispose-fired `session_shutdown`
|
|
24
|
+
* finds no mapped runtime → no-op (avoids double-detach). Detaches every runtime
|
|
25
|
+
* in the process (headless = one player per process).
|
|
26
|
+
*/
|
|
27
|
+
export declare function detachAllPiRuntimesForExit(): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Headless-only: wire the live Pi SDK session onto a runtime so the cue pump can
|
|
30
|
+
* inject into it. The interactive CLI's `session_start` payload carries
|
|
31
|
+
* `session`, but the headless SDK's DEFAULT session_start payload does NOT (it's
|
|
32
|
+
* `{ type, reason }`) — so `attachOrRebind` sets `rt.session = null` and the cue
|
|
33
|
+
* pump's `resolveSession` returns null (every cue is dropped). The headless entry
|
|
34
|
+
* HOLDS the session from `createAgentSession`, so it calls this after
|
|
35
|
+
* `bindExtensions` (by which point the runtime exists + has claimed) to set it.
|
|
36
|
+
* (3a live smoke — devops.)
|
|
37
|
+
*/
|
|
38
|
+
export declare function setRuntimeSession(workflowId: string, session: PiAgentSession): void;
|
|
39
|
+
/** Override the Temporal connection factory (inject a fake Client). */
|
|
40
|
+
export declare function __setPiClientFactoryForTests(factory: (config: Config) => Promise<Client>): void;
|
|
41
|
+
/** Stop timers, clear the per-player runtime map + shared-client singletons + factory. */
|
|
42
|
+
export declare function __resetPiRuntimesForTests(): void;
|
|
43
|
+
/** Default export — interactive-mode extension (the human `pi` CLI entry). */
|
|
44
|
+
declare const piExtension: (pi: ExtensionAPI) => void;
|
|
45
|
+
export default piExtension;
|