@agentproto/runtime 0.5.0 → 0.6.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,18 +1,22 @@
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
+ import { AcpPermissionResolution, AcpMcpServer } from '@agentproto/acp';
5
5
  import { ChildProcess } from 'node:child_process';
6
- import { D as DeclaredAdapterOption } from './spawn-defaults-d5gAhNkV.js';
6
+ import { R as ResolvedAuthSpec, D as DeclaredAdapterOption, A as AdapterAuthDescriptor } from './spawn-defaults-DAbADRd4.js';
7
+ export { a as AuthEcho, b as AuthResolutionError, C as CredentialSource, c as DefaultsAdapterAuthConfig, d as DefaultsAdapterConfig, e as ResolvedSpawnAuthMaterial, f as ResolvedSpawnDefaults, S as SpawnDefaultsConfig, g as credentialFingerprint, n as normalizeSkillsOption, r as resolveAuthSpec, h as resolveSpawnDefaults } from './spawn-defaults-DAbADRd4.js';
7
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
9
  import { AdapterHandle, SetupField, AdapterResolver, AdapterLister, AdapterEntry } from '@agentproto/provider-kit';
9
10
  import { SandboxProvider } from '@agentproto/sandbox';
11
+ import { FrameSink, E2eFrameSink } from '@agentproto/acp/tunnel';
12
+ import { DaemonIdentity } from '@agentproto/secrets/identity';
10
13
  import { WorkspaceFs } from './workspace-fs.js';
11
14
  export { createWorkspaceFs } from './workspace-fs.js';
12
15
  export { ConversationMeta, ConversationStore, ConversationTurn, fileConversationStore } from './conversations.js';
13
16
  import { ProviderPreset } from '@agentproto/provider-presets';
14
17
  import { StepCache } from '@agentproto/workflow-runtime';
15
- export { PROVIDER_ENV_VARS, ProviderEntry, ProvidersFile, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey } from './providers-store.js';
18
+ export { PROVIDER_ENV_VARS, ProviderEntry, ProvidersFile, getProviderKey, injectProviderKeysIntoEnv, loadProviders, providerEnvVar, providersPath, removeProviderKey, setProviderKey } from '@agentproto/providers-store';
19
+ import '@agentproto/model-catalog';
16
20
 
17
21
  /**
18
22
  * Per-session JSONL record of a completed `command_execute` invocation
@@ -137,7 +141,7 @@ declare function projectSessionUsage(desc: UsageDescriptorFields): SessionUsage;
137
141
  * (fire-and-forget HTTP), RoutineRunner (state machine fan-in),
138
142
  * and session_monitor MCP tool (long-poll multiplexed).
139
143
  */
140
- type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:exited" | "session:command-done" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
144
+ type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:permission-request" | "session:permission-resolved" | "session:exited" | "session:command-done" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
141
145
  /**
142
146
  * Structured detail on why a session is awaiting input, when derivable.
143
147
  * `source: "structured"` — a driver-reported ACP-style prompt (e.g. a tool
@@ -168,6 +172,16 @@ interface SessionTurnEndEvent {
168
172
  * Absent for drivers that don't report a reason.
169
173
  */
170
174
  reason?: string;
175
+ /**
176
+ * True when the turn completed normally but produced ZERO assistant
177
+ * output AND zero tool calls (and wasn't awaiting input) — a silent
178
+ * no-op. Grok via hermes/OpenRouter, and any invalid/rejected model id,
179
+ * complete a turn this way: `turnsCompleted` bumps, `$0` cost, empty
180
+ * transcript. Surfaced so an orchestrator can flag it instead of
181
+ * treating a green turn-end as real progress. Absent (not `false`) on
182
+ * a normal, productive turn.
183
+ */
184
+ empty?: boolean;
171
185
  }
