@agentproto/runtime 0.1.0 → 0.3.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,106 @@
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
+ export { PROVIDER_ENV_VARS, ProviderEntry, ProvidersFile, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey } from './providers-store.js';
12
+
13
+ /**
14
+ * In-process pub/sub bus for session lifecycle events. Separate from
15
+ * RuntimeEvents (global daemon bus) — session events are scoped to
16
+ * individual sessions and fire at turn-level granularity.
17
+ *
18
+ * Consumers: EventRing (poll_events cursor), WebhookNotifier
19
+ * (fire-and-forget HTTP), RoutineRunner (state machine fan-in),
20
+ * and wait_for_any MCP tool (long-poll multiplexed).
21
+ */
22
+ type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:exited" | "session:command-done" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed";
23
+ interface SessionTurnEndEvent {
24
+ type: "session:turn-end";
25
+ sessionId: string;
26
+ awaitingInput: boolean;
27
+ label?: string;
28
+ ts: string;
29
+ }
30
+ interface SessionAwaitingInputEvent {
31
+ type: "session:awaiting-input";
32
+ sessionId: string;
33
+ label?: string;
34
+ ts: string;
35
+ }
36
+ interface SessionExitedEvent {
37
+ type: "session:exited";
38
+ sessionId: string;
39
+ exitCode?: number;
40
+ status: "exited" | "killed" | "error";
41
+ label?: string;
42
+ ts: string;
43
+ }
44
+ /** Emitted when execute_command finishes. commandId matches the id
45
+ * returned by the execute_command MCP tool. */
46
+ interface SessionCommandDoneEvent {
47
+ type: "session:command-done";
48
+ sessionId: string;
49
+ commandId: string;
50
+ exitCode: number;
51
+ ts: string;
52
+ }
53
+ /** Emitted by the supervisor when a completion policy's gate passes. */
54
+ interface PolicyPassedEvent {
55
+ type: "policy:passed";
56
+ policyId: string;
57
+ sessionId: string;
58
+ ts: string;
59
+ }
60
+ /** Emitted by the supervisor when a completion policy's gate fails. */
61
+ interface PolicyFailedEvent {
62
+ type: "policy:failed";
63
+ policyId: string;
64
+ sessionId: string;
65
+ exitCode?: number;
66
+ ts: string;
67
+ }
68
+ /**
69
+ * Emitted by the supervisor (WP5) when a `then:"commit"` policy's gate passes
70
+ * but `requireHumanAck` is set: the commit is staged-and-ready but NOT yet
71
+ * executed. Carries the exact paths + message that `ack_policy(approve:true)`
72
+ * will commit. The policy sits in `awaiting-ack` until acked.
73
+ */
74
+ interface PolicyCommitReadyEvent {
75
+ type: "policy:commit-ready";
76
+ policyId: string;
77
+ sessionId: string;
78
+ paths: string[];
79
+ message: string;
80
+ ts: string;
81
+ }
82
+ /** Emitted by the supervisor (WP5) when a `then:"commit"` policy has actually
83
+ * committed — either directly (requireHumanAck:false) or after an approving
84
+ * `ack_policy`. Carries the resulting commit sha. */
85
+ interface PolicyCommittedEvent {
86
+ type: "policy:committed";
87
+ policyId: string;
88
+ sessionId: string;
89
+ sha: string;
90
+ paths: string[];
91
+ message: string;
92
+ ts: string;
93
+ }
94
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent;
95
+ interface SessionEventBus {
96
+ emit(ev: SessionEvent): void;
97
+ /** Subscribe to a specific event type. Returns an unsubscribe fn. */
98
+ on<E extends SessionEventType>(type: E, handler: (ev: Extract<SessionEvent, {
99
+ type: E;
100
+ }>) => void): () => void;
101
+ /** Subscribe to all event types. Returns an unsubscribe fn. */
102
+ onAny(handler: (ev: SessionEvent) => void): () => void;
103
+ }
8
104
 
9
105
  /**
10
106
  * Sessions registry — tracks long-lived child processes spawned by
@@ -78,7 +174,7 @@ interface AgentStreamEvent {
78
174
  data?: unknown;
79
175
  };
80
176
  }
81
- type SessionKind = "terminal" | "agent-cli" | "command";
177
+ type SessionKind = "terminal" | "agent-cli" | "command" | "browser";
82
178
  type SessionStatus = "starting" | "running" | "exited" | "killed" | "error";
83
179
  interface SessionDescriptor {
84
180
  id: string;
@@ -117,12 +213,29 @@ interface SessionDescriptor {
117
213
  * `/sessions/agent` to spin up a fresh ACP runtime. Undefined for
118
214
  * pty/command kinds. */
119
215
  adapterSlug?: string;
216
+ /** The model the session was requested to run (echoed back at spawn). */
217
+ model?: string;
218
+ /** Cumulative estimated USD cost of the session — best-effort, refreshed
219
+ * on each turn-end from the adapter's usage reader (e.g. hermes reads its
220
+ * state.db). Absent for adapters with no usage source. */
221
+ costUsd?: number;
222
+ /** Cumulative input / output token counts (same source + cadence as costUsd). */
223
+ tokensIn?: number;
224
+ tokensOut?: number;
120
225
  /** ACP-level session id (the adapter's own handle — claude-code's
121
226
  * conversation id, hermes' chat id, …). Set at spawnAgent time
122
227
  * from `agentSession.sessionId`. Survives across daemon restarts
123
228
  * via sessions.json so `restart` can pass it as `resumeSessionId`
124
229
  * and reattach to the prior conversation history. */
125
230
  adapterSessionId?: string;
231
+ /** MCP servers mounted into the agent's session at spawn time
232
+ * (orchestrator WP1). Persisted so the resume/re-spawn path can
233
+ * re-mount the same host-chosen toolset instead of resuming a
234
+ * capability-stripped agent. The current `AcpMcpServer` shape is
235
+ * `{ name, transport, ref? }` — a reference, NOT inline credentials
236
+ * — so this carries no secrets today. Should the shape ever grow
237
+ * headers/tokens, NEVER log this field's contents. */
238
+ mcpServers?: AcpMcpServer[];
126
239
  /** Provider-specific resume hints sniffed from the session's
127
240
  * output. claude-code prints `claude --resume <uuid>` on exit;
128
241
  * we capture that uuid as `claudeResumeId`. On `restart`, when a
@@ -134,6 +247,43 @@ interface SessionDescriptor {
134
247
  * Keys are adapter-specific so future adapters can add their own
135
248
  * ("hermesResumeId", etc.) without changing this type. */
136
249
  resumeMetadata?: Record<string, string>;
250
+ /** Set by the orchestration layer when the agent emits an
251
+ * "awaiting-input" turn-end. Cleared on the next turn start.
252
+ * Used by `wait_for_any` to fast-return without subscribing. */
253
+ awaitingInput?: boolean;
254
+ /** Count of turns that have fully completed (turn-end emitted) on this
255
+ * session. Lets `wait_for_any` fast-return for a session that already
256
+ * finished its turn before the wait subscribed — a fast turn that ends
257
+ * in "completed" (not "awaiting-input") leaves no other persisted
258
+ * signal (status stays "running", busy clears). 0 = never ran a turn,
259
+ * so a freshly-spawned idle session is NOT mistaken for done. */
260
+ turnsCompleted?: number;
261
+ /** True while a turn is actively running (mirror of the internal
262
+ * runtime `busy` flag). Lets `wait_for_any` distinguish "idle after a
263
+ * finished turn" from "mid-turn" so it doesn't fast-return a stale
264
+ * turn-end while the NEXT turn is still generating. */
265
+ busy?: boolean;
266
+ /** Session id of the orchestrator that spawned this session, set when
267
+ * the spawn arrived via the scoped orchestrator sub-gateway (the
268
+ * token carried the spawner's identity). Absent for direct `/mcp`
269
+ * spawns (the root operator). Powers the subtree scoping of
270
+ * list/kill and the per-parent child quota (orchestrator WP4).
271
+ * Persisted so the tree survives a daemon restart. */
272
+ parentSessionId?: string;
273
+ /** Recursion depth in the orchestrator tree: a direct (root) spawn is
274
+ * depth 0; a session spawned by a depth-d orchestrator is d+1. Used
275
+ * by the depth-cap guard. Persisted alongside `parentSessionId`.
276
+ * Treated as 0 when absent (legacy rows / direct spawns that predate
277
+ * WP4). */
278
+ depth?: number;
279
+ /** Adapter id that drives this session (e.g. "camofox", "bureau"). */
280
+ browserAdapterId?: string;
281
+ /** Port the browser service listens on. */
282
+ browserPort?: number;
283
+ /** Base URL of the browser service (e.g. "http://127.0.0.1:9377"). */
284
+ browserBaseUrl?: string;
285
+ /** Execution location — "local" (default) or "cloud". */
286
+ browserLocation?: "local" | "cloud";
137
287
  }
