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.
Files changed (199) hide show
  1. package/CLAUDE.md +39 -5
  2. package/README.md +6 -2
  3. package/dashboard/dist/assets/{index-D6Xyje_n.js → index-jmYe6rmS.js} +2 -2
  4. package/dashboard/dist/assets/index-jmYe6rmS.js.map +1 -0
  5. package/dashboard/dist/index.html +1 -1
  6. package/dashboard/package.json +1 -1
  7. package/dist/activities/outbox.d.ts +30 -1
  8. package/dist/activities/outbox.js +96 -3
  9. package/dist/adapters/base.js +5 -0
  10. package/dist/adapters/index.d.ts +1 -1
  11. package/dist/adapters/index.js +7 -0
  12. package/dist/adapters/pi/adapter.d.ts +2 -0
  13. package/dist/adapters/pi/adapter.js +43 -0
  14. package/dist/adapters/pi/index.d.ts +16 -0
  15. package/dist/adapters/pi/index.js +10 -0
  16. package/dist/client/core.js +9 -2
  17. package/dist/client/interface.d.ts +6 -0
  18. package/dist/config.d.ts +79 -0
  19. package/dist/config.js +74 -0
  20. package/dist/daemon.js +32 -1
  21. package/dist/http/aggregate.d.ts +22 -1
  22. package/dist/http/aggregate.js +41 -0
  23. package/dist/http/auth.d.ts +94 -8
  24. package/dist/http/auth.js +93 -9
  25. package/dist/http/body.d.ts +4 -1
  26. package/dist/http/body.js +6 -3
  27. package/dist/http/event-bus.js +1 -0
  28. package/dist/http/event-types.d.ts +34 -2
  29. package/dist/http/event-types.js +1 -0
  30. package/dist/http/gate-audit.d.ts +12 -0
  31. package/dist/http/gate-audit.js +95 -0
  32. package/dist/http/gate-registry.d.ts +167 -0
  33. package/dist/http/gate-registry.js +163 -0
  34. package/dist/http/gate-routes.d.ts +48 -0
  35. package/dist/http/gate-routes.js +102 -0
  36. package/dist/http/ingest-registry.d.ts +30 -0
  37. package/dist/http/ingest-registry.js +108 -0
  38. package/dist/http/inner-loop-routes.d.ts +66 -0
  39. package/dist/http/inner-loop-routes.js +182 -0
  40. package/dist/http/inner-loop.d.ts +92 -0
  41. package/dist/http/inner-loop.js +155 -0
  42. package/dist/http/server.d.ts +38 -3
  43. package/dist/http/server.js +211 -6
  44. package/dist/http/snapshot.d.ts +6 -0
  45. package/dist/http/snapshot.js +6 -0
  46. package/dist/pi/cue-pump.d.ts +61 -0
  47. package/dist/pi/cue-pump.js +95 -0
  48. package/dist/pi/extension.d.ts +45 -0
  49. package/dist/pi/extension.js +407 -0
  50. package/dist/pi/gate-client.d.ts +54 -0
  51. package/dist/pi/gate-client.js +136 -0
  52. package/dist/pi/headless.d.ts +85 -0
  53. package/dist/pi/headless.js +250 -0
  54. package/dist/pi/index.d.ts +28 -0
  55. package/dist/pi/index.js +43 -0
  56. package/dist/pi/inner-loop-client.d.ts +67 -0
  57. package/dist/pi/inner-loop-client.js +164 -0
  58. package/dist/pi/inner-loop-publisher.d.ts +187 -0
  59. package/dist/pi/inner-loop-publisher.js +236 -0
  60. package/dist/pi/lazy-proxy.d.ts +37 -0
  61. package/dist/pi/lazy-proxy.js +55 -0
  62. package/dist/pi/mission-control/actions.d.ts +48 -0
  63. package/dist/pi/mission-control/actions.js +98 -0
  64. package/dist/pi/mission-control/board.d.ts +88 -0
  65. package/dist/pi/mission-control/board.js +141 -0
  66. package/dist/pi/mission-control/extension.d.ts +51 -0
  67. package/dist/pi/mission-control/extension.js +330 -0
  68. package/dist/pi/mission-control/index.d.ts +15 -0
  69. package/dist/pi/mission-control/index.js +32 -0
  70. package/dist/pi/mission-control/inner-tail.d.ts +48 -0
  71. package/dist/pi/mission-control/inner-tail.js +76 -0
  72. package/dist/pi/mission-control/pi-ui.d.ts +43 -0
  73. package/dist/pi/mission-control/pi-ui.js +10 -0
  74. package/dist/pi/mission-control/render.d.ts +6 -0
  75. package/dist/pi/mission-control/render.js +98 -0
  76. package/dist/pi/phase-driver.d.ts +74 -0
  77. package/dist/pi/phase-driver.js +122 -0
  78. package/dist/pi/pi-types.d.ts +222 -0
  79. package/dist/pi/pi-types.js +21 -0
  80. package/dist/pi/probe.d.ts +99 -0
  81. package/dist/pi/probe.js +179 -0
  82. package/dist/pi/render-tools.d.ts +17 -0
  83. package/dist/pi/render-tools.js +56 -0
  84. package/dist/pi/reset-pump.d.ts +47 -0
  85. package/dist/pi/reset-pump.js +85 -0
  86. package/dist/pi/session-seed.d.ts +74 -0
  87. package/dist/pi/session-seed.js +103 -0
  88. package/dist/pi/tool-capability.d.ts +60 -0
  89. package/dist/pi/tool-capability.js +156 -0
  90. package/dist/pi/workflow-client.d.ts +158 -0
  91. package/dist/pi/workflow-client.js +289 -0
  92. package/dist/pi/zod-to-typebox.d.ts +74 -0
  93. package/dist/pi/zod-to-typebox.js +191 -0
  94. package/dist/server-tools.d.ts +2 -0
  95. package/dist/server-tools.js +50 -46
  96. package/dist/spawn.d.ts +55 -0
  97. package/dist/spawn.js +72 -0
  98. package/dist/tools/agent-types.d.ts +2 -2
  99. package/dist/tools/agent-types.js +22 -17
  100. package/dist/tools/attachment-info.d.ts +2 -2
  101. package/dist/tools/attachment-info.js +38 -33
  102. package/dist/tools/broadcast.d.ts +2 -2
  103. package/dist/tools/broadcast.js +69 -64
  104. package/dist/tools/cancel-stage.d.ts +2 -2
  105. package/dist/tools/cancel-stage.js +20 -15
  106. package/dist/tools/clear-state.d.ts +2 -2
  107. package/dist/tools/clear-state.js +25 -20
  108. package/dist/tools/coat-check-evict.d.ts +2 -2
  109. package/dist/tools/coat-check-evict.js +29 -24
  110. package/dist/tools/coat-check-get.d.ts +2 -2
  111. package/dist/tools/coat-check-get.js +38 -33
  112. package/dist/tools/coat-check-list.d.ts +2 -2
  113. package/dist/tools/coat-check-list.js +48 -43
  114. package/dist/tools/coat-check-put.d.ts +2 -2
  115. package/dist/tools/coat-check-put.js +38 -33
  116. package/dist/tools/cue.d.ts +2 -2
  117. package/dist/tools/cue.js +57 -52
  118. package/dist/tools/descriptor.d.ts +72 -0
  119. package/dist/tools/descriptor.js +39 -0
  120. package/dist/tools/destroy.d.ts +2 -2
  121. package/dist/tools/destroy.js +153 -148
  122. package/dist/tools/ensemble.d.ts +2 -2
  123. package/dist/tools/ensemble.js +71 -66
  124. package/dist/tools/evaluate-gate.d.ts +2 -2
  125. package/dist/tools/evaluate-gate.js +33 -27
  126. package/dist/tools/fetch-state.d.ts +2 -2
  127. package/dist/tools/fetch-state.js +42 -37
  128. package/dist/tools/gates.d.ts +2 -2
  129. package/dist/tools/gates.js +39 -34
  130. package/dist/tools/hosts.d.ts +2 -2
  131. package/dist/tools/hosts.js +25 -20
  132. package/dist/tools/listen.d.ts +2 -2
  133. package/dist/tools/listen.js +23 -18
  134. package/dist/tools/load-lineup.d.ts +2 -2
  135. package/dist/tools/load-lineup.js +324 -319
  136. package/dist/tools/migrate.d.ts +2 -2
  137. package/dist/tools/migrate.js +45 -40
  138. package/dist/tools/pause.d.ts +2 -2
  139. package/dist/tools/pause.js +34 -29
  140. package/dist/tools/play.d.ts +2 -2
  141. package/dist/tools/play.js +53 -48
  142. package/dist/tools/quality-gate.d.ts +2 -2
  143. package/dist/tools/quality-gate.js +26 -21
  144. package/dist/tools/recall.d.ts +2 -2
  145. package/dist/tools/recall.js +32 -27
  146. package/dist/tools/recruit.d.ts +2 -2
  147. package/dist/tools/recruit.js +340 -256
  148. package/dist/tools/release.d.ts +2 -2
  149. package/dist/tools/release.js +85 -80
  150. package/dist/tools/report.d.ts +2 -2
  151. package/dist/tools/report.js +28 -23
  152. package/dist/tools/reset.d.ts +3 -0
  153. package/dist/tools/reset.js +51 -0
  154. package/dist/tools/restart.d.ts +2 -2
  155. package/dist/tools/restart.js +51 -46
  156. package/dist/tools/restore.d.ts +2 -2
  157. package/dist/tools/restore.js +76 -71
  158. package/dist/tools/save-lineup.d.ts +2 -2
  159. package/dist/tools/save-lineup.js +32 -27
  160. package/dist/tools/save-state.d.ts +2 -2
  161. package/dist/tools/save-state.js +31 -26
  162. package/dist/tools/schedule.d.ts +2 -2
  163. package/dist/tools/schedule.js +133 -128
  164. package/dist/tools/schedules.d.ts +2 -2
  165. package/dist/tools/schedules.js +41 -36
  166. package/dist/tools/set-ensemble-description.d.ts +2 -2
  167. package/dist/tools/set-ensemble-description.js +26 -21
  168. package/dist/tools/set-name.d.ts +2 -2
  169. package/dist/tools/set-name.js +38 -33
  170. package/dist/tools/set-part.d.ts +2 -2
  171. package/dist/tools/set-part.js +20 -15
  172. package/dist/tools/shutdown.d.ts +2 -2
  173. package/dist/tools/shutdown.js +39 -34
  174. package/dist/tools/stage.d.ts +2 -2
  175. package/dist/tools/stage.js +28 -23
  176. package/dist/tools/stages.d.ts +2 -2
  177. package/dist/tools/stages.js +36 -31
  178. package/dist/tools/unschedule.d.ts +2 -2
  179. package/dist/tools/unschedule.js +30 -25
  180. package/dist/tools/who-am-i.d.ts +2 -2
  181. package/dist/tools/who-am-i.js +36 -31
  182. package/dist/tools/worktree.d.ts +2 -2
  183. package/dist/tools/worktree.js +134 -129
  184. package/dist/tui/index.js +6 -6
  185. package/dist/types.d.ts +47 -2
  186. package/dist/types.js +1 -1
  187. package/dist/utils/default-part.js +1 -0
  188. package/dist/utils/sdk-probe.d.ts +23 -0
  189. package/dist/utils/sdk-probe.js +46 -7
  190. package/dist/worker.d.ts +3 -1
  191. package/dist/worker.js +6 -2
  192. package/dist/workflows/session.js +70 -2
  193. package/dist/workflows/signals.d.ts +32 -2
  194. package/dist/workflows/signals.js +25 -2
  195. package/package.json +4 -1
  196. package/workflow-bundle.js +97 -6
  197. package/dashboard/dist/assets/index-D6Xyje_n.js.map +0 -1
  198. package/dist/tools/helpers.d.ts +0 -21
  199. package/dist/tools/helpers.js +0 -25
