@agentproto/runtime 0.7.0 → 0.8.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/README.md CHANGED
@@ -44,6 +44,7 @@ A per-boot bearer token is generated automatically and written into `<workspace>
44
44
  | Adapter discovery | `GET /adapters` | When `listAgentAdapters` is wired |
45
45
  | Sessions list | `GET /sessions` / `GET /sessions/:id` | id-or-name in `:id` |
46
46
  | Agent spawn | `POST /sessions/agent` | Long-lived ACP agent (needs `resolveAgentAdapter`) |
47
+ | Interrupt turn | `POST /sessions/:id/interrupt` | Cancel the in-flight turn; session stays alive |
47
48
  | **PTY spawn** | **`POST /sessions/terminal`** | Needs `spawnPty` factory |
48
49
  | **PTY attach** | **`WS /sessions/:id/pty`** | JSON frames `{kind:data|input|resize|exit|ping|pong}`; multi-subscriber, min-size resize, ring-buffer replay |
49
50
  | SSE attach | `GET /sessions/:id/stream` | Line-by-line text events |
@@ -0,0 +1,569 @@
1
+ import { CatalogProvider } from '@agentproto/model-catalog';
2
+
3
+ /**
4
+ * Pure resolver for `~/.agentproto/config.json`'s `defaults` block —
5
+ * computes the effective `skills` + `options` + `auth` for an `agent_start`
6
+ * spawn before adapter-specific normalization. No fs, no adapter I/O, so
7
+ * it's unit-testable in isolation from `session-spawn.ts` (which owns the
8
+ * fs read + the adapter-manifest lookup).
9
+ *
10
+ * Precedence (lowest → highest): global `defaults` < `defaults.adapters.
11
+ * <slug>` < the explicit `agent_start` call.
12
+ * - `options`: shallow-merged maps, later (higher-precedence) keys win.
13
+ * - `skills`: global ∪ per-adapter when the caller didn't pass `skills`
14
+ * at all; an explicit `skills` (even `[]`) REPLACES the union rather
15
+ * than merging into it — a deliberate exact set, mirroring how an
16
+ * explicit `mcpServers: []` opts out of the hermes default in
17
+ * `session-spawn.ts`.
18
+ * - `auth`: surfaces the RAW billing-auth material (requested mode, both
19
+ * candidate credentials, per-spawn provider pin, and the `explicit`
20
+ * signal) so the descriptor-aware resolver ({@link resolveAuthSpec},
21
+ * which also needs the adapter's provider/subscription descriptor +
22
+ * providers.json) can decide the final mode, env var, scrub set, and
23
+ * credential source. Credentials are named in config or the provider
24
+ * store — never read from the ambient shell env.
25
+ */
26
+
27
+ /**
28
+ * Deterministic billing-auth config for one adapter slug (today, only
29
+ * claude-code interprets it — see `AgentCliStartOptions.auth` in
30
+ * `@agentproto/driver-agent-cli`). EXPLICIT credential selection, not
31
+ * scrub-by-absence: `token`/`apiKey` are the actual secret values, named
32
+ * here (or supplied per-spawn) rather than inherited from the launching
33
+ * shell. Never logged; only a fingerprint (see {@link credentialFingerprint})
34
+ * is ever surfaced back to a caller.
35
+ */
36
+ interface DefaultsAdapterAuthConfig {
37
+ /** `"subscription"` or `"api-key"`. Omitted ⇒ the resolver picks by
38
+ * ordered preference (subscription first for adapters that support it —
39
+ * see {@link resolveAuthSpec}), never a hardcoded default. */
40
+ mode?: "subscription" | "api-key";
41
+ /** The subscription bearer token for `"subscription"` mode — minted via
42
+ * `claude setup-token` (bills the Max/Pro subscription, not API credits),
43
+ * SET to the adapter's `authSubscription.setEnv`. */
44
+ token?: string;
45
+ /** Explicit API key for `"api-key"` mode, SET to `providerEnvVar(provider)`.
46
+ * Wins over the `providers.json` store key for the same provider. */
47
+ apiKey?: string;
48
+ /** Per-spawn provider PIN — overrides the adapter's fixed provider and the
49
+ * model-derived provider (the sharp edge for by-model routers whose config
50
+ * routes a catalog-"anthropic" model elsewhere). A `CatalogProvider` id. */
51
+ provider?: CatalogProvider;
52
+ }
53
+ interface DefaultsAdapterConfig {
54
+ skills?: string[];
55
+ options?: Record<string, boolean | number | string>;
56
+ auth?: DefaultsAdapterAuthConfig;
57
+ }
58
+ /** Shape of `config.json`'s top-level `defaults` block. */
59
+ interface SpawnDefaultsConfig {
60
+ skills?: string[];
61
+ options?: Record<string, boolean | number | string>;
62
+ adapters?: Record<string, DefaultsAdapterConfig>;
63
+ /** Depth cutoff for the role-derived default (see `resolveRole` in
64
+ * `role.ts`) applied when an `agent_start` call omits `role`:
65
+ * `depth < cutoff` → supervisor, `depth >= cutoff` → executor.
66
+ * Default 1 (root spawns keep today's unrestricted behaviour; any
67
+ * spawn made THROUGH an orchestrator defaults to executor). Tune
68
+ * this up (e.g. to a large number) to restore the old permissive
69
+ * behaviour for existing deep spawns wholesale. */
70
+ defaultRoleDepthCutoff?: number;
71
+ /** Trust-boundary cap on pack-carried roles (see `role-registry.ts`'s
72
+ * `loadRoleRegistry`): a role pack whose `toolPolicy.delegation` is
73
+ * `"allow"` at a level ABOVE this cap has it forced to `"deny"` —
74
+ * the pack can still declare the intent, the daemon just refuses to
75
+ * grant it. Lets an operator install third-party role packs without
76
+ * trusting every one of them to self-grant delegation. Undefined
77
+ * (default) ⇒ no cap, any pack-declared level may carry
78
+ * `delegation: "allow"` (back-compat: #214 had no such knob). */
79
+ maxGrantableDelegation?: number;
80
+ /** Default per-session Langfuse tracing opt-in when an `agent_start` call
81
+ * omits `trace`. Default false — sessions trace only when they opt in or
82
+ * this is on. See `filterSessionObserver` / `SpawnAgentInput.trace`. */
83
+ langfuseTracing?: boolean;
84
+ /** Redactor slug applied to traced session content before it's sent to
85
+ * Langfuse (see `@agentproto/redaction`'s registry). Default "secrets"
86
+ * (deny-list by key + value-scan for secret shapes). */
87
+ traceRedactor?: string;
88
+ }
89
+ interface ResolveSpawnDefaultsInput {
90
+ /** Explicit-call `skills`. Undefined ⇒ caller expressed no preference,
91
+ * fall through to the config union. Provided ⇒ replaces it outright. */
92
+ skills?: string[];
93
+ /** Explicit-call AIP-45 `options` map — wins per-key over both the
94
+ * global and per-adapter config defaults. */
95
+ options?: Record<string, boolean | number | string>;
96
+ /** Explicit-call `agent_start.auth` override. `mode` wins over
97
+ * `defaults.adapters.<slug>.auth.mode`; the credential field matching the
98
+ * RESOLVED mode wins over the matching config field. Undefined ⇒ falls
99
+ * through entirely to the per-adapter config default. */
100
+ auth?: DefaultsAdapterAuthConfig;
101
+ }
102
+ interface ResolvedSpawnDefaults {
103
+ skills: string[];
104
+ options: Record<string, boolean | number | string>;
105
+ /** RAW billing-auth material (config precedence applied) — fed to the
106
+ * descriptor-aware {@link resolveAuthSpec}, which owns the final mode /
107
+ * env / scrub / credential-source decision. Both candidate credentials
108
+ * are surfaced (NOT collapsed to one), since the ordered-mode selection
109
+ * needs to know which are available before it picks the mode. */
110
+ auth: ResolvedSpawnAuthMaterial;
111
+ }
112
+ interface ResolvedSpawnAuthMaterial {
113
+ /** Operator-requested mode (per-spawn > per-adapter config), or undefined
114
+ * ⇒ let the resolver pick by ordered preference. */
115
+ requestedMode?: "subscription" | "api-key";
116
+ /** True when the operator explicitly configured `auth` (per-spawn OR in
117
+ * `defaults.adapters.<slug>.auth`). The ONLY way to tell "set mode, no
118
+ * key" (fail-fast) from "set nothing" (ambient) — both give no credential.
119
+ * DECISION 5. */
120
+ explicit: boolean;
121
+ /** Subscription bearer token (per-spawn > config), if configured. */
122
+ subscriptionCredential?: string;
123
+ /** Explicit API key (per-spawn > config), if configured — distinct from the
124
+ * providers.json store key the resolver fetches separately. */
125
+ apiKeyCredential?: string;
126
+ /** Per-spawn provider pin, if given. */
127
+ provider?: CatalogProvider;
128
+ }
129
+ declare function resolveSpawnDefaults(defaults: SpawnDefaultsConfig | undefined, adapterSlug: string, input: ResolveSpawnDefaultsInput): ResolvedSpawnDefaults;
130
+ /**
131
+ * Derive a SAFE, non-secret fingerprint for a resolved auth credential —
132
+ * NEVER the raw value — for recording on the session descriptor / surfacing
133
+ * in `agentproto sessions --watch` and `agent_sessions_list` (the
134
+ * "verifiability" requirement: answer "what was used" without exposing the
135
+ * secret). Format: `<mode> · <shape-prefix>…<last4>` when the shape is known
136
+ * (e.g. `subscription · sk-ant-oat…3f9c`), else `<mode> · …<last4>`.
137
+ *
138
+ * The shape marker is matched from a PUBLIC key-prefix table (longest-match),
139
+ * not derived from `mode` — see {@link CREDENTIAL_FINGERPRINT_PREFIXES}. Only
140
+ * the matched prefix + the last 4 characters are ever surfaced, never the
141
+ * middle (mirrors GitHub's `ghp_…abcd` style).
142
+ */
143
+ declare function credentialFingerprint(mode: "subscription" | "api-key", credential: string): string;
144
+ /**
145
+ * The adapter's billing-auth capability, projected from its AIP-45 manifest
146
+ * (`provider` / `authEnforce` / `authSubscription`) by the host resolver. The
147
+ * runtime reads THIS, never the manifest directly — keeping the LLM-catalog
148
+ * coupling in the runtime and the driver mechanical.
149
+ */
150
+ interface AdapterAuthDescriptor {
151
+ /** FIXED provider for a single-provider adapter; omitted for by-model
152
+ * routers (provider then derives from the requested model). */
153
+ provider?: CatalogProvider;
154
+ /** Enforcement policy — `"always"` engages every spawn (claude-code's
155
+ * #312 fail-fast); `"when-configured"` (default) only when `explicit`. */
156
+ authEnforce?: "always" | "when-configured";
157
+ /** Subscription (OAuth/bearer) support. Presence ⇒ the adapter supports
158
+ * `"subscription"` mode. Mirrors the driver's `AgentCliAuthSubscription`. */
159
+ authSubscription?: {
160
+ setEnv: string;
161
+ conflictEnv?: string[];
162
+ unsetEnvAdd?: string[];
163
+ };
164
+ }
165
+ /** The fully-resolved spec the driver applies mechanically. Structurally
166
+ * matches `@agentproto/driver-agent-cli`'s `ResolvedAuthSpec` (each package
167
+ * owns its own copy; the object flows across the boundary by shape). */
168
+ interface ResolvedAuthSpec {
169
+ mode: "subscription" | "api-key";
170
+ credential?: string;
171
+ setEnv: string;
172
+ unsetEnv: string[];
173
+ explicit: boolean;
174
+ enforce: "always" | "when-configured";
175
+ /** True when NEITHER subscription nor api-key had any credential
176
+ * available and `mode` above is therefore an arbitrary fallback pick —
177
+ * never true when `mode` came from an explicit request or an actually-
178
+ * available credential. Optional (undefined ⇒ false) so existing
179
+ * callers/fixtures that predate this field keep working unchanged.
180
+ * Exists so a fail-fast message can enumerate BOTH auth paths instead of
181
+ * presenting the fallback mode as though the user configured it — the
182
+ * root cause of a zero-credential user being told to buy a subscription
183
+ * they never asked for. */
184
+ neitherConfigured?: boolean;
185
+ /** Non-authenticating hint: true when `providers.json` HAS a key for the
186
+ * resolved provider that is currently being ignored because auth isn't
187
+ * explicitly configured (no `defaults.adapters.<slug>.auth` block — the
188
+ * `explicit` gate above, PR #321). NEVER used to authenticate — set by
189
+ * the caller (session-spawn.ts) as a read-only peek, purely so the
190
+ * fail-fast message can say "you already have a key, it's just not
191
+ * wired in" instead of staying silent about it. `resolveAuthSpec` itself
192
+ * never sets this (it does no I/O). */
193
+ ignoredApiKeyInStore?: boolean;
194
+ }
195
+ /** Where the resolved credential came from — the observable billing axis
196
+ * (DECISION 10②), never inferred. */
197
+ type CredentialSource = "explicit-config" | "providers-store" | "none";
198
+ /**
199
+ * The OBSERVABLE echo (DECISION 9③ / 10②) — recorded on the session
200
+ * descriptor so a verifier checks the RESOLUTION, never the model's
201
+ * self-report. Never carries the raw credential (only its fingerprint).
202
+ */
203
+ interface AuthEcho {
204
+ provider: CatalogProvider;
205
+ authMode: "subscription" | "api-key";
206
+ credentialSource: CredentialSource;
207
+ setEnv: string;
208
+ fingerprint?: string;
209
+ }
210
+ /**
211
+ * Thrown when the operator requested a billing mode the adapter can't serve —
212
+ * today only `"subscription"` on an adapter with no `authSubscription`. A
213
+ * LOUD, distinct failure (DECISION 4②), never a silent downgrade to api-key.
214
+ */
215
+ declare class AuthResolutionError extends Error {
216
+ readonly code = "unsupported_auth_mode";
217
+ constructor(message: string);
218
+ }
219
+ interface ResolveAuthSpecInput {
220
+ descriptor: AdapterAuthDescriptor;
221
+ /** `input.model ?? adapter default model` — for model-derived provider. */
222
+ model?: string;
223
+ /** Per-spawn provider pin (`input.auth.provider`). */
224
+ requestedProvider?: CatalogProvider;
225
+ /** Operator-requested mode; undefined ⇒ ordered preference. */
226
+ requestedMode?: "subscription" | "api-key";
227
+ /** Operator explicitly configured `auth` (DECISION 5). */
228
+ explicit: boolean;
229
+ /** Subscription bearer credential, if configured. */
230
+ subscriptionCredential?: string;
231
+ /** Explicit api-key credential from config, if configured. */
232
+ apiKeyConfigCredential?: string;
233
+ /** api-key credential from `providers.json` (fetched by the caller). */
234
+ apiKeyStoreCredential?: string;
235
+ }
236
+ /**
237
+ * THE billing-auth resolver (DECISIONS 4, 6, 9, 10). Pure: given the adapter
238
+ * descriptor + raw config material + (caller-fetched) store key, it decides
239
+ * the provider, the mode (ordered — subscription over api-key when a
240
+ * subscription credential is present; a requested-but-unsupported mode throws
241
+ * `unsupported_auth_mode`), the env var to SET, the derived SCRUB set, and the
242
+ * credential + its source. Returns the driver `spec` + the observable `echo`,
243
+ * or `undefined` when no provider resolves (⇒ ambient, no injection — never
244
+ * guess). NEVER falls back to a default provider/model. Fail-loud on a
245
+ * configured-but-missing credential is deferred to the driver's mechanical
246
+ * apply (it engages then throws `missing_auth_credential`), so the `explicit`
247
+ * / `enforce` signals are carried through on the spec.
248
+ */
249
+ declare function resolveAuthSpec(input: ResolveAuthSpecInput): {
250
+ spec: ResolvedAuthSpec;
251
+ echo: AuthEcho;
252
+ } | undefined;
253
+ /** Manifest-declared AIP-45 option id + type, the minimum an adapter
254
+ * resolver needs to expose for `normalizeSkillsOption` below. Mirrors
255
+ * `AgentCliOption`'s `id`/`type` fields without importing
256
+ * `@agentproto/driver-agent-cli` into the runtime package. */
257
+ interface DeclaredAdapterOption {
258
+ id: string;
259
+ type: "boolean" | "integer" | "string" | "enum";
260
+ }
261
+ /**
262
+ * Fold the resolved `skills` list into `options.skills` using whatever
263
+ * shape the adapter's manifest declares for that option id (today, only
264
+ * `type: "string"` exists for a skills-shaped option — e.g. hermes'
265
+ * comma-joined `--skills a,b`). Adapters with no declared `skills` option
266
+ * (e.g. claude-code, which auto-discovers from `~/.claude/skills`) are a
267
+ * documented no-op — the effective skills list has nowhere to go, so it's
268
+ * dropped rather than guessing a flag the manifest didn't declare.
269
+ *
270
+ * An `options.skills` already present (from config defaults or the
271
+ * explicit call) is respected as-is and never overwritten here.
272
+ */
273
+ declare function normalizeSkillsOption(skills: string[], options: Record<string, boolean | number | string>, declaredOptions: readonly DeclaredAdapterOption[] | undefined): Record<string, boolean | number | string>;
274
+
275
+ /**
276
+ * `~/.agentproto/config.json` — single hand-editable JSON for the
277
+ * agentproto control plane's defaults. Sits alongside the existing
278
+ * surface files (workspaces.json, credentials.json, sessions.json):
279
+ *
280
+ * workspaces.json which directories are workspaces + which is active
281
+ * credentials.json tunnel host bearer tokens (mode 0600)
282
+ * sessions.json last-known snapshot of the registry (informational)
283
+ * config.json daemon defaults: port, bind, allowed origins,
284
+ * tunnel host, feature toggles
285
+ *
286
+ * Resolution order for every daemon knob is:
287
+ * 1. CLI flag (e.g. --port)
288
+ * 2. Env var (where one exists, e.g. AGENTPROTO_TOKEN)
289
+ * 3. config.json
290
+ * 4. Hardcoded default
291
+ *
292
+ * This means a user can call `agentproto config set daemon.port 18791`
293
+ * once and never re-pass `--port 18791` to `serve install` etc. CLI
294
+ * flags still win for one-off overrides.
295
+ *
296
+ * Schema is intentionally narrow + extensible — unknown keys are
297
+ * preserved on save (deep-merge), so a newer CLI writing a new
298
+ * field won't drop one an older CLI doesn't know about. No secrets
299
+ * here; credentials stay in credentials.json (mode 0600).
300
+ */
301
+
302
+ declare const CONFIG_VERSION: 1;
303
+ interface DaemonConfig {
304
+ /** Absolute path to the workspace the daemon binds to at boot. */
305
+ workspace?: string;
306
+ /** HTTP port. Default 18790. */
307
+ port?: number;
308
+ /** Bind addr. Default 127.0.0.1. */
309
+ bind?: string;
310
+ /** Trusted browser origins for mutating /sessions/* routes (in
311
+ * addition to the hardcoded localhost defaults). */
312
+ allowedOrigins?: string[];
313
+ /** When true, the daemon does NOT auto-trust localhost-on-any-port.
314
+ * Only origins explicitly listed in `allowedOrigins` are allowed.
315
+ * Pair with a curated list (e.g. `["http://localhost:3000"]`) for
316
+ * hardened setups. Default false. */
317
+ strictOrigins?: boolean;
318
+ /** Server label sent in tunnel hello frames. */
319
+ label?: string;
320
+ /** Bearer token gating the gateway at boot (`AuthOptions` with
321
+ * `mode: "bearer"`). Unlike `remote_enable`'s ephemeral quick-tunnel
322
+ * token, this one lives in config.json and survives daemon restarts.
323
+ * Set via `agentproto config set daemon.authToken <token>` (e.g.
324
+ * `$(openssl rand -hex 32)`). Unset ⇒ the gateway boots with
325
+ * `mode: "none"` — fully open on loopback, same as today. */
326
+ authToken?: string;
327
+ }
328
+ interface TunnelConfig {
329
+ /** Cloud WS URL. When set + autoconnect=true, `agentproto serve`
330
+ * bootstraps with `--connect <host>`. */
331
+ host?: string;
332
+ /** apt_ daemon token to present at the tunnel upgrade. When set,
333
+ * `agentproto serve` uses this BEFORE falling back to
334
+ * credentials.json — handy in profiles where the token-per-host
335
+ * mapping in credentials.json doesn't fit (e.g. host = tunnel URL
336
+ * but credentials were minted against the api URL). */
337
+ token?: string;
338
+ /** Whether `agentproto daemon start` connects the tunnel by
339
+ * default. v0 only — implementer can ignore until daemon needs it. */
340
+ autoconnect?: boolean;
341
+ /**
342
+ * Opt into end-to-end encryption of the outbound `serve --connect` tunnel
343
+ * (design: tunnel-e2e/v1). When true, the daemon negotiates a
344
+ * token-authenticated ephemeral handshake with the host and wraps the tunnel
345
+ * frames in an AEAD box, so even the trusted host loses plaintext visibility.
346
+ * The handshake authenticates both ends against the shared `tunnel.token`, so
347
+ * `token` MUST also be set. Fully backward-compatible: if the host doesn't
348
+ * advertise e2e (an older host), the daemon falls back to today's plaintext
349
+ * tunnel. Unset/false ⇒ plaintext, byte-identical to today. */
350
+ e2e?: boolean;
351
+ }
352
+ interface FeaturesConfig {
353
+ /** Hint that PTY is desired — informational; the daemon still
354
+ * detects node-pty's presence at runtime. */
355
+ pty?: boolean;
356
+ }
357
+ /**
358
+ * Policy for `agentproto worktree new` (PLAN.md §1.4 — config carries
359
+ * policy, never state; git itself is the authority for which worktrees
360
+ * exist). This is the fix for the sprawl the plan measured: 31 linked
361
+ * worktrees across 6 different parent directories, because there was no
362
+ * `worktree new` verb and therefore no convention to converge on.
363
+ */
364
+ /**
365
+ * How the daemon isolates a freshly-spawned agent session into its own git
366
+ * worktree (`agent_start.worktree`):
367
+ * - `"always"` — every depth-0 spawn is provisioned into a worktree,
368
+ * whether or not the caller asked. A cwd that is not in
369
+ * a git repo has nothing to isolate, so it spawns plain.
370
+ * - `"on-request"` — isolate ONLY when the caller passes `worktree`. This
371
+ * is the default and the back-compatible behaviour:
372
+ * today's callers pass nothing and spawn exactly where
373
+ * they asked.
374
+ * - `"never"` — isolation is off; an explicit `worktree` field is
375
+ * REJECTED (loud, not silently ignored) so a caller
376
+ * never believes it got an isolated tree it didn't.
377
+ */
378
+ type WorktreeIsolationMode = "always" | "on-request" | "never";
379
+ interface WorktreesConfig {
380
+ /**
381
+ * Absolute path new worktrees are created under. Layout:
382
+ * `<root>/<repoName>/<slug>`. Resolution order (mirrors every other
383
+ * knob in this file, see the module docblock): `--root` flag >
384
+ * `AGENTPROTO_WORKTREES_ROOT` env > this field > the hardcoded default
385
+ * `~/.agentproto/worktrees`. The default is a real single root, not
386
+ * "unconfigured" — `worktree new` converges to one place with zero
387
+ * setup, which is the only way the sprawl actually stops (the 6 roots
388
+ * that exist today are 6 people each inventing a default by hand).
389
+ */
390
+ root?: string;
391
+ /**
392
+ * Policy for `agent_start.worktree` isolation. Resolution order mirrors
393
+ * the module docblock (there is no CLI flag — this is a daemon-side
394
+ * policy read at spawn, not a per-invocation flag):
395
+ * `AGENTPROTO_WORKTREES_ISOLATION` env > this field > the hardcoded
396
+ * default `"on-request"`. `"on-request"` is deliberately the default:
397
+ * any other would break back-compat by isolating callers that never
398
+ * asked (see `worktree-isolation.ts`).
399
+ */
400
+ isolation?: WorktreeIsolationMode;
401
+ }
402
+ interface PairingConfig {
403
+ /** Rendezvous broker WS URL (ws:// or wss://) used by `pair offer` and by
404
+ * autoconnect on boot. When unset, `pair offer` requires an explicit
405
+ * `--rendezvous`. Mirrors `tunnel.host`. */
406
+ rendezvous?: string;
407
+ /** Whether the daemon opens standing rendezvous connections for every
408
+ * persisted pairing on boot (so a paired client can reconnect anytime).
409
+ * Mirrors `tunnel.autoconnect`. Default true when a rendezvous is set. */
410
+ autoconnect?: boolean;
411
+ }
412
+ /**
413
+ * A user-defined generic ACP agent — the config-file half of
414
+ * `AcpAgentSpec` (the slug is the record key in `acpAgents`, so it's
415
+ * omitted here). Any CLI that already speaks the Agent Client Protocol
416
+ * can be wired with zero code by declaring one of these under
417
+ * `acpAgents.<slug>` in `~/.agentproto/config.json`; the CLI's
418
+ * `acpHandleFromSpec` mints a runnable `AgentCliHandle` from it at
419
+ * resolve time (see `packages/cli/src/registry/acp-generic.ts`). Kept
420
+ * in this package (not the CLI's) so `config.ts` stays the single
421
+ * source of truth for the config surface without a cli→runtime→cli
422
+ * import cycle — the CLI's `AcpAgentSpec` extends this shape.
423
+ */
424
+ interface AcpAgentConfigEntry {
425
+ /** Display name. Defaults to the slug when omitted. */
426
+ name?: string;
427
+ /** One-line description surfaced in `agentproto acp ls`. */
428
+ description?: string;
429
+ /** Executable to spawn, e.g. "gemini". */
430
+ bin: string;
431
+ /** Extra argv appended after `bin`, e.g. ["--experimental-acp"]. */
432
+ bin_args?: string[];
433
+ /** Extra environment variables for the spawned process. */
434
+ env?: Record<string, string>;
435
+ /** Flag the CLI uses to receive the working directory, if it needs
436
+ * one passed explicitly (most ACP agents take cwd over the wire). */
437
+ cwd_flag?: string;
438
+ /** When true, advertise resumable + native-resume continuation. */
439
+ resumable?: boolean;
440
+ /** Known model ids for the agent (informational + validation hints). */
441
+ models?: {
442
+ default?: string;
443
+ allowed?: string[];
444
+ };
445
+ /** Shown when `bin` is missing from PATH (how to install the CLI). */
446
+ install_hint?: string;
447
+ }
448
+ /**
449
+ * Per-environment connection bundle. A profile overrides specific
450
+ * fields of the top-level `daemon` / `tunnel` / `features` blocks
451
+ * when selected via `--profile <name>` (or the top-level
452
+ * `activeProfile` setting). Missing fields fall through to the
453
+ * top-level config, so a profile only needs to declare what's
454
+ * different — typically just `tunnel.host` + `tunnel.token`.
455
+ *
456
+ * Example:
457
+ * {
458
+ * "daemon": { "workspace": "/code", "port": 18790 },
459
+ * "activeProfile": "local",
460
+ * "profiles": {
461
+ * "local": { "tunnel": { "host": "ws://localhost:3200/connect",
462
+ * "token": "apt_local", "autoconnect": true } },
463
+ * "prod": { "tunnel": { "host": "wss://tunnel.guilde.work/connect",
464
+ * "token": "apt_prod", "autoconnect": true },
465
+ * "daemon": { "port": 18791 } }
466
+ * }
467
+ * }
468
+ *
469
+ * Sandbox daemons generate per-sandbox profile entries at provision
470
+ * time so the daemon inside the sandbox boots with
471
+ * `agentproto serve --profile sandbox-<id>` and no extra plumbing.
472
+ */
473
+ interface ProfileConfig {
474
+ daemon?: DaemonConfig;
475
+ tunnel?: TunnelConfig;
476
+ features?: FeaturesConfig;
477
+ }
478
+ /**
479
+ * A user-defined named terminal/TUI preset stored in
480
+ * `~/.agentproto/config.json` under `terminalPresets`. Presets keep
481
+ * local launch recipes (argv, env, cwd, name/label) out of shared
482
+ * adapter manifests — e.g. pointing a Claude Code TUI at a local
483
+ * LLM gateway without retyping proxy env vars every spawn.
484
+ */
485
+ interface TerminalPreset {
486
+ /** Command + args to spawn. When provided, `sessions terminal` can
487
+ * be used without `-- <argv...>`. */
488
+ argv?: string[];
489
+ /** Extra environment variables layered on top of the daemon's
490
+ * inherited process.env. Values MUST be strings. */
491
+ env?: Record<string, string>;
492
+ /** Working directory for the PTY session. Relative paths are
493
+ * resolved against the current working directory at CLI time. */
494
+ cwd?: string;
495
+ /** Workspace slug used for cwd fallback when `cwd` is omitted. */
496
+ workspace?: string;
497
+ /** Stable session name passed to the registry (`name` field). */
498
+ name?: string;
499
+ /** Human-readable label surfaced in session listings. */
500
+ label?: string;
501
+ }
502
+ interface AgentprotoConfig {
503
+ version?: number;
504
+ daemon?: DaemonConfig;
505
+ tunnel?: TunnelConfig;
506
+ features?: FeaturesConfig;
507
+ /** E2E daemon-pairing defaults (rendezvous URL + autoconnect). */
508
+ pairing?: PairingConfig;
509
+ /** Where `agentproto worktree new` creates worktrees. See
510
+ * {@link WorktreesConfig}. */
511
+ worktrees?: WorktreesConfig;
512
+ /** Named connection profiles. See `ProfileConfig` for the merge
513
+ * semantics — a profile's fields shallow-override the top-level
514
+ * defaults for the selected run. */
515
+ profiles?: Record<string, ProfileConfig>;
516
+ /** Profile name to use when `--profile` isn't passed. When unset,
517
+ * the top-level `daemon` / `tunnel` blocks are used directly. */
518
+ activeProfile?: string;
519
+ /** Default `skills` + `options` auto-applied to every `agent_start`
520
+ * spawn — global and per-adapter. See `resolveSpawnDefaults` in
521
+ * `spawn-defaults.ts` for the merge precedence with an explicit call.
522
+ * Absent ⇒ current behaviour exactly (no regression). */
523
+ defaults?: SpawnDefaultsConfig;
524
+ /** User-defined generic ACP agents, keyed by adapter slug. Each entry
525
+ * is minted into a runnable handle by the CLI's `acpHandleFromSpec`
526
+ * when `resolveAdapter(slug)` finds no npm adapter package. User
527
+ * entries shadow the curated `ACP_CATALOG` on slug collision. */
528
+ acpAgents?: Record<string, AcpAgentConfigEntry>;
529
+ /** User-defined named terminal/TUI presets. Local-only; never
530
+ * packaged in shared adapter manifests or defaults. */
531
+ terminalPresets?: Record<string, TerminalPreset>;
532
+ /** Unknown keys preserved across save round-trips. */
533
+ [unknown: string]: unknown;
534
+ }
535
+ declare const CONFIG_FILE_PATH: () => string;
536
+ /**
537
+ * Load config.json. Returns an empty object (NOT null) when the file
538
+ * is missing, malformed, or unreadable — callers can `cfg.daemon?.port`
539
+ * safely without null-guards. Errors during a malformed-read are
540
+ * logged once so the user notices the file is broken without the
541
+ * daemon refusing to boot.
542
+ */
543
+ declare function loadConfig(path?: string): Promise<AgentprotoConfig>;
544
+ /**
545
+ * Write config.json atomically (tmp + rename) so a concurrent
546
+ * `agentproto config edit` can't half-truncate the file. Writes
547
+ * `next` AS-IS — callers are expected to pass the full desired
548
+ * state (loaded the existing config, mutated, passed it back).
549
+ *
550
+ * Earlier versions deep-merged with the on-disk file, but that made
551
+ * deletions impossible: `setConfigKey(cfg, "x", undefined)` would
552
+ * remove the key from memory, then the deep-merge would silently
553
+ * re-add it from disk. The current design trusts the caller's
554
+ * snapshot and uses atomic rename for crash safety.
555
+ */
556
+ declare function saveConfig(next: AgentprotoConfig, path?: string): Promise<void>;
557
+ /**
558
+ * Read a dot-notation key (`daemon.port`) out of a config. Returns
559
+ * `undefined` when any segment is missing.
560
+ */
561
+ declare function getConfigKey(cfg: AgentprotoConfig, dotted: string): unknown;
562
+ /**
563
+ * Set a dot-notation key in a config. Returns a new object — does
564
+ * NOT mutate. Creates intermediate objects as needed. Setting
565
+ * `value: undefined` is treated as a delete.
566
+ */
567
+ declare function setConfigKey(cfg: AgentprotoConfig, dotted: string, value: unknown): AgentprotoConfig;
568
+
569
+ export { type AdapterAuthDescriptor as A, type CredentialSource as C, type DeclaredAdapterOption as D, type FeaturesConfig as F, type PairingConfig as P, type ResolvedAuthSpec as R, type SpawnDefaultsConfig as S, type TerminalPreset as T, type WorktreeIsolationMode as W, type AuthEcho as a, AuthResolutionError as b, type DefaultsAdapterAuthConfig as c, type DefaultsAdapterConfig as d, type ResolvedSpawnAuthMaterial as e, type ResolvedSpawnDefaults as f, credentialFingerprint as g, resolveSpawnDefaults as h, type AcpAgentConfigEntry as i, type AgentprotoConfig as j, CONFIG_FILE_PATH as k, CONFIG_VERSION as l, type DaemonConfig as m, normalizeSkillsOption as n, type ProfileConfig as o, type TunnelConfig as p, type WorktreesConfig as q, resolveAuthSpec as r, getConfigKey as s, loadConfig as t, saveConfig as u, setConfigKey as v };