138
288
  interface SessionsRegistry {
139
289
  spawn(input: SpawnSessionInput): SessionDescriptor;
@@ -156,6 +306,11 @@ interface SessionsRegistry {
156
306
  * `attachPty(id, ...)`. Throws when the registry was constructed
157
307
  * without a `spawnPty` factory (node-pty optional dep missing). */
158
308
  spawnPty(input: SpawnPtyInput): SessionDescriptor;
309
+ /** Register an already-running browser service adapter as a tracked
310
+ * session (kind="browser"). Idempotent by identity — each call
311
+ * mints a fresh session id. The `stop` callback is invoked by
312
+ * `kill()` best-effort. */
313
+ registerBrowser(input: RegisterBrowserInput): SessionDescriptor;
159
314
  /** Send a follow-up turn to a live agent session. Throws when the
160
315
  * session is missing, not an agent-cli kind, or busy. The events
161
316
  * stream into the existing ring buffer + line emitter so /stream
@@ -214,6 +369,30 @@ interface SessionsRegistry {
214
369
  * awaited — the daemon shutdown loop handles process tree teardown. */
215
370
  shutdown(): void;
216
371
  }
372
+ interface RegisterBrowserInput {
373
+ /** Adapter id (e.g. "camofox", "bureau", "chromium"). */
374
+ adapterId: string;
375
+ /** Port the browser service listens on. */
376
+ port: number;
377
+ /** Base URL of the browser service (e.g. "http://127.0.0.1:9377"). */
378
+ baseUrl: string;
379
+ /** Execution location — "local" (default) or "cloud". Included in the
380
+ * dedup key so a cloud call never returns a pre-existing local session. */
381
+ location?: "local" | "cloud";
382
+ /** PID of the service process — undefined when managed by launchd/systemd. */
383
+ pid?: number;
384
+ /** True when the service was already healthy before this call (idempotent start). */
385
+ wasAlreadyRunning: boolean;
386
+ /** Initial lifecycle status. "running" when the service is confirmed
387
+ * healthy; "starting" when a non-blocking cold start returned before the
388
+ * service finished converging (health-wait continues in the background and
389
+ * `browser_status` / `list_browsers` flip it to running once up).
390
+ * Defaults to "running" when omitted. */
391
+ status?: Extract<SessionStatus, "running" | "starting">;
392
+ /** Async shutdown callback — called by kill() best-effort. */
393
+ stop: () => Promise<void>;
394
+ label?: string;
395
+ }
217
396
  interface RegisterSessionInput {
218
397
  /** Pre-existing child process to adopt (e.g. from a tunnel spawn). */
219
398
  child: ChildProcess;
@@ -242,6 +421,33 @@ interface SpawnAgentInput {
242
421
  label?: string;
243
422
  /** Pretty command for the descriptor (display only). */
244
423
  commandPreview?: string;
424
+ /** MCP servers mounted at spawn time. Persisted on the descriptor so
425
+ * the resume/re-spawn path can re-mount the same toolset (orchestrator
426
+ * WP1). */
427
+ mcpServers?: AcpMcpServer[];
428
+ /** Spawning orchestrator's session id — set when the spawn arrived
429
+ * through the scoped sub-gateway (orchestrator WP4). Recorded on the
430
+ * descriptor for subtree scoping + quota accounting. */
431
+ parentSessionId?: string;
432
+ /** Recursion depth for the new session (orchestrator WP4). Defaults to
433
+ * 0 when omitted (direct/root spawn). */
434
+ depth?: number;
435
+ /** Requested model id — recorded on the descriptor for display + echo. */
436
+ model?: string;
437
+ /** Hard ceiling on cumulative session cost (USD). When set and the
438
+ * adapter's usage reader reports a higher cost at a turn-end, the session
439
+ * is stopped (best-effort, turn-granular — caps continuation, can't abort
440
+ * a turn mid-flight). */
441
+ maxCostUsd?: number;
442
+ /** Best-effort usage reader, called on each turn-end to refresh the
443
+ * cost/token fields on the descriptor. Adapter-specific (e.g. hermes
444
+ * reads its state.db keyed by the adapter session id). Omit for adapters
445
+ * with no usage source. */
446
+ readUsage?: () => Promise<{
447
+ costUsd?: number;
448
+ tokensIn?: number;
449
+ tokensOut?: number;
450
+ } | null>;
245
451
  }
246
452
  interface SpawnSessionInput {
247
453
  kind: SessionKind;
@@ -286,6 +492,833 @@ interface SpawnPtyInput {
286
492
  label?: string;
287
493
  }
288
494
 
495
+ /**
496
+ * MCP tools that expose browser service adapters (Camofox, Bureau, …) to
497
+ * agents connected to the daemon. Lets a remote operator start, stop, and
498
+ * inspect browser service sessions through the same MCP connection it uses
499
+ * for fs/exec/agent-cli.
500
+ *
501
+ * Five tools:
502
+ * list_adapter_browsers discover available browser adapter ids + metadata
503
+ * start_browser ensure a browser adapter is up, register as a session
504
+ * stop_browser kill a running browser session
505
+ * list_browsers browse alive/recent browser sessions
506
+ * browser_status descriptor + live health probe for one session
507
+ *
508
+ * Auth: same as every other daemon tool — gated by the gateway's auth source.
509
+ *
510
+ * Injection pattern: `resolveBrowserAdapter` is passed in, not imported.
511
+ * This keeps the runtime package free of a hard dep on @agentproto/adapter-browser.
512
+ * The types below are minimal structural shapes — equivalent to `AgentSessionLike`
513
+ * in sessions.ts.
514
+ */
515
+
516
+ /**
517
+ * Minimal shape of a BrowserAdapterInstance — structurally compatible with
518
+ * @agentproto/adapter-browser's BrowserAdapterInstance. Kept local so the
519
+ * runtime package stays free of a hard dependency on the adapter package.
520
+ */
521
+ interface BrowserInstanceLike {
522
+ id: string;
523
+ port: number;
524
+ baseUrl: string;
525
+ pid?: number;
526
+ wasAlreadyRunning: boolean;
527
+ /** False when a non-blocking cold start returned before the service
528
+ * finished converging (background health-wait still running). */
529
+ healthy: boolean;
530
+ stop(): Promise<void>;
531
+ }
532
+ /**
533
+ * Minimal shape of a BrowserAdapterHandle — structurally compatible with
534
+ * @agentproto/adapter-browser's BrowserAdapterHandle.
535
+ */
536
+ interface BrowserAdapterLike {
537
+ id: string;
538
+ name: string;
539
+ description: string;
540
+ defaultPort: number;
541
+ healthPath: string;
542
+ /** Where this adapter runs — "local" (default) or "cloud". */
543
+ location?: "local" | "cloud";
544
+ ensure(opts: {
545
+ port?: number;
546
+ /** forwarded to dependent adapters (e.g. bureau→camofox) */
547
+ camofoxPort?: number;
548
+ launchCmd?: string;
549
+ env?: Record<string, string>;
550
+ timeoutMs?: number;
551
+ /** Opt-in non-blocking cold start: wait only this long for a freshly
552
+ * spawned service before returning `healthy:false` and converging
553
+ * in the background. */
554
+ initialWaitMs?: number;
555
+ log?: (s: string) => void;
556
+ /** Override execution location ("local" or "cloud"). */
557
+ location?: "local" | "cloud";
558
+ /** Base URL of the remote service when location="cloud". */
559
+ baseUrl?: string;
560
+ /** Override the binary path for a local spawn. */
561
+ binPath?: string;
562
+ }): Promise<BrowserInstanceLike>;
563
+ }
564
+ type BrowserAdapterResolver = (id: string) => BrowserAdapterLike | undefined;
565
+ type BrowserAdapterLister = () => {
566
+ id: string;
567
+ name: string;
568
+ description: string;
569
+ defaultPort: number;
570
+ /** Where this adapter runs — "local" (default) or "cloud". */
571
+ location?: "local" | "cloud";
572
+ /** Install methods — informational metadata from the adapter manifest. */
573
+ install?: unknown[];
574
+ /** Config prompts — informational metadata from the adapter manifest. */
575
+ config?: unknown[];
576
+ }[];
577
+ /**
578
+ * Family-specific descriptor surfaced in the kit-path `list_adapter_browsers`
579
+ * response. Fields match the legacy lister shape so the tool output is
580
+ * unchanged regardless of which path is active.
581
+ */
582
+ interface BrowserAdapterInfo {
583
+ id: string;
584
+ name: string;
585
+ description: string;
586
+ defaultPort: number;
587
+ }
588
+
589
+ /**
590
+ * Shared types for pluggable tunnel providers.
591
+ *
592
+ * Extracted here so both `RemoteController` (single-gateway tunnel) and
593
+ * `TunnelRegistry` (multi-tunnel general surface) can share the same
594
+ * provider contract without creating a circular dep.
595
+ *
596
+ * The tunnel family is the FIRST consumer of `@agentproto/adapter-kit`:
597
+ * {@link TunnelProviderHandle} extends the kit's generic `AdapterHandle`
598
+ * (slug/name/version/description/requiresSetup/check) with the
599
+ * tunnel-specific `capabilities` + the existing `start`/`stop` lifecycle.
600
+ */
601
+
602
+ interface ProviderStartOptions {
603
+ /** Local target the tunnel forwards to. */
604
+ target: {
605
+ host: string;
606
+ port: number;
607
+ };
608
+ /** Workspace path — for log / state files. */
609
+ workspace: string;
610
+ /** Called whenever the provider has a status update worth logging. */
611
+ onLog?: (line: string) => void;
612
+ }
613
+ interface ProviderStartResult {
614
+ publicUrl: string;
615
+ pid: number | null;
616
+ }
617
+ /**
618
+ * A pluggable tunnel provider.
619
+ *
620
+ * `start` returns the public URL once the upstream tunnel is ready.
621
+ * `stop` is idempotent — calling it twice or on an already-dead child
622
+ * must not throw.
623
+ */
624
+ interface RemoteProvider {
625
+ readonly id: string;
626
+ start(opts: ProviderStartOptions): Promise<ProviderStartResult>;
627
+ stop(): Promise<void>;
628
+ }
629
+
630
+ /**
631
+ * Slug-keyed tunnel provider registry — the single source of truth that both
632
+ * halves of the tunnel family consult:
633
+ *
634
+ * - the adapter-kit listing path (`list_tunnel_adapters` / `setup_tunnel_provider`)
635
+ * - the lifecycle path (`TunnelRegistry`: create / restore / start / stop)
636
+ *
637
+ * Before this module those two halves spoke different languages — the
638
+ * lifecycle engine only knew the short discriminators `"quick"`/`"named"` and
639
+ * had no ngrok case at all, while the adapter half spoke slugs. They never
640
+ * reconciled, so ngrok was listable-but-not-creatable. Routing everything
641
+ * through one registry collapses that: a built-in OR a third-party
642
+ * `@agentproto/adapter-<slug>` / `@<scope>/agentproto-adapter-<slug>` provider
643
+ * resolves the same way and works end to end.
644
+ */
645
+
646
+ /** Per-slug credentials, as stored by the creds store / setup tool. */
647
+ type TunnelCreds = Record<string, string>;
648
+
649
+ /**
650
+ * TunnelRegistry — multi-tunnel public-URL manager.
651
+ *
652
+ * Manages a named set of cloudflared (or future provider) tunnels,
653
+ * each forwarding a local port to a public URL. Independent from
654
+ * RemoteController which handles the single "expose this gateway"
655
+ * use-case with bearer-token gating. This registry is for the general
656
+ * "create a public endpoint for any local port" surface.
657
+ *
658
+ * Lifecycle:
659
+ * create(input) → spawns provider, waits for URL, stores descriptor
660
+ * list() → TunnelDescriptor[]
661
+ * get(id) → TunnelDescriptor | undefined
662
+ * stop(id) → SIGTERM provider, mark stopped
663
+ * shutdown() → stop all active tunnels (called on daemon exit)
664
+ *
665
+ * Persistence: `~/.agentproto/tunnels.json` — descriptors survive
666
+ * daemon restarts; active entries are marked "stopped" on next boot
667
+ * since their child processes are gone (same GHOST pattern as
668
+ * sessions.ts).
669
+ */
670
+
671
+ type TunnelStatus = "starting" | "active" | "stopped" | "error";
672
+ /**
673
+ * Provider identifier — an open set of slugs (built-in `cloudflare-quick` /
674
+ * `cloudflare-named` / `ngrok`, legacy short names `quick` / `named`, or any
675
+ * third-party `@scope/agentproto-adapter-<slug>`). Normalized to a canonical
676
+ * slug only at resolve time, so persisted descriptors keep their original
677
+ * value and need no migration.
678
+ */
679
+ type TunnelProvider = string;
680
+ interface TunnelDescriptor {
681
+ id: string;
682
+ /** Optional user-friendly slug. Accepts id-or-name in stop/status calls. */
683
+ name?: string;
684
+ /** Free-text label surfaced in list / CLI table. */
685
+ label?: string;
686
+ provider: TunnelProvider;
687
+ targetHost: string;
688
+ targetPort: number;
689
+ publicUrl: string;
690
+ status: TunnelStatus;
691
+ pid: number | null;
692
+ createdAt: string;
693
+ stoppedAt?: string;
694
+ lastError?: string;
695
+ /**
696
+ * When true, the tunnel is relaunched on daemon boot (see
697
+ * `restoreOnBoot`). Only meaningful for `named` tunnels — a relaunched
698
+ * `quick` tunnel gets a fresh random URL, so autostart for it merely
699
+ * keeps *a* tunnel up, not a stable URL.
700
+ */
701
+ autostart?: boolean;
702
+ /** Stable public hostname routed to this tunnel. */
703
+ hostname?: string;
704
+ /** Cloudflare tunnel id or name (the `run` target). */
705
+ tunnelId?: string;
706
+ /** Path to the tunnel credentials JSON (defaults under ~/.cloudflared). */
707
+ credentialsFile?: string;
708
+ }
709
+ interface CreateTunnelInput {
710
+ targetPort: number;
711
+ provider?: TunnelProvider;
712
+ name?: string;
713
+ label?: string;
714
+ targetHost?: string;
715
+ /** Relaunch this tunnel on daemon boot. See `TunnelDescriptor.autostart`. */
716
+ autostart?: boolean;
717
+ /** Stable public hostname (e.g. app.example.com). */
718
+ hostname?: string;
719
+ /** Cloudflare tunnel id or name to `run`. */
720
+ tunnelId?: string;
721
+ /** Path to the credentials JSON (defaults to ~/.cloudflared/<tunnelId>.json). */
722
+ credentialsFile?: string;
723
+ }
724
+ interface TunnelRegistryOptions {
725
+ /** Absolute path for persistence file. Defaults to ~/.agentproto/tunnels.json */
726
+ persistPath?: string;
727
+ /** Absolute path to workspace — used as scratch dir for provider config files. */
728
+ workspace?: string;
729
+ /** Hook for surfacing provider log lines. */
730
+ onLog?: (line: string) => void;
731
+ /**
732
+ * Read stored credentials for a provider slug. Lets the lifecycle build a
733
+ * provider that keeps its secrets in the creds store (e.g. ngrok's authtoken,
734
+ * or any third-party provider) rather than on the descriptor. Injected by the
735
+ * daemon (index.ts); descriptor-carried config (named's hostname/tunnelId)
736
+ * still takes precedence.
737
+ */
738
+ readCreds?: (slug: string) => Promise<TunnelCreds | null>;
739
+ }
740
+ declare class TunnelRegistry {
741
+ private readonly tunnels;
742
+ private readonly persistPath;
743
+ private readonly workspace;
744
+ private readonly onLog;
745
+ private readonly readCreds;
746
+ private persistTimer;
747
+ constructor(opts?: TunnelRegistryOptions);
748
+ create(input: CreateTunnelInput): Promise<TunnelDescriptor>;
749
+ /**
750
+ * Drive an entry's provider through start and fold the result back into
751
+ * its descriptor. On failure the descriptor is marked `error` and the
752
+ * error re-thrown (callers that want best-effort — e.g. boot restore —
753
+ * catch it). Shared by `create` and `restoreOnBoot`.
754
+ */
755
+ private startEntry;
756
+ /**
757
+ * Relaunch every `autostart` tunnel that isn't currently running. Called
758
+ * once on daemon boot — `loadFromDisk` has already ghosted formerly-live
759
+ * entries to `stopped` and given autostart ones a real provider. Quick
760
+ * tunnels are skipped (a relaunch yields a fresh random URL, defeating
761
+ * the point); named tunnels come back on their stable hostname.
762
+ *
763
+ * Best-effort: a provider that fails to start leaves its descriptor in
764
+ * `error` and the others still come up.
765
+ */
766
+ restoreOnBoot(): Promise<void>;
767
+ list(): TunnelDescriptor[];
768
+ get(id: string): TunnelDescriptor | undefined;
769
+ findByIdOrName(idOrName: string): TunnelDescriptor | undefined;
770
+ stop(idOrName: string): Promise<boolean>;
771
+ shutdown(): Promise<void>;
772
+ protected pickProviderForTest(_provider: TunnelProvider, desc: TunnelDescriptor): Promise<RemoteProvider>;
773
+ /**
774
+ * Build the creds passed to a provider: stored creds (ngrok authtoken,
775
+ * third-party secrets) merged with descriptor-carried config (named's
776
+ * hostname/tunnelId/credentialsFile), descriptor taking precedence.
777
+ */
778
+ private credsForDescriptor;
779
+ private findEntryByIdOrName;
780
+ private schedulePersist;
781
+ private persistNow;
782
+ private loadFromDisk;
783
+ }
784
+
785
+ /**
786
+ * Cursor-based ring buffer for session events. Bridges the in-process
787
+ * SessionEventBus (push) to the poll_events MCP tool (pull).
788
+ *
789
+ * Capacity default: 1 000 events. Oldest entry dropped on overflow —
790
+ * callers that lag beyond the cap see a gap in their cursor but never
791
+ * a block. `since()` returns `nextCursor = totalEmitted` so a fresh
792
+ * call with that value always starts just past the current tail.
793
+ */
794
+
795
+ interface EventRingSnapshot {
796
+ events: SessionEvent[];
797
+ nextCursor: number;
798
+ }
799
+ interface EventRingQueryOpts {
800
+ sessionIds?: string[];
801
+ types?: SessionEventType[];
802
+ /** Default 50. */
803
+ limit?: number;
804
+ }
805
+ interface EventRing {
806
+ /**
807
+ * Return events whose ring-index >= `cursor`. Pass 0 to get all
808
+ * retained events; pass the previously returned `nextCursor` for an
809
+ * incremental read. The returned `nextCursor` is always equal to
810
+ * `totalEmitted` — use it as the next `cursor` argument.
811
+ *
812
+ * Negative or below-base cursors are clamped to the oldest retained
813
+ * event (no error, just a potential replay of a partial window).
814
+ */
815
+ since(cursor: number, opts?: EventRingQueryOpts): EventRingSnapshot;
816
+ /** Wire the ring to a SessionEventBus. Returns an unsubscribe fn. */
817
+ wire(bus: SessionEventBus): () => void;
818
+ }
819
+
820
+ /**
821
+ * Completion-policy supervisor — WP1 + WP2 + WP3 + WP4 + WP5 + WP6 + WP7.
822
+ *
823
+ * WP7 (judge-gate): a gate may be a judge AGENT instead of a shell command —
824
+ * `gate: { judge: { adapter, model?, prompt, timeoutMs? } }`. When the trigger
825
+ * fires the supervisor spawns a short-lived agent (resolveAgentAdapter →
826
+ * spawnAgent), prompts it with the rubric (+ a minimal context tail of the
827
+ * watched session output), waits for its turn-end on the bus, reads the final
828
+ * reply and parses the LAST `VERDICT: PASS|FAIL` line (case-insensitive). PASS
829
+ * = gate pass (then-action), FAIL = gate fail (existing nudge/escalate path).
830
+ * Fail-safe: no resolver / spawn fail / timeout (default 120s) / unparseable
831
+ * verdict → FAIL (+ reason on state.error). The judge session is ALWAYS killed
832
+ * when the gate settles. The judge runs while the policy holds its `gating`
833
+ * slot, so it is accounted for in the WP6 concurrency cap. Persistence: a judge
834
+ * gate left in `gating` at crash is re-armed to `watching` like any active
835
+ * policy and simply re-runs the judge — no in-flight judge is persisted.
836
+ *
837
+ *
838
+ * State machine per attached policy:
839
+ * watching → gating → acting → done
840
+ * ↘ nudging (retries < maxRetries) → watching (gate failed, nudge sent)
841
+ * ↘ blocked (gate failed, retries exhausted)
842
+ * ↘ awaiting-ack → done (then:"commit" + requireHumanAck, ack approve)
843
+ * ↘ cancelled (ack reject)
844
+ * watching → cancelled (watched session exited before turn-end)
845
+ *
846
+ * Trigger: session:turn-end on the watched session(s).
847
+ * Gate: optional shell command via the same allowlist + cwd-anchor as
848
+ * execute_command. exit 0 = pass.
849
+ * Action (then:"emit"): emits policy:passed / policy:failed on the bus
850
+ * (readable via poll_events).
851
+ *
852
+ * WP5 (then:"commit"): on a GREEN gate, prepare a host-side git commit in the
853
+ * watched session's cwd — `git add -- <explicit paths>` then `git commit -m
854
+ * <message>` via the same allowlisted runCommand path (argv array, shell:false).
855
+ * GUARDRAILS: never `git add -A`/glob, never `git push`, never `--force`, never
856
+ * a branch the policy didn't ask for (commits the cwd's current branch), message
857
+ * passed as argv (no shell injection), empty paths → error (no commit). A `--`
858
+ * separator precedes the paths so a path can never be parsed as a git flag.
859
+ * "nothing to commit" is treated as SUCCESS (idempotent re-commit after a
860
+ * restart). `requireHumanAck` defaults to TRUE: the gate-green transition goes
861
+ * to `awaiting-ack` and emits `policy:commit-ready` (paths + message) instead of
862
+ * committing; the commit runs only on `ack_policy(approve:true)` →
863
+ * `policy:committed` (+ sha) → done; `approve:false` → cancelled. With
864
+ * `requireHumanAck:false` the commit runs directly at the green gate.
865
+ * onFail (WP2): when gate fails, re-prompts the session(s) (sendPrompt) up to
866
+ * maxRetries times, then transitions to blocked + emits policy:failed.
867
+ *
868
+ * WP3: persists PolicyRunState + input to ~/.agentproto/policies.json (or an
869
+ * injected persistPath). Debounced async write on every transition; sync flush
870
+ * at shutdown(). On boot, active policies (watching/gating/nudging/acting) are
871
+ * re-armed to "watching" and re-subscribed to the bus. Terminal states (done/
872
+ * blocked/cancelled) are kept for history. Session absent at reload → cancelled.
873
+ *
874
+ * WP6 (DAG + concurrency cap):
875
+ * - CHAINING: a policy may declare `next` — a recursive AttachPolicyInput.
876
+ * When the policy reaches `done` (emit-passed or commit-committed), the
877
+ * supervisor automatically attaches `next` as a fresh completion policy on
878
+ * the session(s) named in its own spec (chaining policies over EXISTING
879
+ * sessions — this WP does NOT spawn new sessions). The child's policyId is
880
+ * recorded on the parent as `nextPolicyId`. Chaining is idempotent (a parent
881
+ * already carrying a `nextPolicyId` never re-chains, so a re-armed/re-acked
882
+ * parent after a restart won't double-attach). `next` persists as part of the
883
+ * parent's `input`, so the whole chain survives a restart.
884
+ * - CONCURRENCY CAP: a daemon-wide cap (`concurrencyCap`, default 8) on how
885
+ * many policies may be in `gating`/`acting` at once. When a trigger fires
886
+ * and the cap is already saturated, the policy parks in `queued` (a FIFO
887
+ * gate queue) instead of gating. As each active policy settles (done /
888
+ * blocked / awaiting-ack / back-to-watching after a nudge), the queue is
889
+ * pumped and the oldest queued policy proceeds to gate. Queued policies hold
890
+ * NO slot, so a chained DAG can't deadlock behind the cap. The queued state
891
+ * persists and is re-armed to `watching` on reload (then re-evaluated).
892
+ *
893
+ * WP4 (fan-in): a policy may watch a GROUP of sessions (`sessionIds`). The gate
894
+ * runs ONCE, only after EVERY member of the group has finished its turn. Members
895
+ * are tracked in an idempotent `pending` set: a member is removed on its first
896
+ * `session:turn-end`; a repeated turn-end for an already-removed member is a
897
+ * no-op (no double-count). When the set empties → gating.
898
+ *
899
+ * A member that emits `session:exited` is treated as "this member is done": it
900
+ * is removed from the pending set exactly like a turn-end (a dead session will
901
+ * never emit one), so a partially-crashed group can still complete. If every
902
+ * member exits, the set empties and the gate runs once. The one exception is the
903
+ * degenerate group of size 1 (the legacy single-`sessionId` form): a lone
904
+ * watched session that exits has nothing left to gate on, so the policy is
905
+ * cancelled — preserving the original single-session contract.
906
+ *
907
+ * Back-compat: the legacy `sessionId` (single) form is a group of one. `state.
908
+ * sessionId` is retained as the group's representative (group[0]) and is the id
909
+ * carried on policy:passed / policy:failed events.
910
+ */
911
+
912
+ interface ShellGateSpec {
913
+ command: string;
914
+ args?: string[];
915
+ /** Working directory for the gate command. Defaults to the watched
916
+ * session's cwd, anchored to the workspace. */
917
+ cwd?: string;
918
+ timeoutMs?: number;
919
+ }
920
+ /**
921
+ * Judge-agent gate (WP7). Instead of a shell command, the gate spawns a
922
+ * short-lived LLM agent (via the daemon's `resolveAgentAdapter`), prompts it
923
+ * with `prompt` (+ a minimal context tail of the watched session output, if
924
+ * readable), waits for its turn to end, reads the final reply and parses a
925
+ * verdict line (`VERDICT: PASS` / `VERDICT: FAIL`, last occurrence,
926
+ * case-insensitive). PASS = gate pass (exit 0 equivalent), FAIL = gate fail
927
+ * (→ the existing nudge/escalate path). Fail-safe: a judge timeout OR an
928
+ * unparseable reply is treated as FAIL. The judge session is always killed
929
+ * when the gate settles (success or failure).
930
+ */
931
+ interface JudgeGateSpec {
932
+ judge: {
933
+ /** Agent CLI adapter slug used to spawn the judge (e.g. "claude-code"). */
934
+ adapter: string;
935
+ /** Optional model identifier forwarded to the adapter. */
936
+ model?: string;
937
+ /** The rubric / instructions for the judge. The supervisor appends a
938
+ * fixed instruction to end the reply with `VERDICT: PASS` or
939
+ * `VERDICT: FAIL`. */
940
+ prompt: string;
941
+ /** Max wall-clock for the judge turn before it is killed + FAIL.
942
+ * Default 120_000ms. */
943
+ timeoutMs?: number;
944
+ };
945
+ }
946
+ /** A gate is either a shell command (exit 0 = pass) or a judge agent. */
947
+ type GateSpec = ShellGateSpec | JudgeGateSpec;
948
+ interface OnFailSpec {
949
+ /**
950
+ * Message sent to the watched session via sendPrompt when the gate fails.
951
+ * Use the placeholder {code} to embed the actual exit code.
952
+ * Default: "Le gate a échoué (exit {code}). Corrige et termine jusqu'à ce qu'il passe."
953
+ */
954
+ nudge?: string;
955
+ /**
956
+ * Maximum number of consecutive gate failures before transitioning to
957
+ * blocked. Must be >= 1. Default: 2.
958
+ */
959
+ maxRetries?: number;
960
+ }
961
+ interface CommitSpec {
962
+ /**
963
+ * Explicit, literal paths to stage — workspace/cwd-relative. NEVER a glob,
964
+ * NEVER `-A`/`--all`. Staged via `git add -- <paths>` (the `--` guards each
965
+ * path from being parsed as a flag). An empty array is rejected at attach.
966
+ */
967
+ paths: string[];
968
+ /** Commit message. Passed as a single `-m` argv value — no shell. */
969
+ message: string;
970
+ /**
971
+ * Human-in-the-loop gate before the commit actually runs. Default TRUE:
972
+ * the green gate transitions to `awaiting-ack` + emits `policy:commit-ready`
973
+ * and waits for `ack_policy`. Set false to commit directly at the green gate.
974
+ */
975
+ requireHumanAck?: boolean;
976
+ }
977
+ interface AttachPolicyInput {
978
+ /**
979
+ * Session id whose turn-end triggers the policy (single-session / legacy
980
+ * form). Equivalent to `sessionIds: [sessionId]`. Provide this OR
981
+ * `sessionIds`; if both are given, `sessionIds` wins.
982
+ */
983
+ sessionId?: string;
984
+ /**
985
+ * Fan-in group (WP4): the gate runs once, only after EVERY listed session
986
+ * has finished its turn (turn-end or exit). Supersedes `sessionId` when
987
+ * present and non-empty. A single-element array behaves like `sessionId`.
988
+ */
989
+ sessionIds?: string[];
990
+ /**
991
+ * Optional gate. Absent → always pass immediately. Either a shell gate
992
+ * (exit 0 = pass) or a judge-agent gate (WP7: `{ judge: {...} }`).
993
+ */
994
+ gate?: GateSpec;
995
+ /**
996
+ * Action on a GREEN gate. "emit" (WP1) emits policy:passed. "commit" (WP5)
997
+ * stages explicit paths and commits on the host — see `commit`.
998
+ */
999
+ then: "emit" | "commit";
1000
+ /** Required when then === "commit". Ignored otherwise. */
1001
+ commit?: CommitSpec;
1002
+ /**
1003
+ * What to do when the gate fails (WP2). Absent → immediately blocked
1004
+ * (WP1 behaviour). Present → re-prompt the session up to maxRetries
1005
+ * times before blocking; the session must still be running to nudge.
1006
+ */
1007
+ onFail?: OnFailSpec;
1008
+ /**
1009
+ * DAG chaining (WP6). When this policy reaches `done`, the supervisor
1010
+ * automatically attaches `next` as a fresh completion policy. `next` is a
1011
+ * full AttachPolicyInput (recursive — it may declare its own `next`), and it
1012
+ * carries the session(s) it watches in its own spec. Chaining only attaches
1013
+ * policies onto already-running sessions; it never spawns new sessions.
1014
+ */
1015
+ next?: AttachPolicyInput;
1016
+ }
1017
+ type PolicyRunStatus = "watching" | "queued" | "gating" | "acting" | "nudging" | "awaiting-ack" | "done" | "blocked" | "cancelled";
1018
+ interface PolicyRunState {
1019
+ policyId: string;
1020
+ /** Representative session id (the fan-in group's first member, group[0]).
1021
+ * Carried on policy:passed / policy:failed events. */
1022
+ sessionId: string;
1023
+ /** The full fan-in group being watched. Always present; a length-1 group is
1024
+ * the legacy single-session form. */
1025
+ sessionIds: string[];
1026
+ /** Sessions in the group that have NOT yet finished their turn. The gate
1027
+ * fires once this set empties. Idempotent: a member is removed on its first
1028
+ * turn-end/exit; repeats are no-ops. Reset to the full group on each nudge. */
1029
+ pending: string[];
1030
+ status: PolicyRunStatus;
1031
+ /** Number of nudges sent so far (incremented after each nudge round). */
1032
+ retries: number;
1033
+ startedAt: string;
1034
+ endedAt?: string;
1035
+ lastGate?: {
1036
+ exitCode: number;
1037
+ at: string;
1038
+ };
1039
+ /**
1040
+ * The exact commit prepared at the green gate (WP5). Set when entering
1041
+ * `awaiting-ack` (or just before a direct commit) so that an ack arriving
1042
+ * after a daemon restart commits the same paths/message in the same cwd,
1043
+ * independent of whether the watched session still exists.
1044
+ */
1045
+ commitPlan?: {
1046
+ paths: string[];
1047
+ message: string;
1048
+ cwd: string;
1049
+ };
1050
+ /** The resulting commit sha once `then:"commit"` has committed. */
1051
+ commitSha?: string;
1052
+ /**
1053
+ * DAG chaining (WP6): the policyId of the child policy attached when this
1054
+ * policy reached `done`. Set exactly once (idempotent chaining guard).
1055
+ */
1056
+ nextPolicyId?: string;
1057
+ error?: string;
1058
+ }
1059
+ interface CompletionPolicySupervisor {
1060
+ /**
1061
+ * Attach a completion policy to an already-running session.
1062
+ * Returns immediately with the initial (watching) state.
1063
+ * The state machine runs in the background.
1064
+ */
1065
+ attach(input: AttachPolicyInput): PolicyRunState;
1066
+ getStatus(policyId: string): PolicyRunState | undefined;
1067
+ cancel(policyId: string): void;
1068
+ /**
1069
+ * Human acknowledgement for a `then:"commit"` policy parked in
1070
+ * `awaiting-ack` (WP5). `approve:true` runs the prepared commit (→ done,
1071
+ * emits policy:committed); `approve:false` cancels it (no commit). No-op on
1072
+ * any other state. Returns the (possibly updated) state.
1073
+ */
1074
+ ack(policyId: string, approve: boolean): Promise<PolicyRunState | undefined>;
1075
+ list(): PolicyRunState[];
1076
+ /**
1077
+ * Sync flush of the policy snapshot to disk. Call from the daemon
1078
+ * stop() path — mirrors sessions.shutdown().
1079
+ */
1080
+ shutdown(): void;
1081
+ }
1082
+
1083
+ /**
1084
+ * Fire-and-forget webhook notifier for session lifecycle events.
1085
+ *
1086
+ * Each session can register its own `notifyUrl` (via `start_agent_session`).
1087
+ * A global URL can be set via `AGENTPROTO_NOTIFY_URL` env var or
1088
+ * `~/.agentproto/notify.json` (env wins). Both are POSTed when an event
1089
+ * fires — the union of per-session + global URLs, deduplicated.
1090
+ *
1091
+ * Retry policy: one retry after 2 s on network error. No retry on 4xx/5xx.
1092
+ * Timeout: 10 s per attempt. All errors are swallowed — the notifier never
1093
+ * throws into the session's hot path.
1094
+ */
1095
+
1096
+ interface WebhookNotifier {
1097
+ /** Register a per-session URL (called from start_agent_session). */
1098
+ register(sessionId: string, url: string): void;
1099
+ unregister(sessionId: string): void;
1100
+ /** Handler to wire into SessionEventBus.onAny. Fire-and-forget. */
1101
+ onSessionEvent(ev: SessionEvent): void;
1102
+ }
1103
+
1104
+ /**
1105
+ * Orchestrator sub-gateway (ADR §4.2 / WP2) — the security primitive
1106
+ * that lets a spawned agent become a *scoped* orchestrator without
1107
+ * handing it the daemon's full toolset.
1108
+ *
1109
+ * The plain `/mcp` endpoint registers EVERY tool (execute_command,
1110
+ * fs_*, remote_*, import_mcp, terminals, driver CRUD) and bypasses auth
1111
+ * on loopback — pointing a child at it would be a privilege handout.
1112
+ * This module builds a second, scoped MCP server that registers ONLY a
1113
+ * curated orchestration allowlist, gated by an unguessable per-child
1114
+ * scope-token. The HTTP layer mounts it at `/mcp/orchestrator`
1115
+ * (`http-server.ts`); WP3 will auto-mint a token and inject the URL at
1116
+ * spawn time. WP2 only builds the primitive.
1117
+ *
1118
+ * Three pieces:
1119
+ * - DEFAULT_ORCHESTRATOR_TOOLS — the curated allowlist.
1120
+ * - createScopeTokenRegistry() — mint / verify / revoke scope-tokens.
1121
+ * - createOrchestratorMcpServerFactory() — build a scoped server for
1122
+ * a verified scope.
1123
+ */
1124
+
1125
+ /**
1126
+ * The curated set of tools a scoped child orchestrator may call.
1127
+ *
1128
+ * INCLUDED — the orchestration primitives:
1129
+ * start_agent_session, prompt_agent_session, get_agent_session_output,
1130
+ * wait_for_any, poll_events (spawn + drive + fan-in)
1131
+ * list_sessions, list_agent_sessions, kill_agent_session (observe +
1132
+ * halt). NB: list/kill are NOT yet scoped to the child's own
1133
+ * subtree — that subtree filter is WP4 (parentSessionId/depth). For
1134
+ * WP2 they are exposed daemon-wide; documented debt.
1135
+ *
1136
+ * EXCLUDED — the danger surface (each a separate trust boundary):
1137
+ * execute_command, terminal/PTY tools (raw shell), fs tools
1138
+ * (read/write/delete file, create_directory, …), remote_* (publish
1139
+ * the daemon to the internet), import_mcp / mcp_imported_* /
1140
+ * list_discovered_mcps / remove_imported_mcp (widen the daemon's own
1141
+ * MCP surface), and driver CRUD (create_/delete_/update_driver — these
1142
+ * never register here because the scoped server is built with no
1143
+ * doctype specs).
1144
+ */
1145
+ declare const DEFAULT_ORCHESTRATOR_TOOLS: readonly string[];
1146
+ interface OrchestratorScope {
1147
+ /** The opaque scope-token handed to (and presented by) the child. */
1148
+ token: string;
1149
+ /** The effective tool allowlist for this scope (⊆ the default and,
1150
+ * for a recursively-spawned child, ⊆ its parent's allowlist). */
1151
+ tools: ReadonlySet<string>;
1152
+ /**
1153
+ * Session id of the orchestrator that OWNS this token — i.e. the
1154
+ * child session that presents it back on `/mcp/orchestrator`. The
1155
+ * token is minted *before* the child session exists (the injector
1156
+ * needs the URL to compose the spawn), so this is bound late, after
1157
+ * `spawnAgent` returns the id, via `bindOwner`. Until bound, spawns
1158
+ * through this scope are attributed to no parent and its subtree is
1159
+ * empty (safe degradation). (ADR §4.5 / WP4) */
1160
+ ownerSessionId?: string;
1161
+ /** Recursion depth of the owning orchestrator session. A spawn made
1162
+ * *through* this scope produces a child at `depth + 1`. Root scopes
1163
+ * (minted for a direct `/mcp` spawn) are depth 0. */
1164
+ depth: number;
1165
+ /** Max recursion depth reachable through this scope. A spawn whose
1166
+ * child depth (`depth + 1`) would exceed this is rejected. Clamped
1167
+ * to `HARD_MAX_DEPTH`. */
1168
+ maxDepth: number;
1169
+ /** Max concurrently-alive children the owning orchestrator may spawn
1170
+ * before further spawns are rejected. */
1171
+ maxChildren: number;
1172
+ }
1173
+ /**
1174
+ * Narrow a caller-requested tool list to ⊆ a ceiling subset.
1175
+ *
1176
+ * The `ceiling` is the maximum a caller may end up with — the global
1177
+ * `DEFAULT_ORCHESTRATOR_TOOLS` for a root scope, or the *parent's*
1178
+ * effective tools for a recursively-spawned child (non-re-grant: a
1179
+ * child can never widen beyond what its parent holds — ADR §4.5 #5).
1180
+ * The `{ tools: [...] }` form can only REMOVE from the ceiling, never
1181
+ * add: any name outside it is dropped. Omitting `requested` yields the
1182
+ * full ceiling. Omitting `ceiling` defaults to the global default.
1183
+ */
1184
+ declare function narrowOrchestratorTools(requested?: readonly string[], ceiling?: ReadonlySet<string>): Set<string>;
1185
+ interface MintScopeOptions {
1186
+ /** Caller-requested allowlist — narrowed ⊆ `ceiling` (or the default
1187
+ * when no ceiling is given). */
1188
+ tools?: readonly string[];
1189
+ /** Upper bound on the minted tools (the parent's effective tools for
1190
+ * a recursive child). Omit for a root scope (defaults to the global
1191
+ * default subset). */
1192
+ ceiling?: ReadonlySet<string>;
1193
+ /** Depth of the owning orchestrator (default 0). */
1194
+ depth?: number;
1195
+ /** Max depth reachable through this scope (default DEFAULT_MAX_DEPTH,
1196
+ * clamped to HARD_MAX_DEPTH). */
1197
+ maxDepth?: number;
1198
+ /** Per-orchestrator alive-child quota (default DEFAULT_MAX_CHILDREN). */
1199
+ maxChildren?: number;
1200
+ }
1201
+ interface ScopeTokenRegistry {
1202
+ /** Mint a fresh scope-token. See `MintScopeOptions`. */
1203
+ mint(opts?: MintScopeOptions): OrchestratorScope;
1204
+ /** Resolve a presented token to its scope, or null when unknown. */
1205
+ verify(token: string | null | undefined): OrchestratorScope | null;
1206
+ /** Late-bind the owning child session id onto a minted scope (the
1207
+ * token is minted before the child exists). No-op when the token is
1208
+ * unknown. Mutates the live scope object so `verify` reflects it. */
1209
+ bindOwner(token: string, sessionId: string): void;
1210
+ /** Drop a token (e.g. when the child session ends). */
1211
+ revoke(token: string): void;
1212
+ }
1213
+ /**
1214
+ * In-memory registry of live scope-tokens. Tokens are random 256-bit
1215
+ * hex (unguessable, never persisted) — see ADR §5.3. Single-user-host
1216
+ * trust assumption mirrors the loopback auth bypass on `/mcp`.
1217
+ */
1218
+ declare function createScopeTokenRegistry(): ScopeTokenRegistry;
1219
+ interface OrchestratorGatewayDeps {
1220
+ workspace: string;
1221
+ name?: string;
1222
+ version?: string;
1223
+ registry: SessionsRegistry;
1224
+ sessionEvents: SessionEventBus;
1225
+ eventRing: EventRing;
1226
+ supervisor?: CompletionPolicySupervisor;
1227
+ resolveAgentAdapter?: AgentAdapterResolver;
1228
+ listAgentAdapters?: AgentAdapterLister;
1229
+ /** Orchestrator injector (WP3/WP4). When wired, a child orchestrator
1230
+ * driving the scoped server can itself spawn sub-orchestrators
1231
+ * (`orchestrator: true` on its `start_agent_session`) — the new
1232
+ * token inherits depth+1 and is bounded by the caller's tools
1233
+ * (non-re-grant). Omitted → recursion injection is unavailable on
1234
+ * the scoped surface (a child can still spawn plain sub-agents). */
1235
+ orchestratorInjector?: OrchestratorInjector;
1236
+ /** Optional webhook notifier — same singleton as the root /mcp server.
1237
+ * When provided, per-session notifyUrl values from start_agent_session
1238
+ * are registered on spawn and unregistered on exit, so child
1239
+ * orchestrators spawning through this scoped gateway also fire webhooks. */
1240
+ webhookNotifier?: WebhookNotifier;
1241
+ }
1242
+ type OrchestratorMcpServerFactory = (scope: OrchestratorScope) => Promise<McpServer>;
1243
+ /**
1244
+ * Build a factory that produces a scoped MCP server for a verified
1245
+ * scope. The server is created with NO doctype specs and NO workspace
1246
+ * (so no CRUD verbs, no `self_inspect`, no driver tools leak in), then
1247
+ * the orchestration passes run against a `withToolSubset` wrapper so
1248
+ * only `scope.tools` survive. Critically, `mcpProxy` and the PTY
1249
+ * factory are never wired here — import + terminal tools stay unwired
1250
+ * AND are excluded from the subset (defense in depth).
1251
+ */
1252
+ declare function createOrchestratorMcpServerFactory(deps: OrchestratorGatewayDeps): OrchestratorMcpServerFactory;
1253
+ /**
1254
+ * The auto-injection assembly (ADR §4.4 / WP3) — turns the "make this
1255
+ * child an orchestrator" intent into the concrete `mcpServers` entry
1256
+ * the ACP `session/new` call mounts, plus the token lifecycle.
1257
+ *
1258
+ * One call (`inject`) does three things:
1259
+ * 1. mint a scope-token (narrowed ⊆ default when `tools` is given);
1260
+ * 2. build the `mcpServers` entry pointing the child at the daemon's
1261
+ * own scoped sub-gateway (`/mcp/orchestrator?scope=<token>`);
1262
+ * 3. hand back `bindLifecycle(sessionId)` so the caller can revoke
1263
+ * the token the moment that child session exits — no token leaks
1264
+ * past the life of the session it was minted for.
1265
+ *
1266
+ * Assembled in `createGateway` (not the CLI resolver) because it needs
1267
+ * the gateway's scope-token registry, the session-event bus, and the
1268
+ * HTTP listener's port — all of which live there.
1269
+ */
1270
+ interface OrchestratorInjection {
1271
+ /** The `mcpServers` entry to merge into the child's `session/new`. */
1272
+ entry: AcpMcpServer;
1273
+ /** The minted scope — `token` gates the endpoint, `tools` is the
1274
+ * effective (narrowed) allowlist. */
1275
+ scope: OrchestratorScope;
1276
+ /**
1277
+ * Revoke the scope-token once `sessionId` exits. Subscribes to the
1278
+ * session-event bus for that one session's `session:exited` and
1279
+ * self-unsubscribes on fire. Returns an unsubscribe in case the
1280
+ * caller wants to drop the binding early (rarely needed).
1281
+ */
1282
+ bindLifecycle(sessionId: string): () => void;
1283
+ }
1284
+ interface OrchestratorInjectorDeps {
1285
+ scopeTokens: ScopeTokenRegistry;
1286
+ sessionEvents: SessionEventBus;
1287
+ /** Port the daemon's HTTP listener (and thus `/mcp/orchestrator`)
1288
+ * binds. The child reaches it over loopback. */
1289
+ port: number;
1290
+ /** Loopback host for the injected `ref`. Always loopback — the child
1291
+ * is co-located on the host. Default `127.0.0.1`. */
1292
+ host?: string;
1293
+ /** `name` advertised on the injected `mcpServers` entry. The child
1294
+ * sees its orchestration tools namespaced under this. Default
1295
+ * `agentproto`. */
1296
+ entryName?: string;
1297
+ }
1298
+ type OrchestratorInjector = (opts?: {
1299
+ tools?: readonly string[];
1300
+ /** The calling orchestrator's scope, when this spawn arrives via the
1301
+ * scoped sub-gateway (recursive spawn). The minted child token then
1302
+ * inherits `depth = caller.depth + 1`, its tools are bounded by the
1303
+ * caller's tools (non-re-grant — ADR §4.5 #5), and its depth/child
1304
+ * limits never exceed the caller's. Omit for a root spawn (direct
1305
+ * `/mcp`) → depth 0, full default subset, default limits. */
1306
+ caller?: OrchestratorScope;
1307
+ /** Override the max depth reachable through the minted scope (clamped
1308
+ * to the caller's, then to HARD_MAX_DEPTH). Root-only knob. */
1309
+ maxDepth?: number;
1310
+ /** Override the per-orchestrator child quota for the minted scope
1311
+ * (clamped to the caller's). Root-only knob. */
1312
+ maxChildren?: number;
1313
+ }) => OrchestratorInjection;
1314
+ /**
1315
+ * Build the orchestrator injector. Closed over the gateway's
1316
+ * scope-token registry + session-event bus + HTTP port. Returns a
1317
+ * function that the `start_agent_session` handler calls when its
1318
+ * `orchestrator` field is set.
1319
+ */
1320
+ declare function createOrchestratorInjector(deps: OrchestratorInjectorDeps): OrchestratorInjector;
1321
+
289
1322
  /**
290
1323
  * Tiny node:http server that fronts the runtime gateway.
291
1324
  *
@@ -328,13 +1361,31 @@ type AgentAdapterResolver = (slug: string) => Promise<{
328
1361
  startSession(opts: {
329
1362
  cwd: string;
330
1363
  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. */
1364
+ /** Model identifier forwarded from `start_agent_session`. For ACP
1365
+ * adapters this is applied via session/set_config_option after
1366
+ * newSession (the ACP wrapper does not forward CLI args to claude).
1367
+ * Adapters that don't support model selection ignore it. */
334
1368
  model?: string;
1369
+ /** Effort level forwarded from `start_agent_session`. Effort is
1370
+ * model-dependent — same label ≠ same budget across models; defaults
1371
+ * differ by model. Omit to keep the model's own default. Applied
1372
+ * via session/set_config_option on ACP adapters; others ignore it. */
1373
+ effort?: string;
1374
+ /** MCP servers to mount into the spawned agent's session at spawn
1375
+ * time. Forwarded verbatim to the driver's `start({ mcpServers })`
1376
+ * → the ACP arm's `session/new.mcpServers`, giving the child agent
1377
+ * a host-chosen scoped toolset (e.g. the daemon's own orchestration
1378
+ * gateway). Adapters that don't model MCP mounting ignore it. */
1379
+ mcpServers?: AcpMcpServer[];
335
1380
  }): Promise<AgentSessionLike>;
336
1381
  /** Display label for the descriptor's `command` field. */
337
1382
  commandPreview?: string;
1383
+ /** Best-effort per-session usage reader (adapter-specific, e.g. hermes state.db). */
1384
+ readUsage?: (adapterSessionId: string) => Promise<{
1385
+ costUsd?: number;
1386
+ tokensIn?: number;
1387
+ tokensOut?: number;
1388
+ } | null>;
338
1389
  } | null>;