package/dist/daemon.js CHANGED
@@ -66,6 +66,10 @@ 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");
69
73
  const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
70
74
  const daemon_1 = require("./cli/daemon");
71
75
  const client_3 = require("./client");
@@ -828,6 +832,19 @@ async function main() {
828
832
  // #94/#95 PR-2 — aggregate poll loop + per-ensemble buses. Owned by
829
833
  // the daemon process; `close()` drains every per-ensemble bus.
830
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));
831
848
  const shutdown = () => {
832
849
  if (shuttingDown)
833
850
  return;
@@ -861,6 +878,12 @@ async function main() {
861
878
  // sockets — preventing wasted work in the drain window.
862
879
  aggregateRunner?.close();
863
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();
864
887
  sharedWorker?.shutdown();
865
888
  hostWorker?.shutdown();
866
889
  };
@@ -868,7 +891,7 @@ async function main() {
868
891
  process.on('SIGINT', shutdown);
869
892
  // Create workers (signal handlers already active via mutable refs)
870
893
  log(`Connecting to Temporal at ${config.temporalAddress} (namespace: ${config.temporalNamespace})`);
871
- const workers = await (0, worker_1.createWorkers)(config);
894
+ const workers = await (0, worker_1.createWorkers)(config, ingestTokens, gate);
872
895
  sharedWorker = workers.sharedWorker;
873
896
  hostWorker = workers.hostWorker;
874
897
  log('Workers created — processing tasks');
@@ -953,6 +976,14 @@ async function main() {
953
976
  taskQueue: config.taskQueue,
954
977
  version: daemonVersion(),
955
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,
956
987
  });
957
988
  log(`HTTP listening on http://${httpServerHandle.bindAddr}:${httpServerHandle.port}`);
958
989
  log(`Aggregate poll loop running (bootEpoch=${bootEpoch})`);
@@ -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
  */
@@ -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,
@@ -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
- * Load (or auto-generate) the daemon's HTTP bearer token.
55
- *
56
- * - When `bearerRequired` is true and the persisted config has no
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
- * Load (or auto-generate) the daemon's HTTP bearer token.
156
- *
157
- * - When `bearerRequired` is true and the persisted config has no
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
+ }
@@ -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
package/dist/http/body.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ALLOWED_AGENTS_PROD = exports.ALLOWED_AGENTS_DEV = exports.BODY_INVALID_JSON = exports.BODY_TOO_LARGE = exports.WRITE_BODY_MAX = void 0;
3
+ exports.ALLOWED_AGENTS_PROD = exports.ALLOWED_AGENTS_DEV = exports.BODY_INVALID_JSON = exports.BODY_TOO_LARGE = exports.INGEST_BODY_MAX = exports.WRITE_BODY_MAX = void 0;
4
4
  exports.readJsonBody = readJsonBody;
