@agentproto/runtime 0.1.1 → 0.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/dist/index.d.ts CHANGED
@@ -1,10 +1,155 @@
1
1
  import { DoctypeSpec } from '@agentproto/manifest';
2
2
  import { B as BuildHeartbeatAgent } from './heartbeat-COGpMrJS.js';
3
3
  export { H as HeartbeatAgent, a as HeartbeatRunner, R as RuntimeEvent, b as RuntimeEvents, p as parseDuration } from './heartbeat-COGpMrJS.js';
4
+ import { AcpMcpServer } from '@agentproto/acp';
4
5
  import { ChildProcess } from 'node:child_process';
6
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ import { AdapterHandle, AdapterLister } from '@agentproto/adapter-kit';
5
8
  import { WorkspaceFs } from './workspace-fs.js';
6
9
  export { createWorkspaceFs } from './workspace-fs.js';
7
10
  export { ConversationMeta, ConversationStore, ConversationTurn, fileConversationStore } from './conversations.js';
11
+ import { StepCache } from '@agentproto/workflow-runtime';
12
+ export { PROVIDER_ENV_VARS, ProviderEntry, ProvidersFile, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey } from './providers-store.js';
13
+
14
+ /**
15
+ * In-process pub/sub bus for session lifecycle events. Separate from
16
+ * RuntimeEvents (global daemon bus) — session events are scoped to
17
+ * individual sessions and fire at turn-level granularity.
18
+ *
19
+ * Consumers: EventRing (session_events_poll cursor), WebhookNotifier
20
+ * (fire-and-forget HTTP), RoutineRunner (state machine fan-in),
21
+ * and session_monitor MCP tool (long-poll multiplexed).
22
+ */
23
+ type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:exited" | "session:command-done" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
24
+ /**
25
+ * Structured detail on why a session is awaiting input, when derivable.
26
+ * `source: "structured"` — a driver-reported ACP-style prompt (e.g. a tool
27
+ * permission request with real options). `source: "heuristic"` — a
28
+ * best-effort guess from the tail of the transcript (trailing "?" plus
29
+ * an optional enumerated option list) for drivers that don't report
30
+ * structured prompts. Absent entirely when neither could be determined —
31
+ * callers still have the plain `awaitingInput` boolean in that case.
32
+ */
33
+ interface SessionAwaitingQuestion {
34
+ text: string;
35
+ options?: string[];
36
+ source: "structured" | "heuristic";
37
+ }
38
+ interface SessionTurnEndEvent {
39
+ type: "session:turn-end";
40
+ sessionId: string;
41
+ awaitingInput: boolean;
42
+ label?: string;
43
+ ts: string;
44
+ question?: SessionAwaitingQuestion;
45
+ /**
46
+ * The adapter's stream `turn-end` reason, when the driver reports one
47
+ * (e.g. `"completed"`, `"cancelled"`, `"max_turns"`, or
48
+ * `"watchdog-timeout"` when the ACP client's turn-idle watchdog fired
49
+ * because the adapter went silent — see
50
+ * `@agentproto/acp/client`'s `AcpClientOptions.turnIdleTimeoutMs`).
51
+ * Absent for drivers that don't report a reason.
52
+ */
53
+ reason?: string;
54
+ }
55
+ interface SessionAwaitingInputEvent {
56
+ type: "session:awaiting-input";
57
+ sessionId: string;
58
+ label?: string;
59
+ ts: string;
60
+ question?: SessionAwaitingQuestion;
61
+ }
62
+ interface SessionExitedEvent {
63
+ type: "session:exited";
64
+ sessionId: string;
65
+ exitCode?: number;
66
+ status: "exited" | "killed" | "error";
67
+ label?: string;
68
+ ts: string;
69
+ }
70
+ /** Emitted when command_execute finishes. commandId matches the id
71
+ * returned by the command_execute MCP tool. */
72
+ interface SessionCommandDoneEvent {
73
+ type: "session:command-done";
74
+ sessionId: string;
75
+ commandId: string;
76
+ exitCode: number;
77
+ ts: string;
78
+ }
79
+ /** Emitted by the supervisor when a completion policy's gate passes. */
80
+ interface PolicyPassedEvent {
81
+ type: "policy:passed";
82
+ policyId: string;
83
+ sessionId: string;
84
+ ts: string;
85
+ }
86
+ /** Emitted by the supervisor when a completion policy's gate fails. */
87
+ interface PolicyFailedEvent {
88
+ type: "policy:failed";
89
+ policyId: string;
90
+ sessionId: string;
91
+ exitCode?: number;
92
+ ts: string;
93
+ }
94
+ /**
95
+ * Emitted by the supervisor (WP5) when a `then:"commit"` policy's gate passes
96
+ * but `requireHumanAck` is set: the commit is staged-and-ready but NOT yet
97
+ * executed. Carries the exact paths + message that `policy_ack(approve:true)`
98
+ * will commit. The policy sits in `awaiting-ack` until acked.
99
+ */
100
+ interface PolicyCommitReadyEvent {
101
+ type: "policy:commit-ready";
102
+ policyId: string;
103
+ sessionId: string;
104
+ paths: string[];
105
+ message: string;
106
+ ts: string;
107
+ }
108
+ /** Emitted by the supervisor (WP5) when a `then:"commit"` policy has actually
109
+ * committed — either directly (requireHumanAck:false) or after an approving
110
+ * `policy_ack`. Carries the resulting commit sha. */
111
+ interface PolicyCommittedEvent {
112
+ type: "policy:committed";
113
+ policyId: string;
114
+ sessionId: string;
115
+ sha: string;
116
+ paths: string[];
117
+ message: string;
118
+ ts: string;
119
+ }
120
+ /** Emitted by CronScheduler when a job fires (before the action runs). */
121
+ interface CronFiredEvent {
122
+ type: "cron:fired";
123
+ jobId: string;
124
+ label?: string;
125
+ ts: string;
126
+ }
127
+ /** Emitted by CronScheduler when a job action completes successfully. */
128
+ interface CronSucceededEvent {
129
+ type: "cron:succeeded";
130
+ jobId: string;
131
+ label?: string;
132
+ summary: string;
133
+ ts: string;
134
+ }
135
+ /** Emitted by CronScheduler when a job action fails. */
136
+ interface CronFailedEvent {
137
+ type: "cron:failed";
138
+ jobId: string;
139
+ label?: string;
140
+ error: string;
141
+ ts: string;
142
+ }
143
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
144
+ interface SessionEventBus {
145
+ emit(ev: SessionEvent): void;
146
+ /** Subscribe to a specific event type. Returns an unsubscribe fn. */
147
+ on<E extends SessionEventType>(type: E, handler: (ev: Extract<SessionEvent, {
148
+ type: E;
149
+ }>) => void): () => void;
150
+ /** Subscribe to all event types. Returns an unsubscribe fn. */
151
+ onAny(handler: (ev: SessionEvent) => void): () => void;
152
+ }
8
153
 
9
154
  /**
10
155
  * Sessions registry — tracks long-lived child processes spawned by
@@ -36,6 +181,12 @@ export { ConversationMeta, ConversationStore, ConversationTurn, fileConversation
36
181
  */