339
1390
  /**
340
1391
  * Compact adapter metadata for the discovery endpoints. Independent
@@ -357,6 +1408,128 @@ interface AuthOptions {
357
1408
  token?: string;
358
1409
  }
359
1410
 
1411
+ /**
1412
+ * InboundWatcher — polls agentpush `poll_inbound` on a timer and
1413
+ * spawns one agent per contact_ref that has unread inbound messages.
1414
+ *
1415
+ * Flow per tick:
1416
+ * mcp_imported_call(alias, "poll_inbound", {source, since:cursor})
1417
+ * → group events by contact_ref
1418
+ * → spawn one agent per contact with prompt from promptTemplate
1419
+ * → advance + persist cursor to ~/.agentproto/agentpush-cursors.json
1420
+ *
1421
+ * Cursor key = "${alias}:${source}" — stable across daemon restarts so
1422
+ * cursor resume actually works (a random watcherId would break it).
1423
+ *
1424
+ * Prompt template variables: {{source}}, {{contact_ref}},
1425
+ * {{messages_json}}, {{count}}.
1426
+ *
1427
+ * The mcpServersForChild array is forwarded to the spawned agent's
1428
+ * startSession + spawnAgent so the child can call dispatch_request
1429
+ * directly on the mounted agentpush MCP server.
1430
+ *
1431
+ * State is in-memory; cursors persist to disk (debounced async write
1432
+ * + sync flush at shutdown), matching the supervisor.ts pattern.
1433
+ */
1434
+
1435
+ interface WatcherStartInput {
1436
+ /** Imported MCP alias (e.g. "agentpush"). */
1437
+ alias: string;
1438
+ /** Contact ref / channel to watch — forwarded as `source` to poll_inbound. */
1439
+ source: string;
1440
+ /** Agent adapter slug (e.g. "claude-code"). */
1441
+ adapter: string;
1442
+ /**
1443
+ * Prompt template with placeholders:
1444
+ * {{source}} — the watched source
1445
+ * {{contact_ref}} — the sender's contact_ref
1446
+ * {{messages_json}} — JSON array of the new events
1447
+ * {{count}} — number of new messages
1448
+ */
1449
+ promptTemplate: string;
1450
+ /** Poll interval in ms. Default 5 000. */
1451
+ pollIntervalMs?: number;
1452
+ /** Working directory for spawned agents. Default process.cwd(). */
1453
+ cwd?: string;
1454
+ /** Optional label suffix on spawned sessions. */
1455
+ label?: string;
1456
+ /**
1457
+ * MCP servers to mount in the child agent at spawn time.
1458
+ * Pass agentpush here so the child can call dispatch_request.
1459
+ */
1460
+ mcpServersForChild?: Array<{
1461
+ name: string;
1462
+ transport: "stdio" | "http" | "sse";
1463
+ ref?: string;
1464
+ }>;
1465
+ }
1466
+ interface WatcherDescriptor {
1467
+ watcherId: string;
1468
+ alias: string;
1469
+ source: string;
1470
+ adapter: string;
1471
+ pollIntervalMs: number;
1472
+ status: "running" | "stopped";
1473
+ /** Next `since` value for poll_inbound (exclusive lower bound). */
1474
+ cursor: number;
1475
+ lastPollAt?: string;
1476
+ lastFireAt?: string;
1477
+ /** Total agent sessions spawned since the watcher started. */
1478
+ spawned: number;
1479
+ }
1480
+ interface InboundWatcher {
1481
+ start(input: WatcherStartInput): WatcherDescriptor;
1482
+ /** Returns false when watcherId is unknown. */
1483
+ stop(watcherId: string): boolean;
1484
+ list(): WatcherDescriptor[];
1485
+ /** Stops all watchers and flushes cursor state to disk synchronously. */
1486
+ shutdown(): void;
1487
+ }
1488
+
1489
+ /**
1490
+ * Browser family on top of `@agentproto/adapter-kit` — Phase 3 (lightest
1491
+ * adoption: no creds, no wizard, no catalog file).
1492
+ *
1493
+ * Provides the kit-compatible types (`BrowserAdapterHandle`) and the
1494
+ * `makeBrowserAdapterLister` factory that wraps the existing injected
1495
+ * `listBrowserAdapters` / `resolveBrowserAdapter` functions in a standard
1496
+ * `AdapterLister<BrowserAdapterInfo>`.
1497
+ *
1498
+ * The resulting lister is passed as the `lister` option to
1499
+ * `registerBrowserTools`, which uses it to produce the same
1500
+ * `{ id, name, description, defaultPort }[]` JSON the legacy path emits
1501
+ * (output shape is unchanged — only the internal flow changes).
1502
+ *
1503
+ * Kit invariants honoured:
1504
+ * - `check()` is NEVER called by the lister (OQ-5).
1505
+ * - `BrowserAdapterInfo` carries no cred values (Appendix B).
1506
+ * - `requiresSetup = false` → status is always "ready" without a ledger.
1507
+ */
1508
+
1509
+ /**
1510
+ * Kit-compatible handle for a single in-process browser adapter.
1511
+ * `requiresSetup` is always `false` — browser adapters need no creds or
1512
+ * setup wizard, so the kit classifies them as "ready" on first resolution.
1513
+ */
1514
+ interface BrowserAdapterHandle extends AdapterHandle {
1515
+ readonly requiresSetup: false;
1516
+ readonly defaultPort: number;
1517
+ }
1518
+ /**
1519
+ * Build a kit-compatible `AdapterLister<BrowserAdapterInfo>` from the
1520
+ * existing injected browser adapter functions. The returned lister uses an
1521
+ * empty catalog and `discoverExtras` to surface the injected adapters, then
1522
+ * maps each resolved handle's fields into a `BrowserAdapterInfo` descriptor.
1523
+ *
1524
+ * The caller should pass the result as the `lister` option to
1525
+ * `registerBrowserTools`, which extracts `AdapterEntry.info` to preserve the
1526
+ * existing tool response shape.
1527
+ */
1528
+ declare function makeBrowserAdapterLister(opts: {
1529
+ listBrowserAdapters: BrowserAdapterLister;
1530
+ resolveBrowserAdapter?: BrowserAdapterResolver;
1531
+ }): AdapterLister<BrowserAdapterInfo>;
1532
+
360
1533
  /**
361
1534
  * `.agentproto/` config dir — runtime-managed state at the root of
362
1535
  * every workspace. Mirrors the `.git/` model: user-content stays at
@@ -369,6 +1542,15 @@ interface AuthOptions {
369
1542
  * for `cat .agentproto/runtime.json` diagnostics + future tooling
370
1543
  * that wants to discover a running gateway from disk.
371
1544
  *
1545
+ * The same snapshot is ALSO mirrored to the central, workspace-
1546
+ * independent daemon registry at `~/.agentproto/daemons/<port>.json`
1547
+ * (see daemonRegistryDir / writeDaemonRegistryEntry below). The
1548
+ * per-workspace file only helps `cat`-style local diagnostics and
1549
+ * discovery for REGISTERED workspaces; a daemon launched from an
1550
+ * arbitrary cwd (tunnel mode, a repo checkout) writes its workspace
1551
+ * file somewhere discovery never walks. The central entry is what
1552
+ * makes `agentproto chat` / `sessions` find such a daemon.
1553
+ *
372
1554
  * What does NOT go here:
373
1555
  * - HEARTBEAT.md — user-edited content
374
1556
  * - conversations/ — user-readable chat history
@@ -406,13 +1588,44 @@ interface RuntimeMeta {
406
1588
  */
