switchroom 0.12.0 → 0.12.2

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.
Files changed (29) hide show
  1. package/README.md +26 -11
  2. package/dist/auth-broker/index.js +1 -1
  3. package/dist/cli/skill-validate-pretool.mjs +7209 -0
  4. package/dist/cli/switchroom.js +891 -434
  5. package/dist/vault/broker/server.js +31 -22
  6. package/package.json +3 -2
  7. package/profiles/_shared/agent-self-service.md.hbs +1 -1
  8. package/skills/skill-creator/SKILL.md +52 -0
  9. package/telegram-plugin/auth-snapshot-format.ts +5 -5
  10. package/telegram-plugin/dist/gateway/gateway.js +62 -8
  11. package/telegram-plugin/gateway/access-validator.test.ts +8 -8
  12. package/telegram-plugin/gateway/access-validator.ts +1 -1
  13. package/telegram-plugin/gateway/boot-probes.ts +43 -3
  14. package/telegram-plugin/gateway/gateway.ts +72 -0
  15. package/telegram-plugin/recent-outbound-dedup.ts +1 -1
  16. package/telegram-plugin/registry/turns-schema.ts +1 -1
  17. package/telegram-plugin/tests/auth-add-flow.test.ts +1 -1
  18. package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
  19. package/telegram-plugin/tests/auth-snapshot-format.test.ts +17 -17
  20. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +10 -10
  21. package/telegram-plugin/tests/boot-probes.test.ts +37 -2
  22. package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +1 -1
  23. package/telegram-plugin/tests/fleet-state.test.ts +3 -2
  24. package/telegram-plugin/tests/secret-detect-audit.test.ts +1 -1
  25. package/telegram-plugin/tests/secret-detect-pipeline.test.ts +7 -6
  26. package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +6 -5
  27. package/telegram-plugin/tests/secret-detect.test.ts +8 -8
  28. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +8 -8
  29. package/telegram-plugin/tests/vault-request-access-tool.test.ts +51 -0
@@ -21988,6 +21988,7 @@ __export(exports_client, {
21988
21988
  listGrantsViaBroker: () => listGrantsViaBroker,
21989
21989
  getViaBrokerStructured: () => getViaBrokerStructured,
21990
21990
  getViaBroker: () => getViaBroker,
21991
+ defaultBrokerSocketPath: () => defaultBrokerSocketPath,
21991
21992
  createBrokerClient: () => createBrokerClient,
21992
21993
  brokerIsComposeManaged: () => brokerIsComposeManaged,
21993
21994
  VaultTokenRejectedError: () => VaultTokenRejectedError
@@ -22252,7 +22253,7 @@ async function lockViaBroker(opts) {
22252
22253
  async function unlockViaBroker(passphrase, opts) {
22253
22254
  const dataSocketPath = resolveBrokerSocketPath(opts);
22254
22255
  const unlockSocketPath = unlockSocketFor(dataSocketPath);
22255
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
22256
+ const timeoutMs = opts?.timeoutMs ?? UNLOCK_TIMEOUT_MS;
22256
22257
  return new Promise((resolve8) => {
22257
22258
  let settled = false;
22258
22259
  const settle = (val) => {
@@ -22354,7 +22355,7 @@ async function revokeGrantViaBroker(id, opts) {
22354
22355
  return { kind: "error", msg: resp.msg };
22355
22356
  return { kind: "error", msg: "unexpected broker response" };
22356
22357
  }
22357
- var DEFAULT_TIMEOUT_MS = 2000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH, VaultTokenRejectedError;
22358
+ var DEFAULT_TIMEOUT_MS = 2000, UNLOCK_TIMEOUT_MS = 30000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH, VaultTokenRejectedError;
22358
22359
  var init_client = __esm(() => {
22359
22360
  init_protocol();
22360
22361
  init_peercred();
@@ -23085,6 +23086,8 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23085
23086
  NPM_CONFIG_PREFIX: "/state/agent/home/.npm-global",
23086
23087
  PIP_BREAK_SYSTEM_PACKAGES: "1",
23087
23088
  PIP_USER: "1",
23089
+ DISABLE_AUTOUPDATER: "1",
23090
+ CLAUDE_CODE_ATTRIBUTION_HEADER: "0",
23088
23091
  SWITCHROOM_AGENT_NAME: a.name,
23089
23092
  SWITCHROOM_CONTAINER: "1",
23090
23093
  SWITCHROOM_VAULT_BROKER_SOCK: `/run/switchroom/broker/sock`,
@@ -27070,6 +27073,151 @@ var init_via_claude = __esm(() => {
27070
27073
  ];
27071
27074
  });
27072
27075
 
27076
+ // src/vault/broker/acl.ts
27077
+ function parseCronUnit(unitName) {
27078
+ const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
27079
+ if (!m)
27080
+ return null;
27081
+ const agentName = m[1];
27082
+ const index = parseInt(m[2], 10);
27083
+ if (!agentName)
27084
+ return null;
27085
+ return { agentName, index };
27086
+ }
27087
+ function agentSlugFromPeer(peer) {
27088
+ if (peer.systemdUnit === null)
27089
+ return null;
27090
+ const parsed = parseCronUnit(peer.systemdUnit);
27091
+ return parsed?.agentName ?? null;
27092
+ }
27093
+ function checkEntryScope(scope, agentSlug) {
27094
+ if (scope === undefined || scope === null) {
27095
+ return { allow: true };
27096
+ }
27097
+ const deny = scope.deny ?? [];
27098
+ const allow = scope.allow ?? [];
27099
+ if (agentSlug !== null && deny.includes(agentSlug)) {
27100
+ return {
27101
+ allow: false,
27102
+ reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
27103
+ };
27104
+ }
27105
+ if (allow.length > 0) {
27106
+ if (agentSlug === null || !allow.includes(agentSlug)) {
27107
+ return {
27108
+ allow: false,
27109
+ 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)`
27110
+ };
27111
+ }
27112
+ }
27113
+ return { allow: true };
27114
+ }
27115
+ function checkAcl(peer, config, key) {
27116
+ if (peer.systemdUnit !== null) {
27117
+ const parsed = parseCronUnit(peer.systemdUnit);
27118
+ if (parsed === null) {
27119
+ return {
27120
+ allow: false,
27121
+ reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
27122
+ };
27123
+ }
27124
+ const { agentName, index } = parsed;
27125
+ const agentConfig = config.agents?.[agentName];
27126
+ if (!agentConfig) {
27127
+ return { allow: false, reason: `agent '${agentName}' not found in config` };
27128
+ }
27129
+ const schedule = agentConfig.schedule ?? [];
27130
+ if (index >= schedule.length || index < 0) {
27131
+ return {
27132
+ allow: false,
27133
+ reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
27134
+ };
27135
+ }
27136
+ const entry = schedule[index];
27137
+ const allowedKeys = entry.secrets ?? [];
27138
+ if (!allowedKeys.includes(key)) {
27139
+ return {
27140
+ allow: false,
27141
+ reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
27142
+ };
27143
+ }
27144
+ return { allow: true };
27145
+ }
27146
+ return {
27147
+ allow: false,
27148
+ reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
27149
+ };
27150
+ }
27151
+ function checkAclByAgent(config, agentName, key) {
27152
+ if (!agentName) {
27153
+ return { allow: false, reason: "agent name unresolved" };
27154
+ }
27155
+ const agentConfig = config.agents?.[agentName];
27156
+ if (!agentConfig) {
27157
+ return { allow: false, reason: `agent '${agentName}' not found in config` };
27158
+ }
27159
+ const googleSlot = parseGoogleAccountSlotKey(key);
27160
+ if (googleSlot !== null) {
27161
+ return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
27162
+ }
27163
+ const agentBot = agentConfig.bot_token;
27164
+ const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
27165
+ if (typeof botRef === "string" && botRef.startsWith("vault:")) {
27166
+ const botKey = botRef.slice("vault:".length).split("#")[0];
27167
+ if (botKey.length > 0 && botKey === key) {
27168
+ return { allow: true };
27169
+ }
27170
+ }
27171
+ const schedule = agentConfig.schedule ?? [];
27172
+ if (schedule.length === 0) {
27173
+ return {
27174
+ allow: false,
27175
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets'; nothing is broker-accessible`
27176
+ };
27177
+ }
27178
+ for (const entry of schedule) {
27179
+ const allowed = entry?.secrets ?? [];
27180
+ if (allowed.includes(key)) {
27181
+ return { allow: true };
27182
+ }
27183
+ }
27184
+ return {
27185
+ allow: false,
27186
+ reason: `key '${key}' not in ACL for agent '${agentName}'`
27187
+ };
27188
+ }
27189
+ function parseGoogleAccountSlotKey(key) {
27190
+ const match = key.match(/^google:([^:]+):([a-z_]+)$/);
27191
+ if (!match)
27192
+ return null;
27193
+ return { account: match[1], field: match[2] };
27194
+ }
27195
+ function checkGoogleAccountAcl(config, agentName, account, key) {
27196
+ const accounts = config.google_accounts ?? {};
27197
+ const accountKey = account.toLowerCase();
27198
+ const accountEntry = accounts[accountKey] ?? accounts[account];
27199
+ if (!accountEntry) {
27200
+ return {
27201
+ allow: false,
27202
+ reason: `google_accounts['${account}'] not configured (key '${key}')`
27203
+ };
27204
+ }
27205
+ const enabled = accountEntry.enabled_for ?? [];
27206
+ if (enabled.length === 0) {
27207
+ return {
27208
+ allow: false,
27209
+ reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
27210
+ };
27211
+ }
27212
+ if (!enabled.includes(agentName)) {
27213
+ return {
27214
+ allow: false,
27215
+ reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
27216
+ };
27217
+ }
27218
+ return { allow: true };
27219
+ }
27220
+
27073
27221
  // src/util/audit-hashchain.ts
27074
27222
  import { createHash as createHash6 } from "node:crypto";
27075
27223
  import { openSync as openSync7, readSync as readSync2, fstatSync as fstatSync2, closeSync as closeSync7 } from "node:fs";
@@ -27916,6 +28064,108 @@ var init_doctor_auth_broker = __esm(() => {
27916
28064
  init_paths();
27917
28065
  });
27918
28066
 
28067
+ // src/cli/doctor-hostd.ts
28068
+ import { spawnSync as spawnSync6 } from "node:child_process";
28069
+ function realDockerInspect(ref, format) {
28070
+ try {
28071
+ const r = spawnSync6("docker", ["inspect", ref, "--format", format], {
28072
+ encoding: "utf-8",
28073
+ timeout: 5000
28074
+ });
28075
+ if (r.status !== 0)
28076
+ return null;
28077
+ const out = (r.stdout ?? "").trim();
28078
+ return out.length > 0 ? out : null;
28079
+ } catch {
28080
+ return null;
28081
+ }
28082
+ }
28083
+ function imageCreatedAt(container, dockerInspect) {
28084
+ const imageId = dockerInspect(container, "{{.Image}}");
28085
+ if (!imageId)
28086
+ return null;
28087
+ return dockerInspect(imageId, "{{.Created}}");
28088
+ }
28089
+ function runHostdChecks(config, deps = {}) {
28090
+ const dockerInspect = deps.dockerInspect ?? realDockerInspect;
28091
+ const enabled = config.host_control?.enabled === true;
28092
+ if (!enabled) {
28093
+ return [
28094
+ {
28095
+ name: "hostd: configured",
28096
+ status: "warn",
28097
+ detail: "host_control.enabled is not true \u2014 /restart and /update apply " + "use the legacy in-agent fallback, which fails on docker installs (#926)",
28098
+ fix: "Set `host_control: { enabled: true }` in switchroom.yaml and run `switchroom hostd install`"
28099
+ }
28100
+ ];
28101
+ }
28102
+ const results = [
28103
+ {
28104
+ name: "hostd: configured",
28105
+ status: "ok",
28106
+ detail: "host_control.enabled: true"
28107
+ }
28108
+ ];
28109
+ const status = dockerInspect(HOSTD_CONTAINER, "{{.State.Status}}");
28110
+ if (status !== "running") {
28111
+ results.push({
28112
+ name: "hostd: running",
28113
+ status: "fail",
28114
+ detail: status === null ? `${HOSTD_CONTAINER} container not found` : `${HOSTD_CONTAINER} is ${status}, not running`,
28115
+ fix: "Run `switchroom hostd install` on the host to (re)create the daemon"
28116
+ });
28117
+ return results;
28118
+ }
28119
+ results.push({
28120
+ name: "hostd: running",
28121
+ status: "ok",
28122
+ detail: `${HOSTD_CONTAINER} running`
28123
+ });
28124
+ const hostdCreated = imageCreatedAt(HOSTD_CONTAINER, dockerInspect);
28125
+ let agentCreated = null;
28126
+ for (const name of Object.keys(config.agents ?? {})) {
28127
+ agentCreated = imageCreatedAt(`switchroom-${name}`, dockerInspect);
28128
+ if (agentCreated)
28129
+ break;
28130
+ }
28131
+ if (!hostdCreated || !agentCreated) {
28132
+ results.push({
28133
+ name: "hostd: image drift",
28134
+ status: "ok",
28135
+ detail: "skipped \u2014 no running agent image to compare against"
28136
+ });
28137
+ return results;
28138
+ }
28139
+ const hostdMs = Date.parse(hostdCreated);
28140
+ const agentMs = Date.parse(agentCreated);
28141
+ if (Number.isNaN(hostdMs) || Number.isNaN(agentMs)) {
28142
+ results.push({
28143
+ name: "hostd: image drift",
28144
+ status: "ok",
28145
+ detail: "skipped \u2014 unparseable image timestamps"
28146
+ });
28147
+ return results;
28148
+ }
28149
+ const lagHours = (agentMs - hostdMs) / 3600000;
28150
+ if (lagHours > HOSTD_DRIFT_HOURS) {
28151
+ results.push({
28152
+ name: "hostd: image drift",
28153
+ status: "warn",
28154
+ detail: `hostd image is ~${lagHours.toFixed(1)}h older than the agent fleet ` + `image \u2014 likely a \`switchroom update --skip-images\` left it behind ` + `(that flag skips the refresh-hostd step)`,
28155
+ fix: "Run `switchroom hostd install` (or `switchroom update` without `--skip-images`)"
28156
+ });
28157
+ } else {
28158
+ results.push({
28159
+ name: "hostd: image drift",
28160
+ status: "ok",
28161
+ detail: `in sync with the agent fleet image (\u00b1${HOSTD_DRIFT_HOURS}h)`
28162
+ });
28163
+ }
28164
+ return results;
28165
+ }
28166
+ var HOSTD_CONTAINER = "switchroom-hostd", HOSTD_DRIFT_HOURS = 2;
28167
+ var init_doctor_hostd = () => {};
28168
+
27919
28169
  // src/cli/doctor-drive.ts
27920
28170
  import {
27921
28171
  existsSync as realExistsSync,
@@ -28205,6 +28455,181 @@ function runCredentialsMigrationChecks(config, deps = {}) {
28205
28455
  }
28206
28456
  var init_doctor_credentials_migration = () => {};
28207
28457
 
28458
+ // src/cli/doctor-secret-access.ts
28459
+ import {
28460
+ accessSync,
28461
+ constants as fsConstants4,
28462
+ existsSync as existsSync46,
28463
+ realpathSync as realpathSync4,
28464
+ statSync as statSync20
28465
+ } from "node:fs";
28466
+ import { userInfo } from "node:os";
28467
+ function resolveVaultPath2(config) {
28468
+ return config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
28469
+ }
28470
+ function defaultStatVault(path4) {
28471
+ if (!existsSync46(path4)) {
28472
+ return { exists: false, readable: false, uid: -1, mode: 0, realPath: path4 };
28473
+ }
28474
+ let real = path4;
28475
+ try {
28476
+ real = realpathSync4(path4);
28477
+ } catch {}
28478
+ let uid = -1;
28479
+ let mode = 0;
28480
+ try {
28481
+ const s = statSync20(real);
28482
+ uid = s.uid;
28483
+ mode = s.mode & 511;
28484
+ } catch {
28485
+ return { exists: true, readable: false, uid, mode, realPath: real };
28486
+ }
28487
+ let readable = false;
28488
+ try {
28489
+ accessSync(real, fsConstants4.R_OK);
28490
+ readable = true;
28491
+ } catch {
28492
+ readable = false;
28493
+ }
28494
+ return { exists: true, readable, uid, mode, realPath: real };
28495
+ }
28496
+ function collectVaultRefs2(value, out) {
28497
+ if (typeof value === "string") {
28498
+ if (value.startsWith("vault:")) {
28499
+ const key = value.slice("vault:".length).split("#")[0].trim();
28500
+ if (key)
28501
+ out.add(key);
28502
+ }
28503
+ return;
28504
+ }
28505
+ if (Array.isArray(value)) {
28506
+ for (const v of value)
28507
+ collectVaultRefs2(v, out);
28508
+ return;
28509
+ }
28510
+ if (value && typeof value === "object") {
28511
+ for (const v of Object.values(value)) {
28512
+ collectVaultRefs2(v, out);
28513
+ }
28514
+ }
28515
+ }
28516
+ function runSecretAccessChecks(config, deps = {}) {
28517
+ const results = [];
28518
+ const vaultPath = deps.vaultPath ?? resolveVaultPath2(config);
28519
+ const statVault = deps.statVault ?? defaultStatVault;
28520
+ const selfUid = deps.selfUid ?? (typeof process.getuid === "function" ? process.getuid() : -1);
28521
+ let selfUser = deps.selfUser;
28522
+ if (selfUser === undefined) {
28523
+ try {
28524
+ selfUser = userInfo().username;
28525
+ } catch {
28526
+ selfUser = "<you>";
28527
+ }
28528
+ }
28529
+ const vf = statVault(vaultPath);
28530
+ if (!vf.exists) {
28531
+ results.push({
28532
+ name: "vault: operator readable",
28533
+ status: "ok",
28534
+ detail: `vault file not present at ${vaultPath} \u2014 see the Vault section`
28535
+ });
28536
+ } else if (!vf.readable) {
28537
+ results.push({
28538
+ name: "vault: operator readable",
28539
+ status: "fail",
28540
+ detail: `${vf.realPath} is owned by uid ${vf.uid} (mode 0${vf.mode.toString(8)}) \u2014 ` + `the operator (uid ${selfUid} ${selfUser}) cannot read it, so every ` + `\`switchroom vault \u2026\` fails. The broker still works (CAP_DAC_READ_SEARCH), ` + `which masks this until you touch the vault directly.`,
28541
+ fix: `sudo chown ${selfUser}:${selfUser} ${vf.realPath}`
28542
+ });
28543
+ } else {
28544
+ results.push({
28545
+ name: "vault: operator readable",
28546
+ status: "ok",
28547
+ detail: `operator can read ${vf.realPath}`
28548
+ });
28549
+ }
28550
+ const passphrase = deps.passphrase ?? process.env.SWITCHROOM_VAULT_PASSPHRASE;
28551
+ if (!passphrase) {
28552
+ results.push({
28553
+ name: "agent secret access",
28554
+ status: "warn",
28555
+ detail: "SWITCHROOM_VAULT_PASSPHRASE not set \u2014 cannot enumerate vault keys/ACLs " + "to verify per-agent secret access",
28556
+ fix: "Export SWITCHROOM_VAULT_PASSPHRASE and re-run `switchroom doctor`"
28557
+ });
28558
+ return results;
28559
+ }
28560
+ let entries;
28561
+ try {
28562
+ entries = (deps.openVault ?? openVault)(passphrase, vaultPath);
28563
+ } catch (err) {
28564
+ results.push({
28565
+ name: "agent secret access",
28566
+ status: vf.readable ? "fail" : "warn",
28567
+ detail: `cannot open the vault: ${err.message}`,
28568
+ fix: vf.readable ? "SWITCHROOM_VAULT_PASSPHRASE may be wrong, or the vault is corrupt" : "fix the vault file ownership above first (operator cannot read it)"
28569
+ });
28570
+ return results;
28571
+ }
28572
+ const agents = Object.keys(config.agents ?? {});
28573
+ for (const name of agents) {
28574
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, config.agents[name]);
28575
+ const cronKeys = new Set;
28576
+ for (const entry of resolved.schedule ?? []) {
28577
+ for (const s of entry.secrets ?? [])
28578
+ cronKeys.add(s);
28579
+ }
28580
+ const refKeys = new Set;
28581
+ collectVaultRefs2(resolved, refKeys);
28582
+ const needed = new Set([...cronKeys, ...refKeys]);
28583
+ if (needed.size === 0) {
28584
+ results.push({
28585
+ name: `secret access: ${name}`,
28586
+ status: "ok",
28587
+ detail: "no declared vault secrets"
28588
+ });
28589
+ continue;
28590
+ }
28591
+ const gaps = [];
28592
+ for (const key of [...needed].sort()) {
28593
+ const isGoogleSlot = key.startsWith("google:");
28594
+ if (!isGoogleSlot && !(key in entries)) {
28595
+ gaps.push(`'${key}' missing from the vault`);
28596
+ continue;
28597
+ }
28598
+ const byScope = checkEntryScope(entries[key]?.scope, name);
28599
+ if (cronKeys.has(key)) {
28600
+ const byAgent = checkAclByAgent(config, name, key);
28601
+ if (!byAgent.allow) {
28602
+ gaps.push(`'${key}' (cron) \u2014 no static ACL grants read (${byAgent.reason})`);
28603
+ continue;
28604
+ }
28605
+ }
28606
+ if (!byScope.allow) {
28607
+ gaps.push(`'${key}' \u2014 per-key scope denies read (${byScope.reason})`);
28608
+ }
28609
+ }
28610
+ if (gaps.length === 0) {
28611
+ results.push({
28612
+ name: `secret access: ${name}`,
28613
+ status: "ok",
28614
+ detail: `${needed.size} secret(s): all present + ACL ok`
28615
+ });
28616
+ } else {
28617
+ results.push({
28618
+ name: `secret access: ${name}`,
28619
+ status: "fail",
28620
+ detail: `${gaps.length}/${needed.size} unreachable \u2014 ${gaps.join("; ")}`,
28621
+ fix: "`switchroom vault set <key>` for missing keys; " + "`switchroom vault set <key> --allow " + name + "` to grant this agent read access"
28622
+ });
28623
+ }
28624
+ }
28625
+ return results;
28626
+ }
28627
+ var init_doctor_secret_access = __esm(() => {
28628
+ init_paths();
28629
+ init_merge();
28630
+ init_vault();
28631
+ });
28632
+
28208
28633
  // src/cli/doctor-inlined-secrets.ts
28209
28634
  import { readFileSync as fsReadFileSync } from "node:fs";
28210
28635
  function isSecretShapedKey(key) {
@@ -28394,7 +28819,9 @@ __export(exports_doctor, {
28394
28819
  parsePythonVersion: () => parsePythonVersion,
28395
28820
  parseNodeVersion: () => parseNodeVersion,
28396
28821
  parseEnvFile: () => parseEnvFile,
28822
+ mffEnvState: () => mffEnvState,
28397
28823
  mffEnvPath: () => mffEnvPath,
28824
+ mffAgentName: () => mffAgentName,
28398
28825
  isSwitchroomCheckout: () => isSwitchroomCheckout,
28399
28826
  findChromium: () => findChromium,
28400
28827
  deriveEd25519PublicKeyBytes: () => deriveEd25519PublicKeyBytes,
@@ -28420,18 +28847,18 @@ __export(exports_doctor, {
28420
28847
  checkAgents: () => checkAgents,
28421
28848
  MFF_VAULT_KEY: () => MFF_VAULT_KEY
28422
28849
  });
28423
- import { execSync as execSync3, spawnSync as spawnSync6 } from "node:child_process";
28850
+ import { execSync as execSync3, spawnSync as spawnSync7 } from "node:child_process";
28424
28851
  import {
28425
- accessSync,
28426
- constants as fsConstants4,
28427
- existsSync as existsSync46,
28852
+ accessSync as accessSync2,
28853
+ constants as fsConstants5,
28854
+ existsSync as existsSync47,
28428
28855
  lstatSync as lstatSync5,
28429
28856
  mkdirSync as mkdirSync26,
28430
28857
  readFileSync as readFileSync44,
28431
28858
  readdirSync as readdirSync17,
28432
- statSync as statSync20
28859
+ statSync as statSync21
28433
28860
  } from "node:fs";
28434
- import { join as join41, resolve as resolve29 } from "node:path";
28861
+ import { dirname as dirname12, join as join41, resolve as resolve29 } from "node:path";
28435
28862
  import { createPublicKey, createPrivateKey } from "node:crypto";
28436
28863
  function statusGlyph(status) {
28437
28864
  switch (status) {
@@ -28445,14 +28872,14 @@ function statusGlyph(status) {
28445
28872
  }
28446
28873
  function findInNvm(bin) {
28447
28874
  const nvmRoot = join41(process.env.HOME ?? "", ".nvm", "versions", "node");
28448
- if (!existsSync46(nvmRoot))
28875
+ if (!existsSync47(nvmRoot))
28449
28876
  return null;
28450
28877
  try {
28451
28878
  const versions = readdirSync17(nvmRoot).sort().reverse();
28452
28879
  for (const v of versions) {
28453
28880
  const candidate = join41(nvmRoot, v, "bin", bin);
28454
28881
  try {
28455
- const s = statSync20(candidate);
28882
+ const s = statSync21(candidate);
28456
28883
  if (s.isFile() || s.isSymbolicLink()) {
28457
28884
  return candidate;
28458
28885
  }
@@ -28617,7 +29044,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
28617
29044
  }
28618
29045
  cacheLocations.push(join41(homeDir, ".cache", "ms-playwright"));
28619
29046
  for (const cacheDir of cacheLocations) {
28620
- if (!existsSync46(cacheDir))
29047
+ if (!existsSync47(cacheDir))
28621
29048
  continue;
28622
29049
  try {
28623
29050
  const entries = readdirSync17(cacheDir).filter((e) => e.startsWith("chromium"));
@@ -28629,7 +29056,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
28629
29056
  join41(cacheDir, entry, "chrome-linux", "headless_shell")
28630
29057
  ];
28631
29058
  for (const path4 of candidates2) {
28632
- if (existsSync46(path4))
29059
+ if (existsSync47(path4))
28633
29060
  return path4;
28634
29061
  }
28635
29062
  }
@@ -28652,7 +29079,7 @@ function checkChromium() {
28652
29079
  function checkDepsCacheWritable(depsRoot = resolvePath("~/.switchroom/deps")) {
28653
29080
  try {
28654
29081
  mkdirSync26(depsRoot, { recursive: true });
28655
- accessSync(depsRoot, fsConstants4.W_OK);
29082
+ accessSync2(depsRoot, fsConstants5.W_OK);
28656
29083
  return {
28657
29084
  name: "~/.switchroom/deps writable",
28658
29085
  status: "ok",
@@ -28710,7 +29137,7 @@ function checkLegacyState() {
28710
29137
  const results = [];
28711
29138
  const h = process.env.HOME ?? "/root";
28712
29139
  const clerkDir = join41(h, LEGACY_STATE_DIR);
28713
- const clerkPresent = existsSync46(clerkDir);
29140
+ const clerkPresent = existsSync47(clerkDir);
28714
29141
  results.push({
28715
29142
  name: "legacy ~/.clerk state",
28716
29143
  status: clerkPresent ? "warn" : "ok",
@@ -28752,7 +29179,7 @@ function checkVault(config) {
28752
29179
  status: "ok",
28753
29180
  detail: "Approval auth: passphrase (two-factor)"
28754
29181
  };
28755
- if (!existsSync46(vaultPath)) {
29182
+ if (!existsSync47(vaultPath)) {
28756
29183
  return [
28757
29184
  postureResult,
28758
29185
  {
@@ -28845,7 +29272,7 @@ function checkHindsightConsumer(config, opts) {
28845
29272
  }
28846
29273
  function probeAuthBrokerSocket(consumerName) {
28847
29274
  const containerPath = `/run/switchroom/auth-broker/${consumerName}/sock`;
28848
- const r = spawnSync6("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
29275
+ const r = spawnSync7("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
28849
29276
  if (r.error || r.status === null)
28850
29277
  return "unreachable";
28851
29278
  if (r.status === 0)
@@ -28924,7 +29351,7 @@ async function checkHindsight(config) {
28924
29351
  function checkPendingRetainsQueue(dir) {
28925
29352
  const home2 = process.env.HOME ?? "";
28926
29353
  const pendingDir = dir ?? process.env.HINDSIGHT_PENDING_DIR ?? join41(home2, ".hindsight", "pending-retains");
28927
- if (!existsSync46(pendingDir)) {
29354
+ if (!existsSync47(pendingDir)) {
28928
29355
  return {
28929
29356
  name: "pending-retains queue",
28930
29357
  status: "ok",
@@ -28995,7 +29422,7 @@ function tryReadHostFile(path4) {
28995
29422
  }
28996
29423
  }
28997
29424
  function parseEnvFile(path4) {
28998
- if (!existsSync46(path4))
29425
+ if (!existsSync47(path4))
28999
29426
  return {};
29000
29427
  let content;
29001
29428
  try {
@@ -29105,7 +29532,7 @@ async function checkTelegram(config) {
29105
29532
  }
29106
29533
  function checkStartShStale(agentName, startShPath) {
29107
29534
  const label = `${agentName}: start.sh scheduler block`;
29108
- if (!existsSync46(startShPath)) {
29535
+ if (!existsSync47(startShPath)) {
29109
29536
  return {
29110
29537
  name: label,
29111
29538
  status: "warn",
@@ -29173,7 +29600,7 @@ function checkLeakedHomeSwitchroom(agentName, agentDir) {
29173
29600
  function checkRepoHygiene(repoRoot) {
29174
29601
  const results = [];
29175
29602
  const exportDir = join41(repoRoot, "clerk-export");
29176
- if (existsSync46(exportDir)) {
29603
+ if (existsSync47(exportDir)) {
29177
29604
  results.push({
29178
29605
  name: "repo hygiene: clerk-export/ on disk (#1072)",
29179
29606
  status: "warn",
@@ -29182,7 +29609,7 @@ function checkRepoHygiene(repoRoot) {
29182
29609
  });
29183
29610
  }
29184
29611
  const knownTarball = join41(repoRoot, "clerk-export-with-secrets.tar.gz");
29185
- if (existsSync46(knownTarball)) {
29612
+ if (existsSync47(knownTarball)) {
29186
29613
  results.push({
29187
29614
  name: "repo hygiene: clerk-export-with-secrets.tar.gz on disk (#1072)",
29188
29615
  status: "warn",
@@ -29222,10 +29649,10 @@ function checkRepoHygiene(repoRoot) {
29222
29649
  }
29223
29650
  function isSwitchroomCheckout(dir) {
29224
29651
  try {
29225
- if (!existsSync46(join41(dir, ".git")))
29652
+ if (!existsSync47(join41(dir, ".git")))
29226
29653
  return false;
29227
29654
  const pkgPath = join41(dir, "package.json");
29228
- if (!existsSync46(pkgPath))
29655
+ if (!existsSync47(pkgPath))
29229
29656
  return false;
29230
29657
  const pkg = JSON.parse(readFileSync44(pkgPath, "utf-8"));
29231
29658
  return pkg.name === "switchroom";
@@ -29240,7 +29667,7 @@ function checkAgents(config, configPath) {
29240
29667
  const authStatuses = getAllAuthStatuses(config);
29241
29668
  for (const [name, agentConfig] of Object.entries(config.agents)) {
29242
29669
  const agentDir = resolve29(agentsDir, name);
29243
- if (!existsSync46(agentDir)) {
29670
+ if (!existsSync47(agentDir)) {
29244
29671
  results.push({
29245
29672
  name: `${name}: scaffold`,
29246
29673
  status: "fail",
@@ -29292,7 +29719,7 @@ function checkAgents(config, configPath) {
29292
29719
  name: `${name}: auth`,
29293
29720
  status: "fail",
29294
29721
  detail: auth?.pendingAuth ? "pending (auth flow in progress)" : "not authenticated",
29295
- fix: `Run \`switchroom auth add <label> --from-oauth\` then \`switchroom auth use <label>\` (RFC H \u2014 see docs/auth.md)`
29722
+ fix: `Run \`switchroom auth add <label> --via-claude\` then \`switchroom auth use <label>\` (RFC H \u2014 see docs/auth.md)`
29296
29723
  });
29297
29724
  }
29298
29725
  } else {
@@ -29332,13 +29759,13 @@ function checkAgents(config, configPath) {
29332
29759
  name: `${name}: auth slots`,
29333
29760
  status: status2,
29334
29761
  detail,
29335
- fix: quotaOut > 0 ? `Quota-exhausted account(s) will auto-recover when the window resets; broker auto-rotates per \`auth.fallback_order\` (see \`switchroom auth show\`).` : expired > 0 ? `Expired account(s) \u2014 add a fresh one via \`switchroom auth add <label> --from-oauth\` and \`switchroom auth use <label>\` (RFC H).` : undefined
29762
+ fix: quotaOut > 0 ? `Quota-exhausted account(s) will auto-recover when the window resets; broker auto-rotates per \`auth.fallback_order\` (see \`switchroom auth show\`).` : expired > 0 ? `Expired account(s) \u2014 add a fresh one via \`switchroom auth add <label> --via-claude\` and \`switchroom auth use <label>\` (RFC H).` : undefined
29336
29763
  });
29337
29764
  }
29338
29765
  }
29339
29766
  if (agentConfig.channels?.telegram?.plugin === "switchroom") {
29340
29767
  const mcpJsonPath = join41(agentDir, ".mcp.json");
29341
- if (!existsSync46(mcpJsonPath)) {
29768
+ if (!existsSync47(mcpJsonPath)) {
29342
29769
  results.push({
29343
29770
  name: `${name}: .mcp.json`,
29344
29771
  status: "fail",
@@ -29406,8 +29833,28 @@ ${title}`));
29406
29833
  }
29407
29834
  return { oks, warns, fails };
29408
29835
  }
29409
- function mffEnvPath() {
29410
- return resolve29(process.env.HOME ?? "/root", ".switchroom/credentials/my-family-finance/.env");
29836
+ function mffAgentName(config) {
29837
+ for (const [name, ac] of Object.entries(config?.agents ?? {})) {
29838
+ if ((ac?.skills ?? []).includes("my-family-finance")) {
29839
+ return name;
29840
+ }
29841
+ }
29842
+ return;
29843
+ }
29844
+ function mffEnvPath(config) {
29845
+ const home2 = process.env.HOME ?? "/root";
29846
+ const agent = mffAgentName(config);
29847
+ return agent ? resolve29(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve29(home2, ".switchroom/credentials/my-family-finance/.env");
29848
+ }
29849
+ function mffEnvState(envPath) {
29850
+ if (!existsSync47(envPath))
29851
+ return "absent";
29852
+ try {
29853
+ accessSync2(envPath, fsConstants5.R_OK);
29854
+ return "readable";
29855
+ } catch {
29856
+ return "agent-private";
29857
+ }
29411
29858
  }
29412
29859
  function checkMffVaultKeyPresent(passphrase, vaultPath) {
29413
29860
  if (!passphrase) {
@@ -29418,7 +29865,7 @@ function checkMffVaultKeyPresent(passphrase, vaultPath) {
29418
29865
  fix: "Export SWITCHROOM_VAULT_PASSPHRASE to enable MFF vault probes"
29419
29866
  };
29420
29867
  }
29421
- if (!existsSync46(vaultPath)) {
29868
+ if (!existsSync47(vaultPath)) {
29422
29869
  return {
29423
29870
  name: "mff: vault key present",
29424
29871
  status: "fail",
@@ -29471,7 +29918,7 @@ function deriveEd25519PublicKeyBytes(keyMaterial) {
29471
29918
  }
29472
29919
  }
29473
29920
  function checkMffVaultKeyFormat(passphrase, vaultPath) {
29474
- if (!passphrase || !existsSync46(vaultPath)) {
29921
+ if (!passphrase || !existsSync47(vaultPath)) {
29475
29922
  return {
29476
29923
  name: "mff: vault key format",
29477
29924
  status: "warn",
@@ -29513,12 +29960,20 @@ function checkMffVaultKeyFormat(passphrase, vaultPath) {
29513
29960
  }
29514
29961
  }
29515
29962
  function checkMffEnvFile(envPath = mffEnvPath()) {
29516
- if (!existsSync46(envPath)) {
29963
+ const state = mffEnvState(envPath);
29964
+ if (state === "absent") {
29517
29965
  return {
29518
29966
  name: "mff: .env present",
29519
29967
  status: "fail",
29520
29968
  detail: `${envPath} not found`,
29521
- fix: "Create ~/.switchroom/credentials/my-family-finance/.env with MFF_API_URL=https://..."
29969
+ fix: "Place the MFF skill's .env in the per-agent credentials dir, e.g. `~/.switchroom/credentials/<agent>/my-family-finance/.env` (MFF_API_URL=https://...), then `switchroom update`"
29970
+ };
29971
+ }
29972
+ if (state === "agent-private") {
29973
+ return {
29974
+ name: "mff: .env present",
29975
+ status: "ok",
29976
+ detail: `present, agent-private (${envPath}) \u2014 per-agent since WS6-F2; the operator-run doctor cannot read it by design`
29522
29977
  };
29523
29978
  }
29524
29979
  const env2 = parseEnvFile(envPath);
@@ -29537,6 +29992,14 @@ function checkMffEnvFile(envPath = mffEnvPath()) {
29537
29992
  };
29538
29993
  }
29539
29994
  async function checkMffApiReachable(envPath = mffEnvPath(), timeoutMs = 5000) {
29995
+ const state = mffEnvState(envPath);
29996
+ if (state !== "readable") {
29997
+ return {
29998
+ name: "mff: API reachable",
29999
+ status: "warn",
30000
+ detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
30001
+ };
30002
+ }
29540
30003
  const env2 = parseEnvFile(envPath);
29541
30004
  const apiUrl = env2.MFF_API_URL?.trim();
29542
30005
  if (!apiUrl) {
@@ -29581,6 +30044,14 @@ async function checkMffApiReachable(envPath = mffEnvPath(), timeoutMs = 5000) {
29581
30044
  }
29582
30045
  }
29583
30046
  async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
30047
+ const state = mffEnvState(envPath);
30048
+ if (state !== "readable") {
30049
+ return {
30050
+ name: "mff: auth flow",
30051
+ status: "warn",
30052
+ detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
30053
+ };
30054
+ }
29584
30055
  const env2 = parseEnvFile(envPath);
29585
30056
  const apiUrl = env2.MFF_API_URL?.trim();
29586
30057
  if (!apiUrl) {
@@ -29590,9 +30061,9 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
29590
30061
  detail: "skipped (MFF_API_URL not set)"
29591
30062
  };
29592
30063
  }
29593
- const credDir = resolve29(process.env.HOME ?? "/root", ".switchroom/credentials/my-family-finance");
30064
+ const credDir = dirname12(envPath);
29594
30065
  const authScript = join41(credDir, "claude-auth.py");
29595
- if (!existsSync46(authScript)) {
30066
+ if (!existsSync47(authScript)) {
29596
30067
  return {
29597
30068
  name: "mff: auth flow",
29598
30069
  status: "warn",
@@ -29603,7 +30074,7 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
29603
30074
  const python3 = which("python3") ?? "python3";
29604
30075
  let token;
29605
30076
  try {
29606
- const result = spawnSync6(python3, [authScript, "--quiet"], {
30077
+ const result = spawnSync7(python3, [authScript, "--quiet"], {
29607
30078
  timeout: timeoutMs,
29608
30079
  encoding: "utf-8",
29609
30080
  env: { ...process.env, ...env2 }
@@ -29671,6 +30142,14 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
29671
30142
  }
29672
30143
  }
29673
30144
  async function checkMffCloudflareUa(envPath = mffEnvPath(), timeoutMs = 5000) {
30145
+ const state = mffEnvState(envPath);
30146
+ if (state !== "readable") {
30147
+ return {
30148
+ name: "mff: Cloudflare UA bypass",
30149
+ status: "warn",
30150
+ detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
30151
+ };
30152
+ }
29674
30153
  const env2 = parseEnvFile(envPath);
29675
30154
  const apiUrl = env2.MFF_API_URL?.trim();
29676
30155
  if (!apiUrl) {
@@ -29732,7 +30211,7 @@ async function checkMffCloudflareUa(envPath = mffEnvPath(), timeoutMs = 5000) {
29732
30211
  fix: "Check MFF_API_URL and whether the /api/health endpoint is publicly accessible"
29733
30212
  };
29734
30213
  }
29735
- async function checkMff(passphrase, vaultPath, envPath = mffEnvPath()) {
30214
+ async function checkMff(passphrase, vaultPath, config, envPath = mffEnvPath(config)) {
29736
30215
  const results = [];
29737
30216
  results.push(checkMffVaultKeyPresent(passphrase, vaultPath));
29738
30217
  results.push(checkMffVaultKeyFormat(passphrase, vaultPath));
@@ -29835,7 +30314,7 @@ function registerDoctorCommand(program3) {
29835
30314
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
29836
30315
  const vaultPath = config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
29837
30316
  if (opts.skill === "mff") {
29838
- const mffResults = await checkMff(passphrase, vaultPath);
30317
+ const mffResults = await checkMff(passphrase, vaultPath, config);
29839
30318
  if (opts.json) {
29840
30319
  console.log(JSON.stringify({ sections: [{ title: "MFF Skill", results: mffResults }] }, null, 2));
29841
30320
  } else {
@@ -29862,6 +30341,7 @@ function registerDoctorCommand(program3) {
29862
30341
  },
29863
30342
  { title: "Legacy State", results: checkLegacyState() },
29864
30343
  { title: "Vault", results: checkVault(config) },
30344
+ { title: "Vault access", results: runSecretAccessChecks(config) },
29865
30345
  { title: "Memory (Hindsight)", results: await checkHindsight(config) },
29866
30346
  { title: "Telegram", results: await checkTelegram(config) },
29867
30347
  { title: "Agents", results: checkAgents(config, configPath) },
@@ -29869,8 +30349,9 @@ function registerDoctorCommand(program3) {
29869
30349
  { title: "Audit integrity (WS10-F4)", results: runAuditIntegrityChecks() },
29870
30350
  { title: "Docker (Phase 1a)", results: runDockerSection(config) },
29871
30351
  { title: "Auth Broker", results: runAuthBrokerChecks(config) },
30352
+ { title: "Host control (hostd)", results: runHostdChecks(config) },
29872
30353
  { title: "Google Drive", results: runDriveChecks(config) },
29873
- { title: "MFF Skill", results: await checkMff(passphrase, vaultPath) }
30354
+ { title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) }
29874
30355
  ];
29875
30356
  const cwd = process.cwd();
29876
30357
  if (isSwitchroomCheckout(cwd)) {
@@ -29923,8 +30404,10 @@ var init_doctor = __esm(() => {
29923
30404
  init_hindsight();
29924
30405
  init_doctor_docker();
29925
30406
  init_doctor_auth_broker();
30407
+ init_doctor_hostd();
29926
30408
  init_doctor_drive();
29927
30409
  init_doctor_credentials_migration();
30410
+ init_doctor_secret_access();
29928
30411
  init_doctor_inlined_secrets();
29929
30412
  init_doctor_audit_integrity();
29930
30413
  MANIFEST_WARN_ONLY = new Set([
@@ -45564,9 +46047,9 @@ __export(exports_server, {
45564
46047
  dispatchTool: () => dispatchTool,
45565
46048
  TOOLS: () => TOOLS
45566
46049
  });
45567
- import { spawnSync as spawnSync9 } from "node:child_process";
46050
+ import { spawnSync as spawnSync10 } from "node:child_process";
45568
46051
  function execCli(args) {
45569
- const r = spawnSync9(CLI_BIN, args, {
46052
+ const r = spawnSync10(CLI_BIN, args, {
45570
46053
  encoding: "utf-8",
45571
46054
  env: process.env,
45572
46055
  timeout: 15000
@@ -46059,7 +46542,7 @@ __export(exports_server2, {
46059
46542
  TOOLS: () => TOOLS2
46060
46543
  });
46061
46544
  import { randomBytes as randomBytes13 } from "node:crypto";
46062
- import { existsSync as existsSync69, readFileSync as readFileSync59 } from "node:fs";
46545
+ import { existsSync as existsSync71, readFileSync as readFileSync59 } from "node:fs";
46063
46546
  function selfSocketPath() {
46064
46547
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
46065
46548
  }
@@ -46074,7 +46557,7 @@ async function dispatchTool2(name, args) {
46074
46557
  return errorText2("hostd MCP: SWITCHROOM_AGENT_NAME env var is not set \u2014 cannot " + "determine which per-agent socket to talk to.");
46075
46558
  }
46076
46559
  const sockPath = selfSocketPath();
46077
- if (!existsSync69(sockPath)) {
46560
+ if (!existsSync71(sockPath)) {
46078
46561
  return errorText2(`hostd MCP: socket not bound at ${sockPath}. The host-control ` + `daemon is either not installed (run \`switchroom hostd install\`) ` + `or this agent isn't admin-flagged in switchroom.yaml. RFC C ` + `bind-mounts the per-agent socket only when host_control.enabled ` + `is true AND the agent has admin: true.`);
46079
46562
  }
46080
46563
  let req;
@@ -46193,13 +46676,13 @@ function resolveAuditLogPath() {
46193
46676
  if (process.env.HOSTD_AUDIT_LOG_PATH)
46194
46677
  return process.env.HOSTD_AUDIT_LOG_PATH;
46195
46678
  const bindMounted = "/host-home/.switchroom/host-control-audit.log";
46196
- if (existsSync69(bindMounted))
46679
+ if (existsSync71(bindMounted))
46197
46680
  return bindMounted;
46198
46681
  return defaultAuditLogPath2();
46199
46682
  }
46200
46683
  function getLastUpdateApplyStatus() {
46201
46684
  const path8 = resolveAuditLogPath();
46202
- if (!existsSync69(path8)) {
46685
+ if (!existsSync71(path8)) {
46203
46686
  return errorText2(`get_status: audit log not found at ${path8}. No update_apply has run yet?`);
46204
46687
  }
46205
46688
  let raw;
@@ -46411,8 +46894,8 @@ var {
46411
46894
  } = import__.default;
46412
46895
 
46413
46896
  // src/build-info.ts
46414
- var VERSION = "0.12.0";
46415
- var COMMIT_SHA = "2870fb03";
46897
+ var VERSION = "0.12.2";
46898
+ var COMMIT_SHA = "1eeb64be";
46416
46899
 
46417
46900
  // src/cli/agent.ts
46418
46901
  init_source();
@@ -48496,6 +48979,16 @@ function buildSettingsHooksBlock(p) {
48496
48979
  }
48497
48980
  ]
48498
48981
  },
48982
+ {
48983
+ matcher: "^(Write|Edit|MultiEdit)$",
48984
+ hooks: [
48985
+ {
48986
+ type: "command",
48987
+ command: wrap("hook:skill-validate-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "skill-validate-pretool.mjs")}"`),
48988
+ timeout: 10
48989
+ }
48990
+ ]
48991
+ },
48499
48992
  {
48500
48993
  hooks: [
48501
48994
  {
@@ -55184,145 +55677,6 @@ var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
55184
55677
 
55185
55678
  // src/vault/broker/server.ts
55186
55679
  init_peercred();
55187
-
55188
- // src/vault/broker/acl.ts
55189
- function parseCronUnit(unitName) {
55190
- const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
55191
- if (!m)
55192
- return null;
55193
- const agentName = m[1];
55194
- const index = parseInt(m[2], 10);
55195
- if (!agentName)
55196
- return null;
55197
- return { agentName, index };
55198
- }
55199
- function agentSlugFromPeer(peer) {
55200
- if (peer.systemdUnit === null)
55201
- return null;
55202
- const parsed = parseCronUnit(peer.systemdUnit);
55203
- return parsed?.agentName ?? null;
55204
- }
55205
- function checkEntryScope(scope, agentSlug) {
55206
- if (scope === undefined || scope === null) {
55207
- return { allow: true };
55208
- }
55209
- const deny = scope.deny ?? [];
55210
- const allow = scope.allow ?? [];
55211
- if (agentSlug !== null && deny.includes(agentSlug)) {
55212
- return {
55213
- allow: false,
55214
- reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
55215
- };
55216
- }
55217
- if (allow.length > 0) {
55218
- if (agentSlug === null || !allow.includes(agentSlug)) {
55219
- return {
55220
- allow: false,
55221
- 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)`
55222
- };
55223
- }
55224
- }
55225
- return { allow: true };
55226
- }
55227
- function checkAcl(peer, config, key) {
55228
- if (peer.systemdUnit !== null) {
55229
- const parsed = parseCronUnit(peer.systemdUnit);
55230
- if (parsed === null) {
55231
- return {
55232
- allow: false,
55233
- reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
55234
- };
55235
- }
55236
- const { agentName, index } = parsed;
55237
- const agentConfig = config.agents?.[agentName];
55238
- if (!agentConfig) {
55239
- return { allow: false, reason: `agent '${agentName}' not found in config` };
55240
- }
55241
- const schedule = agentConfig.schedule ?? [];
55242
- if (index >= schedule.length || index < 0) {
55243
- return {
55244
- allow: false,
55245
- reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
55246
- };
55247
- }
55248
- const entry = schedule[index];
55249
- const allowedKeys = entry.secrets ?? [];
55250
- if (!allowedKeys.includes(key)) {
55251
- return {
55252
- allow: false,
55253
- reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
55254
- };
55255
- }
55256
- return { allow: true };
55257
- }
55258
- return {
55259
- allow: false,
55260
- reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
55261
- };
55262
- }
55263
- function checkAclByAgent(config, agentName, key) {
55264
- if (!agentName) {
55265
- return { allow: false, reason: "agent name unresolved" };
55266
- }
55267
- const agentConfig = config.agents?.[agentName];
55268
- if (!agentConfig) {
55269
- return { allow: false, reason: `agent '${agentName}' not found in config` };
55270
- }
55271
- const googleSlot = parseGoogleAccountSlotKey(key);
55272
- if (googleSlot !== null) {
55273
- return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
55274
- }
55275
- const schedule = agentConfig.schedule ?? [];
55276
- if (schedule.length === 0) {
55277
- return {
55278
- allow: false,
55279
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets'; nothing is broker-accessible`
55280
- };
55281
- }
55282
- for (const entry of schedule) {
55283
- const allowed = entry?.secrets ?? [];
55284
- if (allowed.includes(key)) {
55285
- return { allow: true };
55286
- }
55287
- }
55288
- return {
55289
- allow: false,
55290
- reason: `key '${key}' not in ACL for agent '${agentName}'`
55291
- };
55292
- }
55293
- function parseGoogleAccountSlotKey(key) {
55294
- const match = key.match(/^google:([^:]+):([a-z_]+)$/);
55295
- if (!match)
55296
- return null;
55297
- return { account: match[1], field: match[2] };
55298
- }
55299
- function checkGoogleAccountAcl(config, agentName, account, key) {
55300
- const accounts = config.google_accounts ?? {};
55301
- const accountKey = account.toLowerCase();
55302
- const accountEntry = accounts[accountKey] ?? accounts[account];
55303
- if (!accountEntry) {
55304
- return {
55305
- allow: false,
55306
- reason: `google_accounts['${account}'] not configured (key '${key}')`
55307
- };
55308
- }
55309
- const enabled = accountEntry.enabled_for ?? [];
55310
- if (enabled.length === 0) {
55311
- return {
55312
- allow: false,
55313
- reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
55314
- };
55315
- }
55316
- if (!enabled.includes(agentName)) {
55317
- return {
55318
- allow: false,
55319
- reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
55320
- };
55321
- }
55322
- return { allow: true };
55323
- }
55324
-
55325
- // src/vault/broker/server.ts
55326
55680
  init_protocol();
55327
55681
 
55328
55682
  // src/vault/broker/audit-log.ts
@@ -57962,35 +58316,37 @@ class VaultBroker {
57962
58316
  const grantId = dotIdx !== -1 ? req.token.slice(0, dotIdx) : undefined;
57963
58317
  const sentinelKey = Object.keys(this.secrets)[0] ?? "__list_check__";
57964
58318
  const tokenCheck = await validateGrant(this.grantsDb, req.token, sentinelKey);
57965
- if (!tokenCheck.ok && tokenCheck.reason !== "grant-key-not-allowed") {
58319
+ if (!tokenCheck.ok && tokenCheck.reason === "grant-revoked") {
57966
58320
  this.auditLogger.write({
57967
58321
  ts: new Date().toISOString(),
57968
58322
  op: "list",
57969
58323
  caller: auditCaller,
57970
58324
  pid: auditPid,
57971
58325
  cgroup: auditCgroup,
57972
- result: `denied:${tokenCheck.reason}`,
58326
+ result: `denied:grant-revoked`,
57973
58327
  method: "grant",
57974
58328
  grant_id: grantId
57975
58329
  });
57976
- socket.write(encodeResponse(errorResponse("DENIED", tokenCheck.reason)));
58330
+ socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
58331
+ return;
58332
+ }
58333
+ if (tokenCheck.ok || tokenCheck.reason === "grant-key-not-allowed") {
58334
+ const grantRow = tokenCheck.ok ? tokenCheck.grant : this.grantsDb.query("SELECT key_allow FROM vault_grants WHERE id = ?").get(grantId ?? "");
58335
+ const allowedKeys = grantRow ? typeof grantRow.key_allow === "string" ? JSON.parse(grantRow.key_allow) : grantRow.key_allow : [];
58336
+ const visibleKeys2 = allowedKeys.filter((k) => (k in this.secrets));
58337
+ this.auditLogger.write({
58338
+ ts: new Date().toISOString(),
58339
+ op: "list",
58340
+ caller: auditCaller,
58341
+ pid: auditPid,
58342
+ cgroup: auditCgroup,
58343
+ result: `allowed:${visibleKeys2.length}`,
58344
+ method: "grant",
58345
+ grant_id: grantId
58346
+ });
58347
+ socket.write(encodeResponse({ ok: true, keys: visibleKeys2 }));
57977
58348
  return;
57978
58349
  }
57979
- const grantRow = tokenCheck.ok ? tokenCheck.grant : this.grantsDb.query("SELECT key_allow FROM vault_grants WHERE id = ?").get(grantId ?? "");
57980
- const allowedKeys = grantRow ? typeof grantRow.key_allow === "string" ? JSON.parse(grantRow.key_allow) : grantRow.key_allow : [];
57981
- const visibleKeys2 = allowedKeys.filter((k) => (k in this.secrets));
57982
- this.auditLogger.write({
57983
- ts: new Date().toISOString(),
57984
- op: "list",
57985
- caller: auditCaller,
57986
- pid: auditPid,
57987
- cgroup: auditCgroup,
57988
- result: `allowed:${visibleKeys2.length}`,
57989
- method: "grant",
57990
- grant_id: grantId
57991
- });
57992
- socket.write(encodeResponse({ ok: true, keys: visibleKeys2 }));
57993
- return;
57994
58350
  }
57995
58351
  if (!isOperator && agentName === null && process.platform === "linux" && peer === null) {
57996
58352
  const reason = "Unable to identify caller (peercred unavailable); denying on Linux";
@@ -58065,10 +58421,9 @@ class VaultBroker {
58065
58421
  });
58066
58422
  socket.write(encodeResponse(entryResponse(entry2)));
58067
58423
  return;
58068
- } else {
58424
+ } else if (grantResult.reason === "grant-revoked") {
58069
58425
  const dotIdx = req.token.indexOf(".");
58070
58426
  const grantId = dotIdx !== -1 ? req.token.slice(0, dotIdx) : undefined;
58071
- const denyReason = grantResult.reason;
58072
58427
  this.auditLogger.write({
58073
58428
  ts: new Date().toISOString(),
58074
58429
  op: "get",
@@ -58076,11 +58431,11 @@ class VaultBroker {
58076
58431
  caller: auditCaller,
58077
58432
  pid: auditPid,
58078
58433
  cgroup: auditCgroup,
58079
- result: `denied:${denyReason}`,
58434
+ result: `denied:grant-revoked`,
58080
58435
  method: "grant",
58081
58436
  grant_id: grantId
58082
58437
  });
58083
- socket.write(encodeResponse(errorResponse("DENIED", denyReason)));
58438
+ socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
58084
58439
  return;
58085
58440
  }
58086
58441
  }
@@ -68131,7 +68486,7 @@ async function stepScaffoldAgents(config, agentBots, userId, nonInteractive, swi
68131
68486
  if (switchroomConfigPath && !config.auth?.active) {
68132
68487
  try {
68133
68488
  await ensureAuthActiveDefault(switchroomConfigPath);
68134
- console.log(source_default.gray(" Set auth.active: default \u2014 run `switchroom auth add default --from-oauth` to log in"));
68489
+ console.log(source_default.gray(" Set auth.active: default \u2014 run `switchroom auth add default --via-claude` to log in"));
68135
68490
  } catch (err) {
68136
68491
  console.log(source_default.yellow(` \u26a0 Could not set auth.active default: ${err.message}`));
68137
68492
  }
@@ -68410,27 +68765,36 @@ init_doctor();
68410
68765
  init_source();
68411
68766
  init_loader();
68412
68767
  init_lifecycle();
68413
- import { cpSync as cpSync2, existsSync as existsSync47, mkdirSync as mkdirSync27, readFileSync as readFileSync45, realpathSync as realpathSync4, rmSync as rmSync12, statSync as statSync21 } from "node:fs";
68414
- import { spawnSync as spawnSync7 } from "node:child_process";
68415
- import { join as join42, dirname as dirname12, resolve as resolve30 } from "node:path";
68768
+ import { cpSync as cpSync2, existsSync as existsSync48, mkdirSync as mkdirSync27, readFileSync as readFileSync45, realpathSync as realpathSync5, rmSync as rmSync12, statSync as statSync22 } from "node:fs";
68769
+ import { spawnSync as spawnSync8 } from "node:child_process";
68770
+ import { join as join42, dirname as dirname13, resolve as resolve30 } from "node:path";
68416
68771
  import { homedir as homedir22 } from "node:os";
68417
68772
  var DEFAULT_COMPOSE_PATH = join42(homedir22(), ".switchroom", "compose", "docker-compose.yml");
68418
68773
  function isGitCheckout(scriptPath) {
68419
- let dir = dirname12(scriptPath);
68774
+ let dir = dirname13(scriptPath);
68420
68775
  for (let i = 0;i < 10; i++) {
68421
- if (existsSync47(join42(dir, ".git")))
68776
+ if (existsSync48(join42(dir, ".git")))
68422
68777
  return true;
68423
- const parent = dirname12(dir);
68778
+ const parent = dirname13(dir);
68424
68779
  if (parent === dir)
68425
68780
  return false;
68426
68781
  dir = parent;
68427
68782
  }
68428
68783
  return false;
68429
68784
  }
68785
+ function rebuildRefusalMessage(scriptPath) {
68786
+ if (isGitCheckout(scriptPath))
68787
+ return null;
68788
+ return `--rebuild builds the CLI from a git checkout, but switchroom is ` + `running from "${scriptPath}" \u2014 a published install (no .git ` + `ancestor). Rebuilding from source here would drift this host off ` + `the reviewed, CI-published release. Use the published path:
68789
+ ` + `
68790
+ ` + ` npm i -g switchroom@latest && switchroom update
68791
+ ` + `
68792
+ ` + `(\`--rebuild\` is for switchroom maintainers iterating on a source ` + `checkout \u2014 not for published installs.)`;
68793
+ }
68430
68794
  function planUpdate(opts) {
68431
68795
  const composePath = opts.composePath ?? DEFAULT_COMPOSE_PATH;
68432
68796
  const runner = opts.runner ?? defaultRunner;
68433
- const scriptPath = process.argv[1] ?? "";
68797
+ const scriptPath = opts.scriptPath ?? process.argv[1] ?? "";
68434
68798
  const steps = [];
68435
68799
  const releaseOverrideArgs = [];
68436
68800
  if (opts.channel)
@@ -68458,7 +68822,7 @@ function planUpdate(opts) {
68458
68822
  steps.push({
68459
68823
  name: "pull-images",
68460
68824
  description: "Pull broker / kernel / agent images from GHCR",
68461
- skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync47(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
68825
+ skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync48(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
68462
68826
  run: () => {
68463
68827
  const r = runner("docker", [
68464
68828
  "compose",
@@ -68477,8 +68841,9 @@ function planUpdate(opts) {
68477
68841
  name: "rebuild-source",
68478
68842
  description: "git pull upstream main + bun install + npm run build",
68479
68843
  run: () => {
68480
- if (!isGitCheckout(scriptPath)) {
68481
- throw new Error(`--rebuild requires a git checkout, but the CLI is running ` + `from ${scriptPath} which has no .git ancestor (looks like ` + `an installed binary). Drop --rebuild or invoke from a ` + `source checkout.`);
68844
+ const refusal = rebuildRefusalMessage(scriptPath);
68845
+ if (refusal) {
68846
+ throw new Error(refusal);
68482
68847
  }
68483
68848
  const pull = runner("git", ["pull", "--ff-only", "upstream", "main"]);
68484
68849
  if (pull.status !== 0)
@@ -68537,16 +68902,16 @@ function planUpdate(opts) {
68537
68902
  }
68538
68903
  const source = resolve30(import.meta.dirname, "../../skills");
68539
68904
  const dest = join42(homedir22(), ".switchroom", "skills", "_bundled");
68540
- if (!existsSync47(source)) {
68905
+ if (!existsSync48(source)) {
68541
68906
  process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
68542
68907
  `);
68543
68908
  return;
68544
68909
  }
68545
68910
  try {
68546
- if (existsSync47(dest)) {
68911
+ if (existsSync48(dest)) {
68547
68912
  rmSync12(dest, { recursive: true, force: true });
68548
68913
  }
68549
- mkdirSync27(dirname12(dest), { recursive: true });
68914
+ mkdirSync27(dirname13(dest), { recursive: true });
68550
68915
  cpSync2(source, dest, { recursive: true, dereference: false });
68551
68916
  } catch (err) {
68552
68917
  throw new Error(`sync-bundled-skills failed: ${err.message}`);
@@ -68605,7 +68970,7 @@ function planUpdate(opts) {
68605
68970
  return steps;
68606
68971
  }
68607
68972
  function defaultRunner(cmd, args) {
68608
- const r = spawnSync7(cmd, args, { stdio: "inherit" });
68973
+ const r = spawnSync8(cmd, args, { stdio: "inherit" });
68609
68974
  return { status: r.status ?? 1 };
68610
68975
  }
68611
68976
  function writeMarkerInPreferredLocation(agent, reason, runner) {
@@ -68642,16 +69007,16 @@ function defaultStatusProbe(composePath) {
68642
69007
  let scriptPath = rawScriptPath;
68643
69008
  try {
68644
69009
  if (rawScriptPath)
68645
- scriptPath = realpathSync4(rawScriptPath);
69010
+ scriptPath = realpathSync5(rawScriptPath);
68646
69011
  } catch {}
68647
69012
  if (scriptPath) {
68648
69013
  try {
68649
- cliBuiltAt = new Date(statSync21(scriptPath).mtimeMs).toISOString();
69014
+ cliBuiltAt = new Date(statSync22(scriptPath).mtimeMs).toISOString();
68650
69015
  } catch {}
68651
- let dir = dirname12(scriptPath);
69016
+ let dir = dirname13(scriptPath);
68652
69017
  for (let i = 0;i < 8; i++) {
68653
69018
  const pkgPath = join42(dir, "package.json");
68654
- if (existsSync47(pkgPath)) {
69019
+ if (existsSync48(pkgPath)) {
68655
69020
  try {
68656
69021
  const pkg = JSON.parse(readFileSync45(pkgPath, "utf-8"));
68657
69022
  if (typeof pkg.version === "string")
@@ -68661,7 +69026,7 @@ function defaultStatusProbe(composePath) {
68661
69026
  }
68662
69027
  break;
68663
69028
  }
68664
- const parent = dirname12(dir);
69029
+ const parent = dirname13(dir);
68665
69030
  if (parent === dir)
68666
69031
  break;
68667
69032
  dir = parent;
@@ -68674,13 +69039,13 @@ function defaultStatusProbe(composePath) {
68674
69039
  warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
68675
69040
  }
68676
69041
  const services = [];
68677
- if (!existsSync47(composePath)) {
69042
+ if (!existsSync48(composePath)) {
68678
69043
  warnings.push(`compose file not found at ${composePath}; service status unknown`);
68679
69044
  return { cliVersion, cliBuiltAt, services, warnings };
68680
69045
  }
68681
69046
  let serviceList = [];
68682
69047
  try {
68683
- const r = spawnSync7("docker", ["compose", "-p", "switchroom", "-f", composePath, "config", "--services"], { encoding: "utf-8", timeout: 1e4 });
69048
+ const r = spawnSync8("docker", ["compose", "-p", "switchroom", "-f", composePath, "config", "--services"], { encoding: "utf-8", timeout: 1e4 });
68684
69049
  if (r.status !== 0) {
68685
69050
  warnings.push(`docker compose config --services failed: ${r.stderr?.trim() ?? r.error?.message ?? "unknown"}`);
68686
69051
  return { cliVersion, cliBuiltAt, services, warnings };
@@ -68697,7 +69062,7 @@ function defaultStatusProbe(composePath) {
68697
69062
  let containerCreatedAt = null;
68698
69063
  let status = "<unknown>";
68699
69064
  try {
68700
- const r = spawnSync7("docker", ["inspect", "-f", "{{.Config.Image}}|{{.Created}}|{{.State.Status}}", containerName2], { encoding: "utf-8", timeout: 5000 });
69065
+ const r = spawnSync8("docker", ["inspect", "-f", "{{.Config.Image}}|{{.Created}}|{{.State.Status}}", containerName2], { encoding: "utf-8", timeout: 5000 });
68701
69066
  if (r.status === 0) {
68702
69067
  const [img, created, st] = r.stdout.trim().split("|");
68703
69068
  image = img ?? null;
@@ -68713,7 +69078,7 @@ function defaultStatusProbe(composePath) {
68713
69078
  let imagePulledAt = null;
68714
69079
  if (image) {
68715
69080
  try {
68716
- const r = spawnSync7("docker", ["image", "inspect", "-f", "{{.Id}}|{{.Created}}|{{.Metadata.LastTagTime}}", image], { encoding: "utf-8", timeout: 5000 });
69081
+ const r = spawnSync8("docker", ["image", "inspect", "-f", "{{.Id}}|{{.Created}}|{{.Metadata.LastTagTime}}", image], { encoding: "utf-8", timeout: 5000 });
68717
69082
  if (r.status === 0) {
68718
69083
  const [id, created, lastTag] = r.stdout.trim().split("|");
68719
69084
  imageDigestShort = id?.replace(/^sha256:/, "").slice(0, 12) ?? null;
@@ -68798,6 +69163,14 @@ async function runUpdate(opts) {
68798
69163
  `));
68799
69164
  return 2;
68800
69165
  }
69166
+ if (opts.rebuild) {
69167
+ const refusal = rebuildRefusalMessage(opts.scriptPath ?? process.argv[1] ?? "");
69168
+ if (refusal) {
69169
+ stderr(source_default.red(refusal + `
69170
+ `));
69171
+ return 2;
69172
+ }
69173
+ }
68801
69174
  const steps = planUpdate(opts);
68802
69175
  if (opts.check) {
68803
69176
  stdout(source_default.bold(`switchroom update --check (dry-run)
@@ -68835,7 +69208,7 @@ Dry-run only; nothing was changed. Re-run without --check to apply.
68835
69208
  return 0;
68836
69209
  }
68837
69210
  function registerUpdateCommand(program3) {
68838
- program3.command("update").description("Update switchroom on this host: pull images, refresh scaffolds, recreate containers. Wraps the full `pull && apply && up -d` flow.").option("--check", "Dry-run: print the steps that would execute, exit 0.").option("--skip-images", "Skip the docker image pull (offline mode).").option("--rebuild", "Source-checkout users: also git pull + bun install + npm run build before applying. Auto-skipped when the CLI is an installed binary.").option("--status", "Read-only snapshot: report local CLI version, image digest + pull time, container creation time per service. Does NOT invoke any update steps. Wired by Telegram /upgrade-status (#927).").option("--json", "Output as JSON (currently only honored under --status; other modes ignore).").addOption(new Option("--channel <c>", "Override the resolved release block for this update run: follow the named channel (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved release block for this update run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--force", "[legacy v0.6 no-op]").option("--no-restart", "[legacy v0.6 no-op]").option("--resume <file>", "[legacy v0.6 no-op]").option("--phase <phase>", "[legacy v0.6 no-op]").action(async (opts) => {
69211
+ program3.command("update").description("Update switchroom on this host: pull images, refresh scaffolds, recreate containers. Wraps the full `pull && apply && up -d` flow.").option("--check", "Dry-run: print the steps that would execute, exit 0.").option("--skip-images", "Skip the docker image pull (offline mode).").option("--rebuild", "Source-checkout / maintainer only: git pull + bun install + npm run build before applying. REFUSED on a published install \u2014 use `npm i -g switchroom@latest && switchroom update` there.").option("--status", "Read-only snapshot: report local CLI version, image digest + pull time, container creation time per service. Does NOT invoke any update steps. Wired by Telegram /upgrade-status (#927).").option("--json", "Output as JSON (currently only honored under --status; other modes ignore).").addOption(new Option("--channel <c>", "Override the resolved release block for this update run: follow the named channel (dev|rc|latest). Mutually exclusive with --pin.").choices(["dev", "rc", "latest"]).conflicts("pin")).addOption(new Option("--pin <p>", "Override the resolved release block for this update run: pin to a specific build (sha-<7-40 hex> or v<semver>). Mutually exclusive with --channel.").conflicts("channel")).option("--force", "[legacy v0.6 no-op]").option("--no-restart", "[legacy v0.6 no-op]").option("--resume <file>", "[legacy v0.6 no-op]").option("--phase <phase>", "[legacy v0.6 no-op]").action(async (opts) => {
68839
69212
  if (opts.pin && !/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/.test(opts.pin)) {
68840
69213
  console.error(source_default.red(`--pin "${opts.pin}" is invalid. Expected sha-<7-40 hex> or v<semver>.`));
68841
69214
  process.exit(2);
@@ -68861,8 +69234,8 @@ init_source();
68861
69234
  init_helpers();
68862
69235
  init_lifecycle();
68863
69236
  import { execSync as execSync4 } from "node:child_process";
68864
- import { existsSync as existsSync48, readFileSync as readFileSync46 } from "node:fs";
68865
- import { dirname as dirname13, join as join43 } from "node:path";
69237
+ import { existsSync as existsSync49, readFileSync as readFileSync46 } from "node:fs";
69238
+ import { dirname as dirname14, join as join43 } from "node:path";
68866
69239
  function getClaudeCodeVersion() {
68867
69240
  try {
68868
69241
  const out = execSync4("claude --version 2>/dev/null", {
@@ -68913,15 +69286,15 @@ function locateSwitchroomInstallDir() {
68913
69286
  let dir = import.meta.dirname;
68914
69287
  for (let i = 0;i < 10 && dir && dir !== "/"; i++) {
68915
69288
  const pkgPath = join43(dir, "package.json");
68916
- if (existsSync48(pkgPath)) {
69289
+ if (existsSync49(pkgPath)) {
68917
69290
  try {
68918
69291
  const pkg = JSON.parse(readFileSync46(pkgPath, "utf-8"));
68919
- if (pkg.name === "switchroom" && existsSync48(join43(dir, ".git"))) {
69292
+ if (pkg.name === "switchroom" && existsSync49(join43(dir, ".git"))) {
68920
69293
  return dir;
68921
69294
  }
68922
69295
  } catch {}
68923
69296
  }
68924
- dir = dirname13(dir);
69297
+ dir = dirname14(dir);
68925
69298
  }
68926
69299
  return null;
68927
69300
  }
@@ -69135,13 +69508,13 @@ function registerHandoffCommand(program3) {
69135
69508
  // src/issues/store.ts
69136
69509
  import {
69137
69510
  closeSync as closeSync10,
69138
- existsSync as existsSync49,
69511
+ existsSync as existsSync50,
69139
69512
  mkdirSync as mkdirSync28,
69140
69513
  openSync as openSync10,
69141
69514
  readdirSync as readdirSync18,
69142
69515
  readFileSync as readFileSync47,
69143
69516
  renameSync as renameSync10,
69144
- statSync as statSync22,
69517
+ statSync as statSync23,
69145
69518
  unlinkSync as unlinkSync10,
69146
69519
  writeFileSync as writeFileSync25,
69147
69520
  writeSync as writeSync6
@@ -69467,7 +69840,7 @@ var ISSUES_FILE = "issues.jsonl";
69467
69840
  var ISSUES_LOCK = "issues.lock";
69468
69841
  function readAll(stateDir) {
69469
69842
  const path4 = join44(stateDir, ISSUES_FILE);
69470
- if (!existsSync49(path4))
69843
+ if (!existsSync50(path4))
69471
69844
  return [];
69472
69845
  let raw;
69473
69846
  try {
@@ -69544,7 +69917,7 @@ function record(stateDir, input, nowFn = Date.now) {
69544
69917
  });
69545
69918
  }
69546
69919
  function resolve33(stateDir, fingerprint, nowFn = Date.now) {
69547
- if (!existsSync49(join44(stateDir, ISSUES_FILE)))
69920
+ if (!existsSync50(join44(stateDir, ISSUES_FILE)))
69548
69921
  return 0;
69549
69922
  return withLock(stateDir, () => {
69550
69923
  const all = readAll(stateDir);
@@ -69562,7 +69935,7 @@ function resolve33(stateDir, fingerprint, nowFn = Date.now) {
69562
69935
  });
69563
69936
  }
69564
69937
  function resolveAllBySource(stateDir, source, nowFn = Date.now) {
69565
- if (!existsSync49(join44(stateDir, ISSUES_FILE)))
69938
+ if (!existsSync50(join44(stateDir, ISSUES_FILE)))
69566
69939
  return 0;
69567
69940
  return withLock(stateDir, () => {
69568
69941
  const all = readAll(stateDir);
@@ -69580,7 +69953,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
69580
69953
  });
69581
69954
  }
69582
69955
  function prune(stateDir, opts = {}) {
69583
- if (!existsSync49(join44(stateDir, ISSUES_FILE)))
69956
+ if (!existsSync50(join44(stateDir, ISSUES_FILE)))
69584
69957
  return 0;
69585
69958
  return withLock(stateDir, () => {
69586
69959
  const all = readAll(stateDir);
@@ -69637,7 +70010,7 @@ function sweepOrphanTmpFiles(stateDir) {
69637
70010
  continue;
69638
70011
  const tmpPath = join44(stateDir, entry);
69639
70012
  try {
69640
- const stat = statSync22(tmpPath);
70013
+ const stat = statSync23(tmpPath);
69641
70014
  if (stat.mtimeMs < cutoff) {
69642
70015
  unlinkSync10(tmpPath);
69643
70016
  }
@@ -69930,20 +70303,20 @@ function relTime(deltaMs) {
69930
70303
 
69931
70304
  // src/cli/deps.ts
69932
70305
  init_source();
69933
- import { existsSync as existsSync52 } from "node:fs";
70306
+ import { existsSync as existsSync53 } from "node:fs";
69934
70307
  import { homedir as homedir25 } from "node:os";
69935
70308
  import { join as join47, resolve as resolve34 } from "node:path";
69936
70309
 
69937
70310
  // src/deps/python.ts
69938
70311
  import { createHash as createHash9 } from "node:crypto";
69939
70312
  import {
69940
- existsSync as existsSync50,
70313
+ existsSync as existsSync51,
69941
70314
  mkdirSync as mkdirSync29,
69942
70315
  readFileSync as readFileSync48,
69943
70316
  rmSync as rmSync13,
69944
70317
  writeFileSync as writeFileSync26
69945
70318
  } from "node:fs";
69946
- import { dirname as dirname14, join as join45 } from "node:path";
70319
+ import { dirname as dirname15, join as join45 } from "node:path";
69947
70320
  import { homedir as homedir23 } from "node:os";
69948
70321
  import { execFileSync as execFileSync14 } from "node:child_process";
69949
70322
 
@@ -69965,7 +70338,7 @@ function ensurePythonEnv(opts) {
69965
70338
  const { skillName, requirementsPath, force = false } = opts;
69966
70339
  const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
69967
70340
  const hostPython = opts.pythonBin ?? "python3";
69968
- if (!existsSync50(requirementsPath)) {
70341
+ if (!existsSync51(requirementsPath)) {
69969
70342
  throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
69970
70343
  }
69971
70344
  const venvDir = join45(cacheRoot, skillName);
@@ -69974,7 +70347,7 @@ function ensurePythonEnv(opts) {
69974
70347
  const pythonBin = join45(binDir, "python");
69975
70348
  const pipBin = join45(binDir, "pip");
69976
70349
  const targetHash = hashFile(requirementsPath);
69977
- if (!force && existsSync50(stampPath) && existsSync50(pythonBin)) {
70350
+ if (!force && existsSync51(stampPath) && existsSync51(pythonBin)) {
69978
70351
  const existingHash = readFileSync48(stampPath, "utf8").trim();
69979
70352
  if (existingHash === targetHash) {
69980
70353
  return {
@@ -69987,10 +70360,10 @@ function ensurePythonEnv(opts) {
69987
70360
  };
69988
70361
  }
69989
70362
  }
69990
- if (existsSync50(venvDir)) {
70363
+ if (existsSync51(venvDir)) {
69991
70364
  rmSync13(venvDir, { recursive: true, force: true });
69992
70365
  }
69993
- mkdirSync29(dirname14(venvDir), { recursive: true });
70366
+ mkdirSync29(dirname15(venvDir), { recursive: true });
69994
70367
  try {
69995
70368
  execFileSync14(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
69996
70369
  } catch (err) {
@@ -70025,13 +70398,13 @@ function ensurePythonEnv(opts) {
70025
70398
  import { createHash as createHash10 } from "node:crypto";
70026
70399
  import {
70027
70400
  copyFileSync as copyFileSync9,
70028
- existsSync as existsSync51,
70401
+ existsSync as existsSync52,
70029
70402
  mkdirSync as mkdirSync30,
70030
70403
  readFileSync as readFileSync49,
70031
70404
  rmSync as rmSync14,
70032
70405
  writeFileSync as writeFileSync27
70033
70406
  } from "node:fs";
70034
- import { dirname as dirname15, join as join46 } from "node:path";
70407
+ import { dirname as dirname16, join as join46 } from "node:path";
70035
70408
  import { homedir as homedir24 } from "node:os";
70036
70409
  import { execFileSync as execFileSync15 } from "node:child_process";
70037
70410
 
@@ -70058,14 +70431,14 @@ function defaultNodeCacheRoot() {
70058
70431
  return join46(homedir24(), ".switchroom", "deps", "node");
70059
70432
  }
70060
70433
  function hashDepInputs(packageJsonPath) {
70061
- const sourceDir = dirname15(packageJsonPath);
70434
+ const sourceDir = dirname16(packageJsonPath);
70062
70435
  const hasher = createHash10("sha256");
70063
70436
  hasher.update(`package.json
70064
70437
  `);
70065
70438
  hasher.update(readFileSync49(packageJsonPath));
70066
70439
  for (const lockName of ALL_LOCKFILES) {
70067
70440
  const lockPath = join46(sourceDir, lockName);
70068
- if (existsSync51(lockPath)) {
70441
+ if (existsSync52(lockPath)) {
70069
70442
  hasher.update(`
70070
70443
  `);
70071
70444
  hasher.update(lockName);
@@ -70080,16 +70453,16 @@ function ensureNodeEnv(opts) {
70080
70453
  const { skillName, packageJsonPath, force = false } = opts;
70081
70454
  const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
70082
70455
  const installer = opts.installer ?? "bun";
70083
- if (!existsSync51(packageJsonPath)) {
70456
+ if (!existsSync52(packageJsonPath)) {
70084
70457
  throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
70085
70458
  }
70086
- const sourceDir = dirname15(packageJsonPath);
70459
+ const sourceDir = dirname16(packageJsonPath);
70087
70460
  const envDir = join46(cacheRoot, skillName);
70088
70461
  const stampPath = join46(envDir, ".package.sha256");
70089
70462
  const nodeModulesDir = join46(envDir, "node_modules");
70090
70463
  const binDir = join46(nodeModulesDir, ".bin");
70091
70464
  const targetHash = hashDepInputs(packageJsonPath);
70092
- if (!force && existsSync51(stampPath) && existsSync51(nodeModulesDir)) {
70465
+ if (!force && existsSync52(stampPath) && existsSync52(nodeModulesDir)) {
70093
70466
  const existingHash = readFileSync49(stampPath, "utf8").trim();
70094
70467
  if (existingHash === targetHash) {
70095
70468
  return {
@@ -70101,7 +70474,7 @@ function ensureNodeEnv(opts) {
70101
70474
  };
70102
70475
  }
70103
70476
  }
70104
- if (existsSync51(envDir)) {
70477
+ if (existsSync52(envDir)) {
70105
70478
  rmSync14(envDir, { recursive: true, force: true });
70106
70479
  }
70107
70480
  mkdirSync30(envDir, { recursive: true });
@@ -70109,7 +70482,7 @@ function ensureNodeEnv(opts) {
70109
70482
  let copiedLockfile = false;
70110
70483
  for (const lockName of LOCKFILES_FOR[installer]) {
70111
70484
  const lockPath = join46(sourceDir, lockName);
70112
- if (existsSync51(lockPath)) {
70485
+ if (existsSync52(lockPath)) {
70113
70486
  copyFileSync9(lockPath, join46(envDir, lockName));
70114
70487
  copiedLockfile = true;
70115
70488
  }
@@ -70145,22 +70518,22 @@ function registerDepsCommand(program3) {
70145
70518
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
70146
70519
  deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
70147
70520
  const skillsRoot = builtinSkillsRoot();
70148
- if (!existsSync52(skillsRoot)) {
70521
+ if (!existsSync53(skillsRoot)) {
70149
70522
  console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
70150
70523
  process.exit(1);
70151
70524
  }
70152
70525
  const skillDir = join47(skillsRoot, skill);
70153
- if (!existsSync52(skillDir)) {
70526
+ if (!existsSync53(skillDir)) {
70154
70527
  console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
70155
70528
  process.exit(1);
70156
70529
  }
70157
70530
  const requirementsPath = join47(skillDir, "requirements.txt");
70158
70531
  const packageJsonPath = join47(skillDir, "package.json");
70159
- const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync52(requirementsPath));
70160
- const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync52(packageJsonPath));
70532
+ const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync53(requirementsPath));
70533
+ const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync53(packageJsonPath));
70161
70534
  let did = 0;
70162
70535
  if (wantPython) {
70163
- if (!existsSync52(requirementsPath)) {
70536
+ if (!existsSync53(requirementsPath)) {
70164
70537
  console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
70165
70538
  process.exit(1);
70166
70539
  }
@@ -70184,7 +70557,7 @@ function registerDepsCommand(program3) {
70184
70557
  }
70185
70558
  }
70186
70559
  if (wantNode) {
70187
- if (!existsSync52(packageJsonPath)) {
70560
+ if (!existsSync53(packageJsonPath)) {
70188
70561
  console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
70189
70562
  process.exit(1);
70190
70563
  }
@@ -70217,9 +70590,9 @@ function registerDepsCommand(program3) {
70217
70590
  // src/cli/workspace.ts
70218
70591
  init_helpers();
70219
70592
  init_loader();
70220
- import { existsSync as existsSync53 } from "node:fs";
70593
+ import { existsSync as existsSync54 } from "node:fs";
70221
70594
  import { resolve as resolve35, sep as sep2 } from "node:path";
70222
- import { spawnSync as spawnSync8 } from "node:child_process";
70595
+ import { spawnSync as spawnSync9 } from "node:child_process";
70223
70596
 
70224
70597
  // src/agents/workspace.ts
70225
70598
  import { readFile, stat } from "node:fs/promises";
@@ -70932,7 +71305,7 @@ function registerWorkspaceCommand(program3) {
70932
71305
  process.exit(1);
70933
71306
  }
70934
71307
  const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
70935
- const child = spawnSync8(editor, [target], { stdio: "inherit" });
71308
+ const child = spawnSync9(editor, [target], { stdio: "inherit" });
70936
71309
  if (child.status !== 0 && child.status !== null) {
70937
71310
  process.exit(child.status);
70938
71311
  }
@@ -70994,12 +71367,12 @@ function registerWorkspaceCommand(program3) {
70994
71367
  if (!dir)
70995
71368
  return;
70996
71369
  const gitDir = resolve35(dir, ".git");
70997
- if (!existsSync53(gitDir)) {
71370
+ if (!existsSync54(gitDir)) {
70998
71371
  process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
70999
71372
  `);
71000
71373
  return;
71001
71374
  }
71002
- const statusResult = spawnSync8("git", ["status", "--short"], {
71375
+ const statusResult = spawnSync9("git", ["status", "--short"], {
71003
71376
  cwd: dir,
71004
71377
  encoding: "utf-8"
71005
71378
  });
@@ -71014,7 +71387,7 @@ function registerWorkspaceCommand(program3) {
71014
71387
  return;
71015
71388
  }
71016
71389
  const message = opts.message || `checkpoint: ${new Date().toISOString()}`;
71017
- const addResult = spawnSync8("git", ["add", "-A"], {
71390
+ const addResult = spawnSync9("git", ["add", "-A"], {
71018
71391
  cwd: dir,
71019
71392
  encoding: "utf-8"
71020
71393
  });
@@ -71023,7 +71396,7 @@ function registerWorkspaceCommand(program3) {
71023
71396
  `);
71024
71397
  process.exit(1);
71025
71398
  }
71026
- const commitResult = spawnSync8("git", ["commit", "-m", message], {
71399
+ const commitResult = spawnSync9("git", ["commit", "-m", message], {
71027
71400
  cwd: dir,
71028
71401
  encoding: "utf-8"
71029
71402
  });
@@ -71032,7 +71405,7 @@ function registerWorkspaceCommand(program3) {
71032
71405
  `);
71033
71406
  process.exit(1);
71034
71407
  }
71035
- const shaResult = spawnSync8("git", ["rev-parse", "--short", "HEAD"], {
71408
+ const shaResult = spawnSync9("git", ["rev-parse", "--short", "HEAD"], {
71036
71409
  cwd: dir,
71037
71410
  encoding: "utf-8"
71038
71411
  });
@@ -71048,12 +71421,12 @@ function registerWorkspaceCommand(program3) {
71048
71421
  if (!dir)
71049
71422
  return;
71050
71423
  const gitDir = resolve35(dir, ".git");
71051
- if (!existsSync53(gitDir)) {
71424
+ if (!existsSync54(gitDir)) {
71052
71425
  process.stdout.write(`Workspace is not a git repository.
71053
71426
  `);
71054
71427
  return;
71055
71428
  }
71056
- const child = spawnSync8("git", ["status", "--short"], {
71429
+ const child = spawnSync9("git", ["status", "--short"], {
71057
71430
  cwd: dir,
71058
71431
  stdio: "inherit"
71059
71432
  });
@@ -71073,7 +71446,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
71073
71446
  const agentsDir = resolveAgentsDir(config);
71074
71447
  const agentDir = resolve35(agentsDir, agentName);
71075
71448
  const dir = resolveAgentWorkspaceDir(agentDir);
71076
- if (!existsSync53(dir)) {
71449
+ if (!existsSync54(dir)) {
71077
71450
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
71078
71451
  `);
71079
71452
  return;
@@ -71109,7 +71482,7 @@ function safeParseInt(value, fallback) {
71109
71482
  init_helpers();
71110
71483
  init_loader();
71111
71484
  init_merge();
71112
- import { copyFileSync as copyFileSync10, existsSync as existsSync54, readFileSync as readFileSync50, writeFileSync as writeFileSync28 } from "node:fs";
71485
+ import { copyFileSync as copyFileSync10, existsSync as existsSync55, readFileSync as readFileSync50, writeFileSync as writeFileSync28 } from "node:fs";
71113
71486
  import { join as join48, resolve as resolve36 } from "node:path";
71114
71487
  init_schema();
71115
71488
  function resolveSoulTargetOrExit(program3, agentName) {
@@ -71125,7 +71498,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
71125
71498
  const agentsDir = resolveAgentsDir(config);
71126
71499
  const agentDir = resolve36(agentsDir, agentName);
71127
71500
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
71128
- if (!existsSync54(workspaceDir)) {
71501
+ if (!existsSync55(workspaceDir)) {
71129
71502
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
71130
71503
  process.exit(1);
71131
71504
  }
@@ -71151,7 +71524,7 @@ function registerSoulCommand(program3) {
71151
71524
  const t = resolveSoulTargetOrExit(program3, agentName);
71152
71525
  if (!t)
71153
71526
  return;
71154
- if (!existsSync54(t.soulPath)) {
71527
+ if (!existsSync55(t.soulPath)) {
71155
71528
  console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
71156
71529
  process.exit(1);
71157
71530
  }
@@ -71166,7 +71539,7 @@ function registerSoulCommand(program3) {
71166
71539
  console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
71167
71540
  process.exit(1);
71168
71541
  }
71169
- const exists = existsSync54(t.soulPath);
71542
+ const exists = existsSync55(t.soulPath);
71170
71543
  if (exists && !opts.yes) {
71171
71544
  if (!isInteractive()) {
71172
71545
  console.error(`soul: ${t.soulPath} already exists. Re-run with --yes to ` + `replace it (the current file is backed up to SOUL.md.bak).`);
@@ -71181,7 +71554,7 @@ function registerSoulCommand(program3) {
71181
71554
  let backupPath;
71182
71555
  if (exists) {
71183
71556
  backupPath = `${t.soulPath}.bak`;
71184
- if (existsSync54(backupPath)) {
71557
+ if (existsSync55(backupPath)) {
71185
71558
  backupPath = `${t.soulPath}.bak.${Date.now()}`;
71186
71559
  }
71187
71560
  copyFileSync10(t.soulPath, backupPath);
@@ -71200,7 +71573,7 @@ function registerSoulCommand(program3) {
71200
71573
  // src/cli/debug.ts
71201
71574
  init_helpers();
71202
71575
  init_loader();
71203
- import { existsSync as existsSync55, readFileSync as readFileSync51, readdirSync as readdirSync19, statSync as statSync23 } from "node:fs";
71576
+ import { existsSync as existsSync56, readFileSync as readFileSync51, readdirSync as readdirSync19, statSync as statSync24 } from "node:fs";
71204
71577
  import { resolve as resolve37, join as join49 } from "node:path";
71205
71578
  import { createHash as createHash11 } from "node:crypto";
71206
71579
  init_merge();
@@ -71216,7 +71589,7 @@ function sha256(content) {
71216
71589
  }
71217
71590
  function findLatestTranscriptJsonl(claudeConfigDir) {
71218
71591
  const projectsDir = join49(claudeConfigDir, "projects");
71219
- if (!existsSync55(projectsDir))
71592
+ if (!existsSync56(projectsDir))
71220
71593
  return;
71221
71594
  try {
71222
71595
  const entries = readdirSync19(projectsDir, { withFileTypes: true });
@@ -71226,9 +71599,9 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
71226
71599
  continue;
71227
71600
  const projectPath = join49(projectsDir, entry.name);
71228
71601
  const transcriptPath = join49(projectPath, "transcript.jsonl");
71229
- if (!existsSync55(transcriptPath))
71602
+ if (!existsSync56(transcriptPath))
71230
71603
  continue;
71231
- const stat3 = statSync23(transcriptPath);
71604
+ const stat3 = statSync24(transcriptPath);
71232
71605
  if (!latest || stat3.mtimeMs > latest.mtime) {
71233
71606
  latest = { path: transcriptPath, mtime: stat3.mtimeMs };
71234
71607
  }
@@ -71289,7 +71662,7 @@ function registerDebugCommand(program3) {
71289
71662
  }
71290
71663
  const agentsDir = resolveAgentsDir(config);
71291
71664
  const agentDir = resolve37(agentsDir, agentName);
71292
- if (!existsSync55(agentDir)) {
71665
+ if (!existsSync56(agentDir)) {
71293
71666
  console.error(`Agent directory not found: ${agentDir}`);
71294
71667
  process.exit(1);
71295
71668
  }
@@ -71344,7 +71717,7 @@ function registerDebugCommand(program3) {
71344
71717
  }
71345
71718
  console.log(`=== Append System Prompt (per-session) ===
71346
71719
  `);
71347
- const handoffContent = existsSync55(handoffPath) ? readFileSync51(handoffPath, "utf-8") : "";
71720
+ const handoffContent = existsSync56(handoffPath) ? readFileSync51(handoffPath, "utf-8") : "";
71348
71721
  if (handoffContent.trim().length > 0) {
71349
71722
  console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
71350
71723
  console.log(handoffContent);
@@ -71355,7 +71728,7 @@ function registerDebugCommand(program3) {
71355
71728
  }
71356
71729
  console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
71357
71730
  `);
71358
- const claudeMdContent = existsSync55(claudeMdPath) ? readFileSync51(claudeMdPath, "utf-8") : "";
71731
+ const claudeMdContent = existsSync56(claudeMdPath) ? readFileSync51(claudeMdPath, "utf-8") : "";
71359
71732
  if (claudeMdContent.trim().length > 0) {
71360
71733
  console.log(`(${formatBytes(claudeMdContent.length)})`);
71361
71734
  console.log(claudeMdContent);
@@ -71366,7 +71739,7 @@ function registerDebugCommand(program3) {
71366
71739
  }
71367
71740
  console.log(`=== Persona (SOUL.md) ===
71368
71741
  `);
71369
- const soulMdContent = existsSync55(soulMdPath) ? readFileSync51(soulMdPath, "utf-8") : existsSync55(workspaceSoulMdPath) ? readFileSync51(workspaceSoulMdPath, "utf-8") : "";
71742
+ const soulMdContent = existsSync56(soulMdPath) ? readFileSync51(soulMdPath, "utf-8") : existsSync56(workspaceSoulMdPath) ? readFileSync51(workspaceSoulMdPath, "utf-8") : "";
71370
71743
  if (soulMdContent.trim().length > 0) {
71371
71744
  console.log(`(${formatBytes(soulMdContent.length)})`);
71372
71745
  console.log(soulMdContent);
@@ -71447,7 +71820,7 @@ init_source();
71447
71820
 
71448
71821
  // src/worktree/claim.ts
71449
71822
  import { execFileSync as execFileSync16 } from "node:child_process";
71450
- import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as existsSync57, unlinkSync as unlinkSync12 } from "node:fs";
71823
+ import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as existsSync58, unlinkSync as unlinkSync12 } from "node:fs";
71451
71824
  import { join as join51, resolve as resolve39 } from "node:path";
71452
71825
  import { homedir as homedir27 } from "node:os";
71453
71826
  import { randomBytes as randomBytes11 } from "node:crypto";
@@ -71459,7 +71832,7 @@ import {
71459
71832
  readFileSync as readFileSync52,
71460
71833
  readdirSync as readdirSync20,
71461
71834
  unlinkSync as unlinkSync11,
71462
- existsSync as existsSync56,
71835
+ existsSync as existsSync57,
71463
71836
  renameSync as renameSync11
71464
71837
  } from "node:fs";
71465
71838
  import { join as join50, resolve as resolve38 } from "node:path";
@@ -71573,7 +71946,7 @@ function expandHome(p) {
71573
71946
  }
71574
71947
  async function claimWorktree(input, codeRepos) {
71575
71948
  const repoPath = resolveRepoPath(input.repo, codeRepos);
71576
- if (!existsSync57(repoPath)) {
71949
+ if (!existsSync58(repoPath)) {
71577
71950
  throw new Error(`Repository path does not exist: ${repoPath}`);
71578
71951
  }
71579
71952
  let concurrencyCap = DEFAULT_CONCURRENCY;
@@ -71627,7 +72000,7 @@ async function claimWorktree(input, codeRepos) {
71627
72000
 
71628
72001
  // src/worktree/release.ts
71629
72002
  import { execFileSync as execFileSync17 } from "node:child_process";
71630
- import { existsSync as existsSync58 } from "node:fs";
72003
+ import { existsSync as existsSync59 } from "node:fs";
71631
72004
  function releaseWorktree(input) {
71632
72005
  const { id } = input;
71633
72006
  const record2 = readRecord(id);
@@ -71635,7 +72008,7 @@ function releaseWorktree(input) {
71635
72008
  return { released: true };
71636
72009
  }
71637
72010
  let gitSuccess = true;
71638
- if (existsSync58(record2.path)) {
72011
+ if (existsSync59(record2.path)) {
71639
72012
  try {
71640
72013
  execFileSync17("git", ["worktree", "remove", "--force", record2.path], {
71641
72014
  cwd: record2.repo,
@@ -71674,7 +72047,7 @@ function listWorktrees() {
71674
72047
 
71675
72048
  // src/worktree/reaper.ts
71676
72049
  import { execFileSync as execFileSync18 } from "node:child_process";
71677
- import { existsSync as existsSync59 } from "node:fs";
72050
+ import { existsSync as existsSync60 } from "node:fs";
71678
72051
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
71679
72052
  function isPathInUse(path7) {
71680
72053
  try {
@@ -71701,7 +72074,7 @@ function hasUncommittedChanges(repoPath, worktreePath) {
71701
72074
  function reapRecord(record2) {
71702
72075
  const { id, path: path7, repo, branch, ownerAgent } = record2;
71703
72076
  let warning = null;
71704
- if (existsSync59(path7)) {
72077
+ if (existsSync60(path7)) {
71705
72078
  if (hasUncommittedChanges(repo, path7)) {
71706
72079
  warning = `[worktree-reaper] Reaped worktree with uncommitted changes: ` + `id=${id} branch=${branch} agent=${ownerAgent ?? "unknown"} path=${path7}`;
71707
72080
  }
@@ -71722,7 +72095,7 @@ function runReaper(nowMs) {
71722
72095
  const warnings = [];
71723
72096
  for (const record2 of records) {
71724
72097
  const heartbeatAge = now - new Date(record2.heartbeatAt).getTime();
71725
- const worktreeExists = existsSync59(record2.path);
72098
+ const worktreeExists = existsSync60(record2.path);
71726
72099
  if (!worktreeExists) {
71727
72100
  deleteRecord(record2.id);
71728
72101
  reaped.push(record2.id);
@@ -72177,7 +72550,7 @@ function registerDriveMcpLauncherCommand(program3) {
72177
72550
 
72178
72551
  // src/cli/apply.ts
72179
72552
  init_source();
72180
- import { accessSync as accessSync2, constants as fsConstants5, copyFileSync as copyFileSync11, existsSync as existsSync62, mkdirSync as mkdirSync35, readdirSync as readdirSync22, renameSync as renameSync12, writeFileSync as writeFileSync32 } from "node:fs";
72553
+ import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync64, mkdirSync as mkdirSync35, readdirSync as readdirSync23, renameSync as renameSync12, writeFileSync as writeFileSync32 } from "node:fs";
72181
72554
  import { mkdir, writeFile } from "node:fs/promises";
72182
72555
  import { spawnSync as childSpawnSync } from "node:child_process";
72183
72556
  import readline from "node:readline";
@@ -72522,7 +72895,7 @@ agents:
72522
72895
 
72523
72896
  // src/cli/apply.ts
72524
72897
  init_resolver();
72525
- import { dirname as dirname18, join as join55, resolve as resolve41 } from "node:path";
72898
+ import { dirname as dirname19, join as join56, resolve as resolve41 } from "node:path";
72526
72899
  import { homedir as homedir29 } from "node:os";
72527
72900
  import { execFileSync as execFileSync19 } from "node:child_process";
72528
72901
  init_vault();
@@ -72530,7 +72903,7 @@ init_loader();
72530
72903
  init_loader();
72531
72904
 
72532
72905
  // src/cli/update-prompt-hook.ts
72533
- import { existsSync as existsSync60, readFileSync as readFileSync53, writeFileSync as writeFileSync31, chmodSync as chmodSync10, mkdirSync as mkdirSync34 } from "node:fs";
72906
+ import { existsSync as existsSync61, readFileSync as readFileSync53, writeFileSync as writeFileSync31, chmodSync as chmodSync10, mkdirSync as mkdirSync34 } from "node:fs";
72534
72907
  import { join as join53 } from "node:path";
72535
72908
  var HOOK_FILENAME = "update-card-on-prompt.sh";
72536
72909
  function updatePromptHookScript() {
@@ -72602,7 +72975,7 @@ function installUpdatePromptHook(agentDir) {
72602
72975
  const scriptPath = join53(hooksDir, HOOK_FILENAME);
72603
72976
  const desired = updatePromptHookScript();
72604
72977
  let installed = false;
72605
- const existing = existsSync60(scriptPath) ? readFileSync53(scriptPath, "utf-8") : "";
72978
+ const existing = existsSync61(scriptPath) ? readFileSync53(scriptPath, "utf-8") : "";
72606
72979
  if (existing !== desired) {
72607
72980
  writeFileSync31(scriptPath, desired, { mode: 493 });
72608
72981
  chmodSync10(scriptPath, 493);
@@ -72613,7 +72986,7 @@ function installUpdatePromptHook(agentDir) {
72613
72986
  } catch {}
72614
72987
  }
72615
72988
  const settingsPath = join53(agentDir, ".claude", "settings.json");
72616
- if (!existsSync60(settingsPath)) {
72989
+ if (!existsSync61(settingsPath)) {
72617
72990
  return { scriptPath, settingsPath, installed };
72618
72991
  }
72619
72992
  const raw = readFileSync53(settingsPath, "utf-8");
@@ -72729,24 +73102,114 @@ function detectInstallType() {
72729
73102
  }
72730
73103
  }
72731
73104
 
73105
+ // src/cli/operator-uid.ts
73106
+ import {
73107
+ chownSync as chownSync2,
73108
+ existsSync as existsSync63,
73109
+ lstatSync as lstatSync7,
73110
+ readdirSync as readdirSync22,
73111
+ realpathSync as realpathSync6,
73112
+ statSync as statSync25
73113
+ } from "node:fs";
73114
+ import { join as join55 } from "node:path";
73115
+ function resolveOperatorUid() {
73116
+ const sudoUid = process.env.SUDO_UID;
73117
+ if (sudoUid !== undefined) {
73118
+ const parsed = parseInt(sudoUid, 10);
73119
+ if (Number.isFinite(parsed) && parsed > 0)
73120
+ return parsed;
73121
+ }
73122
+ if (typeof process.getuid === "function") {
73123
+ const uid = process.getuid();
73124
+ if (uid > 0)
73125
+ return uid;
73126
+ }
73127
+ return;
73128
+ }
73129
+ function operatorOwnedPaths(home2) {
73130
+ const root = join55(home2, ".switchroom");
73131
+ return [
73132
+ join55(root, "vault"),
73133
+ join55(root, "vault-auto-unlock"),
73134
+ join55(root, "vault-audit.log"),
73135
+ join55(root, "host-control-audit.log"),
73136
+ join55(root, "accounts"),
73137
+ join55(root, "compose")
73138
+ ];
73139
+ }
73140
+ function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
73141
+ const chown = deps.chown ?? ((p, u, g) => chownSync2(p, u, g));
73142
+ const exists = deps.exists ?? ((p) => existsSync63(p));
73143
+ const isSymlink = deps.isSymlink ?? ((p) => {
73144
+ try {
73145
+ return lstatSync7(p).isSymbolicLink();
73146
+ } catch {
73147
+ return false;
73148
+ }
73149
+ });
73150
+ const isDir = deps.isDir ?? ((p) => {
73151
+ try {
73152
+ return statSync25(p).isDirectory();
73153
+ } catch {
73154
+ return false;
73155
+ }
73156
+ });
73157
+ const realpath2 = deps.realpath ?? ((p) => {
73158
+ try {
73159
+ return realpathSync6(p);
73160
+ } catch {
73161
+ return p;
73162
+ }
73163
+ });
73164
+ const readdir2 = deps.readdir ?? ((p) => {
73165
+ try {
73166
+ return readdirSync22(p);
73167
+ } catch {
73168
+ return [];
73169
+ }
73170
+ });
73171
+ const chowned = [];
73172
+ const seen = new Set;
73173
+ const visit = (path8) => {
73174
+ if (!exists(path8))
73175
+ return;
73176
+ const target = isSymlink(path8) ? realpath2(path8) : path8;
73177
+ if (seen.has(target))
73178
+ return;
73179
+ seen.add(target);
73180
+ try {
73181
+ chown(target, operatorUid, operatorUid);
73182
+ chowned.push(target);
73183
+ } catch {}
73184
+ if (isDir(target)) {
73185
+ for (const entry of readdir2(target)) {
73186
+ visit(join55(target, entry));
73187
+ }
73188
+ }
73189
+ };
73190
+ for (const p of operatorOwnedPaths(home2))
73191
+ visit(p);
73192
+ return chowned;
73193
+ }
73194
+
72732
73195
  // src/cli/apply.ts
72733
73196
  var EMBEDDED_EXAMPLES = {
72734
73197
  switchroom: switchroom_default,
72735
73198
  minimal: minimal_default
72736
73199
  };
72737
- var DEFAULT_COMPOSE_PATH2 = join55(homedir29(), ".switchroom", "compose", "docker-compose.yml");
73200
+ var DEFAULT_COMPOSE_PATH2 = join56(homedir29(), ".switchroom", "compose", "docker-compose.yml");
72738
73201
  var COMPOSE_PROJECT2 = "switchroom";
72739
73202
  function resolveVaultBindMountDir(homeDir, ctx) {
72740
73203
  const isCustomPath = ctx.migrationKind === "custom-path-skipped";
72741
73204
  if (isCustomPath && ctx.customVaultPath) {
72742
- return dirname18(ctx.customVaultPath);
73205
+ return dirname19(ctx.customVaultPath);
72743
73206
  }
72744
- return join55(homeDir, ".switchroom", "vault");
73207
+ return join56(homeDir, ".switchroom", "vault");
72745
73208
  }
72746
73209
  function inspectVaultBindMountDir(vaultDir) {
72747
- if (!existsSync62(vaultDir))
73210
+ if (!existsSync64(vaultDir))
72748
73211
  return { kind: "missing" };
72749
- const entries = readdirSync22(vaultDir);
73212
+ const entries = readdirSync23(vaultDir);
72750
73213
  const unknown = [];
72751
73214
  for (const name of entries) {
72752
73215
  if (KNOWN_VAULT_ARTIFACT_NAMES.has(name))
@@ -72772,30 +73235,30 @@ function hasVaultRefs(value) {
72772
73235
  async function ensureHostMountSources(config) {
72773
73236
  const home2 = homedir29();
72774
73237
  const dirs = [
72775
- join55(home2, ".switchroom", "approvals"),
72776
- join55(home2, ".switchroom", "scheduler"),
72777
- join55(home2, ".switchroom", "logs"),
72778
- join55(home2, ".switchroom", "compose"),
72779
- join55(home2, ".switchroom", "broker-operator")
73238
+ join56(home2, ".switchroom", "approvals"),
73239
+ join56(home2, ".switchroom", "scheduler"),
73240
+ join56(home2, ".switchroom", "logs"),
73241
+ join56(home2, ".switchroom", "compose"),
73242
+ join56(home2, ".switchroom", "broker-operator")
72780
73243
  ];
72781
73244
  for (const name of Object.keys(config.agents)) {
72782
- dirs.push(join55(home2, ".switchroom", "agents", name));
72783
- dirs.push(join55(home2, ".switchroom", "logs", name));
72784
- dirs.push(join55(home2, ".claude", "projects", name));
73245
+ dirs.push(join56(home2, ".switchroom", "agents", name));
73246
+ dirs.push(join56(home2, ".switchroom", "logs", name));
73247
+ dirs.push(join56(home2, ".claude", "projects", name));
72785
73248
  }
72786
73249
  for (const dir of dirs) {
72787
73250
  await mkdir(dir, { recursive: true });
72788
73251
  }
72789
- const autoUnlockPath = join55(home2, ".switchroom", "vault-auto-unlock");
72790
- if (!existsSync62(autoUnlockPath)) {
73252
+ const autoUnlockPath = join56(home2, ".switchroom", "vault-auto-unlock");
73253
+ if (!existsSync64(autoUnlockPath)) {
72791
73254
  writeFileSync32(autoUnlockPath, "", { mode: 384 });
72792
73255
  }
72793
- const auditLogPath = join55(home2, ".switchroom", "vault-audit.log");
72794
- if (!existsSync62(auditLogPath)) {
73256
+ const auditLogPath = join56(home2, ".switchroom", "vault-audit.log");
73257
+ if (!existsSync64(auditLogPath)) {
72795
73258
  writeFileSync32(auditLogPath, "", { mode: 420 });
72796
73259
  }
72797
- const hostdAuditLogPath = join55(home2, ".switchroom", "host-control-audit.log");
72798
- if (!existsSync62(hostdAuditLogPath)) {
73260
+ const hostdAuditLogPath = join56(home2, ".switchroom", "host-control-audit.log");
73261
+ if (!existsSync64(hostdAuditLogPath)) {
72799
73262
  writeFileSync32(hostdAuditLogPath, "", { mode: 420 });
72800
73263
  }
72801
73264
  }
@@ -72816,7 +73279,7 @@ ${out.trim()}`;
72816
73279
  }
72817
73280
  function runApplyPreflight(config, opts = {}) {
72818
73281
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
72819
- if (hasVaultRefs(config) && !existsSync62(vaultPath)) {
73282
+ if (hasVaultRefs(config) && !existsSync64(vaultPath)) {
72820
73283
  throw new Error(`Config references vault keys (vault:<name>) but ${vaultPath} is missing. ` + `Run \`switchroom setup\` first to initialise the vault.`);
72821
73284
  }
72822
73285
  const detect = opts.detectComposeV2 ?? detectComposeV2;
@@ -72827,7 +73290,7 @@ function runApplyPreflight(config, opts = {}) {
72827
73290
  detectAndReportLegacyGdriveSlots(vaultPath);
72828
73291
  }
72829
73292
  function detectAndReportLegacyGdriveSlots(vaultPath) {
72830
- if (!existsSync62(vaultPath))
73293
+ if (!existsSync64(vaultPath))
72831
73294
  return;
72832
73295
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
72833
73296
  if (!passphrase)
@@ -72868,8 +73331,8 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
72868
73331
  }
72869
73332
  function writeInstallTypeCache(homeDir = homedir29()) {
72870
73333
  const ctx = detectInstallType();
72871
- const dir = join55(homeDir, ".switchroom");
72872
- const out = join55(dir, "install-type.json");
73334
+ const dir = join56(homeDir, ".switchroom");
73335
+ const out = join56(dir, "install-type.json");
72873
73336
  const tmp = `${out}.tmp`;
72874
73337
  mkdirSync35(dir, { recursive: true });
72875
73338
  const payload = {
@@ -72918,14 +73381,14 @@ Applying switchroom config...
72918
73381
  writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
72919
73382
  `));
72920
73383
  try {
72921
- installUpdatePromptHook(join55(agentsDir, name));
73384
+ installUpdatePromptHook(join56(agentsDir, name));
72922
73385
  } catch (hookErr) {
72923
73386
  writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
72924
73387
  `));
72925
73388
  }
72926
73389
  try {
72927
73390
  const uid = allocateAgentUid(name);
72928
- alignAgentUid(name, join55(agentsDir, name), uid, {
73391
+ alignAgentUid(name, join56(agentsDir, name), uid, {
72929
73392
  confirm: !options.nonInteractive,
72930
73393
  writeOut
72931
73394
  });
@@ -72962,7 +73425,7 @@ Applying switchroom config...
72962
73425
  for (const name of agentNames) {
72963
73426
  try {
72964
73427
  const uid = allocateAgentUid(name);
72965
- alignAgentUid(name, join55(agentsDir, name), uid, {
73428
+ alignAgentUid(name, join56(agentsDir, name), uid, {
72966
73429
  confirm: !options.nonInteractive,
72967
73430
  writeOut
72968
73431
  });
@@ -73036,20 +73499,7 @@ Applying switchroom config...
73036
73499
  process.exit(6);
73037
73500
  }
73038
73501
  const composePath = options.outPath ?? DEFAULT_COMPOSE_PATH2;
73039
- const operatorUid = (() => {
73040
- const sudoUid = process.env.SUDO_UID;
73041
- if (sudoUid !== undefined) {
73042
- const parsed = parseInt(sudoUid, 10);
73043
- if (Number.isFinite(parsed) && parsed > 0)
73044
- return parsed;
73045
- }
73046
- if (typeof process.getuid === "function") {
73047
- const uid = process.getuid();
73048
- if (uid > 0)
73049
- return uid;
73050
- }
73051
- return;
73052
- })();
73502
+ const operatorUid = resolveOperatorUid();
73053
73503
  const composeRelease = resolveRelease({
73054
73504
  override: options.releaseOverride,
73055
73505
  root: config.release
@@ -73064,7 +73514,7 @@ Applying switchroom config...
73064
73514
  switchroomConfigPath,
73065
73515
  operatorUid
73066
73516
  });
73067
- await mkdir(dirname18(composePath), { recursive: true });
73517
+ await mkdir(dirname19(composePath), { recursive: true });
73068
73518
  await writeFile(composePath, composeContent, {
73069
73519
  encoding: "utf8",
73070
73520
  mode: 384
@@ -73079,6 +73529,13 @@ Wrote `) + composePath + source_default.gray(` (${composeBytes} bytes)
73079
73529
  `);
73080
73530
  writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
73081
73531
  `));
73532
+ if (process.geteuid?.() === 0 && operatorUid !== undefined) {
73533
+ const restored = restoreOperatorOwnership(homedir29(), operatorUid);
73534
+ if (restored.length > 0) {
73535
+ writeOut(source_default.gray(` Restored operator ownership of ${restored.length} ~/.switchroom path(s)
73536
+ `));
73537
+ }
73538
+ }
73082
73539
  writeOut(source_default.bold(`
73083
73540
  Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
73084
73541
  `));
@@ -73121,7 +73578,7 @@ function copyExampleConfig2(name) {
73121
73578
  throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
73122
73579
  }
73123
73580
  const dest = resolve41(process.cwd(), "switchroom.yaml");
73124
- if (existsSync62(dest)) {
73581
+ if (existsSync64(dest)) {
73125
73582
  console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
73126
73583
  return;
73127
73584
  }
@@ -73132,7 +73589,7 @@ function copyExampleConfig2(name) {
73132
73589
  return;
73133
73590
  }
73134
73591
  const exampleFile = resolve41(import.meta.dirname, `../../examples/${name}.yaml`);
73135
- if (!existsSync62(exampleFile)) {
73592
+ if (!existsSync64(exampleFile)) {
73136
73593
  throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
73137
73594
  }
73138
73595
  copyFileSync11(exampleFile, dest);
@@ -73143,11 +73600,11 @@ function findUnwritableAgentDirs(config, opts) {
73143
73600
  const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
73144
73601
  const unwritable = [];
73145
73602
  for (const name of targets) {
73146
- const startSh = join55(agentsDir, name, "start.sh");
73147
- if (!existsSync62(startSh))
73603
+ const startSh = join56(agentsDir, name, "start.sh");
73604
+ if (!existsSync64(startSh))
73148
73605
  continue;
73149
73606
  try {
73150
- accessSync2(startSh, fsConstants5.W_OK);
73607
+ accessSync3(startSh, fsConstants6.W_OK);
73151
73608
  } catch {
73152
73609
  unwritable.push(name);
73153
73610
  }
@@ -73322,8 +73779,8 @@ function runRedactStdin() {
73322
73779
  }
73323
73780
 
73324
73781
  // src/cli/status-ask.ts
73325
- import { readFileSync as readFileSync54, existsSync as existsSync63, readdirSync as readdirSync23 } from "node:fs";
73326
- import { join as join56 } from "node:path";
73782
+ import { readFileSync as readFileSync54, existsSync as existsSync65, readdirSync as readdirSync24 } from "node:fs";
73783
+ import { join as join57 } from "node:path";
73327
73784
  import { homedir as homedir30 } from "node:os";
73328
73785
 
73329
73786
  // src/status-ask/report.ts
@@ -73645,7 +74102,7 @@ function runReport(opts) {
73645
74102
  function resolveSources(explicitPath) {
73646
74103
  if (explicitPath != null && explicitPath.trim() !== "") {
73647
74104
  const trimmed = explicitPath.trim();
73648
- if (!existsSync63(trimmed)) {
74105
+ if (!existsSync65(trimmed)) {
73649
74106
  process.stderr.write(`status-ask report: ${trimmed}: file not found
73650
74107
  `);
73651
74108
  process.exit(1);
@@ -73659,20 +74116,20 @@ function resolveSources(explicitPath) {
73659
74116
  const config = loadConfig();
73660
74117
  agentsDir = resolveAgentsDir(config);
73661
74118
  } catch {
73662
- agentsDir = join56(homedir30(), ".switchroom", "agents");
74119
+ agentsDir = join57(homedir30(), ".switchroom", "agents");
73663
74120
  }
73664
- if (!existsSync63(agentsDir))
74121
+ if (!existsSync65(agentsDir))
73665
74122
  return [];
73666
74123
  const sources = [];
73667
74124
  let entries;
73668
74125
  try {
73669
- entries = readdirSync23(agentsDir);
74126
+ entries = readdirSync24(agentsDir);
73670
74127
  } catch {
73671
74128
  return [];
73672
74129
  }
73673
74130
  for (const name of entries) {
73674
- const path8 = join56(agentsDir, name, "runtime-metrics.jsonl");
73675
- if (existsSync63(path8)) {
74131
+ const path8 = join57(agentsDir, name, "runtime-metrics.jsonl");
74132
+ if (existsSync65(path8)) {
73676
74133
  sources.push({ path: path8, agent: name });
73677
74134
  }
73678
74135
  }
@@ -73693,17 +74150,17 @@ function inferAgentFromPath(p) {
73693
74150
 
73694
74151
  // src/cli/agent-config.ts
73695
74152
  init_helpers();
73696
- import { join as join57 } from "node:path";
74153
+ import { join as join58 } from "node:path";
73697
74154
  import { homedir as homedir31 } from "node:os";
73698
74155
  import {
73699
- existsSync as existsSync64,
74156
+ existsSync as existsSync66,
73700
74157
  mkdirSync as mkdirSync36,
73701
74158
  appendFileSync as appendFileSync3,
73702
74159
  readFileSync as readFileSync55
73703
74160
  } from "node:fs";
73704
- var AUDIT_ROOT = join57(homedir31(), ".switchroom", "audit");
74161
+ var AUDIT_ROOT = join58(homedir31(), ".switchroom", "audit");
73705
74162
  function auditPathFor(agent) {
73706
- return join57(AUDIT_ROOT, agent, "agent-config.jsonl");
74163
+ return join58(AUDIT_ROOT, agent, "agent-config.jsonl");
73707
74164
  }
73708
74165
  function appendAudit(agent, cmd, args, exit, opts = {}) {
73709
74166
  const row = {
@@ -73717,7 +74174,7 @@ function appendAudit(agent, cmd, args, exit, opts = {}) {
73717
74174
  const path8 = opts.auditPath ?? auditPathFor(agent);
73718
74175
  const dir = path8.slice(0, path8.lastIndexOf("/"));
73719
74176
  try {
73720
- if (!existsSync64(dir)) {
74177
+ if (!existsSync66(dir)) {
73721
74178
  mkdirSync36(dir, { recursive: true });
73722
74179
  }
73723
74180
  appendFileSync3(path8, JSON.stringify(row) + `
@@ -73729,7 +74186,7 @@ function isContainerContext(env2 = process.env, opts = {}) {
73729
74186
  return true;
73730
74187
  const probe2 = opts.dockerEnvPath ?? "/.dockerenv";
73731
74188
  try {
73732
- if (existsSync64(probe2))
74189
+ if (existsSync66(probe2))
73733
74190
  return true;
73734
74191
  } catch {}
73735
74192
  return false;
@@ -73790,7 +74247,7 @@ function getAgentSlice(config, agent) {
73790
74247
  }
73791
74248
  function readAuditTail(agent, limit, opts = {}) {
73792
74249
  const path8 = opts.auditPath ?? auditPathFor(agent);
73793
- if (!existsSync64(path8))
74250
+ if (!existsSync66(path8))
73794
74251
  return [];
73795
74252
  let raw;
73796
74253
  try {
@@ -73950,32 +74407,32 @@ var import_yaml14 = __toESM(require_dist(), 1);
73950
74407
  init_paths();
73951
74408
  import {
73952
74409
  closeSync as closeSync12,
73953
- existsSync as existsSync65,
74410
+ existsSync as existsSync67,
73954
74411
  fsyncSync as fsyncSync5,
73955
74412
  mkdirSync as mkdirSync37,
73956
74413
  openSync as openSync12,
73957
- readdirSync as readdirSync24,
74414
+ readdirSync as readdirSync25,
73958
74415
  readFileSync as readFileSync56,
73959
74416
  renameSync as renameSync13,
73960
- statSync as statSync24,
74417
+ statSync as statSync26,
73961
74418
  unlinkSync as unlinkSync13,
73962
74419
  writeSync as writeSync7
73963
74420
  } from "node:fs";
73964
- import { join as join58, resolve as resolve42 } from "node:path";
74421
+ import { join as join59, resolve as resolve42 } from "node:path";
73965
74422
  var STAGING_SUBDIR = ".staging";
73966
74423
  function overlayPathsFor(agent, opts = {}) {
73967
74424
  const base = opts.root ? resolve42(opts.root, agent) : resolve42(resolveDualPath(`~/.switchroom/agents/${agent}`));
73968
- const scheduleDir = join58(base, "schedule.d");
73969
- const scheduleStagingDir = join58(scheduleDir, STAGING_SUBDIR);
73970
- const skillsDir = join58(base, "skills.d");
73971
- const skillsStagingDir = join58(skillsDir, STAGING_SUBDIR);
74425
+ const scheduleDir = join59(base, "schedule.d");
74426
+ const scheduleStagingDir = join59(scheduleDir, STAGING_SUBDIR);
74427
+ const skillsDir = join59(base, "skills.d");
74428
+ const skillsStagingDir = join59(skillsDir, STAGING_SUBDIR);
73972
74429
  return {
73973
74430
  agentRoot: base,
73974
74431
  scheduleDir,
73975
74432
  scheduleStagingDir,
73976
74433
  skillsDir,
73977
74434
  skillsStagingDir,
73978
- lockPath: join58(base, ".lock"),
74435
+ lockPath: join59(base, ".lock"),
73979
74436
  stagingDir: scheduleStagingDir
73980
74437
  };
73981
74438
  }
@@ -74001,7 +74458,7 @@ function withAgentLock(paths, fn) {
74001
74458
  if (e.code !== "EEXIST")
74002
74459
  throw err;
74003
74460
  try {
74004
- const age = Date.now() - statSync24(paths.lockPath).mtimeMs;
74461
+ const age = Date.now() - statSync26(paths.lockPath).mtimeMs;
74005
74462
  if (age > 30000) {
74006
74463
  unlinkSync13(paths.lockPath);
74007
74464
  continue;
@@ -74029,8 +74486,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
74029
74486
  const paths = overlayPathsFor(agent, opts);
74030
74487
  return withAgentLock(paths, () => {
74031
74488
  ensureDirs(paths);
74032
- const stagingPath = join58(paths.scheduleStagingDir, `${slug}.yaml`);
74033
- const finalPath = join58(paths.scheduleDir, `${slug}.yaml`);
74489
+ const stagingPath = join59(paths.scheduleStagingDir, `${slug}.yaml`);
74490
+ const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
74034
74491
  const fd = openSync12(stagingPath, "w", 384);
74035
74492
  try {
74036
74493
  writeSync7(fd, yamlText);
@@ -74046,8 +74503,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
74046
74503
  const paths = overlayPathsFor(agent, opts);
74047
74504
  return withAgentLock(paths, () => {
74048
74505
  ensureSkillsDirs(paths);
74049
- const stagingPath = join58(paths.skillsStagingDir, `${slug}.yaml`);
74050
- const finalPath = join58(paths.skillsDir, `${slug}.yaml`);
74506
+ const stagingPath = join59(paths.skillsStagingDir, `${slug}.yaml`);
74507
+ const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
74051
74508
  const fd = openSync12(stagingPath, "w", 384);
74052
74509
  try {
74053
74510
  writeSync7(fd, yamlText);
@@ -74062,8 +74519,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
74062
74519
  function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
74063
74520
  const paths = overlayPathsFor(agent, opts);
74064
74521
  return withAgentLock(paths, () => {
74065
- const finalPath = join58(paths.skillsDir, `${slug}.yaml`);
74066
- if (!existsSync65(finalPath))
74522
+ const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
74523
+ if (!existsSync67(finalPath))
74067
74524
  return false;
74068
74525
  unlinkSync13(finalPath);
74069
74526
  return true;
@@ -74071,13 +74528,13 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
74071
74528
  }
74072
74529
  function listSkillsOverlayEntries(agent, opts = {}) {
74073
74530
  const paths = overlayPathsFor(agent, opts);
74074
- if (!existsSync65(paths.skillsDir))
74531
+ if (!existsSync67(paths.skillsDir))
74075
74532
  return [];
74076
74533
  const out = [];
74077
- for (const name of readdirSync24(paths.skillsDir)) {
74534
+ for (const name of readdirSync25(paths.skillsDir)) {
74078
74535
  if (!/\.ya?ml$/i.test(name))
74079
74536
  continue;
74080
- const full = join58(paths.skillsDir, name);
74537
+ const full = join59(paths.skillsDir, name);
74081
74538
  try {
74082
74539
  const raw = readFileSync56(full, "utf-8");
74083
74540
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -74089,8 +74546,8 @@ function listSkillsOverlayEntries(agent, opts = {}) {
74089
74546
  function deleteOverlayEntry(agent, slug, opts = {}) {
74090
74547
  const paths = overlayPathsFor(agent, opts);
74091
74548
  return withAgentLock(paths, () => {
74092
- const finalPath = join58(paths.scheduleDir, `${slug}.yaml`);
74093
- if (!existsSync65(finalPath))
74549
+ const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
74550
+ if (!existsSync67(finalPath))
74094
74551
  return false;
74095
74552
  unlinkSync13(finalPath);
74096
74553
  return true;
@@ -74098,13 +74555,13 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
74098
74555
  }
74099
74556
  function listOverlayEntries(agent, opts = {}) {
74100
74557
  const paths = overlayPathsFor(agent, opts);
74101
- if (!existsSync65(paths.scheduleDir))
74558
+ if (!existsSync67(paths.scheduleDir))
74102
74559
  return [];
74103
74560
  const out = [];
74104
- for (const name of readdirSync24(paths.scheduleDir)) {
74561
+ for (const name of readdirSync25(paths.scheduleDir)) {
74105
74562
  if (!/\.ya?ml$/i.test(name))
74106
74563
  continue;
74107
- const full = join58(paths.scheduleDir, name);
74564
+ const full = join59(paths.scheduleDir, name);
74108
74565
  try {
74109
74566
  const raw = readFileSync56(full, "utf-8");
74110
74567
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -74249,23 +74706,23 @@ function reconcileAgentCronOnly(agent) {
74249
74706
  // src/cli/agent-config-pending.ts
74250
74707
  import {
74251
74708
  closeSync as closeSync13,
74252
- existsSync as existsSync66,
74709
+ existsSync as existsSync68,
74253
74710
  fsyncSync as fsyncSync6,
74254
74711
  mkdirSync as mkdirSync38,
74255
74712
  openSync as openSync13,
74256
- readdirSync as readdirSync25,
74713
+ readdirSync as readdirSync26,
74257
74714
  readFileSync as readFileSync57,
74258
74715
  renameSync as renameSync14,
74259
74716
  unlinkSync as unlinkSync14,
74260
74717
  writeFileSync as writeFileSync33,
74261
74718
  writeSync as writeSync8
74262
74719
  } from "node:fs";
74263
- import { join as join59 } from "node:path";
74720
+ import { join as join60 } from "node:path";
74264
74721
  import { randomBytes as randomBytes12 } from "node:crypto";
74265
74722
  var STAGE_ID_PREFIX = "cap_";
74266
74723
  function pendingDir(agent, opts = {}) {
74267
74724
  const paths = overlayPathsFor(agent, opts);
74268
- return join59(paths.scheduleDir, ".pending");
74725
+ return join60(paths.scheduleDir, ".pending");
74269
74726
  }
74270
74727
  function ensurePendingDir(agent, opts = {}) {
74271
74728
  const dir = pendingDir(agent, opts);
@@ -74278,8 +74735,8 @@ function newStageId() {
74278
74735
  function stagePendingScheduleEntry(opts) {
74279
74736
  const dir = ensurePendingDir(opts.agent, { root: opts.root });
74280
74737
  const stageId = opts.stageId ?? newStageId();
74281
- const yamlPath = join59(dir, `${stageId}.yaml`);
74282
- const metaPath = join59(dir, `${stageId}.meta.json`);
74738
+ const yamlPath = join60(dir, `${stageId}.yaml`);
74739
+ const metaPath = join60(dir, `${stageId}.meta.json`);
74283
74740
  const meta = {
74284
74741
  v: 1,
74285
74742
  stage_id: stageId,
@@ -74306,16 +74763,16 @@ function stagePendingScheduleEntry(opts) {
74306
74763
  }
74307
74764
  function listPendingScheduleEntries(agent, opts = {}) {
74308
74765
  const dir = pendingDir(agent, opts);
74309
- if (!existsSync66(dir))
74766
+ if (!existsSync68(dir))
74310
74767
  return [];
74311
74768
  const out = [];
74312
- for (const name of readdirSync25(dir).sort()) {
74769
+ for (const name of readdirSync26(dir).sort()) {
74313
74770
  if (!name.endsWith(".meta.json"))
74314
74771
  continue;
74315
74772
  const stageId = name.slice(0, -".meta.json".length);
74316
- const metaPath = join59(dir, name);
74317
- const yamlPath = join59(dir, `${stageId}.yaml`);
74318
- if (!existsSync66(yamlPath))
74773
+ const metaPath = join60(dir, name);
74774
+ const yamlPath = join60(dir, `${stageId}.yaml`);
74775
+ if (!existsSync68(yamlPath))
74319
74776
  continue;
74320
74777
  try {
74321
74778
  const meta = JSON.parse(readFileSync57(metaPath, "utf-8"));
@@ -74333,8 +74790,8 @@ function commitPendingScheduleEntry(opts) {
74333
74790
  return { committed: false, reason: "not_found" };
74334
74791
  const slug = match.meta.entry.name ?? match.stageId;
74335
74792
  const paths = overlayPathsFor(opts.agent, { root: opts.root });
74336
- const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
74337
- if (existsSync66(finalPath)) {
74793
+ const finalPath = join60(paths.scheduleDir, `${slug}.yaml`);
74794
+ if (existsSync68(finalPath)) {
74338
74795
  return { committed: false, reason: "slug_collision" };
74339
74796
  }
74340
74797
  renameSync14(match.yamlPath, finalPath);
@@ -74356,7 +74813,7 @@ function denyPendingScheduleEntry(opts) {
74356
74813
  }
74357
74814
 
74358
74815
  // src/cli/agent-config-write.ts
74359
- import { existsSync as existsSync67, readFileSync as readFileSync58 } from "node:fs";
74816
+ import { existsSync as existsSync69, readFileSync as readFileSync58 } from "node:fs";
74360
74817
  var MAX_ENTRIES_PER_AGENT = 20;
74361
74818
  function checkOperatorContext(verb, env2 = process.env) {
74362
74819
  if (env2.SWITCHROOM_OPERATOR === "1")
@@ -74590,7 +75047,7 @@ function scheduleRemove(opts) {
74590
75047
  }
74591
75048
  let priorContent = null;
74592
75049
  try {
74593
- if (existsSync67(match.path))
75050
+ if (existsSync69(match.path))
74594
75051
  priorContent = readFileSync58(match.path, "utf-8");
74595
75052
  } catch {}
74596
75053
  deleteOverlayEntry(agent, match.slug, { root: opts.root });
@@ -74783,10 +75240,10 @@ function registerAgentConfigWriteCommands(program3) {
74783
75240
 
74784
75241
  // src/cli/agent-config-skill-write.ts
74785
75242
  var import_yaml15 = __toESM(require_dist(), 1);
74786
- import { existsSync as existsSync68 } from "node:fs";
75243
+ import { existsSync as existsSync70 } from "node:fs";
74787
75244
  init_reconcile_default_skills();
74788
75245
  var import_yaml16 = __toESM(require_dist(), 1);
74789
- import { join as join60 } from "node:path";
75246
+ import { join as join61 } from "node:path";
74790
75247
  var MAX_SKILLS_PER_AGENT = 20;
74791
75248
  var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
74792
75249
  function exitCodeFor2(code) {
@@ -74861,8 +75318,8 @@ function skillInstall(opts) {
74861
75318
  return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
74862
75319
  }
74863
75320
  const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
74864
- const skillPath = join60(poolDir, skillName);
74865
- if (!existsSync68(skillPath)) {
75321
+ const skillPath = join61(poolDir, skillName);
75322
+ if (!existsSync70(skillPath)) {
74866
75323
  return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
74867
75324
  }
74868
75325
  const yamlText = import_yaml15.stringify({ skills: [skillName] });
@@ -75046,27 +75503,27 @@ init_source();
75046
75503
  init_helpers();
75047
75504
  init_loader();
75048
75505
  import {
75049
- existsSync as existsSync70,
75050
- readdirSync as readdirSync26,
75506
+ existsSync as existsSync72,
75507
+ readdirSync as readdirSync27,
75051
75508
  readFileSync as readFileSync60,
75052
75509
  renameSync as renameSync15,
75053
- statSync as statSync25,
75510
+ statSync as statSync27,
75054
75511
  unlinkSync as unlinkSync15
75055
75512
  } from "node:fs";
75056
75513
  import { createHash as createHash12 } from "node:crypto";
75057
- import { join as join61 } from "node:path";
75514
+ import { join as join62 } from "node:path";
75058
75515
  function planCronUnitRenames(agentsDir, agents) {
75059
75516
  const plans = [];
75060
75517
  for (const [agentName, agentConfig] of Object.entries(agents)) {
75061
75518
  const schedule = agentConfig.schedule ?? [];
75062
75519
  if (schedule.length === 0)
75063
75520
  continue;
75064
- const telegramDir = join61(agentsDir, agentName, "telegram");
75065
- if (!existsSync70(telegramDir))
75521
+ const telegramDir = join62(agentsDir, agentName, "telegram");
75522
+ if (!existsSync72(telegramDir))
75066
75523
  continue;
75067
75524
  let entries;
75068
75525
  try {
75069
- entries = readdirSync26(telegramDir);
75526
+ entries = readdirSync27(telegramDir);
75070
75527
  } catch {
75071
75528
  continue;
75072
75529
  }
@@ -75083,8 +75540,8 @@ function planCronUnitRenames(agentsDir, agents) {
75083
75540
  continue;
75084
75541
  plans.push({
75085
75542
  agent: agentName,
75086
- from: join61(telegramDir, file),
75087
- to: join61(telegramDir, canonical),
75543
+ from: join62(telegramDir, file),
75544
+ to: join62(telegramDir, canonical),
75088
75545
  scheduleIdx: idx,
75089
75546
  entry
75090
75547
  });
@@ -75096,7 +75553,7 @@ function sha256File2(path8) {
75096
75553
  return createHash12("sha256").update(readFileSync60(path8)).digest("hex");
75097
75554
  }
75098
75555
  function renamePair(from, to, opts = {}) {
75099
- if (existsSync70(to)) {
75556
+ if (existsSync72(to)) {
75100
75557
  let identical = false;
75101
75558
  try {
75102
75559
  identical = sha256File2(from) === sha256File2(to);
@@ -75189,7 +75646,7 @@ function registerMigrateCommand(program3) {
75189
75646
  const fromSidecar = p.from.replace(/\.sh$/, ".source");
75190
75647
  const toSidecar = p.to.replace(/\.sh$/, ".source");
75191
75648
  let sidecarStatus = null;
75192
- if (existsSync70(fromSidecar) && statSync25(fromSidecar).isFile()) {
75649
+ if (existsSync72(fromSidecar) && statSync27(fromSidecar).isFile()) {
75193
75650
  sidecarStatus = renamePair(fromSidecar, toSidecar);
75194
75651
  }
75195
75652
  switch (status.kind) {
@@ -75222,10 +75679,10 @@ function registerMigrateCommand(program3) {
75222
75679
  init_source();
75223
75680
  init_helpers();
75224
75681
  init_audit_reader();
75225
- import { existsSync as existsSync71, mkdirSync as mkdirSync39, readdirSync as readdirSync27, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync26, copyFileSync as copyFileSync12 } from "node:fs";
75682
+ import { existsSync as existsSync73, mkdirSync as mkdirSync39, readdirSync as readdirSync28, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync28, copyFileSync as copyFileSync12 } from "node:fs";
75226
75683
  import { homedir as homedir32 } from "node:os";
75227
- import { join as join62 } from "node:path";
75228
- import { spawnSync as spawnSync10 } from "node:child_process";
75684
+ import { join as join63 } from "node:path";
75685
+ import { spawnSync as spawnSync11 } from "node:child_process";
75229
75686
  var DEFAULT_IMAGE_TAG = "latest";
75230
75687
  var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
75231
75688
  function renderHostdComposeFile(opts) {
@@ -75308,14 +75765,14 @@ networks:
75308
75765
  `;
75309
75766
  }
75310
75767
  function hostdDir() {
75311
- return join62(homedir32(), ".switchroom", "hostd");
75768
+ return join63(homedir32(), ".switchroom", "hostd");
75312
75769
  }
75313
75770
  function hostdComposePath() {
75314
- return join62(hostdDir(), "docker-compose.yml");
75771
+ return join63(hostdDir(), "docker-compose.yml");
75315
75772
  }
75316
75773
  function backupExistingCompose() {
75317
75774
  const p = hostdComposePath();
75318
- if (!existsSync71(p))
75775
+ if (!existsSync73(p))
75319
75776
  return null;
75320
75777
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
75321
75778
  const bak = `${p}.bak-${ts}`;
@@ -75323,7 +75780,7 @@ function backupExistingCompose() {
75323
75780
  return bak;
75324
75781
  }
75325
75782
  function runDocker(args) {
75326
- const r = spawnSync10("docker", args, { encoding: "utf8" });
75783
+ const r = spawnSync11("docker", args, { encoding: "utf8" });
75327
75784
  return {
75328
75785
  ok: r.status === 0,
75329
75786
  stdout: r.stdout ?? "",
@@ -75391,7 +75848,7 @@ function doStatus() {
75391
75848
  const composeYml = hostdComposePath();
75392
75849
  console.log(source_default.bold("switchroom-hostd"));
75393
75850
  console.log("");
75394
- if (!existsSync71(composeYml)) {
75851
+ if (!existsSync73(composeYml)) {
75395
75852
  console.log(source_default.yellow(" compose: not installed"));
75396
75853
  console.log(source_default.dim(" run `switchroom hostd install` to set up."));
75397
75854
  return;
@@ -75412,15 +75869,15 @@ function doStatus() {
75412
75869
  } else {
75413
75870
  console.log(source_default.green(` container: ${ps.stdout.trim()}`));
75414
75871
  }
75415
- if (existsSync71(dir)) {
75872
+ if (existsSync73(dir)) {
75416
75873
  const entries = [];
75417
75874
  try {
75418
- for (const name of readdirSync27(dir)) {
75875
+ for (const name of readdirSync28(dir)) {
75419
75876
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
75420
75877
  continue;
75421
- const sockPath = join62(dir, name, "sock");
75422
- if (existsSync71(sockPath)) {
75423
- const st = statSync26(sockPath);
75878
+ const sockPath = join63(dir, name, "sock");
75879
+ if (existsSync73(sockPath)) {
75880
+ const st = statSync28(sockPath);
75424
75881
  if ((st.mode & 61440) === 49152) {
75425
75882
  entries.push(`${name} \u2192 ${sockPath}`);
75426
75883
  }
@@ -75438,7 +75895,7 @@ function doStatus() {
75438
75895
  }
75439
75896
  function doUninstall() {
75440
75897
  const composeYml = hostdComposePath();
75441
- if (!existsSync71(composeYml)) {
75898
+ if (!existsSync73(composeYml)) {
75442
75899
  console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
75443
75900
  return;
75444
75901
  }
@@ -75462,7 +75919,7 @@ function registerHostdCommand(program3) {
75462
75919
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
75463
75920
  hostd.command("audit").description("Tail and filter the hostd audit log (privileged-verb call history)").option("--tail <n>", "Number of matching entries to show (default: 50)", "50").option("--agent <name>", "Filter to a specific caller agent").option("--op <verb>", "Filter to a specific hostd verb (e.g. update_apply, agent_restart)").option("--error", "Show only failed (error/denied) entries").option("--verbose", "Show the captured stderr / error tail under each failed row").option("--path <file>", "Override audit log path (for debugging)").action((opts) => {
75464
75921
  const logPath = opts.path ?? defaultAuditLogPath2();
75465
- if (!existsSync71(logPath)) {
75922
+ if (!existsSync73(logPath)) {
75466
75923
  console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
75467
75924
  The log is created when hostd handles its first privileged-verb request.`));
75468
75925
  return;