switchroom 0.19.33 → 0.19.35

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.
@@ -4432,6 +4432,7 @@ var init_schema = __esm(() => {
4432
4432
  }).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback — switchroom emits only the vars set.");
4433
4433
  HindsightConfigSchema = exports_external.object({
4434
4434
  gpu: exports_external.boolean().optional().describe("Force GPU passthrough for the hindsight container on (`true`) or off " + "(`false`), overriding host autodetection in BOTH directions. Absent " + "(the default) → autodetect from the persisted host-capabilities " + "verdict (`~/.switchroom/host-capabilities.json`), which enables " + "`--gpus all` only when that file proves BOTH a GPU and the nvidia " + "container toolkit. Set `true` when that verdict is wrong or unreadable " + "and you know the host has a working toolkit — switchroom cannot verify " + "it for you, and `docker run --gpus all` hard-fails container create on " + "a host without one. Set `false` to pin the container to CPU on a GPU " + "host. This is also the declarative opt-out for the recreate-time GPU " + "drop guard (`switchroom memory setup --recreate` refuses to silently " + "turn a GPU container into a CPU one). `--gpu`/`--no-gpu` on `memory " + "setup` override this for a single run."),
4435
+ cp_access_key: exports_external.string().min(1).optional().describe("Access key for the hindsight control-plane DASHBOARD (upstream " + "`HINDSIGHT_CP_ACCESS_KEY`, port 9999). Literal or — strongly preferred " + "— a `vault:` reference such as `vault:hindsight_cp_access_key`, read " + "through the broker at container-launch time. This is the ONLY thing " + "that arms the dashboard's login: upstream's middleware short-circuits " + "to `next()` when the var is unset, so an unset key means the dashboard " + "has no authentication at all, not weak authentication. Because of that, " + "leaving it unset is FAIL-CLOSED: switchroom pins " + "`HINDSIGHT_CP_HOSTNAME=127.0.0.1` so the loginless dashboard is " + "reachable only from the host, and warns. Set this to serve the " + "dashboard on the LAN/tailnet."),
4435
4436
  llm: exports_external.object({
4436
4437
  provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
4437
4438
  model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
@@ -4961,108 +4962,440 @@ var init_atomic = __esm(() => {
4961
4962
  TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
4962
4963
  });
4963
4964
 
4964
- // src/config/merge.ts
4965
- function dedupe(items) {
4966
- const seen = new Set;
4967
- const out = [];
4968
- for (const item of items) {
4969
- if (seen.has(item))
4970
- continue;
4971
- seen.add(item);
4972
- out.push(item);
4965
+ // src/vault/broker/peercred-ffi.ts
4966
+ function getPeerCred(fd) {
4967
+ if (process.platform !== "linux")
4968
+ return null;
4969
+ try {
4970
+ const ffi = __require("bun:ffi");
4971
+ const { dlopen, FFIType, ptr } = ffi;
4972
+ const SOL_SOCKET = 1;
4973
+ const SO_PEERCRED = 17;
4974
+ const UCRED_SIZE = 12;
4975
+ const cache = getPeerCred;
4976
+ const lib = cache._lib ?? (() => {
4977
+ const candidates = ["libc.so.6", "libc.so"];
4978
+ const symbolSpec = {
4979
+ getsockopt: {
4980
+ args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
4981
+ returns: FFIType.i32
4982
+ }
4983
+ };
4984
+ const errors2 = [];
4985
+ for (const name of candidates) {
4986
+ try {
4987
+ const opened = dlopen(name, symbolSpec);
4988
+ cache._lib = opened;
4989
+ return opened;
4990
+ } catch (e) {
4991
+ errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
4992
+ }
4993
+ }
4994
+ process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
4995
+ `);
4996
+ throw new Error("no libc candidate could be opened");
4997
+ })();
4998
+ const credBuf = new ArrayBuffer(UCRED_SIZE);
4999
+ const lenBuf = new Uint32Array(1);
5000
+ lenBuf[0] = UCRED_SIZE;
5001
+ const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
5002
+ if (rc !== 0)
5003
+ return null;
5004
+ if (lenBuf[0] !== UCRED_SIZE)
5005
+ return null;
5006
+ const view = new DataView(credBuf);
5007
+ return {
5008
+ pid: view.getInt32(0, true),
5009
+ uid: view.getInt32(4, true),
5010
+ gid: view.getInt32(8, true)
5011
+ };
5012
+ } catch {
5013
+ return null;
4973
5014
  }
4974
- return out;
4975
5015
  }
4976
- function deepMergeJson(base, override) {
4977
- if (override === undefined)
4978
- return base;
4979
- if (base === undefined)
4980
- return override;
4981
- if (typeof base !== "object" || base === null || Array.isArray(base) || typeof override !== "object" || override === null || Array.isArray(override)) {
4982
- return override;
4983
- }
4984
- const out = { ...base };
4985
- for (const [k, v] of Object.entries(override)) {
4986
- if (k === "__proto__" || k === "constructor" || k === "prototype")
4987
- continue;
4988
- out[k] = deepMergeJson(out[k], v);
5016
+
5017
+ // src/broker-common/peercred-path.ts
5018
+ function matchSocketName(socketPath, patterns, maxNameLen) {
5019
+ if (typeof socketPath !== "string" || socketPath.length === 0)
5020
+ return null;
5021
+ let name = null;
5022
+ for (const re of patterns) {
5023
+ const m = socketPath.match(re);
5024
+ if (m && typeof m[1] === "string") {
5025
+ name = m[1];
5026
+ break;
5027
+ }
4989
5028
  }
4990
- return out;
5029
+ if (name === null || name.length === 0)
5030
+ return null;
5031
+ if (maxNameLen !== undefined && name.length > maxNameLen)
5032
+ return null;
5033
+ return name;
4991
5034
  }
4992
- function resolveAgentConfig(defaults, profiles, agent) {
4993
- if (!mergeAgentConfig.suppressDeprecationLogs && !mergeAgentConfig.notifiedWorkerIsolationMove && defaults?.subagents?.worker?.isolation === "worktree") {
4994
- mergeAgentConfig.notifiedWorkerIsolationMove = true;
4995
- console.warn("[switchroom] NOTICE: defaults.subagents.worker.isolation moved to the " + "`coding` profile in switchroom 0.6.6 (#682). Agents extending coding " + "still get worktree-isolated workers; other agents would have hard-failed " + "the first time they delegated. See CHANGELOG.");
4996
- }
4997
- const name = agent.extends;
4998
- const profile = name && profiles ? profiles[name] : undefined;
4999
- if (!profile) {
5000
- return mergeAgentConfig(defaults, agent);
5001
- }
5002
- const { extends: _unused, ...profileWithoutExtends } = profile;
5003
- const layered = mergeAgentConfig(defaults, profileWithoutExtends);
5004
- return mergeAgentConfig(layered, agent);
5035
+ function parseSocketIdentity(socketPath, spec) {
5036
+ const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
5037
+ if (name === null)
5038
+ return null;
5039
+ const operatorName = spec.operatorName ?? "operator";
5040
+ if (name === operatorName)
5041
+ return { kind: "operator" };
5042
+ if (spec.reservedNames.has(name))
5043
+ return null;
5044
+ return { kind: "agent", name };
5005
5045
  }
5006
- function foldDeprecatedTelegramFields(config) {
5007
- const c = config;
5008
- const root = c;
5009
- const deprecations = [];
5010
- const hasRoot = root.voice_in !== undefined || root.telegraph !== undefined || root.webhook_sources !== undefined;
5011
- if (!hasRoot)
5012
- return { config: c, deprecations };
5013
- const channels = { ...c.channels ?? {} };
5014
- const tg = { ...channels.telegram ?? {} };
5015
- if (root.voice_in !== undefined) {
5016
- if (tg.voice_in === undefined)
5017
- tg.voice_in = root.voice_in;
5018
- deprecations.push("voice_in at the agent root is deprecated; move under channels.telegram.voice_in (#596).");
5019
- }
5020
- if (root.telegraph !== undefined) {
5021
- if (tg.telegraph === undefined)
5022
- tg.telegraph = root.telegraph;
5023
- deprecations.push("telegraph at the agent root is deprecated; move under channels.telegram.telegraph (#596).");
5046
+
5047
+ // src/vault/broker/peercred.ts
5048
+ import { execFileSync } from "node:child_process";
5049
+ import { readFileSync, readlinkSync, fstatSync } from "node:fs";
5050
+ function socketPathToIdentity(socketPath) {
5051
+ return parseSocketIdentity(socketPath, {
5052
+ patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
5053
+ reservedNames: RESERVED_AGENT_NAMES
5054
+ });
5055
+ }
5056
+ function socketPathToAgent(socketPath) {
5057
+ const identity = socketPathToIdentity(socketPath);
5058
+ return identity?.kind === "agent" ? identity.name : null;
5059
+ }
5060
+ function isReservedAgentName(name) {
5061
+ return RESERVED_AGENT_NAMES.has(name);
5062
+ }
5063
+ function unlockSocketFor(dataSocketPath) {
5064
+ if (dataSocketPath.endsWith("/sock")) {
5065
+ return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
5024
5066
  }
5025
- if (root.webhook_sources !== undefined) {
5026
- if (tg.webhook_sources === undefined)
5027
- tg.webhook_sources = root.webhook_sources;
5028
- deprecations.push("webhook_sources at the agent root is deprecated; move under channels.telegram.webhook_sources (#596).");
5067
+ return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
5068
+ }
5069
+ function parseSsRows(output) {
5070
+ const rows = [];
5071
+ const lines = output.split(`
5072
+ `);
5073
+ for (const line of lines) {
5074
+ if (!line.trim() || line.startsWith("Netid"))
5075
+ continue;
5076
+ const tokens = line.split(/\s+/).filter((t) => t.length > 0);
5077
+ if (tokens.length < 8)
5078
+ continue;
5079
+ const localAddr = tokens[4];
5080
+ const localInode = tokens[5];
5081
+ const peerAddr = tokens[6];
5082
+ const peerInode = tokens[7];
5083
+ const usersToken = tokens.slice(8).join(" ");
5084
+ const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
5085
+ const pid = m ? parseInt(m[1], 10) : null;
5086
+ rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
5029
5087
  }
5030
- channels.telegram = tg;
5031
- const { voice_in: _vi, telegraph: _tg, webhook_sources: _ws, ...rest } = root;
5032
- return {
5033
- config: { ...rest, channels },
5034
- deprecations
5035
- };
5088
+ return rows;
5036
5089
  }
5037
- function mergeAgentConfig(defaultsIn, agentIn) {
5038
- const { config: agent, deprecations: agentDeprecations } = foldDeprecatedTelegramFields(agentIn);
5039
- const defaultsMigration = defaultsIn ? foldDeprecatedTelegramFields(defaultsIn) : null;
5040
- const defaults = defaultsMigration?.config;
5041
- const allDeprecations = [
5042
- ...agentDeprecations,
5043
- ...defaultsMigration?.deprecations ?? []
5044
- ];
5045
- if (allDeprecations.length > 0 && !mergeAgentConfig.suppressDeprecationLogs) {
5046
- for (const msg of allDeprecations) {
5047
- console.warn(`[switchroom] DEPRECATION: ${msg}`);
5090
+ function findClientPids(rows, socketPath) {
5091
+ const pids = [];
5092
+ for (const serverRow of rows) {
5093
+ if (serverRow.localAddr !== socketPath)
5094
+ continue;
5095
+ for (const clientRow of rows) {
5096
+ if (clientRow.localAddr !== "*")
5097
+ continue;
5098
+ if (clientRow.localInode !== serverRow.peerInode)
5099
+ continue;
5100
+ if (clientRow.pid === null)
5101
+ continue;
5102
+ pids.push(clientRow.pid);
5103
+ break;
5048
5104
  }
5049
5105
  }
5050
- if (!defaults)
5051
- return agent;
5052
- const merged = { ...agent };
5053
- if (defaults.bot_token !== undefined && merged.bot_token === undefined) {
5054
- merged.bot_token = defaults.bot_token;
5055
- }
5056
- if (defaults.timezone !== undefined && merged.timezone === undefined) {
5057
- merged.timezone = defaults.timezone;
5106
+ return pids;
5107
+ }
5108
+ function findClientPidByServerInode(rows, socketPath, serverInode) {
5109
+ const serverInodeStr = String(serverInode);
5110
+ for (const serverRow of rows) {
5111
+ if (serverRow.localAddr !== socketPath)
5112
+ continue;
5113
+ if (serverRow.localInode !== serverInodeStr)
5114
+ continue;
5115
+ for (const clientRow of rows) {
5116
+ if (clientRow.localAddr !== "*")
5117
+ continue;
5118
+ if (clientRow.localInode !== serverRow.peerInode)
5119
+ continue;
5120
+ if (clientRow.pid === null)
5121
+ continue;
5122
+ return clientRow.pid;
5123
+ }
5124
+ return null;
5058
5125
  }
5059
- if (defaults.model !== undefined && merged.model === undefined) {
5060
- merged.model = defaults.model;
5126
+ return null;
5127
+ }
5128
+ function readUid(pid) {
5129
+ try {
5130
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
5131
+ const m = status.match(/^Uid:\s+(\d+)/m);
5132
+ if (!m)
5133
+ return null;
5134
+ return parseInt(m[1], 10);
5135
+ } catch {
5136
+ return null;
5061
5137
  }
5062
- if (defaults.dangerous_mode !== undefined && merged.dangerous_mode === undefined) {
5063
- merged.dangerous_mode = defaults.dangerous_mode;
5138
+ }
5139
+ function readExe(pid) {
5140
+ try {
5141
+ return readlinkSync(`/proc/${pid}/exe`);
5142
+ } catch {
5143
+ return null;
5064
5144
  }
5065
- if (defaults.network_isolation !== undefined && merged.network_isolation === undefined) {
5145
+ }
5146
+ function readSystemdUnit(pid) {
5147
+ try {
5148
+ const content = readFileSync(`/proc/${pid}/cgroup`, "utf8");
5149
+ const lines = content.split(`
5150
+ `);
5151
+ for (const line of lines) {
5152
+ if (!line.trim())
5153
+ continue;
5154
+ const parts = line.split(":");
5155
+ if (parts.length < 3)
5156
+ continue;
5157
+ const controller = parts[1];
5158
+ const isV2 = parts[0] === "0" && controller === "";
5159
+ const isV1Systemd = controller === "name=systemd";
5160
+ if (!isV2 && !isV1Systemd)
5161
+ continue;
5162
+ const cgroupPath = parts.slice(2).join(":");
5163
+ const segments = cgroupPath.split("/");
5164
+ const lastSegment = segments[segments.length - 1];
5165
+ if (!lastSegment)
5166
+ continue;
5167
+ if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
5168
+ return lastSegment;
5169
+ }
5170
+ }
5171
+ return null;
5172
+ } catch {
5173
+ return null;
5174
+ }
5175
+ }
5176
+ function verifySystemdUnit(unitName, runner) {
5177
+ let raw;
5178
+ try {
5179
+ const out = runner("systemctl", [
5180
+ "--user",
5181
+ "show",
5182
+ unitName,
5183
+ "--property=LoadState,ActiveState"
5184
+ ], { timeout: 500, encoding: "utf8" });
5185
+ raw = typeof out === "string" ? out : out.toString("utf8");
5186
+ } catch {
5187
+ return false;
5188
+ }
5189
+ const props = {};
5190
+ for (const line of raw.split(`
5191
+ `)) {
5192
+ const m = line.match(/^([A-Za-z]+)=(.*)$/);
5193
+ if (m)
5194
+ props[m[1]] = m[2];
5195
+ }
5196
+ if (props.LoadState !== "loaded")
5197
+ return false;
5198
+ if (props.ActiveState !== "active" && props.ActiveState !== "activating") {
5199
+ return false;
5200
+ }
5201
+ return true;
5202
+ }
5203
+ function readFdInode(fd) {
5204
+ try {
5205
+ const stat = fstatSync(fd);
5206
+ return stat.ino;
5207
+ } catch {
5208
+ return null;
5209
+ }
5210
+ }
5211
+ function fdFromSocket(socket) {
5212
+ const handle = socket._handle;
5213
+ if (!handle || typeof handle.fd !== "number" || handle.fd < 0)
5214
+ return null;
5215
+ return handle.fd;
5216
+ }
5217
+ function identify(socketPath, socket, execFileSyncOverride) {
5218
+ if (process.platform !== "linux") {
5219
+ return null;
5220
+ }
5221
+ const runner = execFileSyncOverride ?? execFileSync;
5222
+ let pid = null;
5223
+ if (socket !== undefined) {
5224
+ const fd = fdFromSocket(socket);
5225
+ if (fd !== null) {
5226
+ const cred = getPeerCred(fd);
5227
+ if (cred !== null)
5228
+ pid = cred.pid;
5229
+ }
5230
+ }
5231
+ if (pid === null) {
5232
+ let ssOutput;
5233
+ try {
5234
+ const raw = runner("ss", ["-xpn"], {
5235
+ timeout: 200,
5236
+ encoding: "utf8"
5237
+ });
5238
+ ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
5239
+ } catch {
5240
+ return null;
5241
+ }
5242
+ const rows = parseSsRows(ssOutput);
5243
+ let serverInode = null;
5244
+ if (socket !== undefined) {
5245
+ const fd = fdFromSocket(socket);
5246
+ if (fd !== null)
5247
+ serverInode = readFdInode(fd);
5248
+ }
5249
+ if (serverInode !== null) {
5250
+ pid = findClientPidByServerInode(rows, socketPath, serverInode);
5251
+ } else {
5252
+ const clientPids = findClientPids(rows, socketPath);
5253
+ if (clientPids.length === 0)
5254
+ return null;
5255
+ if (clientPids.length > 1) {
5256
+ process.stderr.write(`[vault-broker] peercred: ${clientPids.length} connected peers found for ${socketPath}; ` + `using pid=${clientPids[0]}. ` + `Multiple simultaneous connections reduce identification accuracy. ` + `(This warning means identify() was called without a socket arg — likely a stale call site.)
5257
+ `);
5258
+ }
5259
+ pid = clientPids[0];
5260
+ }
5261
+ }
5262
+ if (pid === null)
5263
+ return null;
5264
+ const uid = readUid(pid);
5265
+ if (uid === null) {
5266
+ return null;
5267
+ }
5268
+ const brokerUid = typeof process.getuid === "function" ? process.getuid() : null;
5269
+ if (brokerUid !== null && uid !== brokerUid) {
5270
+ process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
5271
+ `);
5272
+ return null;
5273
+ }
5274
+ const exe = readExe(pid);
5275
+ if (exe === null) {
5276
+ return null;
5277
+ }
5278
+ const cgroupClaim = readSystemdUnit(pid);
5279
+ let systemdUnit = null;
5280
+ if (cgroupClaim !== null) {
5281
+ if (verifySystemdUnit(cgroupClaim, runner)) {
5282
+ systemdUnit = cgroupClaim;
5283
+ } else {
5284
+ process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
5285
+ `);
5286
+ }
5287
+ }
5288
+ return { uid, pid, exe, systemdUnit };
5289
+ }
5290
+ var SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE, RESERVED_AGENT_NAMES;
5291
+ var init_peercred = __esm(() => {
5292
+ SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
5293
+ SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
5294
+ RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
5295
+ });
5296
+
5297
+ // src/config/merge.ts
5298
+ function dedupe(items) {
5299
+ const seen = new Set;
5300
+ const out = [];
5301
+ for (const item of items) {
5302
+ if (seen.has(item))
5303
+ continue;
5304
+ seen.add(item);
5305
+ out.push(item);
5306
+ }
5307
+ return out;
5308
+ }
5309
+ function deepMergeJson(base, override) {
5310
+ if (override === undefined)
5311
+ return base;
5312
+ if (base === undefined)
5313
+ return override;
5314
+ if (typeof base !== "object" || base === null || Array.isArray(base) || typeof override !== "object" || override === null || Array.isArray(override)) {
5315
+ return override;
5316
+ }
5317
+ const out = { ...base };
5318
+ for (const [k, v] of Object.entries(override)) {
5319
+ if (k === "__proto__" || k === "constructor" || k === "prototype")
5320
+ continue;
5321
+ out[k] = deepMergeJson(out[k], v);
5322
+ }
5323
+ return out;
5324
+ }
5325
+ function resolveAgentConfig(defaults, profiles, agent) {
5326
+ if (!mergeAgentConfig.suppressDeprecationLogs && !mergeAgentConfig.notifiedWorkerIsolationMove && defaults?.subagents?.worker?.isolation === "worktree") {
5327
+ mergeAgentConfig.notifiedWorkerIsolationMove = true;
5328
+ console.warn("[switchroom] NOTICE: defaults.subagents.worker.isolation moved to the " + "`coding` profile in switchroom 0.6.6 (#682). Agents extending coding " + "still get worktree-isolated workers; other agents would have hard-failed " + "the first time they delegated. See CHANGELOG.");
5329
+ }
5330
+ const name = agent.extends;
5331
+ const profile = name && profiles ? profiles[name] : undefined;
5332
+ if (!profile) {
5333
+ return mergeAgentConfig(defaults, agent);
5334
+ }
5335
+ const { extends: _unused, ...profileWithoutExtends } = profile;
5336
+ const layered = mergeAgentConfig(defaults, profileWithoutExtends);
5337
+ return mergeAgentConfig(layered, agent);
5338
+ }
5339
+ function foldDeprecatedTelegramFields(config) {
5340
+ const c = config;
5341
+ const root = c;
5342
+ const deprecations = [];
5343
+ const hasRoot = root.voice_in !== undefined || root.telegraph !== undefined || root.webhook_sources !== undefined;
5344
+ if (!hasRoot)
5345
+ return { config: c, deprecations };
5346
+ const channels = { ...c.channels ?? {} };
5347
+ const tg = { ...channels.telegram ?? {} };
5348
+ if (root.voice_in !== undefined) {
5349
+ if (tg.voice_in === undefined)
5350
+ tg.voice_in = root.voice_in;
5351
+ deprecations.push("voice_in at the agent root is deprecated; move under channels.telegram.voice_in (#596).");
5352
+ }
5353
+ if (root.telegraph !== undefined) {
5354
+ if (tg.telegraph === undefined)
5355
+ tg.telegraph = root.telegraph;
5356
+ deprecations.push("telegraph at the agent root is deprecated; move under channels.telegram.telegraph (#596).");
5357
+ }
5358
+ if (root.webhook_sources !== undefined) {
5359
+ if (tg.webhook_sources === undefined)
5360
+ tg.webhook_sources = root.webhook_sources;
5361
+ deprecations.push("webhook_sources at the agent root is deprecated; move under channels.telegram.webhook_sources (#596).");
5362
+ }
5363
+ channels.telegram = tg;
5364
+ const { voice_in: _vi, telegraph: _tg, webhook_sources: _ws, ...rest } = root;
5365
+ return {
5366
+ config: { ...rest, channels },
5367
+ deprecations
5368
+ };
5369
+ }
5370
+ function mergeAgentConfig(defaultsIn, agentIn) {
5371
+ const { config: agent, deprecations: agentDeprecations } = foldDeprecatedTelegramFields(agentIn);
5372
+ const defaultsMigration = defaultsIn ? foldDeprecatedTelegramFields(defaultsIn) : null;
5373
+ const defaults = defaultsMigration?.config;
5374
+ const allDeprecations = [
5375
+ ...agentDeprecations,
5376
+ ...defaultsMigration?.deprecations ?? []
5377
+ ];
5378
+ if (allDeprecations.length > 0 && !mergeAgentConfig.suppressDeprecationLogs) {
5379
+ for (const msg of allDeprecations) {
5380
+ console.warn(`[switchroom] DEPRECATION: ${msg}`);
5381
+ }
5382
+ }
5383
+ if (!defaults)
5384
+ return agent;
5385
+ const merged = { ...agent };
5386
+ if (defaults.bot_token !== undefined && merged.bot_token === undefined) {
5387
+ merged.bot_token = defaults.bot_token;
5388
+ }
5389
+ if (defaults.timezone !== undefined && merged.timezone === undefined) {
5390
+ merged.timezone = defaults.timezone;
5391
+ }
5392
+ if (defaults.model !== undefined && merged.model === undefined) {
5393
+ merged.model = defaults.model;
5394
+ }
5395
+ if (defaults.dangerous_mode !== undefined && merged.dangerous_mode === undefined) {
5396
+ merged.dangerous_mode = defaults.dangerous_mode;
5397
+ }
5398
+ if (defaults.network_isolation !== undefined && merged.network_isolation === undefined) {
5066
5399
  merged.network_isolation = defaults.network_isolation;
5067
5400
  }
5068
5401
  if (defaults.thinking_effort !== undefined && merged.thinking_effort === undefined) {
@@ -18310,617 +18643,1070 @@ var require_lib = __commonJS((exports, module) => {
18310
18643
  }
18311
18644
  });
18312
18645
 
18313
- // src/vault/broker/server.ts
18314
- import * as net from "node:net";
18315
- import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
18316
-
18317
- // src/agents/compose.ts
18318
- init_schema();
18319
-
18320
- // src/agents/scaffold.ts
18321
- import { dirname as dirname2, isAbsolute, join as join4, relative, resolve as resolve6 } from "node:path";
18322
- init_atomic();
18323
-
18324
- // src/agents/agent-uid.ts
18325
- import { createHash } from "node:crypto";
18326
-
18327
- // src/vault/broker/peercred.ts
18328
- import { execFileSync } from "node:child_process";
18329
- import { readFileSync, readlinkSync, fstatSync } from "node:fs";
18330
-
18331
- // src/vault/broker/peercred-ffi.ts
18332
- function getPeerCred(fd) {
18333
- if (process.platform !== "linux")
18334
- return null;
18646
+ // src/vault/flock.ts
18647
+ import {
18648
+ existsSync as existsSync5,
18649
+ openSync as openSync2,
18650
+ closeSync as closeSync2,
18651
+ writeSync as writeSync2,
18652
+ fsyncSync as fsyncSync2,
18653
+ unlinkSync,
18654
+ readFileSync as readFileSync5,
18655
+ readdirSync as readdirSync3,
18656
+ rmdirSync,
18657
+ statSync as statSync3,
18658
+ constants as fsConstants
18659
+ } from "node:fs";
18660
+ function lockPathFor(vaultPath) {
18661
+ return `${vaultPath}.lock`;
18662
+ }
18663
+ function readLockHolder(lockPath) {
18335
18664
  try {
18336
- const ffi = __require("bun:ffi");
18337
- const { dlopen, FFIType, ptr } = ffi;
18338
- const SOL_SOCKET = 1;
18339
- const SO_PEERCRED = 17;
18340
- const UCRED_SIZE = 12;
18341
- const cache = getPeerCred;
18342
- const lib = cache._lib ?? (() => {
18343
- const candidates = ["libc.so.6", "libc.so"];
18344
- const symbolSpec = {
18345
- getsockopt: {
18346
- args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
18347
- returns: FFIType.i32
18348
- }
18349
- };
18350
- const errors2 = [];
18351
- for (const name of candidates) {
18352
- try {
18353
- const opened = dlopen(name, symbolSpec);
18354
- cache._lib = opened;
18355
- return opened;
18356
- } catch (e) {
18357
- errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
18358
- }
18359
- }
18360
- process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
18665
+ const raw = readFileSync5(lockPath, "utf8");
18666
+ const lines = raw.split(`
18361
18667
  `);
18362
- throw new Error("no libc candidate could be opened");
18363
- })();
18364
- const credBuf = new ArrayBuffer(UCRED_SIZE);
18365
- const lenBuf = new Uint32Array(1);
18366
- lenBuf[0] = UCRED_SIZE;
18367
- const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
18368
- if (rc !== 0)
18668
+ const pid = Number.parseInt(lines[0] ?? "", 10);
18669
+ const acquiredAtMs = Number.parseInt(lines[1] ?? "", 10);
18670
+ if (!Number.isFinite(pid) || pid <= 0)
18369
18671
  return null;
18370
- if (lenBuf[0] !== UCRED_SIZE)
18672
+ if (!Number.isFinite(acquiredAtMs) || acquiredAtMs <= 0)
18371
18673
  return null;
18372
- const view = new DataView(credBuf);
18373
- return {
18374
- pid: view.getInt32(0, true),
18375
- uid: view.getInt32(4, true),
18376
- gid: view.getInt32(8, true)
18377
- };
18674
+ return { pid, acquiredAtMs, argv0: lines[2] ?? "" };
18378
18675
  } catch {
18379
18676
  return null;
18380
18677
  }
18381
18678
  }
18382
-
18383
- // src/broker-common/peercred-path.ts
18384
- function matchSocketName(socketPath, patterns, maxNameLen) {
18385
- if (typeof socketPath !== "string" || socketPath.length === 0)
18386
- return null;
18387
- let name = null;
18388
- for (const re of patterns) {
18389
- const m = socketPath.match(re);
18390
- if (m && typeof m[1] === "string") {
18391
- name = m[1];
18392
- break;
18393
- }
18679
+ function pidIsLive(pid) {
18680
+ if (process.platform === "linux") {
18681
+ return existsSync5(`/proc/${pid}`);
18394
18682
  }
18395
- if (name === null || name.length === 0)
18683
+ try {
18684
+ process.kill(pid, 0);
18685
+ return true;
18686
+ } catch {
18687
+ return false;
18688
+ }
18689
+ }
18690
+ function parseProcStartTimeMs(statLine, procStat, now) {
18691
+ const tailStart = statLine.lastIndexOf(")");
18692
+ if (tailStart < 0)
18396
18693
  return null;
18397
- if (maxNameLen !== undefined && name.length > maxNameLen)
18694
+ const tokens = statLine.slice(tailStart + 1).trim().split(/\s+/);
18695
+ const starttimeTicks = Number.parseInt(tokens[19] ?? "", 10);
18696
+ if (!Number.isFinite(starttimeTicks))
18398
18697
  return null;
18399
- return name;
18400
- }
18401
- function parseSocketIdentity(socketPath, spec) {
18402
- const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
18403
- if (name === null)
18698
+ const btimeMatch = procStat.match(/^btime\s+(\d+)/m);
18699
+ if (!btimeMatch)
18404
18700
  return null;
18405
- const operatorName = spec.operatorName ?? "operator";
18406
- if (name === operatorName)
18407
- return { kind: "operator" };
18408
- if (spec.reservedNames.has(name))
18701
+ const bootEpochSec = Number.parseInt(btimeMatch[1], 10);
18702
+ if (!Number.isFinite(bootEpochSec))
18409
18703
  return null;
18410
- return { kind: "agent", name };
18411
- }
18412
-
18413
- // src/vault/broker/peercred.ts
18414
- var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
18415
- var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
18416
- var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
18417
- function socketPathToIdentity(socketPath) {
18418
- return parseSocketIdentity(socketPath, {
18419
- patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
18420
- reservedNames: RESERVED_AGENT_NAMES
18421
- });
18422
- }
18423
- function socketPathToAgent(socketPath) {
18424
- const identity = socketPathToIdentity(socketPath);
18425
- return identity?.kind === "agent" ? identity.name : null;
18426
- }
18427
- function isReservedAgentName(name) {
18428
- return RESERVED_AGENT_NAMES.has(name);
18429
- }
18430
- function unlockSocketFor(dataSocketPath) {
18431
- if (dataSocketPath.endsWith("/sock")) {
18432
- return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
18433
- }
18434
- return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
18435
- }
18436
- function parseSsRows(output) {
18437
- const rows = [];
18438
- const lines = output.split(`
18439
- `);
18440
- for (const line of lines) {
18441
- if (!line.trim() || line.startsWith("Netid"))
18442
- continue;
18443
- const tokens = line.split(/\s+/).filter((t) => t.length > 0);
18444
- if (tokens.length < 8)
18445
- continue;
18446
- const localAddr = tokens[4];
18447
- const localInode = tokens[5];
18448
- const peerAddr = tokens[6];
18449
- const peerInode = tokens[7];
18450
- const usersToken = tokens.slice(8).join(" ");
18451
- const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
18452
- const pid = m ? parseInt(m[1], 10) : null;
18453
- rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
18454
- }
18455
- return rows;
18456
- }
18457
- function findClientPids(rows, socketPath) {
18458
- const pids = [];
18459
- for (const serverRow of rows) {
18460
- if (serverRow.localAddr !== socketPath)
18461
- continue;
18462
- for (const clientRow of rows) {
18463
- if (clientRow.localAddr !== "*")
18464
- continue;
18465
- if (clientRow.localInode !== serverRow.peerInode)
18466
- continue;
18467
- if (clientRow.pid === null)
18468
- continue;
18469
- pids.push(clientRow.pid);
18470
- break;
18471
- }
18472
- }
18473
- return pids;
18474
- }
18475
- function findClientPidByServerInode(rows, socketPath, serverInode) {
18476
- const serverInodeStr = String(serverInode);
18477
- for (const serverRow of rows) {
18478
- if (serverRow.localAddr !== socketPath)
18479
- continue;
18480
- if (serverRow.localInode !== serverInodeStr)
18481
- continue;
18482
- for (const clientRow of rows) {
18483
- if (clientRow.localAddr !== "*")
18484
- continue;
18485
- if (clientRow.localInode !== serverRow.peerInode)
18486
- continue;
18487
- if (clientRow.pid === null)
18488
- continue;
18489
- return clientRow.pid;
18490
- }
18704
+ const USER_HZ = 100;
18705
+ const startEpochMs = bootEpochSec * 1000 + starttimeTicks / USER_HZ * 1000;
18706
+ const bootEpochMs = bootEpochSec * 1000;
18707
+ const SLOP_MS = 5000;
18708
+ if (startEpochMs < bootEpochMs - SLOP_MS || startEpochMs > now + SLOP_MS) {
18491
18709
  return null;
18492
18710
  }
18493
- return null;
18711
+ return Math.floor(startEpochMs);
18494
18712
  }
18495
- function readUid(pid) {
18713
+ function pidStartTimeMs(pid) {
18714
+ if (process.platform !== "linux")
18715
+ return null;
18496
18716
  try {
18497
- const status = readFileSync(`/proc/${pid}/status`, "utf8");
18498
- const m = status.match(/^Uid:\s+(\d+)/m);
18499
- if (!m)
18500
- return null;
18501
- return parseInt(m[1], 10);
18717
+ const statLine = readFileSync5(`/proc/${pid}/stat`, "utf8");
18718
+ const procStat = readFileSync5("/proc/stat", "utf8");
18719
+ return parseProcStartTimeMs(statLine, procStat, Date.now());
18502
18720
  } catch {
18503
18721
  return null;
18504
18722
  }
18505
18723
  }
18506
- function readExe(pid) {
18724
+ function pidIsOriginalHolder(holder) {
18725
+ const startMs = pidStartTimeMs(holder.pid);
18726
+ if (startMs === null)
18727
+ return true;
18728
+ return startMs <= holder.acquiredAtMs + 100;
18729
+ }
18730
+ function lockFileMtimeIsOlderThan(lockPath, budgetMs) {
18507
18731
  try {
18508
- return readlinkSync(`/proc/${pid}/exe`);
18732
+ const s = statSync3(lockPath);
18733
+ const threshold = Math.max(STALE_MTIME_FLOOR_MS, budgetMs * 2);
18734
+ return Date.now() - s.mtimeMs > threshold;
18509
18735
  } catch {
18510
- return null;
18736
+ return false;
18511
18737
  }
18512
18738
  }
18513
- function readSystemdUnit(pid) {
18739
+ function clearStaleSentinelDir(lockPath) {
18514
18740
  try {
18515
- const content = readFileSync(`/proc/${pid}/cgroup`, "utf8");
18516
- const lines = content.split(`
18517
- `);
18518
- for (const line of lines) {
18519
- if (!line.trim())
18520
- continue;
18521
- const parts = line.split(":");
18522
- if (parts.length < 3)
18523
- continue;
18524
- const controller = parts[1];
18525
- const isV2 = parts[0] === "0" && controller === "";
18526
- const isV1Systemd = controller === "name=systemd";
18527
- if (!isV2 && !isV1Systemd)
18528
- continue;
18529
- const cgroupPath = parts.slice(2).join(":");
18530
- const segments = cgroupPath.split("/");
18531
- const lastSegment = segments[segments.length - 1];
18532
- if (!lastSegment)
18533
- continue;
18534
- if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
18535
- return lastSegment;
18536
- }
18741
+ if (!existsSync5(lockPath))
18742
+ return true;
18743
+ const s = statSync3(lockPath);
18744
+ if (!s.isDirectory())
18745
+ return true;
18746
+ const now = Date.now();
18747
+ for (const entry of readdirSync3(lockPath)) {
18748
+ try {
18749
+ const childStat = statSync3(`${lockPath}/${entry}`);
18750
+ if (now - childStat.mtimeMs < SENTINEL_DIR_RECENT_WRITE_MS) {
18751
+ return false;
18752
+ }
18753
+ } catch {}
18537
18754
  }
18538
- return null;
18539
- } catch {
18540
- return null;
18541
- }
18542
- }
18543
- function verifySystemdUnit(unitName, runner) {
18544
- let raw;
18545
- try {
18546
- const out = runner("systemctl", [
18547
- "--user",
18548
- "show",
18549
- unitName,
18550
- "--property=LoadState,ActiveState"
18551
- ], { timeout: 500, encoding: "utf8" });
18552
- raw = typeof out === "string" ? out : out.toString("utf8");
18755
+ for (const entry of readdirSync3(lockPath)) {
18756
+ try {
18757
+ unlinkSync(`${lockPath}/${entry}`);
18758
+ } catch {}
18759
+ }
18760
+ rmdirSync(lockPath);
18761
+ return true;
18553
18762
  } catch {
18554
18763
  return false;
18555
18764
  }
18556
- const props = {};
18557
- for (const line of raw.split(`
18558
- `)) {
18559
- const m = line.match(/^([A-Za-z]+)=(.*)$/);
18560
- if (m)
18561
- props[m[1]] = m[2];
18562
- }
18563
- if (props.LoadState !== "loaded")
18564
- return false;
18565
- if (props.ActiveState !== "active" && props.ActiveState !== "activating") {
18566
- return false;
18567
- }
18568
- return true;
18569
- }
18570
- function readFdInode(fd) {
18571
- try {
18572
- const stat = fstatSync(fd);
18573
- return stat.ino;
18574
- } catch {
18575
- return null;
18576
- }
18577
- }
18578
- function fdFromSocket(socket) {
18579
- const handle = socket._handle;
18580
- if (!handle || typeof handle.fd !== "number" || handle.fd < 0)
18581
- return null;
18582
- return handle.fd;
18583
18765
  }
18584
- function identify(socketPath, socket, execFileSyncOverride) {
18585
- if (process.platform !== "linux") {
18586
- return null;
18587
- }
18588
- const runner = execFileSyncOverride ?? execFileSync;
18589
- let pid = null;
18590
- if (socket !== undefined) {
18591
- const fd = fdFromSocket(socket);
18592
- if (fd !== null) {
18593
- const cred = getPeerCred(fd);
18594
- if (cred !== null)
18595
- pid = cred.pid;
18766
+ function acquireLock(vaultPath, options = {}) {
18767
+ const budgetMs = options.budgetMs ?? DEFAULT_LOCK_RETRY_MS;
18768
+ const lockPath = lockPathFor(vaultPath);
18769
+ const deadline = Date.now() + budgetMs;
18770
+ const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
18771
+ while (true) {
18772
+ let fd = null;
18773
+ try {
18774
+ fd = openSync2(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
18775
+ } catch (err) {
18776
+ const code = err?.code ?? "";
18777
+ if (code === "EEXIST") {
18778
+ let isDir = false;
18779
+ try {
18780
+ isDir = statSync3(lockPath).isDirectory();
18781
+ } catch {}
18782
+ if (isDir) {
18783
+ if (clearStaleSentinelDir(lockPath))
18784
+ continue;
18785
+ }
18786
+ const holder = readLockHolder(lockPath);
18787
+ const isDeadPid = holder !== null && !pidIsLive(holder.pid);
18788
+ const isReusedPid = holder !== null && pidIsLive(holder.pid) && !pidIsOriginalHolder(holder);
18789
+ const isUnparseableAndOld = holder === null && lockFileMtimeIsOlderThan(lockPath, budgetMs);
18790
+ if (isDeadPid || isReusedPid || isUnparseableAndOld) {
18791
+ try {
18792
+ unlinkSync(lockPath);
18793
+ } catch {}
18794
+ continue;
18795
+ }
18796
+ if (Date.now() >= deadline) {
18797
+ throw makeBusyError(vaultPath, lockPath, holder, budgetMs);
18798
+ }
18799
+ Atomics.wait(sleepBuf, 0, 0, 100);
18800
+ continue;
18801
+ } else {
18802
+ throw err;
18803
+ }
18596
18804
  }
18597
- }
18598
- if (pid === null) {
18599
- let ssOutput;
18600
18805
  try {
18601
- const raw = runner("ss", ["-xpn"], {
18602
- timeout: 200,
18603
- encoding: "utf8"
18604
- });
18605
- ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
18806
+ const meta = `${process.pid}
18807
+ ${Date.now()}
18808
+ ${process.argv[1] ?? ""}
18809
+ `;
18810
+ writeSync2(fd, meta);
18811
+ fsyncSync2(fd);
18606
18812
  } catch {
18607
- return null;
18608
- }
18609
- const rows = parseSsRows(ssOutput);
18610
- let serverInode = null;
18611
- if (socket !== undefined) {
18612
- const fd = fdFromSocket(socket);
18613
- if (fd !== null)
18614
- serverInode = readFdInode(fd);
18813
+ try {
18814
+ closeSync2(fd);
18815
+ } catch {}
18816
+ try {
18817
+ unlinkSync(lockPath);
18818
+ } catch {}
18819
+ throw new Error(`vault flock: failed to write holder metadata to ${lockPath}`);
18615
18820
  }
18616
- if (serverInode !== null) {
18617
- pid = findClientPidByServerInode(rows, socketPath, serverInode);
18618
- } else {
18619
- const clientPids = findClientPids(rows, socketPath);
18620
- if (clientPids.length === 0)
18621
- return null;
18622
- if (clientPids.length > 1) {
18623
- process.stderr.write(`[vault-broker] peercred: ${clientPids.length} connected peers found for ${socketPath}; ` + `using pid=${clientPids[0]}. ` + `Multiple simultaneous connections reduce identification accuracy. ` + `(This warning means identify() was called without a socket arg — likely a stale call site.)
18624
- `);
18821
+ const ownedFd = fd;
18822
+ return {
18823
+ release: () => {
18824
+ try {
18825
+ closeSync2(ownedFd);
18826
+ } catch {}
18827
+ try {
18828
+ unlinkSync(lockPath);
18829
+ } catch {}
18625
18830
  }
18626
- pid = clientPids[0];
18627
- }
18628
- }
18629
- if (pid === null)
18630
- return null;
18631
- const uid = readUid(pid);
18632
- if (uid === null) {
18633
- return null;
18634
- }
18635
- const brokerUid = typeof process.getuid === "function" ? process.getuid() : null;
18636
- if (brokerUid !== null && uid !== brokerUid) {
18637
- process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
18638
- `);
18639
- return null;
18640
- }
18641
- const exe = readExe(pid);
18642
- if (exe === null) {
18643
- return null;
18644
- }
18645
- const cgroupClaim = readSystemdUnit(pid);
18646
- let systemdUnit = null;
18647
- if (cgroupClaim !== null) {
18648
- if (verifySystemdUnit(cgroupClaim, runner)) {
18649
- systemdUnit = cgroupClaim;
18650
- } else {
18651
- process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
18652
- `);
18653
- }
18831
+ };
18654
18832
  }
18655
- return { uid, pid, exe, systemdUnit };
18656
18833
  }
18657
-
18658
- // src/agents/agent-uid.ts
18659
- var AGENT_UID_MIN = 10001;
18660
- var AGENT_UID_MAX = 10999;
18661
- function allocateAgentUid(name) {
18662
- if (isReservedAgentName(name)) {
18663
- throw new Error(`agent name '${name}' is reserved by switchroom for another identity kind ` + `(see vault/broker/peercred.ts:RESERVED_AGENT_NAMES). Pick a different name.`);
18664
- }
18665
- const hash = createHash("sha256").update(name).digest();
18666
- const u32 = hash.readUInt32BE(0);
18667
- const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
18668
- return AGENT_UID_MIN + u32 % range;
18834
+ function makeBusyError(vaultPath, lockPath, holder, budgetMs) {
18835
+ const holderPid = holder?.pid ?? null;
18836
+ const heldForMs = holder ? Date.now() - holder.acquiredAtMs : null;
18837
+ const holderClause = holder ? `held by pid ${holder.pid} (acquired ${Math.round((heldForMs ?? 0) / 1000)}s ago)` : "held by another writer (holder PID unreadable — lock file empty or unparseable)";
18838
+ return new VaultBusyError(`vault busy: ${holderClause} at ${lockPath} (retried for ${budgetMs}ms). ` + `Try again in a moment. If the holder process is gone, the next acquirer ` + `will clear the stale lock automatically.`, { vaultPath, lockPath, holderPid, heldForMs, budgetMs });
18669
18839
  }
18840
+ var DEFAULT_LOCK_RETRY_MS = 5000, STALE_MTIME_FLOOR_MS = 1e4, SENTINEL_DIR_RECENT_WRITE_MS = 60000, VaultBusyError;
18841
+ var init_flock = __esm(() => {
18842
+ VaultBusyError = class VaultBusyError extends Error {
18843
+ vaultPath;
18844
+ lockPath;
18845
+ holderPid;
18846
+ heldForMs;
18847
+ budgetMs;
18848
+ constructor(message, fields) {
18849
+ super(message);
18850
+ this.name = "VaultBusyError";
18851
+ this.vaultPath = fields.vaultPath;
18852
+ this.lockPath = fields.lockPath;
18853
+ this.holderPid = fields.holderPid;
18854
+ this.heldForMs = fields.heldForMs;
18855
+ this.budgetMs = fields.budgetMs;
18856
+ }
18857
+ };
18858
+ });
18670
18859
 
18671
- // src/setup/hindsight-recall-tunables.ts
18672
- var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
18673
- var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
18674
-
18675
- // src/agents/scaffold.ts
18676
- init_schema();
18677
-
18678
- // src/config/users.ts
18679
- init_merge();
18680
-
18681
- // src/agents/scaffold.ts
18682
- init_merge();
18683
- init_timezone();
18684
-
18685
- // src/cli/agent-config.ts
18686
- import { join } from "node:path";
18687
- import { homedir as homedir2 } from "node:os";
18688
-
18689
- // src/cli/helpers.ts
18690
- init_loader();
18691
-
18692
- // src/cli/agent-config.ts
18693
- init_loader();
18694
- init_merge();
18695
-
18696
- // src/config/google-workspace-acl.ts
18697
- function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
18698
- if (!agentGoogleAccount)
18699
- return false;
18700
- const account = agentGoogleAccount.trim().toLowerCase();
18701
- if (account.length === 0)
18702
- return false;
18703
- const acctEntry = googleAccounts?.[account];
18704
- if (!acctEntry)
18705
- return false;
18706
- const enabledFor = acctEntry.enabled_for ?? [];
18707
- return enabledFor.includes(agentName);
18708
- }
18709
- function vaultRefKey(value) {
18710
- if (typeof value !== "string" || !value.startsWith("vault:"))
18711
- return null;
18712
- const key = value.slice("vault:".length).split("#")[0];
18713
- return key.length > 0 ? key : null;
18714
- }
18715
- function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
18716
- if (!agentName || !key)
18717
- return false;
18718
- const agentConfig = config.agents?.[agentName];
18719
- if (!agentConfig)
18720
- return false;
18721
- if (agentConfig.mcp_servers?.["gdrive"] === false) {
18722
- return false;
18723
- }
18724
- const account = agentConfig.google_workspace?.account;
18725
- if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
18726
- return false;
18727
- }
18728
- const gw = config.google_workspace;
18729
- if (!gw)
18730
- return false;
18731
- for (const ref of [gw.google_client_id, gw.google_client_secret]) {
18732
- if (vaultRefKey(ref) === key)
18733
- return true;
18860
+ // src/vault/vault.ts
18861
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
18862
+ import {
18863
+ readFileSync as readFileSync6,
18864
+ writeSync as writeSync3,
18865
+ existsSync as existsSync6,
18866
+ renameSync as renameSync2,
18867
+ mkdirSync as mkdirSync2,
18868
+ unlinkSync as unlinkSync2,
18869
+ fsyncSync as fsyncSync3,
18870
+ openSync as openSync3,
18871
+ closeSync as closeSync3,
18872
+ lstatSync,
18873
+ realpathSync as realpathSync2
18874
+ } from "node:fs";
18875
+ import { dirname, basename as basename2, resolve as resolve5 } from "node:path";
18876
+ function atomicWriteFileSync(path, data, mode) {
18877
+ let effectivePath = path;
18878
+ try {
18879
+ if (existsSync6(path) && lstatSync(path).isSymbolicLink()) {
18880
+ effectivePath = realpathSync2(path);
18881
+ }
18882
+ } catch {}
18883
+ const dir = dirname(resolve5(effectivePath));
18884
+ const tmp = resolve5(dir, `.${basename2(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
18885
+ try {
18886
+ const fd = openSync3(tmp, "wx", mode);
18887
+ try {
18888
+ writeSync3(fd, data);
18889
+ fsyncSync3(fd);
18890
+ } finally {
18891
+ closeSync3(fd);
18892
+ }
18893
+ renameSync2(tmp, effectivePath);
18894
+ try {
18895
+ const dirFd = openSync3(dir, "r");
18896
+ try {
18897
+ fsyncSync3(dirFd);
18898
+ } finally {
18899
+ closeSync3(dirFd);
18900
+ }
18901
+ } catch {}
18902
+ } catch (err) {
18903
+ try {
18904
+ if (existsSync6(tmp))
18905
+ unlinkSync2(tmp);
18906
+ } catch {}
18907
+ throw err;
18734
18908
  }
18735
- return false;
18736
18909
  }
18737
-
18738
- // src/vault/broker/acl.ts
18739
- var WEBKITE_VAULT_KEYS = new Set([
18740
- "webkite/cloudflare-account-id",
18741
- "webkite/cloudflare-api-token",
18742
- "webkite/firecrawl-api-key"
18743
- ]);
18744
- function isWebkiteCredentialKeyForAgent(agentConfig, key) {
18745
- if (!WEBKITE_VAULT_KEYS.has(key))
18746
- return false;
18747
- if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
18748
- return false;
18749
- return true;
18910
+ function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
18911
+ return scryptSync(passphrase, salt, 32, {
18912
+ N: params.N,
18913
+ r: params.r,
18914
+ p: params.p,
18915
+ maxmem: SCRYPT_MAXMEM
18916
+ });
18750
18917
  }
18751
- function checkEntryScope(scope, agentSlug) {
18752
- if (scope === undefined || scope === null) {
18753
- return { allow: true };
18754
- }
18755
- const deny = scope.deny ?? [];
18756
- const allow = scope.allow ?? [];
18757
- if (agentSlug !== null && deny.includes(agentSlug)) {
18758
- return {
18759
- allow: false,
18760
- reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
18761
- };
18918
+ function encrypt(key, plaintext) {
18919
+ const iv = randomBytes(12);
18920
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
18921
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
18922
+ const tag = cipher.getAuthTag();
18923
+ return {
18924
+ iv: iv.toString("hex"),
18925
+ data: encrypted.toString("hex"),
18926
+ tag: tag.toString("hex")
18927
+ };
18928
+ }
18929
+ function decrypt(key, iv, data, tag) {
18930
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
18931
+ decipher.setAuthTag(Buffer.from(tag, "hex"));
18932
+ const decrypted = Buffer.concat([
18933
+ decipher.update(Buffer.from(data, "hex")),
18934
+ decipher.final()
18935
+ ]);
18936
+ return decrypted.toString("utf8");
18937
+ }
18938
+ function normalizeEntry(raw) {
18939
+ if (typeof raw === "string") {
18940
+ return { kind: "string", value: raw };
18762
18941
  }
18763
- if (allow.length > 0) {
18764
- if (agentSlug === null || !allow.includes(agentSlug)) {
18765
- return {
18766
- allow: false,
18767
- reason: agentSlug === null ? "caller agent slug could not be determined; entry has a non-empty allow list (scope-allow)" : `agent '${agentSlug}' is not in the entry's allow list (scope-allow)`
18768
- };
18769
- }
18942
+ return raw;
18943
+ }
18944
+ function normalizeSecrets(raw) {
18945
+ const out = {};
18946
+ for (const [k, v] of Object.entries(raw)) {
18947
+ out[k] = normalizeEntry(v);
18770
18948
  }
18771
- return { allow: true };
18949
+ return out;
18772
18950
  }
18773
- function checkAclByAgent(config, agentName, key) {
18774
- if (!agentName) {
18775
- return { allow: false, reason: "agent name unresolved" };
18951
+ function openVault(passphrase, vaultPath) {
18952
+ if (!existsSync6(vaultPath)) {
18953
+ throw new VaultError(`Vault file not found: ${vaultPath}`);
18776
18954
  }
18777
- const agentConfig = config.agents?.[agentName];
18778
- if (!agentConfig) {
18779
- return { allow: false, reason: `agent '${agentName}' not found in config` };
18955
+ let vaultFile;
18956
+ try {
18957
+ vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
18958
+ } catch {
18959
+ throw new VaultError(`Failed to read vault file: ${vaultPath}`);
18780
18960
  }
18781
- const googleSlot = parseGoogleAccountSlotKey(key);
18782
- if (googleSlot !== null) {
18783
- return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
18961
+ const salt = Buffer.from(vaultFile.salt, "hex");
18962
+ const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
18963
+ const key = deriveKey(passphrase, salt, kdfParams);
18964
+ let plaintext;
18965
+ try {
18966
+ plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
18967
+ } catch {
18968
+ throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
18784
18969
  }
18785
- if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
18786
- return { allow: true };
18970
+ let vaultData;
18971
+ try {
18972
+ vaultData = JSON.parse(plaintext);
18973
+ } catch {
18974
+ throw new VaultError("Vault data is corrupted");
18787
18975
  }
18788
- if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
18789
- return { allow: true };
18976
+ return normalizeSecrets(vaultData.secrets ?? {});
18977
+ }
18978
+ function saveVault(passphrase, vaultPath, secrets) {
18979
+ if (!existsSync6(vaultPath)) {
18980
+ throw new VaultError(`Vault file not found: ${vaultPath}`);
18790
18981
  }
18791
- const agentBot = agentConfig.bot_token;
18792
- const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
18793
- if (typeof botRef === "string" && botRef.startsWith("vault:")) {
18794
- const botKey = botRef.slice("vault:".length).split("#")[0];
18795
- if (botKey.length > 0 && botKey === key) {
18796
- return { allow: true };
18982
+ let releaseLock = null;
18983
+ try {
18984
+ releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
18985
+ } catch (err) {
18986
+ if (err instanceof VaultBusyError) {
18987
+ throw new VaultError(err.message, { cause: err });
18797
18988
  }
18989
+ throw err;
18798
18990
  }
18799
- const cfgWithProfiles = config;
18800
- const profileName = agentConfig.extends;
18801
- const profileMcp = profileName != null && profileName.length > 0 ? cfgWithProfiles.profiles?.[profileName]?.mcp_servers ?? {} : {};
18802
- const effectiveMcp = {
18803
- ...cfgWithProfiles.defaults?.mcp_servers ?? {},
18804
- ...profileMcp,
18805
- ...agentConfig.mcp_servers ?? {}
18806
- };
18807
- for (const mcpEntry of Object.values(effectiveMcp)) {
18808
- if (!mcpEntry || typeof mcpEntry !== "object")
18809
- continue;
18810
- const declared = mcpEntry.secrets;
18811
- if (Array.isArray(declared) && declared.includes(key)) {
18812
- return { allow: true };
18991
+ try {
18992
+ let vaultFile;
18993
+ try {
18994
+ vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
18995
+ } catch {
18996
+ throw new VaultError(`Failed to read vault file: ${vaultPath}`);
18997
+ }
18998
+ const salt = Buffer.from(vaultFile.salt, "hex");
18999
+ const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
19000
+ vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
19001
+ const vaultData = { secrets };
19002
+ const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
19003
+ vaultFile.iv = iv;
19004
+ vaultFile.data = data;
19005
+ vaultFile.tag = tag;
19006
+ atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
19007
+ } finally {
19008
+ if (releaseLock !== null) {
19009
+ try {
19010
+ releaseLock();
19011
+ } catch {}
18813
19012
  }
18814
19013
  }
18815
- const cfgSecrets = config;
18816
- const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
18817
- const standingSecrets = [
18818
- ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
18819
- ...Array.isArray(profileSecrets) ? profileSecrets : [],
18820
- ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
18821
- ];
18822
- if (standingSecrets.includes(key)) {
18823
- return { allow: true };
18824
- }
18825
- const schedule = agentConfig.schedule ?? [];
18826
- if (schedule.length === 0) {
18827
- return {
18828
- allow: false,
18829
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
18830
- };
18831
- }
18832
- for (const entry of schedule) {
18833
- const allowed = entry?.secrets ?? [];
18834
- if (allowed.includes(key)) {
18835
- return { allow: true };
19014
+ }
19015
+ function acquireVaultLock(vaultPath) {
19016
+ try {
19017
+ return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
19018
+ } catch (err) {
19019
+ if (err instanceof VaultBusyError) {
19020
+ throw new VaultError(err.message, { cause: err });
18836
19021
  }
19022
+ throw err;
18837
19023
  }
18838
- return {
18839
- allow: false,
18840
- reason: `key '${key}' not in ACL for agent '${agentName}'`
18841
- };
18842
19024
  }
18843
- function parseGoogleAccountSlotKey(key) {
18844
- const match = key.match(/^google:([^:]+):([a-z_]+)$/);
18845
- if (!match)
18846
- return null;
18847
- return { account: match[1], field: match[2] };
19025
+ var KNOWN_VAULT_ARTIFACT_NAMES, SAVE_VAULT_LOCK_RETRY_MS, SCRYPT_N = 32768, SCRYPT_R = 8, SCRYPT_P = 1, SCRYPT_MAXMEM, VaultError, LEGACY_SCRYPT_N = 16384;
19026
+ var init_vault = __esm(() => {
19027
+ init_flock();
19028
+ KNOWN_VAULT_ARTIFACT_NAMES = new Set([
19029
+ "vault.enc",
19030
+ "vault.enc.bak",
19031
+ "vault.enc.tmp",
19032
+ "vault.enc.lock",
19033
+ ".vault.enc.symlink-tmp"
19034
+ ]);
19035
+ SAVE_VAULT_LOCK_RETRY_MS = DEFAULT_LOCK_RETRY_MS;
19036
+ SCRYPT_MAXMEM = 128 * 1024 * 1024;
19037
+ VaultError = class VaultError extends Error {
19038
+ cause;
19039
+ constructor(message, options) {
19040
+ super(message);
19041
+ this.name = "VaultError";
19042
+ if (options?.cause !== undefined)
19043
+ this.cause = options.cause;
19044
+ }
19045
+ };
19046
+ });
19047
+
19048
+ // src/vault/broker/protocol.ts
19049
+ function decodeRequest(line) {
19050
+ if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
19051
+ throw new RangeError(`Request frame too large (${Buffer.byteLength(line, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
19052
+ }
19053
+ const obj = JSON.parse(line);
19054
+ return RequestSchema.parse(obj);
18848
19055
  }
18849
- function checkGoogleAccountAcl(config, agentName, account, key) {
18850
- const accounts = config.google_accounts ?? {};
18851
- const accountKey = account.toLowerCase();
18852
- const accountEntry = accounts[accountKey] ?? accounts[account];
18853
- if (!accountEntry) {
18854
- return {
18855
- allow: false,
18856
- reason: `google_accounts['${account}'] not configured (key '${key}')`
18857
- };
19056
+ function encodeResponse(resp) {
19057
+ const json = JSON.stringify(resp);
19058
+ if (Buffer.byteLength(json, "utf8") > MAX_FRAME_BYTES) {
19059
+ throw new Error(`Response frame too large (${Buffer.byteLength(json, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
18858
19060
  }
18859
- const enabled = accountEntry.enabled_for ?? [];
18860
- if (enabled.length === 0) {
19061
+ return json + `
19062
+ `;
19063
+ }
19064
+ function errorResponse(code, msg) {
19065
+ return { ok: false, code, msg };
19066
+ }
19067
+ function entryResponse(entry) {
19068
+ const stripped = stripWireFields(entry);
19069
+ return { ok: true, entry: stripped };
19070
+ }
19071
+ function stripWireFields(entry) {
19072
+ if (entry.kind === "string" || entry.kind === "binary") {
18861
19073
  return {
18862
- allow: false,
18863
- reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
19074
+ kind: entry.kind,
19075
+ value: entry.value,
19076
+ ...entry.format !== undefined ? { format: entry.format } : {}
18864
19077
  };
18865
19078
  }
18866
- if (!enabled.includes(agentName)) {
18867
- return {
18868
- allow: false,
18869
- reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
18870
- };
19079
+ return {
19080
+ kind: "files",
19081
+ files: entry.files
19082
+ };
19083
+ }
19084
+ var MAX_FRAME_BYTES, AgentNameSchema, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, PreflightAccessRequestSchema, OkPreflightAccessResponseSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalLookupByRequestRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, ApprovalConsumeRecordRequestSchema, RequestSchema, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, OkApprovalConsumeRecordResponseSchema, ErrorResponseSchema, ResponseSchema;
19085
+ var init_protocol = __esm(() => {
19086
+ init_zod();
19087
+ init_peercred();
19088
+ MAX_FRAME_BYTES = 64 * 1024;
19089
+ AgentNameSchema = exports_external.string().min(1).max(64, "agent name max 64 chars").regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII (alnum + _- only, first char alnum)").refine((s) => !isReservedAgentName(s), {
19090
+ message: "agent name is reserved"
19091
+ });
19092
+ GetRequestSchema = exports_external.object({
19093
+ v: exports_external.literal(1),
19094
+ op: exports_external.literal("get"),
19095
+ key: exports_external.string().min(1),
19096
+ filename: exports_external.string().optional(),
19097
+ token: exports_external.string().optional()
19098
+ });
19099
+ PutRequestSchema = exports_external.object({
19100
+ v: exports_external.literal(1),
19101
+ op: exports_external.literal("put"),
19102
+ key: exports_external.string().min(1),
19103
+ entry: exports_external.union([
19104
+ exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
19105
+ exports_external.object({ kind: exports_external.literal("binary"), value: exports_external.string() })
19106
+ ]),
19107
+ token: exports_external.string().optional(),
19108
+ passphrase: exports_external.string().optional(),
19109
+ attest_via_posture: exports_external.boolean().optional()
19110
+ });
19111
+ ListRequestSchema = exports_external.object({
19112
+ v: exports_external.literal(1),
19113
+ op: exports_external.literal("list"),
19114
+ token: exports_external.string().optional()
19115
+ });
19116
+ MintGrantRequestSchema = exports_external.object({
19117
+ v: exports_external.literal(1),
19118
+ op: exports_external.literal("mint_grant"),
19119
+ agent: AgentNameSchema,
19120
+ keys: exports_external.array(exports_external.string().min(1)),
19121
+ ttl_seconds: exports_external.number().int().positive().nullable(),
19122
+ description: exports_external.string().optional(),
19123
+ write_keys: exports_external.array(exports_external.string().min(1)).optional(),
19124
+ passphrase: exports_external.string().optional(),
19125
+ attest_via_posture: exports_external.boolean().optional(),
19126
+ decision_id: exports_external.string().optional()
19127
+ });
19128
+ ListGrantsRequestSchema = exports_external.object({
19129
+ v: exports_external.literal(1),
19130
+ op: exports_external.literal("list_grants"),
19131
+ agent: AgentNameSchema.optional(),
19132
+ passphrase: exports_external.string().optional(),
19133
+ attest_via_posture: exports_external.boolean().optional()
19134
+ });
19135
+ RevokeGrantRequestSchema = exports_external.object({
19136
+ v: exports_external.literal(1),
19137
+ op: exports_external.literal("revoke_grant"),
19138
+ id: exports_external.string().min(1)
19139
+ });
19140
+ StatusRequestSchema = exports_external.object({
19141
+ v: exports_external.literal(1),
19142
+ op: exports_external.literal("status")
19143
+ });
19144
+ LockRequestSchema = exports_external.object({
19145
+ v: exports_external.literal(1),
19146
+ op: exports_external.literal("lock")
19147
+ });
19148
+ PreflightAccessRequestSchema = exports_external.object({
19149
+ v: exports_external.literal(1),
19150
+ op: exports_external.literal("preflight_access"),
19151
+ agent: AgentNameSchema,
19152
+ keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
19153
+ });
19154
+ OkPreflightAccessResponseSchema = exports_external.object({
19155
+ ok: exports_external.literal(true),
19156
+ op: exports_external.literal("preflight_access"),
19157
+ results: exports_external.array(exports_external.object({
19158
+ key: exports_external.string(),
19159
+ exists: exports_external.boolean(),
19160
+ acl_ok: exports_external.boolean(),
19161
+ acl_reason: exports_external.string().optional(),
19162
+ scope_ok: exports_external.boolean(),
19163
+ scope_reason: exports_external.string().optional()
19164
+ }))
19165
+ });
19166
+ ApprovalRequestRequestSchema = exports_external.object({
19167
+ v: exports_external.literal(1),
19168
+ op: exports_external.literal("approval_request"),
19169
+ agent_unit: exports_external.string().min(1),
19170
+ scope: exports_external.string().min(1),
19171
+ action: exports_external.string().min(1),
19172
+ approver_set: exports_external.array(exports_external.string()),
19173
+ why: exports_external.string().optional(),
19174
+ ttl_ms: exports_external.number().int().positive().optional()
19175
+ });
19176
+ ApprovalLookupRequestSchema = exports_external.object({
19177
+ v: exports_external.literal(1),
19178
+ op: exports_external.literal("approval_lookup"),
19179
+ agent_unit: exports_external.string().min(1),
19180
+ scope: exports_external.string().min(1),
19181
+ action: exports_external.string().min(1),
19182
+ current_approver_set: exports_external.array(exports_external.string())
19183
+ });
19184
+ ApprovalLookupByRequestRequestSchema = exports_external.object({
19185
+ v: exports_external.literal(1),
19186
+ op: exports_external.literal("approval_lookup_by_request"),
19187
+ agent_unit: exports_external.string().min(1),
19188
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
19189
+ current_approver_set: exports_external.array(exports_external.string())
19190
+ });
19191
+ ApprovalConsumeRequestSchema = exports_external.object({
19192
+ v: exports_external.literal(1),
19193
+ op: exports_external.literal("approval_consume"),
19194
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/)
19195
+ });
19196
+ ApprovalRevokeRequestSchema = exports_external.object({
19197
+ v: exports_external.literal(1),
19198
+ op: exports_external.literal("approval_revoke"),
19199
+ decision_id: exports_external.string().min(1),
19200
+ actor: exports_external.string().min(1),
19201
+ reason: exports_external.string().optional()
19202
+ });
19203
+ ApprovalListRequestSchema = exports_external.object({
19204
+ v: exports_external.literal(1),
19205
+ op: exports_external.literal("approval_list"),
19206
+ agent_unit: exports_external.string().optional()
19207
+ });
19208
+ ApprovalDecisionModeSchema = exports_external.enum([
19209
+ "allow_once",
19210
+ "allow_always",
19211
+ "allow_ttl",
19212
+ "deny",
19213
+ "deny_perm"
19214
+ ]);
19215
+ ApprovalRecordRequestSchema = exports_external.object({
19216
+ v: exports_external.literal(1),
19217
+ op: exports_external.literal("approval_record"),
19218
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
19219
+ decision: ApprovalDecisionModeSchema,
19220
+ approver_set: exports_external.array(exports_external.string()),
19221
+ granted_by_user_id: exports_external.number().int(),
19222
+ ttl_ms: exports_external.number().int().positive().nullable().optional()
19223
+ });
19224
+ ApprovalConsumeRecordRequestSchema = exports_external.object({
19225
+ v: exports_external.literal(1),
19226
+ op: exports_external.literal("approval_consume_record"),
19227
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
19228
+ decision: ApprovalDecisionModeSchema,
19229
+ approver_set: exports_external.array(exports_external.string()),
19230
+ granted_by_user_id: exports_external.number().int(),
19231
+ ttl_ms: exports_external.number().int().positive().nullable().optional()
19232
+ });
19233
+ RequestSchema = exports_external.discriminatedUnion("op", [
19234
+ GetRequestSchema,
19235
+ PutRequestSchema,
19236
+ ListRequestSchema,
19237
+ StatusRequestSchema,
19238
+ LockRequestSchema,
19239
+ PreflightAccessRequestSchema,
19240
+ MintGrantRequestSchema,
19241
+ ListGrantsRequestSchema,
19242
+ RevokeGrantRequestSchema,
19243
+ ApprovalRequestRequestSchema,
19244
+ ApprovalLookupRequestSchema,
19245
+ ApprovalLookupByRequestRequestSchema,
19246
+ ApprovalConsumeRequestSchema,
19247
+ ApprovalRevokeRequestSchema,
19248
+ ApprovalListRequestSchema,
19249
+ ApprovalRecordRequestSchema,
19250
+ ApprovalConsumeRecordRequestSchema
19251
+ ]);
19252
+ VaultEntrySchema = exports_external.union([
19253
+ exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
19254
+ exports_external.object({ kind: exports_external.literal("binary"), value: exports_external.string() }),
19255
+ exports_external.object({
19256
+ kind: exports_external.literal("files"),
19257
+ files: exports_external.record(exports_external.string(), exports_external.object({
19258
+ encoding: exports_external.enum(["utf8", "base64"]),
19259
+ value: exports_external.string()
19260
+ }))
19261
+ })
19262
+ ]);
19263
+ ErrorCode = exports_external.enum([
19264
+ "LOCKED",
19265
+ "DENIED",
19266
+ "UNKNOWN_KEY",
19267
+ "BAD_REQUEST",
19268
+ "INTERNAL",
19269
+ "AUDIT_UNAVAILABLE"
19270
+ ]);
19271
+ OkEntryResponseSchema = exports_external.object({
19272
+ ok: exports_external.literal(true),
19273
+ entry: VaultEntrySchema
19274
+ });
19275
+ OkKeysResponseSchema = exports_external.object({
19276
+ ok: exports_external.literal(true),
19277
+ keys: exports_external.array(exports_external.string())
19278
+ });
19279
+ BrokerStatus = exports_external.object({
19280
+ unlocked: exports_external.boolean(),
19281
+ keyCount: exports_external.number().int().nonnegative(),
19282
+ uptimeSec: exports_external.number().nonnegative()
19283
+ });
19284
+ OkStatusResponseSchema = exports_external.object({
19285
+ ok: exports_external.literal(true),
19286
+ status: BrokerStatus
19287
+ });
19288
+ OkLockResponseSchema = exports_external.object({
19289
+ ok: exports_external.literal(true),
19290
+ locked: exports_external.literal(true)
19291
+ });
19292
+ OkPutResponseSchema = exports_external.object({
19293
+ ok: exports_external.literal(true),
19294
+ put: exports_external.literal(true),
19295
+ key: exports_external.string()
19296
+ });
19297
+ OkMintGrantResponseSchema = exports_external.object({
19298
+ ok: exports_external.literal(true),
19299
+ token: exports_external.string(),
19300
+ id: exports_external.string(),
19301
+ expires_at: exports_external.number().nullable()
19302
+ });
19303
+ GrantMetaSchema = exports_external.object({
19304
+ id: exports_external.string(),
19305
+ agent_slug: exports_external.string(),
19306
+ key_allow: exports_external.array(exports_external.string()),
19307
+ write_allow: exports_external.array(exports_external.string()).default([]),
19308
+ expires_at: exports_external.number().nullable(),
19309
+ created_at: exports_external.number(),
19310
+ description: exports_external.string().nullable()
19311
+ });
19312
+ OkListGrantsResponseSchema = exports_external.object({
19313
+ ok: exports_external.literal(true),
19314
+ grants: exports_external.array(GrantMetaSchema)
19315
+ });
19316
+ OkRevokeGrantResponseSchema = exports_external.object({
19317
+ ok: exports_external.literal(true),
19318
+ revoked: exports_external.boolean()
19319
+ });
19320
+ OkApprovalRequestResponseSchema = exports_external.discriminatedUnion("state", [
19321
+ exports_external.object({
19322
+ ok: exports_external.literal(true),
19323
+ kind: exports_external.literal("approval_request"),
19324
+ state: exports_external.literal("pending"),
19325
+ request_id: exports_external.string(),
19326
+ expires_at: exports_external.number()
19327
+ }),
19328
+ exports_external.object({
19329
+ ok: exports_external.literal(true),
19330
+ kind: exports_external.literal("approval_request"),
19331
+ state: exports_external.literal("rate_limited"),
19332
+ retry_after_ms: exports_external.number()
19333
+ })
19334
+ ]);
19335
+ ApprovalDecisionMetaSchema = exports_external.object({
19336
+ id: exports_external.string(),
19337
+ agent_unit: exports_external.string(),
19338
+ scope: exports_external.string(),
19339
+ action: exports_external.string(),
19340
+ decision: ApprovalDecisionModeSchema,
19341
+ granted_at: exports_external.number(),
19342
+ granted_by_user_id: exports_external.number(),
19343
+ ttl_expires_at: exports_external.number().nullable(),
19344
+ last_used_at: exports_external.number().nullable(),
19345
+ revoked_at: exports_external.number().nullable(),
19346
+ revoke_reason: exports_external.string().nullable(),
19347
+ origin: exports_external.enum(["agent", "operator"]).optional()
19348
+ });
19349
+ OkApprovalLookupResponseSchema = exports_external.object({
19350
+ ok: exports_external.literal(true),
19351
+ state: exports_external.enum(["granted", "denied", "pending", "expired", "drift_revoked", "no_decision"]),
19352
+ decision: ApprovalDecisionMetaSchema.nullable().optional()
19353
+ });
19354
+ OkApprovalConsumeResponseSchema = exports_external.object({
19355
+ ok: exports_external.literal(true),
19356
+ consumed: exports_external.boolean(),
19357
+ agent_unit: exports_external.string().optional(),
19358
+ scope: exports_external.string().optional(),
19359
+ action: exports_external.string().optional(),
19360
+ why: exports_external.string().nullable().optional()
19361
+ });
19362
+ OkApprovalRevokeResponseSchema = exports_external.object({
19363
+ ok: exports_external.literal(true),
19364
+ revoked: exports_external.boolean()
19365
+ });
19366
+ OkApprovalListResponseSchema = exports_external.object({
19367
+ ok: exports_external.literal(true),
19368
+ decisions: exports_external.array(ApprovalDecisionMetaSchema)
19369
+ });
19370
+ OkApprovalRecordResponseSchema = exports_external.object({
19371
+ ok: exports_external.literal(true),
19372
+ decision_id: exports_external.string()
19373
+ });
19374
+ OkApprovalConsumeRecordResponseSchema = exports_external.object({
19375
+ ok: exports_external.literal(true),
19376
+ consumed: exports_external.boolean(),
19377
+ decision_id: exports_external.string().optional(),
19378
+ agent_unit: exports_external.string().optional(),
19379
+ scope: exports_external.string().optional(),
19380
+ action: exports_external.string().optional(),
19381
+ why: exports_external.string().nullable().optional()
19382
+ });
19383
+ ErrorResponseSchema = exports_external.object({
19384
+ ok: exports_external.literal(false),
19385
+ code: ErrorCode,
19386
+ msg: exports_external.string()
19387
+ });
19388
+ ResponseSchema = exports_external.union([
19389
+ OkEntryResponseSchema,
19390
+ OkKeysResponseSchema,
19391
+ OkStatusResponseSchema,
19392
+ OkLockResponseSchema,
19393
+ OkPreflightAccessResponseSchema,
19394
+ OkPutResponseSchema,
19395
+ OkMintGrantResponseSchema,
19396
+ OkListGrantsResponseSchema,
19397
+ OkRevokeGrantResponseSchema,
19398
+ OkApprovalRequestResponseSchema,
19399
+ OkApprovalLookupResponseSchema,
19400
+ OkApprovalConsumeResponseSchema,
19401
+ OkApprovalRevokeResponseSchema,
19402
+ OkApprovalListResponseSchema,
19403
+ OkApprovalRecordResponseSchema,
19404
+ OkApprovalConsumeRecordResponseSchema,
19405
+ ErrorResponseSchema
19406
+ ]);
19407
+ });
19408
+ // src/vault/broker/client.ts
19409
+ import { homedir as homedir3 } from "node:os";
19410
+ import { join as join3 } from "node:path";
19411
+ var LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH;
19412
+ var init_client = __esm(() => {
19413
+ init_protocol();
19414
+ init_peercred();
19415
+ LEGACY_SOCKET_PATH = join3(homedir3(), ".switchroom", "vault-broker.sock");
19416
+ OPERATOR_SOCKET_PATH = join3(homedir3(), ".switchroom", "broker-operator", "sock");
19417
+ });
19418
+
19419
+ // src/vault/resolver.ts
19420
+ var materializedDirs;
19421
+ var init_resolver = __esm(() => {
19422
+ init_vault();
19423
+ init_loader();
19424
+ init_client();
19425
+ materializedDirs = new Set;
19426
+ });
19427
+
19428
+ // src/vault/broker/server.ts
19429
+ import * as net from "node:net";
19430
+ import { mkdirSync as mkdirSync8, chmodSync as chmodSync5, chownSync as chownSync2, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync5, statSync as statSync7, unlinkSync as unlinkSync6, writeFileSync as writeFileSync5 } from "node:fs";
19431
+
19432
+ // src/agents/compose.ts
19433
+ init_schema();
19434
+
19435
+ // src/agents/scaffold.ts
19436
+ import { dirname as dirname2, isAbsolute, join as join4, relative, resolve as resolve6 } from "node:path";
19437
+ init_atomic();
19438
+
19439
+ // src/agents/agent-uid.ts
19440
+ init_peercred();
19441
+ import { createHash } from "node:crypto";
19442
+ var AGENT_UID_MIN = 10001;
19443
+ var AGENT_UID_MAX = 10999;
19444
+ function allocateAgentUid(name) {
19445
+ if (isReservedAgentName(name)) {
19446
+ throw new Error(`agent name '${name}' is reserved by switchroom for another identity kind ` + `(see vault/broker/peercred.ts:RESERVED_AGENT_NAMES). Pick a different name.`);
18871
19447
  }
18872
- return { allow: true };
19448
+ const hash = createHash("sha256").update(name).digest();
19449
+ const u32 = hash.readUInt32BE(0);
19450
+ const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
19451
+ return AGENT_UID_MIN + u32 % range;
18873
19452
  }
18874
19453
 
18875
- // src/scheduler/cron-introspect.ts
18876
- init_overlay_loader();
19454
+ // src/setup/hindsight-recall-tunables.ts
19455
+ var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
19456
+ var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
19457
+
19458
+ // src/agents/scaffold.ts
19459
+ init_schema();
19460
+
19461
+ // src/config/users.ts
19462
+ init_merge();
19463
+
19464
+ // src/config/thinking-effort-risk.ts
19465
+ var RISKY_EFFORTS = new Set(["medium", "high", "xhigh", "max"]);
19466
+
19467
+ // src/agents/scaffold.ts
19468
+ init_merge();
19469
+ init_timezone();
18877
19470
 
18878
19471
  // src/cli/agent-config.ts
18879
- var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
19472
+ import { join } from "node:path";
19473
+ import { homedir as homedir2 } from "node:os";
18880
19474
 
18881
- // src/agents/profiles.ts
18882
- var import_handlebars = __toESM(require_lib(), 1);
18883
- import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
18884
- import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
18885
- function resolveProfilesRoot() {
18886
- const envOverride = process.env.SWITCHROOM_PROFILES_ROOT?.trim();
18887
- if (envOverride) {
18888
- return resolve4(envOverride);
19475
+ // src/cli/helpers.ts
19476
+ init_loader();
19477
+
19478
+ // src/cli/agent-config.ts
19479
+ init_loader();
19480
+ init_merge();
19481
+
19482
+ // src/config/google-workspace-acl.ts
19483
+ function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
19484
+ if (!agentGoogleAccount)
19485
+ return false;
19486
+ const account = agentGoogleAccount.trim().toLowerCase();
19487
+ if (account.length === 0)
19488
+ return false;
19489
+ const acctEntry = googleAccounts?.[account];
19490
+ if (!acctEntry)
19491
+ return false;
19492
+ const enabledFor = acctEntry.enabled_for ?? [];
19493
+ return enabledFor.includes(agentName);
19494
+ }
19495
+ function vaultRefKey(value) {
19496
+ if (typeof value !== "string" || !value.startsWith("vault:"))
19497
+ return null;
19498
+ const key = value.slice("vault:".length).split("#")[0];
19499
+ return key.length > 0 ? key : null;
19500
+ }
19501
+ function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
19502
+ if (!agentName || !key)
19503
+ return false;
19504
+ const agentConfig = config.agents?.[agentName];
19505
+ if (!agentConfig)
19506
+ return false;
19507
+ if (agentConfig.mcp_servers?.["gdrive"] === false) {
19508
+ return false;
18889
19509
  }
18890
- const candidates = [
18891
- resolve4(import.meta.dirname, "../../profiles"),
18892
- resolve4(import.meta.dirname, "profiles")
18893
- ];
18894
- for (const candidate of candidates) {
18895
- if (existsSync4(candidate)) {
18896
- return candidate;
18897
- }
19510
+ const account = agentConfig.google_workspace?.account;
19511
+ if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
19512
+ return false;
18898
19513
  }
18899
- return candidates[0];
18900
- }
18901
- var PROFILES_ROOT = resolveProfilesRoot();
18902
- import_handlebars.default.registerHelper("json", (value) => {
18903
- return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
18904
- });
18905
- import_handlebars.default.registerHelper("isNumber", (value) => {
18906
- return typeof value === "number" && Number.isFinite(value);
18907
- });
18908
- var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
18909
- var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
18910
- for (const name of SHARED_FRAGMENTS) {
18911
- const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
18912
- if (existsSync4(fragPath)) {
18913
- import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
19514
+ const gw = config.google_workspace;
19515
+ if (!gw)
19516
+ return false;
19517
+ for (const ref of [gw.google_client_id, gw.google_client_secret]) {
19518
+ if (vaultRefKey(ref) === key)
19519
+ return true;
18914
19520
  }
19521
+ return false;
18915
19522
  }
18916
19523
 
18917
- // src/litellm/timeout-budget.ts
18918
- var LITELLM_ROUTER_MARGIN_S = 10;
18919
- var LITELLM_TIMEOUT_TIERS = {
18920
- interactive: {
18921
- group: "gpt-oss-20b",
18922
- localTimeoutS: 90,
18923
- fallbackGroup: "gpt-oss-20b-openrouter",
19524
+ // src/vault/broker/acl.ts
19525
+ var WEBKITE_VAULT_KEYS = new Set([
19526
+ "webkite/cloudflare-account-id",
19527
+ "webkite/cloudflare-api-token",
19528
+ "webkite/firecrawl-api-key"
19529
+ ]);
19530
+ function isWebkiteCredentialKeyForAgent(agentConfig, key) {
19531
+ if (!WEBKITE_VAULT_KEYS.has(key))
19532
+ return false;
19533
+ if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
19534
+ return false;
19535
+ return true;
19536
+ }
19537
+ function checkEntryScope(scope, agentSlug) {
19538
+ if (scope === undefined || scope === null) {
19539
+ return { allow: true };
19540
+ }
19541
+ const deny = scope.deny ?? [];
19542
+ const allow = scope.allow ?? [];
19543
+ if (agentSlug !== null && deny.includes(agentSlug)) {
19544
+ return {
19545
+ allow: false,
19546
+ reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
19547
+ };
19548
+ }
19549
+ if (allow.length > 0) {
19550
+ if (agentSlug === null || !allow.includes(agentSlug)) {
19551
+ return {
19552
+ allow: false,
19553
+ reason: agentSlug === null ? "caller agent slug could not be determined; entry has a non-empty allow list (scope-allow)" : `agent '${agentSlug}' is not in the entry's allow list (scope-allow)`
19554
+ };
19555
+ }
19556
+ }
19557
+ return { allow: true };
19558
+ }
19559
+ function checkAclByAgent(config, agentName, key) {
19560
+ if (!agentName) {
19561
+ return { allow: false, reason: "agent name unresolved" };
19562
+ }
19563
+ const agentConfig = config.agents?.[agentName];
19564
+ if (!agentConfig) {
19565
+ return { allow: false, reason: `agent '${agentName}' not found in config` };
19566
+ }
19567
+ const googleSlot = parseGoogleAccountSlotKey(key);
19568
+ if (googleSlot !== null) {
19569
+ return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
19570
+ }
19571
+ if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
19572
+ return { allow: true };
19573
+ }
19574
+ if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
19575
+ return { allow: true };
19576
+ }
19577
+ const agentBot = agentConfig.bot_token;
19578
+ const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
19579
+ if (typeof botRef === "string" && botRef.startsWith("vault:")) {
19580
+ const botKey = botRef.slice("vault:".length).split("#")[0];
19581
+ if (botKey.length > 0 && botKey === key) {
19582
+ return { allow: true };
19583
+ }
19584
+ }
19585
+ const cfgWithProfiles = config;
19586
+ const profileName = agentConfig.extends;
19587
+ const profileMcp = profileName != null && profileName.length > 0 ? cfgWithProfiles.profiles?.[profileName]?.mcp_servers ?? {} : {};
19588
+ const effectiveMcp = {
19589
+ ...cfgWithProfiles.defaults?.mcp_servers ?? {},
19590
+ ...profileMcp,
19591
+ ...agentConfig.mcp_servers ?? {}
19592
+ };
19593
+ for (const mcpEntry of Object.values(effectiveMcp)) {
19594
+ if (!mcpEntry || typeof mcpEntry !== "object")
19595
+ continue;
19596
+ const declared = mcpEntry.secrets;
19597
+ if (Array.isArray(declared) && declared.includes(key)) {
19598
+ return { allow: true };
19599
+ }
19600
+ }
19601
+ const cfgSecrets = config;
19602
+ const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
19603
+ const standingSecrets = [
19604
+ ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
19605
+ ...Array.isArray(profileSecrets) ? profileSecrets : [],
19606
+ ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
19607
+ ];
19608
+ if (standingSecrets.includes(key)) {
19609
+ return { allow: true };
19610
+ }
19611
+ const schedule = agentConfig.schedule ?? [];
19612
+ if (schedule.length === 0) {
19613
+ return {
19614
+ allow: false,
19615
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets', no mcp_servers.*.secrets[], and no agents.${agentName}.secrets[] standing grant declaring '${key}'; nothing is broker-accessible`
19616
+ };
19617
+ }
19618
+ for (const entry of schedule) {
19619
+ const allowed = entry?.secrets ?? [];
19620
+ if (allowed.includes(key)) {
19621
+ return { allow: true };
19622
+ }
19623
+ }
19624
+ return {
19625
+ allow: false,
19626
+ reason: `key '${key}' not in ACL for agent '${agentName}'`
19627
+ };
19628
+ }
19629
+ function parseGoogleAccountSlotKey(key) {
19630
+ const match = key.match(/^google:([^:]+):([a-z_]+)$/);
19631
+ if (!match)
19632
+ return null;
19633
+ return { account: match[1], field: match[2] };
19634
+ }
19635
+ function checkGoogleAccountAcl(config, agentName, account, key) {
19636
+ const accounts = config.google_accounts ?? {};
19637
+ const accountKey = account.toLowerCase();
19638
+ const accountEntry = accounts[accountKey] ?? accounts[account];
19639
+ if (!accountEntry) {
19640
+ return {
19641
+ allow: false,
19642
+ reason: `google_accounts['${account}'] not configured (key '${key}')`
19643
+ };
19644
+ }
19645
+ const enabled = accountEntry.enabled_for ?? [];
19646
+ if (enabled.length === 0) {
19647
+ return {
19648
+ allow: false,
19649
+ reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
19650
+ };
19651
+ }
19652
+ if (!enabled.includes(agentName)) {
19653
+ return {
19654
+ allow: false,
19655
+ reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
19656
+ };
19657
+ }
19658
+ return { allow: true };
19659
+ }
19660
+
19661
+ // src/scheduler/cron-introspect.ts
19662
+ init_overlay_loader();
19663
+
19664
+ // src/cli/agent-config.ts
19665
+ var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
19666
+
19667
+ // src/agents/profiles.ts
19668
+ var import_handlebars = __toESM(require_lib(), 1);
19669
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
19670
+ import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
19671
+ function resolveProfilesRoot() {
19672
+ const envOverride = process.env.SWITCHROOM_PROFILES_ROOT?.trim();
19673
+ if (envOverride) {
19674
+ return resolve4(envOverride);
19675
+ }
19676
+ const candidates = [
19677
+ resolve4(import.meta.dirname, "../../profiles"),
19678
+ resolve4(import.meta.dirname, "profiles")
19679
+ ];
19680
+ for (const candidate of candidates) {
19681
+ if (existsSync4(candidate)) {
19682
+ return candidate;
19683
+ }
19684
+ }
19685
+ return candidates[0];
19686
+ }
19687
+ var PROFILES_ROOT = resolveProfilesRoot();
19688
+ import_handlebars.default.registerHelper("json", (value) => {
19689
+ return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
19690
+ });
19691
+ import_handlebars.default.registerHelper("isNumber", (value) => {
19692
+ return typeof value === "number" && Number.isFinite(value);
19693
+ });
19694
+ var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
19695
+ var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
19696
+ for (const name of SHARED_FRAGMENTS) {
19697
+ const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
19698
+ if (existsSync4(fragPath)) {
19699
+ import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
19700
+ }
19701
+ }
19702
+
19703
+ // src/litellm/timeout-budget.ts
19704
+ var LITELLM_ROUTER_MARGIN_S = 10;
19705
+ var LITELLM_TIMEOUT_TIERS = {
19706
+ interactive: {
19707
+ group: "gpt-oss-20b",
19708
+ localTimeoutS: 90,
19709
+ fallbackGroup: "gpt-oss-20b-openrouter",
18924
19710
  fallbackTimeoutS: 60
18925
19711
  },
18926
19712
  retain: {
@@ -18971,12 +19757,15 @@ function pgMib(mib) {
18971
19757
  }
18972
19758
  var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
18973
19759
  var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
19760
+ var HINDSIGHT_PG_FSYNC_ENV = "SWITCHROOM_HINDSIGHT_PG_FSYNC";
19761
+ var HINDSIGHT_PG_DEFAULT_FSYNC = "on";
18974
19762
  var HINDSIGHT_PG_DEFAULTS = [
18975
19763
  [
18976
19764
  HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
18977
19765
  pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
18978
19766
  ],
18979
- [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
19767
+ [HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)],
19768
+ [HINDSIGHT_PG_FSYNC_ENV, HINDSIGHT_PG_DEFAULT_FSYNC]
18980
19769
  ];
18981
19770
  var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
18982
19771
 
@@ -19138,6 +19927,8 @@ var HINDSIGHT_RETAIN_CLIENT_DEADLINE_S = hindsightRetainClientTimeoutSeconds() +
19138
19927
  var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
19139
19928
  var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
19140
19929
  var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
19930
+ var HINDSIGHT_CP_UNAUTHENTICATED_BIND_ADDR = "127.0.0.1";
19931
+ var HINDSIGHT_CP_NO_ACCESS_KEY_WARNING = "hindsight: no `hindsight.cp_access_key` configured — the control-plane " + "dashboard has NO login (the access-key middleware is inert when " + "HINDSIGHT_CP_ACCESS_KEY is unset), so it is pinned to " + `${HINDSIGHT_CP_UNAUTHENTICATED_BIND_ADDR} and will NOT be reachable from ` + "the LAN or tailnet. Set `hindsight.cp_access_key: vault:<key>` in " + "switchroom.yaml to arm the login and open it up.";
19141
19932
 
19142
19933
  // src/memory/hindsight.ts
19143
19934
  var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
@@ -19180,1019 +19971,244 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
19180
19971
  ` + `- Restatements of the user's current request or the task in progress.
19181
19972
  ` + `- Volatile state written as a timeless assertion. A version, count, size,
19182
19973
  ` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
19183
- ` + ` claim is true only at the instant it was said. Concretely, never produce a
19184
- ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
19185
- ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
19186
- ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
19187
- ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
19188
- ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
19189
- ` + ` cannot date it, drop it. An undated one is recalled forever as though it
19190
- ` + ` were still true, which is worse than not remembering it at all.
19191
- ` + `- Transient state (unread counts, build status, what is running right now) unless
19192
- ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19193
- ` + `- Greetings, acknowledgements, and routine operational chatter.
19194
- ` + `
19195
- ` + `Write each fact so it stands alone: name the thing, the number, and the date. A
19196
- ` + `sentence that only makes sense while reading this transcript is not durable.
19197
- ` + `
19198
- ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
19199
- ` + `nothing durable remains, return an empty facts list.
19200
- `;
19201
- var SUPERSEDED_RETAIN_MISSIONS = [
19202
- "Extract technical decisions, architectural choices, user preferences, project context, and people/tool relationships. Ignore routine greetings and transient operational details.",
19203
- "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.",
19204
- "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise, " + "including in-flight workflow/process narration (a sub-task started, " + "paused, or is still running) — only retain the outcome once a task " + "actually completes or a decision is made.",
19205
- "Extract durable facts that will still be true and useful weeks from now: " + "user preferences and standing rules, ongoing projects and recurring " + "commitments, technical and architectural decisions with their rationale, " + "and people/tool relationships. A preference revealed by a request is " + "durable — record the preference (what the user likes, wants, or always " + `does), not the request itself.
19206
-
19207
- ` + `NEVER extract:
19208
- ` + "- Agent tool-use traces or narration of what the assistant did (e.g. " + `"the assistant used X to query Y", "ran a search", "sent the message").
19209
- ` + "- In-flight workflow/process narration (a sub-task started, paused, or is " + "still running) — retain the outcome only once the task completes or a " + `decision is made.
19210
- ` + `- Operation, request, batch or session IDs, UUIDs, hashes, or error codes.
19211
- ` + "- Hindsight's own errors, retries, backlogs, or internal state — the " + `memory system's self-reports are not memories.
19212
- ` + `- Restatements of the user's current request or the task in progress.
19213
- ` + "- Transient state (unread counts, build status, what is running right now) " + "unless the fact is explicitly dated, in which case record it as a dated " + `observation.
19214
- ` + `- Greetings, acknowledgements, and routine operational chatter.
19215
-
19216
- ` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list.",
19217
- `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.
19218
- ` + `
19219
- ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
19220
- ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
19221
- ` + `the location where some output was written? If yes, drop it — it is transcript
19222
- ` + `exhaust, not memory.
19223
- ` + `
19224
- ` + `NEVER extract:
19225
- ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
19226
- ` + ` text resembles any of these: "File created successfully at /path/to/file",
19227
- ` + ` "A background command with ID bctz4yskm is running, and its output will be
19228
- ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
19229
- ` + ` and is running in the background", "User executed a Bash command to sleep for
19230
- ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
19231
- ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
19232
- ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
19233
- ` + ` assistant used X to query Y", "ran a search", "sent the message").
19234
- ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
19235
- ` + ` running) — retain the outcome only once the task completes or a decision is made.
19236
- ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
19237
- ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
19238
- ` + ` reset assistant state").
19239
- ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
19240
- ` + ` system's self-reports are not memories.
19241
- ` + `- Restatements of the user's current request or the task in progress.
19242
- ` + `- Transient state (unread counts, build status, what is running right now) unless
19243
- ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19244
- ` + `- Greetings, acknowledgements, and routine operational chatter.
19245
- ` + `
19246
- ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
19247
- ` + "nothing durable remains, return an empty facts list.",
19248
- `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.
19249
- ` + `
19250
- ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
19251
- ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
19252
- ` + `the location where some output was written? If yes, drop it — it is transcript
19253
- ` + `exhaust, not memory.
19254
- ` + `
19255
- ` + `NEVER extract:
19256
- ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
19257
- ` + ` text resembles any of these: "File created successfully at /path/to/file",
19258
- ` + ` "A background command with ID bctz4yskm is running, and its output will be
19259
- ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
19260
- ` + ` and is running in the background", "User executed a Bash command to sleep for
19261
- ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
19262
- ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
19263
- ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
19264
- ` + ` assistant used X to query Y", "ran a search", "sent the message").
19265
- ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
19266
- ` + ` running) — retain the outcome only once the task completes or a decision is made.
19267
- ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
19268
- ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
19269
- ` + ` reset assistant state").
19270
- ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
19271
- ` + ` system's self-reports are not memories.
19272
- ` + `- Restatements of the user's current request or the task in progress.
19273
- ` + `- Volatile state written as a timeless assertion. A version, count, size,
19274
- ` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
19275
- ` + ` claim is true only at the instant it was said. Concretely, never produce a
19276
- ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
19277
- ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
19278
- ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
19279
- ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
19280
- ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
19281
- ` + ` cannot date it, drop it. An undated one is recalled forever as though it
19282
- ` + ` were still true, which is worse than not remembering it at all.
19283
- ` + `- Transient state (unread counts, build status, what is running right now) unless
19284
- ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19285
- ` + `- Greetings, acknowledgements, and routine operational chatter.
19286
- ` + `
19287
- ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
19288
- ` + "nothing durable remains, return an empty facts list.",
19289
- `Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
19290
- ` + `
19291
- ` + `DURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:
19292
- ` + `- PREFERENCE — what the user likes, wants, or always does; a standing rule or correction.
19293
- ` + `- DECISION — a settled choice that changes how future work is done, including a choice NOT to do something. A decision about the mechanics of the CURRENT task (which worker to dispatch, which branch to rebase, which PR to merge now, what to do next) is process narration, not a durable decision — drop it unless it establishes a standing rule or permanently changes a system.
19294
- ` + `- FINDING — a root cause, a measurement, or verified behaviour of a system. Include the number.
19295
- ` + `- OUTCOME — a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.
19296
- ` + `- RELATIONSHIP — who a person is, what a project or tool is, and how they connect.
19297
- ` + `If a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.
19298
- ` + `
19299
- ` + `A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.
19300
- ` + `
19301
- ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
19302
- ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
19303
- ` + `the location where some output was written? If yes, drop it — it is transcript
19304
- ` + `exhaust, not memory.
19305
- ` + `
19306
- ` + `NEVER extract:
19307
- ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
19308
- ` + ` text resembles any of these: "File created successfully at /path/to/file",
19309
- ` + ` "A background command with ID bctz4yskm is running, and its output will be
19310
- ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
19311
- ` + ` and is running in the background", "User executed a Bash command to sleep for
19312
- ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
19313
- ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
19314
- ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
19315
- ` + ` assistant used X to query Y", "ran a search", "sent the message").
19316
- ` + `- The act of delegating, dispatching, spawning, launching, steering or merging
19317
- ` + ` work — including when it succeeded. "X was dispatched and completed" is the
19318
- ` + ` session describing itself. Record only what the work LEARNED or CHANGED.
19319
- ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
19320
- ` + ` running) — retain the outcome only once the task completes or a decision is made.
19321
- ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
19322
- ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
19323
- ` + ` reset assistant state").
19324
- ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
19325
- ` + ` system's self-reports are not memories.
19326
- ` + `- Restatements of the user's current request or the task in progress.
19327
- ` + `- Transient state (unread counts, build status, what is running right now) unless
19328
- ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19329
- ` + `- Greetings, acknowledgements, and routine operational chatter.
19330
- ` + `
19331
- ` + `Write each fact so it stands alone: name the thing, the number, and the date. A
19332
- ` + `sentence that only makes sense while reading this transcript is not durable.
19333
- ` + `
19334
- ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
19335
- ` + `nothing durable remains, return an empty facts list.
19336
- `
19337
- ];
19338
- var DEFAULT_OBSERVATIONS_MISSION = `Synthesise durable, standing knowledge about the people, projects, and systems this agent works with: preferences and standing rules, roles and relationships, skills and recurring patterns, technical and operational decisions with their rationale, and the state of long-running work once it lands.
19339
- ` + `
19340
- ` + `The test is durability, not notability: an observation must still be worth reading weeks from now. A single dated event belongs in an observation only when it establishes or changes a standing fact.
19341
- ` + `
19342
- ` + `Do NOT synthesise observations from:
19343
- ` + `- Transcript exhaust — tool calls and their results, file paths, temp or scratchpad directories, where some output was written, or narration of what an assistant did.
19344
- ` + `- Identifiers with no standing meaning: session, agent, request, batch or command IDs, UUIDs, hashes, error codes.
19345
- ` + `- In-flight process narration — a task started, paused, or still running. Record the outcome once it lands, not the running state.
19346
- ` + `- The memory system's own errors, retries, backlogs, or internal state.
19347
- ` + `- Transient state (what is running right now, unread counts, build status) unless the fact is explicitly dated, in which case record it as dated.
19348
- ` + `
19349
- ` + "If the new facts contain nothing durable, record nothing rather than synthesising a weak observation.";
19350
- var SUPERSEDED_OBSERVATIONS_MISSIONS = [
19351
- "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context — how habits, setbacks, and encouragement connect over time."
19352
- ];
19353
- var PROFILE_MEMORY_DEFAULTS = {
19354
- "health-coach": {
19355
- disposition: { skepticism: 2, literalism: 2, empathy: 5 },
19356
- observations_mission: `You consolidate the memory of a health and fitness coach working with one person. This bank records how that person actually lives and trains.
19357
- ` + `
19358
- ` + `Synthesise into durable observations:
19359
- ` + `- Goals, targets, and the plan currently in force, with the reasoning behind each.
19360
- ` + `- Training, nutrition, sleep, and alcohol patterns as they hold over weeks — what the person reliably does, not what they did once.
19361
- ` + `- Constraints that shape the plan: injuries, medical guidance, schedule, equipment, foods and sessions they refuse.
19362
- ` + `- Motivations, and what actually helps or backfires when they slip.
19363
- ` + `- Trends in the numbers: direction and range over time, not any single reading.
19364
- ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
19365
- ` + `
19366
- ` + `A single day's log, weight reading, or session is evidence, not an observation. Record one only when it establishes or changes a standing pattern, target, or constraint — then fold it into the observation for that pattern and say what changed and roughly when.
19367
- ` + `
19368
- ` + `Granularity: one observation per habit, target, constraint, or trend. Aggregate repeated daily evidence into the observation for that pattern rather than creating one per day, and keep training, nutrition, and sleep as separate observations.
19369
- ` + `
19370
- ` + "Word observations as the person's own pattern and framing, never as a verdict on them."
19371
- },
19372
- "executive-assistant": {
19373
- disposition: { skepticism: 4, literalism: 4, empathy: 3 },
19374
- observations_mission: `You consolidate the memory of an executive assistant working for one person. This bank records that person's commitments, people, and standing arrangements.
19375
- ` + `
19376
- ` + `Synthesise into durable observations:
19377
- ` + `- Standing rules and preferences: how they want things scheduled, written, and filed, and when they want to be interrupted.
19378
- ` + `- People and organisations, and the relationship: who they are, what they are involved in, how to reach them.
19379
- ` + `- Recurring commitments and routines, and the constraints around them.
19380
- ` + `- Obligations and their state: what was promised to whom, the deadline, and what is still outstanding.
19381
- ` + `- Decisions made and decisions deferred, with the reasoning and the trade accepted.
19382
- ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
19383
- ` + `
19384
- ` + `Live commitment state IS durable knowledge here, not ephemeral chatter. An outstanding obligation, a travel window, an unanswered request, a "currently X" arrangement — these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
19385
- ` + `
19386
- ` + `Granularity: one observation per person, arrangement, or obligation. Aggregate repeated mentions into that observation rather than creating siblings; never merge two different people or two different commitments into one.
19387
- ` + `
19388
- ` + "Do not synthesise from transcript exhaust: tool calls and their results, message or event identifiers, or narration of what the assistant did."
19389
- },
19390
- coding: {
19391
- disposition: { skepticism: 4, literalism: 5, empathy: 2 },
19392
- observations_mission: `You consolidate the memory of a software-engineering agent. This bank records real work on real codebases.
19393
- ` + `
19394
- ` + `Synthesise into durable observations:
19395
- ` + `- Architecture and design decisions, each with its rationale and the trade accepted.
19396
- ` + `- Root causes, with the evidence chain, and negative results — what was ruled out matters as much as what was found.
19397
- ` + `- How the repository works: build, test, and lint commands, conventions, CI gates, and where things live.
19398
- ` + `- Outcomes of code work: issue and PR numbers, what changed, whether it merged, what review found.
19399
- ` + `- The user's standing rules, preferences, and corrections to this agent's behaviour.
19400
- ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
19401
- ` + `
19402
- ` + `Repository and service state IS durable knowledge here, not ephemeral chatter. Versions, a failing gate, an open PR, a measured number, a "currently X" claim — these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
19403
- ` + `
19404
- ` + `Granularity: one observation per distinct decision, cause, convention, or work item. Aggregate repeated evidence about the same one into that observation rather than creating siblings; never merge two separate decisions into a single summary.
19405
- ` + `
19406
- ` + "Do not synthesise from transcript exhaust: tool calls and their results, scratch paths, session or request identifiers, or narration of what the agent did. Prefer specific and falsifiable — naming the file, the number, the commit, or the decision beats summarising the topic."
19407
- }
19408
- };
19409
- // src/agents/reconcile-default-skills.ts
19410
- var warnedMissingPool = new Set;
19411
- var warnedMissingDefault = new Set;
19412
-
19413
- // src/telegram/state.ts
19414
- init_paths();
19415
-
19416
- // src/agents/scaffold.ts
19417
- init_paths();
19418
- init_loader();
19419
-
19420
- // src/vault/vault.ts
19421
- import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
19422
- import {
19423
- readFileSync as readFileSync6,
19424
- writeSync as writeSync3,
19425
- existsSync as existsSync6,
19426
- renameSync as renameSync2,
19427
- mkdirSync as mkdirSync2,
19428
- unlinkSync as unlinkSync2,
19429
- fsyncSync as fsyncSync3,
19430
- openSync as openSync3,
19431
- closeSync as closeSync3,
19432
- lstatSync,
19433
- realpathSync as realpathSync2
19434
- } from "node:fs";
19435
- import { dirname, basename as basename2, resolve as resolve5 } from "node:path";
19436
-
19437
- // src/vault/flock.ts
19438
- import {
19439
- existsSync as existsSync5,
19440
- openSync as openSync2,
19441
- closeSync as closeSync2,
19442
- writeSync as writeSync2,
19443
- fsyncSync as fsyncSync2,
19444
- unlinkSync,
19445
- readFileSync as readFileSync5,
19446
- readdirSync as readdirSync3,
19447
- rmdirSync,
19448
- statSync as statSync3,
19449
- constants as fsConstants
19450
- } from "node:fs";
19451
- var DEFAULT_LOCK_RETRY_MS = 5000;
19452
- function lockPathFor(vaultPath) {
19453
- return `${vaultPath}.lock`;
19454
- }
19455
- function readLockHolder(lockPath) {
19456
- try {
19457
- const raw = readFileSync5(lockPath, "utf8");
19458
- const lines = raw.split(`
19459
- `);
19460
- const pid = Number.parseInt(lines[0] ?? "", 10);
19461
- const acquiredAtMs = Number.parseInt(lines[1] ?? "", 10);
19462
- if (!Number.isFinite(pid) || pid <= 0)
19463
- return null;
19464
- if (!Number.isFinite(acquiredAtMs) || acquiredAtMs <= 0)
19465
- return null;
19466
- return { pid, acquiredAtMs, argv0: lines[2] ?? "" };
19467
- } catch {
19468
- return null;
19469
- }
19470
- }
19471
- function pidIsLive(pid) {
19472
- if (process.platform === "linux") {
19473
- return existsSync5(`/proc/${pid}`);
19474
- }
19475
- try {
19476
- process.kill(pid, 0);
19477
- return true;
19478
- } catch {
19479
- return false;
19480
- }
19481
- }
19482
- function parseProcStartTimeMs(statLine, procStat, now) {
19483
- const tailStart = statLine.lastIndexOf(")");
19484
- if (tailStart < 0)
19485
- return null;
19486
- const tokens = statLine.slice(tailStart + 1).trim().split(/\s+/);
19487
- const starttimeTicks = Number.parseInt(tokens[19] ?? "", 10);
19488
- if (!Number.isFinite(starttimeTicks))
19489
- return null;
19490
- const btimeMatch = procStat.match(/^btime\s+(\d+)/m);
19491
- if (!btimeMatch)
19492
- return null;
19493
- const bootEpochSec = Number.parseInt(btimeMatch[1], 10);
19494
- if (!Number.isFinite(bootEpochSec))
19495
- return null;
19496
- const USER_HZ = 100;
19497
- const startEpochMs = bootEpochSec * 1000 + starttimeTicks / USER_HZ * 1000;
19498
- const bootEpochMs = bootEpochSec * 1000;
19499
- const SLOP_MS = 5000;
19500
- if (startEpochMs < bootEpochMs - SLOP_MS || startEpochMs > now + SLOP_MS) {
19501
- return null;
19502
- }
19503
- return Math.floor(startEpochMs);
19504
- }
19505
- function pidStartTimeMs(pid) {
19506
- if (process.platform !== "linux")
19507
- return null;
19508
- try {
19509
- const statLine = readFileSync5(`/proc/${pid}/stat`, "utf8");
19510
- const procStat = readFileSync5("/proc/stat", "utf8");
19511
- return parseProcStartTimeMs(statLine, procStat, Date.now());
19512
- } catch {
19513
- return null;
19514
- }
19515
- }
19516
- function pidIsOriginalHolder(holder) {
19517
- const startMs = pidStartTimeMs(holder.pid);
19518
- if (startMs === null)
19519
- return true;
19520
- return startMs <= holder.acquiredAtMs + 100;
19521
- }
19522
- var STALE_MTIME_FLOOR_MS = 1e4;
19523
- function lockFileMtimeIsOlderThan(lockPath, budgetMs) {
19524
- try {
19525
- const s = statSync3(lockPath);
19526
- const threshold = Math.max(STALE_MTIME_FLOOR_MS, budgetMs * 2);
19527
- return Date.now() - s.mtimeMs > threshold;
19528
- } catch {
19529
- return false;
19530
- }
19531
- }
19532
- var SENTINEL_DIR_RECENT_WRITE_MS = 60000;
19533
- function clearStaleSentinelDir(lockPath) {
19534
- try {
19535
- if (!existsSync5(lockPath))
19536
- return true;
19537
- const s = statSync3(lockPath);
19538
- if (!s.isDirectory())
19539
- return true;
19540
- const now = Date.now();
19541
- for (const entry of readdirSync3(lockPath)) {
19542
- try {
19543
- const childStat = statSync3(`${lockPath}/${entry}`);
19544
- if (now - childStat.mtimeMs < SENTINEL_DIR_RECENT_WRITE_MS) {
19545
- return false;
19546
- }
19547
- } catch {}
19548
- }
19549
- for (const entry of readdirSync3(lockPath)) {
19550
- try {
19551
- unlinkSync(`${lockPath}/${entry}`);
19552
- } catch {}
19553
- }
19554
- rmdirSync(lockPath);
19555
- return true;
19556
- } catch {
19557
- return false;
19558
- }
19559
- }
19560
- function acquireLock(vaultPath, options = {}) {
19561
- const budgetMs = options.budgetMs ?? DEFAULT_LOCK_RETRY_MS;
19562
- const lockPath = lockPathFor(vaultPath);
19563
- const deadline = Date.now() + budgetMs;
19564
- const sleepBuf = new Int32Array(new SharedArrayBuffer(4));
19565
- while (true) {
19566
- let fd = null;
19567
- try {
19568
- fd = openSync2(lockPath, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 384);
19569
- } catch (err) {
19570
- const code = err?.code ?? "";
19571
- if (code === "EEXIST") {
19572
- let isDir = false;
19573
- try {
19574
- isDir = statSync3(lockPath).isDirectory();
19575
- } catch {}
19576
- if (isDir) {
19577
- if (clearStaleSentinelDir(lockPath))
19578
- continue;
19579
- }
19580
- const holder = readLockHolder(lockPath);
19581
- const isDeadPid = holder !== null && !pidIsLive(holder.pid);
19582
- const isReusedPid = holder !== null && pidIsLive(holder.pid) && !pidIsOriginalHolder(holder);
19583
- const isUnparseableAndOld = holder === null && lockFileMtimeIsOlderThan(lockPath, budgetMs);
19584
- if (isDeadPid || isReusedPid || isUnparseableAndOld) {
19585
- try {
19586
- unlinkSync(lockPath);
19587
- } catch {}
19588
- continue;
19589
- }
19590
- if (Date.now() >= deadline) {
19591
- throw makeBusyError(vaultPath, lockPath, holder, budgetMs);
19592
- }
19593
- Atomics.wait(sleepBuf, 0, 0, 100);
19594
- continue;
19595
- } else {
19596
- throw err;
19597
- }
19598
- }
19599
- try {
19600
- const meta = `${process.pid}
19601
- ${Date.now()}
19602
- ${process.argv[1] ?? ""}
19603
- `;
19604
- writeSync2(fd, meta);
19605
- fsyncSync2(fd);
19606
- } catch {
19607
- try {
19608
- closeSync2(fd);
19609
- } catch {}
19610
- try {
19611
- unlinkSync(lockPath);
19612
- } catch {}
19613
- throw new Error(`vault flock: failed to write holder metadata to ${lockPath}`);
19614
- }
19615
- const ownedFd = fd;
19616
- return {
19617
- release: () => {
19618
- try {
19619
- closeSync2(ownedFd);
19620
- } catch {}
19621
- try {
19622
- unlinkSync(lockPath);
19623
- } catch {}
19624
- }
19625
- };
19626
- }
19627
- }
19628
-
19629
- class VaultBusyError extends Error {
19630
- vaultPath;
19631
- lockPath;
19632
- holderPid;
19633
- heldForMs;
19634
- budgetMs;
19635
- constructor(message, fields) {
19636
- super(message);
19637
- this.name = "VaultBusyError";
19638
- this.vaultPath = fields.vaultPath;
19639
- this.lockPath = fields.lockPath;
19640
- this.holderPid = fields.holderPid;
19641
- this.heldForMs = fields.heldForMs;
19642
- this.budgetMs = fields.budgetMs;
19643
- }
19644
- }
19645
- function makeBusyError(vaultPath, lockPath, holder, budgetMs) {
19646
- const holderPid = holder?.pid ?? null;
19647
- const heldForMs = holder ? Date.now() - holder.acquiredAtMs : null;
19648
- const holderClause = holder ? `held by pid ${holder.pid} (acquired ${Math.round((heldForMs ?? 0) / 1000)}s ago)` : "held by another writer (holder PID unreadable — lock file empty or unparseable)";
19649
- return new VaultBusyError(`vault busy: ${holderClause} at ${lockPath} (retried for ${budgetMs}ms). ` + `Try again in a moment. If the holder process is gone, the next acquirer ` + `will clear the stale lock automatically.`, { vaultPath, lockPath, holderPid, heldForMs, budgetMs });
19650
- }
19651
-
19652
- // src/vault/vault.ts
19653
- var KNOWN_VAULT_ARTIFACT_NAMES = new Set([
19654
- "vault.enc",
19655
- "vault.enc.bak",
19656
- "vault.enc.tmp",
19657
- "vault.enc.lock",
19658
- ".vault.enc.symlink-tmp"
19659
- ]);
19660
- var SAVE_VAULT_LOCK_RETRY_MS = DEFAULT_LOCK_RETRY_MS;
19661
- var SCRYPT_N = 32768;
19662
- var SCRYPT_R = 8;
19663
- var SCRYPT_P = 1;
19664
- var SCRYPT_MAXMEM = 128 * 1024 * 1024;
19665
- function atomicWriteFileSync(path, data, mode) {
19666
- let effectivePath = path;
19667
- try {
19668
- if (existsSync6(path) && lstatSync(path).isSymbolicLink()) {
19669
- effectivePath = realpathSync2(path);
19670
- }
19671
- } catch {}
19672
- const dir = dirname(resolve5(effectivePath));
19673
- const tmp = resolve5(dir, `.${basename2(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
19674
- try {
19675
- const fd = openSync3(tmp, "wx", mode);
19676
- try {
19677
- writeSync3(fd, data);
19678
- fsyncSync3(fd);
19679
- } finally {
19680
- closeSync3(fd);
19681
- }
19682
- renameSync2(tmp, effectivePath);
19683
- try {
19684
- const dirFd = openSync3(dir, "r");
19685
- try {
19686
- fsyncSync3(dirFd);
19687
- } finally {
19688
- closeSync3(dirFd);
19689
- }
19690
- } catch {}
19691
- } catch (err) {
19692
- try {
19693
- if (existsSync6(tmp))
19694
- unlinkSync2(tmp);
19695
- } catch {}
19696
- throw err;
19697
- }
19698
- }
19699
-
19700
- class VaultError extends Error {
19701
- cause;
19702
- constructor(message, options) {
19703
- super(message);
19704
- this.name = "VaultError";
19705
- if (options?.cause !== undefined)
19706
- this.cause = options.cause;
19707
- }
19708
- }
19709
- var LEGACY_SCRYPT_N = 16384;
19710
- function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
19711
- return scryptSync(passphrase, salt, 32, {
19712
- N: params.N,
19713
- r: params.r,
19714
- p: params.p,
19715
- maxmem: SCRYPT_MAXMEM
19716
- });
19717
- }
19718
- function encrypt(key, plaintext) {
19719
- const iv = randomBytes(12);
19720
- const cipher = createCipheriv("aes-256-gcm", key, iv);
19721
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
19722
- const tag = cipher.getAuthTag();
19723
- return {
19724
- iv: iv.toString("hex"),
19725
- data: encrypted.toString("hex"),
19726
- tag: tag.toString("hex")
19727
- };
19728
- }
19729
- function decrypt(key, iv, data, tag) {
19730
- const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
19731
- decipher.setAuthTag(Buffer.from(tag, "hex"));
19732
- const decrypted = Buffer.concat([
19733
- decipher.update(Buffer.from(data, "hex")),
19734
- decipher.final()
19735
- ]);
19736
- return decrypted.toString("utf8");
19737
- }
19738
- function normalizeEntry(raw) {
19739
- if (typeof raw === "string") {
19740
- return { kind: "string", value: raw };
19741
- }
19742
- return raw;
19743
- }
19744
- function normalizeSecrets(raw) {
19745
- const out = {};
19746
- for (const [k, v] of Object.entries(raw)) {
19747
- out[k] = normalizeEntry(v);
19748
- }
19749
- return out;
19750
- }
19751
- function openVault(passphrase, vaultPath) {
19752
- if (!existsSync6(vaultPath)) {
19753
- throw new VaultError(`Vault file not found: ${vaultPath}`);
19754
- }
19755
- let vaultFile;
19756
- try {
19757
- vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
19758
- } catch {
19759
- throw new VaultError(`Failed to read vault file: ${vaultPath}`);
19760
- }
19761
- const salt = Buffer.from(vaultFile.salt, "hex");
19762
- const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
19763
- const key = deriveKey(passphrase, salt, kdfParams);
19764
- let plaintext;
19765
- try {
19766
- plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
19767
- } catch {
19768
- throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
19769
- }
19770
- let vaultData;
19771
- try {
19772
- vaultData = JSON.parse(plaintext);
19773
- } catch {
19774
- throw new VaultError("Vault data is corrupted");
19775
- }
19776
- return normalizeSecrets(vaultData.secrets ?? {});
19777
- }
19778
- function saveVault(passphrase, vaultPath, secrets) {
19779
- if (!existsSync6(vaultPath)) {
19780
- throw new VaultError(`Vault file not found: ${vaultPath}`);
19781
- }
19782
- let releaseLock = null;
19783
- try {
19784
- releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
19785
- } catch (err) {
19786
- if (err instanceof VaultBusyError) {
19787
- throw new VaultError(err.message, { cause: err });
19788
- }
19789
- throw err;
19790
- }
19791
- try {
19792
- let vaultFile;
19793
- try {
19794
- vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
19795
- } catch {
19796
- throw new VaultError(`Failed to read vault file: ${vaultPath}`);
19797
- }
19798
- const salt = Buffer.from(vaultFile.salt, "hex");
19799
- const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
19800
- vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
19801
- const vaultData = { secrets };
19802
- const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
19803
- vaultFile.iv = iv;
19804
- vaultFile.data = data;
19805
- vaultFile.tag = tag;
19806
- atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
19807
- } finally {
19808
- if (releaseLock !== null) {
19809
- try {
19810
- releaseLock();
19811
- } catch {}
19812
- }
19813
- }
19814
- }
19815
- function acquireVaultLock(vaultPath) {
19816
- try {
19817
- return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
19818
- } catch (err) {
19819
- if (err instanceof VaultBusyError) {
19820
- throw new VaultError(err.message, { cause: err });
19821
- }
19822
- throw err;
19823
- }
19824
- }
19825
-
19826
- // src/vault/resolver.ts
19827
- init_loader();
19828
-
19829
- // src/vault/broker/client.ts
19830
- import { homedir as homedir3 } from "node:os";
19831
- import { join as join3 } from "node:path";
19832
-
19833
- // src/vault/broker/protocol.ts
19834
- init_zod();
19835
- var MAX_FRAME_BYTES = 64 * 1024;
19836
- var AgentNameSchema = exports_external.string().min(1).max(64, "agent name max 64 chars").regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, "agent name must be kebab-case ASCII (alnum + _- only, first char alnum)").refine((s) => !isReservedAgentName(s), {
19837
- message: "agent name is reserved"
19838
- });
19839
- var GetRequestSchema = exports_external.object({
19840
- v: exports_external.literal(1),
19841
- op: exports_external.literal("get"),
19842
- key: exports_external.string().min(1),
19843
- filename: exports_external.string().optional(),
19844
- token: exports_external.string().optional()
19845
- });
19846
- var PutRequestSchema = exports_external.object({
19847
- v: exports_external.literal(1),
19848
- op: exports_external.literal("put"),
19849
- key: exports_external.string().min(1),
19850
- entry: exports_external.union([
19851
- exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
19852
- exports_external.object({ kind: exports_external.literal("binary"), value: exports_external.string() })
19853
- ]),
19854
- token: exports_external.string().optional(),
19855
- passphrase: exports_external.string().optional(),
19856
- attest_via_posture: exports_external.boolean().optional()
19857
- });
19858
- var ListRequestSchema = exports_external.object({
19859
- v: exports_external.literal(1),
19860
- op: exports_external.literal("list"),
19861
- token: exports_external.string().optional()
19862
- });
19863
- var MintGrantRequestSchema = exports_external.object({
19864
- v: exports_external.literal(1),
19865
- op: exports_external.literal("mint_grant"),
19866
- agent: AgentNameSchema,
19867
- keys: exports_external.array(exports_external.string().min(1)),
19868
- ttl_seconds: exports_external.number().int().positive().nullable(),
19869
- description: exports_external.string().optional(),
19870
- write_keys: exports_external.array(exports_external.string().min(1)).optional(),
19871
- passphrase: exports_external.string().optional(),
19872
- attest_via_posture: exports_external.boolean().optional(),
19873
- decision_id: exports_external.string().optional()
19874
- });
19875
- var ListGrantsRequestSchema = exports_external.object({
19876
- v: exports_external.literal(1),
19877
- op: exports_external.literal("list_grants"),
19878
- agent: AgentNameSchema.optional(),
19879
- passphrase: exports_external.string().optional(),
19880
- attest_via_posture: exports_external.boolean().optional()
19881
- });
19882
- var RevokeGrantRequestSchema = exports_external.object({
19883
- v: exports_external.literal(1),
19884
- op: exports_external.literal("revoke_grant"),
19885
- id: exports_external.string().min(1)
19886
- });
19887
- var StatusRequestSchema = exports_external.object({
19888
- v: exports_external.literal(1),
19889
- op: exports_external.literal("status")
19890
- });
19891
- var LockRequestSchema = exports_external.object({
19892
- v: exports_external.literal(1),
19893
- op: exports_external.literal("lock")
19894
- });
19895
- var PreflightAccessRequestSchema = exports_external.object({
19896
- v: exports_external.literal(1),
19897
- op: exports_external.literal("preflight_access"),
19898
- agent: AgentNameSchema,
19899
- keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
19900
- });
19901
- var OkPreflightAccessResponseSchema = exports_external.object({
19902
- ok: exports_external.literal(true),
19903
- op: exports_external.literal("preflight_access"),
19904
- results: exports_external.array(exports_external.object({
19905
- key: exports_external.string(),
19906
- exists: exports_external.boolean(),
19907
- acl_ok: exports_external.boolean(),
19908
- acl_reason: exports_external.string().optional(),
19909
- scope_ok: exports_external.boolean(),
19910
- scope_reason: exports_external.string().optional()
19911
- }))
19912
- });
19913
- var ApprovalRequestRequestSchema = exports_external.object({
19914
- v: exports_external.literal(1),
19915
- op: exports_external.literal("approval_request"),
19916
- agent_unit: exports_external.string().min(1),
19917
- scope: exports_external.string().min(1),
19918
- action: exports_external.string().min(1),
19919
- approver_set: exports_external.array(exports_external.string()),
19920
- why: exports_external.string().optional(),
19921
- ttl_ms: exports_external.number().int().positive().optional()
19922
- });
19923
- var ApprovalLookupRequestSchema = exports_external.object({
19924
- v: exports_external.literal(1),
19925
- op: exports_external.literal("approval_lookup"),
19926
- agent_unit: exports_external.string().min(1),
19927
- scope: exports_external.string().min(1),
19928
- action: exports_external.string().min(1),
19929
- current_approver_set: exports_external.array(exports_external.string())
19930
- });
19931
- var ApprovalLookupByRequestRequestSchema = exports_external.object({
19932
- v: exports_external.literal(1),
19933
- op: exports_external.literal("approval_lookup_by_request"),
19934
- agent_unit: exports_external.string().min(1),
19935
- request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
19936
- current_approver_set: exports_external.array(exports_external.string())
19937
- });
19938
- var ApprovalConsumeRequestSchema = exports_external.object({
19939
- v: exports_external.literal(1),
19940
- op: exports_external.literal("approval_consume"),
19941
- request_id: exports_external.string().regex(/^[0-9a-f]{32}$/)
19942
- });
19943
- var ApprovalRevokeRequestSchema = exports_external.object({
19944
- v: exports_external.literal(1),
19945
- op: exports_external.literal("approval_revoke"),
19946
- decision_id: exports_external.string().min(1),
19947
- actor: exports_external.string().min(1),
19948
- reason: exports_external.string().optional()
19949
- });
19950
- var ApprovalListRequestSchema = exports_external.object({
19951
- v: exports_external.literal(1),
19952
- op: exports_external.literal("approval_list"),
19953
- agent_unit: exports_external.string().optional()
19954
- });
19955
- var ApprovalDecisionModeSchema = exports_external.enum([
19956
- "allow_once",
19957
- "allow_always",
19958
- "allow_ttl",
19959
- "deny",
19960
- "deny_perm"
19961
- ]);
19962
- var ApprovalRecordRequestSchema = exports_external.object({
19963
- v: exports_external.literal(1),
19964
- op: exports_external.literal("approval_record"),
19965
- request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
19966
- decision: ApprovalDecisionModeSchema,
19967
- approver_set: exports_external.array(exports_external.string()),
19968
- granted_by_user_id: exports_external.number().int(),
19969
- ttl_ms: exports_external.number().int().positive().nullable().optional()
19970
- });
19971
- var ApprovalConsumeRecordRequestSchema = exports_external.object({
19972
- v: exports_external.literal(1),
19973
- op: exports_external.literal("approval_consume_record"),
19974
- request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
19975
- decision: ApprovalDecisionModeSchema,
19976
- approver_set: exports_external.array(exports_external.string()),
19977
- granted_by_user_id: exports_external.number().int(),
19978
- ttl_ms: exports_external.number().int().positive().nullable().optional()
19979
- });
19980
- var RequestSchema = exports_external.discriminatedUnion("op", [
19981
- GetRequestSchema,
19982
- PutRequestSchema,
19983
- ListRequestSchema,
19984
- StatusRequestSchema,
19985
- LockRequestSchema,
19986
- PreflightAccessRequestSchema,
19987
- MintGrantRequestSchema,
19988
- ListGrantsRequestSchema,
19989
- RevokeGrantRequestSchema,
19990
- ApprovalRequestRequestSchema,
19991
- ApprovalLookupRequestSchema,
19992
- ApprovalLookupByRequestRequestSchema,
19993
- ApprovalConsumeRequestSchema,
19994
- ApprovalRevokeRequestSchema,
19995
- ApprovalListRequestSchema,
19996
- ApprovalRecordRequestSchema,
19997
- ApprovalConsumeRecordRequestSchema
19998
- ]);
19999
- var VaultEntrySchema = exports_external.union([
20000
- exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
20001
- exports_external.object({ kind: exports_external.literal("binary"), value: exports_external.string() }),
20002
- exports_external.object({
20003
- kind: exports_external.literal("files"),
20004
- files: exports_external.record(exports_external.string(), exports_external.object({
20005
- encoding: exports_external.enum(["utf8", "base64"]),
20006
- value: exports_external.string()
20007
- }))
20008
- })
20009
- ]);
20010
- var ErrorCode = exports_external.enum([
20011
- "LOCKED",
20012
- "DENIED",
20013
- "UNKNOWN_KEY",
20014
- "BAD_REQUEST",
20015
- "INTERNAL",
20016
- "AUDIT_UNAVAILABLE"
20017
- ]);
20018
- var OkEntryResponseSchema = exports_external.object({
20019
- ok: exports_external.literal(true),
20020
- entry: VaultEntrySchema
20021
- });
20022
- var OkKeysResponseSchema = exports_external.object({
20023
- ok: exports_external.literal(true),
20024
- keys: exports_external.array(exports_external.string())
20025
- });
20026
- var BrokerStatus = exports_external.object({
20027
- unlocked: exports_external.boolean(),
20028
- keyCount: exports_external.number().int().nonnegative(),
20029
- uptimeSec: exports_external.number().nonnegative()
20030
- });
20031
- var OkStatusResponseSchema = exports_external.object({
20032
- ok: exports_external.literal(true),
20033
- status: BrokerStatus
20034
- });
20035
- var OkLockResponseSchema = exports_external.object({
20036
- ok: exports_external.literal(true),
20037
- locked: exports_external.literal(true)
20038
- });
20039
- var OkPutResponseSchema = exports_external.object({
20040
- ok: exports_external.literal(true),
20041
- put: exports_external.literal(true),
20042
- key: exports_external.string()
20043
- });
20044
- var OkMintGrantResponseSchema = exports_external.object({
20045
- ok: exports_external.literal(true),
20046
- token: exports_external.string(),
20047
- id: exports_external.string(),
20048
- expires_at: exports_external.number().nullable()
20049
- });
20050
- var GrantMetaSchema = exports_external.object({
20051
- id: exports_external.string(),
20052
- agent_slug: exports_external.string(),
20053
- key_allow: exports_external.array(exports_external.string()),
20054
- write_allow: exports_external.array(exports_external.string()).default([]),
20055
- expires_at: exports_external.number().nullable(),
20056
- created_at: exports_external.number(),
20057
- description: exports_external.string().nullable()
20058
- });
20059
- var OkListGrantsResponseSchema = exports_external.object({
20060
- ok: exports_external.literal(true),
20061
- grants: exports_external.array(GrantMetaSchema)
20062
- });
20063
- var OkRevokeGrantResponseSchema = exports_external.object({
20064
- ok: exports_external.literal(true),
20065
- revoked: exports_external.boolean()
20066
- });
20067
- var OkApprovalRequestResponseSchema = exports_external.discriminatedUnion("state", [
20068
- exports_external.object({
20069
- ok: exports_external.literal(true),
20070
- kind: exports_external.literal("approval_request"),
20071
- state: exports_external.literal("pending"),
20072
- request_id: exports_external.string(),
20073
- expires_at: exports_external.number()
20074
- }),
20075
- exports_external.object({
20076
- ok: exports_external.literal(true),
20077
- kind: exports_external.literal("approval_request"),
20078
- state: exports_external.literal("rate_limited"),
20079
- retry_after_ms: exports_external.number()
20080
- })
20081
- ]);
20082
- var ApprovalDecisionMetaSchema = exports_external.object({
20083
- id: exports_external.string(),
20084
- agent_unit: exports_external.string(),
20085
- scope: exports_external.string(),
20086
- action: exports_external.string(),
20087
- decision: ApprovalDecisionModeSchema,
20088
- granted_at: exports_external.number(),
20089
- granted_by_user_id: exports_external.number(),
20090
- ttl_expires_at: exports_external.number().nullable(),
20091
- last_used_at: exports_external.number().nullable(),
20092
- revoked_at: exports_external.number().nullable(),
20093
- revoke_reason: exports_external.string().nullable(),
20094
- origin: exports_external.enum(["agent", "operator"]).optional()
20095
- });
20096
- var OkApprovalLookupResponseSchema = exports_external.object({
20097
- ok: exports_external.literal(true),
20098
- state: exports_external.enum(["granted", "denied", "pending", "expired", "drift_revoked", "no_decision"]),
20099
- decision: ApprovalDecisionMetaSchema.nullable().optional()
20100
- });
20101
- var OkApprovalConsumeResponseSchema = exports_external.object({
20102
- ok: exports_external.literal(true),
20103
- consumed: exports_external.boolean(),
20104
- agent_unit: exports_external.string().optional(),
20105
- scope: exports_external.string().optional(),
20106
- action: exports_external.string().optional(),
20107
- why: exports_external.string().nullable().optional()
20108
- });
20109
- var OkApprovalRevokeResponseSchema = exports_external.object({
20110
- ok: exports_external.literal(true),
20111
- revoked: exports_external.boolean()
20112
- });
20113
- var OkApprovalListResponseSchema = exports_external.object({
20114
- ok: exports_external.literal(true),
20115
- decisions: exports_external.array(ApprovalDecisionMetaSchema)
20116
- });
20117
- var OkApprovalRecordResponseSchema = exports_external.object({
20118
- ok: exports_external.literal(true),
20119
- decision_id: exports_external.string()
20120
- });
20121
- var OkApprovalConsumeRecordResponseSchema = exports_external.object({
20122
- ok: exports_external.literal(true),
20123
- consumed: exports_external.boolean(),
20124
- decision_id: exports_external.string().optional(),
20125
- agent_unit: exports_external.string().optional(),
20126
- scope: exports_external.string().optional(),
20127
- action: exports_external.string().optional(),
20128
- why: exports_external.string().nullable().optional()
20129
- });
20130
- var ErrorResponseSchema = exports_external.object({
20131
- ok: exports_external.literal(false),
20132
- code: ErrorCode,
20133
- msg: exports_external.string()
20134
- });
20135
- var ResponseSchema = exports_external.union([
20136
- OkEntryResponseSchema,
20137
- OkKeysResponseSchema,
20138
- OkStatusResponseSchema,
20139
- OkLockResponseSchema,
20140
- OkPreflightAccessResponseSchema,
20141
- OkPutResponseSchema,
20142
- OkMintGrantResponseSchema,
20143
- OkListGrantsResponseSchema,
20144
- OkRevokeGrantResponseSchema,
20145
- OkApprovalRequestResponseSchema,
20146
- OkApprovalLookupResponseSchema,
20147
- OkApprovalConsumeResponseSchema,
20148
- OkApprovalRevokeResponseSchema,
20149
- OkApprovalListResponseSchema,
20150
- OkApprovalRecordResponseSchema,
20151
- OkApprovalConsumeRecordResponseSchema,
20152
- ErrorResponseSchema
20153
- ]);
20154
- function decodeRequest(line) {
20155
- if (Buffer.byteLength(line, "utf8") > MAX_FRAME_BYTES) {
20156
- throw new RangeError(`Request frame too large (${Buffer.byteLength(line, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
20157
- }
20158
- const obj = JSON.parse(line);
20159
- return RequestSchema.parse(obj);
20160
- }
20161
- function encodeResponse(resp) {
20162
- const json = JSON.stringify(resp);
20163
- if (Buffer.byteLength(json, "utf8") > MAX_FRAME_BYTES) {
20164
- throw new Error(`Response frame too large (${Buffer.byteLength(json, "utf8")} bytes; max ${MAX_FRAME_BYTES})`);
20165
- }
20166
- return json + `
19974
+ ` + ` claim is true only at the instant it was said. Concretely, never produce a
19975
+ ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
19976
+ ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
19977
+ ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
19978
+ ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
19979
+ ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
19980
+ ` + ` cannot date it, drop it. An undated one is recalled forever as though it
19981
+ ` + ` were still true, which is worse than not remembering it at all.
19982
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
19983
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
19984
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
19985
+ ` + `
19986
+ ` + `Write each fact so it stands alone: name the thing, the number, and the date. A
19987
+ ` + `sentence that only makes sense while reading this transcript is not durable.
19988
+ ` + `
19989
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
19990
+ ` + `nothing durable remains, return an empty facts list.
20167
19991
  `;
20168
- }
20169
- function errorResponse(code, msg) {
20170
- return { ok: false, code, msg };
20171
- }
20172
- function entryResponse(entry) {
20173
- const stripped = stripWireFields(entry);
20174
- return { ok: true, entry: stripped };
20175
- }
20176
- function stripWireFields(entry) {
20177
- if (entry.kind === "string" || entry.kind === "binary") {
20178
- return {
20179
- kind: entry.kind,
20180
- value: entry.value,
20181
- ...entry.format !== undefined ? { format: entry.format } : {}
20182
- };
19992
+ var SUPERSEDED_RETAIN_MISSIONS = [
19993
+ "Extract technical decisions, architectural choices, user preferences, project context, and people/tool relationships. Ignore routine greetings and transient operational details.",
19994
+ "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.",
19995
+ "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise, " + "including in-flight workflow/process narration (a sub-task started, " + "paused, or is still running) — only retain the outcome once a task " + "actually completes or a decision is made.",
19996
+ "Extract durable facts that will still be true and useful weeks from now: " + "user preferences and standing rules, ongoing projects and recurring " + "commitments, technical and architectural decisions with their rationale, " + "and people/tool relationships. A preference revealed by a request is " + "durable — record the preference (what the user likes, wants, or always " + `does), not the request itself.
19997
+
19998
+ ` + `NEVER extract:
19999
+ ` + "- Agent tool-use traces or narration of what the assistant did (e.g. " + `"the assistant used X to query Y", "ran a search", "sent the message").
20000
+ ` + "- In-flight workflow/process narration (a sub-task started, paused, or is " + "still running) — retain the outcome only once the task completes or a " + `decision is made.
20001
+ ` + `- Operation, request, batch or session IDs, UUIDs, hashes, or error codes.
20002
+ ` + "- Hindsight's own errors, retries, backlogs, or internal state — the " + `memory system's self-reports are not memories.
20003
+ ` + `- Restatements of the user's current request or the task in progress.
20004
+ ` + "- Transient state (unread counts, build status, what is running right now) " + "unless the fact is explicitly dated, in which case record it as a dated " + `observation.
20005
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
20006
+
20007
+ ` + "If a candidate fact matches an exclusion, drop it rather than rewording " + "it. If nothing durable remains, return an empty facts list.",
20008
+ `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.
20009
+ ` + `
20010
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
20011
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
20012
+ ` + `the location where some output was written? If yes, drop it — it is transcript
20013
+ ` + `exhaust, not memory.
20014
+ ` + `
20015
+ ` + `NEVER extract:
20016
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
20017
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
20018
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
20019
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
20020
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
20021
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
20022
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
20023
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
20024
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
20025
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
20026
+ ` + ` running) — retain the outcome only once the task completes or a decision is made.
20027
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
20028
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
20029
+ ` + ` reset assistant state").
20030
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
20031
+ ` + ` system's self-reports are not memories.
20032
+ ` + `- Restatements of the user's current request or the task in progress.
20033
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
20034
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
20035
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
20036
+ ` + `
20037
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
20038
+ ` + "nothing durable remains, return an empty facts list.",
20039
+ `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.
20040
+ ` + `
20041
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
20042
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
20043
+ ` + `the location where some output was written? If yes, drop it — it is transcript
20044
+ ` + `exhaust, not memory.
20045
+ ` + `
20046
+ ` + `NEVER extract:
20047
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
20048
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
20049
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
20050
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
20051
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
20052
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
20053
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
20054
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
20055
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
20056
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
20057
+ ` + ` running) — retain the outcome only once the task completes or a decision is made.
20058
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
20059
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
20060
+ ` + ` reset assistant state").
20061
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
20062
+ ` + ` system's self-reports are not memories.
20063
+ ` + `- Restatements of the user's current request or the task in progress.
20064
+ ` + `- Volatile state written as a timeless assertion. A version, count, size,
20065
+ ` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
20066
+ ` + ` claim is true only at the instant it was said. Concretely, never produce a
20067
+ ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
20068
+ ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
20069
+ ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
20070
+ ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
20071
+ ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
20072
+ ` + ` cannot date it, drop it. An undated one is recalled forever as though it
20073
+ ` + ` were still true, which is worse than not remembering it at all.
20074
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
20075
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
20076
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
20077
+ ` + `
20078
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
20079
+ ` + "nothing durable remains, return an empty facts list.",
20080
+ `Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
20081
+ ` + `
20082
+ ` + `DURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:
20083
+ ` + `- PREFERENCE — what the user likes, wants, or always does; a standing rule or correction.
20084
+ ` + `- DECISION — a settled choice that changes how future work is done, including a choice NOT to do something. A decision about the mechanics of the CURRENT task (which worker to dispatch, which branch to rebase, which PR to merge now, what to do next) is process narration, not a durable decision — drop it unless it establishes a standing rule or permanently changes a system.
20085
+ ` + `- FINDING — a root cause, a measurement, or verified behaviour of a system. Include the number.
20086
+ ` + `- OUTCOME — a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.
20087
+ ` + `- RELATIONSHIP — who a person is, what a project or tool is, and how they connect.
20088
+ ` + `If a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.
20089
+ ` + `
20090
+ ` + `A preference revealed by a request is durable — record the preference (what the user likes, wants, or always does), not the request itself.
20091
+ ` + `
20092
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
20093
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
20094
+ ` + `the location where some output was written? If yes, drop it — it is transcript
20095
+ ` + `exhaust, not memory.
20096
+ ` + `
20097
+ ` + `NEVER extract:
20098
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
20099
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
20100
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
20101
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
20102
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
20103
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
20104
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
20105
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
20106
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
20107
+ ` + `- The act of delegating, dispatching, spawning, launching, steering or merging
20108
+ ` + ` work — including when it succeeded. "X was dispatched and completed" is the
20109
+ ` + ` session describing itself. Record only what the work LEARNED or CHANGED.
20110
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
20111
+ ` + ` running) — retain the outcome only once the task completes or a decision is made.
20112
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
20113
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
20114
+ ` + ` reset assistant state").
20115
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state — the memory
20116
+ ` + ` system's self-reports are not memories.
20117
+ ` + `- Restatements of the user's current request or the task in progress.
20118
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
20119
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
20120
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
20121
+ ` + `
20122
+ ` + `Write each fact so it stands alone: name the thing, the number, and the date. A
20123
+ ` + `sentence that only makes sense while reading this transcript is not durable.
20124
+ ` + `
20125
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
20126
+ ` + `nothing durable remains, return an empty facts list.
20127
+ `
20128
+ ];
20129
+ var DEFAULT_OBSERVATIONS_MISSION = `Synthesise durable, standing knowledge about the people, projects, and systems this agent works with: preferences and standing rules, roles and relationships, skills and recurring patterns, technical and operational decisions with their rationale, and the state of long-running work once it lands.
20130
+ ` + `
20131
+ ` + `The test is durability, not notability: an observation must still be worth reading weeks from now. A single dated event belongs in an observation only when it establishes or changes a standing fact.
20132
+ ` + `
20133
+ ` + `Do NOT synthesise observations from:
20134
+ ` + `- Transcript exhaust — tool calls and their results, file paths, temp or scratchpad directories, where some output was written, or narration of what an assistant did.
20135
+ ` + `- Identifiers with no standing meaning: session, agent, request, batch or command IDs, UUIDs, hashes, error codes.
20136
+ ` + `- In-flight process narration — a task started, paused, or still running. Record the outcome once it lands, not the running state.
20137
+ ` + `- The memory system's own errors, retries, backlogs, or internal state.
20138
+ ` + `- Transient state (what is running right now, unread counts, build status) unless the fact is explicitly dated, in which case record it as dated.
20139
+ ` + `
20140
+ ` + "If the new facts contain nothing durable, record nothing rather than synthesising a weak observation.";
20141
+ var SUPERSEDED_OBSERVATIONS_MISSIONS = [
20142
+ "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context — how habits, setbacks, and encouragement connect over time."
20143
+ ];
20144
+ var PROFILE_MEMORY_DEFAULTS = {
20145
+ "health-coach": {
20146
+ disposition: { skepticism: 2, literalism: 2, empathy: 5 },
20147
+ observations_mission: `You consolidate the memory of a health and fitness coach working with one person. This bank records how that person actually lives and trains.
20148
+ ` + `
20149
+ ` + `Synthesise into durable observations:
20150
+ ` + `- Goals, targets, and the plan currently in force, with the reasoning behind each.
20151
+ ` + `- Training, nutrition, sleep, and alcohol patterns as they hold over weeks — what the person reliably does, not what they did once.
20152
+ ` + `- Constraints that shape the plan: injuries, medical guidance, schedule, equipment, foods and sessions they refuse.
20153
+ ` + `- Motivations, and what actually helps or backfires when they slip.
20154
+ ` + `- Trends in the numbers: direction and range over time, not any single reading.
20155
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
20156
+ ` + `
20157
+ ` + `A single day's log, weight reading, or session is evidence, not an observation. Record one only when it establishes or changes a standing pattern, target, or constraint — then fold it into the observation for that pattern and say what changed and roughly when.
20158
+ ` + `
20159
+ ` + `Granularity: one observation per habit, target, constraint, or trend. Aggregate repeated daily evidence into the observation for that pattern rather than creating one per day, and keep training, nutrition, and sleep as separate observations.
20160
+ ` + `
20161
+ ` + "Word observations as the person's own pattern and framing, never as a verdict on them."
20162
+ },
20163
+ "executive-assistant": {
20164
+ disposition: { skepticism: 4, literalism: 4, empathy: 3 },
20165
+ observations_mission: `You consolidate the memory of an executive assistant working for one person. This bank records that person's commitments, people, and standing arrangements.
20166
+ ` + `
20167
+ ` + `Synthesise into durable observations:
20168
+ ` + `- Standing rules and preferences: how they want things scheduled, written, and filed, and when they want to be interrupted.
20169
+ ` + `- People and organisations, and the relationship: who they are, what they are involved in, how to reach them.
20170
+ ` + `- Recurring commitments and routines, and the constraints around them.
20171
+ ` + `- Obligations and their state: what was promised to whom, the deadline, and what is still outstanding.
20172
+ ` + `- Decisions made and decisions deferred, with the reasoning and the trade accepted.
20173
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
20174
+ ` + `
20175
+ ` + `Live commitment state IS durable knowledge here, not ephemeral chatter. An outstanding obligation, a travel window, an unanswered request, a "currently X" arrangement — these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
20176
+ ` + `
20177
+ ` + `Granularity: one observation per person, arrangement, or obligation. Aggregate repeated mentions into that observation rather than creating siblings; never merge two different people or two different commitments into one.
20178
+ ` + `
20179
+ ` + "Do not synthesise from transcript exhaust: tool calls and their results, message or event identifiers, or narration of what the assistant did."
20180
+ },
20181
+ coding: {
20182
+ disposition: { skepticism: 4, literalism: 5, empathy: 2 },
20183
+ observations_mission: `You consolidate the memory of a software-engineering agent. This bank records real work on real codebases.
20184
+ ` + `
20185
+ ` + `Synthesise into durable observations:
20186
+ ` + `- Architecture and design decisions, each with its rationale and the trade accepted.
20187
+ ` + `- Root causes, with the evidence chain, and negative results — what was ruled out matters as much as what was found.
20188
+ ` + `- How the repository works: build, test, and lint commands, conventions, CI gates, and where things live.
20189
+ ` + `- Outcomes of code work: issue and PR numbers, what changed, whether it merged, what review found.
20190
+ ` + `- The user's standing rules, preferences, and corrections to this agent's behaviour.
20191
+ ` + `- Corrections to earlier beliefs, recorded explicitly as corrections.
20192
+ ` + `
20193
+ ` + `Repository and service state IS durable knowledge here, not ephemeral chatter. Versions, a failing gate, an open PR, a measured number, a "currently X" claim — these are precisely what this agent must recall later. Keep them, and embed the date inside the observation text so a later reader can judge staleness. Do not drop them as ephemeral.
20194
+ ` + `
20195
+ ` + `Granularity: one observation per distinct decision, cause, convention, or work item. Aggregate repeated evidence about the same one into that observation rather than creating siblings; never merge two separate decisions into a single summary.
20196
+ ` + `
20197
+ ` + "Do not synthesise from transcript exhaust: tool calls and their results, scratch paths, session or request identifiers, or narration of what the agent did. Prefer specific and falsifiable — naming the file, the number, the commit, or the decision beats summarising the topic."
20183
20198
  }
20184
- return {
20185
- kind: "files",
20186
- files: entry.files
20187
- };
20188
- }
20199
+ };
20200
+ // src/agents/reconcile-default-skills.ts
20201
+ var warnedMissingPool = new Set;
20202
+ var warnedMissingDefault = new Set;
20189
20203
 
20190
- // src/vault/broker/client.ts
20191
- var LEGACY_SOCKET_PATH = join3(homedir3(), ".switchroom", "vault-broker.sock");
20192
- var OPERATOR_SOCKET_PATH = join3(homedir3(), ".switchroom", "broker-operator", "sock");
20204
+ // src/telegram/state.ts
20205
+ init_paths();
20193
20206
 
20194
- // src/vault/resolver.ts
20195
- var materializedDirs = new Set;
20207
+ // src/agents/scaffold.ts
20208
+ init_paths();
20209
+ init_loader();
20210
+ init_resolver();
20211
+ init_vault();
20196
20212
 
20197
20213
  // src/setup/onboarding.ts
20198
20214
  init_paths();
@@ -20375,11 +20391,13 @@ function errText(err) {
20375
20391
  }
20376
20392
 
20377
20393
  // src/vault/broker/server.ts
20394
+ init_vault();
20378
20395
  import { dirname as dirname7, resolve as resolve9, basename as basename5 } from "node:path";
20379
20396
  import * as os3 from "node:os";
20380
20397
  import * as path5 from "node:path";
20381
20398
 
20382
20399
  // src/vault/migrate-layout.ts
20400
+ init_vault();
20383
20401
  import {
20384
20402
  copyFileSync as copyFileSync2,
20385
20403
  chmodSync as chmodSync2,
@@ -20610,6 +20628,10 @@ function readAutoUnlockFile(filePath) {
20610
20628
  }
20611
20629
  var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
20612
20630
 
20631
+ // src/vault/broker/server.ts
20632
+ init_peercred();
20633
+ init_protocol();
20634
+
20613
20635
  // src/vault/broker/audit-log.ts
20614
20636
  import * as fs3 from "node:fs";
20615
20637
  import * as os2 from "node:os";