407
1589
  token: string;
408
1590
  }
1591
+ /** `~/.agentproto/daemons/` — the central, workspace-independent daemon
1592
+ * registry. Each live daemon owns one `<port>.json` here. */
1593
+ declare function daemonRegistryDir(): string;
1594
+ /**
1595
+ * Write `<registry>/<port>.json` (mode 0600 — holds the bearer token).
1596
+ * Best-effort; failure here never gates the gateway. Mirrors the
1597
+ * per-workspace runtime.json so the same `RuntimeMeta` shape is readable
1598
+ * from either location.
1599
+ */
1600
+ declare function writeDaemonRegistryEntry(meta: RuntimeMeta): Promise<void>;
1601
+ /** Remove a daemon's central registry entry. Best-effort; missing is a
1602
+ * no-op. Called from graceful shutdown alongside unlinkRuntimeMeta. */
1603
+ declare function unlinkDaemonRegistryEntry(port: number): Promise<void>;
1604
+ /**
1605
+ * Read every entry in the central daemon registry, newest-first by
1606
+ * `startedAt`. No liveness filtering here — callers decide (discovery
1607
+ * skips dead PIDs; the sweep deletes them). Malformed / unreadable
1608
+ * entries are skipped silently.
1609
+ */
1610
+ declare function readDaemonRegistry(): Promise<Array<{
1611
+ meta: Partial<RuntimeMeta> & Record<string, unknown>;
1612
+ path: string;
1613
+ mtime: Date;
1614
+ }>>;
1615
+ /**
1616
+ * Delete central registry entries whose PID is dead (kill -9, crash,
1617
+ * reboot). Returns the paths cleaned. Called at `serve` boot — the same
1618
+ * footgun the per-workspace sweep guards against, applied to the central
1619
+ * registry. Skips `currentPort` (the booting daemon's own entry).
1620
+ */
1621
+ declare function sweepStaleDaemonRegistry(currentPort: number): Promise<string[]>;
409
1622
  /**
410
1623
  * Delete `<workspace>/.agentproto/runtime.json`. Best-effort — called
411
1624
  * from the daemon's graceful shutdown path so a CLI looking for the
412
1625
  * live daemon doesn't pick up a stale token after this process exits.
413
1626
  * Missing file is a no-op (the daemon may never have written one).
414
1627
  */
415
- declare function unlinkRuntimeMeta(workspace: string): Promise<void>;
1628
+ declare function unlinkRuntimeMeta(workspace: string, port?: number): Promise<void>;
416
1629
  /**
417
1630
  * Read a runtime.json from `<workspace>/.agentproto/`. Returns the
418
1631
  * parsed object (no validation) plus the on-disk mtime, or null when
@@ -493,6 +1706,12 @@ interface CreateGatewayOptions {
493
1706
  * `GET /adapters` HTTP route + `list_adapters` MCP tool so UIs
494
1707
  * can discover what's installed on the host. */
495
1708
  listAgentAdapters?: AgentAdapterLister;
1709
+ /** Optional browser adapter resolver — when provided, enables the
1710
+ * `start_browser` MCP tool (launches Camofox / Bureau / Chromium). */
1711
+ resolveBrowserAdapter?: BrowserAdapterResolver;
1712
+ /** Optional browser adapter lister — when provided, enables the
1713
+ * `list_adapter_browsers` MCP tool. */
1714
+ listBrowserAdapters?: BrowserAdapterLister;
496
1715
  /** Optional PTY factory (node-pty wrapper, typically from the cli
497
1716
  * layer's `loadNodePtyFactory()`). When provided, enables
498
1717
  * `POST /sessions/terminal`, the `start_terminal_session` MCP
@@ -522,11 +1741,23 @@ interface GatewayHandle {
522
1741
  * inside the same process can register their child processes for
523
1742
  * visibility through /sessions and the CLI TUI. */
