switchroom 0.13.39 → 0.13.41

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.
@@ -10972,7 +10972,7 @@ var AgentBindMountSchema = exports_external.object({
10972
10972
  var ScheduleEntrySchema = exports_external.object({
10973
10973
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
10974
10974
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
10975
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10975
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10976
10976
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
10977
10977
  });
10978
10978
  var AgentSoulSchema = exports_external.object({
@@ -10972,7 +10972,7 @@ var AgentBindMountSchema = exports_external.object({
10972
10972
  var ScheduleEntrySchema = exports_external.object({
10973
10973
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
10974
10974
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
10975
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10975
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10976
10976
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
10977
10977
  });
10978
10978
  var AgentSoulSchema = exports_external.object({
@@ -13536,7 +13536,7 @@ var init_schema = __esm(() => {
13536
13536
  ScheduleEntrySchema = exports_external.object({
13537
13537
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
13538
13538
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
13539
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "\u2014 this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
13539
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "\u2014 this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
13540
13540
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
13541
13541
  });
13542
13542
  AgentSoulSchema = exports_external.object({
@@ -22991,6 +22991,9 @@ function generateCompose(opts) {
22991
22991
  lines.push(` - ${homePrefix}/.switchroom/vault-auto-unlock:/state/vault-auto-unlock:ro`);
22992
22992
  lines.push(` - ${homePrefix}/.switchroom/vault-audit.log:/root/.switchroom/vault-audit.log`);
22993
22993
  lines.push(` - ${homePrefix}/.switchroom/vault-grants.db:/root/.switchroom/vault-grants.db`);
22994
+ for (const a of describeAgents(config)) {
22995
+ lines.push(` - ${homePrefix}/.switchroom/agents/${a.name}/.vault-token:/root/.switchroom/agents/${a.name}/.vault-token`);
22996
+ }
22994
22997
  lines.push(` - /etc/machine-id:/etc/machine-id:ro`);
22995
22998
  lines.push(``);
22996
22999
  lines.push(` approval-kernel:`);
@@ -47744,8 +47747,8 @@ var {
47744
47747
  } = import__.default;
47745
47748
 
47746
47749
  // src/build-info.ts
47747
- var VERSION = "0.13.39";
47748
- var COMMIT_SHA = "8681f423";
47750
+ var VERSION = "0.13.41";
47751
+ var COMMIT_SHA = "c5897e47";
47749
47752
 
47750
47753
  // src/cli/agent.ts
47751
47754
  init_source();
@@ -47838,7 +47841,7 @@ import {
47838
47841
  } from "node:fs";
47839
47842
  import { homedir as homedir4 } from "node:os";
47840
47843
  import { execSync, execFileSync as execFileSync2 } from "node:child_process";
47841
- import { basename as basename3, join as join8, resolve as resolve10 } from "node:path";
47844
+ import { join as join8, resolve as resolve10 } from "node:path";
47842
47845
  import { createHash as createHash2 } from "node:crypto";
47843
47846
 
47844
47847
  // src/agents/cron-unit-name.ts
@@ -47849,14 +47852,10 @@ function cronUnitHash(cron, prompt) {
47849
47852
  function cronUnitName(cron, prompt) {
47850
47853
  return `cron-${cronUnitHash(cron, prompt)}`;
47851
47854
  }
47852
- function cronScriptFilename(cron, prompt) {
47853
- return `${cronUnitName(cron, prompt)}.sh`;
47854
- }
47855
47855
  var CRON_SCRIPT_BASENAME_RE = /^cron-[0-9a-f]{12}\.sh$/;
47856
47856
  var LEGACY_CRON_SCRIPT_BASENAME_RE = /^cron-(\d+)\.sh$/;
47857
47857
 
47858
47858
  // src/agents/scaffold.ts
47859
- init_overlay_loader();
47860
47859
  init_schema();
47861
47860
  init_merge();
47862
47861
  init_timezone();
@@ -48029,52 +48028,6 @@ function applyTelegramProgressGuidance(body, args) {
48029
48028
  return body;
48030
48029
  return body + buildTelegramProgressGuidance({ defaultChatId: args.defaultChatId });
48031
48030
  }
48032
- function buildCronTelegramGuidance(args) {
48033
- const issuesBlock = args.jobSlug ? `
48034
-
48035
- ## If you need to record a transient issue
48036
-
48037
- If something half-broken happens during this run (e.g. an upstream API timed out, a vault key was missing, a non-fatal data gap), record it via:
48038
-
48039
- \`\`\`
48040
- switchroom issues record --severity warn --source "cron:${args.jobSlug}" --code <stable-code> --summary "<one-line>" --detail "Fix: <one-line remediation, e.g. exact command the user can run>"
48041
- \`\`\`
48042
-
48043
- The \`--detail\` field is rendered as a "\u2192 Fix: ..." line under the issue on the user's Telegram card. Make it actionable \u2014 a copy-pastable command or a one-line action, not a description of the problem. Examples:
48044
-
48045
- - \`--detail "Fix: switchroom vault unlock"\` (when the vault broker is locked)
48046
- - \`--detail "Fix: re-run \`claude setup-token\` for ken@example.com"\` (when an account is unauthenticated)
48047
- - \`--detail "Fix: \\\`switchroom auth heal --account=<name>\\\`"\` (when an OAuth token expired)
48048
-
48049
- Skip \`--detail\` if there's no clean one-line fix \u2014 leaving it empty means the card shows just the summary, which is fine.
48050
-
48051
- Use the EXACT \`--source "cron:${args.jobSlug}"\` shown above \u2014 the cron wrapper auto-resolves issues with that source on a clean run. Picking a different source means the issue persists across recoveries.
48052
- ` : "";
48053
- return `
48054
-
48055
- ## Delivery instructions (cron context)
48056
-
48057
- This task runs as a one-shot \`claude -p\` invocation \u2014 there is no live Telegram session. Your stdout is discarded; the user will NOT see anything you print.
48058
-
48059
- To deliver your response to the user, you MUST call:
48060
-
48061
- \`\`\`
48062
- mcp__switchroom-telegram__reply(chat_id="${args.chatId}", text="<your message>")
48063
- \`\`\`
48064
-
48065
- The \`reply\` tool handles markdown\u2192HTML conversion, chunking, and all formatting automatically \u2014 write normal markdown and it will render correctly on the user's phone.
48066
-
48067
- After calling \`reply\`, print \`HEARTBEAT_OK\` as your final stdout line and nothing else. This confirms successful execution to the cron watchdog.
48068
-
48069
- If you have nothing useful to say (data is dull, all signals are nominal), print \`HEARTBEAT_OK\` without calling \`reply\` \u2014 a silent heartbeat is correct behaviour, not an error.
48070
- ${issuesBlock}`;
48071
- }
48072
- function applyCronTelegramGuidance(body, args) {
48073
- if (!args.chatId)
48074
- return body;
48075
- const trimmed = body.replace(/\s+$/, "");
48076
- return trimmed + buildCronTelegramGuidance({ chatId: args.chatId, jobSlug: args.jobSlug });
48077
- }
48078
48031
 
48079
48032
  // src/agents/scaffold.ts
48080
48033
  init_hindsight();
@@ -48584,70 +48537,6 @@ function parseDurationToSeconds(d) {
48584
48537
  return;
48585
48538
  }
48586
48539
  }
48587
- function buildCronScript(agentDir, prompt, model, chatId, userId, secrets = [], brokerSocket, jobSlug) {
48588
- const slug = jobSlug ?? "unknown";
48589
- const wrappedPrompt = applyCronTelegramGuidance(prompt, { chatId, jobSlug: slug });
48590
- const secretsComment = secrets.length > 0 ? `# Allowed vault keys for this cron (broker ACL): ${secrets.join(", ")}
48591
- ` : "";
48592
- const brokerSocketExport = brokerSocket ? `export SWITCHROOM_VAULT_BROKER_SOCK=${shellSingleQuote(brokerSocket)}
48593
- ` : "";
48594
- return `#!/bin/bash
48595
- # Auto-generated by switchroom scaffold/reconcile.
48596
- # One-shot scheduled task \u2014 runs claude -p, delivers output via MCP reply tool.
48597
- ${secretsComment}${brokerSocketExport}
48598
- export NVM_DIR="$HOME/.nvm"
48599
- [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
48600
- export PATH="$HOME/.bun/bin:$PATH"
48601
-
48602
- cd ${shellSingleQuote(agentDir)}
48603
-
48604
- # Auth: always OAuth, never API key.
48605
- # Defensively unset ANTHROPIC_API_KEY so any ambient env or systemd
48606
- # Environment= mapping cannot silently shift cron auth from OAuth
48607
- # subscription quota to API billing.
48608
- unset ANTHROPIC_API_KEY
48609
- export CLAUDE_CONFIG_DIR=${shellSingleQuote(agentDir + "/.claude")}
48610
- # SWITCHROOM_AGENT_NAME mirrors the gateway's container/unit environment.
48611
- # Required so in-prompt \`switchroom issues
48612
- # record\` calls without an explicit --agent flag attribute correctly,
48613
- # and so the vault broker client can resolve a default agent.
48614
- export SWITCHROOM_AGENT_NAME=${shellSingleQuote(basename3(agentDir))}
48615
-
48616
- # CLAUDE_CODE_OAUTH_TOKEN injection was removed with RFC H (auth-broker).
48617
- # Claude reads .credentials.json directly; the auth-broker writes it
48618
- # atomically and refreshes ahead of claude's own window.
48619
- unset CLAUDE_CODE_OAUTH_TOKEN
48620
-
48621
- # MCP-only delivery path (issue #269, closes #251): the prompt instructs
48622
- # the model to call mcp__switchroom-telegram__reply directly, then print
48623
- # HEARTBEAT_OK as its sole stdout line. Stdout is discarded here so the
48624
- # trailing model summary doesn't arrive as a second Telegram message.
48625
- # Stderr remains open so systemd captures auth/network/bad-prompt errors
48626
- # via journalctl \u2014 silently swallowing those would make a broken cron job
48627
- # invisible to operators.
48628
- #
48629
- # We deliberately do NOT use \`exec\` here \u2014 the success-trailer below must
48630
- # run after \`claude -p\` returns. Same reasoning as PR #565: when a cron
48631
- # completes cleanly, any unresolved issues filed against this job's source
48632
- # get auto-closed. Failure (non-zero exit) leaves issues open for the
48633
- # Telegram surface to render, exactly as before.
48634
- export TELEGRAM_STATE_DIR=${shellSingleQuote(join8(agentDir, "telegram"))}
48635
- claude -p ${shellSingleQuote(wrappedPrompt)} \\
48636
- --model ${shellSingleQuote(model)} \\
48637
- --no-session-persistence \\
48638
- > /dev/null
48639
- rc=$?
48640
- if [ $rc -eq 0 ]; then
48641
- # Best-effort auto-resolve. Failure here (e.g. switchroom not on PATH in a
48642
- # weird environment) must NOT mask the cron's own success \u2014 hence the
48643
- # trailing \`|| true\`. PR #565 added bulk-close-by-source; we use the same
48644
- # source string the agent's own \`issues record\` calls should use.
48645
- switchroom issues resolve --source "cron:${slug}" --quiet \\
48646
- --state-dir "$TELEGRAM_STATE_DIR" >/dev/null 2>&1 || true
48647
- fi
48648
- exit $rc
48649
- `;
48650
- }
48651
48540
  function resolveSkillsPoolDir(override) {
48652
48541
  return resolveDualPath(override ?? "~/.switchroom/skills");
48653
48542
  }
@@ -49663,20 +49552,18 @@ ${body}
49663
49552
  writeFileSync5(mdPath, content, "utf-8");
49664
49553
  }
49665
49554
  }
49666
- if ((agentConfig.schedule?.length ?? 0) > 0) {
49667
- const cronChatId = userId ?? telegramConfig.forum_chat_id;
49668
- const brokerSocket = switchroomConfig?.vault?.broker?.socket ? resolveDualPath(switchroomConfig.vault.broker.socket) : resolveDualPath("~/.switchroom/vault-broker.sock");
49669
- for (let i = 0;i < agentConfig.schedule.length; i++) {
49670
- const entry = agentConfig.schedule[i];
49671
- const model = entry.model ?? "claude-sonnet-4-6";
49672
- const filename = cronScriptFilename(entry.cron, entry.prompt);
49673
- const stem = filename.replace(/\.sh$/, "");
49674
- const script = buildCronScript(agentDir, entry.prompt, model, cronChatId, userId, entry.secrets ?? [], brokerSocket, stem);
49675
- const scriptPath = join8(agentDir, "telegram", filename);
49676
- writeFileSync5(scriptPath, script, { encoding: "utf-8", mode: 448 });
49677
- const source = entry[OVERLAY_SOURCE] ? "overlay" : "main";
49678
- writeFileSync5(join8(agentDir, "telegram", `${stem}.source`), `${source}
49679
- `, { encoding: "utf-8", mode: 384 });
49555
+ {
49556
+ const telegramDir = join8(agentDir, "telegram");
49557
+ if (existsSync11(telegramDir)) {
49558
+ for (const file of readdirSync5(telegramDir)) {
49559
+ const isCronScript = CRON_SCRIPT_BASENAME_RE.test(file) || LEGACY_CRON_SCRIPT_BASENAME_RE.test(file);
49560
+ if (isCronScript) {
49561
+ unlinkSync4(join8(telegramDir, file));
49562
+ const sidecar = join8(telegramDir, file.replace(/\.sh$/, ".source"));
49563
+ if (existsSync11(sidecar))
49564
+ unlinkSync4(sidecar);
49565
+ }
49566
+ }
49680
49567
  }
49681
49568
  }
49682
49569
  copyProfileSkills(profilePath, join8(agentDir, ".claude", "skills"));
@@ -50439,55 +50326,19 @@ ${body}
50439
50326
  changes.push(settingsPath);
50440
50327
  }
50441
50328
  }
50442
- if ((agentConfig.schedule?.length ?? 0) > 0) {
50443
- let cronUserId;
50444
- const cronAccessPath = join8(agentDir, "telegram", "access.json");
50445
- if (existsSync11(cronAccessPath)) {
50446
- try {
50447
- const cronAccess = JSON.parse(readFileSync11(cronAccessPath, "utf-8"));
50448
- cronUserId = cronAccess.allowFrom?.[0];
50449
- } catch {}
50450
- }
50451
- const reconBrokerSocket = switchroomConfig?.vault?.broker?.socket ? resolveDualPath(switchroomConfig.vault.broker.socket) : resolveDualPath("~/.switchroom/vault-broker.sock");
50452
- const reconCronChatId = cronUserId ?? telegramConfig.forum_chat_id;
50453
- const canonicalFilenames = new Set;
50454
- for (let i = 0;i < agentConfig.schedule.length; i++) {
50455
- const entry = agentConfig.schedule[i];
50456
- const model = entry.model ?? "claude-sonnet-4-6";
50457
- const filename = cronScriptFilename(entry.cron, entry.prompt);
50458
- canonicalFilenames.add(filename);
50459
- const script = buildCronScript(agentDir, entry.prompt, model, reconCronChatId, cronUserId, entry.secrets ?? [], reconBrokerSocket, filename.replace(/\.sh$/, ""));
50460
- const scriptPath = join8(agentDir, "telegram", filename);
50461
- const before = existsSync11(scriptPath) ? readFileSync11(scriptPath, "utf-8") : "";
50462
- if (script !== before) {
50463
- writeFileSync5(scriptPath, script, { encoding: "utf-8", mode: 448 });
50464
- changes.push(scriptPath);
50465
- }
50466
- const source = entry[OVERLAY_SOURCE] ? "overlay" : "main";
50467
- const stem = filename.replace(/\.sh$/, "");
50468
- const sidecarPath = join8(agentDir, "telegram", `${stem}.source`);
50469
- const sidecarBody = `${source}
50470
- `;
50471
- const sidecarBefore = existsSync11(sidecarPath) ? readFileSync11(sidecarPath, "utf-8") : "";
50472
- if (sidecarBody !== sidecarBefore) {
50473
- writeFileSync5(sidecarPath, sidecarBody, { encoding: "utf-8", mode: 384 });
50474
- changes.push(sidecarPath);
50475
- }
50476
- }
50477
- const telegramDir = join8(agentDir, "telegram");
50478
- if (existsSync11(telegramDir)) {
50479
- const files = readdirSync5(telegramDir);
50480
- for (const file of files) {
50481
- const isCron = CRON_SCRIPT_BASENAME_RE.test(file) || LEGACY_CRON_SCRIPT_BASENAME_RE.test(file);
50482
- if (isCron && !canonicalFilenames.has(file)) {
50483
- const staleScript = join8(telegramDir, file);
50484
- unlinkSync4(staleScript);
50485
- changes.push(staleScript);
50486
- const sourceSidecar = staleScript.replace(/\.sh$/, ".source");
50487
- if (existsSync11(sourceSidecar)) {
50488
- unlinkSync4(sourceSidecar);
50489
- changes.push(sourceSidecar);
50490
- }
50329
+ const telegramDir = join8(agentDir, "telegram");
50330
+ if (existsSync11(telegramDir)) {
50331
+ const files = readdirSync5(telegramDir);
50332
+ for (const file of files) {
50333
+ const isCronScript = CRON_SCRIPT_BASENAME_RE.test(file) || LEGACY_CRON_SCRIPT_BASENAME_RE.test(file);
50334
+ if (isCronScript) {
50335
+ const staleScript = join8(telegramDir, file);
50336
+ unlinkSync4(staleScript);
50337
+ changes.push(staleScript);
50338
+ const sourceSidecar = staleScript.replace(/\.sh$/, ".source");
50339
+ if (existsSync11(sourceSidecar)) {
50340
+ unlinkSync4(sourceSidecar);
50341
+ changes.push(sourceSidecar);
50491
50342
  }
50492
50343
  }
50493
50344
  }
@@ -56254,7 +56105,7 @@ init_compose();
56254
56105
  init_vault();
56255
56106
  import * as net3 from "node:net";
56256
56107
  import { mkdirSync as mkdirSync20, chmodSync as chmodSync7, chownSync as chownSync2, existsSync as existsSync33, readFileSync as readFileSync29, readdirSync as readdirSync15, statSync as statSync19, unlinkSync as unlinkSync8, writeFileSync as writeFileSync18, renameSync as renameSync9 } from "node:fs";
56257
- import { dirname as dirname6, resolve as resolve25, basename as basename5 } from "node:path";
56108
+ import { dirname as dirname6, resolve as resolve25, basename as basename4 } from "node:path";
56258
56109
  import * as os4 from "node:os";
56259
56110
  import * as path3 from "node:path";
56260
56111
 
@@ -56276,7 +56127,7 @@ import {
56276
56127
  unlinkSync as unlinkSync7
56277
56128
  } from "node:fs";
56278
56129
  import { createHash as createHash4 } from "node:crypto";
56279
- import { basename as basename4, dirname as dirname3, join as join24 } from "node:path";
56130
+ import { basename as basename3, dirname as dirname3, join as join24 } from "node:path";
56280
56131
  function vaultLayoutPaths(home2) {
56281
56132
  const switchroomRoot = join24(home2, ".switchroom");
56282
56133
  return {
@@ -56429,7 +56280,7 @@ function sha256File(path) {
56429
56280
  return createHash4("sha256").update(data).digest("hex");
56430
56281
  }
56431
56282
  function atomicReplaceWithSymlink(target, linkTarget) {
56432
- const tmp = join24(dirname3(target), `.${basename4(target)}.symlink-tmp`);
56283
+ const tmp = join24(dirname3(target), `.${basename3(target)}.symlink-tmp`);
56433
56284
  if (existsSync31(tmp)) {
56434
56285
  try {
56435
56286
  unlinkSync7(tmp);
@@ -59992,6 +59843,13 @@ class VaultBroker {
59992
59843
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
59993
59844
  writeFileSync18(tmpPath, mintResult.token, { mode: 384 });
59994
59845
  renameSync9(tmpPath, tokenPath);
59846
+ try {
59847
+ const uid = allocateAgentUid(agent);
59848
+ chownSync2(tokenPath, uid, uid);
59849
+ } catch (chownErr) {
59850
+ process.stderr.write(`[vault-broker] mint_grant: token written but chown failed for agent ${agent}: ${chownErr.message} (CAP_CHOWN missing?)
59851
+ `);
59852
+ }
59995
59853
  } catch (err) {
59996
59854
  process.stderr.write(`[vault-broker] mint_grant: failed to write token file for agent ${agent}: ${err.message}
59997
59855
  `);
@@ -60364,12 +60222,12 @@ class VaultBroker {
60364
60222
  }
60365
60223
  function detectVaultLayoutDrift(vaultPath) {
60366
60224
  const dir = dirname6(vaultPath);
60367
- if (basename5(dir) !== "vault")
60225
+ if (basename4(dir) !== "vault")
60368
60226
  return;
60369
- if (basename5(vaultPath) !== "vault.enc")
60227
+ if (basename4(vaultPath) !== "vault.enc")
60370
60228
  return;
60371
60229
  const switchroomDir = dirname6(dir);
60372
- if (basename5(switchroomDir) !== ".switchroom")
60230
+ if (basename4(switchroomDir) !== ".switchroom")
60373
60231
  return;
60374
60232
  const home2 = dirname6(switchroomDir);
60375
60233
  const result = inspectVaultLayout(home2);
@@ -62646,7 +62504,7 @@ async function getVaultPassphrase() {
62646
62504
  }
62647
62505
  function registerDispatchVerb(tg, _program) {
62648
62506
  const dispatch = tg.command("dispatch").description("Webhook dispatch utilities.");
62649
- dispatch.command("test").description("Dry-run dispatch rule matching against a captured payload file. " + "Prints which rules would match and the rendered prompt, without " + "spawning a claude -p process.").requiredOption("--agent <name>", "Agent name (must exist in switchroom.yaml)").requiredOption("--payload <file>", "Path to a JSON payload file").requiredOption("--event <type>", "GitHub event type (e.g. 'pull_request', 'push')").option("--source <name>", "Webhook source (default: github)", "github").action(withConfigError(async (opts) => {
62507
+ dispatch.command("test").description("Dry-run dispatch rule matching against a captured payload file. " + "Prints which rules would match and the rendered prompt, without " + "actually injecting an inbound into the agent's live session.").requiredOption("--agent <name>", "Agent name (must exist in switchroom.yaml)").requiredOption("--payload <file>", "Path to a JSON payload file").requiredOption("--event <type>", "GitHub event type (e.g. 'pull_request', 'push')").option("--source <name>", "Webhook source (default: github)", "github").action(withConfigError(async (opts) => {
62650
62508
  const config = getConfig(_program);
62651
62509
  const agentRaw = config.agents[opts.agent];
62652
62510
  if (!agentRaw) {
@@ -73980,7 +73838,7 @@ function registerDriveMcpLauncherCommand(program3) {
73980
73838
 
73981
73839
  // src/cli/apply.ts
73982
73840
  init_source();
73983
- import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync68, mkdirSync as mkdirSync36, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
73841
+ import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync68, mkdirSync as mkdirSync36, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
73984
73842
  import { mkdir, writeFile } from "node:fs/promises";
73985
73843
  import { spawnSync as childSpawnSync } from "node:child_process";
73986
73844
  import readline from "node:readline";
@@ -74713,6 +74571,16 @@ async function ensureHostMountSources(config) {
74713
74571
  if (!existsSync68(hostdAuditLogPath)) {
74714
74572
  writeFileSync32(hostdAuditLogPath, "", { mode: 420 });
74715
74573
  }
74574
+ for (const name of Object.keys(config.agents)) {
74575
+ const tokenPath = join62(home2, ".switchroom", "agents", name, ".vault-token");
74576
+ if (!existsSync68(tokenPath)) {
74577
+ writeFileSync32(tokenPath, "", { mode: 384 });
74578
+ }
74579
+ try {
74580
+ const uid = allocateAgentUid(name);
74581
+ chownSync4(tokenPath, uid, uid);
74582
+ } catch {}
74583
+ }
74716
74584
  }
74717
74585
  function detectComposeV2() {
74718
74586
  try {
@@ -77016,189 +76884,12 @@ function registerHostdMcpCommand(program3) {
77016
76884
  });
77017
76885
  }
77018
76886
 
77019
- // src/cli/migrate.ts
77020
- init_source();
77021
- init_helpers();
77022
- init_loader();
77023
- import {
77024
- existsSync as existsSync76,
77025
- readdirSync as readdirSync29,
77026
- readFileSync as readFileSync61,
77027
- renameSync as renameSync16,
77028
- statSync as statSync29,
77029
- unlinkSync as unlinkSync16
77030
- } from "node:fs";
77031
- import { createHash as createHash13 } from "node:crypto";
77032
- import { join as join68 } from "node:path";
77033
- function planCronUnitRenames(agentsDir, agents) {
77034
- const plans = [];
77035
- for (const [agentName, agentConfig] of Object.entries(agents)) {
77036
- const schedule = agentConfig.schedule ?? [];
77037
- if (schedule.length === 0)
77038
- continue;
77039
- const telegramDir = join68(agentsDir, agentName, "telegram");
77040
- if (!existsSync76(telegramDir))
77041
- continue;
77042
- let entries;
77043
- try {
77044
- entries = readdirSync29(telegramDir);
77045
- } catch {
77046
- continue;
77047
- }
77048
- for (const file of entries) {
77049
- const m = file.match(LEGACY_CRON_SCRIPT_BASENAME_RE);
77050
- if (!m)
77051
- continue;
77052
- const idx = Number.parseInt(m[1], 10);
77053
- const entry = schedule[idx];
77054
- if (!entry)
77055
- continue;
77056
- const canonical = cronScriptFilename(entry.cron, entry.prompt);
77057
- if (canonical === file)
77058
- continue;
77059
- plans.push({
77060
- agent: agentName,
77061
- from: join68(telegramDir, file),
77062
- to: join68(telegramDir, canonical),
77063
- scheduleIdx: idx,
77064
- entry
77065
- });
77066
- }
77067
- }
77068
- return plans;
77069
- }
77070
- function sha256File2(path8) {
77071
- return createHash13("sha256").update(readFileSync61(path8)).digest("hex");
77072
- }
77073
- function renamePair(from, to, opts = {}) {
77074
- if (existsSync76(to)) {
77075
- let identical = false;
77076
- try {
77077
- identical = sha256File2(from) === sha256File2(to);
77078
- } catch {
77079
- identical = false;
77080
- }
77081
- if (identical) {
77082
- if (!opts.dryRun) {
77083
- try {
77084
- unlinkSync16(from);
77085
- } catch {}
77086
- }
77087
- return { kind: "deduped", legacy: from };
77088
- }
77089
- return { kind: "skipped", reason: "target exists, legacy preserved", legacy: from };
77090
- }
77091
- if (!opts.dryRun)
77092
- renameSync16(from, to);
77093
- return { kind: "renamed" };
77094
- }
77095
- function extractPromptFromLegacyScript(path8) {
77096
- let body;
77097
- try {
77098
- body = readFileSync61(path8, "utf-8");
77099
- } catch {
77100
- return null;
77101
- }
77102
- const idx = body.indexOf(`
77103
- claude -p '`);
77104
- if (idx < 0)
77105
- return null;
77106
- let i = idx + `
77107
- claude -p '`.length;
77108
- let out = "";
77109
- while (i < body.length) {
77110
- const ch = body[i];
77111
- if (ch === "'") {
77112
- if (body.startsWith(`'"'"'`, i)) {
77113
- out += "'";
77114
- i += 5;
77115
- continue;
77116
- }
77117
- return out;
77118
- }
77119
- out += ch;
77120
- i++;
77121
- }
77122
- return null;
77123
- }
77124
- function detectPromptDrift(legacyPath, entry, ctx) {
77125
- const embedded = extractPromptFromLegacyScript(legacyPath);
77126
- const expected = applyCronTelegramGuidance(entry.prompt, ctx);
77127
- return {
77128
- drifted: embedded !== null && embedded !== expected,
77129
- embedded,
77130
- expected
77131
- };
77132
- }
77133
- function registerMigrateCommand(program3) {
77134
- const cmd = program3.command("migrate").description("One-shot config/state migrations.");
77135
- cmd.command("cron-unit-names").description("Rename legacy cron-<index>.sh scripts to the Phase D content-hash " + "form (cron-<sha12>.sh). Idempotent.").option("--dry-run", "Print the renames without performing them", false).option("--strict", "Treat drift (legacy script content disagrees with current schedule entry) as a hard error", false).action(withConfigError(async (opts) => {
77136
- const config2 = getConfig(program3);
77137
- const agentsDir = resolveAgentsDir(config2);
77138
- const plans = planCronUnitRenames(agentsDir, config2.agents);
77139
- if (plans.length === 0) {
77140
- console.log(source_default.green("Nothing to migrate \u2014 all cron scripts already use the content-hash scheme."));
77141
- return;
77142
- }
77143
- let driftErrors = 0;
77144
- for (const p of plans) {
77145
- const drift = detectPromptDrift(p.from, p.entry, {
77146
- chatId: "-",
77147
- jobSlug: p.to.split("/").pop().replace(/\.sh$/, "")
77148
- });
77149
- if (drift.drifted) {
77150
- const msg = `DRIFT: ${p.from} was scaffolded with a prompt that differs from the current schedule[${p.scheduleIdx}] entry (cron=${JSON.stringify(p.entry.cron)}); renaming to ${p.to} \u2014 verify intent`;
77151
- if (opts.strict) {
77152
- console.error(source_default.red(`error: ${msg}`));
77153
- driftErrors++;
77154
- continue;
77155
- }
77156
- console.error(source_default.yellow(msg));
77157
- }
77158
- if (opts.dryRun) {
77159
- console.log(source_default.cyan(`[dry-run] ${p.agent}: ${p.from} \u2192 ${p.to}`));
77160
- continue;
77161
- }
77162
- try {
77163
- const status = renamePair(p.from, p.to);
77164
- const fromSidecar = p.from.replace(/\.sh$/, ".source");
77165
- const toSidecar = p.to.replace(/\.sh$/, ".source");
77166
- let sidecarStatus = null;
77167
- if (existsSync76(fromSidecar) && statSync29(fromSidecar).isFile()) {
77168
- sidecarStatus = renamePair(fromSidecar, toSidecar);
77169
- }
77170
- switch (status.kind) {
77171
- case "renamed":
77172
- console.log(source_default.green(`renamed: ${p.agent}: ${p.from} \u2192 ${p.to}`));
77173
- break;
77174
- case "deduped":
77175
- console.log(source_default.green(`deduped: ${p.agent}: target already present with identical contents, legacy ${p.from} removed`));
77176
- break;
77177
- case "skipped":
77178
- console.log(source_default.yellow(`skipped: target exists, legacy preserved at ${p.from}`));
77179
- break;
77180
- }
77181
- if (sidecarStatus && sidecarStatus.kind === "skipped") {
77182
- console.log(source_default.yellow(`skipped: target exists, legacy preserved at ${fromSidecar}`));
77183
- } else if (sidecarStatus && sidecarStatus.kind === "deduped") {
77184
- console.log(source_default.green(`deduped: sidecar ${fromSidecar} removed (identical to target)`));
77185
- }
77186
- } catch (err2) {
77187
- console.error(source_default.red(`failed: ${p.agent}: ${p.from} \u2192 ${p.to}: ${err2.message}`));
77188
- }
77189
- }
77190
- if (opts.strict && driftErrors > 0) {
77191
- process.exitCode = 1;
77192
- }
77193
- }));
77194
- }
77195
-
77196
76887
  // src/cli/hostd.ts
77197
76888
  init_source();
77198
76889
  init_helpers();
77199
- import { existsSync as existsSync77, mkdirSync as mkdirSync40, readdirSync as readdirSync30, readFileSync as readFileSync62, writeFileSync as writeFileSync34, statSync as statSync30, copyFileSync as copyFileSync12 } from "node:fs";
76890
+ import { existsSync as existsSync76, mkdirSync as mkdirSync40, readdirSync as readdirSync29, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync29, copyFileSync as copyFileSync12 } from "node:fs";
77200
76891
  import { homedir as homedir38 } from "node:os";
77201
- import { join as join69 } from "node:path";
76892
+ import { join as join68 } from "node:path";
77202
76893
  import { spawnSync as spawnSync11 } from "node:child_process";
77203
76894
  init_audit_reader();
77204
76895
  var DEFAULT_IMAGE_TAG = "latest";
@@ -77289,14 +76980,14 @@ networks:
77289
76980
  `;
77290
76981
  }
77291
76982
  function hostdDir() {
77292
- return join69(homedir38(), ".switchroom", "hostd");
76983
+ return join68(homedir38(), ".switchroom", "hostd");
77293
76984
  }
77294
76985
  function hostdComposePath() {
77295
- return join69(hostdDir(), "docker-compose.yml");
76986
+ return join68(hostdDir(), "docker-compose.yml");
77296
76987
  }
77297
76988
  function backupExistingCompose() {
77298
76989
  const p = hostdComposePath();
77299
- if (!existsSync77(p))
76990
+ if (!existsSync76(p))
77300
76991
  return null;
77301
76992
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
77302
76993
  const bak = `${p}.bak-${ts}`;
@@ -77373,7 +77064,7 @@ function doStatus() {
77373
77064
  const composeYml = hostdComposePath();
77374
77065
  console.log(source_default.bold("switchroom-hostd"));
77375
77066
  console.log("");
77376
- if (!existsSync77(composeYml)) {
77067
+ if (!existsSync76(composeYml)) {
77377
77068
  console.log(source_default.yellow(" compose: not installed"));
77378
77069
  console.log(source_default.dim(" run `switchroom hostd install` to set up."));
77379
77070
  return;
@@ -77394,15 +77085,15 @@ function doStatus() {
77394
77085
  } else {
77395
77086
  console.log(source_default.green(` container: ${ps.stdout.trim()}`));
77396
77087
  }
77397
- if (existsSync77(dir)) {
77088
+ if (existsSync76(dir)) {
77398
77089
  const entries = [];
77399
77090
  try {
77400
- for (const name of readdirSync30(dir)) {
77091
+ for (const name of readdirSync29(dir)) {
77401
77092
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
77402
77093
  continue;
77403
- const sockPath = join69(dir, name, "sock");
77404
- if (existsSync77(sockPath)) {
77405
- const st = statSync30(sockPath);
77094
+ const sockPath = join68(dir, name, "sock");
77095
+ if (existsSync76(sockPath)) {
77096
+ const st = statSync29(sockPath);
77406
77097
  if ((st.mode & 61440) === 49152) {
77407
77098
  entries.push(`${name} \u2192 ${sockPath}`);
77408
77099
  }
@@ -77420,7 +77111,7 @@ function doStatus() {
77420
77111
  }
77421
77112
  function doUninstall() {
77422
77113
  const composeYml = hostdComposePath();
77423
- if (!existsSync77(composeYml)) {
77114
+ if (!existsSync76(composeYml)) {
77424
77115
  console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
77425
77116
  return;
77426
77117
  }
@@ -77444,12 +77135,12 @@ function registerHostdCommand(program3) {
77444
77135
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
77445
77136
  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) => {
77446
77137
  const logPath = opts.path ?? defaultAuditLogPath2();
77447
- if (!existsSync77(logPath)) {
77138
+ if (!existsSync76(logPath)) {
77448
77139
  console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
77449
77140
  The log is created when hostd handles its first privileged-verb request.`));
77450
77141
  return;
77451
77142
  }
77452
- const raw = readFileSync62(logPath, "utf-8");
77143
+ const raw = readFileSync61(logPath, "utf-8");
77453
77144
  const limit = Math.max(1, parseInt(opts.tail ?? "50", 10) || 50);
77454
77145
  const filters = {
77455
77146
  agent: opts.agent,
@@ -77528,7 +77219,6 @@ registerAgentConfigWriteCommands(program3);
77528
77219
  registerAgentConfigSkillWriteCommands(program3);
77529
77220
  registerAgentConfigMcpCommand(program3);
77530
77221
  registerHostdMcpCommand(program3);
77531
- registerMigrateCommand(program3);
77532
77222
  registerHostdCommand(program3);
77533
77223
 
77534
77224
  // bin/switchroom.ts
@@ -774,16 +774,24 @@
774
774
  // usage % cell: live 5h/7d utilization from the last cached
775
775
  // probe (cost-gated — see refreshQuota). null → "—" + a per-
776
776
  // account ↻ that force-probes. quotaStale → value shown muted
777
- // with ↻ to refresh.
778
- const pctCell = (pct, label, stale) => {
777
+ // with ↻ to refresh. When `resetAt` is provided we append a
778
+ // muted "resets in Xh" line so the operator can see WHEN the
779
+ // window rolls over without hovering for a tooltip — matches
780
+ // anthropic-ratelimit-unified-{5h,7d}-reset headers exposed by
781
+ // the broker probe (src/auth/quota.ts:97-98).
782
+ const pctCell = (pct, label, stale, resetAt) => {
779
783
  if (pct == null) return '<span style="color:var(--text-dim)">—</span>';
780
784
  let cls = 'quota-pct';
781
785
  if (pct >= 90) cls += ' high';
782
786
  else if (pct >= 70) cls += ' mid';
783
787
  const v = `${Math.round(pct)}%`;
784
- return stale
788
+ const reset = resetAt
789
+ ? `<div style="font-size:.72rem;color:var(--text-dim);font-weight:normal;margin-top:.1rem">resets ${formatTimestamp(resetAt)}</div>`
790
+ : '';
791
+ const pctSpan = stale
785
792
  ? `<span class="${cls}" title="stale — click ↻" style="opacity:.55">${v}</span>`
786
793
  : `<span class="${cls}">${v}</span>`;
794
+ return reset ? `<div>${pctSpan}${reset}</div>` : pctSpan;
787
795
  };
788
796
  const enriched = accounts.map(a => {
789
797
  const q = a.quota || null;
@@ -792,9 +800,8 @@
792
800
  return {
793
801
  a,
794
802
  usedBy: a.usedBy || [],
795
- fiveH: pctCell(u ? u.fiveHourPct : null, '5h', a.quotaStale),
796
- sevenD: pctCell(u ? u.sevenDayPct : null, '7d', a.quotaStale),
797
- fiveReset: u && u.fiveHourResetAt ? formatTimestamp(u.fiveHourResetAt) : '<span style="color:var(--text-dim)">—</span>',
803
+ fiveH: pctCell(u ? u.fiveHourPct : null, '5h', a.quotaStale, u ? u.fiveHourResetAt : null),
804
+ sevenD: pctCell(u ? u.sevenDayPct : null, '7d', a.quotaStale, u ? u.sevenDayResetAt : null),
798
805
  captured: u && u.capturedAt
799
806
  ? formatTimestamp(u.capturedAt)
800
807
  : '<span style="color:var(--text-dim)">never</span>',
@@ -13707,7 +13707,7 @@ var AgentBindMountSchema = exports_external.object({
13707
13707
  var ScheduleEntrySchema = exports_external.object({
13708
13708
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
13709
13709
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
13710
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
13710
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
13711
13711
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
13712
13712
  });
13713
13713
  var AgentSoulSchema = exports_external.object({
@@ -10964,7 +10964,7 @@ var init_schema = __esm(() => {
10964
10964
  ScheduleEntrySchema = exports_external.object({
10965
10965
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
10966
10966
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
10967
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10967
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10968
10968
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
10969
10969
  });
10970
10970
  AgentSoulSchema = exports_external.object({
@@ -10964,7 +10964,7 @@ var init_schema = __esm(() => {
10964
10964
  ScheduleEntrySchema = exports_external.object({
10965
10965
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
10966
10966
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
10967
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10967
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "— this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
10968
10968
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default — broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary — " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
10969
10969
  });
10970
10970
  AgentSoulSchema = exports_external.object({
@@ -16934,6 +16934,13 @@ class VaultBroker {
16934
16934
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
16935
16935
  writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
16936
16936
  renameSync3(tmpPath, tokenPath);
16937
+ try {
16938
+ const uid = allocateAgentUid(agent);
16939
+ chownSync(tokenPath, uid, uid);
16940
+ } catch (chownErr) {
16941
+ process.stderr.write(`[vault-broker] mint_grant: token written but chown failed for agent ${agent}: ${chownErr.message} (CAP_CHOWN missing?)
16942
+ `);
16943
+ }
16937
16944
  } catch (err) {
16938
16945
  process.stderr.write(`[vault-broker] mint_grant: failed to write token file for agent ${agent}: ${err.message}
16939
16946
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.39",
3
+ "version": "0.13.41",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23608,7 +23608,7 @@ var init_schema = __esm(() => {
23608
23608
  ScheduleEntrySchema = exports_external.object({
23609
23609
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
23610
23610
  prompt: exports_external.string().describe("Prompt to send at the scheduled time"),
23611
- model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated `claude -p` and could set --model per task. " + "Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "\u2014 this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
23611
+ model: exports_external.string().optional().describe("DEPRECATED / IGNORED. Pre-v0.8 the singleton scheduler ran each " + "task as an isolated headless invocation and could set --model per " + "task. Post cron-fold-in (v0.8) the fire is injected into the agent's " + "running session, so it always uses the agent's configured model " + "\u2014 this field has no effect. Accepted (optional) only so existing " + "configs keep validating; set the model at the agent level instead. " + "See docs/scheduling.md."),
23612
23612
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing.")
23613
23613
  });
23614
23614
  AgentSoulSchema = exports_external.object({
@@ -48730,10 +48730,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
48730
48730
  }
48731
48731
 
48732
48732
  // ../src/build-info.ts
48733
- var VERSION = "0.13.39";
48734
- var COMMIT_SHA = "8681f423";
48735
- var COMMIT_DATE = "2026-05-25T07:06:31Z";
48736
- var LATEST_PR = 1797;
48733
+ var VERSION = "0.13.41";
48734
+ var COMMIT_SHA = "c5897e47";
48735
+ var COMMIT_DATE = "2026-05-25T07:56:53Z";
48736
+ var LATEST_PR = 1803;
48737
48737
  var COMMITS_AHEAD_OF_TAG = 0;
48738
48738
 
48739
48739
  // gateway/boot-version.ts