5
5
  exports.stringField = stringField;
6
6
  exports.allowedAgentsForCurrentMode = allowedAgentsForCurrentMode;
@@ -12,6 +12,9 @@ const responses_1 = require("./responses");
12
12
  const validation_1 = require("../utils/validation");
13
13
  /** Hard cap on incoming JSON body size (1 MiB). */
14
14
  exports.WRITE_BODY_MAX = 1024 * 1024;
15
+ /** 3c Tier-2 ingest cap (32 KiB) — the DOS backstop for `/inner/ingest`; the
16
+ * source already ~2KB-truncates summaries, so real frames are far smaller. */
17
+ exports.INGEST_BODY_MAX = 32 * 1024;
15
18
  exports.BODY_TOO_LARGE = Symbol('body-too-large');
16
19
  exports.BODY_INVALID_JSON = Symbol('body-invalid-json');
17
20
  /**
@@ -24,13 +27,13 @@ exports.BODY_INVALID_JSON = Symbol('body-invalid-json');
24
27
  * handler ends. Explicit `req.destroy()` would race the response
25
28
  * write — left alone.
26
29
  */
27
- async function readJsonBody(req) {
30
+ async function readJsonBody(req, maxBytes = exports.WRITE_BODY_MAX) {
28
31
  const chunks = [];
29
32
  let total = 0;
30
33
  for await (const chunk of req) {
31
34
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
32
35
  total += buf.length;
33
- if (total > exports.WRITE_BODY_MAX)
36
+ if (total > maxBytes)
34
37
  return exports.BODY_TOO_LARGE;
35
38
  chunks.push(buf);
36
39
  }
@@ -64,6 +64,7 @@ const NON_ESSENTIAL_AFTER_THROTTLE = new Set([
64
64
  function topicOf(kind) {
65
65
  switch (kind) {
66
66
  case 'player.phase_changed': return 'phase';
67
+ case 'player.activity': return 'phase';
67
68
  case 'chat.appended':
68
69
  case 'chat.compressed': return 'chat';
69
70
  case 'flags.changed': return 'flags';
@@ -152,7 +152,7 @@ export interface PlayerSummaryV1 {
152
152
  * stability rule at §6 — adding new adapters in future versions remains
153
153
  * non-breaking, removing one requires `/v2/`. See #535.
154
154
  */
155
- agentType: 'claude' | 'copilot' | 'mock' | 'claude-api' | 'opencode' | 'claude-code-headless';
155
+ agentType: 'claude' | 'copilot' | 'mock' | 'claude-api' | 'opencode' | 'claude-code-headless' | 'pi';
156
156
  playerType?: string;
157
157
  /** Authoritative attachment phase (post-v0.26 — see WIRE-PROTOCOL.md). */
158
158
  phase?: AttachmentPhase;
@@ -194,6 +194,20 @@ export interface PlayerSummaryV1 {
194
194
  /** Q5.6 — ISO timestamp of the most recent activity. Already on
195
195
  * `MaestroPlayerInfo`; passed through verbatim by `toPlayerSummaryV1`. */
196
196
  lastActivityAt?: string;
197
+ /**
198
+ * 3c Tier-1 coarse observability — the tool the player is currently
199
+ * executing, or `null` when idle/between tools. Sourced from session
200
+ * metadata via the heartbeat piggyback (~30s freshness); the live,
201
+ * fine-grained tail is the off-wire `/inner` side-channel (MD-F). Additive.
202
+ */
203
+ currentTool?: string | null;
204
+ /**
205
+ * 3c Tier-1 coarse — estimated context tokens in use (pull-only, from Pi's
206
+ * `getContextUsage()`; `null`/absent right after compaction). Additive.
207
+ */
208
+ contextTokens?: number;
209
+ /** 3c Tier-1 coarse — context usage as a percentage of the model window. Additive. */
210
+ contextPercent?: number;
197
211
  }
198
212
  /**
199
213
  * The eventId token used in the `id:` line of the SSE frame and as the
@@ -269,7 +283,7 @@ export declare const PR1_SENTINEL_EVENT_ID: EventIdToken;
269
283
  * **Append-only**. Do not remove. New event types ship as additive `/v1/`
270
284
  * additions; removals require `/v2/`.
271
285
  */
272
- export declare const SSE_EVENT_KINDS: readonly ["snapshot", "gap", "throttled", "heartbeat", "ensemble.created", "ensemble.destroyed", "player.added", "player.removed", "player.phase_changed", "chat.appended", "chat.compressed", "flags.changed", "schedules.changed", "host_profile.changed"];
286
+ export declare const SSE_EVENT_KINDS: readonly ["snapshot", "gap", "throttled", "heartbeat", "ensemble.created", "ensemble.destroyed", "player.added", "player.removed", "player.phase_changed", "chat.appended", "chat.compressed", "flags.changed", "schedules.changed", "host_profile.changed", "player.activity"];
273
287
  export type SseEventKind = (typeof SSE_EVENT_KINDS)[number];
274
288
  /** Common envelope for every event. */
275
289
  export interface SseEventBase {
@@ -364,6 +378,24 @@ export type TempoEvent = (SseEventBase & {
364
378
  }) | (SseEventBase & {
365
379
  type: 'host_profile.changed';
366
380
  payload: HostProfile;
381
+ }) | (SseEventBase & {
382
+ type: 'player.activity';
383
+ /**
384
+ * 3c Tier-1 coarse activity (MD-F). Emitted by the aggregate poll/diff
385
+ * when a player's `currentTool` or context usage changes between polls.
386
+ * `busy/idle` is DERIVED consumer-side from the player's phase
387
+ * (busy = phase==='processing'); `activityCount`/`lastActivityAt` already
388
+ * ride `PlayerSummaryV1`. This is the ON-wire coarse tier; the fine,
389
+ * live inner tail is the off-wire `/inner` side-channel.
390
+ */
391
+ payload: {
392
+ playerId: string;
393
+ ensemble: string;
394
+ currentTool: string | null;
395
+ contextTokens?: number;
396
+ contextPercent?: number;
397
+ at: string;
398
+ };
367
399
  });
368
400
  /**
369
401
  * Subset of event categories the server can pre-filter on `?topics=...`.
@@ -33,4 +33,5 @@ exports.SSE_EVENT_KINDS = [
33
33
  'flags.changed',
34
34
  'schedules.changed',
35
35
  'host_profile.changed',
36
+ 'player.activity',
36
37
  ];
@@ -0,0 +1,12 @@
1
+ import type { GateAuditSink } from './gate-registry';
2
+ /** Root of the per-player gate-audit tree. */
3
+ export declare function gateAuditRoot(): string;
4
+ /** Absolute JSONL path for a (ensemble, workflowId) pair under `root`. */
5
+ export declare function gateAuditPath(ensemble: string, workflowId: string, root?: string): string;
6
+ /**
7
+ * Build the daemon's audit sink. Returns a {@link GateAuditSink} that appends one
8
+ * JSON line per record. Append + mkdir are synchronous (durable-before-return);
9
+ * any I/O error is logged + swallowed so a disk problem never wedges a gate
10
+ * decision. `root` is injectable for tests (defaults to {@link gateAuditRoot}).
11
+ */
12
+ export declare function createGateAuditSink(root?: string): GateAuditSink;