37
182
  interface AgentSessionLike {
38
183
  sessionId: string;
184
+ /** OS-level process id of the spawned child, when the driver owns a
185
+ * real subprocess. Mirrored onto `SessionDescriptor.pid` at
186
+ * `spawnAgent` time so `processAlive` can be computed without
187
+ * adapter-specific forensics. Undefined for drivers that don't
188
+ * expose a process (e.g. a future non-subprocess transport). */
189
+ pid?: number;
39
190
  send(message: unknown): AsyncIterable<AgentStreamEvent>;
40
191
  cancel(): Promise<void>;
41
192
  close(): Promise<void>;
@@ -69,7 +220,15 @@ type PtyFactory = (opts: PtyFactoryOptions) => PtyProcess;
69
220
  interface AgentStreamEvent {
70
221
  kind: string;
71
222
  text?: string;
223
+ /** Correlates a "tool-call" with its later "tool-result" (and an
224
+ * "agent-prompt" with the permission it's asking about) — see
225
+ * @agentproto/acp's `StreamEvent`. */
226
+ toolCallId?: string;
72
227
  toolName?: string;
228
+ /** Tool-call input, e.g. an ACP `tool_call`'s `arguments` — see @agentproto/acp's `StreamEvent`. */
229
+ arguments?: unknown;
230
+ /** Tool-call output, e.g. an ACP `tool_call_update`'s `result` — see @agentproto/acp's `StreamEvent`. */
231
+ result?: unknown;
73
232
  isError?: boolean;
74
233
  reason?: string;
75
234
  error?: {
@@ -77,8 +236,32 @@ interface AgentStreamEvent {
77
236
  code?: number;
78
237
  data?: unknown;
79
238
  };
239
+ /** Structured options offered by an "agent-prompt" event (e.g. an ACP
240
+ * `requestPermission` callback surfaced as a clarifying question rather
241
+ * than auto-answered). Typed `unknown` — like `@agentproto/acp`'s own
242
+ * `StreamEvent["options"]` — so this structural type stays assignable
243
+ * from any driver's runtime session without a hard dependency on its
244
+ * exact option shape (`{optionId,name,kind}` for ACP, or whatever a
245
+ * future driver reports); `normalizeAgentPromptOptions` narrows it
246
+ * defensively at the one place it's consumed. */
247
+ options?: unknown;
248
+ /** "plan" event entries — see @agentproto/acp's `StreamEvent`'s `plan` kind. */
249
+ entries?: Array<{
250
+ content: string;
251
+ priority: string;
252
+ status: string;
253
+ }>;
254
+ /** "usage_update" context-window size (tokens). */
255
+ size?: number;
256
+ /** "usage_update" tokens currently in context. */
257
+ used?: number;
258
+ /** "usage_update" cumulative session cost, when the adapter reports one. */
259
+ cost?: {
260
+ amount: number;
261
+ currency: string;
262
+ };
80
263
  }
81
- type SessionKind = "terminal" | "agent-cli" | "command";
264
+ type SessionKind = "terminal" | "agent-cli" | "command" | "browser";
82
265
  type SessionStatus = "starting" | "running" | "exited" | "killed" | "error";
83
266
  interface SessionDescriptor {
84
267
  id: string;
@@ -95,6 +278,19 @@ interface SessionDescriptor {
95
278
  /** Last time anything was written to stdout/stderr. Lets the UI
96
279
  * spot stuck sessions ("running for 2h, last output 12min ago"). */
97
280
  lastOutputAt?: string;
281
+ /** Last time ANY adapter-process activity was observed — ACP
282
+ * JSON-RPC traffic, protocol-level events, not just ring-buffer
283
+ * output lines. Stays current during long tool-call chains where
284
+ * `lastOutputAt` goes stale. Updated on incoming session/update
285
+ * notifications AND on outbound RPC calls. ISO 8601. */
286
+ lastActivityAt?: string;
287
+ /** Whether the underlying OS process is still alive. Computed via
288
+ * `process.kill(pid, 0)` at read time (list()/get()) — cheap,
289
+ * zero-overhead, standard POSIX check. Absent when `pid` is null
290
+ * (no process to check). Never persisted — recomputed fresh on
291
+ * every read since it's a live OS query, stale the instant it's
292
+ * written to disk. */
293
+ processAlive?: boolean;
98
294
  /** Free-text label the spawner can attach (e.g. conversation id,
99
295
  * operator name) so the UI can group/filter. */
100
296
  label?: string;
@@ -117,12 +313,29 @@ interface SessionDescriptor {
117
313
  * `/sessions/agent` to spin up a fresh ACP runtime. Undefined for
118
314
  * pty/command kinds. */
119
315
  adapterSlug?: string;
316
+ /** The model the session was requested to run (echoed back at spawn). */
317
+ model?: string;
318
+ /** Cumulative estimated USD cost of the session — best-effort, refreshed
319
+ * on each turn-end from the adapter's usage reader (e.g. hermes reads its
320
+ * state.db). Absent for adapters with no usage source. */
321
+ costUsd?: number;
322
+ /** Cumulative input / output token counts (same source + cadence as costUsd). */
323
+ tokensIn?: number;
324
+ tokensOut?: number;
120
325
  /** ACP-level session id (the adapter's own handle — claude-code's
121
326
  * conversation id, hermes' chat id, …). Set at spawnAgent time
122
327
  * from `agentSession.sessionId`. Survives across daemon restarts
123
328
  * via sessions.json so `restart` can pass it as `resumeSessionId`
124
329
  * and reattach to the prior conversation history. */
125
330
  adapterSessionId?: string;
331
+ /** MCP servers mounted into the agent's session at spawn time
332
+ * (orchestrator WP1). Persisted so the resume/re-spawn path can
333
+ * re-mount the same host-chosen toolset instead of resuming a
334
+ * capability-stripped agent. The current `AcpMcpServer` shape is
335
+ * `{ name, transport, ref? }` — a reference, NOT inline credentials
336
+ * — so this carries no secrets today. Should the shape ever grow
337
+ * headers/tokens, NEVER log this field's contents. */
338
+ mcpServers?: AcpMcpServer[];
126
339
  /** Provider-specific resume hints sniffed from the session's
127
340
  * output. claude-code prints `claude --resume <uuid>` on exit;
128
341
  * we capture that uuid as `claudeResumeId`. On `restart`, when a
@@ -134,6 +347,54 @@ interface SessionDescriptor {
134
347
  * Keys are adapter-specific so future adapters can add their own
135
348
  * ("hermesResumeId", etc.) without changing this type. */
136
349
  resumeMetadata?: Record<string, string>;
350
+ /** Set by the orchestration layer when the agent emits an
351
+ * "awaiting-input" turn-end. Cleared on the next turn start.
352
+ * Used by `session_monitor` to fast-return without subscribing. */
353
+ awaitingInput?: boolean;
354
+ /**
355
+ * Structured detail on WHY the session is awaiting input, when it can be
356
+ * determined — lets an orchestrator distinguish "blocked on a real
357
+ * question" from "just finished a turn" without re-reading raw output.
358
+ * `source: "structured"` comes from a driver-reported ACP-style prompt
359
+ * (`AgentStreamEvent.options`, e.g. a tool permission request);
360
+ * `source: "heuristic"` is a best-effort guess from the tail of the
361
+ * transcript (trailing "?" + an optional enumerated option list) for
362
+ * drivers that don't report structured prompts. Cleared alongside
363
+ * `awaitingInput` on the next turn start. */
364
+ awaitingQuestion?: SessionAwaitingQuestion;
365
+ /** Count of turns that have fully completed (turn-end emitted) on this
366
+ * session. Lets `session_monitor` fast-return for a session that already
367
+ * finished its turn before the wait subscribed — a fast turn that ends
368
+ * in "completed" (not "awaiting-input") leaves no other persisted
369
+ * signal (status stays "running", busy clears). 0 = never ran a turn,
370
+ * so a freshly-spawned idle session is NOT mistaken for done. */
371
+ turnsCompleted?: number;
372
+ /** True while a turn is actively running (mirror of the internal
373
+ * runtime `busy` flag). Lets `session_monitor` distinguish "idle after a
374
+ * finished turn" from "mid-turn" so it doesn't fast-return a stale
375
+ * turn-end while the NEXT turn is still generating. */
376
+ busy?: boolean;
377
+ /** Session id of the orchestrator that spawned this session, set when
378
+ * the spawn arrived via the scoped orchestrator sub-gateway (the
379
+ * token carried the spawner's identity). Absent for direct `/mcp`
380
+ * spawns (the root operator). Powers the subtree scoping of
381
+ * list/kill and the per-parent child quota (orchestrator WP4).
382
+ * Persisted so the tree survives a daemon restart. */
383
+ parentSessionId?: string;
384
+ /** Recursion depth in the orchestrator tree: a direct (root) spawn is
385
+ * depth 0; a session spawned by a depth-d orchestrator is d+1. Used
386
+ * by the depth-cap guard. Persisted alongside `parentSessionId`.
387
+ * Treated as 0 when absent (legacy rows / direct spawns that predate
388
+ * WP4). */
389
+ depth?: number;
390
+ /** Adapter id that drives this session (e.g. "camofox", "bureau"). */
391
+ browserAdapterId?: string;
392
+ /** Port the browser service listens on. */
393
+ browserPort?: number;
394
+ /** Base URL of the browser service (e.g. "http://127.0.0.1:9377"). */
395
+ browserBaseUrl?: string;
396
+ /** Execution location — "local" (default) or "cloud". */
397
+ browserLocation?: "local" | "cloud";
137
398
  }
138
399
  interface SessionsRegistry {
139
400
  spawn(input: SpawnSessionInput): SessionDescriptor;
@@ -156,20 +417,35 @@ interface SessionsRegistry {
156
417
  * `attachPty(id, ...)`. Throws when the registry was constructed
157
418
  * without a `spawnPty` factory (node-pty optional dep missing). */
158
419
  spawnPty(input: SpawnPtyInput): SessionDescriptor;
420
+ /** Register an already-running browser service adapter as a tracked
421
+ * session (kind="browser"). Idempotent by identity — each call
422
+ * mints a fresh session id. The `stop` callback is invoked by
423
+ * `kill()` best-effort. */
424
+ registerBrowser(input: RegisterBrowserInput): SessionDescriptor;
159
425
  /** Send a follow-up turn to a live agent session. Throws when the
160
- * session is missing, not an agent-cli kind, or busy. The events
161
- * stream into the existing ring buffer + line emitter so /stream
162
- * consumers see them as they arrive. */
426
+ * session is missing, not an agent-cli kind, dead (exited/killed/
427
+ * error and unresumable `SessionNotAliveError`), or busy
428
+ * (mid-turn). The events stream into the existing ring buffer +
429
+ * line emitter so /stream consumers see them as they arrive. */
163
430
  sendPrompt(id: string, message: unknown): Promise<void>;
164
- /** Fire-and-forget variant of `sendPrompt`. Validates the same
165
- * preconditions synchronously throws on missing session / wrong
166
- * kind / busy but returns immediately after the turn is started,
167
- * letting the caller stream output via the SSE endpoint instead of
168
- * blocking on the full turn. Async errors during the turn are
169
- * pushed into the ring buffer as `[error]` lines so `/stream`
170
- * subscribers see them. Used by the web UI's chat input where a
171
- * long turn would otherwise freeze the textbox. */
172
- enqueuePrompt(id: string, message: unknown): void;
431
+ /** Fire-and-forget variant of `sendPrompt` for the TURN ITSELF only.
432
+ * Admission (resume attempt + the missing/wrong-kind/dead/busy
433
+ * checks `sendPrompt` throws) is AWAITED before this resolves, so a
434
+ * dead or busy session rejects instead of silently reporting
435
+ * success only resolves once the turn has actually started.
436
+ * Errors during the turn's own execution (network drop, child died
437
+ * mid-turn) are pushed into the ring buffer as `[error]` lines so
438
+ * `/stream` subscribers see them. Used by the web UI's chat input
439
+ * and MCP `agent_prompt`, where a long turn would otherwise freeze
440
+ * the caller. */
441
+ enqueuePrompt(id: string, message: unknown): Promise<void>;
442
+ /** Stamp `lastActivityAt` on a live agent-cli session's descriptor
443
+ * and schedule a debounced persist. Called from the `onActivity`
444
+ * callback threaded down through the driver → ACP client, which
445
+ * fires on ANY adapter-process traffic (not just ring-buffer
446
+ * output) — see `SessionDescriptor.lastActivityAt`. No-op when the
447
+ * id is unknown (session already forgotten). */
448
+ pulseActivity(id: string): void;
173
449
  list(): SessionDescriptor[];
174
450
  get(id: string): SessionDescriptor | undefined;
175
451
  /** Subscribe to a session's output. Returns an unsubscribe fn.
@@ -214,6 +490,30 @@ interface SessionsRegistry {
214
490
  * awaited — the daemon shutdown loop handles process tree teardown. */
215
491
  shutdown(): void;
216
492
  }
493
+ interface RegisterBrowserInput {
494
+ /** Adapter id (e.g. "camofox", "bureau", "chromium"). */
495
+ adapterId: string;
496
+ /** Port the browser service listens on. */
497
+ port: number;
498
+ /** Base URL of the browser service (e.g. "http://127.0.0.1:9377"). */
499
+ baseUrl: string;
500
+ /** Execution location — "local" (default) or "cloud". Included in the
501
+ * dedup key so a cloud call never returns a pre-existing local session. */
502
+ location?: "local" | "cloud";
503
+ /** PID of the service process — undefined when managed by launchd/systemd. */
504
+ pid?: number;
505
+ /** True when the service was already healthy before this call (idempotent start). */
506
+ wasAlreadyRunning: boolean;
507
+ /** Initial lifecycle status. "running" when the service is confirmed
508
+ * healthy; "starting" when a non-blocking cold start returned before the
509
+ * service finished converging (health-wait continues in the background and
510
+ * `browser_status` / `list_browsers` flip it to running once up).
511
+ * Defaults to "running" when omitted. */
512
+ status?: Extract<SessionStatus, "running" | "starting">;
513
+ /** Async shutdown callback — called by kill() best-effort. */
514
+ stop: () => Promise<void>;
515
+ label?: string;
516
+ }
217
517
  interface RegisterSessionInput {
218
518
  /** Pre-existing child process to adopt (e.g. from a tunnel spawn). */
219
519
  child: ChildProcess;
@@ -242,6 +542,33 @@ interface SpawnAgentInput {
242
542
  label?: string;
243
543
  /** Pretty command for the descriptor (display only). */
244
544
  commandPreview?: string;
545
+ /** MCP servers mounted at spawn time. Persisted on the descriptor so
546
+ * the resume/re-spawn path can re-mount the same toolset (orchestrator
547
+ * WP1). */
548
+ mcpServers?: AcpMcpServer[];
549
+ /** Spawning orchestrator's session id — set when the spawn arrived
550
+ * through the scoped sub-gateway (orchestrator WP4). Recorded on the
551
+ * descriptor for subtree scoping + quota accounting. */
552
+ parentSessionId?: string;
553
+ /** Recursion depth for the new session (orchestrator WP4). Defaults to
554
+ * 0 when omitted (direct/root spawn). */
555
+ depth?: number;
556
+ /** Requested model id — recorded on the descriptor for display + echo. */
557
+ model?: string;
558
+ /** Hard ceiling on cumulative session cost (USD). When set and the
559
+ * adapter's usage reader reports a higher cost at a turn-end, the session
560
+ * is stopped (best-effort, turn-granular — caps continuation, can't abort
561
+ * a turn mid-flight). */
562
+ maxCostUsd?: number;
563
+ /** Best-effort usage reader, called on each turn-end to refresh the
564
+ * cost/token fields on the descriptor. Adapter-specific (e.g. hermes
565
+ * reads its state.db keyed by the adapter session id). Omit for adapters
566
+ * with no usage source. */
567
+ readUsage?: () => Promise<{
568
+ costUsd?: number;
569
+ tokensIn?: number;
570
+ tokensOut?: number;
571
+ } | null>;
245
572
  }
246
573
  interface SpawnSessionInput {
247
574
  kind: SessionKind;
@@ -287,27 +614,848 @@ interface SpawnPtyInput {
287
614
  }
288
615
 
289
616
  /**
290
- * Tiny node:http server that fronts the runtime gateway.
617
+ * MCP tools that expose browser service adapters (Camofox, Bureau, …) to
618
+ * agents connected to the daemon. Lets a remote operator start, stop, and
619
+ * inspect browser service sessions through the same MCP connection it uses
620
+ * for fs/exec/agent-cli.
621
+ *
622
+ * Five tools:
623
+ * browser_adapter_list discover available browser adapter ids + metadata
624
+ * start_browser ensure a browser adapter is up, register as a session
625
+ * stop_browser kill a running browser session
626
+ * list_browsers browse alive/recent browser sessions
627
+ * browser_status descriptor + live health probe for one session
628
+ *
629
+ * Auth: same as every other daemon tool — gated by the gateway's auth source.
630
+ *
631
+ * Injection pattern: `resolveBrowserAdapter` is passed in, not imported.
632
+ * This keeps the runtime package free of a hard dep on @agentproto/adapter-browser.
633
+ * The types below are minimal structural shapes — equivalent to `AgentSessionLike`
634
+ * in sessions.ts.
635
+ */
636
+
637
+ /**
638
+ * Minimal shape of a BrowserAdapterInstance — structurally compatible with
639
+ * @agentproto/adapter-browser's BrowserAdapterInstance. Kept local so the
640
+ * runtime package stays free of a hard dependency on the adapter package.
641
+ */
642
+ interface BrowserInstanceLike {
643
+ id: string;
644
+ port: number;
645
+ baseUrl: string;
646
+ pid?: number;
647
+ wasAlreadyRunning: boolean;
648
+ /** False when a non-blocking cold start returned before the service
649
+ * finished converging (background health-wait still running). */
650
+ healthy: boolean;
651
+ stop(): Promise<void>;
652
+ }
653
+ /**
654
+ * Minimal shape of a BrowserAdapterHandle — structurally compatible with
655
+ * @agentproto/adapter-browser's BrowserAdapterHandle.
656
+ */
657
+ interface BrowserAdapterLike {
658
+ id: string;
659
+ name: string;
660
+ description: string;
661
+ defaultPort: number;
662
+ healthPath: string;
663
+ /** Where this adapter runs — "local" (default) or "cloud". */
664
+ location?: "local" | "cloud";
665
+ ensure(opts: {
666
+ port?: number;
667
+ /** forwarded to dependent adapters (e.g. bureau→camofox) */
668
+ camofoxPort?: number;
669
+ launchCmd?: string;
670
+ env?: Record<string, string>;
671
+ timeoutMs?: number;
672
+ /** Opt-in non-blocking cold start: wait only this long for a freshly
673
+ * spawned service before returning `healthy:false` and converging
674
+ * in the background. */
675
+ initialWaitMs?: number;
676
+ log?: (s: string) => void;
677
+ /** Override execution location ("local" or "cloud"). */
678
+ location?: "local" | "cloud";
679
+ /** Base URL of the remote service when location="cloud". */
680
+ baseUrl?: string;
681
+ /** Override the binary path for a local spawn. */
682
+ binPath?: string;
683
+ }): Promise<BrowserInstanceLike>;
684
+ }
685
+ type BrowserAdapterResolver = (id: string) => BrowserAdapterLike | undefined;
686
+ type BrowserAdapterLister = () => {
687
+ id: string;
688
+ name: string;
689
+ description: string;
690
+ defaultPort: number;
691
+ /** Where this adapter runs — "local" (default) or "cloud". */
692
+ location?: "local" | "cloud";
693
+ /** Install methods — informational metadata from the adapter manifest. */
694
+ install?: unknown[];
695
+ /** Config prompts — informational metadata from the adapter manifest. */
696
+ config?: unknown[];
697
+ }[];
698
+ /**
699
+ * Family-specific descriptor surfaced in the kit-path `browser_adapter_list`
700
+ * response. Fields match the legacy lister shape so the tool output is
701
+ * unchanged regardless of which path is active.
702
+ */
703
+ interface BrowserAdapterInfo {
704
+ id: string;
705
+ name: string;
706
+ description: string;
707
+ defaultPort: number;
708
+ }
709
+
710
+ /**
711
+ * Shared types for pluggable tunnel providers.
712
+ *
713
+ * Extracted here so both `RemoteController` (single-gateway tunnel) and
714
+ * `TunnelRegistry` (multi-tunnel general surface) can share the same
715
+ * provider contract without creating a circular dep.
716
+ *
717
+ * The tunnel family is the FIRST consumer of `@agentproto/adapter-kit`:
718
+ * {@link TunnelProviderHandle} extends the kit's generic `AdapterHandle`
719
+ * (slug/name/version/description/requiresSetup/check) with the
720
+ * tunnel-specific `capabilities` + the existing `start`/`stop` lifecycle.
721
+ */
722
+
723
+ interface ProviderStartOptions {
724
+ /** Local target the tunnel forwards to. */
725
+ target: {
726
+ host: string;
727
+ port: number;
728
+ };
729
+ /** Workspace path — for log / state files. */
730
+ workspace: string;
731
+ /** Called whenever the provider has a status update worth logging. */
732
+ onLog?: (line: string) => void;
733
+ }
734
+ interface ProviderStartResult {
735
+ publicUrl: string;
736
+ pid: number | null;
737
+ }
738
+ /**
739
+ * A pluggable tunnel provider.
740
+ *
741
+ * `start` returns the public URL once the upstream tunnel is ready.
742
+ * `stop` is idempotent — calling it twice or on an already-dead child
743
+ * must not throw.
744
+ */
745
+ interface RemoteProvider {
746
+ readonly id: string;
747
+ start(opts: ProviderStartOptions): Promise<ProviderStartResult>;
748
+ stop(): Promise<void>;
749
+ }
750
+
751
+ /**
752
+ * Slug-keyed tunnel provider registry — the single source of truth that both
753
+ * halves of the tunnel family consult:
754
+ *
755
+ * - the adapter-kit listing path (`list_tunnel_adapters` / `setup_tunnel_provider`)
756
+ * - the lifecycle path (`TunnelRegistry`: create / restore / start / stop)
757
+ *
758
+ * Before this module those two halves spoke different languages — the
759
+ * lifecycle engine only knew the short discriminators `"quick"`/`"named"` and
760
+ * had no ngrok case at all, while the adapter half spoke slugs. They never
761
+ * reconciled, so ngrok was listable-but-not-creatable. Routing everything
762
+ * through one registry collapses that: a built-in OR a third-party
763
+ * `@agentproto/adapter-<slug>` / `@<scope>/agentproto-adapter-<slug>` provider
764
+ * resolves the same way and works end to end.
765
+ */
766
+
767
+ /** Per-slug credentials, as stored by the creds store / setup tool. */
768
+ type TunnelCreds = Record<string, string>;
769
+
770
+ /**
771
+ * TunnelRegistry — multi-tunnel public-URL manager.
772
+ *
773
+ * Manages a named set of cloudflared (or future provider) tunnels,
774
+ * each forwarding a local port to a public URL. Independent from
775
+ * RemoteController which handles the single "expose this gateway"
776
+ * use-case with bearer-token gating. This registry is for the general
777
+ * "create a public endpoint for any local port" surface.
778
+ *
779
+ * Lifecycle:
780
+ * create(input) → spawns provider, waits for URL, stores descriptor
781
+ * list() → TunnelDescriptor[]
782
+ * get(id) → TunnelDescriptor | undefined
783
+ * stop(id) → SIGTERM provider, mark stopped
784
+ * shutdown() → stop all active tunnels (called on daemon exit)
785
+ *
786
+ * Persistence: `~/.agentproto/tunnels.json` — descriptors survive
787
+ * daemon restarts; active entries are marked "stopped" on next boot
788
+ * since their child processes are gone (same GHOST pattern as
789
+ * sessions.ts).
790
+ */
791
+
792
+ type TunnelStatus = "starting" | "active" | "stopped" | "error";
793
+ /**
794
+ * Provider identifier — an open set of slugs (built-in `cloudflare-quick` /
795
+ * `cloudflare-named` / `ngrok`, legacy short names `quick` / `named`, or any
796
+ * third-party `@scope/agentproto-adapter-<slug>`). Normalized to a canonical
797
+ * slug only at resolve time, so persisted descriptors keep their original
798
+ * value and need no migration.
799
+ */
800
+ type TunnelProvider = string;
801
+ interface TunnelDescriptor {
802
+ id: string;
803
+ /** Optional user-friendly slug. Accepts id-or-name in stop/status calls. */
804
+ name?: string;
805
+ /** Free-text label surfaced in list / CLI table. */
806
+ label?: string;
807
+ provider: TunnelProvider;
808
+ targetHost: string;
809
+ targetPort: number;
810
+ publicUrl: string;
811
+ status: TunnelStatus;
812
+ pid: number | null;
813
+ createdAt: string;
814
+ stoppedAt?: string;
815
+ lastError?: string;
816
+ /**
817
+ * When true, the tunnel is relaunched on daemon boot (see
818
+ * `restoreOnBoot`). Only meaningful for `named` tunnels — a relaunched
819
+ * `quick` tunnel gets a fresh random URL, so autostart for it merely
820
+ * keeps *a* tunnel up, not a stable URL.
821
+ */
822
+ autostart?: boolean;
823
+ /** Stable public hostname routed to this tunnel. */
824
+ hostname?: string;
825
+ /** Cloudflare tunnel id or name (the `run` target). */
826
+ tunnelId?: string;
827
+ /** Path to the tunnel credentials JSON (defaults under ~/.cloudflared). */
828
+ credentialsFile?: string;
829
+ }
830
+ interface CreateTunnelInput {
831
+ targetPort: number;
832
+ provider?: TunnelProvider;
833
+ name?: string;
834
+ label?: string;
835
+ targetHost?: string;
836
+ /** Relaunch this tunnel on daemon boot. See `TunnelDescriptor.autostart`. */
837
+ autostart?: boolean;
838
+ /** Stable public hostname (e.g. app.example.com). */
839
+ hostname?: string;
840
+ /** Cloudflare tunnel id or name to `run`. */
841
+ tunnelId?: string;
842
+ /** Path to the credentials JSON (defaults to ~/.cloudflared/<tunnelId>.json). */
843
+ credentialsFile?: string;
844
+ }
845
+ interface TunnelRegistryOptions {
846
+ /** Absolute path for persistence file. Defaults to ~/.agentproto/tunnels.json */
847
+ persistPath?: string;
848
+ /** Absolute path to workspace — used as scratch dir for provider config files. */
849
+ workspace?: string;
850
+ /** Hook for surfacing provider log lines. */
851
+ onLog?: (line: string) => void;
852
+ /**
853
+ * Read stored credentials for a provider slug. Lets the lifecycle build a
854
+ * provider that keeps its secrets in the creds store (e.g. ngrok's authtoken,
855
+ * or any third-party provider) rather than on the descriptor. Injected by the
856
+ * daemon (index.ts); descriptor-carried config (named's hostname/tunnelId)
857
+ * still takes precedence.
858
+ */
859
+ readCreds?: (slug: string) => Promise<TunnelCreds | null>;
860
+ }
861
+ declare class TunnelRegistry {
862
+ private readonly tunnels;
863
+ private readonly persistPath;
864
+ private readonly workspace;
865
+ private readonly onLog;
866
+ private readonly readCreds;
867
+ private persistTimer;
868
+ constructor(opts?: TunnelRegistryOptions);
869
+ create(input: CreateTunnelInput): Promise<TunnelDescriptor>;
870
+ /**
871
+ * Drive an entry's provider through start and fold the result back into
872
+ * its descriptor. On failure the descriptor is marked `error` and the
873
+ * error re-thrown (callers that want best-effort — e.g. boot restore —
874
+ * catch it). Shared by `create` and `restoreOnBoot`.
875
+ */
876
+ private startEntry;
877
+ /**
878
+ * Relaunch every `autostart` tunnel that isn't currently running. Called
879
+ * once on daemon boot — `loadFromDisk` has already ghosted formerly-live
880
+ * entries to `stopped` and given autostart ones a real provider. Quick
881
+ * tunnels are skipped (a relaunch yields a fresh random URL, defeating
882
+ * the point); named tunnels come back on their stable hostname.
883
+ *
884
+ * Best-effort: a provider that fails to start leaves its descriptor in
885
+ * `error` and the others still come up.
886
+ */
887
+ restoreOnBoot(): Promise<void>;
888
+ list(): TunnelDescriptor[];
889
+ get(id: string): TunnelDescriptor | undefined;
890
+ findByIdOrName(idOrName: string): TunnelDescriptor | undefined;
891
+ stop(idOrName: string): Promise<boolean>;
892
+ shutdown(): Promise<void>;
893
+ protected pickProviderForTest(_provider: TunnelProvider, desc: TunnelDescriptor): Promise<RemoteProvider>;
894
+ /**
895
+ * Build the creds passed to a provider: stored creds (ngrok authtoken,
896
+ * third-party secrets) merged with descriptor-carried config (named's
897
+ * hostname/tunnelId/credentialsFile), descriptor taking precedence.
898
+ */
899
+ private credsForDescriptor;
900
+ private findEntryByIdOrName;
901
+ private schedulePersist;
902
+ private persistNow;
903
+ private loadFromDisk;
904
+ }
905
+
906
+ /**
907
+ * Fire-and-forget webhook notifier for session lifecycle events.
291
908
  *
292
- * Routes:
293
- * GET /health — { status, workspace, registered, uptime }
294
- * GET /events — SSE stream of RuntimeEvent
295
- * POST /mcp MCP Streamable HTTP transport (POST)
296
- * GET /mcp — MCP SSE response stream (GET)
297
- * DELETE /mcp — close MCP session
298
- * GET /conversations — JSON list of conversation summaries
299
- * GET /conversations/<id> — markdown body of one conversation
300
- * POST /heartbeat/tick — force-fire one heartbeat tick
909
+ * Each session can register its own `notifyUrl` (via `agent_start`).
910
+ * A global URL can be set via `AGENTPROTO_NOTIFY_URL` env var or
911
+ * `~/.agentproto/notify.json` (env wins). Both are POSTed when an event
912
+ * fires the union of per-session + global URLs, deduplicated.
301
913
  *
302
- * No auth in `mode: "none"` (loopback default). `mode: "bearer"`
303
- * checks `Authorization: Bearer <token>` against the configured
304
- * token. Health is always public so external monitors can probe.
914
+ * Retry policy: one retry after 2 s on network error. No retry on 4xx/5xx.
915
+ * Timeout: 10 s per attempt. All errors are swallowed — the notifier never
916
+ * throws into the session's hot path.
917
+ */
918
+
919
+ interface WebhookNotifier {
920
+ /** Register a per-session URL (called from agent_start). */
921
+ register(sessionId: string, url: string): void;
922
+ unregister(sessionId: string): void;
923
+ /** Handler to wire into SessionEventBus.onAny. Fire-and-forget. */
924
+ onSessionEvent(ev: SessionEvent): void;
925
+ }
926
+
927
+ /**
928
+ * Cursor-based ring buffer for session events. Bridges the in-process
929
+ * SessionEventBus (push) to the session_events_poll MCP tool (pull).
305
930
  *
306
- * MCP transport: stateless mode for v1 each request is independent.
307
- * Stateful session pinning can come later when long-running streaming
308
- * tool calls become a real use case.
931
+ * Capacity default: 1 000 events. Oldest entry dropped on overflow
932
+ * callers that lag beyond the cap see a gap in their cursor but never
933
+ * a block. `since()` returns `nextCursor = totalEmitted` so a fresh
934
+ * call with that value always starts just past the current tail.
309
935
  */
310
936
 
937
+ interface EventRingSnapshot {
938
+ events: SessionEvent[];
939
+ nextCursor: number;
940
+ }
941
+ interface EventRingQueryOpts {
942
+ sessionIds?: string[];
943
+ types?: SessionEventType[];
944
+ /** Default 50. */
945
+ limit?: number;
946
+ }
947
+ interface EventRing {
948
+ /**
949
+ * Return events whose ring-index >= `cursor`. Pass 0 to get all
950
+ * retained events; pass the previously returned `nextCursor` for an
951
+ * incremental read. The returned `nextCursor` is always equal to
952
+ * `totalEmitted` — use it as the next `cursor` argument.
953
+ *
954
+ * Negative or below-base cursors are clamped to the oldest retained
955
+ * event (no error, just a potential replay of a partial window).
956
+ */
957
+ since(cursor: number, opts?: EventRingQueryOpts): EventRingSnapshot;
958
+ /** Wire the ring to a SessionEventBus. Returns an unsubscribe fn. */
959
+ wire(bus: SessionEventBus): () => void;
960
+ }
961
+
962
+ /**
963
+ * Completion-policy supervisor — WP1 + WP2 + WP3 + WP4 + WP5 + WP6 + WP7.
964
+ *
965
+ * WP7 (judge-gate): a gate may be a judge AGENT instead of a shell command —
966
+ * `gate: { judge: { adapter, model?, prompt, timeoutMs? } }`. When the trigger
967
+ * fires the supervisor spawns a short-lived agent (resolveAgentAdapter →
968
+ * spawnAgent), prompts it with the rubric (+ a minimal context tail of the
969
+ * watched session output), waits for its turn-end on the bus, reads the final
970
+ * reply and parses the LAST `VERDICT: PASS|FAIL` line (case-insensitive). PASS
971
+ * = gate pass (then-action), FAIL = gate fail (existing nudge/escalate path).
972
+ * Fail-safe: no resolver / spawn fail / timeout (default 120s) / unparseable
973
+ * verdict → FAIL (+ reason on state.error). The judge session is ALWAYS killed
974
+ * when the gate settles. The judge runs while the policy holds its `gating`
975
+ * slot, so it is accounted for in the WP6 concurrency cap. Persistence: a judge
976
+ * gate left in `gating` at crash is re-armed to `watching` like any active
977
+ * policy and simply re-runs the judge — no in-flight judge is persisted.
978
+ *
979
+ *
980
+ * State machine per attached policy:
981
+ * watching → gating → acting → done
982
+ * ↘ nudging (retries < maxRetries) → watching (gate failed, nudge sent)
983
+ * ↘ blocked (gate failed, retries exhausted)
984
+ * ↘ awaiting-ack → done (then:"commit" + requireHumanAck, ack approve)
985
+ * ↘ cancelled (ack reject)
986
+ * watching → cancelled (watched session exited before turn-end)
987
+ *
988
+ * Trigger: session:turn-end on the watched session(s).
989
+ * Gate: optional shell command via the same allowlist + cwd-anchor as
990
+ * command_execute. exit 0 = pass.
991
+ * Action (then:"emit"): emits policy:passed / policy:failed on the bus
992
+ * (readable via session_events_poll).
993
+ *
994
+ * WP5 (then:"commit"): on a GREEN gate, prepare a host-side git commit in the
995
+ * watched session's cwd — `git add -- <explicit paths>` then `git commit -m
996
+ * <message>` via the same allowlisted runCommand path (argv array, shell:false).
997
+ * GUARDRAILS: never `git add -A`/glob, never `git push`, never `--force`, never
998
+ * a branch the policy didn't ask for (commits the cwd's current branch), message
999
+ * passed as argv (no shell injection), empty paths → error (no commit). A `--`
1000
+ * separator precedes the paths so a path can never be parsed as a git flag.
1001
+ * "nothing to commit" is treated as SUCCESS (idempotent re-commit after a
1002
+ * restart). `requireHumanAck` defaults to TRUE: the gate-green transition goes
1003
+ * to `awaiting-ack` and emits `policy:commit-ready` (paths + message) instead of
1004
+ * committing; the commit runs only on `policy_ack(approve:true)` →
1005
+ * `policy:committed` (+ sha) → done; `approve:false` → cancelled. With
1006
+ * `requireHumanAck:false` the commit runs directly at the green gate.
1007
+ * onFail (WP2): when gate fails, re-prompts the session(s) (sendPrompt) up to
1008
+ * maxRetries times, then transitions to blocked + emits policy:failed.
1009
+ *
1010
+ * WP3: persists PolicyRunState + input to ~/.agentproto/policies.json (or an
1011
+ * injected persistPath). Debounced async write on every transition; sync flush
1012
+ * at shutdown(). On boot, active policies (watching/gating/nudging/acting) are
1013
+ * re-armed to "watching" and re-subscribed to the bus. Terminal states (done/
1014
+ * blocked/cancelled) are kept for history. Session absent at reload → cancelled.
1015
+ *
1016
+ * WP6 (DAG + concurrency cap):
1017
+ * - CHAINING: a policy may declare `next` — a recursive AttachPolicyInput.
1018
+ * When the policy reaches `done` (emit-passed or commit-committed), the
1019
+ * supervisor automatically attaches `next` as a fresh completion policy on
1020
+ * the session(s) named in its own spec (chaining policies over EXISTING
1021
+ * sessions — this WP does NOT spawn new sessions). The child's policyId is
1022
+ * recorded on the parent as `nextPolicyId`. Chaining is idempotent (a parent
1023
+ * already carrying a `nextPolicyId` never re-chains, so a re-armed/re-acked
1024
+ * parent after a restart won't double-attach). `next` persists as part of the
1025
+ * parent's `input`, so the whole chain survives a restart.
1026
+ * - CONCURRENCY CAP: a daemon-wide cap (`concurrencyCap`, default 8) on how
1027
+ * many policies may be in `gating`/`acting` at once. When a trigger fires
1028
+ * and the cap is already saturated, the policy parks in `queued` (a FIFO
1029
+ * gate queue) instead of gating. As each active policy settles (done /
1030
+ * blocked / awaiting-ack / back-to-watching after a nudge), the queue is
1031
+ * pumped and the oldest queued policy proceeds to gate. Queued policies hold
1032
+ * NO slot, so a chained DAG can't deadlock behind the cap. The queued state
1033
+ * persists and is re-armed to `watching` on reload (then re-evaluated).
1034
+ *
1035
+ * WP4 (fan-in): a policy may watch a GROUP of sessions (`sessionIds`). The gate
1036
+ * runs ONCE, only after EVERY member of the group has finished its turn. Members
1037
+ * are tracked in an idempotent `pending` set: a member is removed on its first
1038
+ * `session:turn-end`; a repeated turn-end for an already-removed member is a
1039
+ * no-op (no double-count). When the set empties → gating.
1040
+ *
1041
+ * A member that emits `session:exited` is treated as "this member is done": it
1042
+ * is removed from the pending set exactly like a turn-end (a dead session will
1043
+ * never emit one), so a partially-crashed group can still complete. If every
1044
+ * member exits, the set empties and the gate runs once. The one exception is the
1045
+ * degenerate group of size 1 (the legacy single-`sessionId` form): a lone
1046
+ * watched session that exits has nothing left to gate on, so the policy is
1047
+ * cancelled — preserving the original single-session contract.
1048
+ *
1049
+ * Back-compat: the legacy `sessionId` (single) form is a group of one. `state.
1050
+ * sessionId` is retained as the group's representative (group[0]) and is the id
1051
+ * carried on policy:passed / policy:failed events.
1052
+ */
1053
+
1054
+ interface ShellGateSpec {
1055
+ command: string;
1056
+ args?: string[];
1057
+ /** Working directory for the gate command. Defaults to the watched
1058
+ * session's own cwd (trusted as-is — it was already an explicit,
1059
+ * vetted choice made at `agent_start` time). When given explicitly,
1060
+ * anchored to that session's cwd (not the daemon's boot workspace)
1061
+ * so a stray/injected value can't escape it. */
1062
+ cwd?: string;
1063
+ timeoutMs?: number;
1064
+ }
1065
+ /**
1066
+ * Judge-agent gate (WP7). Instead of a shell command, the gate spawns a
1067
+ * short-lived LLM agent (via the daemon's `resolveAgentAdapter`), prompts it
1068
+ * with `prompt` (+ a minimal context tail of the watched session output, if
1069
+ * readable), waits for its turn to end, reads the final reply and parses a
1070
+ * verdict line (`VERDICT: PASS` / `VERDICT: FAIL`, last occurrence,
1071
+ * case-insensitive). PASS = gate pass (exit 0 equivalent), FAIL = gate fail
1072
+ * (→ the existing nudge/escalate path). Fail-safe: a judge timeout OR an
1073
+ * unparseable reply is treated as FAIL. The judge session is always killed
1074
+ * when the gate settles (success or failure).
1075
+ */
1076
+ interface JudgeGateSpec {
1077
+ judge: {
1078
+ /** Agent CLI adapter slug used to spawn the judge (e.g. "claude-code"). */
1079
+ adapter: string;
1080
+ /** Optional model identifier forwarded to the adapter. */
1081
+ model?: string;
1082
+ /** The rubric / instructions for the judge. The supervisor appends a
1083
+ * fixed instruction to end the reply with `VERDICT: PASS` or
1084
+ * `VERDICT: FAIL`. */
1085
+ prompt: string;
1086
+ /** Max wall-clock for the judge turn before it is killed + FAIL.
1087
+ * Default 120_000ms. */
1088
+ timeoutMs?: number;
1089
+ };
1090
+ }
1091
+ /** A gate is either a shell command (exit 0 = pass) or a judge agent. */
1092
+ type GateSpec = ShellGateSpec | JudgeGateSpec;
1093
+ interface OnFailSpec {
1094
+ /**
1095
+ * Message sent to the watched session via sendPrompt when the gate fails.
1096
+ * Use the placeholder {code} to embed the actual exit code.
1097
+ * Default: "Le gate a échoué (exit {code}). Corrige et termine jusqu'à ce qu'il passe."
1098
+ */
1099
+ nudge?: string;
1100
+ /**
1101
+ * Maximum number of consecutive gate failures before transitioning to
1102
+ * blocked. Must be >= 1. Default: 2.
1103
+ */
1104
+ maxRetries?: number;
1105
+ }
1106
+ interface CommitSpec {
1107
+ /**
1108
+ * Explicit, literal paths to stage — workspace/cwd-relative. NEVER a glob,
1109
+ * NEVER `-A`/`--all`. Staged via `git add -- <paths>` (the `--` guards each
1110
+ * path from being parsed as a flag). An empty array is rejected at attach.
1111
+ */
1112
+ paths: string[];
1113
+ /** Commit message. Passed as a single `-m` argv value — no shell. */
1114
+ message: string;
1115
+ /**
1116
+ * Human-in-the-loop gate before the commit actually runs. Default TRUE:
1117
+ * the green gate transitions to `awaiting-ack` + emits `policy:commit-ready`
1118
+ * and waits for `policy_ack`. Set false to commit directly at the green gate.
1119
+ */
1120
+ requireHumanAck?: boolean;
1121
+ }
1122
+ interface AttachPolicyInput {
1123
+ /**
1124
+ * Session id whose turn-end triggers the policy (single-session / legacy
1125
+ * form). Equivalent to `sessionIds: [sessionId]`. Provide this OR
1126
+ * `sessionIds`; if both are given, `sessionIds` wins.
1127
+ */
1128
+ sessionId?: string;
1129
+ /**
1130
+ * Fan-in group (WP4): the gate runs once, only after EVERY listed session
1131
+ * has finished its turn (turn-end or exit). Supersedes `sessionId` when
1132
+ * present and non-empty. A single-element array behaves like `sessionId`.
1133
+ */
1134
+ sessionIds?: string[];
1135
+ /**
1136
+ * Optional gate. Absent → always pass immediately. Either a shell gate
1137
+ * (exit 0 = pass) or a judge-agent gate (WP7: `{ judge: {...} }`).
1138
+ */
1139
+ gate?: GateSpec;
1140
+ /**
1141
+ * Action on a GREEN gate. "emit" (WP1) emits policy:passed. "commit" (WP5)
1142
+ * stages explicit paths and commits on the host — see `commit`.
1143
+ */
1144
+ then: "emit" | "commit";
1145
+ /** Required when then === "commit". Ignored otherwise. */
1146
+ commit?: CommitSpec;
1147
+ /**
1148
+ * What to do when the gate fails (WP2). Absent → immediately blocked
1149
+ * (WP1 behaviour). Present → re-prompt the session up to maxRetries
1150
+ * times before blocking; the session must still be running to nudge.
1151
+ */
1152
+ onFail?: OnFailSpec;
1153
+ /**
1154
+ * DAG chaining (WP6). When this policy reaches `done`, the supervisor
1155
+ * automatically attaches `next` as a fresh completion policy. `next` is a
1156
+ * full AttachPolicyInput (recursive — it may declare its own `next`), and it
1157
+ * carries the session(s) it watches in its own spec. Chaining only attaches
1158
+ * policies onto already-running sessions; it never spawns new sessions.
1159
+ */
1160
+ next?: AttachPolicyInput;
1161
+ }
1162
+ type PolicyRunStatus = "watching" | "queued" | "gating" | "acting" | "nudging" | "awaiting-ack" | "done" | "blocked" | "cancelled";
1163
+ interface PolicyRunState {
1164
+ policyId: string;
1165
+ /** Representative session id (the fan-in group's first member, group[0]).
1166
+ * Carried on policy:passed / policy:failed events. */
1167
+ sessionId: string;
1168
+ /** The full fan-in group being watched. Always present; a length-1 group is
1169
+ * the legacy single-session form. */
1170
+ sessionIds: string[];
1171
+ /** Sessions in the group that have NOT yet finished their turn. The gate
1172
+ * fires once this set empties. Idempotent: a member is removed on its first
1173
+ * turn-end/exit; repeats are no-ops. Reset to the full group on each nudge. */
1174
+ pending: string[];
1175
+ status: PolicyRunStatus;
1176
+ /** Number of nudges sent so far (incremented after each nudge round). */
1177
+ retries: number;
1178
+ startedAt: string;
1179
+ endedAt?: string;
1180
+ lastGate?: {
1181
+ exitCode: number;
1182
+ at: string;
1183
+ };
1184
+ /**
1185
+ * The exact commit prepared at the green gate (WP5). Set when entering
1186
+ * `awaiting-ack` (or just before a direct commit) so that an ack arriving
1187
+ * after a daemon restart commits the same paths/message in the same cwd,
1188
+ * independent of whether the watched session still exists.
1189
+ */
1190
+ commitPlan?: {
1191
+ paths: string[];
1192
+ message: string;
1193
+ cwd: string;
1194
+ };
1195
+ /** The resulting commit sha once `then:"commit"` has committed. */
1196
+ commitSha?: string;
1197
+ /**
1198
+ * DAG chaining (WP6): the policyId of the child policy attached when this
1199
+ * policy reached `done`. Set exactly once (idempotent chaining guard).
1200
+ */
1201
+ nextPolicyId?: string;
1202
+ error?: string;
1203
+ }
1204
+ interface CompletionPolicySupervisor {
1205
+ /**
1206
+ * Attach a completion policy to an already-running session.
1207
+ * Returns immediately with the initial (watching) state.
1208
+ * The state machine runs in the background.
1209
+ */
1210
+ attach(input: AttachPolicyInput): PolicyRunState;
1211
+ getStatus(policyId: string): PolicyRunState | undefined;
1212
+ cancel(policyId: string): void;
1213
+ /**
1214
+ * Human acknowledgement for a `then:"commit"` policy parked in
1215
+ * `awaiting-ack` (WP5). `approve:true` runs the prepared commit (→ done,
1216
+ * emits policy:committed); `approve:false` cancels it (no commit). No-op on
1217
+ * any other state. Returns the (possibly updated) state.
1218
+ */
1219
+ ack(policyId: string, approve: boolean): Promise<PolicyRunState | undefined>;
1220
+ list(): PolicyRunState[];
1221
+ /**
1222
+ * Subscribe to policy settlements — fired whenever a policy transitions
1223
+ * into a terminal state (done / blocked / cancelled) OR into awaiting-ack.
1224
+ * Used by the `/policies/:id/wait` long-poll to block until a policy
1225
+ * resolves without reimplementing the state machine's event surface.
1226
+ * Returns an unsubscribe fn. The callback receives the policyId; read
1227
+ * the full state via `getStatus(policyId)`.
1228
+ */
1229
+ onSettle(cb: (policyId: string) => void): () => void;
1230
+ /**
1231
+ * Sync flush of the policy snapshot to disk. Call from the daemon
1232
+ * stop() path — mirrors sessions.shutdown().
1233
+ */
1234
+ shutdown(): void;
1235
+ }
1236
+
1237
+ /**
1238
+ * Orchestrator sub-gateway (ADR §4.2 / WP2) — the security primitive
1239
+ * that lets a spawned agent become a *scoped* orchestrator without
1240
+ * handing it the daemon's full toolset.
1241
+ *
1242
+ * The plain `/mcp` endpoint registers EVERY tool (command_execute,
1243
+ * fs_*, remote_*, mcp_import, terminals, driver CRUD) and bypasses auth
1244
+ * on loopback — pointing a child at it would be a privilege handout.
1245
+ * This module builds a second, scoped MCP server that registers ONLY a
1246
+ * curated orchestration allowlist, gated by an unguessable per-child
1247
+ * scope-token. The HTTP layer mounts it at `/mcp/orchestrator`
1248
+ * (`http-server.ts`); WP3 will auto-mint a token and inject the URL at
1249
+ * spawn time. WP2 only builds the primitive.
1250
+ *
1251
+ * Three pieces:
1252
+ * - DEFAULT_ORCHESTRATOR_TOOLS — the curated allowlist.
1253
+ * - createScopeTokenRegistry() — mint / verify / revoke scope-tokens.
1254
+ * - createOrchestratorMcpServerFactory() — build a scoped server for
1255
+ * a verified scope.
1256
+ */
1257
+
1258
+ /**
1259
+ * The curated set of tools a scoped child orchestrator may call.
1260
+ *
1261
+ * INCLUDED — the orchestration primitives:
1262
+ * agent_start, agent_prompt, agent_output, agent_kill,
1263
+ * session_monitor, session_events_poll (spawn + drive + fan-in)
1264
+ * session_list, session_tree (observe). NB: list is NOT yet scoped
1265
+ * to the child's own subtree — that subtree filter is WP4
1266
+ * (parentSessionId/depth). For WP2 it is exposed daemon-wide;
1267
+ * documented debt.
1268
+ *
1269
+ * EXCLUDED — the danger surface (each a separate trust boundary):
1270
+ * command_execute, terminal/PTY tools (raw shell), file_* /
1271
+ * directory_* tools, remote_* (publish the daemon to the internet),
1272
+ * mcp_import / mcp_discovered_* / mcp_imported_* (widen the daemon's
1273
+ * own MCP surface), and driver CRUD (create_/delete_/update_driver —
1274
+ * these never register here because the scoped server is built with
1275
+ * no doctype specs).
1276
+ */
1277
+ declare const DEFAULT_ORCHESTRATOR_TOOLS: readonly string[];
1278
+ interface OrchestratorScope {
1279
+ /** The opaque scope-token handed to (and presented by) the child. */
1280
+ token: string;
1281
+ /** The effective tool allowlist for this scope (⊆ the default and,
1282
+ * for a recursively-spawned child, ⊆ its parent's allowlist). */
1283
+ tools: ReadonlySet<string>;
1284
+ /**
1285
+ * Session id of the orchestrator that OWNS this token — i.e. the
1286
+ * child session that presents it back on `/mcp/orchestrator`. The
1287
+ * token is minted *before* the child session exists (the injector
1288
+ * needs the URL to compose the spawn), so this is bound late, after
1289
+ * `spawnAgent` returns the id, via `bindOwner`. Until bound, spawns
1290
+ * through this scope are attributed to no parent and its subtree is
1291
+ * empty (safe degradation). (ADR §4.5 / WP4) */
1292
+ ownerSessionId?: string;
1293
+ /** Recursion depth of the owning orchestrator session. A spawn made
1294
+ * *through* this scope produces a child at `depth + 1`. Root scopes
1295
+ * (minted for a direct `/mcp` spawn) are depth 0. */
1296
+ depth: number;
1297
+ /** Max recursion depth reachable through this scope. A spawn whose
1298
+ * child depth (`depth + 1`) would exceed this is rejected. Clamped
1299
+ * to `HARD_MAX_DEPTH`. */
1300
+ maxDepth: number;
1301
+ /** Max concurrently-alive children the owning orchestrator may spawn
1302
+ * before further spawns are rejected. */
1303
+ maxChildren: number;
1304
+ }
1305
+ /**
1306
+ * Narrow a caller-requested tool list to ⊆ a ceiling subset.
1307
+ *
1308
+ * The `ceiling` is the maximum a caller may end up with — the global
1309
+ * `DEFAULT_ORCHESTRATOR_TOOLS` for a root scope, or the *parent's*
1310
+ * effective tools for a recursively-spawned child (non-re-grant: a
1311
+ * child can never widen beyond what its parent holds — ADR §4.5 #5).
1312
+ * The `{ tools: [...] }` form can only REMOVE from the ceiling, never
1313
+ * add: any name outside it is dropped. Omitting `requested` yields the
1314
+ * full ceiling. Omitting `ceiling` defaults to the global default.
1315
+ */
1316
+ declare function narrowOrchestratorTools(requested?: readonly string[], ceiling?: ReadonlySet<string>): Set<string>;
1317
+ interface MintScopeOptions {
1318
+ /** Caller-requested allowlist — narrowed ⊆ `ceiling` (or the default
1319
+ * when no ceiling is given). */
1320
+ tools?: readonly string[];
1321
+ /** Upper bound on the minted tools (the parent's effective tools for
1322
+ * a recursive child). Omit for a root scope (defaults to the global
1323
+ * default subset). */
1324
+ ceiling?: ReadonlySet<string>;
1325
+ /** Depth of the owning orchestrator (default 0). */
1326
+ depth?: number;
1327
+ /** Max depth reachable through this scope (default DEFAULT_MAX_DEPTH,
1328
+ * clamped to HARD_MAX_DEPTH). */
1329
+ maxDepth?: number;
1330
+ /** Per-orchestrator alive-child quota (default DEFAULT_MAX_CHILDREN). */
1331
+ maxChildren?: number;
1332
+ }
1333
+ interface ScopeTokenRegistry {
1334
+ /** Mint a fresh scope-token. See `MintScopeOptions`. */
1335
+ mint(opts?: MintScopeOptions): OrchestratorScope;
1336
+ /** Resolve a presented token to its scope, or null when unknown. */
1337
+ verify(token: string | null | undefined): OrchestratorScope | null;
1338
+ /** Late-bind the owning child session id onto a minted scope (the
1339
+ * token is minted before the child exists). No-op when the token is
1340
+ * unknown. Mutates the live scope object so `verify` reflects it. */
1341
+ bindOwner(token: string, sessionId: string): void;
1342
+ /** Drop a token (e.g. when the child session ends). */
1343
+ revoke(token: string): void;
1344
+ }
1345
+ /**
1346
+ * In-memory registry of live scope-tokens. Tokens are random 256-bit
1347
+ * hex (unguessable, never persisted) — see ADR §5.3. Single-user-host
1348
+ * trust assumption mirrors the loopback auth bypass on `/mcp`.
1349
+ */
1350
+ declare function createScopeTokenRegistry(): ScopeTokenRegistry;
1351
+ interface OrchestratorGatewayDeps {
1352
+ workspace: string;
1353
+ name?: string;
1354
+ version?: string;
1355
+ registry: SessionsRegistry;
1356
+ sessionEvents: SessionEventBus;
1357
+ eventRing: EventRing;
1358
+ supervisor?: CompletionPolicySupervisor;
1359
+ resolveAgentAdapter?: AgentAdapterResolver;
1360
+ listAgentAdapters?: AgentAdapterLister;
1361
+ /** Orchestrator injector (WP3/WP4). When wired, a child orchestrator
1362
+ * driving the scoped server can itself spawn sub-orchestrators
1363
+ * (`orchestrator: true` on its `agent_start`) — the new
1364
+ * token inherits depth+1 and is bounded by the caller's tools
1365
+ * (non-re-grant). Omitted → recursion injection is unavailable on
1366
+ * the scoped surface (a child can still spawn plain sub-agents). */
1367
+ orchestratorInjector?: OrchestratorInjector;
1368
+ /** Optional webhook notifier — same singleton as the root /mcp server.
1369
+ * When provided, per-session notifyUrl values from agent_start
1370
+ * are registered on spawn and unregistered on exit, so child
1371
+ * orchestrators spawning through this scoped gateway also fire webhooks. */
1372
+ webhookNotifier?: WebhookNotifier;
1373
+ /** Forwarded to `registerSessionTools` — the daemon's own plain `/mcp`
1374
+ * gateway URL, defaulted onto `hermes` `agent_start` spawns issued
1375
+ * through this scoped sub-gateway that pass no `mcpServers`. See
1376
+ * `RegisterAgentToolsOptions.daemonMcpUrl`. */
1377
+ daemonMcpUrl?: string;
1378
+ }
1379
+ type OrchestratorMcpServerFactory = (scope: OrchestratorScope) => Promise<McpServer>;
1380
+ /**
1381
+ * Build a factory that produces a scoped MCP server for a verified
1382
+ * scope. The server is created with NO doctype specs and NO workspace
1383
+ * (so no CRUD verbs, no `self_inspect`, no driver tools leak in), then
1384
+ * the orchestration passes run against a `withToolSubset` wrapper so
1385
+ * only `scope.tools` survive. Critically, `mcpProxy` and the PTY
1386
+ * factory are never wired here — import + terminal tools stay unwired
1387
+ * AND are excluded from the subset (defense in depth).
1388
+ */
1389
+ declare function createOrchestratorMcpServerFactory(deps: OrchestratorGatewayDeps): OrchestratorMcpServerFactory;
1390
+ /**
1391
+ * The auto-injection assembly (ADR §4.4 / WP3) — turns the "make this
1392
+ * child an orchestrator" intent into the concrete `mcpServers` entry
1393
+ * the ACP `session/new` call mounts, plus the token lifecycle.
1394
+ *
1395
+ * One call (`inject`) does three things:
1396
+ * 1. mint a scope-token (narrowed ⊆ default when `tools` is given);
1397
+ * 2. build the `mcpServers` entry pointing the child at the daemon's
1398
+ * own scoped sub-gateway (`/mcp/orchestrator?scope=<token>`);
1399
+ * 3. hand back `bindLifecycle(sessionId)` so the caller can revoke
1400
+ * the token the moment that child session exits — no token leaks
1401
+ * past the life of the session it was minted for.
1402
+ *
1403
+ * Assembled in `createGateway` (not the CLI resolver) because it needs
1404
+ * the gateway's scope-token registry, the session-event bus, and the
1405
+ * HTTP listener's port — all of which live there.
1406
+ */
1407
+ interface OrchestratorInjection {
1408
+ /** The `mcpServers` entry to merge into the child's `session/new`. */
1409
+ entry: AcpMcpServer;
1410
+ /** The minted scope — `token` gates the endpoint, `tools` is the
1411
+ * effective (narrowed) allowlist. */
1412
+ scope: OrchestratorScope;
1413
+ /**
1414
+ * Revoke the scope-token once `sessionId` exits. Subscribes to the
1415
+ * session-event bus for that one session's `session:exited` and
1416
+ * self-unsubscribes on fire. Returns an unsubscribe in case the
1417
+ * caller wants to drop the binding early (rarely needed).
1418
+ */
1419
+ bindLifecycle(sessionId: string): () => void;
1420
+ }
1421
+ interface OrchestratorInjectorDeps {
1422
+ scopeTokens: ScopeTokenRegistry;
1423
+ sessionEvents: SessionEventBus;
1424
+ /** Port the daemon's HTTP listener (and thus `/mcp/orchestrator`)
1425
+ * binds. The child reaches it over loopback. */
1426
+ port: number;
1427
+ /** Loopback host for the injected `ref`. Always loopback — the child
1428
+ * is co-located on the host. Default `127.0.0.1`. */
1429
+ host?: string;
1430
+ /** `name` advertised on the injected `mcpServers` entry. The child
1431
+ * sees its orchestration tools namespaced under this. Default
1432
+ * `agentproto`. */
1433
+ entryName?: string;
1434
+ }
1435
+ type OrchestratorInjector = (opts?: {
1436
+ tools?: readonly string[];
1437
+ /** The calling orchestrator's scope, when this spawn arrives via the
1438
+ * scoped sub-gateway (recursive spawn). The minted child token then
1439
+ * inherits `depth = caller.depth + 1`, its tools are bounded by the
1440
+ * caller's tools (non-re-grant — ADR §4.5 #5), and its depth/child
1441
+ * limits never exceed the caller's. Omit for a root spawn (direct
1442
+ * `/mcp`) → depth 0, full default subset, default limits. */
1443
+ caller?: OrchestratorScope;
1444
+ /** Override the max depth reachable through the minted scope (clamped
1445
+ * to the caller's, then to HARD_MAX_DEPTH). Root-only knob. */
1446
+ maxDepth?: number;
1447
+ /** Override the per-orchestrator child quota for the minted scope
1448
+ * (clamped to the caller's). Root-only knob. */
1449
+ maxChildren?: number;
1450
+ }) => OrchestratorInjection;
1451
+ /**
1452
+ * Build the orchestrator injector. Closed over the gateway's
1453
+ * scope-token registry + session-event bus + HTTP port. Returns a
1454
+ * function that the `agent_start` handler calls when its
1455
+ * `orchestrator` field is set.
1456
+ */
1457
+ declare function createOrchestratorInjector(deps: OrchestratorInjectorDeps): OrchestratorInjector;
1458
+
311
1459
  /**
312
1460
  * Pluggable adapter resolver — keeps the runtime package free of any
313
1461
  * @agentproto/cli dep. The host (cli `serve`, playground, embedding
@@ -328,13 +1476,50 @@ type AgentAdapterResolver = (slug: string) => Promise<{
328
1476
  startSession(opts: {
329
1477
  cwd: string;
330
1478
  resumeSessionId?: string;
331
- /** Model identifier forwarded from `start_agent_session`. Adapters
332
- * that support model selection (e.g. via a `--model` CLI flag) may
333
- * honour this; others silently ignore it. */
1479
+ /**
1480
+ * Manifest-declared mode id forwarded from `agent_start` (AIP-45
1481
+ * `AgentCliHandle.modes` e.g. claude-code's `plan` /
1482
+ * `accept-edits` / `bypass-permissions`, codex's `read-only`,
1483
+ * mastracode/opencode's `plan`). Applied at spawn time via
1484
+ * `composeSpawn`'s mode patch (`bin_args_append` / `env`) — BEFORE
1485
+ * the child process is exec'd, unlike `model`/`effort` below.
1486
+ * Adapters with no declared `modes` (e.g. hermes) ignore it; an
1487
+ * unknown id for an adapter that DOES declare modes throws
1488
+ * `RuntimeConfigError` (composeSpawn validates against the
1489
+ * manifest, so a typo fails the spawn rather than silently no-op).
1490
+ */
1491
+ mode?: string;
1492
+ /** Model identifier forwarded from `agent_start`. For ACP
1493
+ * adapters this is applied via session/set_config_option after
1494
+ * newSession (the ACP wrapper does not forward CLI args to claude).
1495
+ * Adapters that don't support model selection ignore it. */
334
1496
  model?: string;
1497
+ /** Effort level forwarded from `agent_start`. Effort is
1498
+ * model-dependent — same label ≠ same budget across models; defaults
1499
+ * differ by model. Omit to keep the model's own default. Applied
1500
+ * via session/set_config_option on ACP adapters; others ignore it. */
1501
+ effort?: string;
1502
+ /** MCP servers to mount into the spawned agent's session at spawn
1503
+ * time. Forwarded verbatim to the driver's `start({ mcpServers })`
1504
+ * → the ACP arm's `session/new.mcpServers`, giving the child agent
1505
+ * a host-chosen scoped toolset (e.g. the daemon's own orchestration
1506
+ * gateway). Adapters that don't model MCP mounting ignore it. */
1507
+ mcpServers?: AcpMcpServer[];
1508
+ /** Called on any adapter-process activity (ACP JSON-RPC traffic in
1509
+ * either direction) — forwarded to the driver's
1510
+ * `runtime.start({ onActivity })`. The caller (agent_start's MCP
1511
+ * handler, POST /sessions/agent) wires this to pulse
1512
+ * `SessionDescriptor.lastActivityAt` via `registry.pulseActivity(id)`. */
1513
+ onActivity?: () => void;
335
1514
  }): Promise<AgentSessionLike>;
336
1515
  /** Display label for the descriptor's `command` field. */
337
1516
  commandPreview?: string;
1517
+ /** Best-effort per-session usage reader (adapter-specific, e.g. hermes state.db). */
1518
+ readUsage?: (adapterSessionId: string) => Promise<{
1519
+ costUsd?: number;
1520
+ tokensIn?: number;
1521
+ tokensOut?: number;
1522
+ } | null>;
338
1523
  } | null>;
339
1524
  /**
340
1525
  * Compact adapter metadata for the discovery endpoints. Independent
@@ -357,6 +1542,150 @@ interface AuthOptions {
357
1542
  token?: string;
358
1543
  }
359
1544
 
1545
+ /**
1546
+ * InboundWatcher — polls agentpush `poll_inbound` on a timer and
1547
+ * spawns one agent per contact_ref that has unread inbound messages.
1548
+ *
1549
+ * Flow per tick:
1550
+ * mcp_imported_call(alias, "poll_inbound", {source, since:cursor})
1551
+ * → group events by contact_ref
1552
+ * → spawn one agent per contact with prompt from promptTemplate
1553
+ * → advance + persist cursor to ~/.agentproto/agentpush-cursors.json
1554
+ *
1555
+ * Cursor key = "${alias}:${source}" — stable across daemon restarts so
1556
+ * cursor resume actually works (a random watcherId would break it).
1557
+ *
1558
+ * Prompt template variables: {{source}}, {{contact_ref}},
1559
+ * {{messages_json}}, {{count}}.
1560
+ *
1561
+ * The mcpServersForChild array is forwarded to the spawned agent's
1562
+ * startSession + spawnAgent so the child can call dispatch_request
1563
+ * directly on the mounted agentpush MCP server.
1564
+ *
1565
+ * State is in-memory; cursors persist to disk (debounced async write
1566
+ * + sync flush at shutdown), matching the supervisor.ts pattern.
1567
+ */
1568
+
1569
+ interface WatcherStartInput {
1570
+ /** Imported MCP alias (e.g. "agentpush"). */
1571
+ alias: string;
1572
+ /** Contact ref / channel to watch — forwarded as `source` to poll_inbound. */
1573
+ source: string;
1574
+ /** Agent adapter slug (e.g. "claude-code"). */
1575
+ adapter: string;
1576
+ /**
1577
+ * Prompt template with placeholders:
1578
+ * {{source}} — the watched source
1579
+ * {{contact_ref}} — the sender's contact_ref
1580
+ * {{messages_json}} — JSON array of the new events
1581
+ * {{count}} — number of new messages
1582
+ */
1583
+ promptTemplate: string;
1584
+ /** Poll interval in ms. Default 5 000. */
1585
+ pollIntervalMs?: number;
1586
+ /** Working directory for spawned agents. Default process.cwd(). */
1587
+ cwd?: string;
1588
+ /** Optional label suffix on spawned sessions. */
1589
+ label?: string;
1590
+ /**
1591
+ * MCP servers to mount in the child agent at spawn time.
1592
+ * Pass agentpush here so the child can call dispatch_request.
1593
+ */
1594
+ mcpServersForChild?: Array<{
1595
+ name: string;
1596
+ transport: "stdio" | "http" | "sse";
1597
+ ref?: string;
1598
+ }>;
1599
+ }
1600
+ interface WatcherDescriptor {
1601
+ watcherId: string;
1602
+ alias: string;
1603
+ source: string;
1604
+ adapter: string;
1605
+ pollIntervalMs: number;
1606
+ status: "running" | "stopped";
1607
+ /** Next `since` value for poll_inbound (exclusive lower bound). */
1608
+ cursor: number;
1609
+ lastPollAt?: string;
1610
+ lastFireAt?: string;
1611
+ /** Total agent sessions spawned since the watcher started. */
1612
+ spawned: number;
1613
+ }
1614
+ interface InboundWatcher {
1615
+ start(input: WatcherStartInput): WatcherDescriptor;
1616
+ /** Returns false when watcherId is unknown. */
1617
+ stop(watcherId: string): boolean;
1618
+ list(): WatcherDescriptor[];
1619
+ /** Stops all watchers and flushes cursor state to disk synchronously. */
1620
+ shutdown(): void;
1621
+ }
1622
+
1623
+ /**
1624
+ * Browser family on top of `@agentproto/adapter-kit` — Phase 3 (lightest
1625
+ * adoption: no creds, no wizard, no catalog file).
1626
+ *
1627
+ * Provides the kit-compatible types (`BrowserAdapterHandle`) and the
1628
+ * `makeBrowserAdapterLister` factory that wraps the existing injected
1629
+ * `listBrowserAdapters` / `resolveBrowserAdapter` functions in a standard
1630
+ * `AdapterLister<BrowserAdapterInfo>`.
1631
+ *
1632
+ * The resulting lister is passed as the `lister` option to
1633
+ * `registerBrowserTools`, which uses it to produce the same
1634
+ * `{ id, name, description, defaultPort }[]` JSON the legacy path emits
1635
+ * (output shape is unchanged — only the internal flow changes).
1636
+ *
1637
+ * Kit invariants honoured:
1638
+ * - `check()` is NEVER called by the lister (OQ-5).
1639
+ * - `BrowserAdapterInfo` carries no cred values (Appendix B).
1640
+ * - `requiresSetup = false` → status is always "ready" without a ledger.
1641
+ */
1642
+
1643
+ /**
1644
+ * Kit-compatible handle for a single in-process browser adapter.
1645
+ * `requiresSetup` is always `false` — browser adapters need no creds or
1646
+ * setup wizard, so the kit classifies them as "ready" on first resolution.
1647
+ */
1648
+ interface BrowserAdapterHandle extends AdapterHandle {
1649
+ readonly requiresSetup: false;
1650
+ readonly defaultPort: number;
1651
+ }
1652
+ /**
1653
+ * Build a kit-compatible `AdapterLister<BrowserAdapterInfo>` from the
1654
+ * existing injected browser adapter functions. The returned lister uses an
1655
+ * empty catalog and `discoverExtras` to surface the injected adapters, then
1656
+ * maps each resolved handle's fields into a `BrowserAdapterInfo` descriptor.
1657
+ *
1658
+ * The caller should pass the result as the `lister` option to
1659
+ * `registerBrowserTools`, which extracts `AdapterEntry.info` to preserve the
1660
+ * existing tool response shape.
1661
+ */
1662
+ declare function makeBrowserAdapterLister(opts: {
1663
+ listBrowserAdapters: BrowserAdapterLister;
1664
+ resolveBrowserAdapter?: BrowserAdapterResolver;
1665
+ }): AdapterLister<BrowserAdapterInfo>;
1666
+
1667
+ /**
1668
+ * Turns a raw tool-call name + args (and its eventual result) into a short,
1669
+ * human-readable line for ring-buffer / CLI / transcript rendering. Without
1670
+ * this, every tool call renders as a bare `[tool] view` with no args and no
1671
+ * outcome — this module is the one place that knows how to summarize both.
1672
+ */
1673
+ /** A one-line human summary of a tool call, e.g. `read src/foo.ts` or `⏰ wake in 30s — checking CI`. */
1674
+ declare function formatToolCall(toolName: string, args: unknown): string;
1675
+ /**
1676
+ * A short outcome line for a completed tool call, or `null` when there's
1677
+ * nothing useful to show (e.g. an empty/void result). `toolName` is
1678
+ * accepted for parity with `formatToolCall` and future bespoke result
1679
+ * formatting, but generic text extraction covers today's cases.
1680
+ */
1681
+ declare function formatToolResult(toolName: string | undefined, result: unknown, isError: boolean): string | null;
1682
+
1683
+ /** A journal-file StepCache scoped to one cacheKey. Best-effort: read/parse
1684
+ * failures degrade to "no cache" (a miss), never throw into the run. */
1685
+ declare function createFileStepCache(cacheKey: string, opts?: {
1686
+ dir?: string;
1687
+ }): StepCache;
1688
+
360
1689
  /**
361
1690
  * `.agentproto/` config dir — runtime-managed state at the root of
362
1691
  * every workspace. Mirrors the `.git/` model: user-content stays at
@@ -369,6 +1698,15 @@ interface AuthOptions {
369
1698
  * for `cat .agentproto/runtime.json` diagnostics + future tooling
370
1699
  * that wants to discover a running gateway from disk.
371
1700
  *
1701
+ * The same snapshot is ALSO mirrored to the central, workspace-
1702
+ * independent daemon registry at `~/.agentproto/daemons/<port>.json`
1703
+ * (see daemonRegistryDir / writeDaemonRegistryEntry below). The
1704
+ * per-workspace file only helps `cat`-style local diagnostics and
1705
+ * discovery for REGISTERED workspaces; a daemon launched from an
1706
+ * arbitrary cwd (tunnel mode, a repo checkout) writes its workspace
1707
+ * file somewhere discovery never walks. The central entry is what
1708
+ * makes `agentproto chat` / `sessions` find such a daemon.
1709
+ *
372
1710
  * What does NOT go here:
373
1711
  * - HEARTBEAT.md — user-edited content
374
1712
  * - conversations/ — user-readable chat history
@@ -406,13 +1744,44 @@ interface RuntimeMeta {
406
1744
  */
407
1745
  token: string;
408
1746
  }
1747
+ /** `~/.agentproto/daemons/` — the central, workspace-independent daemon
1748
+ * registry. Each live daemon owns one `<port>.json` here. */
1749
+ declare function daemonRegistryDir(): string;
1750
+ /**
1751
+ * Write `<registry>/<port>.json` (mode 0600 — holds the bearer token).
1752
+ * Best-effort; failure here never gates the gateway. Mirrors the
1753
+ * per-workspace runtime.json so the same `RuntimeMeta` shape is readable
1754
+ * from either location.
1755
+ */
1756
+ declare function writeDaemonRegistryEntry(meta: RuntimeMeta): Promise<void>;
1757
+ /** Remove a daemon's central registry entry. Best-effort; missing is a
1758
+ * no-op. Called from graceful shutdown alongside unlinkRuntimeMeta. */
1759
+ declare function unlinkDaemonRegistryEntry(port: number): Promise<void>;
1760
+ /**
1761
+ * Read every entry in the central daemon registry, newest-first by
1762
+ * `startedAt`. No liveness filtering here — callers decide (discovery
1763
+ * skips dead PIDs; the sweep deletes them). Malformed / unreadable
1764
+ * entries are skipped silently.
1765
+ */
1766
+ declare function readDaemonRegistry(): Promise<Array<{
1767
+ meta: Partial<RuntimeMeta> & Record<string, unknown>;
1768
+ path: string;
1769
+ mtime: Date;
1770
+ }>>;
1771
+ /**
1772
+ * Delete central registry entries whose PID is dead (kill -9, crash,
1773
+ * reboot). Returns the paths cleaned. Called at `serve` boot — the same
1774
+ * footgun the per-workspace sweep guards against, applied to the central
1775
+ * registry. Skips `currentPort` (the booting daemon's own entry).
1776
+ */
1777
+ declare function sweepStaleDaemonRegistry(currentPort: number): Promise<string[]>;
409
1778
  /**
410
1779
  * Delete `<workspace>/.agentproto/runtime.json`. Best-effort — called
411
1780
  * from the daemon's graceful shutdown path so a CLI looking for the
412
1781
  * live daemon doesn't pick up a stale token after this process exits.
413
1782
  * Missing file is a no-op (the daemon may never have written one).
414
1783
  */
415
- declare function unlinkRuntimeMeta(workspace: string): Promise<void>;
1784
+ declare function unlinkRuntimeMeta(workspace: string, port?: number): Promise<void>;
416
1785
  /**
417
1786
  * Read a runtime.json from `<workspace>/.agentproto/`. Returns the
418
1787
  * parsed object (no validation) plus the on-disk mtime, or null when
@@ -435,6 +1804,90 @@ declare function readRuntimeMeta(workspace: string): Promise<{
435
1804
  */
436
1805
  declare function sweepStaleRuntimeMetas(workspaces: readonly string[], currentWorkspace: string): Promise<string[]>;
437
1806
 
1807
+ /**
1808
+ * MCP tools for event-driven orchestration:
1809
+ * - session_events_poll — cheap cursor-based snapshot of session events
1810
+ * - session_monitor — multiplexed long-poll (1 call for N sessions)
1811
+ *
1812
+ * These complement the existing per-session waitForTurnEnd inside
1813
+ * agent_output. The new tools handle multi-session fan-in
1814
+ * and external-client retrigger without burning polling tokens.
1815
+ */
1816
+
1817
+ /**
1818
+ * Event kind a session wait can target. Mirrors the `session_monitor`
1819
+ * `event` parameter. `turn-end` also matches `awaiting-input` (both
1820
+ * signal end-of-turn), matching the MCP tool's semantics.
1821
+ */
1822
+ type SessionWaitEvent = "turn-end" | "awaiting-input" | "exited" | "any";
1823
+ /**
1824
+ * Result of a single-session wait. On a hit, carries the session id, the
1825
+ * matched event (ring-bus form, without the `session:` prefix), the
1826
+ * session's current status, and whether it's awaiting input. On a
1827
+ * timeout, `timedOut: true` and the watched id list.
1828
+ */
1829
+ interface SessionWaitResult {
1830
+ timedOut?: boolean;
1831
+ sessionId?: string;
1832
+ event?: string;
1833
+ source?: "ring" | "state" | "bus";
1834
+ awaitingInput?: boolean;
1835
+ status?: string;
1836
+ turnsCompleted?: number;
1837
+ sessionIds?: string[];
1838
+ since?: number;
1839
+ /** Structured awaiting-input question (harness-parity). Surface on
1840
+ * turn-end / awaiting-input matches so callers (MCP session_monitor +
1841
+ * REST /sessions/:id/wait) can read the question without a separate
1842
+ * transcript fetch. Mirrors desc.awaitingQuestion / ev.question. */
1843
+ question?: SessionAwaitingQuestion;
1844
+ }
1845
+ /**
1846
+ * Block until one of the listed sessions fires a matching lifecycle event
1847
+ * (turn-end / awaiting-input / exited / any), or until `timeoutMs` elapses.
1848
+ *
1849
+ * This is the single-session-capable core that the MCP `session_monitor`
1850
+ * tool and the REST `GET /sessions/:id/wait` route both call. The MCP tool
1851
+ * passes N ids (multiplexed fan-in); the REST route passes exactly one.
1852
+ * The semantics are identical: race-free cursor replay → synchronous
1853
+ * already-in-target-state check → bus long-poll → timeout.
1854
+ *
1855
+ * `since` is an EventRing cursor: when provided, already-emitted matching
1856
+ * events for the watched sessions that occurred after that cursor are
1857
+ * returned immediately (race-free replay) before subscribing to the bus.
1858
+ */
1859
+ declare function monitorSessionWait(opts: {
1860
+ registry: SessionsRegistry;
1861
+ sessionEvents: SessionEventBus;
1862
+ eventRing: EventRing;
1863
+ sessionIds: string[];
1864
+ event?: SessionWaitEvent;
1865
+ timeoutMs?: number;
1866
+ since?: number;
1867
+ }): Promise<SessionWaitResult>;
1868
+ /**
1869
+ * Block until the named policy's status transitions out of the active
1870
+ * `watching` / `queued` / `gating` / `nudging` states — i.e. reaches
1871
+ * `done`, `blocked`, `awaiting-ack`, or `cancelled` — then return the
1872
+ * full PolicyRunState. Returns `{ timedOut: true }` on timeout.
1873
+ *
1874
+ * Hooks into the supervisor's `onSettle` callback (the single reliable
1875
+ * signal — some terminal transitions like `cancel()` / `ack(false)` /
1876
+ * single-session-exit emit no SessionEventBus event). The MCP
1877
+ * `policy_status` tool and the REST `GET /policies/:id/wait` route both
1878
+ * delegate here, so the wait semantics are shared across transports.
1879
+ */
1880
+ declare function monitorPolicyWait(opts: {
1881
+ supervisor: CompletionPolicySupervisor;
1882
+ policyId: string;
1883
+ timeoutMs?: number;
1884
+ }): Promise<{
1885
+ timedOut: true;
1886
+ } | {
1887
+ timedOut: false;
1888
+ state: PolicyRunState;
1889
+ }>;
1890
+
438
1891
  /**
439
1892
  * @agentproto/runtime — long-running gateway around an agentproto
440
1893
  * workspace dir.
@@ -490,12 +1943,18 @@ interface CreateGatewayOptions {
490
1943
  * Without this, /sessions still works for raw `argv` spawns. */
491
1944
  resolveAgentAdapter?: AgentAdapterResolver;
492
1945
  /** Optional adapter lister — when provided, enables
493
- * `GET /adapters` HTTP route + `list_adapters` MCP tool so UIs
1946
+ * `GET /adapters` HTTP route + `adapter_list` MCP tool so UIs
494
1947
  * can discover what's installed on the host. */
495
1948
  listAgentAdapters?: AgentAdapterLister;
1949
+ /** Optional browser adapter resolver — when provided, enables the
1950
+ * `start_browser` MCP tool (launches Camofox / Bureau / Chromium). */
1951
+ resolveBrowserAdapter?: BrowserAdapterResolver;
1952
+ /** Optional browser adapter lister — when provided, enables the
1953
+ * `browser_adapter_list` MCP tool. */
1954
+ listBrowserAdapters?: BrowserAdapterLister;
496
1955
  /** Optional PTY factory (node-pty wrapper, typically from the cli
497
1956
  * layer's `loadNodePtyFactory()`). When provided, enables
498
- * `POST /sessions/terminal`, the `start_terminal_session` MCP
1957
+ * `POST /sessions/terminal`, the `terminal_start` MCP
499
1958
  * tool family, and the `/sessions/:id/pty` WebSocket. Without it,
500
1959
  * those routes return 501 / the MCP tools aren't registered. */
501
1960
  spawnPty?: PtyFactory;
@@ -512,6 +1971,19 @@ interface CreateGatewayOptions {
512
1971
  /** When true, drop the localhost-wildcard defaults — only the
513
1972
  * explicit `allowedOrigins` list is honoured. */
514
1973
  strictOrigins?: boolean;
1974
+ /**
1975
+ * Opt-in deferred/lazy MCP tool loading (harness-parity item 3, see
1976
+ * `deferred-tools.ts`). When set, every root-gateway tool outside
1977
+ * `alwaysOn` registers but starts disabled — excluded from the first
1978
+ * `tools/list` — until the always-on `tool_search` meta-tool pulls it
1979
+ * in by keyword. Omitted (default) → every tool is eagerly enabled,
1980
+ * identical to pre-existing behaviour. Does not affect the scoped
1981
+ * `/mcp/orchestrator` sub-gateway, which already exposes a small
1982
+ * curated allowlist (`DEFAULT_ORCHESTRATOR_TOOLS`).
1983
+ */
1984
+ deferredTools?: {
1985
+ alwaysOn?: readonly string[];
1986
+ };
515
1987
  }
516
1988
  interface GatewayHandle {
517
1989
  url: string;
@@ -522,11 +1994,23 @@ interface GatewayHandle {
522
1994
  * inside the same process can register their child processes for
523
1995
  * visibility through /sessions and the CLI TUI. */
524
1996
  sessions: SessionsRegistry;
1997
+ /** Tunnel registry — manages public cloudflared tunnels for any
1998
+ * local port. Exposed so embedding hosts can open tunnels
1999
+ * programmatically without going through the HTTP/MCP surface. */
2000
+ tunnels: TunnelRegistry;
525
2001
  /** Per-boot bearer token required on mutating /sessions/* routes
526
2002
  * + WS PTY upgrades. Exposed so an embedding host (e.g. the CLI
527
2003
  * shell that hosts the gateway in-process) can pass it to child
528
2004
  * tools without re-reading the runtime.json file. */
529
2005
  token: string;
2006
+ /** Mint a scope-token for the scoped orchestrator sub-gateway
2007
+ * (`/mcp/orchestrator`). The returned `token` gates that endpoint
2008
+ * and `tools` is the effective allowlist (⊆ the default orchestrator
2009
+ * subset). WP3 will call this at spawn time and inject the URL into
2010
+ * the child's `mcpServers`; for WP2 it's the internal primitive. */
2011
+ mintOrchestratorScope(opts?: {
2012
+ tools?: readonly string[];
2013
+ }): OrchestratorScope;
530
2014
  stop(): Promise<void>;
531
2015
  }
532
2016
  /**
@@ -543,4 +2027,4 @@ interface GatewayHandle {
543
2027
  */
544
2028
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
545
2029
 
546
- export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, BuildHeartbeatAgent, type CreateGatewayOptions, type GatewayHandle, type RuntimeMeta, type SessionDescriptor, type SessionKind, type SessionStatus, type SessionsRegistry, type SpawnAgentInput, type SpawnSessionInput, WorkspaceFs, createGateway, readRuntimeMeta, sweepStaleRuntimeMetas, unlinkRuntimeMeta };
2030
+ export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AttachPolicyInput, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CommitSpec, type CompletionPolicySupervisor, type CreateGatewayOptions, DEFAULT_ORCHESTRATOR_TOOLS, type GateSpec, type GatewayHandle, type InboundWatcher, type JudgeGateSpec, type OnFailSpec, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type PolicyRunState, type PolicyRunStatus, type RegisterBrowserInput, type RegisterSessionInput, type RuntimeMeta, type ScopeTokenRegistry, type SessionDescriptor, type SessionKind, type SessionStatus, type SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, type SpawnSessionInput, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createScopeTokenRegistry, daemonRegistryDir, formatToolCall, formatToolResult, makeBrowserAdapterLister, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, readDaemonRegistry, readRuntimeMeta, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };