@agentproto/runtime 2.5.0 → 2.7.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
@@ -53,6 +53,22 @@ A per-boot bearer token is generated automatically and written into `<workspace>
53
53
  | SSE attach | `GET /sessions/:id/stream` | Line-by-line text events |
54
54
  | Kill / forget / gc | `POST /sessions/:id/kill`, `DELETE /sessions/:id`, `POST /sessions/gc` | SIGTERM, drop from registry, bulk archive terminal sessions |
55
55
 
56
+ ### MCP tool surface
57
+
58
+ The `/mcp` endpoint exposes the core toolset plus several opt-in / feature-gated families:
59
+
60
+ | Tool family | Notes |
61
+ |-------------|-------|
62
+ | `agent_start` / `agent_prompt` / `agent_output` / `agent_kill` / `agent_interrupt` | Long-lived ACP agent lifecycle |
63
+ | `terminal_start` / `terminal_input` / `terminal_output` / `terminal_kill` | Raw PTY sessions |
64
+ | `session_list` / `session_tree` / `session_usage` / `session_restart` / `session_rename` | Session management |
65
+ | `app_install` / `app_run` / `app_list` / `app_status` / `app_stop` / `app_apply` / `app_unapply` / `app_list_applied` | App-kit apps |
66
+ | `app_data_read` / `app_data_write` / `app_data_list` / `app_data_migrate` | App-scoped durable data plane (new) |
67
+ | `harness_preset_list` / `harness_preset_create` / `harness_preset_delete` / `harness_preset_set_default` | Persisted harness→auth-profile presets (new) |
68
+ | `workspace_brain_query` / `workspace_brain_status` / `workspace_brain_ingest` | Per-workspace transcript recall (new) |
69
+ | `conversation_export` | Export a daemon transcript to a target adapter's native store, e.g. `claude-code` (new) |
70
+ | `llm_endpoint_*` (`start`, `stop`, `status`, `set_upstream_link`, `list_links`) | Local LLM Endpoint proxy sidecar — only when `features.llmEndpoint` is enabled (new) |
71
+
56
72
  ### Auth model
57
73
 
58
74
  - `Authorization: Bearer <token>` required on **mutating** `/sessions/*` routes (POST/PATCH/DELETE) and the PTY WS upgrade.
@@ -1,8 +1,8 @@
1
1
  import { AuthProfile } from '@agentproto/auth';
2
- import { A as AdapterAuthDescriptor } from './spawn-defaults-7uHRnYH1.js';
3
- import { R as RouteSpec } from './session-config-DIf6wYYP.js';
2
+ import { A as AdapterAuthDescriptor } from './spawn-defaults-DVgmfxWo.js';
3
+ import { R as RouteSpec } from './session-config-DbWP9RRj.js';
4
4
  import '@agentproto/model-catalog';
5
- import './context-continuity-B9n0t0v-.js';
5
+ import './context-continuity-ib9_bVYM.js';
6
6
 
7
7
  /**
8
8
  * Read-only catalog/vendor endpoint (`agentproto-session-config-axes`
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Pure, dependency-light workspace command-allowlist logic, shared by the
3
+ * daemon's `command_execute` MCP tool (`command-tools.ts`) and any other
4
+ * host that needs to gate shell execution against the same
5
+ * `<workspace>/.agentproto/allowed-commands.json` file — e.g. the
6
+ * `mastra-agent` adapter's own `command_execute` workspace tool, which runs
7
+ * out-of-process from the daemon and has no other way to reach this logic.
8
+ *
9
+ * Kept free of daemon-only concerns (session recording, PR provenance,
10
+ * OS-level sandboxing) so it stays cheap to import from a spawned child
11
+ * process — no `SessionsRegistry`, no `@agentproto/command-sandbox`.
12
+ *
13
+ * Allowlist file shape — each entry in `commands` is either a plain
14
+ * basename string (unconstrained args) or an object constraining the argv
15
+ * prefix:
16
+ * {
17
+ * "version": 1,
18
+ * "commands": [
19
+ * "claude",
20
+ * "node",
21
+ * { "command": "git", "args": ["status"] }
22
+ * ]
23
+ * }
24
+ *
25
+ * Match is by command BASENAME. A plain string entry allows that basename
26
+ * with ANY args. An object entry with `args` only allows invocations whose
27
+ * argv starts with exactly that token sequence (a prefix match). When a
28
+ * basename has BOTH a plain entry and constrained entries, the plain entry
29
+ * wins.
30
+ */
31
+ declare const ALLOWLIST_REL = ".agentproto/allowed-commands.json";
32
+ /** One normalized allowlist entry — a basename with an optional argv-prefix
33
+ * constraint. `args` absent ⇒ unconstrained (any args), matching a plain
34
+ * basename-string entry in the JSON file. */
35
+ interface AllowlistEntry {
36
+ command: string;
37
+ args?: string[];
38
+ }
39
+ /** Load the workspace's allowlist as normalized entries (basename +
40
+ * optional argv-prefix constraint). Cheap stat-cached, same as
41
+ * `loadAllowlist`; both share one cache keyed on the file's mtime. */
42
+ declare function loadAllowlistEntries(workspace: string): Promise<AllowlistEntry[]>;
43
+ /** Basename-only view of the allowlist — every basename that has AT LEAST
44
+ * ONE entry, constrained or not. Existing callers (cron-scheduler.ts,
45
+ * task-ledger.ts, supervisor.ts) gate on basename alone, same as before
46
+ * this change; only `command_execute` itself enforces argv constraints
47
+ * (see `isCommandAllowed`). */
48
+ declare function loadAllowlist(workspace: string): Promise<Set<string>>;
49
+ /** Argv-aware allowlist check used by `command_execute`. A basename with
50
+ * no matching entry ⇒ denied. A basename with any unconstrained entry
51
+ * (plain string, or object with no `args`) ⇒ allowed regardless of args.
52
+ * Otherwise every matching entry is constrained, so `args` must match
53
+ * one of them as a prefix. */
54
+ declare function isCommandAllowed(entries: readonly AllowlistEntry[], baseName: string, args: readonly string[]): boolean;
55
+ /**
56
+ * Command basenames that execute arbitrary code and read the filesystem
57
+ * unrestricted. Allowlisting one grants a caller full host code execution +
58
+ * FS read — the workspace cwd-anchor bounds only the working directory, not
59
+ * what the interpreter itself opens. Used to surface a one-time warning
60
+ * (see `isInterpreterBasename` / `interpreterExecWarning`) until an OS-level
61
+ * command sandbox lands.
62
+ */
63
+ declare const INTERPRETER_BASENAMES: ReadonlySet<string>;
64
+ /** True when `name` (a command basename) is a code interpreter. Case-insensitive
65
+ * so `Rscript`/`RSCRIPT` match. */
66
+ declare function isInterpreterBasename(name: string): boolean;
67
+ /** Human-readable warning for allowlisting/running an interpreter. */
68
+ declare function interpreterExecWarning(baseName: string): string;
69
+
70
+ export { ALLOWLIST_REL, type AllowlistEntry, INTERPRETER_BASENAMES, interpreterExecWarning, isCommandAllowed, isInterpreterBasename, loadAllowlist, loadAllowlistEntries };
@@ -0,0 +1,104 @@
1
+ import { existsSync } from 'fs';
2
+ import { stat, readFile } from 'fs/promises';
3
+ import { resolve } from 'path';
4
+
5
+ /**
6
+ * @agentproto/runtime v0.1.0-alpha
7
+ * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
8
+ */
9
+
10
+ var ALLOWLIST_REL = ".agentproto/allowed-commands.json";
11
+ var allowlistCache = null;
12
+ function normalizeAllowlistEntry(raw) {
13
+ if (typeof raw === "string") {
14
+ const command2 = raw.trim();
15
+ return command2.length > 0 ? { command: command2 } : void 0;
16
+ }
17
+ if (typeof raw.command !== "string" || raw.command.trim().length === 0) {
18
+ return void 0;
19
+ }
20
+ const command = raw.command.trim();
21
+ if (raw.args === void 0) return { command };
22
+ if (!Array.isArray(raw.args) || !raw.args.every((a) => typeof a === "string")) {
23
+ return void 0;
24
+ }
25
+ return { command, args: [...raw.args] };
26
+ }
27
+ async function loadAllowlistEntries(workspace) {
28
+ const path = resolve(workspace, ALLOWLIST_REL);
29
+ if (!existsSync(path)) {
30
+ allowlistCache = null;
31
+ return [];
32
+ }
33
+ try {
34
+ const s = await stat(path);
35
+ if (allowlistCache && allowlistCache.path === path && allowlistCache.entry.mtimeMs === s.mtimeMs) {
36
+ return allowlistCache.entry.entries;
37
+ }
38
+ const raw = await readFile(path, "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ const list = Array.isArray(parsed.commands) ? parsed.commands : [];
41
+ const entries = list.map(normalizeAllowlistEntry).filter((e) => e !== void 0);
42
+ allowlistCache = { path, entry: { mtimeMs: s.mtimeMs, entries } };
43
+ return entries;
44
+ } catch (err) {
45
+ console.error(
46
+ `[command-allowlist] failed to load ${ALLOWLIST_REL} (will deny all):`,
47
+ err
48
+ );
49
+ allowlistCache = null;
50
+ return [];
51
+ }
52
+ }
53
+ async function loadAllowlist(workspace) {
54
+ const entries = await loadAllowlistEntries(workspace);
55
+ return new Set(entries.map((e) => e.command));
56
+ }
57
+ function argsMatchPrefix(pattern, actual) {
58
+ if (pattern.length > actual.length) return false;
59
+ return pattern.every((tok, i) => actual[i] === tok);
60
+ }
61
+ function isCommandAllowed(entries, baseName, args) {
62
+ const matching = entries.filter((e) => e.command === baseName);
63
+ if (matching.length === 0) return false;
64
+ if (matching.some((e) => e.args === void 0)) return true;
65
+ return matching.some((e) => argsMatchPrefix(e.args, args));
66
+ }
67
+ var INTERPRETER_BASENAMES = /* @__PURE__ */ new Set([
68
+ "bash",
69
+ "sh",
70
+ "zsh",
71
+ "dash",
72
+ "ksh",
73
+ "fish",
74
+ "node",
75
+ "deno",
76
+ "bun",
77
+ "tsx",
78
+ "ts-node",
79
+ "python",
80
+ "python2",
81
+ "python3",
82
+ "ruby",
83
+ "perl",
84
+ "php",
85
+ "rscript",
86
+ "osascript",
87
+ "env",
88
+ "xargs",
89
+ "make",
90
+ "npx",
91
+ "uv",
92
+ "uvx",
93
+ "pipx"
94
+ ]);
95
+ function isInterpreterBasename(name) {
96
+ return INTERPRETER_BASENAMES.has(name.toLowerCase());
97
+ }
98
+ function interpreterExecWarning(baseName) {
99
+ return `command_execute ran the interpreter '${baseName}', which executes arbitrary code and can read files outside the workspace \u2014 the cwd anchor does not confine it. Allowlisting interpreters grants full host code execution; prefer allowlisting specific tools. An OS-level command sandbox is planned as the real confinement.`;
100
+ }
101
+
102
+ export { ALLOWLIST_REL, INTERPRETER_BASENAMES, interpreterExecWarning, isCommandAllowed, isInterpreterBasename, loadAllowlist, loadAllowlistEntries };
103
+ //# sourceMappingURL=command-allowlist.mjs.map
104
+ //# sourceMappingURL=command-allowlist.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/command-allowlist.ts"],"names":["command"],"mappings":"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","file":"command-allowlist.mjs","sourcesContent":["/**\n * Pure, dependency-light workspace command-allowlist logic, shared by the\n * daemon's `command_execute` MCP tool (`command-tools.ts`) and any other\n * host that needs to gate shell execution against the same\n * `<workspace>/.agentproto/allowed-commands.json` file — e.g. the\n * `mastra-agent` adapter's own `command_execute` workspace tool, which runs\n * out-of-process from the daemon and has no other way to reach this logic.\n *\n * Kept free of daemon-only concerns (session recording, PR provenance,\n * OS-level sandboxing) so it stays cheap to import from a spawned child\n * process — no `SessionsRegistry`, no `@agentproto/command-sandbox`.\n *\n * Allowlist file shape — each entry in `commands` is either a plain\n * basename string (unconstrained args) or an object constraining the argv\n * prefix:\n * {\n * \"version\": 1,\n * \"commands\": [\n * \"claude\",\n * \"node\",\n * { \"command\": \"git\", \"args\": [\"status\"] }\n * ]\n * }\n *\n * Match is by command BASENAME. A plain string entry allows that basename\n * with ANY args. An object entry with `args` only allows invocations whose\n * argv starts with exactly that token sequence (a prefix match). When a\n * basename has BOTH a plain entry and constrained entries, the plain entry\n * wins.\n */\n\nimport { existsSync } from \"node:fs\"\nimport { readFile, stat } from \"node:fs/promises\"\nimport { resolve } from \"node:path\"\n\nexport const ALLOWLIST_REL = \".agentproto/allowed-commands.json\"\n\n/** One normalized allowlist entry — a basename with an optional argv-prefix\n * constraint. `args` absent ⇒ unconstrained (any args), matching a plain\n * basename-string entry in the JSON file. */\nexport interface AllowlistEntry {\n command: string\n args?: string[]\n}\n\ninterface AllowlistFile {\n version?: number\n commands?: Array<string | { command?: unknown; args?: unknown }>\n}\n\ninterface AllowlistCacheEntry {\n mtimeMs: number\n entries: AllowlistEntry[]\n}\n\nlet allowlistCache: { path: string; entry: AllowlistCacheEntry } | null = null\n\nfunction normalizeAllowlistEntry(\n raw: string | { command?: unknown; args?: unknown },\n): AllowlistEntry | undefined {\n if (typeof raw === \"string\") {\n const command = raw.trim()\n return command.length > 0 ? { command } : undefined\n }\n if (typeof raw.command !== \"string\" || raw.command.trim().length === 0) {\n return undefined\n }\n const command = raw.command.trim()\n if (raw.args === undefined) return { command }\n if (!Array.isArray(raw.args) || !raw.args.every(a => typeof a === \"string\")) {\n // Malformed `args` (not a string array) — drop the constraint rather\n // than silently allowlisting an unintended shape; the operator gets a\n // basename-only entry, which is at least not MORE permissive than\n // the array they wrote.\n return undefined\n }\n return { command, args: [...raw.args] }\n}\n\n/** Load the workspace's allowlist as normalized entries (basename +\n * optional argv-prefix constraint). Cheap stat-cached, same as\n * `loadAllowlist`; both share one cache keyed on the file's mtime. */\nexport async function loadAllowlistEntries(\n workspace: string,\n): Promise<AllowlistEntry[]> {\n const path = resolve(workspace, ALLOWLIST_REL)\n if (!existsSync(path)) {\n allowlistCache = null\n return []\n }\n try {\n const s = await stat(path)\n if (\n allowlistCache &&\n allowlistCache.path === path &&\n allowlistCache.entry.mtimeMs === s.mtimeMs\n ) {\n return allowlistCache.entry.entries\n }\n const raw = await readFile(path, \"utf8\")\n const parsed = JSON.parse(raw) as AllowlistFile\n const list = Array.isArray(parsed.commands) ? parsed.commands : []\n const entries = list\n .map(normalizeAllowlistEntry)\n .filter((e): e is AllowlistEntry => e !== undefined)\n allowlistCache = { path, entry: { mtimeMs: s.mtimeMs, entries } }\n return entries\n } catch (err) {\n // Bad JSON / unreadable file ⇒ deny all and surface in the error\n // the next caller gets. Don't poison the cache.\n console.error(\n `[command-allowlist] failed to load ${ALLOWLIST_REL} (will deny all):`,\n err,\n )\n allowlistCache = null\n return []\n }\n}\n\n/** Basename-only view of the allowlist — every basename that has AT LEAST\n * ONE entry, constrained or not. Existing callers (cron-scheduler.ts,\n * task-ledger.ts, supervisor.ts) gate on basename alone, same as before\n * this change; only `command_execute` itself enforces argv constraints\n * (see `isCommandAllowed`). */\nexport async function loadAllowlist(workspace: string): Promise<Set<string>> {\n const entries = await loadAllowlistEntries(workspace)\n return new Set(entries.map(e => e.command))\n}\n\n/** True when `pattern` (an allowed argv prefix) matches the start of\n * `actual` token-for-token. An empty `pattern` matches anything. */\nfunction argsMatchPrefix(pattern: readonly string[], actual: readonly string[]): boolean {\n if (pattern.length > actual.length) return false\n return pattern.every((tok, i) => actual[i] === tok)\n}\n\n/** Argv-aware allowlist check used by `command_execute`. A basename with\n * no matching entry ⇒ denied. A basename with any unconstrained entry\n * (plain string, or object with no `args`) ⇒ allowed regardless of args.\n * Otherwise every matching entry is constrained, so `args` must match\n * one of them as a prefix. */\nexport function isCommandAllowed(\n entries: readonly AllowlistEntry[],\n baseName: string,\n args: readonly string[],\n): boolean {\n const matching = entries.filter(e => e.command === baseName)\n if (matching.length === 0) return false\n if (matching.some(e => e.args === undefined)) return true\n return matching.some(e => argsMatchPrefix(e.args!, args))\n}\n\n/**\n * Command basenames that execute arbitrary code and read the filesystem\n * unrestricted. Allowlisting one grants a caller full host code execution +\n * FS read — the workspace cwd-anchor bounds only the working directory, not\n * what the interpreter itself opens. Used to surface a one-time warning\n * (see `isInterpreterBasename` / `interpreterExecWarning`) until an OS-level\n * command sandbox lands.\n */\nexport const INTERPRETER_BASENAMES: ReadonlySet<string> = new Set([\n \"bash\", \"sh\", \"zsh\", \"dash\", \"ksh\", \"fish\",\n \"node\", \"deno\", \"bun\", \"tsx\", \"ts-node\",\n \"python\", \"python2\", \"python3\", \"ruby\", \"perl\", \"php\", \"rscript\", \"osascript\",\n \"env\", \"xargs\", \"make\", \"npx\", \"uv\", \"uvx\", \"pipx\",\n])\n\n/** True when `name` (a command basename) is a code interpreter. Case-insensitive\n * so `Rscript`/`RSCRIPT` match. */\nexport function isInterpreterBasename(name: string): boolean {\n return INTERPRETER_BASENAMES.has(name.toLowerCase())\n}\n\n/** Human-readable warning for allowlisting/running an interpreter. */\nexport function interpreterExecWarning(baseName: string): string {\n return (\n `command_execute ran the interpreter '${baseName}', which executes ` +\n `arbitrary code and can read files outside the workspace — the cwd anchor ` +\n `does not confine it. Allowlisting interpreters grants full host code ` +\n `execution; prefer allowlisting specific tools. An OS-level command ` +\n `sandbox is planned as the real confinement.`\n )\n}\n"]}
package/dist/config.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { S as SpawnDefaultsConfig } from './spawn-defaults-7uHRnYH1.js';
1
+ import { S as SpawnDefaultsConfig } from './spawn-defaults-DVgmfxWo.js';
2
2
  import '@agentproto/model-catalog';
3
- import './context-continuity-B9n0t0v-.js';
3
+ import './context-continuity-ib9_bVYM.js';
4
4
 
5
5
  /**
6
6
  * `~/.agentproto/config.json` — single hand-editable JSON for the
@@ -155,6 +155,11 @@ interface FeaturesConfig {
155
155
  /** Hint that PTY is desired — informational; the daemon still
156
156
  * detects node-pty's presence at runtime. */
157
157
  pty?: boolean;
158
+ /** Enable the local LLM Endpoint proxy sidecar (route registration,
159
+ * MCP tools, child-process lifecycle). Default false — the endpoint is
160
+ * an opt-in feature; when off, the `llm-endpoint` custom route is not
161
+ * registered and the `llm_endpoint_*` MCP tools are not exposed. */
162
+ llmEndpoint?: boolean;
158
163
  }
159
164
  /**
160
165
  * Policy for `agentproto worktree new` (PLAN.md §1.4 — config carries
@@ -266,6 +271,22 @@ interface SpawnConfig {
266
271
  */
267
272
  dedupe?: SpawnDedupeMode;
268
273
  }
274
+ /**
275
+ * Provenance policy — the opt-in `gh` PATH shim (`gh-provenance-shim.ts`).
276
+ * Off by default. When `wrapGh` is on, every agent session the daemon spawns
277
+ * gets a shim directory prepended to its PATH so that any `gh pr create` the
278
+ * session (or an adapter subprocess shelling out — claude-code, codex, …) runs
279
+ * has a deterministic `@agentproto-bot` provenance footer appended to the
280
+ * created PR's BODY, matching the footer the cloud runner stamps. The TOOL
281
+ * stamps, never the model; commit messages are never touched (the repo's
282
+ * hygiene-check forbids attribution there). Resolution order mirrors
283
+ * `spawn.attach`: `AGENTPROTO_PROVENANCE_WRAP_GH` env > this field > default
284
+ * `false`.
285
+ */
286
+ interface ProvenanceConfig {
287
+ /** Enable the opt-in `gh` provenance PATH shim for spawned sessions. */
288
+ wrapGh?: boolean;
289
+ }
269
290
  interface PairingConfig {
270
291
  /** Rendezvous broker WS URL (ws:// or wss://) used by `pair offer` and by
271
292
  * autoconnect on boot. When unset, `pair offer` requires an explicit
@@ -309,6 +330,12 @@ interface AcpAgentConfigEntry {
309
330
  default?: string;
310
331
  allowed?: string[];
311
332
  };
333
+ /** Billing endpoint this CLI's own auth bills (e.g. "mistral",
334
+ * "moonshot", "google") — the provider whose wallet/auth-profile the
335
+ * agent consumes. Lets clients link the harness to that provider's
336
+ * wallets even when no model list is declared. Unset when the CLI's
337
+ * billing target isn't a single known endpoint. */
338
+ provider?: string;
312
339
  /** Shown when `bin` is missing from PATH (how to install the CLI). */
313
340
  install_hint?: string;
314
341
  }
@@ -378,6 +405,8 @@ interface AgentprotoConfig {
378
405
  worktrees?: WorktreesConfig;
379
406
  /** Spawn-time policy (`agent_start`). See {@link SpawnConfig}. */
380
407
  spawn?: SpawnConfig;
408
+ /** Provenance policy — the opt-in `gh` PATH shim. See {@link ProvenanceConfig}. */
409
+ provenance?: ProvenanceConfig;
381
410
  /** Named connection profiles. See `ProfileConfig` for the merge
382
411
  * semantics — a profile's fields shallow-override the top-level
383
412
  * defaults for the selected run. */
@@ -435,4 +464,4 @@ declare function getConfigKey(cfg: AgentprotoConfig, dotted: string): unknown;
435
464
  */
436
465
  declare function setConfigKey(cfg: AgentprotoConfig, dotted: string, value: unknown): AgentprotoConfig;
437
466
 
438
- export { type AcpAgentConfigEntry, type AgentprotoConfig, CONFIG_FILE_PATH, CONFIG_VERSION, type DaemonConfig, type FeaturesConfig, type PairingConfig, type ProfileConfig, type SpawnAttachMode, type SpawnConfig, type SpawnDedupeMode, type TerminalPreset, type TunnelConfig, type WorktreeIsolationMode, type WorktreesConfig, getConfigKey, loadConfig, saveConfig, setConfigKey };
467
+ export { type AcpAgentConfigEntry, type AgentprotoConfig, CONFIG_FILE_PATH, CONFIG_VERSION, type DaemonConfig, type FeaturesConfig, type PairingConfig, type ProfileConfig, type ProvenanceConfig, type SpawnAttachMode, type SpawnConfig, type SpawnDedupeMode, type TerminalPreset, type TunnelConfig, type WorktreeIsolationMode, type WorktreesConfig, getConfigKey, loadConfig, saveConfig, setConfigKey };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"names":["fs"],"mappings":"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","file":"config.mjs","sourcesContent":["/**\n * `~/.agentproto/config.json` — single hand-editable JSON for the\n * agentproto control plane's defaults. Sits alongside the existing\n * surface files (workspaces.json, credentials.json, sessions.json):\n *\n * workspaces.json which directories are workspaces + which is active\n * credentials.json tunnel host bearer tokens (mode 0600)\n * sessions.json last-known snapshot of the registry (informational)\n * config.json daemon defaults: port, bind, allowed origins,\n * tunnel host, feature toggles\n *\n * Resolution order for every daemon knob is:\n * 1. CLI flag (e.g. --port)\n * 2. Env var (where one exists, e.g. AGENTPROTO_TOKEN)\n * 3. config.json\n * 4. Hardcoded default\n *\n * This means a user can call `agentproto config set daemon.port 18791`\n * once and never re-pass `--port 18791` to `serve install` etc. CLI\n * flags still win for one-off overrides.\n *\n * Schema is intentionally narrow + extensible — unknown keys are\n * preserved on save (deep-merge), so a newer CLI writing a new\n * field won't drop one an older CLI doesn't know about. No secrets\n * here; credentials stay in credentials.json (mode 0600).\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport type { SpawnDefaultsConfig } from \"./spawn-defaults.js\"\n\nexport const CONFIG_VERSION = 1 as const\n\nexport interface DaemonConfig {\n /** Absolute path to the workspace the daemon binds to at boot. */\n workspace?: string\n /** HTTP port. Default 18790. */\n port?: number\n /** Bind addr. Default 127.0.0.1. */\n bind?: string\n /** Trusted browser origins for mutating /sessions/* routes (in\n * addition to the hardcoded localhost defaults). */\n allowedOrigins?: string[]\n /** When true, the daemon does NOT auto-trust localhost-on-any-port.\n * Only origins explicitly listed in `allowedOrigins` are allowed.\n * Pair with a curated list (e.g. `[\"http://localhost:3000\"]`) for\n * hardened setups. Default false. */\n strictOrigins?: boolean\n /** Server label sent in tunnel hello frames. */\n label?: string\n /** Bearer token gating the gateway at boot (`AuthOptions` with\n * `mode: \"bearer\"`). Unlike `remote_enable`'s ephemeral quick-tunnel\n * token, this one lives in config.json and survives daemon restarts.\n * Set via `agentproto config set daemon.authToken <token>` (e.g.\n * `$(openssl rand -hex 32)`). Unset ⇒ the gateway boots with\n * `mode: \"none\"` — fully open on loopback, same as today. */\n authToken?: string\n /** Opt-in eager resume-on-boot (session-survivability plan §5\n * \"Opt-in vs automatic\", PR-4). When true, a daemon restart doesn't just\n * leave agent-cli sessions dead-but-lazy-resumable — the boot pass eagerly\n * re-spawns the eligible ones (those that died with\n * `endedReason: \"daemon-restart\"` and still pass `canResume`) IN PLACE,\n * restoring liveness without waiting for a prompt. Lazy resume-on-prompt is\n * always on regardless of this flag (it costs nothing until someone acts);\n * this flag only controls the *automatic* boot-time pass. Default false: a\n * box-wide restart should not silently relaunch every adapter (cost, spawn\n * storm, credential re-resolution), and default-off also keeps two daemons\n * sharing the workspace buckets from racing to resume the same rows. Set via\n * `agentproto config set daemon.resumeSessionsOnBoot true`. Surfaced in\n * `daemon_health` / `GET /health`. */\n resumeSessionsOnBoot?: boolean\n /** Idle agent-session reaper (PR-6). When set to a positive number of\n * milliseconds, the daemon periodically retires agent-cli sessions that have\n * been idle (not busy, not awaiting input, `status:\"running\"`) longer than\n * this: it SIGTERMs the adapter child to free the process and flips the row\n * to `killed`/`endedReason:\"idle-reaped\"` — dead-but-lazy-resumable (a later\n * prompt revives it) and, critically, excluded from eager resume-on-boot\n * (#638 gates on `endedReason === \"daemon-restart\"`), so enabling this makes\n * eager resume safe: only genuinely-recent work survives a restart. Default\n * OFF (unset / 0 / negative ⇒ the reaper never runs). Resolution order\n * mirrors the module docblock: `AGENTPROTO_IDLE_REAP_AFTER_MS` env > this\n * field > off. Set via `agentproto config set daemon.idleReapAfterMs\n * <ms>`. Surfaced in `daemon_health` / `GET /health`. */\n idleReapAfterMs?: number\n /** Crash-detect sweep interval in ms (crash-detect PR-1). Unlike\n * `idleReapAfterMs`, this is DEFAULT ON (non-destructive observability):\n * the daemon periodically probes every live agent-cli session's OS\n * process (`process.kill(pid, 0)`) and, when it's provably gone with no\n * exit event ever emitted for it, flips the row to\n * `error`/`endedReason:\"crashed\"` with a `lastError` string — surfacing a\n * death that would otherwise sit silently as `status:\"running\"` until the\n * next prompt's RPC throws (or forever, for a parked session with no next\n * prompt). Detects only; never restarts or notifies (later PRs). Set to a\n * non-positive value to disable the sweep entirely. Resolution order\n * mirrors `idleReapAfterMs`: `AGENTPROTO_CRASH_DETECT_INTERVAL_MS` env >\n * this field > the hardcoded default (30s). Set via `agentproto config\n * set daemon.crashDetectIntervalMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n crashDetectIntervalMs?: number\n /** Restart-scheduler sweep interval in ms (restart-scheduler PR-2). OFF by\n * default (unset / 0 / negative ⇒ the sweep never runs), unlike\n * `crashDetectIntervalMs` — a positive value here only arms the periodic\n * sweep that EXECUTES an already-scheduled restart; it never opts a\n * session in by itself (that's the per-session `agent_start.restartPolicy`\n * field). Resolution order mirrors `idleReapAfterMs`:\n * `AGENTPROTO_RESTART_SWEEP_INTERVAL_MS` env > this field > off. Set via\n * `agentproto config set daemon.restartSweepIntervalMs <ms>`. Surfaced in\n * `daemon_health` / `GET /health`. */\n restartSweepIntervalMs?: number\n /** Turn-liveness watchdog threshold in ms (turn-liveness-watchdog\n * chantier). Same shape as `crashDetectIntervalMs`: DEFAULT ON\n * (non-destructive observability). The daemon periodically sweeps every\n * BUSY agent-cli session and, for one that is mid-turn, NOT legitimately\n * `blockedOn` a subagent/command, and has had no adapter activity\n * (`lastActivityAt`) for longer than this threshold, stamps\n * `stalledSinceMs` on the descriptor and emits `session:stalled` —\n * surfacing a dead adapter stream (network drop, hung child — zero\n * frames mid-turn) that would otherwise sit indistinguishable from\n * healthy long work: `status:\"running\"`, `lastError:null`, no other\n * signal. Conservative by design: a session legitimately blocked on a\n * long tool call is NEVER a candidate, no matter how long it's silent —\n * see `stall-watchdog.ts`'s docblock for why. Detects only; never kills\n * or restarts (this is a later concern, if ever). Set to a non-positive\n * value to disable the sweep entirely. Resolution order mirrors\n * `idleReapAfterMs`: `AGENTPROTO_TURN_STALL_AFTER_MS` env > this field >\n * the hardcoded default (5 min). Set via `agentproto config set\n * daemon.turnStallAfterMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n turnStallAfterMs?: number\n}\n\nexport interface TunnelConfig {\n /** Cloud WS URL. When set + autoconnect=true, `agentproto serve`\n * bootstraps with `--connect <host>`. */\n host?: string\n /** apt_ daemon token to present at the tunnel upgrade. When set,\n * `agentproto serve` uses this BEFORE falling back to\n * credentials.json — handy in profiles where the token-per-host\n * mapping in credentials.json doesn't fit (e.g. host = tunnel URL\n * but credentials were minted against the api URL). */\n token?: string\n /** Whether `agentproto daemon start` connects the tunnel by\n * default. v0 only — implementer can ignore until daemon needs it. */\n autoconnect?: boolean\n /**\n * Opt into end-to-end encryption of the outbound `serve --connect` tunnel\n * (design: tunnel-e2e/v1). When true, the daemon negotiates a\n * token-authenticated ephemeral handshake with the host and wraps the tunnel\n * frames in an AEAD box, so even the trusted host loses plaintext visibility.\n * The handshake authenticates both ends against the shared `tunnel.token`, so\n * `token` MUST also be set. Fully backward-compatible: if the host doesn't\n * advertise e2e (an older host), the daemon falls back to today's plaintext\n * tunnel. Unset/false ⇒ plaintext, byte-identical to today. */\n e2e?: boolean\n}\n\nexport interface FeaturesConfig {\n /** Hint that PTY is desired — informational; the daemon still\n * detects node-pty's presence at runtime. */\n pty?: boolean\n}\n\n/**\n * Policy for `agentproto worktree new` (PLAN.md §1.4 — config carries\n * policy, never state; git itself is the authority for which worktrees\n * exist). This is the fix for the sprawl the plan measured: 31 linked\n * worktrees across 6 different parent directories, because there was no\n * `worktree new` verb and therefore no convention to converge on.\n */\n/**\n * How the daemon isolates a freshly-spawned agent session into its own git\n * worktree (`agent_start.worktree`):\n * - `\"always\"` — every depth-0 spawn is provisioned into a worktree,\n * whether or not the caller asked. A cwd that is not in\n * a git repo has nothing to isolate, so it spawns plain.\n * - `\"on-request\"` — isolate ONLY when the caller passes `worktree`. This\n * is the default and the back-compatible behaviour:\n * today's callers pass nothing and spawn exactly where\n * they asked.\n * - `\"never\"` — isolation is off; an explicit `worktree` field is\n * REJECTED (loud, not silently ignored) so a caller\n * never believes it got an isolated tree it didn't.\n */\nexport type WorktreeIsolationMode = \"always\" | \"on-request\" | \"never\"\n\nexport interface WorktreesConfig {\n /**\n * Absolute path new worktrees are created under. Layout:\n * `<root>/<repoName>/<slug>`. Resolution order (mirrors every other\n * knob in this file, see the module docblock): `--root` flag >\n * `AGENTPROTO_WORKTREES_ROOT` env > this field > the hardcoded default\n * `~/.agentproto/worktrees`. The default is a real single root, not\n * \"unconfigured\" — `worktree new` converges to one place with zero\n * setup, which is the only way the sprawl actually stops (the 6 roots\n * that exist today are 6 people each inventing a default by hand).\n */\n root?: string\n /**\n * Policy for `agent_start.worktree` isolation. Resolution order mirrors\n * the module docblock (there is no CLI flag — this is a daemon-side\n * policy read at spawn, not a per-invocation flag):\n * `AGENTPROTO_WORKTREES_ISOLATION` env > this field > the hardcoded\n * default `\"on-request\"`. `\"on-request\"` is deliberately the default:\n * any other would break back-compat by isolating callers that never\n * asked (see `worktree-isolation.ts`).\n */\n isolation?: WorktreeIsolationMode\n}\n\n/**\n * Policy for `agent_start.attach` — whether a spawn AUTO-attaches to its\n * calling session as parent lineage. `\"always\"` (the default) nests a\n * spawned child under the session that spawned it whenever that identity is\n * derivable (the trusted `?callerSessionId=` on the daemon self-ref URL, or\n * an explicit `parentSessionId` hint), so a supervisor's executors stop\n * landing as depth-0 orphans. `\"on-request\"` reverts to the pre-attach\n * behaviour: auto-attribution is off, and a child nests only when the caller\n * explicitly opts in (`attach: true` / `attach: { parent }` / an explicit\n * `parentSessionId`). Either way a per-call `attach: false` forces an\n * independent root, exactly as `worktree: false` opts out of isolation. See\n * `spawn-attach.ts`.\n */\nexport type SpawnAttachMode = \"always\" | \"on-request\"\n\n/**\n * Policy for `agent_start`'s IMPLICIT dedupe — what happens when a caller\n * spawns with NO `idempotencyKey` at all (see `spawn-dedupe.ts`):\n * - `\"always\"` — the daemon DERIVES an implicit key from the spawn's\n * `label` (required — see below) plus a hash of the\n * initial `prompt`, and dedupes a same-adapter/cwd\n * repeat against it exactly as an explicit key would.\n * This is the default, mirroring `attach`'s own\n * \"opt-in-only guard is not a guard\" precedent above.\n * Deriving needs a `label` to produce anything at all\n * — an unlabelled spawn is untouched, which is what\n * keeps this safe for the fan-out pattern this repo\n * exercises (several agents into one cwd with no\n * shared label): see `spawn-dedupe.ts`'s docblock for\n * the full false-dedup analysis, and PR #803's own\n * no-opt-in label+cwd warning backstop in\n * `session-spawn.ts`, which independently landed on\n * the same label-is-the-signal boundary.\n * - `\"on-request\"` — no implicit derivation; only an explicit\n * `idempotencyKey` dedupes (today's behaviour,\n * unchanged). A per-call `dedupe: true` still opts in\n * under this policy, mirroring `attach: true`.\n * Either way a per-call `dedupe: false` disables implicit derivation for\n * that one spawn regardless of policy — the escape hatch, mirroring\n * `attach: false` / `worktree: false`. An explicit `idempotencyKey` always\n * wins over a derived one (derivation is only attempted when the caller\n * supplied none).\n */\nexport type SpawnDedupeMode = \"always\" | \"on-request\"\n\nexport interface SpawnConfig {\n /**\n * Attach policy for `agent_start`. Resolution order mirrors the module\n * docblock (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_ATTACH` env > this field > the hardcoded default\n * `\"always\"`. Unlike `worktrees.isolation` (whose default preserves\n * back-compat by NOT isolating), attach defaults ON: an orphaned executor\n * is a bug, not a feature, and the auto-parent is descriptor-only lineage\n * that never relaxes a privilege gate.\n */\n attach?: SpawnAttachMode\n /**\n * Implicit-dedupe policy for `agent_start`. Resolution order mirrors\n * `attach` above (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_DEDUPE` env > this field > the hardcoded default\n * `\"always\"`. See {@link SpawnDedupeMode} and `spawn-dedupe.ts` for the\n * full reasoning: a retry-safety guard that only works when a caller\n * remembers to ask for it (`idempotencyKey`) is not a guard — the same\n * argument `attach` already settled for parent lineage.\n */\n dedupe?: SpawnDedupeMode\n}\n\nexport interface PairingConfig {\n /** Rendezvous broker WS URL (ws:// or wss://) used by `pair offer` and by\n * autoconnect on boot. When unset, `pair offer` requires an explicit\n * `--rendezvous`. Mirrors `tunnel.host`. */\n rendezvous?: string\n /** Whether the daemon opens standing rendezvous connections for every\n * persisted pairing on boot (so a paired client can reconnect anytime).\n * Mirrors `tunnel.autoconnect`. Default true when a rendezvous is set. */\n autoconnect?: boolean\n}\n\n/**\n * A user-defined generic ACP agent — the config-file half of\n * `AcpAgentSpec` (the slug is the record key in `acpAgents`, so it's\n * omitted here). Any CLI that already speaks the Agent Client Protocol\n * can be wired with zero code by declaring one of these under\n * `acpAgents.<slug>` in `~/.agentproto/config.json`; the CLI's\n * `acpHandleFromSpec` mints a runnable `AgentCliHandle` from it at\n * resolve time (see `packages/cli/src/registry/acp-generic.ts`). Kept\n * in this package (not the CLI's) so `config.ts` stays the single\n * source of truth for the config surface without a cli→runtime→cli\n * import cycle — the CLI's `AcpAgentSpec` extends this shape.\n */\nexport interface AcpAgentConfigEntry {\n /** Display name. Defaults to the slug when omitted. */\n name?: string\n /** One-line description surfaced in `agentproto acp ls`. */\n description?: string\n /** Executable to spawn, e.g. \"gemini\". */\n bin: string\n /** Extra argv appended after `bin`, e.g. [\"--experimental-acp\"]. */\n bin_args?: string[]\n /** Extra environment variables for the spawned process. */\n env?: Record<string, string>\n /** Flag the CLI uses to receive the working directory, if it needs\n * one passed explicitly (most ACP agents take cwd over the wire). */\n cwd_flag?: string\n /** When true, advertise resumable + native-resume continuation. */\n resumable?: boolean\n /** Known model ids for the agent (informational + validation hints). */\n models?: { default?: string; allowed?: string[] }\n /** Shown when `bin` is missing from PATH (how to install the CLI). */\n install_hint?: string\n}\n\n/**\n * Per-environment connection bundle. A profile overrides specific\n * fields of the top-level `daemon` / `tunnel` / `features` blocks\n * when selected via `--profile <name>` (or the top-level\n * `activeProfile` setting). Missing fields fall through to the\n * top-level config, so a profile only needs to declare what's\n * different — typically just `tunnel.host` + `tunnel.token`.\n *\n * Example:\n * {\n * \"daemon\": { \"workspace\": \"/code\", \"port\": 18790 },\n * \"activeProfile\": \"local\",\n * \"profiles\": {\n * \"local\": { \"tunnel\": { \"host\": \"ws://localhost:3200/connect\",\n * \"token\": \"apt_local\", \"autoconnect\": true } },\n * \"prod\": { \"tunnel\": { \"host\": \"wss://tunnel.guilde.work/connect\",\n * \"token\": \"apt_prod\", \"autoconnect\": true },\n * \"daemon\": { \"port\": 18791 } }\n * }\n * }\n *\n * Sandbox daemons generate per-sandbox profile entries at provision\n * time so the daemon inside the sandbox boots with\n * `agentproto serve --profile sandbox-<id>` and no extra plumbing.\n */\nexport interface ProfileConfig {\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n}\n\n/**\n * A user-defined named terminal/TUI preset stored in\n * `~/.agentproto/config.json` under `terminalPresets`. Presets keep\n * local launch recipes (argv, env, cwd, name/label) out of shared\n * adapter manifests — e.g. pointing a Claude Code TUI at a local\n * LLM gateway without retyping proxy env vars every spawn.\n */\nexport interface TerminalPreset {\n /** Command + args to spawn. When provided, `sessions terminal` can\n * be used without `-- <argv...>`. */\n argv?: string[]\n /** Extra environment variables layered on top of the daemon's\n * inherited process.env. Values MUST be strings. */\n env?: Record<string, string>\n /** Working directory for the PTY session. Relative paths are\n * resolved against the current working directory at CLI time. */\n cwd?: string\n /** Workspace slug used for cwd fallback when `cwd` is omitted. */\n workspace?: string\n /** Stable session name passed to the registry (`name` field). */\n name?: string\n /** Human-readable label surfaced in session listings. */\n label?: string\n}\n\nexport interface AgentprotoConfig {\n version?: number\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n /** E2E daemon-pairing defaults (rendezvous URL + autoconnect). */\n pairing?: PairingConfig\n /** Where `agentproto worktree new` creates worktrees. See\n * {@link WorktreesConfig}. */\n worktrees?: WorktreesConfig\n /** Spawn-time policy (`agent_start`). See {@link SpawnConfig}. */\n spawn?: SpawnConfig\n /** Named connection profiles. See `ProfileConfig` for the merge\n * semantics — a profile's fields shallow-override the top-level\n * defaults for the selected run. */\n profiles?: Record<string, ProfileConfig>\n /** Profile name to use when `--profile` isn't passed. When unset,\n * the top-level `daemon` / `tunnel` blocks are used directly. */\n activeProfile?: string\n /** Default `skills` + `options` auto-applied to every `agent_start`\n * spawn — global and per-adapter. See `resolveSpawnDefaults` in\n * `spawn-defaults.ts` for the merge precedence with an explicit call.\n * Absent ⇒ current behaviour exactly (no regression). */\n defaults?: SpawnDefaultsConfig\n /** User-defined generic ACP agents, keyed by adapter slug. Each entry\n * is minted into a runnable handle by the CLI's `acpHandleFromSpec`\n * when `resolveAdapter(slug)` finds no npm adapter package. User\n * entries shadow the curated `ACP_CATALOG` on slug collision. */\n acpAgents?: Record<string, AcpAgentConfigEntry>\n /** User-defined named terminal/TUI presets. Local-only; never\n * packaged in shared adapter manifests or defaults. */\n terminalPresets?: Record<string, TerminalPreset>\n /** Unknown keys preserved across save round-trips. */\n [unknown: string]: unknown\n}\n\nexport const CONFIG_FILE_PATH = (): string =>\n join(homedir(), \".agentproto\", \"config.json\")\n\n/**\n * Drop any `acpAgents` entries that aren't a shape we can turn into a\n * handle. The one hard requirement is a non-empty string `bin` (the\n * executable to spawn); everything else is optional and defaulted\n * downstream. Invalid entries are removed rather than throwing so the\n * daemon still boots — one warning names the offending slug so the\n * user can fix their config. Returns `undefined` when nothing valid\n * remains, keeping the key absent (== \"no generic agents\").\n */\nfunction sanitizeAcpAgents(\n raw: unknown,\n target: string,\n): Record<string, AcpAgentConfigEntry> | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n console.warn(\n `[runtime/config] ${target}: 'acpAgents' is not an object — ignoring`,\n )\n return undefined\n }\n const out: Record<string, AcpAgentConfigEntry> = {}\n for (const [slug, value] of Object.entries(raw as Record<string, unknown>)) {\n if (\n value &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n typeof (value as { bin?: unknown }).bin === \"string\" &&\n (value as { bin: string }).bin.length > 0\n ) {\n out[slug] = value as AcpAgentConfigEntry\n } else {\n console.warn(\n `[runtime/config] ${target}: acpAgents.${slug} is missing a string 'bin' — ignoring`,\n )\n }\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Load config.json. Returns an empty object (NOT null) when the file\n * is missing, malformed, or unreadable — callers can `cfg.daemon?.port`\n * safely without null-guards. Errors during a malformed-read are\n * logged once so the user notices the file is broken without the\n * daemon refusing to boot.\n */\nexport async function loadConfig(path?: string): Promise<AgentprotoConfig> {\n const target = path ?? CONFIG_FILE_PATH()\n try {\n const raw = await fs.readFile(target, \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const cfg = parsed as AgentprotoConfig\n // Sanitize `acpAgents` in the same tolerant spirit as the rest of\n // this loader: a malformed entry is dropped (with one warning) so a\n // single bad hand-edit can't make every generic ACP agent\n // unresolvable. Full AIP-45 validation happens later, at\n // `acpHandleFromSpec` time, with precise field-level messages.\n if (cfg.acpAgents !== undefined) {\n cfg.acpAgents = sanitizeAcpAgents(cfg.acpAgents, target)\n }\n return cfg\n }\n console.warn(\n `[runtime/config] ${target}: top-level value is not an object — ignoring`,\n )\n return {}\n } catch (err) {\n // ENOENT is the common case; only warn on other shapes.\n const code = (err as NodeJS.ErrnoException).code\n if (code && code !== \"ENOENT\") {\n console.warn(\n `[runtime/config] failed to read ${target}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n return {}\n }\n}\n\n/**\n * Write config.json atomically (tmp + rename) so a concurrent\n * `agentproto config edit` can't half-truncate the file. Writes\n * `next` AS-IS — callers are expected to pass the full desired\n * state (loaded the existing config, mutated, passed it back).\n *\n * Earlier versions deep-merged with the on-disk file, but that made\n * deletions impossible: `setConfigKey(cfg, \"x\", undefined)` would\n * remove the key from memory, then the deep-merge would silently\n * re-add it from disk. The current design trusts the caller's\n * snapshot and uses atomic rename for crash safety.\n */\nexport async function saveConfig(\n next: AgentprotoConfig,\n path?: string,\n): Promise<void> {\n const target = path ?? CONFIG_FILE_PATH()\n const payload = { ...next, version: CONFIG_VERSION }\n const dir = dirname(target)\n await fs.mkdir(dir, { recursive: true })\n const tmp = `${target}.tmp`\n await fs.writeFile(tmp, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, target)\n}\n\n/**\n * Read a dot-notation key (`daemon.port`) out of a config. Returns\n * `undefined` when any segment is missing.\n */\nexport function getConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n): unknown {\n let cur: unknown = cfg\n for (const part of dotted.split(\".\")) {\n if (cur == null || typeof cur !== \"object\") return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * Set a dot-notation key in a config. Returns a new object — does\n * NOT mutate. Creates intermediate objects as needed. Setting\n * `value: undefined` is treated as a delete.\n */\nexport function setConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n value: unknown,\n): AgentprotoConfig {\n const parts = dotted.split(\".\")\n const out: AgentprotoConfig = { ...cfg }\n let cur: Record<string, unknown> = out as Record<string, unknown>\n for (let i = 0; i < parts.length - 1; i++) {\n const k = parts[i]!\n const next = cur[k]\n if (next && typeof next === \"object\" && !Array.isArray(next)) {\n cur[k] = { ...(next as Record<string, unknown>) }\n } else {\n cur[k] = {}\n }\n cur = cur[k] as Record<string, unknown>\n }\n const leaf = parts[parts.length - 1]!\n if (value === undefined) {\n delete cur[leaf]\n } else {\n cur[leaf] = value\n }\n return out\n}\n\n/**\n * Deep merge — objects are recursively combined, everything else\n * (arrays, primitives) is replaced wholesale by `b`. Mirrors what\n * `Object.assign({}, a, b)` does for shallow keys.\n */\nfunction deepMerge<A extends Record<string, unknown>, B extends Record<string, unknown>>(\n a: A,\n b: B,\n): A & B {\n const out: Record<string, unknown> = { ...a }\n for (const [k, v] of Object.entries(b)) {\n const cur = out[k]\n if (\n v &&\n typeof v === \"object\" &&\n !Array.isArray(v) &&\n cur &&\n typeof cur === \"object\" &&\n !Array.isArray(cur)\n ) {\n out[k] = deepMerge(\n cur as Record<string, unknown>,\n v as Record<string, unknown>,\n )\n } else {\n out[k] = v\n }\n }\n return out as A & B\n}\n"]}
1
+ {"version":3,"sources":["../src/config.ts"],"names":["fs"],"mappings":"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","file":"config.mjs","sourcesContent":["/**\n * `~/.agentproto/config.json` — single hand-editable JSON for the\n * agentproto control plane's defaults. Sits alongside the existing\n * surface files (workspaces.json, credentials.json, sessions.json):\n *\n * workspaces.json which directories are workspaces + which is active\n * credentials.json tunnel host bearer tokens (mode 0600)\n * sessions.json last-known snapshot of the registry (informational)\n * config.json daemon defaults: port, bind, allowed origins,\n * tunnel host, feature toggles\n *\n * Resolution order for every daemon knob is:\n * 1. CLI flag (e.g. --port)\n * 2. Env var (where one exists, e.g. AGENTPROTO_TOKEN)\n * 3. config.json\n * 4. Hardcoded default\n *\n * This means a user can call `agentproto config set daemon.port 18791`\n * once and never re-pass `--port 18791` to `serve install` etc. CLI\n * flags still win for one-off overrides.\n *\n * Schema is intentionally narrow + extensible — unknown keys are\n * preserved on save (deep-merge), so a newer CLI writing a new\n * field won't drop one an older CLI doesn't know about. No secrets\n * here; credentials stay in credentials.json (mode 0600).\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { dirname, join } from \"node:path\"\nimport type { SpawnDefaultsConfig } from \"./spawn-defaults.js\"\n\nexport const CONFIG_VERSION = 1 as const\n\nexport interface DaemonConfig {\n /** Absolute path to the workspace the daemon binds to at boot. */\n workspace?: string\n /** HTTP port. Default 18790. */\n port?: number\n /** Bind addr. Default 127.0.0.1. */\n bind?: string\n /** Trusted browser origins for mutating /sessions/* routes (in\n * addition to the hardcoded localhost defaults). */\n allowedOrigins?: string[]\n /** When true, the daemon does NOT auto-trust localhost-on-any-port.\n * Only origins explicitly listed in `allowedOrigins` are allowed.\n * Pair with a curated list (e.g. `[\"http://localhost:3000\"]`) for\n * hardened setups. Default false. */\n strictOrigins?: boolean\n /** Server label sent in tunnel hello frames. */\n label?: string\n /** Bearer token gating the gateway at boot (`AuthOptions` with\n * `mode: \"bearer\"`). Unlike `remote_enable`'s ephemeral quick-tunnel\n * token, this one lives in config.json and survives daemon restarts.\n * Set via `agentproto config set daemon.authToken <token>` (e.g.\n * `$(openssl rand -hex 32)`). Unset ⇒ the gateway boots with\n * `mode: \"none\"` — fully open on loopback, same as today. */\n authToken?: string\n /** Opt-in eager resume-on-boot (session-survivability plan §5\n * \"Opt-in vs automatic\", PR-4). When true, a daemon restart doesn't just\n * leave agent-cli sessions dead-but-lazy-resumable — the boot pass eagerly\n * re-spawns the eligible ones (those that died with\n * `endedReason: \"daemon-restart\"` and still pass `canResume`) IN PLACE,\n * restoring liveness without waiting for a prompt. Lazy resume-on-prompt is\n * always on regardless of this flag (it costs nothing until someone acts);\n * this flag only controls the *automatic* boot-time pass. Default false: a\n * box-wide restart should not silently relaunch every adapter (cost, spawn\n * storm, credential re-resolution), and default-off also keeps two daemons\n * sharing the workspace buckets from racing to resume the same rows. Set via\n * `agentproto config set daemon.resumeSessionsOnBoot true`. Surfaced in\n * `daemon_health` / `GET /health`. */\n resumeSessionsOnBoot?: boolean\n /** Idle agent-session reaper (PR-6). When set to a positive number of\n * milliseconds, the daemon periodically retires agent-cli sessions that have\n * been idle (not busy, not awaiting input, `status:\"running\"`) longer than\n * this: it SIGTERMs the adapter child to free the process and flips the row\n * to `killed`/`endedReason:\"idle-reaped\"` — dead-but-lazy-resumable (a later\n * prompt revives it) and, critically, excluded from eager resume-on-boot\n * (#638 gates on `endedReason === \"daemon-restart\"`), so enabling this makes\n * eager resume safe: only genuinely-recent work survives a restart. Default\n * OFF (unset / 0 / negative ⇒ the reaper never runs). Resolution order\n * mirrors the module docblock: `AGENTPROTO_IDLE_REAP_AFTER_MS` env > this\n * field > off. Set via `agentproto config set daemon.idleReapAfterMs\n * <ms>`. Surfaced in `daemon_health` / `GET /health`. */\n idleReapAfterMs?: number\n /** Crash-detect sweep interval in ms (crash-detect PR-1). Unlike\n * `idleReapAfterMs`, this is DEFAULT ON (non-destructive observability):\n * the daemon periodically probes every live agent-cli session's OS\n * process (`process.kill(pid, 0)`) and, when it's provably gone with no\n * exit event ever emitted for it, flips the row to\n * `error`/`endedReason:\"crashed\"` with a `lastError` string — surfacing a\n * death that would otherwise sit silently as `status:\"running\"` until the\n * next prompt's RPC throws (or forever, for a parked session with no next\n * prompt). Detects only; never restarts or notifies (later PRs). Set to a\n * non-positive value to disable the sweep entirely. Resolution order\n * mirrors `idleReapAfterMs`: `AGENTPROTO_CRASH_DETECT_INTERVAL_MS` env >\n * this field > the hardcoded default (30s). Set via `agentproto config\n * set daemon.crashDetectIntervalMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n crashDetectIntervalMs?: number\n /** Restart-scheduler sweep interval in ms (restart-scheduler PR-2). OFF by\n * default (unset / 0 / negative ⇒ the sweep never runs), unlike\n * `crashDetectIntervalMs` — a positive value here only arms the periodic\n * sweep that EXECUTES an already-scheduled restart; it never opts a\n * session in by itself (that's the per-session `agent_start.restartPolicy`\n * field). Resolution order mirrors `idleReapAfterMs`:\n * `AGENTPROTO_RESTART_SWEEP_INTERVAL_MS` env > this field > off. Set via\n * `agentproto config set daemon.restartSweepIntervalMs <ms>`. Surfaced in\n * `daemon_health` / `GET /health`. */\n restartSweepIntervalMs?: number\n /** Turn-liveness watchdog threshold in ms (turn-liveness-watchdog\n * chantier). Same shape as `crashDetectIntervalMs`: DEFAULT ON\n * (non-destructive observability). The daemon periodically sweeps every\n * BUSY agent-cli session and, for one that is mid-turn, NOT legitimately\n * `blockedOn` a subagent/command, and has had no adapter activity\n * (`lastActivityAt`) for longer than this threshold, stamps\n * `stalledSinceMs` on the descriptor and emits `session:stalled` —\n * surfacing a dead adapter stream (network drop, hung child — zero\n * frames mid-turn) that would otherwise sit indistinguishable from\n * healthy long work: `status:\"running\"`, `lastError:null`, no other\n * signal. Conservative by design: a session legitimately blocked on a\n * long tool call is NEVER a candidate, no matter how long it's silent —\n * see `stall-watchdog.ts`'s docblock for why. Detects only; never kills\n * or restarts (this is a later concern, if ever). Set to a non-positive\n * value to disable the sweep entirely. Resolution order mirrors\n * `idleReapAfterMs`: `AGENTPROTO_TURN_STALL_AFTER_MS` env > this field >\n * the hardcoded default (5 min). Set via `agentproto config set\n * daemon.turnStallAfterMs <ms>`. Surfaced in `daemon_health` /\n * `GET /health`. */\n turnStallAfterMs?: number\n}\n\nexport interface TunnelConfig {\n /** Cloud WS URL. When set + autoconnect=true, `agentproto serve`\n * bootstraps with `--connect <host>`. */\n host?: string\n /** apt_ daemon token to present at the tunnel upgrade. When set,\n * `agentproto serve` uses this BEFORE falling back to\n * credentials.json — handy in profiles where the token-per-host\n * mapping in credentials.json doesn't fit (e.g. host = tunnel URL\n * but credentials were minted against the api URL). */\n token?: string\n /** Whether `agentproto daemon start` connects the tunnel by\n * default. v0 only — implementer can ignore until daemon needs it. */\n autoconnect?: boolean\n /**\n * Opt into end-to-end encryption of the outbound `serve --connect` tunnel\n * (design: tunnel-e2e/v1). When true, the daemon negotiates a\n * token-authenticated ephemeral handshake with the host and wraps the tunnel\n * frames in an AEAD box, so even the trusted host loses plaintext visibility.\n * The handshake authenticates both ends against the shared `tunnel.token`, so\n * `token` MUST also be set. Fully backward-compatible: if the host doesn't\n * advertise e2e (an older host), the daemon falls back to today's plaintext\n * tunnel. Unset/false ⇒ plaintext, byte-identical to today. */\n e2e?: boolean\n}\n\nexport interface FeaturesConfig {\n /** Hint that PTY is desired — informational; the daemon still\n * detects node-pty's presence at runtime. */\n pty?: boolean\n /** Enable the local LLM Endpoint proxy sidecar (route registration,\n * MCP tools, child-process lifecycle). Default false — the endpoint is\n * an opt-in feature; when off, the `llm-endpoint` custom route is not\n * registered and the `llm_endpoint_*` MCP tools are not exposed. */\n llmEndpoint?: boolean\n}\n\n/**\n * Policy for `agentproto worktree new` (PLAN.md §1.4 — config carries\n * policy, never state; git itself is the authority for which worktrees\n * exist). This is the fix for the sprawl the plan measured: 31 linked\n * worktrees across 6 different parent directories, because there was no\n * `worktree new` verb and therefore no convention to converge on.\n */\n/**\n * How the daemon isolates a freshly-spawned agent session into its own git\n * worktree (`agent_start.worktree`):\n * - `\"always\"` — every depth-0 spawn is provisioned into a worktree,\n * whether or not the caller asked. A cwd that is not in\n * a git repo has nothing to isolate, so it spawns plain.\n * - `\"on-request\"` — isolate ONLY when the caller passes `worktree`. This\n * is the default and the back-compatible behaviour:\n * today's callers pass nothing and spawn exactly where\n * they asked.\n * - `\"never\"` — isolation is off; an explicit `worktree` field is\n * REJECTED (loud, not silently ignored) so a caller\n * never believes it got an isolated tree it didn't.\n */\nexport type WorktreeIsolationMode = \"always\" | \"on-request\" | \"never\"\n\nexport interface WorktreesConfig {\n /**\n * Absolute path new worktrees are created under. Layout:\n * `<root>/<repoName>/<slug>`. Resolution order (mirrors every other\n * knob in this file, see the module docblock): `--root` flag >\n * `AGENTPROTO_WORKTREES_ROOT` env > this field > the hardcoded default\n * `~/.agentproto/worktrees`. The default is a real single root, not\n * \"unconfigured\" — `worktree new` converges to one place with zero\n * setup, which is the only way the sprawl actually stops (the 6 roots\n * that exist today are 6 people each inventing a default by hand).\n */\n root?: string\n /**\n * Policy for `agent_start.worktree` isolation. Resolution order mirrors\n * the module docblock (there is no CLI flag — this is a daemon-side\n * policy read at spawn, not a per-invocation flag):\n * `AGENTPROTO_WORKTREES_ISOLATION` env > this field > the hardcoded\n * default `\"on-request\"`. `\"on-request\"` is deliberately the default:\n * any other would break back-compat by isolating callers that never\n * asked (see `worktree-isolation.ts`).\n */\n isolation?: WorktreeIsolationMode\n}\n\n/**\n * Policy for `agent_start.attach` — whether a spawn AUTO-attaches to its\n * calling session as parent lineage. `\"always\"` (the default) nests a\n * spawned child under the session that spawned it whenever that identity is\n * derivable (the trusted `?callerSessionId=` on the daemon self-ref URL, or\n * an explicit `parentSessionId` hint), so a supervisor's executors stop\n * landing as depth-0 orphans. `\"on-request\"` reverts to the pre-attach\n * behaviour: auto-attribution is off, and a child nests only when the caller\n * explicitly opts in (`attach: true` / `attach: { parent }` / an explicit\n * `parentSessionId`). Either way a per-call `attach: false` forces an\n * independent root, exactly as `worktree: false` opts out of isolation. See\n * `spawn-attach.ts`.\n */\nexport type SpawnAttachMode = \"always\" | \"on-request\"\n\n/**\n * Policy for `agent_start`'s IMPLICIT dedupe — what happens when a caller\n * spawns with NO `idempotencyKey` at all (see `spawn-dedupe.ts`):\n * - `\"always\"` — the daemon DERIVES an implicit key from the spawn's\n * `label` (required — see below) plus a hash of the\n * initial `prompt`, and dedupes a same-adapter/cwd\n * repeat against it exactly as an explicit key would.\n * This is the default, mirroring `attach`'s own\n * \"opt-in-only guard is not a guard\" precedent above.\n * Deriving needs a `label` to produce anything at all\n * — an unlabelled spawn is untouched, which is what\n * keeps this safe for the fan-out pattern this repo\n * exercises (several agents into one cwd with no\n * shared label): see `spawn-dedupe.ts`'s docblock for\n * the full false-dedup analysis, and PR #803's own\n * no-opt-in label+cwd warning backstop in\n * `session-spawn.ts`, which independently landed on\n * the same label-is-the-signal boundary.\n * - `\"on-request\"` — no implicit derivation; only an explicit\n * `idempotencyKey` dedupes (today's behaviour,\n * unchanged). A per-call `dedupe: true` still opts in\n * under this policy, mirroring `attach: true`.\n * Either way a per-call `dedupe: false` disables implicit derivation for\n * that one spawn regardless of policy — the escape hatch, mirroring\n * `attach: false` / `worktree: false`. An explicit `idempotencyKey` always\n * wins over a derived one (derivation is only attempted when the caller\n * supplied none).\n */\nexport type SpawnDedupeMode = \"always\" | \"on-request\"\n\nexport interface SpawnConfig {\n /**\n * Attach policy for `agent_start`. Resolution order mirrors the module\n * docblock (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_ATTACH` env > this field > the hardcoded default\n * `\"always\"`. Unlike `worktrees.isolation` (whose default preserves\n * back-compat by NOT isolating), attach defaults ON: an orphaned executor\n * is a bug, not a feature, and the auto-parent is descriptor-only lineage\n * that never relaxes a privilege gate.\n */\n attach?: SpawnAttachMode\n /**\n * Implicit-dedupe policy for `agent_start`. Resolution order mirrors\n * `attach` above (no CLI flag — a daemon-side policy read at spawn):\n * `AGENTPROTO_SPAWN_DEDUPE` env > this field > the hardcoded default\n * `\"always\"`. See {@link SpawnDedupeMode} and `spawn-dedupe.ts` for the\n * full reasoning: a retry-safety guard that only works when a caller\n * remembers to ask for it (`idempotencyKey`) is not a guard — the same\n * argument `attach` already settled for parent lineage.\n */\n dedupe?: SpawnDedupeMode\n}\n\n/**\n * Provenance policy — the opt-in `gh` PATH shim (`gh-provenance-shim.ts`).\n * Off by default. When `wrapGh` is on, every agent session the daemon spawns\n * gets a shim directory prepended to its PATH so that any `gh pr create` the\n * session (or an adapter subprocess shelling out — claude-code, codex, …) runs\n * has a deterministic `@agentproto-bot` provenance footer appended to the\n * created PR's BODY, matching the footer the cloud runner stamps. The TOOL\n * stamps, never the model; commit messages are never touched (the repo's\n * hygiene-check forbids attribution there). Resolution order mirrors\n * `spawn.attach`: `AGENTPROTO_PROVENANCE_WRAP_GH` env > this field > default\n * `false`.\n */\nexport interface ProvenanceConfig {\n /** Enable the opt-in `gh` provenance PATH shim for spawned sessions. */\n wrapGh?: boolean\n}\n\nexport interface PairingConfig {\n /** Rendezvous broker WS URL (ws:// or wss://) used by `pair offer` and by\n * autoconnect on boot. When unset, `pair offer` requires an explicit\n * `--rendezvous`. Mirrors `tunnel.host`. */\n rendezvous?: string\n /** Whether the daemon opens standing rendezvous connections for every\n * persisted pairing on boot (so a paired client can reconnect anytime).\n * Mirrors `tunnel.autoconnect`. Default true when a rendezvous is set. */\n autoconnect?: boolean\n}\n\n/**\n * A user-defined generic ACP agent — the config-file half of\n * `AcpAgentSpec` (the slug is the record key in `acpAgents`, so it's\n * omitted here). Any CLI that already speaks the Agent Client Protocol\n * can be wired with zero code by declaring one of these under\n * `acpAgents.<slug>` in `~/.agentproto/config.json`; the CLI's\n * `acpHandleFromSpec` mints a runnable `AgentCliHandle` from it at\n * resolve time (see `packages/cli/src/registry/acp-generic.ts`). Kept\n * in this package (not the CLI's) so `config.ts` stays the single\n * source of truth for the config surface without a cli→runtime→cli\n * import cycle — the CLI's `AcpAgentSpec` extends this shape.\n */\nexport interface AcpAgentConfigEntry {\n /** Display name. Defaults to the slug when omitted. */\n name?: string\n /** One-line description surfaced in `agentproto acp ls`. */\n description?: string\n /** Executable to spawn, e.g. \"gemini\". */\n bin: string\n /** Extra argv appended after `bin`, e.g. [\"--experimental-acp\"]. */\n bin_args?: string[]\n /** Extra environment variables for the spawned process. */\n env?: Record<string, string>\n /** Flag the CLI uses to receive the working directory, if it needs\n * one passed explicitly (most ACP agents take cwd over the wire). */\n cwd_flag?: string\n /** When true, advertise resumable + native-resume continuation. */\n resumable?: boolean\n /** Known model ids for the agent (informational + validation hints). */\n models?: { default?: string; allowed?: string[] }\n /** Billing endpoint this CLI's own auth bills (e.g. \"mistral\",\n * \"moonshot\", \"google\") — the provider whose wallet/auth-profile the\n * agent consumes. Lets clients link the harness to that provider's\n * wallets even when no model list is declared. Unset when the CLI's\n * billing target isn't a single known endpoint. */\n provider?: string\n /** Shown when `bin` is missing from PATH (how to install the CLI). */\n install_hint?: string\n}\n\n/**\n * Per-environment connection bundle. A profile overrides specific\n * fields of the top-level `daemon` / `tunnel` / `features` blocks\n * when selected via `--profile <name>` (or the top-level\n * `activeProfile` setting). Missing fields fall through to the\n * top-level config, so a profile only needs to declare what's\n * different — typically just `tunnel.host` + `tunnel.token`.\n *\n * Example:\n * {\n * \"daemon\": { \"workspace\": \"/code\", \"port\": 18790 },\n * \"activeProfile\": \"local\",\n * \"profiles\": {\n * \"local\": { \"tunnel\": { \"host\": \"ws://localhost:3200/connect\",\n * \"token\": \"apt_local\", \"autoconnect\": true } },\n * \"prod\": { \"tunnel\": { \"host\": \"wss://tunnel.guilde.work/connect\",\n * \"token\": \"apt_prod\", \"autoconnect\": true },\n * \"daemon\": { \"port\": 18791 } }\n * }\n * }\n *\n * Sandbox daemons generate per-sandbox profile entries at provision\n * time so the daemon inside the sandbox boots with\n * `agentproto serve --profile sandbox-<id>` and no extra plumbing.\n */\nexport interface ProfileConfig {\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n}\n\n/**\n * A user-defined named terminal/TUI preset stored in\n * `~/.agentproto/config.json` under `terminalPresets`. Presets keep\n * local launch recipes (argv, env, cwd, name/label) out of shared\n * adapter manifests — e.g. pointing a Claude Code TUI at a local\n * LLM gateway without retyping proxy env vars every spawn.\n */\nexport interface TerminalPreset {\n /** Command + args to spawn. When provided, `sessions terminal` can\n * be used without `-- <argv...>`. */\n argv?: string[]\n /** Extra environment variables layered on top of the daemon's\n * inherited process.env. Values MUST be strings. */\n env?: Record<string, string>\n /** Working directory for the PTY session. Relative paths are\n * resolved against the current working directory at CLI time. */\n cwd?: string\n /** Workspace slug used for cwd fallback when `cwd` is omitted. */\n workspace?: string\n /** Stable session name passed to the registry (`name` field). */\n name?: string\n /** Human-readable label surfaced in session listings. */\n label?: string\n}\n\nexport interface AgentprotoConfig {\n version?: number\n daemon?: DaemonConfig\n tunnel?: TunnelConfig\n features?: FeaturesConfig\n /** E2E daemon-pairing defaults (rendezvous URL + autoconnect). */\n pairing?: PairingConfig\n /** Where `agentproto worktree new` creates worktrees. See\n * {@link WorktreesConfig}. */\n worktrees?: WorktreesConfig\n /** Spawn-time policy (`agent_start`). See {@link SpawnConfig}. */\n spawn?: SpawnConfig\n /** Provenance policy — the opt-in `gh` PATH shim. See {@link ProvenanceConfig}. */\n provenance?: ProvenanceConfig\n /** Named connection profiles. See `ProfileConfig` for the merge\n * semantics — a profile's fields shallow-override the top-level\n * defaults for the selected run. */\n profiles?: Record<string, ProfileConfig>\n /** Profile name to use when `--profile` isn't passed. When unset,\n * the top-level `daemon` / `tunnel` blocks are used directly. */\n activeProfile?: string\n /** Default `skills` + `options` auto-applied to every `agent_start`\n * spawn — global and per-adapter. See `resolveSpawnDefaults` in\n * `spawn-defaults.ts` for the merge precedence with an explicit call.\n * Absent ⇒ current behaviour exactly (no regression). */\n defaults?: SpawnDefaultsConfig\n /** User-defined generic ACP agents, keyed by adapter slug. Each entry\n * is minted into a runnable handle by the CLI's `acpHandleFromSpec`\n * when `resolveAdapter(slug)` finds no npm adapter package. User\n * entries shadow the curated `ACP_CATALOG` on slug collision. */\n acpAgents?: Record<string, AcpAgentConfigEntry>\n /** User-defined named terminal/TUI presets. Local-only; never\n * packaged in shared adapter manifests or defaults. */\n terminalPresets?: Record<string, TerminalPreset>\n /** Unknown keys preserved across save round-trips. */\n [unknown: string]: unknown\n}\n\nexport const CONFIG_FILE_PATH = (): string =>\n join(homedir(), \".agentproto\", \"config.json\")\n\n/**\n * Drop any `acpAgents` entries that aren't a shape we can turn into a\n * handle. The one hard requirement is a non-empty string `bin` (the\n * executable to spawn); everything else is optional and defaulted\n * downstream. Invalid entries are removed rather than throwing so the\n * daemon still boots — one warning names the offending slug so the\n * user can fix their config. Returns `undefined` when nothing valid\n * remains, keeping the key absent (== \"no generic agents\").\n */\nfunction sanitizeAcpAgents(\n raw: unknown,\n target: string,\n): Record<string, AcpAgentConfigEntry> | undefined {\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n console.warn(\n `[runtime/config] ${target}: 'acpAgents' is not an object — ignoring`,\n )\n return undefined\n }\n const out: Record<string, AcpAgentConfigEntry> = {}\n for (const [slug, value] of Object.entries(raw as Record<string, unknown>)) {\n if (\n value &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n typeof (value as { bin?: unknown }).bin === \"string\" &&\n (value as { bin: string }).bin.length > 0\n ) {\n out[slug] = value as AcpAgentConfigEntry\n } else {\n console.warn(\n `[runtime/config] ${target}: acpAgents.${slug} is missing a string 'bin' — ignoring`,\n )\n }\n }\n return Object.keys(out).length > 0 ? out : undefined\n}\n\n/**\n * Load config.json. Returns an empty object (NOT null) when the file\n * is missing, malformed, or unreadable — callers can `cfg.daemon?.port`\n * safely without null-guards. Errors during a malformed-read are\n * logged once so the user notices the file is broken without the\n * daemon refusing to boot.\n */\nexport async function loadConfig(path?: string): Promise<AgentprotoConfig> {\n const target = path ?? CONFIG_FILE_PATH()\n try {\n const raw = await fs.readFile(target, \"utf8\")\n const parsed = JSON.parse(raw) as unknown\n if (parsed && typeof parsed === \"object\" && !Array.isArray(parsed)) {\n const cfg = parsed as AgentprotoConfig\n // Sanitize `acpAgents` in the same tolerant spirit as the rest of\n // this loader: a malformed entry is dropped (with one warning) so a\n // single bad hand-edit can't make every generic ACP agent\n // unresolvable. Full AIP-45 validation happens later, at\n // `acpHandleFromSpec` time, with precise field-level messages.\n if (cfg.acpAgents !== undefined) {\n cfg.acpAgents = sanitizeAcpAgents(cfg.acpAgents, target)\n }\n return cfg\n }\n console.warn(\n `[runtime/config] ${target}: top-level value is not an object — ignoring`,\n )\n return {}\n } catch (err) {\n // ENOENT is the common case; only warn on other shapes.\n const code = (err as NodeJS.ErrnoException).code\n if (code && code !== \"ENOENT\") {\n console.warn(\n `[runtime/config] failed to read ${target}: ${\n err instanceof Error ? err.message : String(err)\n }`,\n )\n }\n return {}\n }\n}\n\n/**\n * Write config.json atomically (tmp + rename) so a concurrent\n * `agentproto config edit` can't half-truncate the file. Writes\n * `next` AS-IS — callers are expected to pass the full desired\n * state (loaded the existing config, mutated, passed it back).\n *\n * Earlier versions deep-merged with the on-disk file, but that made\n * deletions impossible: `setConfigKey(cfg, \"x\", undefined)` would\n * remove the key from memory, then the deep-merge would silently\n * re-add it from disk. The current design trusts the caller's\n * snapshot and uses atomic rename for crash safety.\n */\nexport async function saveConfig(\n next: AgentprotoConfig,\n path?: string,\n): Promise<void> {\n const target = path ?? CONFIG_FILE_PATH()\n const payload = { ...next, version: CONFIG_VERSION }\n const dir = dirname(target)\n await fs.mkdir(dir, { recursive: true })\n const tmp = `${target}.tmp`\n await fs.writeFile(tmp, JSON.stringify(payload, null, 2) + \"\\n\", \"utf8\")\n await fs.rename(tmp, target)\n}\n\n/**\n * Read a dot-notation key (`daemon.port`) out of a config. Returns\n * `undefined` when any segment is missing.\n */\nexport function getConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n): unknown {\n let cur: unknown = cfg\n for (const part of dotted.split(\".\")) {\n if (cur == null || typeof cur !== \"object\") return undefined\n cur = (cur as Record<string, unknown>)[part]\n }\n return cur\n}\n\n/**\n * Set a dot-notation key in a config. Returns a new object — does\n * NOT mutate. Creates intermediate objects as needed. Setting\n * `value: undefined` is treated as a delete.\n */\nexport function setConfigKey(\n cfg: AgentprotoConfig,\n dotted: string,\n value: unknown,\n): AgentprotoConfig {\n const parts = dotted.split(\".\")\n const out: AgentprotoConfig = { ...cfg }\n let cur: Record<string, unknown> = out as Record<string, unknown>\n for (let i = 0; i < parts.length - 1; i++) {\n const k = parts[i]!\n const next = cur[k]\n if (next && typeof next === \"object\" && !Array.isArray(next)) {\n cur[k] = { ...(next as Record<string, unknown>) }\n } else {\n cur[k] = {}\n }\n cur = cur[k] as Record<string, unknown>\n }\n const leaf = parts[parts.length - 1]!\n if (value === undefined) {\n delete cur[leaf]\n } else {\n cur[leaf] = value\n }\n return out\n}\n\n/**\n * Deep merge — objects are recursively combined, everything else\n * (arrays, primitives) is replaced wholesale by `b`. Mirrors what\n * `Object.assign({}, a, b)` does for shallow keys.\n */\nfunction deepMerge<A extends Record<string, unknown>, B extends Record<string, unknown>>(\n a: A,\n b: B,\n): A & B {\n const out: Record<string, unknown> = { ...a }\n for (const [k, v] of Object.entries(b)) {\n const cur = out[k]\n if (\n v &&\n typeof v === \"object\" &&\n !Array.isArray(v) &&\n cur &&\n typeof cur === \"object\" &&\n !Array.isArray(cur)\n ) {\n out[k] = deepMerge(\n cur as Record<string, unknown>,\n v as Record<string, unknown>,\n )\n } else {\n out[k] = v\n }\n }\n return out as A & B\n}\n"]}
@@ -8,6 +8,18 @@
8
8
  * turn boundaries so sessions never silently run into the model context
9
9
  * limit.
10
10
  */
11
+ /**
12
+ * `manual` suppresses every automatic *nudge* (warn/compact/continue-fresh —
13
+ * see `contextContinuityNextAction`) so the session is left alone until the
14
+ * caller acts. It does NOT suppress `hard-stop`: that state is an
15
+ * unconditional safety floor, independent of mode, by design — its whole
16
+ * purpose is to stop a session from silently running past the model's real
17
+ * context window (further prompts would be truncated/rejected by the
18
+ * provider, or corrupt the conversation) even when nothing is watching.
19
+ * `manual` means "don't nag me", not "let me drive off the window's edge".
20
+ * A session that wants to opt out of the hard stop should set
21
+ * `hardStopAtPct` above what it will ever reach, not rely on mode.
22
+ */
11
23
  type ContextContinuityMode = "manual" | "ask" | "auto";
12
24
  interface ContextContinuityThresholds {
13
25
  /** Percentage at which the UI first warns that context is filling. */