172
186
  interface SessionAwaitingInputEvent {
173
187
  type: "session:awaiting-input";
@@ -176,6 +190,34 @@ interface SessionAwaitingInputEvent {
176
190
  ts: string;
177
191
  question?: SessionAwaitingQuestion;
178
192
  }
193
+ /**
194
+ * Emitted when a permission-hold session parks a `session/request_permission`
195
+ * (see the pending-permissions inbox in sessions.ts). `permissionId` is the
196
+ * stable id `permissions_respond` / `POST /permissions/:id` resolve it with.
197
+ */
198
+ interface SessionPermissionRequestEvent {
199
+ type: "session:permission-request";
200
+ sessionId: string;
201
+ permissionId: string;
202
+ toolName?: string;
203
+ text: string;
204
+ label?: string;
205
+ ts: string;
206
+ }
207
+ /**
208
+ * Emitted when a parked permission is resolved — by an inbox
209
+ * approve/deny, or auto-`cancelled` when the session is torn down while the
210
+ * request is still pending.
211
+ */
212
+ interface SessionPermissionResolvedEvent {
213
+ type: "session:permission-resolved";
214
+ sessionId: string;
215
+ permissionId: string;
216
+ decision: "approve" | "deny" | "cancelled";
217
+ optionId?: string;
218
+ label?: string;
219
+ ts: string;
220
+ }
179
221
  interface SessionExitedEvent {
180
222
  type: "session:exited";
181
223
  sessionId: string;
@@ -183,6 +225,12 @@ interface SessionExitedEvent {
183
225
  status: "exited" | "killed" | "error";
184
226
  label?: string;
185
227
  ts: string;
228
+ /** Mirrors `SessionDescriptor.endedReason` — set when this exit was the
229
+ * daemon dying underneath the session (crash-discovered-at-boot or a
230
+ * forced shutdown kill), not an operator targeting it. Lets a watcher
231
+ * (completion-policy supervisor, `session_monitor`) tell "my agent died
232
+ * because the daemon did" apart from a deliberate kill. Absent otherwise. */
233
+ reason?: "daemon-restart";
186
234
  }
187
235
  /** Emitted when command_execute finishes. commandId matches the id
188
236
  * returned by the command_execute MCP tool. */
@@ -257,7 +305,7 @@ interface CronFailedEvent {
257
305
  error: string;
258
306
  ts: string;
259
307
  }
260
- type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
308
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
261
309
  interface SessionEventBus {
262
310
  emit(ev: SessionEvent): void;
263
311
  /** Subscribe to a specific event type. Returns an unsubscribe fn. */
@@ -320,6 +368,13 @@ interface AgentSessionLike {
320
368
  pid?: number;
321
369
  send(message: unknown): AsyncIterable<AgentStreamEvent>;
322
370
  cancel(): Promise<void>;
371
+ /** Resolve a permission request the driver parked in permission-hold mode
372
+ * (see the pending-permissions inbox below). `requestId` is the driver's
373
+ * own stable id, surfaced on the `agent-prompt` event's `toolCallId`.
374
+ * Returns true when a matching parked request was resolved. Absent for
375
+ * drivers that don't model held permissions (sandbox proxy, future
376
+ * transports). */
377
+ respondPermission?(requestId: string, resolution: AcpPermissionResolution): boolean | Promise<boolean>;
323
378
  close(): Promise<void>;
324
379
  }
325
380
  /**
@@ -358,6 +413,11 @@ interface AgentStreamEvent {
358
413
  toolName?: string;
359
414
  /** Tool-call input, e.g. an ACP `tool_call`'s `arguments` — see @agentproto/acp's `StreamEvent`. */
360
415
  arguments?: unknown;
416
+ /** True when a "tool-call" event ENRICHES one already announced under the
417
+ * same `toolCallId` (the agent knew the input only after announcing the
418
+ * call) rather than announcing a new one — see @agentproto/acp's
419
+ * `StreamEvent`. Consumers merge by toolCallId; it is not a second call. */
420
+ isUpdate?: boolean;
361
421
  /** Tool-call output, e.g. an ACP `tool_call_update`'s `result` — see @agentproto/acp's `StreamEvent`. */
362
422
  result?: unknown;
363
423
  isError?: boolean;
@@ -399,6 +459,20 @@ interface AgentStreamEvent {
399
459
  }
400
460
  type SessionKind = "terminal" | "agent-cli" | "command" | "browser";
401
461
  type SessionStatus = "starting" | "running" | "exited" | "killed" | "error";
462
+ /**
463
+ * The billing-auth resolver's OBSERVABLE echo, recorded on a session
464
+ * descriptor. `mode` + `fingerprint` are always present when recorded (a
465
+ * credential resolved); `provider` / `credentialSource` / `setEnv` are the
466
+ * multi-provider resolver's additions (DECISION 9③/10②) — optional for
467
+ * back-compat with descriptors from before that resolver shipped.
468
+ */
469
+ interface SessionAuthEcho {
470
+ mode: "subscription" | "api-key";
471
+ fingerprint: string;
472
+ provider?: string;
473
+ credentialSource?: "explicit-config" | "providers-store" | "none";
474
+ setEnv?: string;
475
+ }
402
476
  interface SessionDescriptor {
403
477
  id: string;
404
478
  kind: SessionKind;
@@ -411,6 +485,16 @@ interface SessionDescriptor {
411
485
  startedAt: string;
412
486
  endedAt?: string;
413
487
  exitCode?: number;
488
+ /** Set alongside `status: "killed"` when the session ended NOT because an
489
+ * operator targeted it (`kill()`) but because the daemon process it lived
490
+ * in went away out from under it — a hard crash discovered at next boot
491
+ * (`loadHistorySnapshot`'s wasAlive reclassification), or a graceful
492
+ * shutdown/restart that force-kills whatever's still busy
493
+ * (`shutdownImpl`). Absent for every other terminal path (operator kill,
494
+ * natural exit, turn error) — the session's own fault, or at least not
495
+ * the daemon's. Lets the UI show "crashed with the daemon" instead of a
496
+ * bare "killed" that reads as deliberate. */
497
+ endedReason?: "daemon-restart";
414
498
  /** Last time anything was written to stdout/stderr. Lets the UI
415
499
  * spot stuck sessions ("running for 2h, last output 12min ago"). */
416
500
  lastOutputAt?: string;
@@ -451,6 +535,25 @@ interface SessionDescriptor {
451
535
  adapterSlug?: string;
452
536
  /** The model the session was requested to run (echoed back at spawn). */
453
537
  model?: string;
538
+ /**
539
+ * Deterministic billing-auth mode + a non-secret credential fingerprint,
540
+ * recorded at spawn time for adapters that resolved an explicit
541
+ * credential (today: claude-code — see `AgentCliAuth.modes` in
542
+ * `@agentproto/driver-agent-cli`). The "verifiability" answer to "what
543
+ * was used": `mode` is the resolved `"subscription" | "api-key"`;
544
+ * `fingerprint` is `credentialFingerprint(mode, credential)` — e.g.
545
+ * `"subscription · sk-ant-oat…3f9c"` — NEVER the raw credential. Absent
546
+ * when no credential resolved (every adapter besides claude-code, in
547
+ * practice) or for a sandboxed spawn (the box's own daemon resolves its
548
+ * own credential independently). Surfaced in `agentproto sessions
549
+ * --watch`'s DETAIL pane and `agent_sessions_list`.
550
+ *
551
+ * Carries the resolver's OBSERVABLE echo (DECISION 9③/10②): `provider`,
552
+ * `credentialSource`, and the `setEnv` actually set — so a verifier checks
553
+ * the RESOLUTION, not the model's self-report. These are optional for
554
+ * back-compat with descriptors persisted before the multi-provider resolver.
555
+ */
556
+ auth?: SessionAuthEcho;
454
557
  /** Cumulative estimated USD cost of the session — best-effort, refreshed
455
558
  * on each turn-end from the adapter's usage reader (e.g. hermes reads its
456
559
  * state.db). Absent for adapters with no usage source. */
@@ -508,6 +611,14 @@ interface SessionDescriptor {
508
611
  * drivers that don't report structured prompts. Cleared alongside
509
612
  * `awaitingInput` on the next turn start. */
510
613
  awaitingQuestion?: SessionAwaitingQuestion;
614
+ /** True while this session (spawned in permission-hold mode) has at least
615
+ * one permission request parked in the inbox awaiting a human/orchestrator
616
+ * decision. Distinct from `awaitingInput` (a conversational question) — a
617
+ * held permission is resolved via `permissions_respond` / `agentproto
618
+ * permissions approve|deny`, not a normal prompt. Set when a request is
619
+ * registered, cleared when the last pending one for this session resolves.
620
+ * Absent (never `false`) for sessions with no held permissions. */
621
+ awaitingPermission?: boolean;
511
622
  /** Count of turns that have fully completed (turn-end emitted) on this
512
623
  * session. Lets `session_monitor` fast-return for a session that already
513
624
  * finished its turn before the wait subscribed — a fast turn that ends
@@ -524,8 +635,12 @@ interface SessionDescriptor {
524
635
  * from the pending tool call: a spawned sub-agent (`agent_start`) or a
525
636
  * shell/terminal command. Deliberately NO "user" variant — waiting on
526
637
  * the user is already covered by `awaitingInput`/`awaitingQuestion`.
527
- * Set on tool-call, cleared on the MATCHING tool-result (guarded by
528
- * `pendingToolCallId`), at turn start, and in the turn's finally. */
638
+ *
639
+ * Set on tool-call; released (see `releaseBlockedOn`) on the MATCHING
640
+ * tool-result, on `error` (a failing tool never emits a result), on the
641
+ * next assistant `text-delta` (the model has the floor, so nothing is
642
+ * pending), at turn start, and in the turn's finally. A claim about the
643
+ * present tense — anything that disproves it must clear it. */
529
644
  blockedOn?: "subagent" | "command";
530
645
  /** toolCallId of the tool-call that set `blockedOn`. A tool-result only
531
646
  * clears `blockedOn` when its toolCallId matches — so a nested or
@@ -574,6 +689,52 @@ interface SessionDescriptor {
574
689
  * `agent_start.sandbox.reuse`). Only set when `remote` is true. */
575
690
  sandboxTeardown?: "kill" | "pause";
576
691
  }
692
+ /**
693
+ * A `session/request_permission` request parked by a permission-hold session,
694
+ * awaiting a human/orchestrator decision through the cross-session inbox
695
+ * (`permissions_list` / `permissions_respond`, `GET /permissions` /
696
+ * `POST /permissions/:id`, `agentproto permissions ls|approve|deny`).
697
+ */
698
+ interface PendingPermission {
699
+ /** Stable id — the driver's own request id, unique across sessions. Pass to
700
+ * `respondPermission`. */
701
+ id: string;
702
+ sessionId: string;
703
+ /** ACP tool-call id the request was raised for (== `id` today; kept distinct
704
+ * so a future driver can surface a separate correlation id). */
705
+ toolCallId: string;
706
+ /** Tool title/kind the agent is asking permission for, when known. */
707
+ toolName?: string;
708
+ /** Human-readable "Allow X?" line. */
709
+ text: string;
710
+ /** Options the agent offered (ACP `requestPermission` shape). */
711
+ options: Array<{
712
+ optionId: string;
713
+ name?: string;
714
+ kind?: string;
715
+ }>;
716
+ /** ISO timestamp the request was parked. */
717
+ requestedAt: string;
718
+ }
719
+ /** How a caller resolves a pending permission — an explicit `optionId` wins
720
+ * over the `decision`→option mapping (approve → an allow-flavored option,
721
+ * deny → a reject-flavored option). `scope: "always"` prefers an
722
+ * allow-always option when the request offers one. */
723
+ interface PermissionRespondInput {
724
+ decision: "approve" | "deny";
725
+ optionId?: string;
726
+ scope?: "once" | "always";
727
+ }
728
+ type PermissionRespondResult = {
729
+ ok: true;
730
+ permission: PendingPermission;
731
+ decision: "approve" | "deny";
732
+ optionId?: string;
733
+ } | {
734
+ ok: false;
735
+ error: "not_found" | "session_gone" | "no_matching_option" | "unsupported";
736
+ message: string;
737
+ };
577
738
  interface SessionsRegistry {
578
739
  spawn(input: SpawnSessionInput): SessionDescriptor;
579
740
  /** Adopt a ChildProcess that was spawned outside the registry —
@@ -621,12 +782,21 @@ interface SessionsRegistry {
621
782
  * mints a fresh session id. The `stop` callback is invoked by
622
783
  * `kill()` best-effort. */
623
784
  registerBrowser(input: RegisterBrowserInput): SessionDescriptor;
624
- /** Send a follow-up turn to a live agent session. Throws when the
625
- * session is missing, not an agent-cli kind, dead (exited/killed/
626
- * error and unresumable — `SessionNotAliveError`), or busy
785
+ /** Send a follow-up turn to a live agent session and AWAIT it. Throws
786
+ * when the session is missing, not an agent-cli kind, dead (exited/
787
+ * killed/error and unresumable — `SessionNotAliveError`), or busy
627
788
  * (mid-turn). The events stream into the existing ring buffer +
628
- * line emitter so /stream consumers see them as they arrive. */
629
- sendPrompt(id: string, message: unknown): Promise<void>;
789
+ * line emitter so /stream consumers see them as they arrive.
790
+ *
791
+ * `opts.interrupt` behaves exactly as it does on `enqueuePrompt`:
792
+ * a mid-turn session has its in-flight turn cancelled and settled
793
+ * before admission, so this prompt redirects the SAME live session
794
+ * instead of hitting the busy rejection. Ignored (identical to the
795
+ * default) when the session is idle. Omitted or `false` reproduces
796
+ * the previous mid-turn rejection byte-for-byte. */
797
+ sendPrompt(id: string, message: unknown, opts?: {
798
+ interrupt?: boolean;
799
+ }): Promise<void>;
630
800
  /** Fire-and-forget variant of `sendPrompt` for the TURN ITSELF only.
631
801
  * Admission (resume attempt + the missing/wrong-kind/dead/busy
632
802
  * checks `sendPrompt` throws) is AWAITED before this resolves, so a
@@ -661,6 +831,14 @@ interface SessionsRegistry {
661
831
  * Initial backfill: synchronously invokes `onLine` once for each
662
832
  * line currently in the ring buffer so attaches show context. */
663
833
  attach(id: string, onLine: (line: string, stream: "stdout" | "stderr") => void): (() => void) | null;
834
+ /** Subscribe to a session's structured events.jsonl records as they're
835
+ * written — the live-push half of `GET /sessions/:id/events/stream`'s
836
+ * replay-then-subscribe handoff. Thin passthrough to the transcript
837
+ * writer's own `subscribe` (see transcript-writer.ts for the exactly-
838
+ * once contract this enables); no backfill, no existence check — a
839
+ * session id with no writer state simply never calls back. Returns an
840
+ * unsubscribe fn. */
841
+ subscribeToRecords(id: string, onRecord: (record: Record<string, unknown>) => void): () => void;
664
842
  /** Subscribe to a PTY session's byte stream. Returns a control
665
843
  * handle (write/resize/detach) and null when the session is
666
844
  * missing or not a PTY kind. Replays the ring buffer
@@ -692,6 +870,17 @@ interface SessionsRegistry {
692
870
  * tail. Returns null when the session is missing or not a PTY. */
693
871
  readTerminalOutput(id: string, lastBytes?: number): Buffer | null;
694
872
  kill(id: string, signal?: NodeJS.Signals): boolean;
873
+ /** List permission requests currently parked in the pending-permissions
874
+ * inbox across all permission-hold sessions, newest last. Optionally
875
+ * filtered to one session. */
876
+ listPendingPermissions(filter?: {
877
+ sessionId?: string;
878
+ }): PendingPermission[];
879
+ /** Resolve a parked permission by id — maps the decision (or explicit
880
+ * optionId) to one of the offered options, resolves the held driver RPC,
881
+ * clears the session's awaiting-permission state, and emits
882
+ * `session:permission-resolved`. */
883
+ respondPermission(id: string, input: PermissionRespondInput): Promise<PermissionRespondResult>;
695
884
  /** Stop tracking a session (after it exited and the user clicked
696
885
  * "clear"). Doesn't kill — use `kill` first. */
697
886
  forget(id: string): boolean;
@@ -764,6 +953,11 @@ interface SpawnAgentInput {
764
953
  depth?: number;
765
954
  /** Requested model id — recorded on the descriptor for display + echo. */
766
955
  model?: string;
956
+ /** Resolved auth echo (mode + fingerprint + provider/source/setEnv) —
957
+ * recorded verbatim onto {@link SessionDescriptor.auth}. See that field's
958
+ * doc for the full contract; the caller (`session-spawn.ts`) computes this
959
+ * via the billing-auth resolver, never passing the raw credential here. */
960
+ auth?: SessionAuthEcho;
767
961
  /** Hard ceiling on cumulative session cost (USD). When set and the
768
962
  * adapter's usage reader reports a higher cost at a turn-end, the session
769
963
  * is stopped (best-effort, turn-granular — caps continuation, can't abort
@@ -790,6 +984,11 @@ interface SpawnAgentInput {
790
984
  /** What session close does to the box, when `remote` is true — see
791
985
  * `SessionDescriptor.sandboxTeardown`. */
792
986
  sandboxTeardown?: "kill" | "pause";
987
+ /** True when the driver session was started in permission-hold mode
988
+ * (`AgentCliStartOptions.permissionHold`) — its `agent-prompt` events carry
989
+ * respondable permission requests. Gates whether the registry registers
990
+ * them in the pending-permissions inbox. Default false. */
991
+ permissionHold?: boolean;
793
992
  }
794
993
  interface SpawnSessionInput {
795
994
  kind: SessionKind;
@@ -1142,6 +1341,156 @@ declare class TunnelRegistry {
1142
1341
  private loadFromDisk;
1143
1342
  }
1144
1343
 
1344
+ /**
1345
+ * Daemon-side pairing registry (design: DESIGN §3/§4, PLAN deliverable 2).
1346
+ *
1347
+ * Owns everything the daemon needs to be *paired with* over an untrusted
1348
+ * rendezvous:
1349
+ *
1350
+ * - **Offer store** — in-memory, single-use, expiry-checked one-time tokens
1351
+ * minted by `pair offer`. A daemon restart voids outstanding offers
1352
+ * (acceptable + documented). The P1 `verifyOfferToken` predicate plugs in
1353
+ * here and SPENDS the token on the first successful handshake.
1354
+ * - **Pairings store** — `~/.agentproto/pairings.json` (0600, atomic write),
1355
+ * one record per paired client: `{clientPub, name, fingerprint, createdAt,
1356
+ * lastSeen, pairRoot, rendezvousUrl}`. `pairRoot` is the long-term shared
1357
+ * secret (`derivePairRoot`) from which reconnect routing tokens derive.
1358
+ * - **Rendezvous connections** — for a fresh offer, and for every persisted
1359
+ * pairing on boot (autoconnect), the daemon dials the rendezvous *outbound*
1360
+ * (`side=daemon&t=<token>`), runs `daemonHandshakeOverSink` with the P1
1361
+ * crypto, and — on success — serves the spliced, E2E-wrapped channel exactly
1362
+ * like `serve --connect` serves a tunnel host. The serving path itself is
1363
+ * injected (`serve`) so this module stays free of pty/adapter concerns and
1364
+ * the CLI can reuse its `createTunnelServer` config verbatim.
1365
+ * - **Reconnect epochs** — a persisted pairing's standing connection uses the
1366
+ * pairing-derived routing token `t' = HKDF(pairRoot, "rv-route"‖epoch)`,
1367
+ * rotated per UTC day. The daemon parks on BOTH the current and previous
1368
+ * epoch tokens so a client whose clock straddles midnight still finds it;
1369
+ * reconnect-with-backoff keeps the standing connection alive.
1370
+ * - **Revocation** — removing a pairing drops its rendezvous connections and
1371
+ * stops the daemon parking on its tokens, so it can no longer be reached.
1372
+ *
1373
+ * ## Reconnect authentication (why no stable client key)
1374
+ *
1375
+ * P1's handshake carries only a client *ephemeral* key (regenerated per
1376
+ * handshake) — there is no persistent client identity key, and the PLAN forbids
1377
+ * changing the handshake. So a reconnect authenticates the client by
1378
+ * **possession of the epoch routing token**, which derives from `pairRoot` — a
1379
+ * secret only the paired client and daemon share (it fell out of the original
1380
+ * ECDH). The daemon dials a pairing's epoch token, and on that connection the
1381
+ * `verifyOfferToken` predicate accepts a hello whose offer-token field equals
1382
+ * that same epoch token (constant-time). Token possession ⇒ paired party. The
1383
+ * stored `fingerprint` (of the first handshake's ephemeral key) is a stable
1384
+ * display/revocation handle, not an authentication input.
1385
+ */
1386
+
1387
+ declare const PAIRINGS_VERSION: 1;
1388
+ /** One persisted pairing. Written verbatim to `pairings.json`. */
1389
+ interface PairingRecord {
1390
+ /** The client ephemeral public key from the FIRST handshake (a snapshot —
1391
+ * reconnects use fresh ephemerals; this is not an auth input). */
1392
+ clientPub: string;
1393
+ /** Human-facing label the client chose on `pair accept`. */
1394
+ name: string;
1395
+ /** `identityFingerprint(clientPub)` from the first handshake — the stable
1396
+ * display + revocation handle. */
1397
+ fingerprint: string;
1398
+ /** ISO-8601 first-pair timestamp. */
1399
+ createdAt: string;
1400
+ /** ISO-8601 of the most recent successful (re)connect. */
1401
+ lastSeen: string;
1402
+ /** Long-term shared secret (base64), from which epoch routing tokens derive. */
1403
+ pairRoot: string;
1404
+ /** Rendezvous endpoint to reconnect through. */
1405
+ rendezvousUrl: string;
1406
+ }
1407
+ /** Mode of a served channel — first-contact offer vs. an established reconnect. */
1408
+ type PairingChannelMode = "offer" | "reconnect";
1409
+ /** Context handed to the injected `serve` when a channel comes up. */
1410
+ interface PairingChannelContext {
1411
+ mode: PairingChannelMode;
1412
+ /** Stable pairing fingerprint (present for reconnect; for an offer it's the
1413
+ * freshly-derived one). */
1414
+ fingerprint: string;
1415
+ /** Client label. */
1416
+ name: string;
1417
+ }
1418
+ /** Handle to a live served channel — the registry closes it on teardown. */
1419
+ interface PairingChannelHandle {
1420
+ close(): Promise<void>;
1421
+ }
1422
+ interface CreatedOffer {
1423
+ /** The one-time offer token (base64url). */
1424
+ token: string;
1425
+ /** Offer expiry, unix seconds. */
1426
+ exp: number;
1427
+ /** The full `agentproto://pair?…` offer URL. */
1428
+ url: string;
1429
+ /** The daemon's identity fingerprint (shown to the human at accept time). */
1430
+ fingerprint: string;
1431
+ /** The rendezvous the offer routes through. */
1432
+ rendezvousUrl: string;
1433
+ /** True when `rendezvousUrl` is the hosted default — i.e. neither an explicit
1434
+ * `--rendezvous` nor a configured `pairing.rendezvous` applied, so the offer
1435
+ * falls back to `HOSTED_RENDEZVOUS_URL`. Surfaced so `pair offer` can flag,
1436
+ * never silently, that the daemon is relaying through our infrastructure. */
1437
+ rendezvousIsHostedDefault: boolean;
1438
+ }
1439
+ interface CreateOfferInput {
1440
+ /** Time-to-live in ms. Default 10 minutes. */
1441
+ ttlMs?: number;
1442
+ /** Rendezvous URL override; falls back to the configured default. */
1443
+ rendezvousUrl?: string;
1444
+ }
1445
+ interface PairingRegistryDeps {
1446
+ /** Lazily load (create on first use) the daemon identity. Kept lazy so a
1447
+ * daemon that never pairs never writes an identity file. */
1448
+ loadIdentity: () => Promise<DaemonIdentity>;
1449
+ /** Path to `pairings.json`. Defaults to `~/.agentproto/pairings.json`. */
1450
+ pairingsPath?: string;
1451
+ /** Configured rendezvous URL (from `config.pairing.rendezvous`). Three states:
1452
+ * - `undefined` (key absent) → `createOffer` falls back to the hosted default
1453
+ * (`HOSTED_RENDEZVOUS_URL`).
1454
+ * - a non-empty URL → that endpoint is the default for offers.
1455
+ * - `""` (explicitly empty) → an explicit opt-out: no default applies, so an
1456
+ * offer without `--rendezvous` fails closed. Wire it through verbatim (do
1457
+ * not coerce `""` to `undefined`) or the opt-out silently reverts to the
1458
+ * hosted default. */
1459
+ defaultRendezvousUrl?: string;
1460
+ /** Dial a rendezvous WS and adapt it to a FrameSink. Injected by the CLI
1461
+ * (uses `ws` + `wrapWebSocket`). Rejects if the dial fails. The `signal`
1462
+ * aborts on registry shutdown. */
1463
+ dial: (wsUrl: string, signal: AbortSignal) => Promise<FrameSink>;
1464
+ /** Serve the E2E-wrapped channel (the CLI builds `createTunnelServer` over
1465
+ * it with the same config `serve --connect` uses). */
1466
+ serve: (sink: E2eFrameSink, ctx: PairingChannelContext) => PairingChannelHandle;
1467
+ /** Injectable clock (ms). Defaults to Date.now. */
1468
+ now?: () => number;
1469
+ /** Diagnostic log sink. */
1470
+ log?: (line: string) => void;
1471
+ /** Reconnect backoff floor / ceiling (ms). Defaults 1s / 30s. */
1472
+ reconnectMinMs?: number;
1473
+ reconnectMaxMs?: number;
1474
+ /** How long a parked daemon socket waits for a client hello before recycling
1475
+ * the connection. Kept just under the broker's park timeout. Default 110s. */
1476
+ handshakeTimeoutMs?: number;
1477
+ }
1478
+ interface PairingRegistry {
1479
+ /** Mint a one-time offer + start dialing the rendezvous for it. */
1480
+ createOffer(input?: CreateOfferInput): Promise<CreatedOffer>;
1481
+ /** All persisted pairings (copies). Loads `pairings.json` on first call. */
1482
+ list(): Promise<PairingRecord[]>;
1483
+ /** Drop a pairing by fingerprint or name; stops its rendezvous connections.
1484
+ * Returns false when nothing matched. */
1485
+ revoke(idOrName: string): Promise<boolean>;
1486
+ /** Start standing reconnect connections for every persisted pairing. Call
1487
+ * after the gateway is up (so the injected `serve` can reach it). */
1488
+ startAutoconnect(): Promise<void>;
1489
+ /** Tear down every offer + reconnect connection and served channel. */
1490
+ shutdown(): Promise<void>;
1491
+ }
1492
+ declare function createPairingRegistry(deps: PairingRegistryDeps): PairingRegistry;
1493
+
1145
1494
  /**
1146
1495
  * Fire-and-forget webhook notifier for session lifecycle events.
1147
1496
  *
@@ -1870,6 +2219,17 @@ type AgentAdapterResolver = (slug: string) => Promise<{
1870
2219
  * handler, POST /sessions/agent) wires this to pulse
1871
2220
  * `SessionDescriptor.lastActivityAt` via `registry.pulseActivity(id)`. */
1872
2221
  onActivity?: () => void;
2222
+ /** Start the session in permission-hold mode — forwarded to the driver's
2223
+ * `runtime.start({ permissionHold })` so each ACP permission request is
2224
+ * surfaced + parked in the daemon's inbox instead of auto-answered.
2225
+ * Adapters/arms with no permission surface ignore it. Default false. */
2226
+ permissionHold?: boolean;
2227
+ /** FULLY-RESOLVED billing-auth spec forwarded from `agent_start` to the
2228
+ * driver's `runtime.start({ auth })`, computed by the runtime's
2229
+ * `resolveAuthSpec` (provider, ordered mode, setEnv/scrub, credential
2230
+ * source all pre-decided). The driver applies it mechanically. Absent
2231
+ * when the resolver produced no spec (ambient). */
2232
+ auth?: ResolvedAuthSpec;
1873
2233
  }): Promise<AgentSessionLike>;
1874
2234
  /** Display label for the descriptor's `command` field. */
1875
2235
  commandPreview?: string;
@@ -1887,6 +2247,15 @@ type AgentAdapterResolver = (slug: string) => Promise<{
1887
2247
  * documented no-op for that adapter (e.g. claude-code, which
1888
2248
  * auto-discovers skills and declares no such option). */
1889
2249
  declaredOptions?: readonly DeclaredAdapterOption[];
2250
+ /** Billing-auth descriptor projected from the adapter manifest
2251
+ * (`provider` / `authEnforce` / `authSubscription`) — the sole input the
2252
+ * runtime's `resolveAuthSpec` reads about the adapter's auth capability
2253
+ * (keeping the catalog coupling in the runtime, the driver mechanical).
2254
+ * Omitted ⇒ ambient (no credential injection). */
2255
+ authDescriptor?: AdapterAuthDescriptor;
2256
+ /** Adapter's default model id (`models.default`) — lets the resolver derive
2257
+ * a provider for a by-model spawn that omitted `model`. */
2258
+ defaultModel?: string;
1890
2259
  } | null>;
1891
2260
  /**
1892
2261
  * UI-safe projection of an AIP-45 `modes[]` entry as surfaced by
@@ -2098,6 +2467,27 @@ interface McpCredentialDeps {
2098
2467
  declare function setMcpCredentialDeps(d: McpCredentialDeps): void;
2099
2468
  declare function getMcpCredentialDeps(): McpCredentialDeps;
2100
2469
 
2470
+ /**
2471
+ * MCP tools that drive the pairing registry — the daemon's "let a client pair
2472
+ * with me over an untrusted rendezvous" surface (design: DESIGN §6).
2473
+ *
2474
+ * Three tools, mirroring the CLI verbs:
2475
+ *
2476
+ * pair_offer — mint a one-time offer + start dialing the rendezvous; returns
2477
+ * the `agentproto://pair?…` URL, fingerprint, and expiry.
2478
+ * pair_list — list persisted pairings (name, fingerprint, lastSeen).
2479
+ * pair_revoke — drop a pairing by fingerprint or name; stops its rendezvous
2480
+ * connections so it can no longer reconnect.
2481
+ *
2482
+ * Registered beside `remote_*` (remote-tools.ts) — same closure-rebind pattern;
2483
+ * the registry singleton lives on the gateway.
2484
+ */
2485
+
2486
+ interface RegisterPairingToolsOptions {
2487
+ registry: PairingRegistry;
2488
+ }
2489
+ declare function registerPairingTools(server: McpServer, opts: RegisterPairingToolsOptions): void;
2490
+
2101
2491
  /**
2102
2492
  * Provider-preset family on top of `@agentproto/provider-kit` — the *thin,
2103
2493
  * honest* lister (Option B). Unlike tunnels/sandboxes/eval-reporters, presets
@@ -2491,6 +2881,16 @@ interface CreateGatewayOptions {
2491
2881
  /** Optional sandbox provider lister — mirrors `listAgentAdapters`.
2492
2882
  * Overrides the default catalog-driven lister behind `list_sandbox_providers`. */
2493
2883
  listSandboxProviders?: SandboxProviderLister;
2884
+ /**
2885
+ * Optional E2E pairing registry (see `createPairingRegistry`). When wired,
2886
+ * the gateway mounts the `/pairings/*` REST routes and the `pair_*` MCP
2887
+ * tools, and exposes the registry on the handle. The CLI builds it (injecting
2888
+ * the ws `dial` + a `createTunnelServer`-backed `serve`) and passes it here so
2889
+ * the runtime stays free of pty/adapter concerns while the pairing surface is
2890
+ * reachable over MCP + HTTP. Autoconnect is the caller's to start (after this
2891
+ * returns) so the injected `serve` can capture the finished gateway.
2892
+ */
2893
+ pairingRegistry?: PairingRegistry;
2494
2894
  }
2495
2895
  interface GatewayHandle {
2496
2896
  url: string;
@@ -2505,6 +2905,10 @@ interface GatewayHandle {
2505
2905
  * local port. Exposed so embedding hosts can open tunnels
2506
2906
  * programmatically without going through the HTTP/MCP surface. */
2507
2907
  tunnels: TunnelRegistry;
2908
+ /** E2E pairing registry, when one was wired via
2909
+ * `CreateGatewayOptions.pairingRegistry`. Undefined otherwise. Exposed so
2910
+ * the CLI can `startAutoconnect()` after boot and `shutdown()` it. */
2911
+ pairing?: PairingRegistry;
2508
2912
  /** Per-boot bearer token required on mutating /sessions/* routes
2509
2913
  * + WS PTY upgrades. Exposed so an embedding host (e.g. the CLI
2510
2914
  * shell that hosts the gateway in-process) can pass it to child
@@ -2534,4 +2938,4 @@ interface GatewayHandle {
2534
2938
  */
2535
2939
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
2536
2940
 
2537
- export { type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AttachPolicyInput, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CommitSpec, type CompletionPolicySupervisor, type CreateGatewayOptions, DEFAULT_ORCHESTRATOR_TOOLS, type DeclaredAdapterPreset, type GateSpec, type GatewayHandle, type InboundWatcher, type JudgeGateSpec, type McpCredentialDeps, type OnFailSpec, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, type PolicyRunState, type PolicyRunStatus, type PresetInfo, type PricingResolver, type RegisterBrowserInput, type RegisterSessionInput, type RuntimeMeta, type SandboxAdapterInfo, type SandboxProviderCapabilities, type SandboxProviderHandle, type SandboxProviderLister, type SandboxProviderResolver, type ScopeTokenRegistry, type SessionDescriptor, type SessionKind, type SessionObserver, type SessionStatus, type SessionUsage, type SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, type SpawnSessionInput, type TokenPricing, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type UsageComputeInput, type UsageSource, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createScopeTokenRegistry, daemonRegistryDir, declaredPresetToProviderPreset, deriveSessionUsage, formatToolCall, formatToolResult, getMcpCredentialDeps, listPresets, makeBrowserAdapterLister, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, projectSessionUsage, readDaemonRegistry, readRuntimeMeta, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
2941
+ export { AdapterAuthDescriptor, type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AttachPolicyInput, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CommitSpec, type CompletionPolicySupervisor, type CreateGatewayOptions, type CreateOfferInput, type CreatedOffer, DEFAULT_ORCHESTRATOR_TOOLS, DeclaredAdapterOption, type DeclaredAdapterPreset, type GateSpec, type GatewayHandle, type InboundWatcher, type JudgeGateSpec, type McpCredentialDeps, type OnFailSpec, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, PAIRINGS_VERSION, type PairingChannelContext, type PairingChannelHandle, type PairingChannelMode, type PairingRecord, type PairingRegistry, type PairingRegistryDeps, type PendingPermission, type PermissionRespondInput, type PermissionRespondResult, type PolicyRunState, type PolicyRunStatus, type PresetInfo, type PricingResolver, type RegisterBrowserInput, type RegisterPairingToolsOptions, type RegisterSessionInput, ResolvedAuthSpec, type RuntimeMeta, type SandboxAdapterInfo, type SandboxProviderCapabilities, type SandboxProviderHandle, type SandboxProviderLister, type SandboxProviderResolver, type ScopeTokenRegistry, type SessionDescriptor, type SessionKind, type SessionObserver, type SessionStatus, type SessionUsage, type SessionWaitEvent, type SessionWaitResult, type SessionsRegistry, type ShellGateSpec, type SpawnAgentInput, type SpawnSessionInput, type TokenPricing, type TunnelDescriptor, type TunnelProvider, type TunnelStatus, type UsageComputeInput, type UsageSource, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, daemonRegistryDir, declaredPresetToProviderPreset, deriveSessionUsage, formatToolCall, formatToolResult, getMcpCredentialDeps, listPresets, makeBrowserAdapterLister, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, projectSessionUsage, readDaemonRegistry, readRuntimeMeta, registerPairingTools, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };