@agentproto/runtime 0.8.0 → 1.1.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
@@ -2,9 +2,12 @@ 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
4
  import { AcpPermissionResolution, AcpMcpServer } from '@agentproto/acp';
5
+ import { SessionMode } from '@agentproto/acp/client';
6
+ export { SessionMode } from '@agentproto/acp/client';
5
7
  import { ChildProcess } from 'node:child_process';
6
- import { W as WorktreeIsolationMode, R as ResolvedAuthSpec, D as DeclaredAdapterOption, A as AdapterAuthDescriptor } from './config-BRKy_SAF.js';
8
+ import { W as WorktreeIsolationMode, A as AdapterAuthDescriptor, R as ResolvedAuthSpec, D as DeclaredAdapterOption } from './config-BRKy_SAF.js';
7
9
  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 './config-BRKy_SAF.js';
10
+ import { AuthProfile } from '@agentproto/auth';
8
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
12
  import { AdapterHandle, SetupField, AdapterResolver, AdapterLister, AdapterEntry } from '@agentproto/provider-kit';
10
13
  import { SandboxProvider } from '@agentproto/sandbox';
@@ -132,6 +135,177 @@ interface UsageDescriptorFields {
132
135
  */
133
136
  declare function projectSessionUsage(desc: UsageDescriptorFields): SessionUsage;
134
137
 
138
+ /**
139
+ * SessionConfig — the unified per-session config surface, decomposed into
140
+ * orthogonal axes (SPEC §3.1, out-of-repo design doc
141
+ * `agentproto-session-config-axes/SPEC.md`). Pure types + a `decomposeMode`/
142
+ * `composeMode` shim over the legacy AIP-45 `mode` string — no I/O, no runtime
143
+ * wiring, no adapter behavior change. This is the additive type foundation
144
+ * (SPEC §7.2 build step 1); the live spawn-path extraction (deleting gateway
145
+ * modes, sourcing posture from the ACP registry) is a separate later step.
146
+ *
147
+ * Boundary note: this module deliberately does NOT import from
148
+ * `@agentproto/auth`. `packages/runtime` intentionally has no dependency on
149
+ * that package (`mcp-credential-deps.ts:1-9`), so the named-profile record it
150
+ * owns — `AuthProfile` (`packages/auth/src/profile-types.ts:24`, shipped in
151
+ * #470) — is referenced by citation, never redefined here: the `access` axis
152
+ * only needs a `profileRef` string, and the profile record itself stays in
153
+ * `@agentproto/auth`. The narrow `AuthMethod` *facet*, by contrast, is
154
+ * structurally mirrored below (§3.1) — the descriptor's `accessProfile` echo
155
+ * (§3.7) names it, and mirroring a two-value string union is cheaper than
156
+ * taking a package dependency on `@agentproto/auth` for it, the same
157
+ * structural-mirror rationale as `DeclaredAdapterMode` (below) and
158
+ * `DeclaredAdapterOption` (`spawn-defaults.ts:447`).
159
+ *
160
+ * It DOES, however, import a VALUE — the legacy AIP-45 mode-id classification
161
+ * (`inferLegacyModeKind`) — from `@agentproto/driver-agent-cli` (a lean,
162
+ * acyclic runtime → driver edge; the driver never depends back on the runtime).
163
+ * Unlike the type mirrors above (a mirror can't silently break — the compiler
164
+ * still checks structural compatibility), the gateway/posture id SET is a value
165
+ * list needed on BOTH sides of the driver/runtime boundary — here for
166
+ * `decomposeMode` and in the driver's `composeSpawn` back-compat shim — so it
167
+ * is single-sourced in the driver (its AIP-45 home) rather than copied, which
168
+ * for a value list WOULD silently drift. Only the daemon-domain canonical-
169
+ * posture VALUE mapping (`POSTURE_MODE_VALUES`) stays local.
170
+ */
171
+ /**
172
+ * Reasoning / compute budget label.
173
+ *
174
+ * SUPERSET ONLY — this flat union is the documented ceiling, NOT the valid set.
175
+ * The real accepted values are a function of (adapter × model), resolved at
176
+ * runtime (SPEC §3.9): the same label maps to a different compute budget across
177
+ * models, and `"max"` / `"ultracode"` are session-only and model-gated (opus
178
+ * offers `ultracode`, haiku does not —
179
+ * `adapters/claude-code/src/index.ts:310-321`). Consumers must resolve the
180
+ * offerable set per session/model, never treat this enum as authoritative.
181
+ */
182
+ type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max" | "ultracode";
183
+ /**
184
+ * agentproto-canonical posture — a portable, normalized vocabulary for "what
185
+ * the agent may DO" (SPEC §3.1/§3.4a). A canonical posture resolves to a
186
+ * harness `SessionModeId` when the session's advertised `availableModes` has an
187
+ * equivalent (native, enforced, live via `setSessionMode`); otherwise it is
188
+ * applied as an injected system-prompt preamble (advisory, not a permission
189
+ * boundary — SPEC risk Rw).
190
+ */
191
+ type CanonicalPosture = "default" | "plan" | "accept-edits" | "bypass" | "read-only";
192
+ /**
193
+ * The posture axis: either an agentproto-canonical posture OR a raw harness
194
+ * mode id sourced directly from the harness's own ACP mode registry
195
+ * (`SessionModeState.availableModes` — SDK `types.gen.d.ts:4227`). Postures are
196
+ * NOT a manifest mode list going forward (SPEC §3.4a); the `{ harnessModeId }`
197
+ * form carries a native mode the canonical vocabulary doesn't name.
198
+ */
199
+ type Posture = CanonicalPosture | {
200
+ harnessModeId: string;
201
+ };
202
+ /**
203
+ * Endpoint / billing rail — model/route config, NOT a mode (SPEC §3.4a). A
204
+ * gateway is reached by resolving the model's `@route` against the catalog
205
+ * (`packages/model-catalog/src/route-identity/index.ts:1-46`) and applying its
206
+ * base_url + credential-scrub on the route apply-path; `access` (which profile
207
+ * is eligible) is downstream of THIS axis (SPEC §1c).
208
+ */
209
+ interface RouteSpec {
210
+ /** Preset id ("anthropic"/direct, "moonshot", "openrouter", "requesty",
211
+ * "deepseek") or a custom id paired with an explicit `baseUrl`. */
212
+ gateway: string;
213
+ /** Resolved base URL; carried only for a custom gateway the catalog can't
214
+ * resolve. The attached profile's credential is resolved separately (§1c),
215
+ * never inlined here. */
216
+ baseUrl?: string;
217
+ }
218
+ /**
219
+ * What enters context. `"lean"` drops bundled skills
220
+ * (`adapters/claude-code/src/index.ts:196-204`); `"full"` is the default. The
221
+ * `(string & {})` arm keeps a future adapter-declared context profile from
222
+ * being blocked while preserving literal autocompletion for the known values.
223
+ */
224
+ type ContextProfile = "full" | "lean" | (string & {});
225
+ /**
226
+ * How an attached auth profile authenticates — the narrow eligibility gate,
227
+ * a *facet* of a named profile (SPEC §1c/§3.1), NOT a session-level enum. A
228
+ * structural mirror of `@agentproto/auth`'s `AuthMethod`
229
+ * (`packages/auth/src/profile-types.ts:21`, #470): `"api-key"` ↔ `tokenKind
230
+ * "pat"`, `"oauth-bearer"` ↔ `tokenKind "oat"` (a subscription bearer). Kept
231
+ * here — rather than imported — because `packages/runtime` has no dependency
232
+ * on `@agentproto/auth` (see the boundary note above); only the descriptor's
233
+ * `accessProfile` echo (§3.7) needs to name it.
234
+ */
235
+ type AuthMethod = "oauth-bearer" | "api-key";
236
+ /**
237
+ * The complete per-session config surface, decomposed into orthogonal axes
238
+ * (SPEC §3.1). Every field optional; omission = "adapter default" for that
239
+ * axis. The decomposed axes are the canonical form — persistence and transport
240
+ * use these fields, never a recomposed legacy `mode` string (SPEC §3.8).
241
+ */
242
+ interface SessionConfig {
243
+ /** Route-identity ref: `[route:]vendor/product[:pin][@route]`
244
+ * (`packages/model-catalog/src/route-identity/index.ts:1-46`). */
245
+ model?: string;
246
+ effort?: EffortLevel;
247
+ /** Attach a NAMED auth profile by id (SPEC §1c). The profile record —
248
+ * `AuthProfile { id, vendor, method, credentialRef, label? }` — lives in
249
+ * `@agentproto/auth` (`packages/auth/src/profile-types.ts:24`, #470), NOT on
250
+ * the session and NOT in `providers.json`. Omit ⇒ default profile. */
251
+ access?: {
252
+ profileRef?: string;
253
+ };
254
+ /** Endpoint / gateway rail; `access` is downstream of this (SPEC §1c). */
255
+ route?: RouteSpec;
256
+ /** What the agent may DO. */
257
+ posture?: Posture;
258
+ /** What enters context. */
259
+ contextProfile?: ContextProfile;
260
+ }
261
+ /**
262
+ * Manifest-declared AIP-45 mode id + axis discriminant — the minimum
263
+ * `decomposeMode`/`composeMode` need from `AgentCliMode` (driver `types.ts`).
264
+ * Mirrors its `id`/`kind` fields structurally rather than importing the full
265
+ * `AgentCliMode` TYPE surface from `@agentproto/driver-agent-cli` — the same
266
+ * structural-mirror pattern as `DeclaredAdapterOption` (`spawn-defaults.ts:447`)
267
+ * and `DeclaredAdapterPreset` (`preset-tools.ts`). (The module does take a
268
+ * value-level import — `inferLegacyModeKind` — from that package; see the
269
+ * boundary note at the top for why a shared value list is single-sourced, not
270
+ * mirrored.)
271
+ *
272
+ * `kind` narrows to a single meaningful value going forward — `"context"` (the
273
+ * one axis with no ACP-protocol home); routes come from the catalog and
274
+ * postures from the harness's ACP registry (SPEC §3.4a). `"posture"`/`"route"`
275
+ * remain accepted so `decomposeMode` can still classify a LEGACY tagged
276
+ * manifest during migration.
277
+ */
278
+ interface DeclaredAdapterMode {
279
+ id: string;
280
+ kind?: "posture" | "route" | "context";
281
+ }
282
+ /**
283
+ * Map ONE legacy `mode` id (AIP-45 `modes[]`) onto its orthogonal axis
284
+ * (SPEC §3.5). Classification order: (1) the mode's own explicit `kind` tag;
285
+ * (2) inference over well-known ids (gateway ids ⇒ route, known posture ids ⇒
286
+ * posture); (3) a truly-unknown id defaults to `contextProfile` —
287
+ * least-privilege, since defaulting to `posture` or `route` could silently
288
+ * grant elevated permissions or reroute billing (SPEC risk R4).
289
+ *
290
+ * Always yields a `CanonicalPosture` for the posture axis, never a
291
+ * `{ harnessModeId }` — the shim speaks the portable vocabulary only; raw
292
+ * harness modes come from the ACP registry, not a legacy `mode` id.
293
+ */
294
+ declare function decomposeMode(modes: readonly DeclaredAdapterMode[], modeId: string): Partial<SessionConfig>;
295
+ /**
296
+ * The reverse of `decomposeMode` — picks the single legacy mode id whose
297
+ * decomposition matches `cfg`.
298
+ *
299
+ * LOSSY / display-and-back-compat ONLY (SPEC §3.8). A single legacy `mode`
300
+ * string cannot represent an orthogonal combination — `{ posture: "plan",
301
+ * route: { gateway: "moonshot" } }` has no single legacy id — so `composeMode`
302
+ * returns the FIRST declared mode whose axis matches and silently drops the
303
+ * rest. It is NEVER a storage or transport form: the driver applies each axis's
304
+ * env/argv patch directly, and anything that must persist/transmit config uses
305
+ * the decomposed axes. Returns undefined when no declared mode matches.
306
+ */
307
+ declare function composeMode(cfg: Partial<SessionConfig>, modes: readonly DeclaredAdapterMode[]): string | undefined;
308
+
135
309
  /**
136
310
  * In-process pub/sub bus for session lifecycle events. Separate from
137
311
  * RuntimeEvents (global daemon bus) — session events are scoped to
@@ -141,7 +315,8 @@ declare function projectSessionUsage(desc: UsageDescriptorFields): SessionUsage;
141
315
  * (fire-and-forget HTTP), RoutineRunner (state machine fan-in),
142
316
  * and session_monitor MCP tool (long-poll multiplexed).
143
317
  */
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";
318
+
319
+ type SessionEventType = "session:turn-end" | "session:awaiting-input" | "session:permission-request" | "session:permission-resolved" | "session:exited" | "session:command-done" | "session:model-changed" | "session:config-changed" | "session:renamed" | "policy:passed" | "policy:failed" | "policy:commit-ready" | "policy:committed" | "cron:fired" | "cron:succeeded" | "cron:failed";
145
320
  /**
146
321
  * Structured detail on why a session is awaiting input, when derivable.
147
322
  * `source: "structured"` — a driver-reported ACP-style prompt (e.g. a tool
@@ -241,6 +416,78 @@ interface SessionCommandDoneEvent {
241
416
  exitCode: number;
242
417
  ts: string;
243
418
  }
419
+ /**
420
+ * Emitted when `SessionsRegistry.setModel` successfully switches a LIVE
421
+ * agent-cli session's model (`POST /sessions/:id/model`, `session_set_model`
422
+ * MCP tool). Only fires on `applied: true` — a rejected switch changes
423
+ * nothing about the session, so there's nothing to announce. `model` is the
424
+ * new value now reflected on `SessionDescriptor.model` (and therefore on the
425
+ * next `session_list` / SSE `sessionUpdate` a client sees). Consumers:
426
+ * `session_events_poll`, the webhook notifier, and the routine engine —
427
+ * same distribution as every other lifecycle event on this bus.
428
+ */
429
+ interface SessionModelChangedEvent {
430
+ type: "session:model-changed";
431
+ sessionId: string;
432
+ model: string;
433
+ label?: string;
434
+ ts: string;
435
+ }
436
+ /**
437
+ * The `SessionConfig` axis a `session:config-changed` event reports. Reuses
438
+ * the axis vocabulary of `SessionConfig` itself (`session-config.ts`, SPEC
439
+ * §3.1) so there's a single source of truth for the axis names — model /
440
+ * effort / access / route / posture / contextProfile.
441
+ */
442
+ type SessionConfigAxis = keyof SessionConfig;
443
+ /**
444
+ * Emitted when a SINGLE `SessionConfig` axis is changed on a session and the
445
+ * change actually took effect (SPEC §7.2 build step 4). This is the shared,
446
+ * axis-generic successor to `session:model-changed`: the live config verbs
447
+ * (`agent_set_effort` / `agent_set_posture`, step 5) and restart-with-override
448
+ * (step 6) all announce their single-axis change through THIS event, carrying
449
+ * which axis moved and the new value now reflected on the descriptor.
450
+ *
451
+ * A model change still ALSO emits `session:model-changed` as a back-compat
452
+ * alias (`sessions.ts` `setModel`) so existing subscribers keep working; new
453
+ * consumers should prefer this event and switch on `axis`.
454
+ *
455
+ * Discriminated by `axis`, with `value` typed to that axis's `SessionConfig`
456
+ * field — a `model` change carries a `string`, an `effort` change an
457
+ * `EffortLevel`, a `route` change a `RouteSpec`, and so on. Same bus
458
+ * distribution as every other lifecycle event (`session_events_poll`, the
459
+ * webhook notifier, the routine engine).
460
+ */
461
+ type SessionConfigChangedEvent = {
462
+ [A in SessionConfigAxis]: {
463
+ type: "session:config-changed";
464
+ sessionId: string;
465
+ /** Which `SessionConfig` axis changed. */
466
+ axis: A;
467
+ /** The axis's new value, as now reflected on `SessionDescriptor`. */
468
+ value: NonNullable<SessionConfig[A]>;
469
+ label?: string;
470
+ ts: string;
471
+ };
472
+ }[SessionConfigAxis];
473
+ /**
474
+ * Emitted when an operator sets or clears a session's user-facing name
475
+ * (`PATCH /sessions/:id`, the `session_rename` MCP verb). Unlike
476
+ * `session:config-changed`, a name is NOT a `SessionConfig` axis — it never
477
+ * touches the live agent, only the descriptor's display fields — so it rides
478
+ * its own event. `title`/`label` carry the values now on the descriptor
479
+ * (absent when that field was cleared). Same bus distribution as every other
480
+ * lifecycle event: `session_events_poll`, the webhook notifier, the routine
481
+ * engine — which is how a live UI (the VS Code tree / transcript header/tab)
482
+ * learns to repaint the name without waiting for its next snapshot poll.
483
+ */
484
+ interface SessionRenamedEvent {
485
+ type: "session:renamed";
486
+ sessionId: string;
487
+ title?: string;
488
+ label?: string;
489
+ ts: string;
490
+ }
244
491
  /** Emitted by the supervisor when a completion policy's gate passes. */
245
492
  interface PolicyPassedEvent {
246
493
  type: "policy:passed";
@@ -305,7 +552,7 @@ interface CronFailedEvent {
305
552
  error: string;
306
553
  ts: string;
307
554
  }
308
- type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
555
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | SessionRenamedEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
309
556
  interface SessionEventBus {
310
557
  emit(ev: SessionEvent): void;
311
558
  /** Subscribe to a specific event type. Returns an unsubscribe fn. */
@@ -375,8 +622,117 @@ interface AgentSessionLike {
375
622
  * drivers that don't model held permissions (sandbox proxy, future
376
623
  * transports). */
377
624
  respondPermission?(requestId: string, resolution: AcpPermissionResolution): boolean | Promise<boolean>;
625
+ /**
626
+ * Switch the active model on this LIVE session — mirrors
627
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setModel`
628
+ * without importing it (this package stays driver-decoupled, same
629
+ * reasoning as the rest of this structural interface). Optional: absent
630
+ * for a session shape that doesn't support live model switching (a
631
+ * sandboxed proxy session, or a future non-agent-cli transport) —
632
+ * `SessionsRegistry.setModel` treats a missing method as
633
+ * `{applied:false, reason:"not-supported"}` rather than throwing.
634
+ */
635
+ setModel?(modelId: string): Promise<SetSessionModelResult>;
636
+ /**
637
+ * Switch the reasoning/compute budget on this LIVE session — mirrors
638
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setEffort`
639
+ * (ACP `session/set_config_option(configId:"effort")`), same driver-decoupled
640
+ * structural-mirror reasoning as `setModel` above. Optional: absent for a
641
+ * session shape with no live config surface (sandboxed proxy, future
642
+ * transport) — `SessionsRegistry.setEffort` treats a missing method as
643
+ * `{applied:false, reason:"not-supported"}` rather than throwing.
644
+ */
645
+ setEffort?(effort: string): Promise<SetSessionEffortResult>;
646
+ /**
647
+ * Switch the native posture (harness mode) on this LIVE session — mirrors
648
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setSessionMode`
649
+ * (ACP `session/set_mode`). Optional, same treatment as `setModel`/`setEffort`.
650
+ */
651
+ setSessionMode?(modeId: string): Promise<SetSessionModeResult>;
652
+ /**
653
+ * The harness's advertised session modes captured at connect time
654
+ * (`SessionModeState.availableModes`, #482 ACP capability read-surface).
655
+ * `SessionsRegistry.setPosture` resolves a requested posture against this to
656
+ * decide native-live vs restart (SPEC §3.4a). Absent/empty for arms with no
657
+ * native mode registry — treated as "no native mode", so any posture pick
658
+ * routes to restart.
659
+ */
660
+ readonly availableModes?: readonly SessionMode[];
378
661
  close(): Promise<void>;
379
662
  }
663
+ /** Result of `SessionsRegistry.setModel` — see that method's doc comment. */
664
+ interface SetSessionModelResult {
665
+ applied: boolean;
666
+ /** The model id now active. Present only when `applied` is true. */
667
+ model?: string;
668
+ /** Present only when `applied` is false — see
669
+ * `@agentproto/driver-agent-cli`'s `SetModelResult` for the reason
670
+ * vocabulary this passes through verbatim. */
671
+ reason?: string;
672
+ /**
673
+ * Present only on the model↔route guard refusal (SPEC risk R2 / §4.4): the
674
+ * requested model's route-identity crosses the session's current route/vendor
675
+ * boundary, which a live `setModel` cannot perform (route is a spawn-time
676
+ * `ANTHROPIC_BASE_URL`, not a live ACP config option). The daemon refuses the
677
+ * live switch (`applied:false, reason:"requires-restart"`) rather than
678
+ * silently keeping the old endpoint, and hands back the override a
679
+ * restart-with-override (step 6) should carry to apply model + route together.
680
+ */
681
+ suggestedOverride?: {
682
+ route: RouteSpec;
683
+ model: string;
684
+ };
685
+ }
686
+ /**
687
+ * Result of `SessionsRegistry.setEffort` — mirrors the driver's
688
+ * `SetEffortResult` (see `AgentSessionLike.setEffort`). Effort is a per-model
689
+ * capability (SPEC §3.9); a rejected label is a soft `{applied:false, reason}`
690
+ * (SPEC risk R7), never thrown.
691
+ */
692
+ interface SetSessionEffortResult {
693
+ applied: boolean;
694
+ /** The effort label now active. Present only when `applied` is true. */
695
+ effort?: string;
696
+ /** Present only when `applied` is false — passes the driver's reason through
697
+ * verbatim (`"not-supported"`, or the wrapper's own rejection detail). */
698
+ reason?: string;
699
+ }
700
+ /** Result of a mid-session `setSessionMode` attempt on the driver session —
701
+ * structural mirror of `@agentproto/driver-agent-cli`'s `SetSessionModeResult`,
702
+ * used by {@link AgentSessionLike.setSessionMode}. */
703
+ interface SetSessionModeResult {
704
+ applied: boolean;
705
+ /** The mode id that took effect. Present only when `applied` is true. */
706
+ modeId?: string;
707
+ /** Present only when `applied` is false — the driver's reason verbatim. */
708
+ reason?: string;
709
+ }
710
+ /**
711
+ * Result of `SessionsRegistry.setPosture` (SPEC §4.2, build step 5). A posture
712
+ * that maps to a native advertised harness mode is switched LIVE via
713
+ * `setSessionMode` (`applied:true`); one with no native mode (prompt-injected /
714
+ * env-applied) is NOT forced live — it resolves
715
+ * `{applied:false, reason:"requires-restart"}` so the caller routes it through
716
+ * the restart-with-override path (step 6, not implemented here).
717
+ */
718
+ interface SetSessionPostureResult {
719
+ applied: boolean;
720
+ /** The posture now active. Present only when `applied` is true. */
721
+ posture?: Posture;
722
+ /** The native harness mode id switched to — present only on a native
723
+ * (`applied:true`) switch, so a caller can echo exactly what took effect. */
724
+ modeId?: string;
725
+ /** Present only when `applied` is false — `"requires-restart"` when the
726
+ * posture has no native mode (prompt/env apply-path needs a fresh spawn),
727
+ * `"not-supported"` for a session with no live mode surface, or the harness's
728
+ * own rejection detail when a native switch was attempted and refused. */
729
+ reason?: string;
730
+ /** How the requested posture resolved against the harness's advertised modes
731
+ * (`native` | `prompt` | `noop` | `unavailable`, from `resolvePosture`) —
732
+ * lets the caller distinguish "needs restart because prompt-injected" from a
733
+ * genuine native-switch rejection. */
734
+ resolution?: "native" | "prompt" | "noop" | "unavailable";
735
+ }
380
736
  /**
381
737
  * Minimal PTY surface — structurally compatible with
382
738
  * @agentproto/acp/tunnel's PtyProcess (node-pty's IPty wrapper). The
@@ -473,6 +829,24 @@ interface SessionAuthEcho {
473
829
  credentialSource?: "explicit-config" | "providers-store" | "none";
474
830
  setEnv?: string;
475
831
  }
832
+ /**
833
+ * The `access` axis's descriptor ECHO (SPEC §3.6/§3.7) — a non-secret
834
+ * description of the NAMED auth profile attached to the session, recorded so a
835
+ * client chip can NAME the wallet ("Jeremy Max") without re-resolving it. This
836
+ * is deliberately separate from {@link SessionAuthEcho}, which stays as-is: the
837
+ * `auth` echo is the resolver's observable output (mode + credential
838
+ * fingerprint), this is the profile IDENTITY the operator selected. NEVER the
839
+ * credential — `profileRef` resolves through `@agentproto/auth` at read time
840
+ * (`packages/auth/src/profile-types.ts:24`, #470). `profileRef` is always set
841
+ * when this object is present (the profile is the reason it exists); the rest
842
+ * mirror the `AuthProfile` fields the chip renders.
843
+ */
844
+ interface SessionAccessProfileEcho {
845
+ profileRef: string;
846
+ label?: string;
847
+ vendor: string;
848
+ method: AuthMethod;
849
+ }
476
850
  interface SessionDescriptor {
477
851
  id: string;
478
852
  kind: SessionKind;
@@ -535,6 +909,19 @@ interface SessionDescriptor {
535
909
  * about, for a UI that would otherwise show the adapter's argv. Distinct
536
910
  * from `label`, which the spawner supplies and which always wins. */
537
911
  title?: string;
912
+ /** Housekeeping-only visibility flag: hides the session from `list()`'s
913
+ * default view (`session_list`, `GET /sessions`, panels) once set. Never
914
+ * touches the daemon otherwise — the process is already gone by the time
915
+ * this is set (see the terminal-status guard on `archiveSession`), the
916
+ * transcript stays fully readable via `get()`/`findByIdOrName` (neither
917
+ * filters on it), and `list({ includeArchived: true })` still returns it.
918
+ * Set by `archiveSession`/`unarchiveSession` (session-tools.ts's
919
+ * `session_archive`/`session_unarchive`), persisted like every other
920
+ * descriptor field, and round-trips through `loadHistorySnapshot` on
921
+ * reboot since it's carried by the same `...desc` spread every other
922
+ * field is. Absent (not `false`) for every descriptor from before this
923
+ * field existed — treated the same as `false` everywhere it's read. */
924
+ archived?: boolean;
538
925
  /** True when the session was spawned under a real PTY (node-pty)
539
926
  * instead of `child_process.spawn`. PTY sessions carry raw ANSI
540
927
  * bytes (alt-screen, key bindings, colors); attach goes through
@@ -573,8 +960,38 @@ interface SessionDescriptor {
573
960
  * `/sessions/agent` to spin up a fresh ACP runtime. Undefined for
574
961
  * pty/command kinds. */
575
962
  adapterSlug?: string;
963
+ /**
964
+ * AIP-45 mode the session was spawned with (`AgentCliStartOptions.config.
965
+ * mode` — e.g. claude-code's `plan`/`accept-edits`, a gateway preset mode
966
+ * like `moonshot`). Undefined for the adapter's default/native mode.
967
+ * Recorded so a client can tell whether a candidate model switch stays
968
+ * within THIS mode (live-switchable via `setModel`) or needs a different
969
+ * one (`AgentCliModelEntry.mode` on the target) — a mode change is
970
+ * spawn-time env/argv rewiring (e.g. `ANTHROPIC_BASE_URL`), which
971
+ * `POST /sessions/:id/model` cannot perform on a live process; that case
972
+ * is surfaced to clients as `requires-restart`, never silently attempted.
973
+ */
974
+ mode?: string;
576
975
  /** The model the session was requested to run (echoed back at spawn). */
577
976
  model?: string;
977
+ /** Reasoning / compute budget the session resolved to (SPEC §3.1 axis 2).
978
+ * A LIVE-switchable axis; echoed here so the effort chip re-opens on it. */
979
+ effort?: EffortLevel;
980
+ /** What the agent may DO (SPEC §3.1 axis 5) — an agentproto-canonical
981
+ * posture or a raw `{ harnessModeId }` sourced from the harness's ACP mode
982
+ * registry (SPEC §3.4a). */
983
+ posture?: Posture;
984
+ /** Endpoint / gateway rail (SPEC §3.1 axis 4). `baseUrl` is carried only
985
+ * for a custom gateway the catalog can't resolve; `access` is downstream
986
+ * of this axis (SPEC §1c). */
987
+ route?: RouteSpec;
988
+ /** What enters context (SPEC §3.1 axis 5b) — `"lean"` drops bundled skills. */
989
+ contextProfile?: ContextProfile;
990
+ /** The `access` axis echo (SPEC §3.6/§3.7): the NAMED auth profile attached
991
+ * to the session, so the access chip can name the wallet. Distinct from the
992
+ * `auth` fingerprint echo below, which stays as-is — see
993
+ * {@link SessionAccessProfileEcho}. NEVER the credential. */
994
+ accessProfile?: SessionAccessProfileEcho;
578
995
  /**
579
996
  * Deterministic billing-auth mode + a non-secret credential fingerprint,
580
997
  * recorded at spawn time for adapters that resolved an explicit
@@ -710,6 +1127,26 @@ interface SessionDescriptor {
710
1127
  * matching command session; absent otherwise (including for legacy
711
1128
  * rows persisted before this field existed). */
712
1129
  priorCommandSessionId?: string;
1130
+ /** Id of the prior session this one continues from — set when this
1131
+ * session was spawned by `session_restart` (or the cron scheduler's
1132
+ * `prompt-session` action), even when the resume attempt itself
1133
+ * couldn't establish continuity: a fresh fallback spawn (adapter
1134
+ * rejected the resume id as "not found") is still "restarted from"
1135
+ * the prior session, just without conversation history carried over.
1136
+ * Absent for a session spawned directly (not via restart). Persisted
1137
+ * on the STORED descriptor (not just grafted onto the restart
1138
+ * result's JSON, as it used to be) so it survives a `list()`/`get()`
1139
+ * poll refresh and a daemon restart — see `resumeVia` for how the
1140
+ * continuity was (or wasn't) established, and the transcript panel's
1141
+ * chain-walk (vscode package) for the read side. */
1142
+ resumedFrom?: string;
1143
+ /** Human-readable resume path used to arrive at `resumedFrom` — e.g.
1144
+ * "resumed via claude --resume" (provider-native PTY resume) or
1145
+ * "resumed via ACP" (adapter-level resume), or `""` when no
1146
+ * continuity was established (a fresh fallback spawn — see
1147
+ * `resumedFrom`). Only meaningful alongside `resumedFrom`; absent
1148
+ * (never `""`) for a session that wasn't spawned via restart. */
1149
+ resumeVia?: string;
713
1150
  /** Adapter id that drives this session (e.g. "camofox", "bureau"). */
714
1151
  browserAdapterId?: string;
715
1152
  /** Port the browser service listens on. */
@@ -874,6 +1311,67 @@ interface SessionsRegistry {
874
1311
  interruptSession(id: string): Promise<{
875
1312
  wasBusy: boolean;
876
1313
  }>;
1314
+ /**
1315
+ * Switch the model on a LIVE agent-cli session without restarting it —
1316
+ * the mid-session counterpart to `spawnAgent`'s `input.model` (which
1317
+ * only applies at spawn time). Delegates to the driver session's own
1318
+ * `setModel` (see `AgentSessionLike.setModel`), which dispatches on the
1319
+ * adapter's `models.apply` strategy (`config`/`command`/`arg`) and never
1320
+ * throws on a rejected switch.
1321
+ *
1322
+ * On `{applied:true}`, updates `SessionDescriptor.model` so
1323
+ * `session_list`/SSE reflect the switch and emits a
1324
+ * `session:config-changed {axis:"model"}` event on the session event bus
1325
+ * (plus a back-compat `session:model-changed` alias). On `{applied:false}`
1326
+ * the descriptor and event bus are untouched — nothing changed, so nothing
1327
+ * to announce.
1328
+ *
1329
+ * Throws (not a structured result) for the two "this request doesn't
1330
+ * even make sense" cases: an unknown session id, or a session that
1331
+ * isn't an agent-cli kind (or whose driver session predates `setModel`
1332
+ * entirely) — both are caller errors, not an adapter's refusal.
1333
+ */
1334
+ setModel(id: string, modelId: string): Promise<SetSessionModelResult>;
1335
+ /**
1336
+ * Announce a single `SessionConfig` axis change on the session event bus
1337
+ * (SPEC step 6). `setModel` emits its `session:config-changed` event inline,
1338
+ * but restart-with-override (`restartAgentSession`, session-restart-core.ts)
1339
+ * lives OUTSIDE the registry — it re-resolves auth and re-spawns a fresh
1340
+ * session — so it announces each changed axis (access/route/posture/…) of the
1341
+ * new session through this one method rather than reaching into the private
1342
+ * event bus. No-op when the registry was constructed without a `sessionEvents`
1343
+ * bus (the emit is best-effort observability, never load-bearing). The caller
1344
+ * builds the fully-typed event (axis + value already reflected on the new
1345
+ * descriptor); the registry only forwards it. */
1346
+ emitConfigChanged(ev: SessionConfigChangedEvent): void;
1347
+ /**
1348
+ * Switch the reasoning/compute budget (effort) on a LIVE agent-cli session
1349
+ * without restarting it — the live-effort verb (SPEC §4.2, build step 5),
1350
+ * `POST /sessions/:id/effort` + `agent_set_effort`. Delegates to the driver
1351
+ * session's `setEffort` (ACP `set_config_option(configId:"effort")`); effort
1352
+ * is model-dependent (SPEC §3.9), so a label the current model rejects is a
1353
+ * soft `{applied:false, reason}` (SPEC risk R7), never thrown.
1354
+ *
1355
+ * On `{applied:true}` updates `SessionDescriptor.effort` and emits
1356
+ * `session:config-changed {axis:"effort"}`; on `{applied:false}` the
1357
+ * descriptor and bus are untouched. Throws (caller error, not an adapter
1358
+ * refusal) for an unknown session id or a non-agent-cli session — same
1359
+ * contract as `setModel`.
1360
+ */
1361
+ setEffort(id: string, effort: string): Promise<SetSessionEffortResult>;
1362
+ /**
1363
+ * Switch the posture on a LIVE agent-cli session (SPEC §4.2, build step 5),
1364
+ * `POST /sessions/:id/posture` + `agent_set_posture`. When the requested
1365
+ * posture maps to a NATIVE advertised harness mode (`resolvePosture` →
1366
+ * `native`), it's switched live via the driver's `setSessionMode`
1367
+ * (`applied:true`, descriptor + `session:config-changed {axis:"posture"}`
1368
+ * emitted). When there is NO native mode (prompt-injected / env-applied /
1369
+ * a raw mode the session no longer advertises), it is NOT forced live — it
1370
+ * resolves `{applied:false, reason:"requires-restart"}` so the caller routes
1371
+ * it through restart-with-override (step 6, not implemented here). Throws for
1372
+ * an unknown session id or a non-agent-cli session, same as `setModel`.
1373
+ */
1374
+ setPosture(id: string, posture: Posture): Promise<SetSessionPostureResult>;
877
1375
  /** Stamp `lastActivityAt` on a live agent-cli session's descriptor
878
1376
  * and schedule a debounced persist. Called from the `onActivity`
879
1377
  * callback threaded down through the driver → ACP client, which
@@ -881,8 +1379,49 @@ interface SessionsRegistry {
881
1379
  * output) — see `SessionDescriptor.lastActivityAt`. No-op when the
882
1380
  * id is unknown (session already forgotten). */
883
1381
  pulseActivity(id: string): void;
884
- list(): SessionDescriptor[];
1382
+ /** Every non-archived session, newest `startedAt` first — the daemon's
1383
+ * canonical lister (`session_list`, `GET /sessions`, panels, subtree
1384
+ * scoping). Archived sessions are excluded UNLESS `includeArchived` is
1385
+ * true — the default keeps a housekeeping flag from becoming a second,
1386
+ * silent filter every caller has to know about, while
1387
+ * `{ includeArchived: true }` is there for `session_list`'s own opt-in
1388
+ * and for any subtree/authorization computation (`collectSubtree`) that
1389
+ * needs the FULL parent→child graph to stay connected — a subtree BFS
1390
+ * fed the filtered list would silently orphan the non-archived
1391
+ * descendants of an archived ancestor, since each edge is keyed off the
1392
+ * CHILD's own record. `get()`/`findByIdOrName()` are unaffected by this
1393
+ * flag entirely — a transcript stays directly openable by id no matter
1394
+ * how it's archived. */
1395
+ list(opts?: {
1396
+ includeArchived?: boolean;
1397
+ }): SessionDescriptor[];
885
1398
  get(id: string): SessionDescriptor | undefined;
1399
+ /** Archive a TERMINAL-status session (exited/killed/error) — sets
1400
+ * `archived: true` and persists. Pure housekeeping: hides the row from
1401
+ * `list()`'s default view, nothing else. Refuses (throws) a still-alive
1402
+ * session (running/starting) — archiving one would hide it from the
1403
+ * daemon's own default view while it keeps working unattended, which is
1404
+ * a worse foot-gun than the flag is trying to solve. Idempotent: already
1405
+ * archived is a no-op success. Throws when the id is unknown. */
1406
+ archiveSession(id: string): SessionDescriptor;
1407
+ /** Unarchive — the inverse, no status guard (an archived session was
1408
+ * terminal when archived, and archiving never touched daemon state, so
1409
+ * there is nothing to re-validate). Throws only when the id is
1410
+ * unknown. */
1411
+ unarchiveSession(id: string): SessionDescriptor;
1412
+ /** Set or clear a session's user-facing name (`PATCH /sessions/:id`, the
1413
+ * `session_rename` MCP verb). Each of `title`/`label`: a non-empty string
1414
+ * sets that field (trimmed, capped to the derivation's `MAX_LENGTH` by
1415
+ * code point); an empty/whitespace-only string or `null` CLEARS it (the
1416
+ * UI reverts to the derived title / friendly fallback); `undefined`
1417
+ * leaves it untouched. Persists via the same `schedulePersist` every
1418
+ * descriptor mutation uses, and emits `session:renamed` so live UIs
1419
+ * repaint. Pure display state — never touches the live agent. Throws when
1420
+ * the id is unknown. */
1421
+ renameSession(id: string, patch: {
1422
+ title?: string | null;
1423
+ label?: string | null;
1424
+ }): SessionDescriptor;
886
1425
  /** Subscribe to a session's output. Returns an unsubscribe fn.
887
1426
  * Initial backfill: synchronously invokes `onLine` once for each
888
1427
  * line currently in the ring buffer so attaches show context. */
@@ -1009,11 +1548,35 @@ interface SpawnAgentInput {
1009
1548
  depth?: number;
1010
1549
  /** Requested model id — recorded on the descriptor for display + echo. */
1011
1550
  model?: string;
1551
+ /** AIP-45 mode the session was spawned with — recorded onto
1552
+ * {@link SessionDescriptor.mode}. See that field's doc for why a client
1553
+ * needs it (mode-mismatch detection for mid-session model switching). */
1554
+ mode?: string;
1012
1555
  /** Resolved auth echo (mode + fingerprint + provider/source/setEnv) —
1013
1556
  * recorded verbatim onto {@link SessionDescriptor.auth}. See that field's
1014
1557
  * doc for the full contract; the caller (`session-spawn.ts`) computes this
1015
1558
  * via the billing-auth resolver, never passing the raw credential here. */
1016
1559
  auth?: SessionAuthEcho;
1560
+ /** Decomposed config-axis echoes (SPEC §3.7) — recorded verbatim onto the
1561
+ * matching {@link SessionDescriptor} fields, the same optional-spread way
1562
+ * `model`/`mode`/`auth` are. All optional; a caller that resolved an axis
1563
+ * passes its echo so the descriptor round-trips it and the picker re-opens
1564
+ * on it. See each descriptor field's doc for the axis contract. */
1565
+ effort?: EffortLevel;
1566
+ posture?: Posture;
1567
+ route?: RouteSpec;
1568
+ contextProfile?: ContextProfile;
1569
+ /** Named auth-profile echo for the `access` axis (SPEC §3.6). Non-secret —
1570
+ * see {@link SessionAccessProfileEcho}. */
1571
+ accessProfile?: SessionAccessProfileEcho;
1572
+ /** Prior session id this spawn continues from — set by `restartAgentSession`
1573
+ * (session-restart-core.ts) when this is a restart, recorded verbatim onto
1574
+ * {@link SessionDescriptor.resumedFrom}. Absent for a direct (non-restart)
1575
+ * spawn. */
1576
+ resumedFrom?: string;
1577
+ /** Human-readable resume path, recorded onto {@link SessionDescriptor.resumeVia}.
1578
+ * Threaded through alongside `resumedFrom` — see that field's doc. */
1579
+ resumeVia?: string;
1017
1580
  /** Hard ceiling on cumulative session cost (USD). When set and the
1018
1581
  * adapter's usage reader reports a higher cost at a turn-end, the session
1019
1582
  * is stopped (best-effort, turn-granular — caps continuation, can't abort
@@ -1087,6 +1650,17 @@ interface SpawnPtyInput {
1087
1650
  * collide with an existing session's name. */
1088
1651
  name?: string;
1089
1652
  label?: string;
1653
+ /** Parent attribution + depth — same semantics as `SpawnAgentInput`
1654
+ * (orchestrator WP4): set when the spawn came through a scoped
1655
+ * sub-gateway so `session_tree` shows the PTY under its spawner. */
1656
+ parentSessionId?: string;
1657
+ depth?: number;
1658
+ /** Restart lineage — same semantics as `SpawnAgentInput.resumedFrom` /
1659
+ * `resumeVia`, recorded onto {@link SessionDescriptor.resumedFrom} /
1660
+ * {@link SessionDescriptor.resumeVia}. Set by `session_restart` for the
1661
+ * pty-native/pty-plain branches (session-tools.ts). */
1662
+ resumedFrom?: string;
1663
+ resumeVia?: string;
1090
1664
  }
1091
1665
  interface RecordCommandInput {
1092
1666
  workspaceSlug: string;
@@ -1568,6 +2142,95 @@ interface WebhookNotifier {
1568
2142
  onSessionEvent(ev: SessionEvent): void;
1569
2143
  }
1570
2144
 
2145
+ /**
2146
+ * Shared types for pluggable sandbox providers.
2147
+ *
2148
+ * The sandbox family is a `@agentproto/provider-kit` consumer, same shape as
2149
+ * the tunnel family (`remote-providers/types.ts`): {@link SandboxProviderHandle}
2150
+ * extends the kit's generic `AdapterHandle` (slug/name/version/description/
2151
+ * requiresSetup/check) with the AIP-36 capability namespace and the concrete
2152
+ * `@agentproto/sandbox` `SandboxProvider` (`boot()`) the handle wraps.
2153
+ */
2154
+
2155
+ /**
2156
+ * Declared capabilities of a sandbox provider — pure metadata, surfaced in
2157
+ * `list_sandbox_providers`. Never carries secrets. Namespace mirrors AIP-36
2158
+ * SANDBOX.md (`network.egress`, `mounts`, `lifecycle.pause_after_idle`,
2159
+ * `read_only`, `limits.timeout_ms`).
2160
+ */
2161
+ interface SandboxProviderCapabilities {
2162
+ /** Sandbox can be given a network egress allowlist (AIP-36 `network.egress`). */
2163
+ networkEgress: boolean;
2164
+ /** Sandbox supports mounting external filesystems (AIP-36 `mounts`). */
2165
+ mounts: boolean;
2166
+ /** Sandbox can be paused (not just killed) between turns (AIP-36 `lifecycle.pause_after_idle`). */
2167
+ lifecyclePause: boolean;
2168
+ /** Sandbox can be started read-only (AIP-36 `read_only`). */
2169
+ readOnly: boolean;
2170
+ /** Hard cap on `limits.timeout_ms`, when the provider enforces one. */
2171
+ maxTimeoutMs?: number;
2172
+ }
2173
+ /**
2174
+ * A sandbox provider as an adapter-kit handle. Rides on the kit's generic
2175
+ * {@link AdapterHandle} and adds the sandbox-specific `capabilities` plus
2176
+ * the `@agentproto/sandbox` `SandboxProvider` the handle wraps — the thing
2177
+ * `createSandboxAgentSessionHost` actually boots.
2178
+ */
2179
+ interface SandboxProviderHandle extends AdapterHandle {
2180
+ readonly provider: SandboxProvider;
2181
+ readonly capabilities: SandboxProviderCapabilities;
2182
+ /**
2183
+ * Credential fields this provider accepts via `setup_sandbox_provider`
2184
+ * (e.g. e2b's `apiKey`). Omit (or empty) when the provider needs no
2185
+ * credentials (e.g. `local`).
2186
+ */
2187
+ readonly setupFields?: readonly SetupField[];
2188
+ }
2189
+
2190
+ /**
2191
+ * Sandbox family on top of `@agentproto/provider-kit` — mirrors
2192
+ * `tunnel-adapters.ts`. This module is the entire bridge between the
2193
+ * generic kit and `@agentproto/sandbox`'s `SandboxProvider` concept: it
2194
+ * contributes nothing the kit already owns (catalog/status/creds/ledger/
2195
+ * list/MCP-tool plumbing); it only supplies the sandbox-family `TInfo`
2196
+ * (`SandboxAdapterInfo`), the static `SANDBOX_CATALOG`, and the resolver
2197
+ * that maps a catalog slug to a concrete {@link SandboxProviderHandle}
2198
+ * (`./sandbox-providers/registry.js`).
2199
+ *
2200
+ * Kit primitives used:
2201
+ * - `makeCredsStore` → per-slug 0600 creds under `~/.agentproto/sandbox-creds/`
2202
+ * - `makeSetupLedger` → `~/.agentproto/setup/<slug>.json`
2203
+ * - `makeAdapterResolver` → wraps the throwing `load` into null-on-miss
2204
+ * - `makeAdapterLister` → catalog → status-classified `AdapterEntry[]`
2205
+ * - `makeListTool` → registers `list_sandbox_providers`
2206
+ * - `makeSetupTool` → registers `setup_sandbox_provider` (multi-field
2207
+ * form: e2b's `apiKey`, sensitive)
2208
+ *
2209
+ * This is pure additive plumbing (introspection + setup) — it does NOT wire
2210
+ * a `sandbox` field into `agent_start`. That lands in a follow-up PR; the
2211
+ * `resolveSandboxProvider`/`listSandboxProviders` overrides below exist now
2212
+ * so that later wiring can inject the same resolver/lister this module
2213
+ * builds by default, mirroring `resolveAgentAdapter`/`listAgentAdapters`.
2214
+ *
2215
+ * Security: `toSandboxInfo` exposes only `capabilities` — never a cred
2216
+ * value (Appendix B). The setup tool's fields are marked SENSITIVE and the
2217
+ * result NEVER echoes any field value back.
2218
+ */
2219
+
2220
+ /**
2221
+ * Family descriptor (`TInfo`). Pure metadata surfaced in
2222
+ * `list_sandbox_providers`. The kit's `AdapterEntry` already carries
2223
+ * slug/name/description/status/version, so the only sandbox-specific field
2224
+ * is the declared capability set. NEVER carries a cred value.
2225
+ */
2226
+ interface SandboxAdapterInfo {
2227
+ capabilities: SandboxProviderCapabilities;
2228
+ }
2229
+ /** Resolve a sandbox provider slug to a handle, or null when unavailable. */
2230
+ type SandboxProviderResolver = AdapterResolver<SandboxProviderHandle>;
2231
+ /** List every sandbox provider with its live status + capabilities. */
2232
+ type SandboxProviderLister = AdapterLister<SandboxAdapterInfo>;
2233
+
1571
2234
  /**
1572
2235
  * Cursor-based ring buffer for session events. Bridges the in-process
1573
2236
  * SessionEventBus (push) to the session_events_poll MCP tool (pull).
@@ -2133,95 +2796,6 @@ type OrchestratorInjector = (opts?: {
2133
2796
  */
2134
2797
  declare function createOrchestratorInjector(deps: OrchestratorInjectorDeps): OrchestratorInjector;
2135
2798
 
2136
- /**
2137
- * Shared types for pluggable sandbox providers.
2138
- *
2139
- * The sandbox family is a `@agentproto/provider-kit` consumer, same shape as
2140
- * the tunnel family (`remote-providers/types.ts`): {@link SandboxProviderHandle}
2141
- * extends the kit's generic `AdapterHandle` (slug/name/version/description/
2142
- * requiresSetup/check) with the AIP-36 capability namespace and the concrete
2143
- * `@agentproto/sandbox` `SandboxProvider` (`boot()`) the handle wraps.
2144
- */
2145
-
2146
- /**
2147
- * Declared capabilities of a sandbox provider — pure metadata, surfaced in
2148
- * `list_sandbox_providers`. Never carries secrets. Namespace mirrors AIP-36
2149
- * SANDBOX.md (`network.egress`, `mounts`, `lifecycle.pause_after_idle`,
2150
- * `read_only`, `limits.timeout_ms`).
2151
- */
2152
- interface SandboxProviderCapabilities {
2153
- /** Sandbox can be given a network egress allowlist (AIP-36 `network.egress`). */
2154
- networkEgress: boolean;
2155
- /** Sandbox supports mounting external filesystems (AIP-36 `mounts`). */
2156
- mounts: boolean;
2157
- /** Sandbox can be paused (not just killed) between turns (AIP-36 `lifecycle.pause_after_idle`). */
2158
- lifecyclePause: boolean;
2159
- /** Sandbox can be started read-only (AIP-36 `read_only`). */
2160
- readOnly: boolean;
2161
- /** Hard cap on `limits.timeout_ms`, when the provider enforces one. */
2162
- maxTimeoutMs?: number;
2163
- }
2164
- /**
2165
- * A sandbox provider as an adapter-kit handle. Rides on the kit's generic
2166
- * {@link AdapterHandle} and adds the sandbox-specific `capabilities` plus
2167
- * the `@agentproto/sandbox` `SandboxProvider` the handle wraps — the thing
2168
- * `createSandboxAgentSessionHost` actually boots.
2169
- */
2170
- interface SandboxProviderHandle extends AdapterHandle {
2171
- readonly provider: SandboxProvider;
2172
- readonly capabilities: SandboxProviderCapabilities;
2173
- /**
2174
- * Credential fields this provider accepts via `setup_sandbox_provider`
2175
- * (e.g. e2b's `apiKey`). Omit (or empty) when the provider needs no
2176
- * credentials (e.g. `local`).
2177
- */
2178
- readonly setupFields?: readonly SetupField[];
2179
- }
2180
-
2181
- /**
2182
- * Sandbox family on top of `@agentproto/provider-kit` — mirrors
2183
- * `tunnel-adapters.ts`. This module is the entire bridge between the
2184
- * generic kit and `@agentproto/sandbox`'s `SandboxProvider` concept: it
2185
- * contributes nothing the kit already owns (catalog/status/creds/ledger/
2186
- * list/MCP-tool plumbing); it only supplies the sandbox-family `TInfo`
2187
- * (`SandboxAdapterInfo`), the static `SANDBOX_CATALOG`, and the resolver
2188
- * that maps a catalog slug to a concrete {@link SandboxProviderHandle}
2189
- * (`./sandbox-providers/registry.js`).
2190
- *
2191
- * Kit primitives used:
2192
- * - `makeCredsStore` → per-slug 0600 creds under `~/.agentproto/sandbox-creds/`
2193
- * - `makeSetupLedger` → `~/.agentproto/setup/<slug>.json`
2194
- * - `makeAdapterResolver` → wraps the throwing `load` into null-on-miss
2195
- * - `makeAdapterLister` → catalog → status-classified `AdapterEntry[]`
2196
- * - `makeListTool` → registers `list_sandbox_providers`
2197
- * - `makeSetupTool` → registers `setup_sandbox_provider` (multi-field
2198
- * form: e2b's `apiKey`, sensitive)
2199
- *
2200
- * This is pure additive plumbing (introspection + setup) — it does NOT wire
2201
- * a `sandbox` field into `agent_start`. That lands in a follow-up PR; the
2202
- * `resolveSandboxProvider`/`listSandboxProviders` overrides below exist now
2203
- * so that later wiring can inject the same resolver/lister this module
2204
- * builds by default, mirroring `resolveAgentAdapter`/`listAgentAdapters`.
2205
- *
2206
- * Security: `toSandboxInfo` exposes only `capabilities` — never a cred
2207
- * value (Appendix B). The setup tool's fields are marked SENSITIVE and the
2208
- * result NEVER echoes any field value back.
2209
- */
2210
-
2211
- /**
2212
- * Family descriptor (`TInfo`). Pure metadata surfaced in
2213
- * `list_sandbox_providers`. The kit's `AdapterEntry` already carries
2214
- * slug/name/description/status/version, so the only sandbox-specific field
2215
- * is the declared capability set. NEVER carries a cred value.
2216
- */
2217
- interface SandboxAdapterInfo {
2218
- capabilities: SandboxProviderCapabilities;
2219
- }
2220
- /** Resolve a sandbox provider slug to a handle, or null when unavailable. */
2221
- type SandboxProviderResolver = AdapterResolver<SandboxProviderHandle>;
2222
- /** List every sandbox provider with its live status + capabilities. */
2223
- type SandboxProviderLister = AdapterLister<SandboxAdapterInfo>;
2224
-
2225
2799
  /**
2226
2800
  * Policy layer for `agent_start.worktree` — the config-driven decision of
2227
2801
  * WHETHER to isolate a spawn into its own git worktree, kept deliberately
@@ -2343,6 +2917,108 @@ declare function loadWorktreeIsolation(loadCfg?: () => Promise<{
2343
2917
  };
2344
2918
  }>): Promise<WorktreeIsolationMode>;
2345
2919
 
2920
+ /**
2921
+ * Read-only catalog/vendor endpoint (`agentproto-session-config-axes`
2922
+ * SPEC §5) — `GET /catalog/models` + `catalog_models` MCP tool wire into
2923
+ * {@link buildCatalogModels}, the pure join this module owns.
2924
+ *
2925
+ * Reuses three already-shipped pieces instead of rebuilding them:
2926
+ * - the vendor/product/route model + router widening (OpenRouter/
2927
+ * Requesty/HuggingFace) from `@agentproto/model-catalog/route-identity`
2928
+ * (`resolveLlmModelRoute`, `route-identity/index.ts:396-511`) — this is
2929
+ * what keeps the catalog from being capped at any one adapter's
2930
+ * `models.allowed` list (SPEC §5.1);
2931
+ * - the profile eligibility predicate shipped in #470
2932
+ * (`@agentproto/auth`'s `eligibleProfiles`, `packages/auth/src/
2933
+ * eligibility.ts:81-89`) for the profile-aware `runnable` flag (SPEC
2934
+ * §5.3) — the old bare `hasKey` check (`packages/cli/src/commands/
2935
+ * models.ts:113-117`) is the degenerate one-profile-per-provider case
2936
+ * this predicate subsumes;
2937
+ * - `AdapterAuthDescriptor` (`spawn-defaults.ts:226`), the SAME
2938
+ * provider/authSubscription projection `resolveAuthSpec` reads, as the
2939
+ * source for which auth methods an adapter can present on its direct
2940
+ * route (SPEC §3.4's derivable replacement for a hand-maintained
2941
+ * `authSubscription` boolean).
2942
+ *
2943
+ * A gateway/router route (anything where the resolved route differs from
2944
+ * the model's vendor — `openrouter`, `requesty`, `huggingface`, or an
2945
+ * adapter's own gateway mode id like `moonshot`) always bills against the
2946
+ * route's own id and is always reached with an api-key credential — never
2947
+ * oauth-bearer, since no third-party gateway has an Anthropic-style
2948
+ * subscription bearer path (SPEC §1c: "a moonshot profile, not the Claude
2949
+ * sub"). That structural rule is what lets this module compute
2950
+ * `runnable`/`eligibleProfiles` for the widened, non-curated rows without
2951
+ * per-adapter gateway-vendor tables.
2952
+ */
2953
+
2954
+ /** One model entry as declared in an adapter's `models.allowed`
2955
+ * (`AdapterModelInfo`, `packages/cli/src/registry/resolve.ts:134-142`) —
2956
+ * the subset this module needs. */
2957
+ interface CatalogAdapterModelInput {
2958
+ /** Model id exactly as declared — bare (`"claude-opus-4-8"`) or
2959
+ * `vendor/product` form. */
2960
+ id: string;
2961
+ /** The adapter mode id that must be applied to reach this model on a
2962
+ * non-direct route (`AdapterModelInfo.mode`) — e.g. `"moonshot"`.
2963
+ * Undefined ⇒ direct route (the model's own vendor). */
2964
+ mode?: string;
2965
+ }
2966
+ /** One installed adapter's contribution to the catalog. */
2967
+ interface CatalogAdapterInput {
2968
+ slug: string;
2969
+ models: readonly CatalogAdapterModelInput[];
2970
+ /** This adapter's billing-auth capability on its DIRECT route — the same
2971
+ * projection `resolveAuthSpec` reads (`spawn-defaults.ts:226`). Omitted
2972
+ * ⇒ the adapter presents no auth method, so rows it curates are
2973
+ * discoverable but never runnable through it alone. */
2974
+ authDescriptor?: AdapterAuthDescriptor;
2975
+ }
2976
+ interface CatalogModelsQuery {
2977
+ /** Keep only routes reachable via this adapter slug. */
2978
+ adapter?: string;
2979
+ /** Keep only this vendor's entry. */
2980
+ vendor?: string;
2981
+ /** Keep only routes with this route id. */
2982
+ route?: string;
2983
+ /** Drop every route with `runnable: false`. */
2984
+ runnableOnly?: boolean;
2985
+ }
2986
+ interface CatalogPricing {
2987
+ inPer1M: number;
2988
+ outPer1M: number;
2989
+ }
2990
+ interface CatalogRoute {
2991
+ route: string;
2992
+ ref: string;
2993
+ baseUrl: string | null;
2994
+ pricing: CatalogPricing | null;
2995
+ runnable: boolean;
2996
+ eligibleProfiles: string[];
2997
+ adapterModes: string[];
2998
+ adapters: string[];
2999
+ curated: boolean;
3000
+ }
3001
+ interface CatalogProduct {
3002
+ product: string;
3003
+ routes: CatalogRoute[];
3004
+ }
3005
+ interface CatalogVendor {
3006
+ vendor: string;
3007
+ products: CatalogProduct[];
3008
+ }
3009
+ interface CatalogModelsResponse {
3010
+ vendors: CatalogVendor[];
3011
+ }
3012
+ interface BuildCatalogModelsInput {
3013
+ adapters: readonly CatalogAdapterInput[];
3014
+ profiles: readonly AuthProfile[];
3015
+ query?: CatalogModelsQuery;
3016
+ }
3017
+ /** The pure join (SPEC §5): adapter-declared models + router widening +
3018
+ * the #470 eligibility predicate → the vendor/product/route tree. No I/O —
3019
+ * callers (the HTTP route / MCP tool) own loading adapters + profiles. */
3020
+ declare function buildCatalogModels(input: BuildCatalogModelsInput): CatalogModelsResponse;
3021
+
2346
3022
  /**
2347
3023
  * Pluggable adapter resolver — keeps the runtime package free of any
2348
3024
  * @agentproto/cli dep. The host (cli `serve`, playground, embedding
@@ -2482,6 +3158,13 @@ interface AdapterListEntry {
2482
3158
  modes: AdapterListMode[];
2483
3159
  }
2484
3160
  type AgentAdapterLister = () => Promise<AdapterListEntry[]>;
3161
+ /** Loads the read-only vendor/product/route catalog (SPEC §5) for
3162
+ * `GET /catalog/models` + the `catalog_models` MCP tool. A host wires this
3163
+ * from `buildCatalogModels` (`catalog-models.ts`) fed by its installed
3164
+ * adapters + `@agentproto/auth`'s `listAuthProfiles()` — the query params
3165
+ * are forwarded verbatim from the request. Omitted ⇒ the route/tool
3166
+ * report "not enabled" (same convention as `listAgentAdapters`). */
3167
+ type CatalogModelsLister = (query: CatalogModelsQuery) => Promise<CatalogModelsResponse>;
2485
3168
  interface AuthOptions {
2486
3169
  mode: "none" | "bearer";
2487
3170
  token?: string;
@@ -2756,6 +3439,131 @@ declare function declaredPresetToProviderPreset(decl: DeclaredAdapterPreset): Pr
2756
3439
  */
2757
3440
  declare function listPresets(env?: Record<string, string | undefined>, adapterPresets?: readonly DeclaredAdapterPreset[]): AdapterEntry<PresetInfo>[];
2758
3441
 
3442
+ /**
3443
+ * Canonical-posture layer (SPEC §3.4a, build step 2c, out-of-repo design doc
3444
+ * `agentproto-session-config-axes/SPEC.md`). Pure: a map + preambles + a
3445
+ * resolution helper, no I/O, no daemon wiring. It is the piece that makes the
3446
+ * agentproto-canonical posture vocabulary (`CanonicalPosture` from
3447
+ * `./session-config.js`) portable across harnesses:
3448
+ *
3449
+ * 1. **Native enforcement.** A canonical posture is resolved to a *harness*
3450
+ * `SessionModeId` when the session's advertised `availableModes`
3451
+ * (`SessionModeState.availableModes`, surfaced read-only by the ACP
3452
+ * capability layer landed in #482, `packages/acp/src/client/index.ts`) has
3453
+ * an equivalent. That mode is switched live via `setSessionMode` (step 5)
3454
+ * or applied on restart (step 6) — a real permission boundary.
3455
+ * 2. **Prompt-injection fallback.** When no advertised mode matches, the
3456
+ * posture is honoured as an injected system-prompt PREAMBLE — advisory,
3457
+ * NOT a permission boundary (SPEC risk Rw). This module returns the
3458
+ * preamble text; steps 5/6 ride it onto the system prompt at spawn.
3459
+ *
3460
+ * This module deliberately does NOT implement the live-posture verb (step 5) or
3461
+ * the posture restart-override (step 6) — it only tells them WHICH of the two
3462
+ * apply-paths a given (posture × session) resolves to, and supplies the
3463
+ * preamble for the fallback path.
3464
+ *
3465
+ * Read-surface dependency: `availableModes` comes from the #482 ACP capability
3466
+ * read surface (`AcpClientSession.availableModes` →
3467
+ * `AgentCliRuntimeSession.availableModes`) — this module consumes it, never
3468
+ * reimplements it.
3469
+ */
3470
+
3471
+ /**
3472
+ * Per-posture system-prompt preamble — the prompt-injection fallback applied
3473
+ * when a canonical posture has NO native advertised mode on the current harness
3474
+ * (SPEC §3.4a). This is ADVISORY text the model can ignore, never a permission
3475
+ * boundary (SPEC risk Rw) — a consumer must present a prompt-enforced posture
3476
+ * as "advisory", never imply it sandboxes tools.
3477
+ *
3478
+ * `"default"` has no preamble: it is the neutral posture (no constraint to
3479
+ * announce), so it resolves to a no-op rather than an injected string when the
3480
+ * harness advertises no native `default` mode.
3481
+ */
3482
+ declare const POSTURE_PREAMBLES: Readonly<Record<Exclude<CanonicalPosture, "default">, string>>;
3483
+ /**
3484
+ * agentproto-canonical posture → the harness `SessionModeId`s that mean the
3485
+ * same thing. This is the "canonical posture ↔ advertised ACP mode id" map
3486
+ * (SPEC §3.4a): the daemon resolves a portable posture onto whichever native
3487
+ * mode a given harness happens to advertise, since harnesses spell the same
3488
+ * concept differently — claude-code's ACP wrapper uses `acceptEdits` /
3489
+ * `bypassPermissions` (`adapters/claude-code/src/index.ts:216,223`), the
3490
+ * manifest posture ids use `accept-edits` / `bypass-permissions`, codex uses
3491
+ * `full-access`, opencode uses `build`, etc.
3492
+ *
3493
+ * Matching is case- and separator-insensitive (see {@link normalizeModeId}), so
3494
+ * ONE readable spelling here covers every casing/hyphenation a harness might
3495
+ * advertise — `"accept-edits"` already matches claude-code's `acceptEdits`, so
3496
+ * both spellings need not be listed. Aliases are disjoint across postures — no
3497
+ * advertised id maps to two canonical postures — so {@link canonicalForModeId}
3498
+ * is unambiguous. Kept consistent with the legacy `mode`-id normalization in
3499
+ * `session-config.ts` (`POSTURE_MODE_VALUES`).
3500
+ */
3501
+ declare const POSTURE_NATIVE_ALIASES: Readonly<Record<CanonicalPosture, readonly string[]>>;
3502
+ /**
3503
+ * Normalize a mode id for matching: lowercase and strip every non-alphanumeric
3504
+ * character, so `"acceptEdits"`, `"accept-edits"`, and `"Accept_Edits"` all
3505
+ * collapse to `"acceptedits"`. Lets one readable alias in
3506
+ * {@link POSTURE_NATIVE_ALIASES} cover any casing/separator a harness advertises.
3507
+ */
3508
+ declare function normalizeModeId(id: string): string;
3509
+ /**
3510
+ * Inverse of the alias map: which canonical posture (if any) a harness mode id
3511
+ * normalizes to. `undefined` for a harness-specific mode the canonical
3512
+ * vocabulary doesn't name (e.g. opencode's `architect`) — such a mode is still
3513
+ * offerable as a raw `{ harnessModeId }` posture, it just has no portable name.
3514
+ */
3515
+ declare function canonicalForModeId(modeId: string): CanonicalPosture | undefined;
3516
+ /**
3517
+ * Find the advertised harness mode that natively enforces `posture`, or
3518
+ * `undefined` if none does.
3519
+ *
3520
+ * - A `CanonicalPosture` matches an advertised mode whose id normalizes to one
3521
+ * of that posture's aliases.
3522
+ * - A raw `{ harnessModeId }` matches an advertised mode with the (normalized)
3523
+ * same id — it's already a native id, we only confirm the session still
3524
+ * advertises it.
3525
+ */
3526
+ declare function findNativeMode(posture: Posture, availableModes: readonly SessionMode[]): SessionMode | undefined;
3527
+ /**
3528
+ * How a requested posture resolves against a session's advertised modes.
3529
+ *
3530
+ * - `native` — an advertised mode enforces it; switch via `setSessionMode`
3531
+ * (live, step 5) or apply on restart (step 6). A real permission boundary.
3532
+ * - `prompt` — no native mode; honour it as an injected system-prompt preamble
3533
+ * (advisory only, SPEC risk Rw). Rides the system prompt, so it applies at
3534
+ * spawn/restart, never live.
3535
+ * - `noop` — the `default` (neutral) posture with no advertised `default` mode:
3536
+ * nothing to enforce and nothing to announce.
3537
+ * - `unavailable` — a raw `{ harnessModeId }` the session no longer advertises;
3538
+ * it has no canonical name, so there is no preamble to fall back to. The
3539
+ * caller surfaces this rather than silently doing nothing.
3540
+ */
3541
+ type PostureResolution = {
3542
+ readonly kind: "native";
3543
+ readonly mode: SessionMode;
3544
+ } | {
3545
+ readonly kind: "prompt";
3546
+ readonly posture: Exclude<CanonicalPosture, "default">;
3547
+ readonly preamble: string;
3548
+ } | {
3549
+ readonly kind: "noop";
3550
+ readonly posture: "default";
3551
+ } | {
3552
+ readonly kind: "unavailable";
3553
+ readonly requestedModeId: string;
3554
+ };
3555
+ /**
3556
+ * Resolve a requested posture against the session's advertised `availableModes`
3557
+ * (from the #482 read surface) into one of the {@link PostureResolution} arms.
3558
+ * Pure and total — the single decision function steps 5 (live) and 6 (restart
3559
+ * override) call to learn whether a posture pick is native-enforced or
3560
+ * prompt-injected, without either of them re-deriving the map.
3561
+ *
3562
+ * Native enforcement is always preferred: a canonical posture resolves to
3563
+ * `prompt` ONLY when the harness advertises no equivalent mode.
3564
+ */
3565
+ declare function resolvePosture(posture: Posture, availableModes: readonly SessionMode[]): PostureResolution;
3566
+
2759
3567
  /**
2760
3568
  * Per-workspace state buckets — AIP-46 §State partitioning.
2761
3569
  *
@@ -2867,7 +3675,18 @@ declare function resolveBucketSlug(workspaceSlug: string | undefined | null, reg
2867
3675
  * silently pooling into `default` until restart. Never throws: a
2868
3676
  * missing/corrupt registry degrades to "nothing is registered", i.e.
2869
3677
  * everything lands in `default` — today's pooled behaviour, which is
2870
- * the right failure direction. */
3678
+ * the right failure direction.
3679
+ *
3680
+ * A registry read that fails transiently (a race with a concurrent
3681
+ * `saveWorkspacesConfig` tmp+rename) is indistinguishable here from one
3682
+ * that's genuinely empty — but the persist path never actually needs to
3683
+ * tell them apart: `sessions.ts`'s `sourceBucketOf` already keeps a
3684
+ * loaded row homed to the bucket it came from regardless of what this
3685
+ * returns, so a bad read here degrades a NEW session's placement (still
3686
+ * `default`, same as always) and nothing else. See the 2026-07-18
3687
+ * bucket-clobber incident for why that distinction matters for loaded
3688
+ * rows, and `sourceBucketOf`'s docblock in `sessions.ts` for where it's
3689
+ * actually enforced. */
2871
3690
  declare function readRegisteredSlugs(configPath?: string): ReadonlySet<string>;
2872
3691
  /** Bucket directories that exist on disk. Order is not meaningful. */
2873
3692
  declare function listBuckets(root: string): string[];
@@ -2908,6 +3727,118 @@ declare function migrateLegacySessionsFile(opts: {
2908
3727
  registered: ReadonlySet<string>;
2909
3728
  }): MigrationMarker | null;
2910
3729
 
3730
+ /**
3731
+ * Persisted conversation index — the session ↔ native-transcript link,
3732
+ * stored instead of re-derived (DESIGN.md `agentproto-state-multitenancy`
3733
+ * §6).
3734
+ *
3735
+ * Today the link between an agentproto session and the original
3736
+ * claude/hermes conversation file is DERIVED on every read, through
3737
+ * `claudeCodeProjectDir`/`claudeProjectSlug` (conversation-store.ts) — and
3738
+ * until that function was fixed, derived *wrong* for any cwd containing a
3739
+ * dot. Re-deriving is also blind to cwd drift (a worktree that moved) and
3740
+ * carries no record of subagent transcripts at all.
3741
+ *
3742
+ * This module is the fix: an **append-only** `conversations.jsonl`, one
3743
+ * per workspace bucket (co-located with that bucket's `sessions.json`,
3744
+ * `workspace-buckets.ts`), upserted by `sessionId` — readers take the LAST
3745
+ * record per id. Append-only is deliberate, not incidental: it is immune
3746
+ * to the wholesale-rewrite clobber `sessions.ts`'s `persistSnapshot`
3747
+ * already has (see DESIGN.md §1) — a writer can only ever ADD a line, so a
3748
+ * second process racing a first can't shrink what's on disk.
3749
+ *
3750
+ * Write points (best-effort, mirroring how `transcriptWriter`/
3751
+ * `persistSnapshot` never throw into the turn path): session spawn, an
3752
+ * ACP-level resume, and a native graceful-exit resume-hint — see
3753
+ * `sessions.ts`'s calls into `recordConversationLink`.
3754
+ *
3755
+ * No tenant layer yet — the index lives directly under the workspace
3756
+ * bucket dir today (`bucketDir(bucketsRoot, slug)/conversations.jsonl`);
3757
+ * DESIGN.md §9 notes it migrates under `tenants/<t>/…` at a later PR.
3758
+ */
3759
+ /** The claude-code native store: one jsonl per conversation, plus any
3760
+ * subagent transcripts nested under `<sessionId>/subagents/agent-*.jsonl`. */
3761
+ interface ClaudeNativeRef {
3762
+ kind: "claude-jsonl";
3763
+ path: string;
3764
+ subagents: string[];
3765
+ }
3766
+ /** The hermes native store: a row in a shared sqlite db, keyed by the same
3767
+ * id agentproto already records as `adapterSessionId` — no path to derive. */
3768
+ interface HermesNativeRef {
3769
+ kind: "hermes-sqlite";
3770
+ dbPath: string;
3771
+ rowId: string;
3772
+ }
3773
+ type ConversationNativeRef = ClaudeNativeRef | HermesNativeRef;
3774
+ interface ConversationIndexRecord {
3775
+ sessionId: string;
3776
+ workspace: string;
3777
+ cwd: string;
3778
+ adapterSlug: string;
3779
+ adapterSessionId: string;
3780
+ /** Absent when `adapterSlug` has no known native store (an adapter
3781
+ * outside claude-code/hermes) — the record still links session ↔
3782
+ * cwd ↔ adapterSessionId, it just can't point at a native file. */
3783
+ native?: ConversationNativeRef;
3784
+ agentprotoTranscript: string;
3785
+ title?: string;
3786
+ startedAt: string;
3787
+ endedAt?: string;
3788
+ }
3789
+ /** Absolute path to a workspace bucket's conversation index. Sibling of
3790
+ * `bucketSessionsFile` in `workspace-buckets.ts`, same root/slug rule. */
3791
+ declare function conversationIndexPath(bucketsRoot: string, slug: string): string;
3792
+ interface ResolveNativeLinkInput {
3793
+ cwd: string;
3794
+ adapterSlug: string;
3795
+ adapterSessionId: string;
3796
+ }
3797
+ /**
3798
+ * Resolve where THIS provider keeps this conversation, computed once (at
3799
+ * a write point) rather than re-derived on every read. Claude-code's path
3800
+ * uses the corrected `claudeProjectSlug` via `claudeCodeProjectDir` — the
3801
+ * whole point of pairing this module with the encoder fix in the same PR.
3802
+ * Returns `undefined` for an adapter with no known native store (e.g.
3803
+ * mastracode, claude-sdk) — a record without `native` is still useful
3804
+ * (session ↔ cwd ↔ adapterSessionId), it just has nothing further to add.
3805
+ */
3806
+ declare function resolveNativeLink(input: ResolveNativeLinkInput): Promise<ConversationNativeRef | undefined>;
3807
+ /** Append one record. Never rewrites — the caller decides when a fresher
3808
+ * snapshot of the same session is worth a new line; readers always take
3809
+ * the last one. Creates the bucket dir if this is its first conversation
3810
+ * record (a bucket that has only ever held terminal/command sessions
3811
+ * won't have one yet). */
3812
+ declare function appendConversationRecord(bucketsRoot: string, slug: string, record: ConversationIndexRecord): Promise<void>;
3813
+ /** Read one bucket's index, upserted by `sessionId` — a line later in the
3814
+ * file always wins over an earlier one for the same id, append-only's
3815
+ * upsert semantics. A malformed line (partial write, hand-edited garbage)
3816
+ * is skipped, not fatal — the rest of the file still reads. `[]` (not a
3817
+ * throw) when the bucket has no index yet. */
3818
+ declare function readConversationIndex(bucketsRoot: string, slug: string): Promise<ConversationIndexRecord[]>;
3819
+ /** Forward lookup within one already-known bucket. */
3820
+ declare function findConversationRecord(bucketsRoot: string, slug: string, sessionId: string): Promise<ConversationIndexRecord | undefined>;
3821
+ interface LocatedConversation {
3822
+ workspace: string;
3823
+ record: ConversationIndexRecord;
3824
+ }
3825
+ /** Forward lookup: sessionId → its record, scanning every bucket (the
3826
+ * caller doesn't have to know which workspace a session landed in).
3827
+ * `undefined` when no bucket's index has ever recorded this session. */
3828
+ declare function locateConversationBySessionId(bucketsRoot: string, listBuckets: () => string[], sessionId: string): Promise<LocatedConversation | undefined>;
3829
+ interface LocatedConversationByPath extends LocatedConversation {
3830
+ /** Set when the match was a subagent transcript rather than the root
3831
+ * conversation file — the path that actually matched. */
3832
+ matchedSubagentPath?: string;
3833
+ }
3834
+ /** Reverse lookup: a native jsonl path (root conversation OR a subagent
3835
+ * transcript) → the agentproto session/workspace that owns it. Scans
3836
+ * every bucket's index; paths are compared after `path.resolve` so a
3837
+ * relative or `..`-bearing argument still matches an absolute recorded
3838
+ * one. Only meaningful for claude-code's `native.path`/`native.subagents`
3839
+ * — hermes has no per-conversation file to reverse-lookup from. */
3840
+ declare function locateConversationByNativePath(bucketsRoot: string, listBuckets: () => string[], nativePath: string): Promise<LocatedConversationByPath | undefined>;
3841
+
2911
3842
  /** A journal-file StepCache scoped to one cacheKey. Best-effort: read/parse
2912
3843
  * failures degrade to "no cache" (a miss), never throw into the run. */
2913
3844
  declare function createFileStepCache(cacheKey: string, opts?: {
@@ -3032,6 +3963,44 @@ declare function readRuntimeMeta(workspace: string): Promise<{
3032
3963
  */
3033
3964
  declare function sweepStaleRuntimeMetas(workspaces: readonly string[], currentWorkspace: string): Promise<string[]>;
3034
3965
 
3966
+ /**
3967
+ * Conversation as pivot: one store abstraction per provider, read on any
3968
+ * session. A "conversation" is the provider's own persisted session — the
3969
+ * durable object. An agentproto session (ACP or PTY) is just an attachment
3970
+ * to it. `CONVERSATION_STORES` is the single source of truth for "where does
3971
+ * this provider keep its conversations, and how do I read/attach to one" —
3972
+ * `RESUME_STRATEGIES` (resume-strategies.ts) and `EXPORT_STRATEGIES`
3973
+ * (transcript-export.ts) are thin adapters over it so the two historically
3974
+ * drifted tables can never diverge again.
3975
+ */
3976
+
3977
+ /**
3978
+ * Claude Code's own `cwd` → project-dir-name encoder.
3979
+ *
3980
+ * The real rule (verified empirically against `~/.claude/projects/`
3981
+ * directory names for dozens of known cwds, including dotted worktree
3982
+ * paths and macOS temp dirs): every character that is NOT `[a-zA-Z0-9]`
3983
+ * is replaced 1:1 with `-`. Crucially, runs of consecutive non-alnum
3984
+ * characters are NOT collapsed — each input character produces exactly
3985
+ * one output character.
3986
+ *
3987
+ * This used to be `cwd.replace(/\//g, "-")` — slashes only. That's wrong
3988
+ * for any cwd containing a dot (every `.agentproto` worktree, any
3989
+ * dotdir), an underscore-prefixed segment, a space, etc., because those
3990
+ * characters are left untouched instead of becoming `-`, so the computed
3991
+ * directory never matches the one claude actually created.
3992
+ *
3993
+ * Reconstruction proof (both real, both `ls ~/.claude/projects/`-verified):
3994
+ * cwd /Users/jeremy/.agentproto/worktrees/ts/gc-fresh-hold
3995
+ * → -Users-jeremy--agentproto-worktrees-ts-gc-fresh-hold
3996
+ * (note the "--" from "/." — one dash for "/", one for ".", not collapsed)
3997
+ *
3998
+ * cwd /Volumes/SSDExternalMacStudio/Code/_agentproto-worktrees/adapter-claude-sdk
3999
+ * → -Volumes-SSDExternalMacStudio-Code--agentproto-worktrees-adapter-claude-sdk
4000
+ * (same double-dash shape, this time from "/_")
4001
+ */
4002
+ declare function claudeProjectSlug(cwd: string): string;
4003
+
3035
4004
  /**
3036
4005
  * Read-only probe: does a credential actually RESOLVE for an adapter slug's
3037
4006
  * billing auth? Used by `adapter_list` to report an HONEST status instead of
@@ -3194,6 +4163,11 @@ interface CreateGatewayOptions {
3194
4163
  * `GET /adapters` HTTP route + `adapter_list` MCP tool so UIs
3195
4164
  * can discover what's installed on the host. */
3196
4165
  listAgentAdapters?: AgentAdapterLister;
4166
+ /** Optional catalog lister — when provided, enables
4167
+ * `GET /catalog/models` HTTP route + `catalog_models` MCP tool
4168
+ * (SPEC §5) so UIs can discover every runnable model + route without
4169
+ * trial-and-error against a spawn. */
4170
+ listCatalogModels?: CatalogModelsLister;
3197
4171
  /** Optional browser adapter resolver — when provided, enables the
3198
4172
  * `start_browser` MCP tool (launches Camofox / Bureau / Chromium). */
3199
4173
  resolveBrowserAdapter?: BrowserAdapterResolver;
@@ -3334,4 +4308,4 @@ interface GatewayHandle {
3334
4308
  */
3335
4309
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
3336
4310
 
3337
- export { AdapterAuthDescriptor, type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AttachPolicyInput, BUCKETS_ROOT, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CommitSpec, type CompletionPolicySupervisor, type CreateGatewayOptions, type CreateOfferInput, type CreatedOffer, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, DeclaredAdapterOption, type DeclaredAdapterPreset, type GateSpec, type GatewayHandle, type InboundWatcher, type JudgeGateSpec, LEGACY_SESSIONS_FILE, type McpCredentialDeps, type MigrationMarker, 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, WORKTREE_ISOLATION_ENV, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, type WorktreeDecision, type WorktreeField, WorktreeIsolationMode, type WorktreeProvisionOutcome, type WorktreeProvisionRequest, type WorktreeProvisioner, type WorktreeRequest, bucketDir, bucketSessionsFile, bucketTranscriptDir, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, deriveSessionUsage, formatToolCall, formatToolResult, getMcpCredentialDeps, isAgentCliAuthConfigured, isSafeBucketSlug, listBuckets, listPresets, loadWorktreeIsolation, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeWorktreeField, parseWorktreeIsolationMode, policyWatchesSession, projectSessionUsage, readDaemonRegistry, readRegisteredSlugs, readRuntimeMeta, registerPairingTools, resolveBucketSlug, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
4311
+ export { AdapterAuthDescriptor, type AdapterListEntry, type AgentAdapterLister, type AgentAdapterResolver, type AgentSessionLike, type AgentStreamEvent, type AttachPolicyInput, type AuthMethod, BUCKETS_ROOT, type BrowserAdapterHandle, type BrowserAdapterInfo, type BrowserAdapterLister, type BrowserAdapterResolver, BuildHeartbeatAgent, type CanonicalPosture, type CatalogAdapterInput, type CatalogAdapterModelInput, type CatalogModelsLister, type CatalogModelsQuery, type CatalogModelsResponse, type CatalogPricing, type CatalogProduct, type CatalogRoute, type CatalogVendor, type ClaudeNativeRef, type CommitSpec, type CompletionPolicySupervisor, type ContextProfile, type ConversationIndexRecord, type ConversationNativeRef, type CreateGatewayOptions, type CreateOfferInput, type CreatedOffer, DEFAULT_BUCKET, DEFAULT_ORCHESTRATOR_TOOLS, DEFAULT_WORKTREE_ISOLATION, type DeclaredAdapterMode, DeclaredAdapterOption, type DeclaredAdapterPreset, type EffortLevel, type GateSpec, type GatewayHandle, type HermesNativeRef, type InboundWatcher, type JudgeGateSpec, LEGACY_SESSIONS_FILE, type LocatedConversation, type LocatedConversationByPath, type McpCredentialDeps, type MigrationMarker, type OnFailSpec, type OrchestratorGatewayDeps, type OrchestratorInjection, type OrchestratorInjector, type OrchestratorInjectorDeps, type OrchestratorMcpServerFactory, type OrchestratorScope, PAIRINGS_VERSION, POSTURE_NATIVE_ALIASES, POSTURE_PREAMBLES, type PairingChannelContext, type PairingChannelHandle, type PairingChannelMode, type PairingRecord, type PairingRegistry, type PairingRegistryDeps, type PendingPermission, type PermissionRespondInput, type PermissionRespondResult, type PolicyRunState, type PolicyRunStatus, type Posture, type PostureResolution, type PresetInfo, type PricingResolver, type RegisterBrowserInput, type RegisterPairingToolsOptions, type RegisterSessionInput, type ResolveNativeLinkInput, ResolvedAuthSpec, type RouteSpec, type RuntimeMeta, type SandboxAdapterInfo, type SandboxProviderCapabilities, type SandboxProviderHandle, type SandboxProviderLister, type SandboxProviderResolver, type ScopeTokenRegistry, type SessionConfig, 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, WORKTREE_ISOLATION_ENV, type WatcherDescriptor, type WatcherStartInput, WorkspaceFs, type WorktreeDecision, type WorktreeField, WorktreeIsolationMode, type WorktreeProvisionOutcome, type WorktreeProvisionRequest, type WorktreeProvisioner, type WorktreeRequest, appendConversationRecord, bucketDir, bucketSessionsFile, bucketTranscriptDir, buildCatalogModels, canonicalForModeId, claudeProjectSlug, composeMode, composeSessionObservers, conversationIndexPath, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, daemonRegistryDir, decideWorktreeIsolation, declaredPresetToProviderPreset, decomposeMode, deriveSessionUsage, findConversationRecord, findNativeMode, formatToolCall, formatToolResult, getMcpCredentialDeps, isAgentCliAuthConfigured, isSafeBucketSlug, listBuckets, listPresets, loadWorktreeIsolation, locateConversationByNativePath, locateConversationBySessionId, makeBrowserAdapterLister, migrateLegacySessionsFile, migrationMarkerPath, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeModeId, normalizeWorktreeField, parseWorktreeIsolationMode, policyWatchesSession, projectSessionUsage, readConversationIndex, readDaemonRegistry, readRegisteredSlugs, readRuntimeMeta, registerPairingTools, resolveBucketSlug, resolveNativeLink, resolvePosture, setMcpCredentialDeps, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };