switchroom 0.18.14 → 0.18.15

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.
@@ -12179,7 +12179,7 @@ var init_overlay_schema = __esm(() => {
12179
12179
  });
12180
12180
 
12181
12181
  // src/config/overlay-loader.ts
12182
- import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
12182
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
12183
12183
  import { basename, resolve as resolve2 } from "node:path";
12184
12184
  function deriveOverlayTitle(raw, fileName) {
12185
12185
  const titleFromComment = raw.match(/^#[^\S\n]*name:[^\S\n]*(\S.*?)[^\S\n]*$/m)?.[1];
@@ -12243,7 +12243,7 @@ function applyAgentOverlays(config) {
12243
12243
  const merged = [...agentCfg.schedule ?? []];
12244
12244
  for (const file of files) {
12245
12245
  try {
12246
- const raw = readFileSync(file, "utf-8");
12246
+ const raw = readFileSync2(file, "utf-8");
12247
12247
  const parsed = import_yaml.parse(raw);
12248
12248
  const doc = OverlayDocSchema.parse(parsed);
12249
12249
  const title = deriveOverlayTitle(raw, basename(file));
@@ -12284,7 +12284,7 @@ function applyAgentOverlays(config) {
12284
12284
  const seen = new Set(merged);
12285
12285
  for (const file of skillFiles) {
12286
12286
  try {
12287
- const raw = readFileSync(file, "utf-8");
12287
+ const raw = readFileSync2(file, "utf-8");
12288
12288
  const parsed = import_yaml.parse(raw);
12289
12289
  const doc = OverlayDocSchema.parse(parsed);
12290
12290
  for (const skillName of doc.skills ?? []) {
@@ -12359,7 +12359,7 @@ __export(exports_loader, {
12359
12359
  findConfigFile: () => findConfigFile,
12360
12360
  ConfigError: () => ConfigError
12361
12361
  });
12362
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
12362
+ import { readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
12363
12363
  import { homedir } from "node:os";
12364
12364
  import { resolve as resolve3 } from "node:path";
12365
12365
  function formatZodErrors(error) {
@@ -12439,7 +12439,7 @@ function loadConfig(configPath) {
12439
12439
  }
12440
12440
  let raw;
12441
12441
  try {
12442
- raw = readFileSync2(filePath, "utf-8");
12442
+ raw = readFileSync3(filePath, "utf-8");
12443
12443
  } catch (err) {
12444
12444
  throw new ConfigError(`Failed to read config file: ${filePath}`, [
12445
12445
  ` ${err.message}`
@@ -18096,249 +18096,597 @@ import * as net from "node:net";
18096
18096
  import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync7 } from "node:fs";
18097
18097
 
18098
18098
  // src/agents/compose.ts
18099
- import { createHash } from "node:crypto";
18100
18099
  init_schema();
18101
18100
 
18102
18101
  // src/agents/scaffold.ts
18103
18102
  import { join as join4, resolve as resolve6 } from "node:path";
18104
18103
  init_atomic();
18105
- init_schema();
18106
-
18107
- // src/config/users.ts
18108
- init_merge();
18109
-
18110
- // src/agents/scaffold.ts
18111
- init_merge();
18112
-
18113
- // src/config/timezone.ts
18114
- var CONTAINER_DEFAULT_UTC_ZONES = new Set([
18115
- "UTC",
18116
- "Etc/UTC",
18117
- "Etc/Universal",
18118
- "Universal"
18119
- ]);
18120
18104
 
18121
- // src/cli/agent-config.ts
18122
- import { join } from "node:path";
18123
- import { homedir as homedir2 } from "node:os";
18124
-
18125
- // src/cli/helpers.ts
18126
- init_loader();
18105
+ // src/agents/agent-uid.ts
18106
+ import { createHash } from "node:crypto";
18127
18107
 
18128
- // src/cli/agent-config.ts
18129
- init_loader();
18130
- init_merge();
18108
+ // src/vault/broker/peercred.ts
18109
+ import { execFileSync } from "node:child_process";
18110
+ import { readFileSync, readlinkSync, fstatSync } from "node:fs";
18131
18111
 
18132
- // src/config/google-workspace-acl.ts
18133
- function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
18134
- if (!agentGoogleAccount)
18135
- return false;
18136
- const account = agentGoogleAccount.trim().toLowerCase();
18137
- if (account.length === 0)
18138
- return false;
18139
- const acctEntry = googleAccounts?.[account];
18140
- if (!acctEntry)
18141
- return false;
18142
- const enabledFor = acctEntry.enabled_for ?? [];
18143
- return enabledFor.includes(agentName);
18144
- }
18145
- function vaultRefKey(value) {
18146
- if (typeof value !== "string" || !value.startsWith("vault:"))
18112
+ // src/vault/broker/peercred-ffi.ts
18113
+ function getPeerCred(fd) {
18114
+ if (process.platform !== "linux")
18115
+ return null;
18116
+ try {
18117
+ const ffi = __require("bun:ffi");
18118
+ const { dlopen, FFIType, ptr } = ffi;
18119
+ const SOL_SOCKET = 1;
18120
+ const SO_PEERCRED = 17;
18121
+ const UCRED_SIZE = 12;
18122
+ const cache = getPeerCred;
18123
+ const lib = cache._lib ?? (() => {
18124
+ const candidates = ["libc.so.6", "libc.so"];
18125
+ const symbolSpec = {
18126
+ getsockopt: {
18127
+ args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
18128
+ returns: FFIType.i32
18129
+ }
18130
+ };
18131
+ const errors2 = [];
18132
+ for (const name of candidates) {
18133
+ try {
18134
+ const opened = dlopen(name, symbolSpec);
18135
+ cache._lib = opened;
18136
+ return opened;
18137
+ } catch (e) {
18138
+ errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
18139
+ }
18140
+ }
18141
+ process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
18142
+ `);
18143
+ throw new Error("no libc candidate could be opened");
18144
+ })();
18145
+ const credBuf = new ArrayBuffer(UCRED_SIZE);
18146
+ const lenBuf = new Uint32Array(1);
18147
+ lenBuf[0] = UCRED_SIZE;
18148
+ const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
18149
+ if (rc !== 0)
18150
+ return null;
18151
+ if (lenBuf[0] !== UCRED_SIZE)
18152
+ return null;
18153
+ const view = new DataView(credBuf);
18154
+ return {
18155
+ pid: view.getInt32(0, true),
18156
+ uid: view.getInt32(4, true),
18157
+ gid: view.getInt32(8, true)
18158
+ };
18159
+ } catch {
18147
18160
  return null;
18148
- const key = value.slice("vault:".length).split("#")[0];
18149
- return key.length > 0 ? key : null;
18150
- }
18151
- function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
18152
- if (!agentName || !key)
18153
- return false;
18154
- const agentConfig = config.agents?.[agentName];
18155
- if (!agentConfig)
18156
- return false;
18157
- if (agentConfig.mcp_servers?.["gdrive"] === false) {
18158
- return false;
18159
- }
18160
- const account = agentConfig.google_workspace?.account;
18161
- if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
18162
- return false;
18163
18161
  }
18164
- const gw = config.google_workspace;
18165
- if (!gw)
18166
- return false;
18167
- for (const ref of [gw.google_client_id, gw.google_client_secret]) {
18168
- if (vaultRefKey(ref) === key)
18169
- return true;
18162
+ }
18163
+
18164
+ // src/broker-common/peercred-path.ts
18165
+ function matchSocketName(socketPath, patterns, maxNameLen) {
18166
+ if (typeof socketPath !== "string" || socketPath.length === 0)
18167
+ return null;
18168
+ let name = null;
18169
+ for (const re of patterns) {
18170
+ const m = socketPath.match(re);
18171
+ if (m && typeof m[1] === "string") {
18172
+ name = m[1];
18173
+ break;
18174
+ }
18170
18175
  }
18171
- return false;
18176
+ if (name === null || name.length === 0)
18177
+ return null;
18178
+ if (maxNameLen !== undefined && name.length > maxNameLen)
18179
+ return null;
18180
+ return name;
18181
+ }
18182
+ function parseSocketIdentity(socketPath, spec) {
18183
+ const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
18184
+ if (name === null)
18185
+ return null;
18186
+ const operatorName = spec.operatorName ?? "operator";
18187
+ if (name === operatorName)
18188
+ return { kind: "operator" };
18189
+ if (spec.reservedNames.has(name))
18190
+ return null;
18191
+ return { kind: "agent", name };
18172
18192
  }
18173
18193
 
18174
- // src/vault/broker/acl.ts
18175
- var WEBKITE_VAULT_KEYS = new Set([
18176
- "webkite/cloudflare-account-id",
18177
- "webkite/cloudflare-api-token",
18178
- "webkite/firecrawl-api-key"
18179
- ]);
18180
- function isWebkiteCredentialKeyForAgent(agentConfig, key) {
18181
- if (!WEBKITE_VAULT_KEYS.has(key))
18182
- return false;
18183
- if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
18184
- return false;
18185
- return true;
18194
+ // src/vault/broker/peercred.ts
18195
+ var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
18196
+ var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
18197
+ var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
18198
+ function socketPathToIdentity(socketPath) {
18199
+ return parseSocketIdentity(socketPath, {
18200
+ patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
18201
+ reservedNames: RESERVED_AGENT_NAMES
18202
+ });
18186
18203
  }
18187
- function checkEntryScope(scope, agentSlug) {
18188
- if (scope === undefined || scope === null) {
18189
- return { allow: true };
18204
+ function socketPathToAgent(socketPath) {
18205
+ const identity = socketPathToIdentity(socketPath);
18206
+ return identity?.kind === "agent" ? identity.name : null;
18207
+ }
18208
+ function isReservedAgentName(name) {
18209
+ return RESERVED_AGENT_NAMES.has(name);
18210
+ }
18211
+ function unlockSocketFor(dataSocketPath) {
18212
+ if (dataSocketPath.endsWith("/sock")) {
18213
+ return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
18190
18214
  }
18191
- const deny = scope.deny ?? [];
18192
- const allow = scope.allow ?? [];
18193
- if (agentSlug !== null && deny.includes(agentSlug)) {
18194
- return {
18195
- allow: false,
18196
- reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
18197
- };
18215
+ return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
18216
+ }
18217
+ function parseSsRows(output) {
18218
+ const rows = [];
18219
+ const lines = output.split(`
18220
+ `);
18221
+ for (const line of lines) {
18222
+ if (!line.trim() || line.startsWith("Netid"))
18223
+ continue;
18224
+ const tokens = line.split(/\s+/).filter((t) => t.length > 0);
18225
+ if (tokens.length < 8)
18226
+ continue;
18227
+ const localAddr = tokens[4];
18228
+ const localInode = tokens[5];
18229
+ const peerAddr = tokens[6];
18230
+ const peerInode = tokens[7];
18231
+ const usersToken = tokens.slice(8).join(" ");
18232
+ const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
18233
+ const pid = m ? parseInt(m[1], 10) : null;
18234
+ rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
18198
18235
  }
18199
- if (allow.length > 0) {
18200
- if (agentSlug === null || !allow.includes(agentSlug)) {
18201
- return {
18202
- allow: false,
18203
- 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)`
18204
- };
18236
+ return rows;
18237
+ }
18238
+ function findClientPids(rows, socketPath) {
18239
+ const pids = [];
18240
+ for (const serverRow of rows) {
18241
+ if (serverRow.localAddr !== socketPath)
18242
+ continue;
18243
+ for (const clientRow of rows) {
18244
+ if (clientRow.localAddr !== "*")
18245
+ continue;
18246
+ if (clientRow.localInode !== serverRow.peerInode)
18247
+ continue;
18248
+ if (clientRow.pid === null)
18249
+ continue;
18250
+ pids.push(clientRow.pid);
18251
+ break;
18205
18252
  }
18206
18253
  }
18207
- return { allow: true };
18254
+ return pids;
18208
18255
  }
18209
- function checkAclByAgent(config, agentName, key) {
18210
- if (!agentName) {
18211
- return { allow: false, reason: "agent name unresolved" };
18212
- }
18213
- const agentConfig = config.agents?.[agentName];
18214
- if (!agentConfig) {
18215
- return { allow: false, reason: `agent '${agentName}' not found in config` };
18216
- }
18217
- const googleSlot = parseGoogleAccountSlotKey(key);
18218
- if (googleSlot !== null) {
18219
- return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
18256
+ function findClientPidByServerInode(rows, socketPath, serverInode) {
18257
+ const serverInodeStr = String(serverInode);
18258
+ for (const serverRow of rows) {
18259
+ if (serverRow.localAddr !== socketPath)
18260
+ continue;
18261
+ if (serverRow.localInode !== serverInodeStr)
18262
+ continue;
18263
+ for (const clientRow of rows) {
18264
+ if (clientRow.localAddr !== "*")
18265
+ continue;
18266
+ if (clientRow.localInode !== serverRow.peerInode)
18267
+ continue;
18268
+ if (clientRow.pid === null)
18269
+ continue;
18270
+ return clientRow.pid;
18271
+ }
18272
+ return null;
18220
18273
  }
18221
- if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
18222
- return { allow: true };
18274
+ return null;
18275
+ }
18276
+ function readUid(pid) {
18277
+ try {
18278
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
18279
+ const m = status.match(/^Uid:\s+(\d+)/m);
18280
+ if (!m)
18281
+ return null;
18282
+ return parseInt(m[1], 10);
18283
+ } catch {
18284
+ return null;
18223
18285
  }
18224
- if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
18225
- return { allow: true };
18286
+ }
18287
+ function readExe(pid) {
18288
+ try {
18289
+ return readlinkSync(`/proc/${pid}/exe`);
18290
+ } catch {
18291
+ return null;
18226
18292
  }
18227
- const agentBot = agentConfig.bot_token;
18228
- const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
18229
- if (typeof botRef === "string" && botRef.startsWith("vault:")) {
18230
- const botKey = botRef.slice("vault:".length).split("#")[0];
18231
- if (botKey.length > 0 && botKey === key) {
18232
- return { allow: true };
18293
+ }
18294
+ function readSystemdUnit(pid) {
18295
+ try {
18296
+ const content = readFileSync(`/proc/${pid}/cgroup`, "utf8");
18297
+ const lines = content.split(`
18298
+ `);
18299
+ for (const line of lines) {
18300
+ if (!line.trim())
18301
+ continue;
18302
+ const parts = line.split(":");
18303
+ if (parts.length < 3)
18304
+ continue;
18305
+ const controller = parts[1];
18306
+ const isV2 = parts[0] === "0" && controller === "";
18307
+ const isV1Systemd = controller === "name=systemd";
18308
+ if (!isV2 && !isV1Systemd)
18309
+ continue;
18310
+ const cgroupPath = parts.slice(2).join(":");
18311
+ const segments = cgroupPath.split("/");
18312
+ const lastSegment = segments[segments.length - 1];
18313
+ if (!lastSegment)
18314
+ continue;
18315
+ if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
18316
+ return lastSegment;
18317
+ }
18233
18318
  }
18319
+ return null;
18320
+ } catch {
18321
+ return null;
18234
18322
  }
18235
- const cfgWithProfiles = config;
18236
- const profileName = agentConfig.extends;
18237
- const profileMcp = profileName != null && profileName.length > 0 ? cfgWithProfiles.profiles?.[profileName]?.mcp_servers ?? {} : {};
18238
- const effectiveMcp = {
18239
- ...cfgWithProfiles.defaults?.mcp_servers ?? {},
18240
- ...profileMcp,
18241
- ...agentConfig.mcp_servers ?? {}
18242
- };
18243
- for (const mcpEntry of Object.values(effectiveMcp)) {
18244
- if (!mcpEntry || typeof mcpEntry !== "object")
18245
- continue;
18246
- const declared = mcpEntry.secrets;
18247
- if (Array.isArray(declared) && declared.includes(key)) {
18248
- return { allow: true };
18249
- }
18323
+ }
18324
+ function verifySystemdUnit(unitName, runner) {
18325
+ let raw;
18326
+ try {
18327
+ const out = runner("systemctl", [
18328
+ "--user",
18329
+ "show",
18330
+ unitName,
18331
+ "--property=LoadState,ActiveState"
18332
+ ], { timeout: 500, encoding: "utf8" });
18333
+ raw = typeof out === "string" ? out : out.toString("utf8");
18334
+ } catch {
18335
+ return false;
18250
18336
  }
18251
- const cfgSecrets = config;
18252
- const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
18253
- const standingSecrets = [
18254
- ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
18255
- ...Array.isArray(profileSecrets) ? profileSecrets : [],
18256
- ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
18257
- ];
18258
- if (standingSecrets.includes(key)) {
18259
- return { allow: true };
18337
+ const props = {};
18338
+ for (const line of raw.split(`
18339
+ `)) {
18340
+ const m = line.match(/^([A-Za-z]+)=(.*)$/);
18341
+ if (m)
18342
+ props[m[1]] = m[2];
18260
18343
  }
18261
- const schedule = agentConfig.schedule ?? [];
18262
- if (schedule.length === 0) {
18263
- return {
18264
- allow: false,
18265
- 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`
18266
- };
18344
+ if (props.LoadState !== "loaded")
18345
+ return false;
18346
+ if (props.ActiveState !== "active" && props.ActiveState !== "activating") {
18347
+ return false;
18267
18348
  }
18268
- for (const entry of schedule) {
18269
- const allowed = entry?.secrets ?? [];
18270
- if (allowed.includes(key)) {
18271
- return { allow: true };
18272
- }
18349
+ return true;
18350
+ }
18351
+ function readFdInode(fd) {
18352
+ try {
18353
+ const stat = fstatSync(fd);
18354
+ return stat.ino;
18355
+ } catch {
18356
+ return null;
18273
18357
  }
18274
- return {
18275
- allow: false,
18276
- reason: `key '${key}' not in ACL for agent '${agentName}'`
18277
- };
18278
18358
  }
18279
- function parseGoogleAccountSlotKey(key) {
18280
- const match = key.match(/^google:([^:]+):([a-z_]+)$/);
18281
- if (!match)
18359
+ function fdFromSocket(socket) {
18360
+ const handle = socket._handle;
18361
+ if (!handle || typeof handle.fd !== "number" || handle.fd < 0)
18282
18362
  return null;
18283
- return { account: match[1], field: match[2] };
18363
+ return handle.fd;
18284
18364
  }
18285
- function checkGoogleAccountAcl(config, agentName, account, key) {
18286
- const accounts = config.google_accounts ?? {};
18287
- const accountKey = account.toLowerCase();
18288
- const accountEntry = accounts[accountKey] ?? accounts[account];
18289
- if (!accountEntry) {
18290
- return {
18291
- allow: false,
18292
- reason: `google_accounts['${account}'] not configured (key '${key}')`
18293
- };
18365
+ function identify(socketPath, socket, execFileSyncOverride) {
18366
+ if (process.platform !== "linux") {
18367
+ return null;
18294
18368
  }
18295
- const enabled = accountEntry.enabled_for ?? [];
18296
- if (enabled.length === 0) {
18297
- return {
18298
- allow: false,
18299
- reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
18300
- };
18369
+ const runner = execFileSyncOverride ?? execFileSync;
18370
+ let pid = null;
18371
+ if (socket !== undefined) {
18372
+ const fd = fdFromSocket(socket);
18373
+ if (fd !== null) {
18374
+ const cred = getPeerCred(fd);
18375
+ if (cred !== null)
18376
+ pid = cred.pid;
18377
+ }
18301
18378
  }
18302
- if (!enabled.includes(agentName)) {
18303
- return {
18304
- allow: false,
18305
- reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
18306
- };
18379
+ if (pid === null) {
18380
+ let ssOutput;
18381
+ try {
18382
+ const raw = runner("ss", ["-xpn"], {
18383
+ timeout: 200,
18384
+ encoding: "utf8"
18385
+ });
18386
+ ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
18387
+ } catch {
18388
+ return null;
18389
+ }
18390
+ const rows = parseSsRows(ssOutput);
18391
+ let serverInode = null;
18392
+ if (socket !== undefined) {
18393
+ const fd = fdFromSocket(socket);
18394
+ if (fd !== null)
18395
+ serverInode = readFdInode(fd);
18396
+ }
18397
+ if (serverInode !== null) {
18398
+ pid = findClientPidByServerInode(rows, socketPath, serverInode);
18399
+ } else {
18400
+ const clientPids = findClientPids(rows, socketPath);
18401
+ if (clientPids.length === 0)
18402
+ return null;
18403
+ if (clientPids.length > 1) {
18404
+ 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.)
18405
+ `);
18406
+ }
18407
+ pid = clientPids[0];
18408
+ }
18307
18409
  }
18308
- return { allow: true };
18410
+ if (pid === null)
18411
+ return null;
18412
+ const uid = readUid(pid);
18413
+ if (uid === null) {
18414
+ return null;
18415
+ }
18416
+ const brokerUid = typeof process.getuid === "function" ? process.getuid() : null;
18417
+ if (brokerUid !== null && uid !== brokerUid) {
18418
+ process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
18419
+ `);
18420
+ return null;
18421
+ }
18422
+ const exe = readExe(pid);
18423
+ if (exe === null) {
18424
+ return null;
18425
+ }
18426
+ const cgroupClaim = readSystemdUnit(pid);
18427
+ let systemdUnit = null;
18428
+ if (cgroupClaim !== null) {
18429
+ if (verifySystemdUnit(cgroupClaim, runner)) {
18430
+ systemdUnit = cgroupClaim;
18431
+ } else {
18432
+ process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
18433
+ `);
18434
+ }
18435
+ }
18436
+ return { uid, pid, exe, systemdUnit };
18309
18437
  }
18310
18438
 
18311
- // src/scheduler/cron-introspect.ts
18312
- init_overlay_loader();
18313
-
18314
- // src/cli/agent-config.ts
18315
- var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
18316
-
18317
- // src/agents/profiles.ts
18318
- var import_handlebars = __toESM(require_lib(), 1);
18319
- import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
18320
- import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
18321
- var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
18322
- import_handlebars.default.registerHelper("json", (value) => {
18323
- return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
18324
- });
18325
- import_handlebars.default.registerHelper("isNumber", (value) => {
18326
- return typeof value === "number" && Number.isFinite(value);
18327
- });
18328
- var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
18329
- var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
18330
- for (const name of SHARED_FRAGMENTS) {
18331
- const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
18332
- if (existsSync4(fragPath)) {
18333
- import_handlebars.default.registerPartial(name, readFileSync3(fragPath, "utf-8"));
18439
+ // src/agents/agent-uid.ts
18440
+ var AGENT_UID_MIN = 10001;
18441
+ var AGENT_UID_MAX = 10999;
18442
+ function allocateAgentUid(name) {
18443
+ if (isReservedAgentName(name)) {
18444
+ 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.`);
18334
18445
  }
18446
+ const hash = createHash("sha256").update(name).digest();
18447
+ const u32 = hash.readUInt32BE(0);
18448
+ const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
18449
+ return AGENT_UID_MIN + u32 % range;
18335
18450
  }
18336
18451
 
18337
- // src/agents/pane-lock.ts
18338
- var tails = new Map;
18452
+ // src/agents/scaffold.ts
18453
+ init_schema();
18339
18454
 
18340
- // src/agents/inject.ts
18341
- var INJECT_COMMANDS = new Map([
18455
+ // src/config/users.ts
18456
+ init_merge();
18457
+
18458
+ // src/agents/scaffold.ts
18459
+ init_merge();
18460
+
18461
+ // src/config/timezone.ts
18462
+ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
18463
+ "UTC",
18464
+ "Etc/UTC",
18465
+ "Etc/Universal",
18466
+ "Universal"
18467
+ ]);
18468
+
18469
+ // src/cli/agent-config.ts
18470
+ import { join } from "node:path";
18471
+ import { homedir as homedir2 } from "node:os";
18472
+
18473
+ // src/cli/helpers.ts
18474
+ init_loader();
18475
+
18476
+ // src/cli/agent-config.ts
18477
+ init_loader();
18478
+ init_merge();
18479
+
18480
+ // src/config/google-workspace-acl.ts
18481
+ function shouldEmitGdriveMcp(agentName, agentGoogleAccount, googleAccounts) {
18482
+ if (!agentGoogleAccount)
18483
+ return false;
18484
+ const account = agentGoogleAccount.trim().toLowerCase();
18485
+ if (account.length === 0)
18486
+ return false;
18487
+ const acctEntry = googleAccounts?.[account];
18488
+ if (!acctEntry)
18489
+ return false;
18490
+ const enabledFor = acctEntry.enabled_for ?? [];
18491
+ return enabledFor.includes(agentName);
18492
+ }
18493
+ function vaultRefKey(value) {
18494
+ if (typeof value !== "string" || !value.startsWith("vault:"))
18495
+ return null;
18496
+ const key = value.slice("vault:".length).split("#")[0];
18497
+ return key.length > 0 ? key : null;
18498
+ }
18499
+ function isGoogleClientCredentialKeyForAgent(config, agentName, key) {
18500
+ if (!agentName || !key)
18501
+ return false;
18502
+ const agentConfig = config.agents?.[agentName];
18503
+ if (!agentConfig)
18504
+ return false;
18505
+ if (agentConfig.mcp_servers?.["gdrive"] === false) {
18506
+ return false;
18507
+ }
18508
+ const account = agentConfig.google_workspace?.account;
18509
+ if (!shouldEmitGdriveMcp(agentName, account, config.google_accounts)) {
18510
+ return false;
18511
+ }
18512
+ const gw = config.google_workspace;
18513
+ if (!gw)
18514
+ return false;
18515
+ for (const ref of [gw.google_client_id, gw.google_client_secret]) {
18516
+ if (vaultRefKey(ref) === key)
18517
+ return true;
18518
+ }
18519
+ return false;
18520
+ }
18521
+
18522
+ // src/vault/broker/acl.ts
18523
+ var WEBKITE_VAULT_KEYS = new Set([
18524
+ "webkite/cloudflare-account-id",
18525
+ "webkite/cloudflare-api-token",
18526
+ "webkite/firecrawl-api-key"
18527
+ ]);
18528
+ function isWebkiteCredentialKeyForAgent(agentConfig, key) {
18529
+ if (!WEBKITE_VAULT_KEYS.has(key))
18530
+ return false;
18531
+ if ((agentConfig?.mcp_servers ?? {})["webkite"] === false)
18532
+ return false;
18533
+ return true;
18534
+ }
18535
+ function checkEntryScope(scope, agentSlug) {
18536
+ if (scope === undefined || scope === null) {
18537
+ return { allow: true };
18538
+ }
18539
+ const deny = scope.deny ?? [];
18540
+ const allow = scope.allow ?? [];
18541
+ if (agentSlug !== null && deny.includes(agentSlug)) {
18542
+ return {
18543
+ allow: false,
18544
+ reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
18545
+ };
18546
+ }
18547
+ if (allow.length > 0) {
18548
+ if (agentSlug === null || !allow.includes(agentSlug)) {
18549
+ return {
18550
+ allow: false,
18551
+ 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)`
18552
+ };
18553
+ }
18554
+ }
18555
+ return { allow: true };
18556
+ }
18557
+ function checkAclByAgent(config, agentName, key) {
18558
+ if (!agentName) {
18559
+ return { allow: false, reason: "agent name unresolved" };
18560
+ }
18561
+ const agentConfig = config.agents?.[agentName];
18562
+ if (!agentConfig) {
18563
+ return { allow: false, reason: `agent '${agentName}' not found in config` };
18564
+ }
18565
+ const googleSlot = parseGoogleAccountSlotKey(key);
18566
+ if (googleSlot !== null) {
18567
+ return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
18568
+ }
18569
+ if (isGoogleClientCredentialKeyForAgent(config, agentName, key)) {
18570
+ return { allow: true };
18571
+ }
18572
+ if (isWebkiteCredentialKeyForAgent(agentConfig, key)) {
18573
+ return { allow: true };
18574
+ }
18575
+ const agentBot = agentConfig.bot_token;
18576
+ const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
18577
+ if (typeof botRef === "string" && botRef.startsWith("vault:")) {
18578
+ const botKey = botRef.slice("vault:".length).split("#")[0];
18579
+ if (botKey.length > 0 && botKey === key) {
18580
+ return { allow: true };
18581
+ }
18582
+ }
18583
+ const cfgWithProfiles = config;
18584
+ const profileName = agentConfig.extends;
18585
+ const profileMcp = profileName != null && profileName.length > 0 ? cfgWithProfiles.profiles?.[profileName]?.mcp_servers ?? {} : {};
18586
+ const effectiveMcp = {
18587
+ ...cfgWithProfiles.defaults?.mcp_servers ?? {},
18588
+ ...profileMcp,
18589
+ ...agentConfig.mcp_servers ?? {}
18590
+ };
18591
+ for (const mcpEntry of Object.values(effectiveMcp)) {
18592
+ if (!mcpEntry || typeof mcpEntry !== "object")
18593
+ continue;
18594
+ const declared = mcpEntry.secrets;
18595
+ if (Array.isArray(declared) && declared.includes(key)) {
18596
+ return { allow: true };
18597
+ }
18598
+ }
18599
+ const cfgSecrets = config;
18600
+ const profileSecrets = profileName != null && profileName.length > 0 ? cfgSecrets.profiles?.[profileName]?.secrets : undefined;
18601
+ const standingSecrets = [
18602
+ ...Array.isArray(cfgSecrets.defaults?.secrets) ? cfgSecrets.defaults.secrets : [],
18603
+ ...Array.isArray(profileSecrets) ? profileSecrets : [],
18604
+ ...Array.isArray(agentConfig.secrets) ? agentConfig.secrets : []
18605
+ ];
18606
+ if (standingSecrets.includes(key)) {
18607
+ return { allow: true };
18608
+ }
18609
+ const schedule = agentConfig.schedule ?? [];
18610
+ if (schedule.length === 0) {
18611
+ return {
18612
+ allow: false,
18613
+ 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`
18614
+ };
18615
+ }
18616
+ for (const entry of schedule) {
18617
+ const allowed = entry?.secrets ?? [];
18618
+ if (allowed.includes(key)) {
18619
+ return { allow: true };
18620
+ }
18621
+ }
18622
+ return {
18623
+ allow: false,
18624
+ reason: `key '${key}' not in ACL for agent '${agentName}'`
18625
+ };
18626
+ }
18627
+ function parseGoogleAccountSlotKey(key) {
18628
+ const match = key.match(/^google:([^:]+):([a-z_]+)$/);
18629
+ if (!match)
18630
+ return null;
18631
+ return { account: match[1], field: match[2] };
18632
+ }
18633
+ function checkGoogleAccountAcl(config, agentName, account, key) {
18634
+ const accounts = config.google_accounts ?? {};
18635
+ const accountKey = account.toLowerCase();
18636
+ const accountEntry = accounts[accountKey] ?? accounts[account];
18637
+ if (!accountEntry) {
18638
+ return {
18639
+ allow: false,
18640
+ reason: `google_accounts['${account}'] not configured (key '${key}')`
18641
+ };
18642
+ }
18643
+ const enabled = accountEntry.enabled_for ?? [];
18644
+ if (enabled.length === 0) {
18645
+ return {
18646
+ allow: false,
18647
+ reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
18648
+ };
18649
+ }
18650
+ if (!enabled.includes(agentName)) {
18651
+ return {
18652
+ allow: false,
18653
+ reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
18654
+ };
18655
+ }
18656
+ return { allow: true };
18657
+ }
18658
+
18659
+ // src/scheduler/cron-introspect.ts
18660
+ init_overlay_loader();
18661
+
18662
+ // src/cli/agent-config.ts
18663
+ var AUDIT_ROOT = join(homedir2(), ".switchroom", "audit");
18664
+
18665
+ // src/agents/profiles.ts
18666
+ var import_handlebars = __toESM(require_lib(), 1);
18667
+ import { readFileSync as readFileSync4, writeFileSync, existsSync as existsSync4, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync, realpathSync } from "node:fs";
18668
+ import { resolve as resolve4, join as join2, sep as pathSep } from "node:path";
18669
+ var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
18670
+ import_handlebars.default.registerHelper("json", (value) => {
18671
+ return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
18672
+ });
18673
+ import_handlebars.default.registerHelper("isNumber", (value) => {
18674
+ return typeof value === "number" && Number.isFinite(value);
18675
+ });
18676
+ var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
18677
+ var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
18678
+ for (const name of SHARED_FRAGMENTS) {
18679
+ const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
18680
+ if (existsSync4(fragPath)) {
18681
+ import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
18682
+ }
18683
+ }
18684
+
18685
+ // src/agents/pane-lock.ts
18686
+ var tails = new Map;
18687
+
18688
+ // src/agents/inject.ts
18689
+ var INJECT_COMMANDS = new Map([
18342
18690
  ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
18343
18691
  ["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
18344
18692
  ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
@@ -18430,7 +18778,7 @@ init_loader();
18430
18778
  // src/vault/vault.ts
18431
18779
  import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
18432
18780
  import {
18433
- readFileSync as readFileSync5,
18781
+ readFileSync as readFileSync6,
18434
18782
  writeSync as writeSync3,
18435
18783
  existsSync as existsSync6,
18436
18784
  renameSync as renameSync2,
@@ -18452,7 +18800,7 @@ import {
18452
18800
  writeSync as writeSync2,
18453
18801
  fsyncSync as fsyncSync2,
18454
18802
  unlinkSync,
18455
- readFileSync as readFileSync4,
18803
+ readFileSync as readFileSync5,
18456
18804
  readdirSync as readdirSync3,
18457
18805
  rmdirSync,
18458
18806
  statSync as statSync3,
@@ -18464,7 +18812,7 @@ function lockPathFor(vaultPath) {
18464
18812
  }
18465
18813
  function readLockHolder(lockPath) {
18466
18814
  try {
18467
- const raw = readFileSync4(lockPath, "utf8");
18815
+ const raw = readFileSync5(lockPath, "utf8");
18468
18816
  const lines = raw.split(`
18469
18817
  `);
18470
18818
  const pid = Number.parseInt(lines[0] ?? "", 10);
@@ -18516,8 +18864,8 @@ function pidStartTimeMs(pid) {
18516
18864
  if (process.platform !== "linux")
18517
18865
  return null;
18518
18866
  try {
18519
- const statLine = readFileSync4(`/proc/${pid}/stat`, "utf8");
18520
- const procStat = readFileSync4("/proc/stat", "utf8");
18867
+ const statLine = readFileSync5(`/proc/${pid}/stat`, "utf8");
18868
+ const procStat = readFileSync5("/proc/stat", "utf8");
18521
18869
  return parseProcStartTimeMs(statLine, procStat, Date.now());
18522
18870
  } catch {
18523
18871
  return null;
@@ -18702,479 +19050,146 @@ function atomicWriteFileSync(path, data, mode) {
18702
19050
  try {
18703
19051
  if (existsSync6(tmp))
18704
19052
  unlinkSync2(tmp);
18705
- } catch {}
18706
- throw err;
18707
- }
18708
- }
18709
-
18710
- class VaultError extends Error {
18711
- cause;
18712
- constructor(message, options) {
18713
- super(message);
18714
- this.name = "VaultError";
18715
- if (options?.cause !== undefined)
18716
- this.cause = options.cause;
18717
- }
18718
- }
18719
- var LEGACY_SCRYPT_N = 16384;
18720
- function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
18721
- return scryptSync(passphrase, salt, 32, {
18722
- N: params.N,
18723
- r: params.r,
18724
- p: params.p,
18725
- maxmem: SCRYPT_MAXMEM
18726
- });
18727
- }
18728
- function encrypt(key, plaintext) {
18729
- const iv = randomBytes(12);
18730
- const cipher = createCipheriv("aes-256-gcm", key, iv);
18731
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
18732
- const tag = cipher.getAuthTag();
18733
- return {
18734
- iv: iv.toString("hex"),
18735
- data: encrypted.toString("hex"),
18736
- tag: tag.toString("hex")
18737
- };
18738
- }
18739
- function decrypt(key, iv, data, tag) {
18740
- const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
18741
- decipher.setAuthTag(Buffer.from(tag, "hex"));
18742
- const decrypted = Buffer.concat([
18743
- decipher.update(Buffer.from(data, "hex")),
18744
- decipher.final()
18745
- ]);
18746
- return decrypted.toString("utf8");
18747
- }
18748
- function normalizeEntry(raw) {
18749
- if (typeof raw === "string") {
18750
- return { kind: "string", value: raw };
18751
- }
18752
- return raw;
18753
- }
18754
- function normalizeSecrets(raw) {
18755
- const out = {};
18756
- for (const [k, v] of Object.entries(raw)) {
18757
- out[k] = normalizeEntry(v);
18758
- }
18759
- return out;
18760
- }
18761
- function openVault(passphrase, vaultPath) {
18762
- if (!existsSync6(vaultPath)) {
18763
- throw new VaultError(`Vault file not found: ${vaultPath}`);
18764
- }
18765
- let vaultFile;
18766
- try {
18767
- vaultFile = JSON.parse(readFileSync5(vaultPath, "utf8"));
18768
- } catch {
18769
- throw new VaultError(`Failed to read vault file: ${vaultPath}`);
18770
- }
18771
- const salt = Buffer.from(vaultFile.salt, "hex");
18772
- const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
18773
- const key = deriveKey(passphrase, salt, kdfParams);
18774
- let plaintext;
18775
- try {
18776
- plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
18777
- } catch {
18778
- throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
18779
- }
18780
- let vaultData;
18781
- try {
18782
- vaultData = JSON.parse(plaintext);
18783
- } catch {
18784
- throw new VaultError("Vault data is corrupted");
18785
- }
18786
- return normalizeSecrets(vaultData.secrets ?? {});
18787
- }
18788
- function saveVault(passphrase, vaultPath, secrets) {
18789
- if (!existsSync6(vaultPath)) {
18790
- throw new VaultError(`Vault file not found: ${vaultPath}`);
18791
- }
18792
- let releaseLock = null;
18793
- try {
18794
- releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
18795
- } catch (err) {
18796
- if (err instanceof VaultBusyError) {
18797
- throw new VaultError(err.message, { cause: err });
18798
- }
18799
- throw err;
18800
- }
18801
- try {
18802
- let vaultFile;
18803
- try {
18804
- vaultFile = JSON.parse(readFileSync5(vaultPath, "utf8"));
18805
- } catch {
18806
- throw new VaultError(`Failed to read vault file: ${vaultPath}`);
18807
- }
18808
- const salt = Buffer.from(vaultFile.salt, "hex");
18809
- const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
18810
- vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
18811
- const vaultData = { secrets };
18812
- const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
18813
- vaultFile.iv = iv;
18814
- vaultFile.data = data;
18815
- vaultFile.tag = tag;
18816
- atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
18817
- } finally {
18818
- if (releaseLock !== null) {
18819
- try {
18820
- releaseLock();
18821
- } catch {}
18822
- }
18823
- }
18824
- }
18825
- function acquireVaultLock(vaultPath) {
18826
- try {
18827
- return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
18828
- } catch (err) {
18829
- if (err instanceof VaultBusyError) {
18830
- throw new VaultError(err.message, { cause: err });
18831
- }
18832
- throw err;
18833
- }
18834
- }
18835
-
18836
- // src/vault/resolver.ts
18837
- init_loader();
18838
-
18839
- // src/vault/broker/client.ts
18840
- import { homedir as homedir3 } from "node:os";
18841
- import { join as join3 } from "node:path";
18842
-
18843
- // src/vault/broker/protocol.ts
18844
- init_zod();
18845
-
18846
- // src/vault/broker/peercred.ts
18847
- import { execFileSync } from "node:child_process";
18848
- import { readFileSync as readFileSync6, readlinkSync, fstatSync } from "node:fs";
18849
-
18850
- // src/vault/broker/peercred-ffi.ts
18851
- function getPeerCred(fd) {
18852
- if (process.platform !== "linux")
18853
- return null;
18854
- try {
18855
- const ffi = __require("bun:ffi");
18856
- const { dlopen, FFIType, ptr } = ffi;
18857
- const SOL_SOCKET = 1;
18858
- const SO_PEERCRED = 17;
18859
- const UCRED_SIZE = 12;
18860
- const cache = getPeerCred;
18861
- const lib = cache._lib ?? (() => {
18862
- const candidates = ["libc.so.6", "libc.so"];
18863
- const symbolSpec = {
18864
- getsockopt: {
18865
- args: [FFIType.i32, FFIType.i32, FFIType.i32, FFIType.ptr, FFIType.ptr],
18866
- returns: FFIType.i32
18867
- }
18868
- };
18869
- const errors2 = [];
18870
- for (const name of candidates) {
18871
- try {
18872
- const opened = dlopen(name, symbolSpec);
18873
- cache._lib = opened;
18874
- return opened;
18875
- } catch (e) {
18876
- errors2.push(`${name}: ${e instanceof Error ? e.message : String(e)}`);
18877
- }
18878
- }
18879
- process.stderr.write(`[vault-broker] peercred-ffi: dlopen failed for all libc candidates ` + `(${errors2.join("; ")}); falling back to ss-parsing.
18880
- `);
18881
- throw new Error("no libc candidate could be opened");
18882
- })();
18883
- const credBuf = new ArrayBuffer(UCRED_SIZE);
18884
- const lenBuf = new Uint32Array(1);
18885
- lenBuf[0] = UCRED_SIZE;
18886
- const rc = lib.symbols.getsockopt(fd, SOL_SOCKET, SO_PEERCRED, ptr(credBuf), ptr(lenBuf.buffer));
18887
- if (rc !== 0)
18888
- return null;
18889
- if (lenBuf[0] !== UCRED_SIZE)
18890
- return null;
18891
- const view = new DataView(credBuf);
18892
- return {
18893
- pid: view.getInt32(0, true),
18894
- uid: view.getInt32(4, true),
18895
- gid: view.getInt32(8, true)
18896
- };
18897
- } catch {
18898
- return null;
18899
- }
18900
- }
18901
-
18902
- // src/broker-common/peercred-path.ts
18903
- function matchSocketName(socketPath, patterns, maxNameLen) {
18904
- if (typeof socketPath !== "string" || socketPath.length === 0)
18905
- return null;
18906
- let name = null;
18907
- for (const re of patterns) {
18908
- const m = socketPath.match(re);
18909
- if (m && typeof m[1] === "string") {
18910
- name = m[1];
18911
- break;
18912
- }
18913
- }
18914
- if (name === null || name.length === 0)
18915
- return null;
18916
- if (maxNameLen !== undefined && name.length > maxNameLen)
18917
- return null;
18918
- return name;
18919
- }
18920
- function parseSocketIdentity(socketPath, spec) {
18921
- const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
18922
- if (name === null)
18923
- return null;
18924
- const operatorName = spec.operatorName ?? "operator";
18925
- if (name === operatorName)
18926
- return { kind: "operator" };
18927
- if (spec.reservedNames.has(name))
18928
- return null;
18929
- return { kind: "agent", name };
18930
- }
18931
-
18932
- // src/vault/broker/peercred.ts
18933
- var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
18934
- var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
18935
- var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
18936
- function socketPathToIdentity(socketPath) {
18937
- return parseSocketIdentity(socketPath, {
18938
- patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
18939
- reservedNames: RESERVED_AGENT_NAMES
18940
- });
18941
- }
18942
- function socketPathToAgent(socketPath) {
18943
- const identity = socketPathToIdentity(socketPath);
18944
- return identity?.kind === "agent" ? identity.name : null;
18945
- }
18946
- function isReservedAgentName(name) {
18947
- return RESERVED_AGENT_NAMES.has(name);
18948
- }
18949
- function unlockSocketFor(dataSocketPath) {
18950
- if (dataSocketPath.endsWith("/sock")) {
18951
- return dataSocketPath.slice(0, -"/sock".length) + "/unlock";
18952
- }
18953
- return dataSocketPath.replace(/\.sock$/, ".unlock.sock");
18954
- }
18955
- function parseSsRows(output) {
18956
- const rows = [];
18957
- const lines = output.split(`
18958
- `);
18959
- for (const line of lines) {
18960
- if (!line.trim() || line.startsWith("Netid"))
18961
- continue;
18962
- const tokens = line.split(/\s+/).filter((t) => t.length > 0);
18963
- if (tokens.length < 8)
18964
- continue;
18965
- const localAddr = tokens[4];
18966
- const localInode = tokens[5];
18967
- const peerAddr = tokens[6];
18968
- const peerInode = tokens[7];
18969
- const usersToken = tokens.slice(8).join(" ");
18970
- const m = usersToken.match(/users:\(\(".*?",pid=(\d+),fd=\d+\)\)/);
18971
- const pid = m ? parseInt(m[1], 10) : null;
18972
- rows.push({ localAddr, localInode, peerAddr, peerInode, pid });
18973
- }
18974
- return rows;
18975
- }
18976
- function findClientPids(rows, socketPath) {
18977
- const pids = [];
18978
- for (const serverRow of rows) {
18979
- if (serverRow.localAddr !== socketPath)
18980
- continue;
18981
- for (const clientRow of rows) {
18982
- if (clientRow.localAddr !== "*")
18983
- continue;
18984
- if (clientRow.localInode !== serverRow.peerInode)
18985
- continue;
18986
- if (clientRow.pid === null)
18987
- continue;
18988
- pids.push(clientRow.pid);
18989
- break;
18990
- }
18991
- }
18992
- return pids;
18993
- }
18994
- function findClientPidByServerInode(rows, socketPath, serverInode) {
18995
- const serverInodeStr = String(serverInode);
18996
- for (const serverRow of rows) {
18997
- if (serverRow.localAddr !== socketPath)
18998
- continue;
18999
- if (serverRow.localInode !== serverInodeStr)
19000
- continue;
19001
- for (const clientRow of rows) {
19002
- if (clientRow.localAddr !== "*")
19003
- continue;
19004
- if (clientRow.localInode !== serverRow.peerInode)
19005
- continue;
19006
- if (clientRow.pid === null)
19007
- continue;
19008
- return clientRow.pid;
19009
- }
19010
- return null;
19053
+ } catch {}
19054
+ throw err;
19011
19055
  }
19012
- return null;
19013
19056
  }
19014
- function readUid(pid) {
19015
- try {
19016
- const status = readFileSync6(`/proc/${pid}/status`, "utf8");
19017
- const m = status.match(/^Uid:\s+(\d+)/m);
19018
- if (!m)
19019
- return null;
19020
- return parseInt(m[1], 10);
19021
- } catch {
19022
- return null;
19057
+
19058
+ class VaultError extends Error {
19059
+ cause;
19060
+ constructor(message, options) {
19061
+ super(message);
19062
+ this.name = "VaultError";
19063
+ if (options?.cause !== undefined)
19064
+ this.cause = options.cause;
19023
19065
  }
19024
19066
  }
19025
- function readExe(pid) {
19026
- try {
19027
- return readlinkSync(`/proc/${pid}/exe`);
19028
- } catch {
19029
- return null;
19067
+ var LEGACY_SCRYPT_N = 16384;
19068
+ function deriveKey(passphrase, salt, params = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }) {
19069
+ return scryptSync(passphrase, salt, 32, {
19070
+ N: params.N,
19071
+ r: params.r,
19072
+ p: params.p,
19073
+ maxmem: SCRYPT_MAXMEM
19074
+ });
19075
+ }
19076
+ function encrypt(key, plaintext) {
19077
+ const iv = randomBytes(12);
19078
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
19079
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
19080
+ const tag = cipher.getAuthTag();
19081
+ return {
19082
+ iv: iv.toString("hex"),
19083
+ data: encrypted.toString("hex"),
19084
+ tag: tag.toString("hex")
19085
+ };
19086
+ }
19087
+ function decrypt(key, iv, data, tag) {
19088
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(iv, "hex"));
19089
+ decipher.setAuthTag(Buffer.from(tag, "hex"));
19090
+ const decrypted = Buffer.concat([
19091
+ decipher.update(Buffer.from(data, "hex")),
19092
+ decipher.final()
19093
+ ]);
19094
+ return decrypted.toString("utf8");
19095
+ }
19096
+ function normalizeEntry(raw) {
19097
+ if (typeof raw === "string") {
19098
+ return { kind: "string", value: raw };
19030
19099
  }
19100
+ return raw;
19031
19101
  }
19032
- function readSystemdUnit(pid) {
19033
- try {
19034
- const content = readFileSync6(`/proc/${pid}/cgroup`, "utf8");
19035
- const lines = content.split(`
19036
- `);
19037
- for (const line of lines) {
19038
- if (!line.trim())
19039
- continue;
19040
- const parts = line.split(":");
19041
- if (parts.length < 3)
19042
- continue;
19043
- const controller = parts[1];
19044
- const isV2 = parts[0] === "0" && controller === "";
19045
- const isV1Systemd = controller === "name=systemd";
19046
- if (!isV2 && !isV1Systemd)
19047
- continue;
19048
- const cgroupPath = parts.slice(2).join(":");
19049
- const segments = cgroupPath.split("/");
19050
- const lastSegment = segments[segments.length - 1];
19051
- if (!lastSegment)
19052
- continue;
19053
- if (/^switchroom-[a-zA-Z0-9_-]+(-cron-\d+)?\.service$/.test(lastSegment)) {
19054
- return lastSegment;
19055
- }
19056
- }
19057
- return null;
19058
- } catch {
19059
- return null;
19102
+ function normalizeSecrets(raw) {
19103
+ const out = {};
19104
+ for (const [k, v] of Object.entries(raw)) {
19105
+ out[k] = normalizeEntry(v);
19060
19106
  }
19107
+ return out;
19061
19108
  }
19062
- function verifySystemdUnit(unitName, runner) {
19063
- let raw;
19109
+ function openVault(passphrase, vaultPath) {
19110
+ if (!existsSync6(vaultPath)) {
19111
+ throw new VaultError(`Vault file not found: ${vaultPath}`);
19112
+ }
19113
+ let vaultFile;
19064
19114
  try {
19065
- const out = runner("systemctl", [
19066
- "--user",
19067
- "show",
19068
- unitName,
19069
- "--property=LoadState,ActiveState"
19070
- ], { timeout: 500, encoding: "utf8" });
19071
- raw = typeof out === "string" ? out : out.toString("utf8");
19115
+ vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
19072
19116
  } catch {
19073
- return false;
19074
- }
19075
- const props = {};
19076
- for (const line of raw.split(`
19077
- `)) {
19078
- const m = line.match(/^([A-Za-z]+)=(.*)$/);
19079
- if (m)
19080
- props[m[1]] = m[2];
19117
+ throw new VaultError(`Failed to read vault file: ${vaultPath}`);
19081
19118
  }
19082
- if (props.LoadState !== "loaded")
19083
- return false;
19084
- if (props.ActiveState !== "active" && props.ActiveState !== "activating") {
19085
- return false;
19119
+ const salt = Buffer.from(vaultFile.salt, "hex");
19120
+ const kdfParams = vaultFile.kdf ?? { N: LEGACY_SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
19121
+ const key = deriveKey(passphrase, salt, kdfParams);
19122
+ let plaintext;
19123
+ try {
19124
+ plaintext = decrypt(key, vaultFile.iv, vaultFile.data, vaultFile.tag);
19125
+ } catch {
19126
+ throw new VaultError("Failed to decrypt vault. Wrong passphrase?");
19086
19127
  }
19087
- return true;
19088
- }
19089
- function readFdInode(fd) {
19128
+ let vaultData;
19090
19129
  try {
19091
- const stat = fstatSync(fd);
19092
- return stat.ino;
19130
+ vaultData = JSON.parse(plaintext);
19093
19131
  } catch {
19094
- return null;
19132
+ throw new VaultError("Vault data is corrupted");
19095
19133
  }
19134
+ return normalizeSecrets(vaultData.secrets ?? {});
19096
19135
  }
19097
- function fdFromSocket(socket) {
19098
- const handle = socket._handle;
19099
- if (!handle || typeof handle.fd !== "number" || handle.fd < 0)
19100
- return null;
19101
- return handle.fd;
19102
- }
19103
- function identify(socketPath, socket, execFileSyncOverride) {
19104
- if (process.platform !== "linux") {
19105
- return null;
19136
+ function saveVault(passphrase, vaultPath, secrets) {
19137
+ if (!existsSync6(vaultPath)) {
19138
+ throw new VaultError(`Vault file not found: ${vaultPath}`);
19106
19139
  }
19107
- const runner = execFileSyncOverride ?? execFileSync;
19108
- let pid = null;
19109
- if (socket !== undefined) {
19110
- const fd = fdFromSocket(socket);
19111
- if (fd !== null) {
19112
- const cred = getPeerCred(fd);
19113
- if (cred !== null)
19114
- pid = cred.pid;
19140
+ let releaseLock = null;
19141
+ try {
19142
+ releaseLock = acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
19143
+ } catch (err) {
19144
+ if (err instanceof VaultBusyError) {
19145
+ throw new VaultError(err.message, { cause: err });
19115
19146
  }
19147
+ throw err;
19116
19148
  }
19117
- if (pid === null) {
19118
- let ssOutput;
19149
+ try {
19150
+ let vaultFile;
19119
19151
  try {
19120
- const raw = runner("ss", ["-xpn"], {
19121
- timeout: 200,
19122
- encoding: "utf8"
19123
- });
19124
- ssOutput = typeof raw === "string" ? raw : raw.toString("utf8");
19152
+ vaultFile = JSON.parse(readFileSync6(vaultPath, "utf8"));
19125
19153
  } catch {
19126
- return null;
19127
- }
19128
- const rows = parseSsRows(ssOutput);
19129
- let serverInode = null;
19130
- if (socket !== undefined) {
19131
- const fd = fdFromSocket(socket);
19132
- if (fd !== null)
19133
- serverInode = readFdInode(fd);
19154
+ throw new VaultError(`Failed to read vault file: ${vaultPath}`);
19134
19155
  }
19135
- if (serverInode !== null) {
19136
- pid = findClientPidByServerInode(rows, socketPath, serverInode);
19137
- } else {
19138
- const clientPids = findClientPids(rows, socketPath);
19139
- if (clientPids.length === 0)
19140
- return null;
19141
- if (clientPids.length > 1) {
19142
- 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.)
19143
- `);
19144
- }
19145
- pid = clientPids[0];
19156
+ const salt = Buffer.from(vaultFile.salt, "hex");
19157
+ const key = deriveKey(passphrase, salt, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
19158
+ vaultFile.kdf = { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P };
19159
+ const vaultData = { secrets };
19160
+ const { iv, data, tag } = encrypt(key, JSON.stringify(vaultData));
19161
+ vaultFile.iv = iv;
19162
+ vaultFile.data = data;
19163
+ vaultFile.tag = tag;
19164
+ atomicWriteFileSync(vaultPath, JSON.stringify(vaultFile, null, 2), 384);
19165
+ } finally {
19166
+ if (releaseLock !== null) {
19167
+ try {
19168
+ releaseLock();
19169
+ } catch {}
19146
19170
  }
19147
19171
  }
19148
- if (pid === null)
19149
- return null;
19150
- const uid = readUid(pid);
19151
- if (uid === null) {
19152
- return null;
19153
- }
19154
- const brokerUid = typeof process.getuid === "function" ? process.getuid() : null;
19155
- if (brokerUid !== null && uid !== brokerUid) {
19156
- process.stderr.write(`[vault-broker] peercred: UID mismatch — caller uid=${uid}, broker uid=${brokerUid}; denying
19157
- `);
19158
- return null;
19159
- }
19160
- const exe = readExe(pid);
19161
- if (exe === null) {
19162
- return null;
19163
- }
19164
- const cgroupClaim = readSystemdUnit(pid);
19165
- let systemdUnit = null;
19166
- if (cgroupClaim !== null) {
19167
- if (verifySystemdUnit(cgroupClaim, runner)) {
19168
- systemdUnit = cgroupClaim;
19169
- } else {
19170
- process.stderr.write(`[vault-broker] peercred: cgroup claims unit=${cgroupClaim} but systemd-user does not report it as loaded+running; treating caller as unidentified
19171
- `);
19172
+ }
19173
+ function acquireVaultLock(vaultPath) {
19174
+ try {
19175
+ return acquireLock(vaultPath, { budgetMs: SAVE_VAULT_LOCK_RETRY_MS }).release;
19176
+ } catch (err) {
19177
+ if (err instanceof VaultBusyError) {
19178
+ throw new VaultError(err.message, { cause: err });
19172
19179
  }
19180
+ throw err;
19173
19181
  }
19174
- return { uid, pid, exe, systemdUnit };
19175
19182
  }
19176
19183
 
19184
+ // src/vault/resolver.ts
19185
+ init_loader();
19186
+
19187
+ // src/vault/broker/client.ts
19188
+ import { homedir as homedir3 } from "node:os";
19189
+ import { join as join3 } from "node:path";
19190
+
19177
19191
  // src/vault/broker/protocol.ts
19192
+ init_zod();
19178
19193
  var MAX_FRAME_BYTES = 64 * 1024;
19179
19194
  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), {
19180
19195
  message: "agent name is reserved"
@@ -19562,17 +19577,6 @@ init_merge();
19562
19577
  init_paths();
19563
19578
 
19564
19579
  // src/agents/compose.ts
19565
- var AGENT_UID_MIN = 10001;
19566
- var AGENT_UID_MAX = 10999;
19567
- function allocateAgentUid(name) {
19568
- if (isReservedAgentName(name)) {
19569
- 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.`);
19570
- }
19571
- const hash = createHash("sha256").update(name).digest();
19572
- const u32 = hash.readUInt32BE(0);
19573
- const range = AGENT_UID_MAX - AGENT_UID_MIN + 1;
19574
- return AGENT_UID_MIN + u32 % range;
19575
- }
19576
19580
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
19577
19581
 
19578
19582
  // src/vault/broker/server.ts