@agentproto/runtime 0.8.0 → 1.0.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" | "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,60 @@ 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];
244
473
  /** Emitted by the supervisor when a completion policy's gate passes. */
245
474
  interface PolicyPassedEvent {
246
475
  type: "policy:passed";
@@ -305,7 +534,7 @@ interface CronFailedEvent {
305
534
  error: string;
306
535
  ts: string;
307
536
  }
308
- type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
537
+ type SessionEvent = SessionTurnEndEvent | SessionAwaitingInputEvent | SessionPermissionRequestEvent | SessionPermissionResolvedEvent | SessionExitedEvent | SessionCommandDoneEvent | SessionModelChangedEvent | SessionConfigChangedEvent | PolicyPassedEvent | PolicyFailedEvent | PolicyCommitReadyEvent | PolicyCommittedEvent | CronFiredEvent | CronSucceededEvent | CronFailedEvent;
309
538
  interface SessionEventBus {
310
539
  emit(ev: SessionEvent): void;
311
540
  /** Subscribe to a specific event type. Returns an unsubscribe fn. */
@@ -375,8 +604,117 @@ interface AgentSessionLike {
375
604
  * drivers that don't model held permissions (sandbox proxy, future
376
605
  * transports). */
377
606
  respondPermission?(requestId: string, resolution: AcpPermissionResolution): boolean | Promise<boolean>;
607
+ /**
608
+ * Switch the active model on this LIVE session — mirrors
609
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setModel`
610
+ * without importing it (this package stays driver-decoupled, same
611
+ * reasoning as the rest of this structural interface). Optional: absent
612
+ * for a session shape that doesn't support live model switching (a
613
+ * sandboxed proxy session, or a future non-agent-cli transport) —
614
+ * `SessionsRegistry.setModel` treats a missing method as
615
+ * `{applied:false, reason:"not-supported"}` rather than throwing.
616
+ */
617
+ setModel?(modelId: string): Promise<SetSessionModelResult>;
618
+ /**
619
+ * Switch the reasoning/compute budget on this LIVE session — mirrors
620
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setEffort`
621
+ * (ACP `session/set_config_option(configId:"effort")`), same driver-decoupled
622
+ * structural-mirror reasoning as `setModel` above. Optional: absent for a
623
+ * session shape with no live config surface (sandboxed proxy, future
624
+ * transport) — `SessionsRegistry.setEffort` treats a missing method as
625
+ * `{applied:false, reason:"not-supported"}` rather than throwing.
626
+ */
627
+ setEffort?(effort: string): Promise<SetSessionEffortResult>;
628
+ /**
629
+ * Switch the native posture (harness mode) on this LIVE session — mirrors
630
+ * `@agentproto/driver-agent-cli`'s `AgentCliRuntimeSession.setSessionMode`
631
+ * (ACP `session/set_mode`). Optional, same treatment as `setModel`/`setEffort`.
632
+ */
633
+ setSessionMode?(modeId: string): Promise<SetSessionModeResult>;
634
+ /**
635
+ * The harness's advertised session modes captured at connect time
636
+ * (`SessionModeState.availableModes`, #482 ACP capability read-surface).
637
+ * `SessionsRegistry.setPosture` resolves a requested posture against this to
638
+ * decide native-live vs restart (SPEC §3.4a). Absent/empty for arms with no
639
+ * native mode registry — treated as "no native mode", so any posture pick
640
+ * routes to restart.
641
+ */
642
+ readonly availableModes?: readonly SessionMode[];
378
643
  close(): Promise<void>;
379
644
  }
645
+ /** Result of `SessionsRegistry.setModel` — see that method's doc comment. */
646
+ interface SetSessionModelResult {
647
+ applied: boolean;
648
+ /** The model id now active. Present only when `applied` is true. */
649
+ model?: string;
650
+ /** Present only when `applied` is false — see
651
+ * `@agentproto/driver-agent-cli`'s `SetModelResult` for the reason
652
+ * vocabulary this passes through verbatim. */
653
+ reason?: string;
654
+ /**
655
+ * Present only on the model↔route guard refusal (SPEC risk R2 / §4.4): the
656
+ * requested model's route-identity crosses the session's current route/vendor
657
+ * boundary, which a live `setModel` cannot perform (route is a spawn-time
658
+ * `ANTHROPIC_BASE_URL`, not a live ACP config option). The daemon refuses the
659
+ * live switch (`applied:false, reason:"requires-restart"`) rather than
660
+ * silently keeping the old endpoint, and hands back the override a
661
+ * restart-with-override (step 6) should carry to apply model + route together.
662
+ */
663
+ suggestedOverride?: {
664
+ route: RouteSpec;
665
+ model: string;
666
+ };
667
+ }
668
+ /**
669
+ * Result of `SessionsRegistry.setEffort` — mirrors the driver's
670
+ * `SetEffortResult` (see `AgentSessionLike.setEffort`). Effort is a per-model
671
+ * capability (SPEC §3.9); a rejected label is a soft `{applied:false, reason}`
672
+ * (SPEC risk R7), never thrown.
673
+ */
674
+ interface SetSessionEffortResult {
675
+ applied: boolean;
676
+ /** The effort label now active. Present only when `applied` is true. */
677
+ effort?: string;
678
+ /** Present only when `applied` is false — passes the driver's reason through
679
+ * verbatim (`"not-supported"`, or the wrapper's own rejection detail). */
680
+ reason?: string;
681
+ }
682
+ /** Result of a mid-session `setSessionMode` attempt on the driver session —
683
+ * structural mirror of `@agentproto/driver-agent-cli`'s `SetSessionModeResult`,
684
+ * used by {@link AgentSessionLike.setSessionMode}. */
685
+ interface SetSessionModeResult {
686
+ applied: boolean;
687
+ /** The mode id that took effect. Present only when `applied` is true. */
688
+ modeId?: string;
689
+ /** Present only when `applied` is false — the driver's reason verbatim. */
690
+ reason?: string;
691
+ }
692
+ /**
693
+ * Result of `SessionsRegistry.setPosture` (SPEC §4.2, build step 5). A posture
694
+ * that maps to a native advertised harness mode is switched LIVE via
695
+ * `setSessionMode` (`applied:true`); one with no native mode (prompt-injected /
696
+ * env-applied) is NOT forced live — it resolves
697
+ * `{applied:false, reason:"requires-restart"}` so the caller routes it through
698
+ * the restart-with-override path (step 6, not implemented here).
699
+ */
700
+ interface SetSessionPostureResult {
701
+ applied: boolean;
702
+ /** The posture now active. Present only when `applied` is true. */
703
+ posture?: Posture;
704
+ /** The native harness mode id switched to — present only on a native
705
+ * (`applied:true`) switch, so a caller can echo exactly what took effect. */
706
+ modeId?: string;
707
+ /** Present only when `applied` is false — `"requires-restart"` when the
708
+ * posture has no native mode (prompt/env apply-path needs a fresh spawn),
709
+ * `"not-supported"` for a session with no live mode surface, or the harness's
710
+ * own rejection detail when a native switch was attempted and refused. */
711
+ reason?: string;
712
+ /** How the requested posture resolved against the harness's advertised modes
713
+ * (`native` | `prompt` | `noop` | `unavailable`, from `resolvePosture`) —
714
+ * lets the caller distinguish "needs restart because prompt-injected" from a
715
+ * genuine native-switch rejection. */
716
+ resolution?: "native" | "prompt" | "noop" | "unavailable";
717
+ }
380
718
  /**
381
719
  * Minimal PTY surface — structurally compatible with
382
720
  * @agentproto/acp/tunnel's PtyProcess (node-pty's IPty wrapper). The
@@ -473,6 +811,24 @@ interface SessionAuthEcho {
473
811
  credentialSource?: "explicit-config" | "providers-store" | "none";
474
812
  setEnv?: string;
475
813
  }
814
+ /**
815
+ * The `access` axis's descriptor ECHO (SPEC §3.6/§3.7) — a non-secret
816
+ * description of the NAMED auth profile attached to the session, recorded so a
817
+ * client chip can NAME the wallet ("Jeremy Max") without re-resolving it. This
818
+ * is deliberately separate from {@link SessionAuthEcho}, which stays as-is: the
819
+ * `auth` echo is the resolver's observable output (mode + credential
820
+ * fingerprint), this is the profile IDENTITY the operator selected. NEVER the
821
+ * credential — `profileRef` resolves through `@agentproto/auth` at read time
822
+ * (`packages/auth/src/profile-types.ts:24`, #470). `profileRef` is always set
823
+ * when this object is present (the profile is the reason it exists); the rest
824
+ * mirror the `AuthProfile` fields the chip renders.
825
+ */
826
+ interface SessionAccessProfileEcho {
827
+ profileRef: string;
828
+ label?: string;
829
+ vendor: string;
830
+ method: AuthMethod;
831
+ }
476
832
  interface SessionDescriptor {
477
833
  id: string;
478
834
  kind: SessionKind;
@@ -535,6 +891,19 @@ interface SessionDescriptor {
535
891
  * about, for a UI that would otherwise show the adapter's argv. Distinct
536
892
  * from `label`, which the spawner supplies and which always wins. */
537
893
  title?: string;
894
+ /** Housekeeping-only visibility flag: hides the session from `list()`'s
895
+ * default view (`session_list`, `GET /sessions`, panels) once set. Never
896
+ * touches the daemon otherwise — the process is already gone by the time
897
+ * this is set (see the terminal-status guard on `archiveSession`), the
898
+ * transcript stays fully readable via `get()`/`findByIdOrName` (neither
899
+ * filters on it), and `list({ includeArchived: true })` still returns it.
900
+ * Set by `archiveSession`/`unarchiveSession` (session-tools.ts's
901
+ * `session_archive`/`session_unarchive`), persisted like every other
902
+ * descriptor field, and round-trips through `loadHistorySnapshot` on
903
+ * reboot since it's carried by the same `...desc` spread every other
904
+ * field is. Absent (not `false`) for every descriptor from before this
905
+ * field existed — treated the same as `false` everywhere it's read. */
906
+ archived?: boolean;
538
907
  /** True when the session was spawned under a real PTY (node-pty)
539
908
  * instead of `child_process.spawn`. PTY sessions carry raw ANSI
540
909
  * bytes (alt-screen, key bindings, colors); attach goes through
@@ -573,8 +942,38 @@ interface SessionDescriptor {
573
942
  * `/sessions/agent` to spin up a fresh ACP runtime. Undefined for
574
943
  * pty/command kinds. */
575
944
  adapterSlug?: string;
945
+ /**
946
+ * AIP-45 mode the session was spawned with (`AgentCliStartOptions.config.
947
+ * mode` — e.g. claude-code's `plan`/`accept-edits`, a gateway preset mode
948
+ * like `moonshot`). Undefined for the adapter's default/native mode.
949
+ * Recorded so a client can tell whether a candidate model switch stays
950
+ * within THIS mode (live-switchable via `setModel`) or needs a different
951
+ * one (`AgentCliModelEntry.mode` on the target) — a mode change is
952
+ * spawn-time env/argv rewiring (e.g. `ANTHROPIC_BASE_URL`), which
953
+ * `POST /sessions/:id/model` cannot perform on a live process; that case
954
+ * is surfaced to clients as `requires-restart`, never silently attempted.
955
+ */
956
+ mode?: string;
576
957
  /** The model the session was requested to run (echoed back at spawn). */
577
958
  model?: string;
959
+ /** Reasoning / compute budget the session resolved to (SPEC §3.1 axis 2).
960
+ * A LIVE-switchable axis; echoed here so the effort chip re-opens on it. */
961
+ effort?: EffortLevel;
962
+ /** What the agent may DO (SPEC §3.1 axis 5) — an agentproto-canonical
963
+ * posture or a raw `{ harnessModeId }` sourced from the harness's ACP mode
964
+ * registry (SPEC §3.4a). */
965
+ posture?: Posture;
966
+ /** Endpoint / gateway rail (SPEC §3.1 axis 4). `baseUrl` is carried only
967
+ * for a custom gateway the catalog can't resolve; `access` is downstream
968
+ * of this axis (SPEC §1c). */
969
+ route?: RouteSpec;
970
+ /** What enters context (SPEC §3.1 axis 5b) — `"lean"` drops bundled skills. */
971
+ contextProfile?: ContextProfile;
972
+ /** The `access` axis echo (SPEC §3.6/§3.7): the NAMED auth profile attached
973
+ * to the session, so the access chip can name the wallet. Distinct from the
974
+ * `auth` fingerprint echo below, which stays as-is — see
975
+ * {@link SessionAccessProfileEcho}. NEVER the credential. */
976
+ accessProfile?: SessionAccessProfileEcho;
578
977
  /**
579
978
  * Deterministic billing-auth mode + a non-secret credential fingerprint,
580
979
  * recorded at spawn time for adapters that resolved an explicit
@@ -710,6 +1109,26 @@ interface SessionDescriptor {
710
1109
  * matching command session; absent otherwise (including for legacy
711
1110
  * rows persisted before this field existed). */
712
1111
  priorCommandSessionId?: string;
1112
+ /** Id of the prior session this one continues from — set when this
1113
+ * session was spawned by `session_restart` (or the cron scheduler's
1114
+ * `prompt-session` action), even when the resume attempt itself
1115
+ * couldn't establish continuity: a fresh fallback spawn (adapter
1116
+ * rejected the resume id as "not found") is still "restarted from"
1117
+ * the prior session, just without conversation history carried over.
1118
+ * Absent for a session spawned directly (not via restart). Persisted
1119
+ * on the STORED descriptor (not just grafted onto the restart
1120
+ * result's JSON, as it used to be) so it survives a `list()`/`get()`
1121
+ * poll refresh and a daemon restart — see `resumeVia` for how the
1122
+ * continuity was (or wasn't) established, and the transcript panel's
1123
+ * chain-walk (vscode package) for the read side. */
1124
+ resumedFrom?: string;
1125
+ /** Human-readable resume path used to arrive at `resumedFrom` — e.g.
1126
+ * "resumed via claude --resume" (provider-native PTY resume) or
1127
+ * "resumed via ACP" (adapter-level resume), or `""` when no
1128
+ * continuity was established (a fresh fallback spawn — see
1129
+ * `resumedFrom`). Only meaningful alongside `resumedFrom`; absent
1130
+ * (never `""`) for a session that wasn't spawned via restart. */
1131
+ resumeVia?: string;
713
1132
  /** Adapter id that drives this session (e.g. "camofox", "bureau"). */
714
1133
  browserAdapterId?: string;
715
1134
  /** Port the browser service listens on. */
@@ -874,6 +1293,67 @@ interface SessionsRegistry {
874
1293
  interruptSession(id: string): Promise<{
875
1294
  wasBusy: boolean;
876
1295
  }>;
1296
+ /**
1297
+ * Switch the model on a LIVE agent-cli session without restarting it —
1298
+ * the mid-session counterpart to `spawnAgent`'s `input.model` (which
1299
+ * only applies at spawn time). Delegates to the driver session's own
1300
+ * `setModel` (see `AgentSessionLike.setModel`), which dispatches on the
1301
+ * adapter's `models.apply` strategy (`config`/`command`/`arg`) and never
1302
+ * throws on a rejected switch.
1303
+ *
1304
+ * On `{applied:true}`, updates `SessionDescriptor.model` so
1305
+ * `session_list`/SSE reflect the switch and emits a
1306
+ * `session:config-changed {axis:"model"}` event on the session event bus
1307
+ * (plus a back-compat `session:model-changed` alias). On `{applied:false}`
1308
+ * the descriptor and event bus are untouched — nothing changed, so nothing
1309
+ * to announce.
1310
+ *
1311
+ * Throws (not a structured result) for the two "this request doesn't
1312
+ * even make sense" cases: an unknown session id, or a session that
1313
+ * isn't an agent-cli kind (or whose driver session predates `setModel`
1314
+ * entirely) — both are caller errors, not an adapter's refusal.
1315
+ */
1316
+ setModel(id: string, modelId: string): Promise<SetSessionModelResult>;
1317
+ /**
1318
+ * Announce a single `SessionConfig` axis change on the session event bus
1319
+ * (SPEC step 6). `setModel` emits its `session:config-changed` event inline,
1320
+ * but restart-with-override (`restartAgentSession`, session-restart-core.ts)
1321
+ * lives OUTSIDE the registry — it re-resolves auth and re-spawns a fresh
1322
+ * session — so it announces each changed axis (access/route/posture/…) of the
1323
+ * new session through this one method rather than reaching into the private
1324
+ * event bus. No-op when the registry was constructed without a `sessionEvents`
1325
+ * bus (the emit is best-effort observability, never load-bearing). The caller
1326
+ * builds the fully-typed event (axis + value already reflected on the new
1327
+ * descriptor); the registry only forwards it. */
1328
+ emitConfigChanged(ev: SessionConfigChangedEvent): void;
1329
+ /**
1330
+ * Switch the reasoning/compute budget (effort) on a LIVE agent-cli session
1331
+ * without restarting it — the live-effort verb (SPEC §4.2, build step 5),
1332
+ * `POST /sessions/:id/effort` + `agent_set_effort`. Delegates to the driver
1333
+ * session's `setEffort` (ACP `set_config_option(configId:"effort")`); effort
1334
+ * is model-dependent (SPEC §3.9), so a label the current model rejects is a
1335
+ * soft `{applied:false, reason}` (SPEC risk R7), never thrown.
1336
+ *
1337
+ * On `{applied:true}` updates `SessionDescriptor.effort` and emits
1338
+ * `session:config-changed {axis:"effort"}`; on `{applied:false}` the
1339
+ * descriptor and bus are untouched. Throws (caller error, not an adapter
1340
+ * refusal) for an unknown session id or a non-agent-cli session — same
1341
+ * contract as `setModel`.
1342
+ */
1343
+ setEffort(id: string, effort: string): Promise<SetSessionEffortResult>;
1344
+ /**
1345
+ * Switch the posture on a LIVE agent-cli session (SPEC §4.2, build step 5),
1346
+ * `POST /sessions/:id/posture` + `agent_set_posture`. When the requested
1347
+ * posture maps to a NATIVE advertised harness mode (`resolvePosture` →
1348
+ * `native`), it's switched live via the driver's `setSessionMode`
1349
+ * (`applied:true`, descriptor + `session:config-changed {axis:"posture"}`
1350
+ * emitted). When there is NO native mode (prompt-injected / env-applied /
1351
+ * a raw mode the session no longer advertises), it is NOT forced live — it
1352
+ * resolves `{applied:false, reason:"requires-restart"}` so the caller routes
1353
+ * it through restart-with-override (step 6, not implemented here). Throws for
1354
+ * an unknown session id or a non-agent-cli session, same as `setModel`.
1355
+ */
1356
+ setPosture(id: string, posture: Posture): Promise<SetSessionPostureResult>;
877
1357
  /** Stamp `lastActivityAt` on a live agent-cli session's descriptor
878
1358
  * and schedule a debounced persist. Called from the `onActivity`
879
1359
  * callback threaded down through the driver → ACP client, which
@@ -881,8 +1361,36 @@ interface SessionsRegistry {
881
1361
  * output) — see `SessionDescriptor.lastActivityAt`. No-op when the
882
1362
  * id is unknown (session already forgotten). */
883
1363
  pulseActivity(id: string): void;
884
- list(): SessionDescriptor[];
1364
+ /** Every non-archived session, newest `startedAt` first — the daemon's
1365
+ * canonical lister (`session_list`, `GET /sessions`, panels, subtree
1366
+ * scoping). Archived sessions are excluded UNLESS `includeArchived` is
1367
+ * true — the default keeps a housekeeping flag from becoming a second,
1368
+ * silent filter every caller has to know about, while
1369
+ * `{ includeArchived: true }` is there for `session_list`'s own opt-in
1370
+ * and for any subtree/authorization computation (`collectSubtree`) that
1371
+ * needs the FULL parent→child graph to stay connected — a subtree BFS
1372
+ * fed the filtered list would silently orphan the non-archived
1373
+ * descendants of an archived ancestor, since each edge is keyed off the
1374
+ * CHILD's own record. `get()`/`findByIdOrName()` are unaffected by this
1375
+ * flag entirely — a transcript stays directly openable by id no matter
1376
+ * how it's archived. */
1377
+ list(opts?: {
1378
+ includeArchived?: boolean;
1379
+ }): SessionDescriptor[];
885
1380
  get(id: string): SessionDescriptor | undefined;
1381
+ /** Archive a TERMINAL-status session (exited/killed/error) — sets
1382
+ * `archived: true` and persists. Pure housekeeping: hides the row from
1383
+ * `list()`'s default view, nothing else. Refuses (throws) a still-alive
1384
+ * session (running/starting) — archiving one would hide it from the
1385
+ * daemon's own default view while it keeps working unattended, which is
1386
+ * a worse foot-gun than the flag is trying to solve. Idempotent: already
1387
+ * archived is a no-op success. Throws when the id is unknown. */
1388
+ archiveSession(id: string): SessionDescriptor;
1389
+ /** Unarchive — the inverse, no status guard (an archived session was
1390
+ * terminal when archived, and archiving never touched daemon state, so
1391
+ * there is nothing to re-validate). Throws only when the id is
1392
+ * unknown. */
1393
+ unarchiveSession(id: string): SessionDescriptor;
886
1394
  /** Subscribe to a session's output. Returns an unsubscribe fn.
887
1395
  * Initial backfill: synchronously invokes `onLine` once for each
888
1396
  * line currently in the ring buffer so attaches show context. */
@@ -1009,11 +1517,35 @@ interface SpawnAgentInput {
1009
1517
  depth?: number;
1010
1518
  /** Requested model id — recorded on the descriptor for display + echo. */
1011
1519
  model?: string;
1520
+ /** AIP-45 mode the session was spawned with — recorded onto
1521
+ * {@link SessionDescriptor.mode}. See that field's doc for why a client
1522
+ * needs it (mode-mismatch detection for mid-session model switching). */
1523
+ mode?: string;
1012
1524
  /** Resolved auth echo (mode + fingerprint + provider/source/setEnv) —
1013
1525
  * recorded verbatim onto {@link SessionDescriptor.auth}. See that field's
1014
1526
  * doc for the full contract; the caller (`session-spawn.ts`) computes this
1015
1527
  * via the billing-auth resolver, never passing the raw credential here. */
1016
1528
  auth?: SessionAuthEcho;
1529
+ /** Decomposed config-axis echoes (SPEC §3.7) — recorded verbatim onto the
1530
+ * matching {@link SessionDescriptor} fields, the same optional-spread way
1531
+ * `model`/`mode`/`auth` are. All optional; a caller that resolved an axis
1532
+ * passes its echo so the descriptor round-trips it and the picker re-opens
1533
+ * on it. See each descriptor field's doc for the axis contract. */
1534
+ effort?: EffortLevel;
1535
+ posture?: Posture;
1536
+ route?: RouteSpec;
1537
+ contextProfile?: ContextProfile;
1538
+ /** Named auth-profile echo for the `access` axis (SPEC §3.6). Non-secret —
1539
+ * see {@link SessionAccessProfileEcho}. */
1540
+ accessProfile?: SessionAccessProfileEcho;
1541
+ /** Prior session id this spawn continues from — set by `restartAgentSession`
1542
+ * (session-restart-core.ts) when this is a restart, recorded verbatim onto
1543
+ * {@link SessionDescriptor.resumedFrom}. Absent for a direct (non-restart)
1544
+ * spawn. */
1545
+ resumedFrom?: string;
1546
+ /** Human-readable resume path, recorded onto {@link SessionDescriptor.resumeVia}.
1547
+ * Threaded through alongside `resumedFrom` — see that field's doc. */
1548
+ resumeVia?: string;
1017
1549
  /** Hard ceiling on cumulative session cost (USD). When set and the
1018
1550
  * adapter's usage reader reports a higher cost at a turn-end, the session
1019
1551
  * is stopped (best-effort, turn-granular — caps continuation, can't abort
@@ -1087,6 +1619,17 @@ interface SpawnPtyInput {
1087
1619
  * collide with an existing session's name. */
1088
1620
  name?: string;
1089
1621
  label?: string;
1622
+ /** Parent attribution + depth — same semantics as `SpawnAgentInput`
1623
+ * (orchestrator WP4): set when the spawn came through a scoped
1624
+ * sub-gateway so `session_tree` shows the PTY under its spawner. */
1625
+ parentSessionId?: string;
1626
+ depth?: number;
1627
+ /** Restart lineage — same semantics as `SpawnAgentInput.resumedFrom` /
1628
+ * `resumeVia`, recorded onto {@link SessionDescriptor.resumedFrom} /
1629
+ * {@link SessionDescriptor.resumeVia}. Set by `session_restart` for the
1630
+ * pty-native/pty-plain branches (session-tools.ts). */
1631
+ resumedFrom?: string;
1632
+ resumeVia?: string;
1090
1633
  }
1091
1634
  interface RecordCommandInput {
1092
1635
  workspaceSlug: string;
@@ -1568,6 +2111,95 @@ interface WebhookNotifier {
1568
2111
  onSessionEvent(ev: SessionEvent): void;
1569
2112
  }
1570
2113
 
2114
+ /**
2115
+ * Shared types for pluggable sandbox providers.
2116
+ *
2117
+ * The sandbox family is a `@agentproto/provider-kit` consumer, same shape as
2118
+ * the tunnel family (`remote-providers/types.ts`): {@link SandboxProviderHandle}
2119
+ * extends the kit's generic `AdapterHandle` (slug/name/version/description/
2120
+ * requiresSetup/check) with the AIP-36 capability namespace and the concrete
2121
+ * `@agentproto/sandbox` `SandboxProvider` (`boot()`) the handle wraps.
2122
+ */
2123
+
2124
+ /**
2125
+ * Declared capabilities of a sandbox provider — pure metadata, surfaced in
2126
+ * `list_sandbox_providers`. Never carries secrets. Namespace mirrors AIP-36
2127
+ * SANDBOX.md (`network.egress`, `mounts`, `lifecycle.pause_after_idle`,
2128
+ * `read_only`, `limits.timeout_ms`).
2129
+ */
2130
+ interface SandboxProviderCapabilities {
2131
+ /** Sandbox can be given a network egress allowlist (AIP-36 `network.egress`). */
2132
+ networkEgress: boolean;
2133
+ /** Sandbox supports mounting external filesystems (AIP-36 `mounts`). */
2134
+ mounts: boolean;
2135
+ /** Sandbox can be paused (not just killed) between turns (AIP-36 `lifecycle.pause_after_idle`). */
2136
+ lifecyclePause: boolean;
2137
+ /** Sandbox can be started read-only (AIP-36 `read_only`). */
2138
+ readOnly: boolean;
2139
+ /** Hard cap on `limits.timeout_ms`, when the provider enforces one. */
2140
+ maxTimeoutMs?: number;
2141
+ }
2142
+ /**
2143
+ * A sandbox provider as an adapter-kit handle. Rides on the kit's generic
2144
+ * {@link AdapterHandle} and adds the sandbox-specific `capabilities` plus
2145
+ * the `@agentproto/sandbox` `SandboxProvider` the handle wraps — the thing
2146
+ * `createSandboxAgentSessionHost` actually boots.
2147
+ */
2148
+ interface SandboxProviderHandle extends AdapterHandle {
2149
+ readonly provider: SandboxProvider;
2150
+ readonly capabilities: SandboxProviderCapabilities;
2151
+ /**
2152
+ * Credential fields this provider accepts via `setup_sandbox_provider`
2153
+ * (e.g. e2b's `apiKey`). Omit (or empty) when the provider needs no
2154
+ * credentials (e.g. `local`).
2155
+ */
2156
+ readonly setupFields?: readonly SetupField[];
2157
+ }
2158
+
2159
+ /**
2160
+ * Sandbox family on top of `@agentproto/provider-kit` — mirrors
2161
+ * `tunnel-adapters.ts`. This module is the entire bridge between the
2162
+ * generic kit and `@agentproto/sandbox`'s `SandboxProvider` concept: it
2163
+ * contributes nothing the kit already owns (catalog/status/creds/ledger/
2164
+ * list/MCP-tool plumbing); it only supplies the sandbox-family `TInfo`
2165
+ * (`SandboxAdapterInfo`), the static `SANDBOX_CATALOG`, and the resolver
2166
+ * that maps a catalog slug to a concrete {@link SandboxProviderHandle}
2167
+ * (`./sandbox-providers/registry.js`).
2168
+ *
2169
+ * Kit primitives used:
2170
+ * - `makeCredsStore` → per-slug 0600 creds under `~/.agentproto/sandbox-creds/`
2171
+ * - `makeSetupLedger` → `~/.agentproto/setup/<slug>.json`
2172
+ * - `makeAdapterResolver` → wraps the throwing `load` into null-on-miss
2173
+ * - `makeAdapterLister` → catalog → status-classified `AdapterEntry[]`
2174
+ * - `makeListTool` → registers `list_sandbox_providers`
2175
+ * - `makeSetupTool` → registers `setup_sandbox_provider` (multi-field
2176
+ * form: e2b's `apiKey`, sensitive)
2177
+ *
2178
+ * This is pure additive plumbing (introspection + setup) — it does NOT wire
2179
+ * a `sandbox` field into `agent_start`. That lands in a follow-up PR; the
2180
+ * `resolveSandboxProvider`/`listSandboxProviders` overrides below exist now
2181
+ * so that later wiring can inject the same resolver/lister this module
2182
+ * builds by default, mirroring `resolveAgentAdapter`/`listAgentAdapters`.
2183
+ *
2184
+ * Security: `toSandboxInfo` exposes only `capabilities` — never a cred
2185
+ * value (Appendix B). The setup tool's fields are marked SENSITIVE and the
2186
+ * result NEVER echoes any field value back.
2187
+ */
2188
+
2189
+ /**
2190
+ * Family descriptor (`TInfo`). Pure metadata surfaced in
2191
+ * `list_sandbox_providers`. The kit's `AdapterEntry` already carries
2192
+ * slug/name/description/status/version, so the only sandbox-specific field
2193
+ * is the declared capability set. NEVER carries a cred value.
2194
+ */
2195
+ interface SandboxAdapterInfo {
2196
+ capabilities: SandboxProviderCapabilities;
2197
+ }
2198
+ /** Resolve a sandbox provider slug to a handle, or null when unavailable. */
2199
+ type SandboxProviderResolver = AdapterResolver<SandboxProviderHandle>;
2200
+ /** List every sandbox provider with its live status + capabilities. */
2201
+ type SandboxProviderLister = AdapterLister<SandboxAdapterInfo>;
2202
+
1571
2203
  /**
1572
2204
  * Cursor-based ring buffer for session events. Bridges the in-process
1573
2205
  * SessionEventBus (push) to the session_events_poll MCP tool (pull).
@@ -2133,95 +2765,6 @@ type OrchestratorInjector = (opts?: {
2133
2765
  */
2134
2766
  declare function createOrchestratorInjector(deps: OrchestratorInjectorDeps): OrchestratorInjector;
2135
2767
 
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
2768
  /**
2226
2769
  * Policy layer for `agent_start.worktree` — the config-driven decision of
2227
2770
  * WHETHER to isolate a spawn into its own git worktree, kept deliberately
@@ -2343,6 +2886,108 @@ declare function loadWorktreeIsolation(loadCfg?: () => Promise<{
2343
2886
  };
2344
2887
  }>): Promise<WorktreeIsolationMode>;
2345
2888
 
2889
+ /**
2890
+ * Read-only catalog/vendor endpoint (`agentproto-session-config-axes`
2891
+ * SPEC §5) — `GET /catalog/models` + `catalog_models` MCP tool wire into
2892
+ * {@link buildCatalogModels}, the pure join this module owns.
2893
+ *
2894
+ * Reuses three already-shipped pieces instead of rebuilding them:
2895
+ * - the vendor/product/route model + router widening (OpenRouter/
2896
+ * Requesty/HuggingFace) from `@agentproto/model-catalog/route-identity`
2897
+ * (`resolveLlmModelRoute`, `route-identity/index.ts:396-511`) — this is
2898
+ * what keeps the catalog from being capped at any one adapter's
2899
+ * `models.allowed` list (SPEC §5.1);
2900
+ * - the profile eligibility predicate shipped in #470
2901
+ * (`@agentproto/auth`'s `eligibleProfiles`, `packages/auth/src/
2902
+ * eligibility.ts:81-89`) for the profile-aware `runnable` flag (SPEC
2903
+ * §5.3) — the old bare `hasKey` check (`packages/cli/src/commands/
2904
+ * models.ts:113-117`) is the degenerate one-profile-per-provider case
2905
+ * this predicate subsumes;
2906
+ * - `AdapterAuthDescriptor` (`spawn-defaults.ts:226`), the SAME
2907
+ * provider/authSubscription projection `resolveAuthSpec` reads, as the
2908
+ * source for which auth methods an adapter can present on its direct
2909
+ * route (SPEC §3.4's derivable replacement for a hand-maintained
2910
+ * `authSubscription` boolean).
2911
+ *
2912
+ * A gateway/router route (anything where the resolved route differs from
2913
+ * the model's vendor — `openrouter`, `requesty`, `huggingface`, or an
2914
+ * adapter's own gateway mode id like `moonshot`) always bills against the
2915
+ * route's own id and is always reached with an api-key credential — never
2916
+ * oauth-bearer, since no third-party gateway has an Anthropic-style
2917
+ * subscription bearer path (SPEC §1c: "a moonshot profile, not the Claude
2918
+ * sub"). That structural rule is what lets this module compute
2919
+ * `runnable`/`eligibleProfiles` for the widened, non-curated rows without
2920
+ * per-adapter gateway-vendor tables.
2921
+ */
2922
+
2923
+ /** One model entry as declared in an adapter's `models.allowed`
2924
+ * (`AdapterModelInfo`, `packages/cli/src/registry/resolve.ts:134-142`) —
2925
+ * the subset this module needs. */
2926
+ interface CatalogAdapterModelInput {
2927
+ /** Model id exactly as declared — bare (`"claude-opus-4-8"`) or
2928
+ * `vendor/product` form. */
2929
+ id: string;
2930
+ /** The adapter mode id that must be applied to reach this model on a
2931
+ * non-direct route (`AdapterModelInfo.mode`) — e.g. `"moonshot"`.
2932
+ * Undefined ⇒ direct route (the model's own vendor). */
2933
+ mode?: string;
2934
+ }
2935
+ /** One installed adapter's contribution to the catalog. */
2936
+ interface CatalogAdapterInput {
2937
+ slug: string;
2938
+ models: readonly CatalogAdapterModelInput[];
2939
+ /** This adapter's billing-auth capability on its DIRECT route — the same
2940
+ * projection `resolveAuthSpec` reads (`spawn-defaults.ts:226`). Omitted
2941
+ * ⇒ the adapter presents no auth method, so rows it curates are
2942
+ * discoverable but never runnable through it alone. */
2943
+ authDescriptor?: AdapterAuthDescriptor;
2944
+ }
2945
+ interface CatalogModelsQuery {
2946
+ /** Keep only routes reachable via this adapter slug. */
2947
+ adapter?: string;
2948
+ /** Keep only this vendor's entry. */
2949
+ vendor?: string;
2950
+ /** Keep only routes with this route id. */
2951
+ route?: string;
2952
+ /** Drop every route with `runnable: false`. */
2953
+ runnableOnly?: boolean;
2954
+ }
2955
+ interface CatalogPricing {
2956
+ inPer1M: number;
2957
+ outPer1M: number;
2958
+ }
2959
+ interface CatalogRoute {
2960
+ route: string;
2961
+ ref: string;
2962
+ baseUrl: string | null;
2963
+ pricing: CatalogPricing | null;
2964
+ runnable: boolean;
2965
+ eligibleProfiles: string[];
2966
+ adapterModes: string[];
2967
+ adapters: string[];
2968
+ curated: boolean;
2969
+ }
2970
+ interface CatalogProduct {
2971
+ product: string;
2972
+ routes: CatalogRoute[];
2973
+ }
2974
+ interface CatalogVendor {
2975
+ vendor: string;
2976
+ products: CatalogProduct[];
2977
+ }
2978
+ interface CatalogModelsResponse {
2979
+ vendors: CatalogVendor[];
2980
+ }
2981
+ interface BuildCatalogModelsInput {
2982
+ adapters: readonly CatalogAdapterInput[];
2983
+ profiles: readonly AuthProfile[];
2984
+ query?: CatalogModelsQuery;
2985
+ }
2986
+ /** The pure join (SPEC §5): adapter-declared models + router widening +
2987
+ * the #470 eligibility predicate → the vendor/product/route tree. No I/O —
2988
+ * callers (the HTTP route / MCP tool) own loading adapters + profiles. */
2989
+ declare function buildCatalogModels(input: BuildCatalogModelsInput): CatalogModelsResponse;
2990
+
2346
2991
  /**
2347
2992
  * Pluggable adapter resolver — keeps the runtime package free of any
2348
2993
  * @agentproto/cli dep. The host (cli `serve`, playground, embedding
@@ -2482,6 +3127,13 @@ interface AdapterListEntry {
2482
3127
  modes: AdapterListMode[];
2483
3128
  }
2484
3129
  type AgentAdapterLister = () => Promise<AdapterListEntry[]>;
3130
+ /** Loads the read-only vendor/product/route catalog (SPEC §5) for
3131
+ * `GET /catalog/models` + the `catalog_models` MCP tool. A host wires this
3132
+ * from `buildCatalogModels` (`catalog-models.ts`) fed by its installed
3133
+ * adapters + `@agentproto/auth`'s `listAuthProfiles()` — the query params
3134
+ * are forwarded verbatim from the request. Omitted ⇒ the route/tool
3135
+ * report "not enabled" (same convention as `listAgentAdapters`). */
3136
+ type CatalogModelsLister = (query: CatalogModelsQuery) => Promise<CatalogModelsResponse>;
2485
3137
  interface AuthOptions {
2486
3138
  mode: "none" | "bearer";
2487
3139
  token?: string;
@@ -2756,6 +3408,131 @@ declare function declaredPresetToProviderPreset(decl: DeclaredAdapterPreset): Pr
2756
3408
  */
2757
3409
  declare function listPresets(env?: Record<string, string | undefined>, adapterPresets?: readonly DeclaredAdapterPreset[]): AdapterEntry<PresetInfo>[];
2758
3410
 
3411
+ /**
3412
+ * Canonical-posture layer (SPEC §3.4a, build step 2c, out-of-repo design doc
3413
+ * `agentproto-session-config-axes/SPEC.md`). Pure: a map + preambles + a
3414
+ * resolution helper, no I/O, no daemon wiring. It is the piece that makes the
3415
+ * agentproto-canonical posture vocabulary (`CanonicalPosture` from
3416
+ * `./session-config.js`) portable across harnesses:
3417
+ *
3418
+ * 1. **Native enforcement.** A canonical posture is resolved to a *harness*
3419
+ * `SessionModeId` when the session's advertised `availableModes`
3420
+ * (`SessionModeState.availableModes`, surfaced read-only by the ACP
3421
+ * capability layer landed in #482, `packages/acp/src/client/index.ts`) has
3422
+ * an equivalent. That mode is switched live via `setSessionMode` (step 5)
3423
+ * or applied on restart (step 6) — a real permission boundary.
3424
+ * 2. **Prompt-injection fallback.** When no advertised mode matches, the
3425
+ * posture is honoured as an injected system-prompt PREAMBLE — advisory,
3426
+ * NOT a permission boundary (SPEC risk Rw). This module returns the
3427
+ * preamble text; steps 5/6 ride it onto the system prompt at spawn.
3428
+ *
3429
+ * This module deliberately does NOT implement the live-posture verb (step 5) or
3430
+ * the posture restart-override (step 6) — it only tells them WHICH of the two
3431
+ * apply-paths a given (posture × session) resolves to, and supplies the
3432
+ * preamble for the fallback path.
3433
+ *
3434
+ * Read-surface dependency: `availableModes` comes from the #482 ACP capability
3435
+ * read surface (`AcpClientSession.availableModes` →
3436
+ * `AgentCliRuntimeSession.availableModes`) — this module consumes it, never
3437
+ * reimplements it.
3438
+ */
3439
+
3440
+ /**
3441
+ * Per-posture system-prompt preamble — the prompt-injection fallback applied
3442
+ * when a canonical posture has NO native advertised mode on the current harness
3443
+ * (SPEC §3.4a). This is ADVISORY text the model can ignore, never a permission
3444
+ * boundary (SPEC risk Rw) — a consumer must present a prompt-enforced posture
3445
+ * as "advisory", never imply it sandboxes tools.
3446
+ *
3447
+ * `"default"` has no preamble: it is the neutral posture (no constraint to
3448
+ * announce), so it resolves to a no-op rather than an injected string when the
3449
+ * harness advertises no native `default` mode.
3450
+ */
3451
+ declare const POSTURE_PREAMBLES: Readonly<Record<Exclude<CanonicalPosture, "default">, string>>;
3452
+ /**
3453
+ * agentproto-canonical posture → the harness `SessionModeId`s that mean the
3454
+ * same thing. This is the "canonical posture ↔ advertised ACP mode id" map
3455
+ * (SPEC §3.4a): the daemon resolves a portable posture onto whichever native
3456
+ * mode a given harness happens to advertise, since harnesses spell the same
3457
+ * concept differently — claude-code's ACP wrapper uses `acceptEdits` /
3458
+ * `bypassPermissions` (`adapters/claude-code/src/index.ts:216,223`), the
3459
+ * manifest posture ids use `accept-edits` / `bypass-permissions`, codex uses
3460
+ * `full-access`, opencode uses `build`, etc.
3461
+ *
3462
+ * Matching is case- and separator-insensitive (see {@link normalizeModeId}), so
3463
+ * ONE readable spelling here covers every casing/hyphenation a harness might
3464
+ * advertise — `"accept-edits"` already matches claude-code's `acceptEdits`, so
3465
+ * both spellings need not be listed. Aliases are disjoint across postures — no
3466
+ * advertised id maps to two canonical postures — so {@link canonicalForModeId}
3467
+ * is unambiguous. Kept consistent with the legacy `mode`-id normalization in
3468
+ * `session-config.ts` (`POSTURE_MODE_VALUES`).
3469
+ */
3470
+ declare const POSTURE_NATIVE_ALIASES: Readonly<Record<CanonicalPosture, readonly string[]>>;
3471
+ /**
3472
+ * Normalize a mode id for matching: lowercase and strip every non-alphanumeric
3473
+ * character, so `"acceptEdits"`, `"accept-edits"`, and `"Accept_Edits"` all
3474
+ * collapse to `"acceptedits"`. Lets one readable alias in
3475
+ * {@link POSTURE_NATIVE_ALIASES} cover any casing/separator a harness advertises.
3476
+ */
3477
+ declare function normalizeModeId(id: string): string;
3478
+ /**
3479
+ * Inverse of the alias map: which canonical posture (if any) a harness mode id
3480
+ * normalizes to. `undefined` for a harness-specific mode the canonical
3481
+ * vocabulary doesn't name (e.g. opencode's `architect`) — such a mode is still
3482
+ * offerable as a raw `{ harnessModeId }` posture, it just has no portable name.
3483
+ */
3484
+ declare function canonicalForModeId(modeId: string): CanonicalPosture | undefined;
3485
+ /**
3486
+ * Find the advertised harness mode that natively enforces `posture`, or
3487
+ * `undefined` if none does.
3488
+ *
3489
+ * - A `CanonicalPosture` matches an advertised mode whose id normalizes to one
3490
+ * of that posture's aliases.
3491
+ * - A raw `{ harnessModeId }` matches an advertised mode with the (normalized)
3492
+ * same id — it's already a native id, we only confirm the session still
3493
+ * advertises it.
3494
+ */
3495
+ declare function findNativeMode(posture: Posture, availableModes: readonly SessionMode[]): SessionMode | undefined;
3496
+ /**
3497
+ * How a requested posture resolves against a session's advertised modes.
3498
+ *
3499
+ * - `native` — an advertised mode enforces it; switch via `setSessionMode`
3500
+ * (live, step 5) or apply on restart (step 6). A real permission boundary.
3501
+ * - `prompt` — no native mode; honour it as an injected system-prompt preamble
3502
+ * (advisory only, SPEC risk Rw). Rides the system prompt, so it applies at
3503
+ * spawn/restart, never live.
3504
+ * - `noop` — the `default` (neutral) posture with no advertised `default` mode:
3505
+ * nothing to enforce and nothing to announce.
3506
+ * - `unavailable` — a raw `{ harnessModeId }` the session no longer advertises;
3507
+ * it has no canonical name, so there is no preamble to fall back to. The
3508
+ * caller surfaces this rather than silently doing nothing.
3509
+ */
3510
+ type PostureResolution = {
3511
+ readonly kind: "native";
3512
+ readonly mode: SessionMode;
3513
+ } | {
3514
+ readonly kind: "prompt";
3515
+ readonly posture: Exclude<CanonicalPosture, "default">;
3516
+ readonly preamble: string;
3517
+ } | {
3518
+ readonly kind: "noop";
3519
+ readonly posture: "default";
3520
+ } | {
3521
+ readonly kind: "unavailable";
3522
+ readonly requestedModeId: string;
3523
+ };
3524
+ /**
3525
+ * Resolve a requested posture against the session's advertised `availableModes`
3526
+ * (from the #482 read surface) into one of the {@link PostureResolution} arms.
3527
+ * Pure and total — the single decision function steps 5 (live) and 6 (restart
3528
+ * override) call to learn whether a posture pick is native-enforced or
3529
+ * prompt-injected, without either of them re-deriving the map.
3530
+ *
3531
+ * Native enforcement is always preferred: a canonical posture resolves to
3532
+ * `prompt` ONLY when the harness advertises no equivalent mode.
3533
+ */
3534
+ declare function resolvePosture(posture: Posture, availableModes: readonly SessionMode[]): PostureResolution;
3535
+
2759
3536
  /**
2760
3537
  * Per-workspace state buckets — AIP-46 §State partitioning.
2761
3538
  *
@@ -2867,7 +3644,18 @@ declare function resolveBucketSlug(workspaceSlug: string | undefined | null, reg
2867
3644
  * silently pooling into `default` until restart. Never throws: a
2868
3645
  * missing/corrupt registry degrades to "nothing is registered", i.e.
2869
3646
  * everything lands in `default` — today's pooled behaviour, which is
2870
- * the right failure direction. */
3647
+ * the right failure direction.
3648
+ *
3649
+ * A registry read that fails transiently (a race with a concurrent
3650
+ * `saveWorkspacesConfig` tmp+rename) is indistinguishable here from one
3651
+ * that's genuinely empty — but the persist path never actually needs to
3652
+ * tell them apart: `sessions.ts`'s `sourceBucketOf` already keeps a
3653
+ * loaded row homed to the bucket it came from regardless of what this
3654
+ * returns, so a bad read here degrades a NEW session's placement (still
3655
+ * `default`, same as always) and nothing else. See the 2026-07-18
3656
+ * bucket-clobber incident for why that distinction matters for loaded
3657
+ * rows, and `sourceBucketOf`'s docblock in `sessions.ts` for where it's
3658
+ * actually enforced. */
2871
3659
  declare function readRegisteredSlugs(configPath?: string): ReadonlySet<string>;
2872
3660
  /** Bucket directories that exist on disk. Order is not meaningful. */
2873
3661
  declare function listBuckets(root: string): string[];
@@ -2908,6 +3696,118 @@ declare function migrateLegacySessionsFile(opts: {
2908
3696
  registered: ReadonlySet<string>;
2909
3697
  }): MigrationMarker | null;
2910
3698
 
3699
+ /**
3700
+ * Persisted conversation index — the session ↔ native-transcript link,
3701
+ * stored instead of re-derived (DESIGN.md `agentproto-state-multitenancy`
3702
+ * §6).
3703
+ *
3704
+ * Today the link between an agentproto session and the original
3705
+ * claude/hermes conversation file is DERIVED on every read, through
3706
+ * `claudeCodeProjectDir`/`claudeProjectSlug` (conversation-store.ts) — and
3707
+ * until that function was fixed, derived *wrong* for any cwd containing a
3708
+ * dot. Re-deriving is also blind to cwd drift (a worktree that moved) and
3709
+ * carries no record of subagent transcripts at all.
3710
+ *
3711
+ * This module is the fix: an **append-only** `conversations.jsonl`, one
3712
+ * per workspace bucket (co-located with that bucket's `sessions.json`,
3713
+ * `workspace-buckets.ts`), upserted by `sessionId` — readers take the LAST
3714
+ * record per id. Append-only is deliberate, not incidental: it is immune
3715
+ * to the wholesale-rewrite clobber `sessions.ts`'s `persistSnapshot`
3716
+ * already has (see DESIGN.md §1) — a writer can only ever ADD a line, so a
3717
+ * second process racing a first can't shrink what's on disk.
3718
+ *
3719
+ * Write points (best-effort, mirroring how `transcriptWriter`/
3720
+ * `persistSnapshot` never throw into the turn path): session spawn, an
3721
+ * ACP-level resume, and a native graceful-exit resume-hint — see
3722
+ * `sessions.ts`'s calls into `recordConversationLink`.
3723
+ *
3724
+ * No tenant layer yet — the index lives directly under the workspace
3725
+ * bucket dir today (`bucketDir(bucketsRoot, slug)/conversations.jsonl`);
3726
+ * DESIGN.md §9 notes it migrates under `tenants/<t>/…` at a later PR.
3727
+ */
3728
+ /** The claude-code native store: one jsonl per conversation, plus any
3729
+ * subagent transcripts nested under `<sessionId>/subagents/agent-*.jsonl`. */
3730
+ interface ClaudeNativeRef {
3731
+ kind: "claude-jsonl";
3732
+ path: string;
3733
+ subagents: string[];
3734
+ }
3735
+ /** The hermes native store: a row in a shared sqlite db, keyed by the same
3736
+ * id agentproto already records as `adapterSessionId` — no path to derive. */
3737
+ interface HermesNativeRef {
3738
+ kind: "hermes-sqlite";
3739
+ dbPath: string;
3740
+ rowId: string;
3741
+ }
3742
+ type ConversationNativeRef = ClaudeNativeRef | HermesNativeRef;
3743
+ interface ConversationIndexRecord {
3744
+ sessionId: string;
3745
+ workspace: string;
3746
+ cwd: string;
3747
+ adapterSlug: string;
3748
+ adapterSessionId: string;
3749
+ /** Absent when `adapterSlug` has no known native store (an adapter
3750
+ * outside claude-code/hermes) — the record still links session ↔
3751
+ * cwd ↔ adapterSessionId, it just can't point at a native file. */
3752
+ native?: ConversationNativeRef;
3753
+ agentprotoTranscript: string;
3754
+ title?: string;
3755
+ startedAt: string;
3756
+ endedAt?: string;
3757
+ }
3758
+ /** Absolute path to a workspace bucket's conversation index. Sibling of
3759
+ * `bucketSessionsFile` in `workspace-buckets.ts`, same root/slug rule. */
3760
+ declare function conversationIndexPath(bucketsRoot: string, slug: string): string;
3761
+ interface ResolveNativeLinkInput {
3762
+ cwd: string;
3763
+ adapterSlug: string;
3764
+ adapterSessionId: string;
3765
+ }
3766
+ /**
3767
+ * Resolve where THIS provider keeps this conversation, computed once (at
3768
+ * a write point) rather than re-derived on every read. Claude-code's path
3769
+ * uses the corrected `claudeProjectSlug` via `claudeCodeProjectDir` — the
3770
+ * whole point of pairing this module with the encoder fix in the same PR.
3771
+ * Returns `undefined` for an adapter with no known native store (e.g.
3772
+ * mastracode, claude-sdk) — a record without `native` is still useful
3773
+ * (session ↔ cwd ↔ adapterSessionId), it just has nothing further to add.
3774
+ */
3775
+ declare function resolveNativeLink(input: ResolveNativeLinkInput): Promise<ConversationNativeRef | undefined>;
3776
+ /** Append one record. Never rewrites — the caller decides when a fresher
3777
+ * snapshot of the same session is worth a new line; readers always take
3778
+ * the last one. Creates the bucket dir if this is its first conversation
3779
+ * record (a bucket that has only ever held terminal/command sessions
3780
+ * won't have one yet). */
3781
+ declare function appendConversationRecord(bucketsRoot: string, slug: string, record: ConversationIndexRecord): Promise<void>;
3782
+ /** Read one bucket's index, upserted by `sessionId` — a line later in the
3783
+ * file always wins over an earlier one for the same id, append-only's
3784
+ * upsert semantics. A malformed line (partial write, hand-edited garbage)
3785
+ * is skipped, not fatal — the rest of the file still reads. `[]` (not a
3786
+ * throw) when the bucket has no index yet. */
3787
+ declare function readConversationIndex(bucketsRoot: string, slug: string): Promise<ConversationIndexRecord[]>;
3788
+ /** Forward lookup within one already-known bucket. */
3789
+ declare function findConversationRecord(bucketsRoot: string, slug: string, sessionId: string): Promise<ConversationIndexRecord | undefined>;
3790
+ interface LocatedConversation {
3791
+ workspace: string;
3792
+ record: ConversationIndexRecord;
3793
+ }
3794
+ /** Forward lookup: sessionId → its record, scanning every bucket (the
3795
+ * caller doesn't have to know which workspace a session landed in).
3796
+ * `undefined` when no bucket's index has ever recorded this session. */
3797
+ declare function locateConversationBySessionId(bucketsRoot: string, listBuckets: () => string[], sessionId: string): Promise<LocatedConversation | undefined>;
3798
+ interface LocatedConversationByPath extends LocatedConversation {
3799
+ /** Set when the match was a subagent transcript rather than the root
3800
+ * conversation file — the path that actually matched. */
3801
+ matchedSubagentPath?: string;
3802
+ }
3803
+ /** Reverse lookup: a native jsonl path (root conversation OR a subagent
3804
+ * transcript) → the agentproto session/workspace that owns it. Scans
3805
+ * every bucket's index; paths are compared after `path.resolve` so a
3806
+ * relative or `..`-bearing argument still matches an absolute recorded
3807
+ * one. Only meaningful for claude-code's `native.path`/`native.subagents`
3808
+ * — hermes has no per-conversation file to reverse-lookup from. */
3809
+ declare function locateConversationByNativePath(bucketsRoot: string, listBuckets: () => string[], nativePath: string): Promise<LocatedConversationByPath | undefined>;
3810
+
2911
3811
  /** A journal-file StepCache scoped to one cacheKey. Best-effort: read/parse
2912
3812
  * failures degrade to "no cache" (a miss), never throw into the run. */
2913
3813
  declare function createFileStepCache(cacheKey: string, opts?: {
@@ -3032,6 +3932,44 @@ declare function readRuntimeMeta(workspace: string): Promise<{
3032
3932
  */
3033
3933
  declare function sweepStaleRuntimeMetas(workspaces: readonly string[], currentWorkspace: string): Promise<string[]>;
3034
3934
 
3935
+ /**
3936
+ * Conversation as pivot: one store abstraction per provider, read on any
3937
+ * session. A "conversation" is the provider's own persisted session — the
3938
+ * durable object. An agentproto session (ACP or PTY) is just an attachment
3939
+ * to it. `CONVERSATION_STORES` is the single source of truth for "where does
3940
+ * this provider keep its conversations, and how do I read/attach to one" —
3941
+ * `RESUME_STRATEGIES` (resume-strategies.ts) and `EXPORT_STRATEGIES`
3942
+ * (transcript-export.ts) are thin adapters over it so the two historically
3943
+ * drifted tables can never diverge again.
3944
+ */
3945
+
3946
+ /**
3947
+ * Claude Code's own `cwd` → project-dir-name encoder.
3948
+ *
3949
+ * The real rule (verified empirically against `~/.claude/projects/`
3950
+ * directory names for dozens of known cwds, including dotted worktree
3951
+ * paths and macOS temp dirs): every character that is NOT `[a-zA-Z0-9]`
3952
+ * is replaced 1:1 with `-`. Crucially, runs of consecutive non-alnum
3953
+ * characters are NOT collapsed — each input character produces exactly
3954
+ * one output character.
3955
+ *
3956
+ * This used to be `cwd.replace(/\//g, "-")` — slashes only. That's wrong
3957
+ * for any cwd containing a dot (every `.agentproto` worktree, any
3958
+ * dotdir), an underscore-prefixed segment, a space, etc., because those
3959
+ * characters are left untouched instead of becoming `-`, so the computed
3960
+ * directory never matches the one claude actually created.
3961
+ *
3962
+ * Reconstruction proof (both real, both `ls ~/.claude/projects/`-verified):
3963
+ * cwd /Users/jeremy/.agentproto/worktrees/ts/gc-fresh-hold
3964
+ * → -Users-jeremy--agentproto-worktrees-ts-gc-fresh-hold
3965
+ * (note the "--" from "/." — one dash for "/", one for ".", not collapsed)
3966
+ *
3967
+ * cwd /Volumes/SSDExternalMacStudio/Code/_agentproto-worktrees/adapter-claude-sdk
3968
+ * → -Volumes-SSDExternalMacStudio-Code--agentproto-worktrees-adapter-claude-sdk
3969
+ * (same double-dash shape, this time from "/_")
3970
+ */
3971
+ declare function claudeProjectSlug(cwd: string): string;
3972
+
3035
3973
  /**
3036
3974
  * Read-only probe: does a credential actually RESOLVE for an adapter slug's
3037
3975
  * billing auth? Used by `adapter_list` to report an HONEST status instead of
@@ -3194,6 +4132,11 @@ interface CreateGatewayOptions {
3194
4132
  * `GET /adapters` HTTP route + `adapter_list` MCP tool so UIs
3195
4133
  * can discover what's installed on the host. */
3196
4134
  listAgentAdapters?: AgentAdapterLister;
4135
+ /** Optional catalog lister — when provided, enables
4136
+ * `GET /catalog/models` HTTP route + `catalog_models` MCP tool
4137
+ * (SPEC §5) so UIs can discover every runnable model + route without
4138
+ * trial-and-error against a spawn. */
4139
+ listCatalogModels?: CatalogModelsLister;
3197
4140
  /** Optional browser adapter resolver — when provided, enables the
3198
4141
  * `start_browser` MCP tool (launches Camofox / Bureau / Chromium). */
3199
4142
  resolveBrowserAdapter?: BrowserAdapterResolver;
@@ -3334,4 +4277,4 @@ interface GatewayHandle {
3334
4277
  */
3335
4278
  declare function createGateway(opts: CreateGatewayOptions): Promise<GatewayHandle>;
3336
4279
 
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 };
4280
+ 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 };