@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 +1237 -6
- package/dist/index.mjs +8038 -2211
- package/dist/index.mjs.map +1 -1
- package/dist/providers-store.d.ts +57 -0
- package/dist/providers-store.mjs +86 -0
- package/dist/providers-store.mjs.map +1 -0
- package/package.json +11 -4
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`.
|
|
332
|
-
*
|
|
333
|
-
*
|
|
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 };
|