524
1743
  sessions: SessionsRegistry;
1744
+ /** Tunnel registry — manages public cloudflared tunnels for any
1745
+ * local port. Exposed so embedding hosts can open tunnels
1746
+ * programmatically without going through the HTTP/MCP surface. */
1747
+ tunnels: TunnelRegistry;
525
1748
  /** Per-boot bearer token required on mutating /sessions/* routes
526
1749
  * + WS PTY upgrades. Exposed so an embedding host (e.g. the CLI
527
1750
  * shell that hosts the gateway in-process) can pass it to child
528
1751
  * tools without re-reading the runtime.json file. */
529
1752
  token: string;
1753
+ /** Mint a scope-token for the scoped orchestrator sub-gateway
1754
+ * (`/mcp/orchestrator`). The returned `token` gates that endpoint
1755
+ * and `tools` is the effective allowlist (⊆ the default orchestrator
1756
+ * subset). WP3 will call this at spawn time and inject the URL into
1757
+ * the child's `mcpServers`; for WP2 it's the internal primitive. */
1758
+ mintOrchestratorScope(opts?: {
1759
+ tools?: readonly string[];
1760
+ }): OrchestratorScope;
530
1761
  stop(): Promise<void>;
531
1762
  }
532
1763
  /**
@@ -543,4 +1774,4 @@ interface GatewayHandle {
543
1774
  */
544
1775
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
545
1776
 
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 };
1777
+ export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CreateGatewayOptions, DEFAULT_ORCHESTRATOR_TOOLS, type GatewayHandle, type InboundWatcher, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type RegisterBrowserInput, type RegisterSessionInput, type RuntimeMeta, type ScopeTokenRegistry, type SessionDescriptor, type SessionKind, type SessionStatus, type SessionsRegistry, type SpawnAgentInput, type SpawnSessionInput, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createScopeTokenRegistry, daemonRegistryDir, makeBrowserAdapterLister, narrowOrchestratorTools, readDaemonRegistry, readRuntimeMeta, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };