switchroom 0.18.28 → 0.18.30

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 (48) hide show
  1. package/bin/handoff-briefing.sh +15 -2
  2. package/dist/agent-scheduler/index.js +111 -7
  3. package/dist/auth-broker/index.js +154 -73
  4. package/dist/cli/autoaccept-poll.js +8 -3
  5. package/dist/cli/drive-write-pretool.mjs +8 -3
  6. package/dist/cli/ms-365-write-pretool.mjs +158 -11
  7. package/dist/cli/notion-write-pretool.mjs +103 -4
  8. package/dist/cli/switchroom.js +2712 -2219
  9. package/dist/host-control/main.js +110 -70
  10. package/dist/vault/approvals/kernel-server.js +116 -70
  11. package/dist/vault/broker/server.js +314 -202
  12. package/package.json +3 -3
  13. package/profiles/_base/start.sh.hbs +105 -34
  14. package/telegram-plugin/dist/bridge/bridge.js +71 -47
  15. package/telegram-plugin/dist/gateway/gateway.js +1128 -666
  16. package/telegram-plugin/dist/server.js +89 -64
  17. package/telegram-plugin/gateway/backstop-delivery.ts +272 -0
  18. package/telegram-plugin/gateway/forward-origin.ts +9 -1
  19. package/telegram-plugin/gateway/gateway.ts +656 -388
  20. package/telegram-plugin/gateway/model-command.ts +331 -602
  21. package/telegram-plugin/gateway/session-model-file.ts +40 -0
  22. package/telegram-plugin/gateway/turn-record-status.ts +45 -0
  23. package/telegram-plugin/gateway/unhandled-message.ts +177 -0
  24. package/telegram-plugin/history.ts +153 -23
  25. package/telegram-plugin/llm-error-present.ts +24 -0
  26. package/telegram-plugin/model-unavailable.ts +55 -0
  27. package/telegram-plugin/operator-events.ts +113 -0
  28. package/telegram-plugin/pending-user-notice.ts +88 -0
  29. package/telegram-plugin/shared/local-time.ts +99 -0
  30. package/telegram-plugin/tests/backstop-delivery.test.ts +250 -0
  31. package/telegram-plugin/tests/catch-all-forwarded-history.test.ts +103 -0
  32. package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +264 -0
  33. package/telegram-plugin/tests/forward-origin.test.ts +30 -3
  34. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +111 -60
  35. package/telegram-plugin/tests/history.test.ts +88 -0
  36. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +278 -0
  37. package/telegram-plugin/tests/local-time.test.ts +135 -0
  38. package/telegram-plugin/tests/model-command.test.ts +427 -1512
  39. package/telegram-plugin/tests/session-model-file.test.ts +23 -0
  40. package/telegram-plugin/tests/turn-flush-safety.test.ts +34 -0
  41. package/telegram-plugin/tier-downgrade.ts +4 -3
  42. package/telegram-plugin/turn-flush-safety.ts +25 -1
  43. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +399 -2
  44. package/vendor/hindsight-memory/scripts/lib/client.py +47 -0
  45. package/vendor/hindsight-memory/scripts/lib/content.py +93 -7
  46. package/vendor/hindsight-memory/scripts/lib/turnlog.py +450 -0
  47. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +467 -0
  48. package/vendor/hindsight-memory/tests/test_content.py +63 -7
@@ -4013,7 +4013,7 @@ var init_zod = __esm(() => {
4013
4013
  });
4014
4014
 
4015
4015
  // src/config/schema.ts
4016
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
4016
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, ReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
4017
4017
  var init_schema = __esm(() => {
4018
4018
  init_zod();
4019
4019
  CodeRepoEntrySchema = exports_external.object({
@@ -4411,11 +4411,52 @@ var init_schema = __esm(() => {
4411
4411
  approvers: exports_external.array(ApproverIdSchema).min(1).optional().describe("Per-agent approver override. When set, replaces (does not extend) " + "the top-level drive.approvers list for this agent's onboarding card."),
4412
4412
  tier: GoogleWorkspaceTierSchema.optional().describe("Per-agent tier override (RFC G Phase 1). When set, replaces the " + "top-level google_workspace.tier for this agent. Common case: most " + "agents on `core`, one specialist on `extended` for Slides access.")
4413
4413
  }).optional();
4414
+ MicrosoftAccountEmailSchema = exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4415
+ message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
4416
+ }).transform((v) => v.trim().toLowerCase());
4417
+ MicrosoftToolTokenSchema = exports_external.string().min(1).regex(/^[a-z0-9-]+$/, {
4418
+ message: "microsoft_workspace tools[] tokens are forwarded to softeria's " + "--enabled-tools regex; each must be lowercase kebab-case " + "([a-z0-9-]+, e.g. 'mail', 'calendar', 'send-mail') so an unescaped " + "regex metacharacter can't crash the launcher"
4419
+ });
4420
+ MicrosoftAccountBindingSchema = exports_external.object({
4421
+ account: MicrosoftAccountEmailSchema.describe("The Microsoft account this binding uses. Must be a key in top-level " + "`microsoft_accounts:` with this agent in its `enabled_for[]`."),
4422
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist → softeria `--enabled-tools <regex>` " + "(tokens joined with `|`). Omitted = all tools exposed for this account."),
4423
+ org_mode: exports_external.boolean().optional().describe("Per-binding org_mode override (RFC #1873 §6.4).")
4424
+ });
4414
4425
  AgentMicrosoftWorkspaceConfigSchema = exports_external.object({
4415
- account: exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
4416
- message: "microsoft_workspace.account must be a Microsoft account email like " + "'alice@outlook.com' or 'alice@contoso.com' (colons not allowed)"
4417
- }).transform((v) => v.trim().toLowerCase()).optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized)."),
4418
- org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false).")
4426
+ account: MicrosoftAccountEmailSchema.optional().describe("RFC #1873: the Microsoft account this agent uses for the M365 MCP. " + "Must be a key in top-level `microsoft_accounts:` with this agent " + "listed in its `enabled_for[]`. Read by the auth-broker " + "(get-credentials, provider=microsoft) and by the scaffold to " + "decide whether to emit the `ms-365` MCP entry. Normalized to " + "lowercase so it matches the microsoft_accounts key (which is " + "also normalized). Mutually exclusive with `accounts` (plural)."),
4427
+ tools: exports_external.array(MicrosoftToolTokenSchema).min(1).optional().describe("Per-account tool allowlist for the SINGULAR `account` form " + "softeria `--enabled-tools <regex>`. Omitted = all tools. Only " + "valid with the singular `account`; using it together with the " + "plural `accounts` (which carries per-binding `tools`) is an error."),
4428
+ org_mode: exports_external.boolean().optional().describe("Per-agent org_mode override (RFC #1873 §6.4). When set, replaces " + "the top-level microsoft_workspace.org_mode for this agent. " + "Defaults to top-level value (which defaults to false)."),
4429
+ accounts: exports_external.array(MicrosoftAccountBindingSchema).min(1).optional().describe("Plural multi-account form: bind MULTIPLE Microsoft accounts to " + "this agent, each with its own tool scope. Mutually exclusive with " + "the singular `account`. Each account gets its own `ms-365-<slug>` " + "MCP server.")
4430
+ }).superRefine((v, ctx) => {
4431
+ const hasSingular = v.account !== undefined;
4432
+ const hasPlural = v.accounts !== undefined;
4433
+ if (hasSingular && hasPlural) {
4434
+ ctx.addIssue({
4435
+ code: exports_external.ZodIssueCode.custom,
4436
+ message: "microsoft_workspace: use EITHER `account` (singular) OR " + "`accounts` (plural array), not both",
4437
+ path: ["accounts"]
4438
+ });
4439
+ }
4440
+ if (hasPlural && v.tools !== undefined) {
4441
+ ctx.addIssue({
4442
+ code: exports_external.ZodIssueCode.custom,
4443
+ message: "microsoft_workspace: block-level `tools` applies to the singular " + "`account` only; with `accounts` put `tools` inside each binding",
4444
+ path: ["tools"]
4445
+ });
4446
+ }
4447
+ if (hasPlural) {
4448
+ const seen = new Set;
4449
+ for (const b of v.accounts) {
4450
+ if (seen.has(b.account)) {
4451
+ ctx.addIssue({
4452
+ code: exports_external.ZodIssueCode.custom,
4453
+ message: `microsoft_workspace: duplicate account '${b.account}' in accounts[]`,
4454
+ path: ["accounts"]
4455
+ });
4456
+ }
4457
+ seen.add(b.account);
4458
+ }
4459
+ }
4419
4460
  }).optional();
4420
4461
  AgentNotionWorkspaceConfigSchema = exports_external.object({
4421
4462
  databases: exports_external.array(exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,62}$/, {
@@ -4790,6 +4831,26 @@ var init_schema = __esm(() => {
4790
4831
  for (const [name, a] of Object.entries(cfg.agents ?? {})) {
4791
4832
  checkServes(a.serves, ["agents", name, "serves"]);
4792
4833
  }
4834
+ const microsoftAccounts = cfg.microsoft_accounts;
4835
+ for (const [name, a] of Object.entries(cfg.agents ?? {})) {
4836
+ const mw = a.microsoft_workspace;
4837
+ const accounts = mw?.accounts;
4838
+ if (!accounts)
4839
+ continue;
4840
+ accounts.forEach((b, i) => {
4841
+ const acct = b?.account?.trim().toLowerCase();
4842
+ if (!acct)
4843
+ return;
4844
+ const enabledFor = microsoftAccounts?.[acct]?.enabled_for ?? [];
4845
+ if (!enabledFor.includes(name)) {
4846
+ ctx.addIssue({
4847
+ code: exports_external.ZodIssueCode.custom,
4848
+ message: `agent '${name}' binds Microsoft account '${acct}' but is not in ` + `microsoft_accounts['${acct}'].enabled_for[] — operator must run ` + `\`switchroom auth microsoft enable ${acct} ${name}\``,
4849
+ path: ["agents", name, "microsoft_workspace", "accounts", i, "account"]
4850
+ });
4851
+ }
4852
+ });
4853
+ }
4793
4854
  });
4794
4855
  });
4795
4856
 
@@ -5184,6 +5245,25 @@ var init_merge = __esm(() => {
5184
5245
  })(mergeAgentConfig ||= {});
5185
5246
  });
5186
5247
 
5248
+ // src/config/timezone.ts
5249
+ function isResolvableTimezone(zone) {
5250
+ try {
5251
+ new Intl.DateTimeFormat("en-US", { timeZone: zone });
5252
+ return true;
5253
+ } catch {
5254
+ return false;
5255
+ }
5256
+ }
5257
+ var CONTAINER_DEFAULT_UTC_ZONES;
5258
+ var init_timezone = __esm(() => {
5259
+ CONTAINER_DEFAULT_UTC_ZONES = new Set([
5260
+ "UTC",
5261
+ "Etc/UTC",
5262
+ "Etc/Universal",
5263
+ "Universal"
5264
+ ]);
5265
+ });
5266
+
5187
5267
  // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
5188
5268
  var require_identity = __commonJS((exports) => {
5189
5269
  var ALIAS = Symbol.for("yaml.alias");
@@ -12492,8 +12572,30 @@ function loadConfig(configPath) {
12492
12572
  if (notionIssues.length > 0) {
12493
12573
  throw new ConfigError(`Invalid notion_workspace configuration in ${filePath}`, notionIssues);
12494
12574
  }
12575
+ validateAllTimezones(config, filePath);
12495
12576
  return config;
12496
12577
  }
12578
+ function validateAllTimezones(config, filePath) {
12579
+ const issues = [];
12580
+ const check = (zone, where) => {
12581
+ if (zone == null)
12582
+ return;
12583
+ if (!isResolvableTimezone(zone)) {
12584
+ issues.push(` ${where}: "${zone}" is not a resolvable IANA timezone ` + `(shape is valid but no such zone exists — check for a typo, ` + `e.g. "Australia/Melbourne", "America/New_York", "UTC").`);
12585
+ }
12586
+ };
12587
+ check(config.switchroom?.timezone, "switchroom.timezone");
12588
+ check(config.defaults?.timezone, "defaults.timezone");
12589
+ for (const [profileName, profile] of Object.entries(config.profiles ?? {})) {
12590
+ check(profile?.timezone, `profiles.${profileName}.timezone`);
12591
+ }
12592
+ for (const [agentName, agentRaw] of Object.entries(config.agents)) {
12593
+ check(agentRaw?.timezone, `agents.${agentName}.timezone`);
12594
+ }
12595
+ if (issues.length > 0) {
12596
+ throw new ConfigError(`Invalid timezone configuration in ${filePath}`, issues);
12597
+ }
12598
+ }
12497
12599
  function validateAllCronTopicAliases(config, filePath) {
12498
12600
  const issues = [];
12499
12601
  for (const [agentName, agentRaw] of Object.entries(config.agents)) {
@@ -12536,6 +12638,7 @@ var init_loader = __esm(() => {
12536
12638
  init_paths();
12537
12639
  init_overlay_loader();
12538
12640
  init_merge();
12641
+ init_timezone();
12539
12642
  import_yaml2 = __toESM(require_dist(), 1);
12540
12643
  ConfigError = class ConfigError extends Error {
12541
12644
  details;
@@ -18109,7 +18212,7 @@ var require_lib = __commonJS((exports, module) => {
18109
18212
 
18110
18213
  // src/vault/broker/server.ts
18111
18214
  import * as net from "node:net";
18112
- import { mkdirSync as mkdirSync6, chmodSync as chmodSync4, chownSync, existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync7 } from "node:fs";
18215
+ import { mkdirSync as mkdirSync7, chmodSync as chmodSync5, chownSync, existsSync as existsSync12, readFileSync as readFileSync10, readdirSync as readdirSync4, statSync as statSync6, unlinkSync as unlinkSync6, writeFileSync as writeFileSync3, renameSync as renameSync8 } from "node:fs";
18113
18216
 
18114
18217
  // src/agents/compose.ts
18115
18218
  init_schema();
@@ -18473,14 +18576,7 @@ init_merge();
18473
18576
 
18474
18577
  // src/agents/scaffold.ts
18475
18578
  init_merge();
18476
-
18477
- // src/config/timezone.ts
18478
- var CONTAINER_DEFAULT_UTC_ZONES = new Set([
18479
- "UTC",
18480
- "Etc/UTC",
18481
- "Etc/Universal",
18482
- "Universal"
18483
- ]);
18579
+ init_timezone();
18484
18580
 
18485
18581
  // src/cli/agent-config.ts
18486
18582
  import { join } from "node:path";
@@ -18698,63 +18794,6 @@ for (const name of SHARED_FRAGMENTS) {
18698
18794
  }
18699
18795
  }
18700
18796
 
18701
- // src/agents/pane-lock.ts
18702
- var tails = new Map;
18703
-
18704
- // src/agents/inject.ts
18705
- var INJECT_COMMANDS = new Map([
18706
- ["/cost", { description: "Show session cost", expectsOutput: true, dialog: true, argsAllowed: false }],
18707
- ["/status", { description: "Show session status", expectsOutput: true, dialog: true, argsAllowed: false }],
18708
- ["/usage", { description: "Show plan quota", expectsOutput: true, dialog: true, argsAllowed: false }],
18709
- ["/hooks", { description: "List configured hooks", expectsOutput: true, dialog: true, argsAllowed: false }],
18710
- ["/memory", { description: "Open memory picker", expectsOutput: true, dialog: true, argsAllowed: false }],
18711
- ["/help", { description: "Show help / command discovery", expectsOutput: true, dialog: true, argsAllowed: false }],
18712
- ["/context", { description: "Show context window usage", expectsOutput: true, argsAllowed: false }],
18713
- ["/release-notes", { description: "Show release-notes version list", expectsOutput: true, dialog: true, argsAllowed: false }],
18714
- ["/model", { description: "Open model picker", expectsOutput: true, argsAllowed: true }],
18715
- [
18716
- "/clear",
18717
- {
18718
- description: "Clear session screen",
18719
- expectsOutput: false,
18720
- silentNote: "context cleared — fresh slate",
18721
- argsAllowed: false
18722
- }
18723
- ],
18724
- [
18725
- "/compact",
18726
- {
18727
- description: "Compact conversation history",
18728
- expectsOutput: false,
18729
- silentNote: "compaction runs silently",
18730
- argsAllowed: false
18731
- }
18732
- ]
18733
- ]);
18734
- var INJECT_ALLOWLIST = new Set(INJECT_COMMANDS.keys());
18735
- var INJECT_BLOCKED = new Map([
18736
- ["/login", { reason: "would mutate auth state" }],
18737
- ["/logout", { reason: "would terminate the agent's auth session" }],
18738
- ["/exit", { reason: "would kill the agent process" }],
18739
- ["/quit", { reason: "would kill the agent process" }],
18740
- ["/upgrade", { reason: "mutates the Claude Code installation" }],
18741
- ["/init", { reason: "generates/overwrites CLAUDE.md and runs a model turn" }],
18742
- ["/mcp", { reason: "opens an interactive MCP server management dialog" }],
18743
- ["/permissions", { reason: "opens an interactive permissions editor that mutates tool policy" }],
18744
- ["/install-github-app", { reason: "runs a network/OAuth install flow" }],
18745
- ["/add-dir", { reason: "mutates the session's working-directory set" }],
18746
- ["/terminal-setup", { reason: "mutates terminal keybinding configuration" }],
18747
- ["/privacy-settings", { reason: "opens an interactive privacy-settings dialog" }],
18748
- ["/bug", { reason: "submits a bug report over the network" }],
18749
- [
18750
- "/effort",
18751
- {
18752
- reason: "leaves a blocking confirmation modal open and wedges the pane; use the /effort command (it drives the modal), not raw inject"
18753
- }
18754
- ]
18755
- ]);
18756
- var INJECT_BLOCKLIST = new Set(INJECT_BLOCKED.keys());
18757
-
18758
18797
  // src/setup/hindsight.ts
18759
18798
  var HINDSIGHT_DEFAULT_API_PORT = 18888;
18760
18799
  var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
@@ -19588,42 +19627,108 @@ var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
19588
19627
 
19589
19628
  // src/agents/compose.ts
19590
19629
  init_merge();
19630
+ init_timezone();
19591
19631
 
19592
19632
  // src/setup/host-capabilities.ts
19593
19633
  init_paths();
19594
19634
 
19635
+ // src/vault/grants-db-path.ts
19636
+ import * as os from "node:os";
19637
+ import * as path from "node:path";
19638
+ import * as fs from "node:fs";
19639
+ var GRANTS_DB_DIRNAME = "vault-broker";
19640
+ var GRANTS_DB_FILENAME = "vault-grants.db";
19641
+ var GRANTS_DB_CONTAINER_DIR = `/root/.switchroom/${GRANTS_DB_DIRNAME}`;
19642
+ var GRANTS_DB_CONTAINER_PATH = `${GRANTS_DB_CONTAINER_DIR}/${GRANTS_DB_FILENAME}`;
19643
+ function getGrantsDbDir(home2 = os.homedir()) {
19644
+ return path.join(home2, ".switchroom", GRANTS_DB_DIRNAME);
19645
+ }
19646
+ function getGrantsDbPath(home2 = os.homedir()) {
19647
+ return path.join(getGrantsDbDir(home2), GRANTS_DB_FILENAME);
19648
+ }
19649
+ function getLegacyGrantsDbPath(newDbPath = getGrantsDbPath()) {
19650
+ return path.join(path.dirname(path.dirname(newDbPath)), GRANTS_DB_FILENAME);
19651
+ }
19652
+ var WAL_SIDECAR_SUFFIXES = ["-wal", "-shm"];
19653
+ function migrateLegacyGrantsDbLocation(newDbPath = getGrantsDbPath(), deps) {
19654
+ const rename = deps?.rename ?? fs.renameSync;
19655
+ const legacyPath = getLegacyGrantsDbPath(newDbPath);
19656
+ if (path.resolve(legacyPath) === path.resolve(newDbPath)) {
19657
+ return { migrated: false };
19658
+ }
19659
+ if (!fs.existsSync(legacyPath))
19660
+ return { migrated: false };
19661
+ if (fs.existsSync(newDbPath))
19662
+ return { migrated: false };
19663
+ fs.mkdirSync(path.dirname(newDbPath), { recursive: true, mode: 448 });
19664
+ const movedSidecars = [];
19665
+ const rollbackSidecars = () => {
19666
+ for (const m of [...movedSidecars].reverse()) {
19667
+ try {
19668
+ rename(m.to, m.from);
19669
+ } catch {}
19670
+ }
19671
+ };
19672
+ for (const suffix of WAL_SIDECAR_SUFFIXES) {
19673
+ const from = `${legacyPath}${suffix}`;
19674
+ const to = `${newDbPath}${suffix}`;
19675
+ if (!fs.existsSync(from))
19676
+ continue;
19677
+ try {
19678
+ rename(from, to);
19679
+ movedSidecars.push({ from, to });
19680
+ } catch (err) {
19681
+ rollbackSidecars();
19682
+ throw new Error(`grants-db migration: failed to move WAL sidecar ${from} -> ${to} ` + `(${err.message}); aborted with the legacy DB intact — ` + `will retry on the next apply/boot`);
19683
+ }
19684
+ }
19685
+ try {
19686
+ rename(legacyPath, newDbPath);
19687
+ } catch (err) {
19688
+ if (fs.existsSync(newDbPath) && !fs.existsSync(legacyPath)) {
19689
+ return { migrated: false };
19690
+ }
19691
+ rollbackSidecars();
19692
+ throw err;
19693
+ }
19694
+ try {
19695
+ fs.chmodSync(newDbPath, 384);
19696
+ } catch {}
19697
+ return { migrated: true, from: legacyPath, to: newDbPath };
19698
+ }
19699
+
19595
19700
  // src/agents/compose.ts
19596
19701
  var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
19597
19702
 
19598
19703
  // src/vault/broker/server.ts
19599
- import { dirname as dirname5, resolve as resolve8, basename as basename4 } from "node:path";
19704
+ import { dirname as dirname6, resolve as resolve9, basename as basename4 } from "node:path";
19600
19705
  import * as os3 from "node:os";
19601
- import * as path3 from "node:path";
19706
+ import * as path4 from "node:path";
19602
19707
 
19603
19708
  // src/vault/migrate-layout.ts
19604
19709
  import {
19605
19710
  copyFileSync as copyFileSync2,
19606
- chmodSync,
19607
- existsSync as existsSync7,
19711
+ chmodSync as chmodSync2,
19712
+ existsSync as existsSync8,
19608
19713
  fsyncSync as fsyncSync4,
19609
19714
  lstatSync as lstatSync2,
19610
- mkdirSync as mkdirSync3,
19715
+ mkdirSync as mkdirSync4,
19611
19716
  openSync as openSync4,
19612
19717
  closeSync as closeSync4,
19613
19718
  readFileSync as readFileSync7,
19614
- renameSync as renameSync3,
19719
+ renameSync as renameSync4,
19615
19720
  statSync as statSync4,
19616
19721
  symlinkSync,
19617
19722
  unlinkSync as unlinkSync3
19618
19723
  } from "node:fs";
19619
19724
  import { createHash as createHash2 } from "node:crypto";
19620
- import { basename as basename3, dirname as dirname2, join as join5 } from "node:path";
19725
+ import { basename as basename3, dirname as dirname3, join as join6 } from "node:path";
19621
19726
  function vaultLayoutPaths(home2) {
19622
- const switchroomRoot = join5(home2, ".switchroom");
19727
+ const switchroomRoot = join6(home2, ".switchroom");
19623
19728
  return {
19624
- oldPath: join5(switchroomRoot, "vault.enc"),
19625
- newPath: join5(switchroomRoot, "vault", "vault.enc"),
19626
- parent: join5(switchroomRoot, "vault"),
19729
+ oldPath: join6(switchroomRoot, "vault.enc"),
19730
+ newPath: join6(switchroomRoot, "vault", "vault.enc"),
19731
+ parent: join6(switchroomRoot, "vault"),
19627
19732
  switchroomRoot
19628
19733
  };
19629
19734
  }
@@ -19632,11 +19737,11 @@ function inspectVaultLayout(home2) {
19632
19737
  }
19633
19738
  function runMigration(home2, opts) {
19634
19739
  const { oldPath, newPath, parent, switchroomRoot } = vaultLayoutPaths(home2);
19635
- const lockTarget = existsSync7(newPath) ? newPath : existsSync7(oldPath) ? oldPath : null;
19740
+ const lockTarget = existsSync8(newPath) ? newPath : existsSync8(oldPath) ? oldPath : null;
19636
19741
  const release = !opts.dryRun && lockTarget !== null ? acquireVaultLock(lockTarget) : null;
19637
19742
  try {
19638
19743
  const oldStat = lstatSyncOrNull(oldPath);
19639
- const newExists = existsSync7(newPath);
19744
+ const newExists = existsSync8(newPath);
19640
19745
  if (oldStat === null && !newExists) {
19641
19746
  return { kind: "no-vault" };
19642
19747
  }
@@ -19672,12 +19777,12 @@ function runMigration(home2, opts) {
19672
19777
  if (oldStat?.isFile() && !newExists) {
19673
19778
  if (opts.dryRun)
19674
19779
  return { kind: "migrated" };
19675
- mkdirSync3(parent, { recursive: true, mode: 448 });
19780
+ mkdirSync4(parent, { recursive: true, mode: 448 });
19676
19781
  const tempNew = `${newPath}.tmp`;
19677
19782
  copyFileSync2(oldPath, tempNew);
19678
- chmodSync(tempNew, 384);
19783
+ chmodSync2(tempNew, 384);
19679
19784
  fsyncFile(tempNew);
19680
- renameSync3(tempNew, newPath);
19785
+ renameSync4(tempNew, newPath);
19681
19786
  fsyncDir(parent);
19682
19787
  atomicReplaceWithSymlink(oldPath, "vault/vault.enc");
19683
19788
  fsyncDir(switchroomRoot);
@@ -19692,37 +19797,37 @@ function runMigration(home2, opts) {
19692
19797
  }
19693
19798
  }
19694
19799
  }
19695
- function lstatSyncOrNull(path) {
19800
+ function lstatSyncOrNull(path2) {
19696
19801
  try {
19697
- return lstatSync2(path);
19802
+ return lstatSync2(path2);
19698
19803
  } catch {
19699
19804
  return null;
19700
19805
  }
19701
19806
  }
19702
- function sha256File(path) {
19703
- const data = readFileSync7(path);
19807
+ function sha256File(path2) {
19808
+ const data = readFileSync7(path2);
19704
19809
  return createHash2("sha256").update(data).digest("hex");
19705
19810
  }
19706
19811
  function atomicReplaceWithSymlink(target, linkTarget) {
19707
- const tmp = join5(dirname2(target), `.${basename3(target)}.symlink-tmp`);
19708
- if (existsSync7(tmp)) {
19812
+ const tmp = join6(dirname3(target), `.${basename3(target)}.symlink-tmp`);
19813
+ if (existsSync8(tmp)) {
19709
19814
  try {
19710
19815
  unlinkSync3(tmp);
19711
19816
  } catch {}
19712
19817
  }
19713
19818
  symlinkSync(linkTarget, tmp);
19714
- renameSync3(tmp, target);
19819
+ renameSync4(tmp, target);
19715
19820
  }
19716
- function fsyncFile(path) {
19717
- const fd = openSync4(path, "r+");
19821
+ function fsyncFile(path2) {
19822
+ const fd = openSync4(path2, "r+");
19718
19823
  try {
19719
19824
  fsyncSync4(fd);
19720
19825
  } finally {
19721
19826
  closeSync4(fd);
19722
19827
  }
19723
19828
  }
19724
- function fsyncDir(path) {
19725
- const fd = openSync4(path, "r");
19829
+ function fsyncDir(path2) {
19830
+ const fd = openSync4(path2, "r");
19726
19831
  try {
19727
19832
  fsyncSync4(fd);
19728
19833
  } finally {
@@ -19736,14 +19841,14 @@ init_loader();
19736
19841
  // src/vault/auto-unlock.ts
19737
19842
  import { createHmac, randomBytes as randomBytes2, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "node:crypto";
19738
19843
  import {
19739
- chmodSync as chmodSync2,
19844
+ chmodSync as chmodSync3,
19740
19845
  closeSync as closeSync5,
19741
- existsSync as existsSync8,
19846
+ existsSync as existsSync9,
19742
19847
  fsyncSync as fsyncSync5,
19743
- mkdirSync as mkdirSync4,
19848
+ mkdirSync as mkdirSync5,
19744
19849
  openSync as openSync5,
19745
19850
  readFileSync as readFileSync8,
19746
- renameSync as renameSync4,
19851
+ renameSync as renameSync5,
19747
19852
  unlinkSync as unlinkSync4,
19748
19853
  writeSync as writeSync4
19749
19854
  } from "node:fs";
@@ -19779,9 +19884,9 @@ function readMachineId() {
19779
19884
  if (override && override.length > 0)
19780
19885
  return override;
19781
19886
  }
19782
- for (const path of [MACHINE_ID_PRIMARY, MACHINE_ID_FALLBACK]) {
19887
+ for (const path2 of [MACHINE_ID_PRIMARY, MACHINE_ID_FALLBACK]) {
19783
19888
  try {
19784
- const id = readFileSync8(path, "utf8").trim();
19889
+ const id = readFileSync8(path2, "utf8").trim();
19785
19890
  if (id.length > 0)
19786
19891
  return id;
19787
19892
  } catch {}
@@ -19818,7 +19923,7 @@ function decryptAutoUnlock(blob, machineId) {
19818
19923
  }
19819
19924
  }
19820
19925
  function readAutoUnlockFile(filePath) {
19821
- if (!existsSync8(filePath)) {
19926
+ if (!existsSync9(filePath)) {
19822
19927
  throw new AutoUnlockDecryptError("io");
19823
19928
  }
19824
19929
  let blob;
@@ -19832,9 +19937,9 @@ function readAutoUnlockFile(filePath) {
19832
19937
  var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
19833
19938
 
19834
19939
  // src/vault/broker/audit-log.ts
19835
- import * as fs from "node:fs";
19836
- import * as os from "node:os";
19837
- import * as path from "node:path";
19940
+ import * as fs2 from "node:fs";
19941
+ import * as os2 from "node:os";
19942
+ import * as path2 from "node:path";
19838
19943
 
19839
19944
  // src/util/audit-hashchain.ts
19840
19945
  import { createHash as createHash4 } from "node:crypto";
@@ -19856,10 +19961,10 @@ function chainRow(state, entry) {
19856
19961
  next: { seq, lastHash: hash }
19857
19962
  };
19858
19963
  }
19859
- function readTail(path, tailBytes = 64 * 1024) {
19964
+ function readTail(path2, tailBytes = 64 * 1024) {
19860
19965
  let fd;
19861
19966
  try {
19862
- fd = openSync6(path, "r");
19967
+ fd = openSync6(path2, "r");
19863
19968
  } catch {
19864
19969
  return null;
19865
19970
  }
@@ -19879,8 +19984,8 @@ function readTail(path, tailBytes = 64 * 1024) {
19879
19984
  } catch {}
19880
19985
  }
19881
19986
  }
19882
- function seedChain(path) {
19883
- const tail = readTail(path);
19987
+ function seedChain(path2) {
19988
+ const tail = readTail(path2);
19884
19989
  if (tail === null || tail === "") {
19885
19990
  return { seq: 0, lastHash: CHAIN_GENESIS };
19886
19991
  }
@@ -19903,8 +20008,8 @@ function seedChain(path) {
19903
20008
 
19904
20009
  // src/vault/broker/test-isolation-guard.ts
19905
20010
  import { mkdtempSync } from "node:fs";
19906
- import { homedir as homedir4, tmpdir } from "node:os";
19907
- import { join as join6, resolve as resolve7, sep } from "node:path";
20011
+ import { homedir as homedir5, tmpdir } from "node:os";
20012
+ import { join as join7, resolve as resolve8, sep } from "node:path";
19908
20013
  function isTestRuntime() {
19909
20014
  return process.env.VITEST !== undefined;
19910
20015
  }
@@ -19912,11 +20017,11 @@ function escapeHatchSet() {
19912
20017
  return process.env.SWITCHROOM_ALLOW_PROD_VAULT_IN_TEST === "1";
19913
20018
  }
19914
20019
  function realSwitchroomHome() {
19915
- return resolve7(homedir4(), ".switchroom");
20020
+ return resolve8(homedir5(), ".switchroom");
19916
20021
  }
19917
20022
  function isUnderRealSwitchroomHome(p) {
19918
20023
  const home2 = realSwitchroomHome();
19919
- const r = resolve7(p);
20024
+ const r = resolve8(p);
19920
20025
  return r === home2 || r.startsWith(home2 + sep);
19921
20026
  }
19922
20027
  var warnedVault = false;
@@ -19925,7 +20030,7 @@ function safeVaultPath(requestedPath) {
19925
20030
  return requestedPath;
19926
20031
  if (!isUnderRealSwitchroomHome(requestedPath))
19927
20032
  return requestedPath;
19928
- const redirected = join6(mkdtempSync(join6(tmpdir(), "sw-test-vault-")), "vault.enc");
20033
+ const redirected = join7(mkdtempSync(join7(tmpdir(), "sw-test-vault-")), "vault.enc");
19929
20034
  if (!warnedVault) {
19930
20035
  warnedVault = true;
19931
20036
  process.stderr.write(`[test-isolation guard] vault path redirected away from the ` + `production tree to an isolated tmpdir — a vault/broker test ` + `resolved its vault path inside ~/.switchroom. See CLAUDE.md ` + `"Vault & shared-state test discipline".
@@ -19941,7 +20046,7 @@ function safeAuditLogPath(requestedPath) {
19941
20046
  if (!isUnderRealSwitchroomHome(requestedPath))
19942
20047
  return requestedPath;
19943
20048
  if (!cachedTmpAuditLog) {
19944
- cachedTmpAuditLog = join6(mkdtempSync(join6(tmpdir(), "sw-test-audit-")), "vault-audit.log");
20049
+ cachedTmpAuditLog = join7(mkdtempSync(join7(tmpdir(), "sw-test-audit-")), "vault-audit.log");
19945
20050
  }
19946
20051
  if (!warnedAudit) {
19947
20052
  warnedAudit = true;
@@ -19957,7 +20062,7 @@ function failOpenStatePath(logPath) {
19957
20062
  }
19958
20063
  function readFailOpenState(statePath) {
19959
20064
  try {
19960
- const raw = fs.readFileSync(statePath, "utf8");
20065
+ const raw = fs2.readFileSync(statePath, "utf8");
19961
20066
  const parsed = JSON.parse(raw);
19962
20067
  const count = Number(parsed.failOpenCount);
19963
20068
  return {
@@ -19980,9 +20085,9 @@ function resolveRotation(opts) {
19980
20085
  }
19981
20086
  function rotateAuditLog(logPath, maxFiles) {
19982
20087
  const oldest = `${logPath}.${maxFiles}`;
19983
- if (fs.existsSync(oldest)) {
20088
+ if (fs2.existsSync(oldest)) {
19984
20089
  try {
19985
- fs.unlinkSync(oldest);
20090
+ fs2.unlinkSync(oldest);
19986
20091
  } catch (err) {
19987
20092
  process.stderr.write(`[vault-audit] ERROR: could not drop oldest rotation ${oldest}: ${err.message}
19988
20093
  `);
@@ -19991,10 +20096,10 @@ function rotateAuditLog(logPath, maxFiles) {
19991
20096
  for (let n = maxFiles - 1;n >= 1; n--) {
19992
20097
  const from = `${logPath}.${n}`;
19993
20098
  const to = `${logPath}.${n + 1}`;
19994
- if (!fs.existsSync(from))
20099
+ if (!fs2.existsSync(from))
19995
20100
  continue;
19996
20101
  try {
19997
- fs.renameSync(from, to);
20102
+ fs2.renameSync(from, to);
19998
20103
  } catch (err) {
19999
20104
  process.stderr.write(`[vault-audit] ERROR: could not rotate ${from} → ${to}: ${err.message}
20000
20105
  `);
@@ -20003,18 +20108,18 @@ function rotateAuditLog(logPath, maxFiles) {
20003
20108
  }
20004
20109
  const snapshotPath = `${logPath}.1`;
20005
20110
  try {
20006
- fs.copyFileSync(logPath, snapshotPath);
20111
+ fs2.copyFileSync(logPath, snapshotPath);
20007
20112
  } catch (err) {
20008
20113
  process.stderr.write(`[vault-audit] ERROR: could not snapshot active audit log ${logPath} → ${snapshotPath}: ${err.message}
20009
20114
  `);
20010
20115
  return;
20011
20116
  }
20012
20117
  try {
20013
- const fd = fs.openSync(snapshotPath, "r");
20118
+ const fd = fs2.openSync(snapshotPath, "r");
20014
20119
  try {
20015
- fs.fsyncSync(fd);
20120
+ fs2.fsyncSync(fd);
20016
20121
  } finally {
20017
- fs.closeSync(fd);
20122
+ fs2.closeSync(fd);
20018
20123
  }
20019
20124
  } catch (err) {
20020
20125
  process.stderr.write(`[vault-audit] ERROR: could not fsync audit snapshot ${snapshotPath}; leaving active log intact to avoid data loss: ${err.message}
@@ -20022,22 +20127,22 @@ function rotateAuditLog(logPath, maxFiles) {
20022
20127
  return;
20023
20128
  }
20024
20129
  try {
20025
- const dirFd = fs.openSync(path.dirname(snapshotPath), "r");
20130
+ const dirFd = fs2.openSync(path2.dirname(snapshotPath), "r");
20026
20131
  try {
20027
- fs.fsyncSync(dirFd);
20132
+ fs2.fsyncSync(dirFd);
20028
20133
  } finally {
20029
- fs.closeSync(dirFd);
20134
+ fs2.closeSync(dirFd);
20030
20135
  }
20031
20136
  } catch {}
20032
20137
  try {
20033
- fs.truncateSync(logPath, 0);
20138
+ fs2.truncateSync(logPath, 0);
20034
20139
  } catch (err) {
20035
20140
  process.stderr.write(`[vault-audit] ERROR: could not truncate active audit log ${logPath}: ${err.message}
20036
20141
  `);
20037
20142
  }
20038
20143
  }
20039
20144
  function defaultAuditLogPath() {
20040
- return path.join(os.homedir(), ".switchroom", "vault-audit.log");
20145
+ return path2.join(os2.homedir(), ".switchroom", "vault-audit.log");
20041
20146
  }
20042
20147
  function callerFromPeer(peer) {
20043
20148
  if (peer.systemdUnit !== null && peer.systemdUnit.length > 0) {
@@ -20059,7 +20164,7 @@ function createAuditLogger(opts = {}) {
20059
20164
  lastError: String(err.message).slice(0, 300)
20060
20165
  };
20061
20166
  try {
20062
- fs.writeFileSync(statePath, `${JSON.stringify(state)}
20167
+ fs2.writeFileSync(statePath, `${JSON.stringify(state)}
20063
20168
  `, { mode: 384 });
20064
20169
  } catch (persistErr) {
20065
20170
  process.stderr.write(`[vault-audit] ERROR: could not persist fail-open counter ${statePath}: ${persistErr.message}
@@ -20073,7 +20178,7 @@ function createAuditLogger(opts = {}) {
20073
20178
  write(entry) {
20074
20179
  if (rotation.maxBytes > 0) {
20075
20180
  try {
20076
- const size = fs.statSync(logPath).size;
20181
+ const size = fs2.statSync(logPath).size;
20077
20182
  if (size >= rotation.maxBytes) {
20078
20183
  rotateAuditLog(logPath, rotation.maxFiles);
20079
20184
  }
@@ -20082,7 +20187,7 @@ function createAuditLogger(opts = {}) {
20082
20187
  const { line, next } = chainRow(chain, entry);
20083
20188
  let fd;
20084
20189
  try {
20085
- fd = fs.openSync(logPath, "a", 384);
20190
+ fd = fs2.openSync(logPath, "a", 384);
20086
20191
  } catch (err) {
20087
20192
  process.stderr.write(`[vault-audit] ERROR: could not open audit log ${logPath}: ${err.message}
20088
20193
  `);
@@ -20091,7 +20196,7 @@ function createAuditLogger(opts = {}) {
20091
20196
  }
20092
20197
  let durable = false;
20093
20198
  try {
20094
- fs.writeSync(fd, line);
20199
+ fs2.writeSync(fd, line);
20095
20200
  chain = next;
20096
20201
  durable = true;
20097
20202
  } catch (err) {
@@ -20100,7 +20205,7 @@ function createAuditLogger(opts = {}) {
20100
20205
  recordFailOpen(err);
20101
20206
  } finally {
20102
20207
  try {
20103
- fs.closeSync(fd);
20208
+ fs2.closeSync(fd);
20104
20209
  } catch (closeErr) {
20105
20210
  process.stderr.write(`[vault-audit] ERROR: could not close audit log fd: ${closeErr.message}
20106
20211
  `);
@@ -20169,13 +20274,13 @@ function genSalt(rounds, seed_length, callback) {
20169
20274
  throw Error("Illegal callback: " + typeof callback);
20170
20275
  _async(callback);
20171
20276
  } else
20172
- return new Promise(function(resolve8, reject) {
20277
+ return new Promise(function(resolve9, reject) {
20173
20278
  _async(function(err, res) {
20174
20279
  if (err) {
20175
20280
  reject(err);
20176
20281
  return;
20177
20282
  }
20178
- resolve8(res);
20283
+ resolve9(res);
20179
20284
  });
20180
20285
  });
20181
20286
  }
@@ -20195,13 +20300,13 @@ function hash(password, salt, callback, progressCallback) {
20195
20300
  throw Error("Illegal callback: " + typeof callback);
20196
20301
  _async(callback);
20197
20302
  } else
20198
- return new Promise(function(resolve8, reject) {
20303
+ return new Promise(function(resolve9, reject) {
20199
20304
  _async(function(err, res) {
20200
20305
  if (err) {
20201
20306
  reject(err);
20202
20307
  return;
20203
20308
  }
20204
- resolve8(res);
20309
+ resolve9(res);
20205
20310
  });
20206
20311
  });
20207
20312
  }
@@ -20234,13 +20339,13 @@ function compare(password, hashValue, callback, progressCallback) {
20234
20339
  throw Error("Illegal callback: " + typeof callback);
20235
20340
  _async(callback);
20236
20341
  } else
20237
- return new Promise(function(resolve8, reject) {
20342
+ return new Promise(function(resolve9, reject) {
20238
20343
  _async(function(err, res) {
20239
20344
  if (err) {
20240
20345
  reject(err);
20241
20346
  return;
20242
20347
  }
20243
- resolve8(res);
20348
+ resolve9(res);
20244
20349
  });
20245
20350
  });
20246
20351
  }
@@ -21834,6 +21939,11 @@ function rowToGrant(row) {
21834
21939
  description: row.description ?? null
21835
21940
  };
21836
21941
  }
21942
+ function checkpointGrantsWal(db) {
21943
+ try {
21944
+ db.run("PRAGMA wal_checkpoint(TRUNCATE)");
21945
+ } catch {}
21946
+ }
21837
21947
  function keyMatchesPatterns(patterns, key) {
21838
21948
  for (const pat of patterns) {
21839
21949
  if (pat.endsWith("*")) {
@@ -21864,6 +21974,7 @@ async function mintGrant(db, agent_slug, key_allow, ttl_seconds, description, wr
21864
21974
  now,
21865
21975
  description ?? null
21866
21976
  ]);
21977
+ checkpointGrantsWal(db);
21867
21978
  return { token, id, expires_at };
21868
21979
  }
21869
21980
  async function validateGrant(db, token, key) {
@@ -21942,6 +22053,7 @@ async function validateGrantForWrite(db, token, key) {
21942
22053
  function revokeGrant(db, id) {
21943
22054
  const now = Math.floor(Date.now() / 1000);
21944
22055
  const result = db.run(`UPDATE vault_grants SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL`, [now, id]);
22056
+ checkpointGrantsWal(db);
21945
22057
  return (result.changes ?? 0) > 0;
21946
22058
  }
21947
22059
  function listGrants(db, agent_slug) {
@@ -21984,9 +22096,8 @@ function adminOnlyKeysBeingAdded(requestedKeys, existingKeys, patterns) {
21984
22096
  }
21985
22097
 
21986
22098
  // src/vault/grants-db.ts
21987
- import * as os2 from "node:os";
21988
- import * as path2 from "node:path";
21989
- import * as fs2 from "node:fs";
22099
+ import * as path3 from "node:path";
22100
+ import * as fs3 from "node:fs";
21990
22101
  import { Database } from "bun:sqlite";
21991
22102
 
21992
22103
  // src/vault/approvals/schema.ts
@@ -22064,8 +22175,8 @@ function migrateApprovalSchema(db) {
22064
22175
  var DEFAULT_MAX_TTL_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
22065
22176
 
22066
22177
  // src/vault/grants-db.ts
22067
- var DEFAULT_GRANTS_DB_PATH = path2.join(os2.homedir(), ".switchroom", "vault-grants.db");
22068
- var WAL_SIDECAR_SUFFIXES = ["-wal", "-shm"];
22178
+ var DEFAULT_GRANTS_DB_PATH = getGrantsDbPath();
22179
+ var WAL_SIDECAR_SUFFIXES2 = ["-wal", "-shm"];
22069
22180
  function isGrantsDbCorruption(err) {
22070
22181
  if (!err || typeof err !== "object")
22071
22182
  return false;
@@ -22079,12 +22190,12 @@ function isGrantsDbCorruption(err) {
22079
22190
  function quarantineGrantsDb(dbPath) {
22080
22191
  const stamp = new Date().toISOString().replace(/[:.]/g, "-");
22081
22192
  const quarantinePath = `${dbPath}.corrupt-${stamp}`;
22082
- fs2.renameSync(dbPath, quarantinePath);
22083
- for (const suffix of WAL_SIDECAR_SUFFIXES) {
22193
+ fs3.renameSync(dbPath, quarantinePath);
22194
+ for (const suffix of WAL_SIDECAR_SUFFIXES2) {
22084
22195
  const sidecar = `${dbPath}${suffix}`;
22085
- if (fs2.existsSync(sidecar)) {
22196
+ if (fs3.existsSync(sidecar)) {
22086
22197
  try {
22087
- fs2.renameSync(sidecar, `${quarantinePath}${suffix}`);
22198
+ fs3.renameSync(sidecar, `${quarantinePath}${suffix}`);
22088
22199
  } catch {}
22089
22200
  }
22090
22201
  }
@@ -22094,7 +22205,7 @@ function openAndMigrate(dbPath) {
22094
22205
  const db = new Database(dbPath, { create: true });
22095
22206
  try {
22096
22207
  try {
22097
- fs2.chmodSync(dbPath, 384);
22208
+ fs3.chmodSync(dbPath, 384);
22098
22209
  } catch {}
22099
22210
  db.run("PRAGMA journal_mode=WAL");
22100
22211
  db.run("PRAGMA busy_timeout=5000");
@@ -22110,8 +22221,9 @@ function openAndMigrate(dbPath) {
22110
22221
  return db;
22111
22222
  }
22112
22223
  function openGrantsDb(dbPath = DEFAULT_GRANTS_DB_PATH, opener = openAndMigrate) {
22113
- const dir = path2.dirname(dbPath);
22114
- fs2.mkdirSync(dir, { recursive: true });
22224
+ const dir = path3.dirname(dbPath);
22225
+ fs3.mkdirSync(dir, { recursive: true });
22226
+ migrateLegacyGrantsDbLocation(dbPath);
22115
22227
  try {
22116
22228
  return opener(dbPath);
22117
22229
  } catch (err) {
@@ -22462,7 +22574,7 @@ class VaultBroker {
22462
22574
  if (process.platform !== "linux" && process.env.SWITCHROOM_BROKER_ALLOW_NON_LINUX !== "1") {
22463
22575
  throw new Error(`vault-broker is Linux-only (running on ${process.platform}). ` + `The broker's ACL relies on cgroup-based systemd unit identification, ` + `which is not available on this platform. ` + `Use 'switchroom vault get --no-broker' for direct vault access. ` + `If you need to run the broker for development on this platform, ` + `set SWITCHROOM_BROKER_ALLOW_NON_LINUX=1 — but understand that the ` + `broker will accept any same-user caller without per-cron ACL enforcement.`);
22464
22576
  }
22465
- this.socketPath = resolve8(socketPath);
22577
+ this.socketPath = resolve9(socketPath);
22466
22578
  this.unlockSocketPath = unlockSocketFor(this.socketPath);
22467
22579
  this.startedAt = Date.now();
22468
22580
  if (this.testOpts._testConfig) {
@@ -22472,7 +22584,7 @@ class VaultBroker {
22472
22584
  this.config = loadConfig2(configPath);
22473
22585
  }
22474
22586
  if (vaultPath) {
22475
- this.vaultPath = resolve8(vaultPath);
22587
+ this.vaultPath = resolve9(vaultPath);
22476
22588
  } else {
22477
22589
  this.vaultPath = resolvePath(this.config.vault?.path ?? "~/.switchroom/vault.enc");
22478
22590
  }
@@ -22484,13 +22596,13 @@ class VaultBroker {
22484
22596
  this.passphrase = this.testOpts._testPassphrase;
22485
22597
  }
22486
22598
  process.umask(63);
22487
- const parentDir = dirname5(this.socketPath);
22488
- mkdirSync6(parentDir, { recursive: true, mode: 448 });
22599
+ const parentDir = dirname6(this.socketPath);
22600
+ mkdirSync7(parentDir, { recursive: true, mode: 448 });
22489
22601
  try {
22490
- chmodSync4(parentDir, 448);
22602
+ chmodSync5(parentDir, 448);
22491
22603
  } catch {}
22492
22604
  for (const p of [this.socketPath, this.unlockSocketPath]) {
22493
- if (existsSync11(p)) {
22605
+ if (existsSync12(p)) {
22494
22606
  try {
22495
22607
  unlinkSync6(p);
22496
22608
  } catch {}
@@ -22526,7 +22638,7 @@ class VaultBroker {
22526
22638
  try {
22527
22639
  if (ready) {
22528
22640
  writeFileSync3(p, "", { mode: 384 });
22529
- } else if (existsSync11(p)) {
22641
+ } else if (existsSync12(p)) {
22530
22642
  unlinkSync6(p);
22531
22643
  }
22532
22644
  } catch {}
@@ -22559,7 +22671,7 @@ class VaultBroker {
22559
22671
  try {
22560
22672
  entry.server.close();
22561
22673
  } catch {}
22562
- if (existsSync11(sockPath)) {
22674
+ if (existsSync12(sockPath)) {
22563
22675
  try {
22564
22676
  unlinkSync6(sockPath);
22565
22677
  } catch {}
@@ -22567,7 +22679,7 @@ class VaultBroker {
22567
22679
  }
22568
22680
  this.agentServers.clear();
22569
22681
  for (const p of [this.socketPath, this.unlockSocketPath]) {
22570
- if (p && existsSync11(p)) {
22682
+ if (p && existsSync12(p)) {
22571
22683
  try {
22572
22684
  unlinkSync6(p);
22573
22685
  } catch {}
@@ -22575,7 +22687,7 @@ class VaultBroker {
22575
22687
  }
22576
22688
  try {
22577
22689
  const pidPath = resolvePath(PID_FILE_DEFAULT);
22578
- if (existsSync11(pidPath))
22690
+ if (existsSync12(pidPath))
22579
22691
  unlinkSync6(pidPath);
22580
22692
  } catch {}
22581
22693
  }
@@ -22593,7 +22705,7 @@ class VaultBroker {
22593
22705
  return this.config;
22594
22706
  }
22595
22707
  bindAgentSocket(socketPath) {
22596
- const abs = resolve8(socketPath);
22708
+ const abs = resolve9(socketPath);
22597
22709
  const agentName = socketPathToAgent(abs);
22598
22710
  if (agentName === null) {
22599
22711
  return Promise.reject(new Error(`bindAgentSocket: socket path '${abs}' does not match the canonical ` + `/run/switchroom/broker/<agent>.sock shape — refusing to bind without ` + `a verifiable agent identity`));
@@ -22601,16 +22713,16 @@ class VaultBroker {
22601
22713
  return new Promise((resolveP, rejectP) => {
22602
22714
  if (abs.endsWith("/sock")) {
22603
22715
  const dir = abs.slice(0, -"/sock".length);
22604
- if (existsSync11(dir)) {
22716
+ if (existsSync12(dir)) {
22605
22717
  try {
22606
22718
  chownSync(dir, 0, 0);
22607
22719
  } catch {}
22608
22720
  try {
22609
- chmodSync4(dir, 448);
22721
+ chmodSync5(dir, 448);
22610
22722
  } catch {}
22611
22723
  }
22612
22724
  }
22613
- if (existsSync11(abs)) {
22725
+ if (existsSync12(abs)) {
22614
22726
  try {
22615
22727
  unlinkSync6(abs);
22616
22728
  } catch (err) {
@@ -22625,7 +22737,7 @@ class VaultBroker {
22625
22737
  server.on("error", (err) => rejectP(err));
22626
22738
  server.listen(abs, () => {
22627
22739
  try {
22628
- chmodSync4(abs, 432);
22740
+ chmodSync5(abs, 432);
22629
22741
  } catch {}
22630
22742
  let agentUid = null;
22631
22743
  try {
@@ -22642,7 +22754,7 @@ class VaultBroker {
22642
22754
  } catch {}
22643
22755
  this.agentServers.set(abs, { server, agentName });
22644
22756
  const unlockAbs = unlockSocketFor(abs);
22645
- if (existsSync11(unlockAbs)) {
22757
+ if (existsSync12(unlockAbs)) {
22646
22758
  try {
22647
22759
  unlinkSync6(unlockAbs);
22648
22760
  } catch {}
@@ -22659,7 +22771,7 @@ class VaultBroker {
22659
22771
  });
22660
22772
  unlockServer.listen(unlockAbs, () => {
22661
22773
  try {
22662
- chmodSync4(unlockAbs, 432);
22774
+ chmodSync5(unlockAbs, 432);
22663
22775
  } catch {}
22664
22776
  if (agentUid !== null) {
22665
22777
  try {
@@ -22673,7 +22785,7 @@ class VaultBroker {
22673
22785
  });
22674
22786
  }
22675
22787
  _bindDataSocket() {
22676
- return new Promise((resolve9, reject) => {
22788
+ return new Promise((resolve10, reject) => {
22677
22789
  const server = net.createServer((socket) => {
22678
22790
  this._handleDataConnection(socket);
22679
22791
  });
@@ -22682,10 +22794,10 @@ class VaultBroker {
22682
22794
  });
22683
22795
  server.listen(this.socketPath, () => {
22684
22796
  try {
22685
- chmodSync4(this.socketPath, 384);
22797
+ chmodSync5(this.socketPath, 384);
22686
22798
  } catch {}
22687
22799
  this.server = server;
22688
- resolve9();
22800
+ resolve10();
22689
22801
  });
22690
22802
  });
22691
22803
  }
@@ -22697,12 +22809,12 @@ class VaultBroker {
22697
22809
  if (!Number.isFinite(uid) || uid <= 0)
22698
22810
  return;
22699
22811
  try {
22700
- if (existsSync11(this.vaultPath))
22812
+ if (existsSync12(this.vaultPath))
22701
22813
  chownSync(this.vaultPath, uid, uid);
22702
22814
  } catch {}
22703
22815
  }
22704
22816
  bindOperatorListener(socketPath, operatorUid) {
22705
- const abs = resolve8(socketPath);
22817
+ const abs = resolve9(socketPath);
22706
22818
  const identity = socketPathToIdentity(abs);
22707
22819
  if (identity?.kind !== "operator") {
22708
22820
  return Promise.reject(new Error(`bindOperatorListener: socket path '${abs}' does not match the canonical ` + `/run/switchroom/broker/operator/sock shape — refusing to bind`));
@@ -22710,17 +22822,17 @@ class VaultBroker {
22710
22822
  const unlockAbs = unlockSocketFor(abs);
22711
22823
  if (abs.endsWith("/sock")) {
22712
22824
  const dir = abs.slice(0, -"/sock".length);
22713
- if (existsSync11(dir)) {
22825
+ if (existsSync12(dir)) {
22714
22826
  try {
22715
22827
  chownSync(dir, 0, 0);
22716
22828
  } catch {}
22717
22829
  try {
22718
- chmodSync4(dir, 448);
22830
+ chmodSync5(dir, 448);
22719
22831
  } catch {}
22720
22832
  }
22721
22833
  }
22722
22834
  for (const p of [abs, unlockAbs]) {
22723
- if (existsSync11(p)) {
22835
+ if (existsSync12(p)) {
22724
22836
  try {
22725
22837
  unlinkSync6(p);
22726
22838
  } catch {}
@@ -22733,7 +22845,7 @@ class VaultBroker {
22733
22845
  dataServer.on("error", (err) => rejectP(err));
22734
22846
  dataServer.listen(abs, () => {
22735
22847
  try {
22736
- chmodSync4(abs, 384);
22848
+ chmodSync5(abs, 384);
22737
22849
  } catch {}
22738
22850
  try {
22739
22851
  chownSync(abs, operatorUid, operatorUid);
@@ -22744,7 +22856,7 @@ class VaultBroker {
22744
22856
  unlockServer.on("error", (err) => rejectP(err));
22745
22857
  unlockServer.listen(unlockAbs, () => {
22746
22858
  try {
22747
- chmodSync4(unlockAbs, 384);
22859
+ chmodSync5(unlockAbs, 384);
22748
22860
  } catch {}
22749
22861
  try {
22750
22862
  chownSync(unlockAbs, operatorUid, operatorUid);
@@ -22755,7 +22867,7 @@ class VaultBroker {
22755
22867
  chownSync(dir, operatorUid, operatorUid);
22756
22868
  } catch {}
22757
22869
  try {
22758
- chmodSync4(dir, 448);
22870
+ chmodSync5(dir, 448);
22759
22871
  } catch {}
22760
22872
  }
22761
22873
  this.agentServers.set(abs, { server: dataServer, agentName: "__operator__" });
@@ -22766,7 +22878,7 @@ class VaultBroker {
22766
22878
  });
22767
22879
  }
22768
22880
  _bindUnlockSocket() {
22769
- return new Promise((resolve9, reject) => {
22881
+ return new Promise((resolve10, reject) => {
22770
22882
  const server = net.createServer((socket) => {
22771
22883
  this._handleUnlockConnection(socket);
22772
22884
  });
@@ -22775,10 +22887,10 @@ class VaultBroker {
22775
22887
  });
22776
22888
  server.listen(this.unlockSocketPath, () => {
22777
22889
  try {
22778
- chmodSync4(this.unlockSocketPath, 384);
22890
+ chmodSync5(this.unlockSocketPath, 384);
22779
22891
  } catch {}
22780
22892
  this.unlockServer = server;
22781
- resolve9();
22893
+ resolve10();
22782
22894
  });
22783
22895
  });
22784
22896
  }
@@ -23617,13 +23729,13 @@ class VaultBroker {
23617
23729
  return;
23618
23730
  }
23619
23731
  try {
23620
- const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os3.homedir(), ".switchroom", "agents");
23621
- const tokenDir = path3.join(agentsDir, agent);
23622
- mkdirSync6(tokenDir, { recursive: true });
23623
- const tokenPath = path3.join(tokenDir, ".vault-token");
23732
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path4.join(os3.homedir(), ".switchroom", "agents");
23733
+ const tokenDir = path4.join(agentsDir, agent);
23734
+ mkdirSync7(tokenDir, { recursive: true });
23735
+ const tokenPath = path4.join(tokenDir, ".vault-token");
23624
23736
  const tmpPath = `${tokenPath}.tmp.${process.pid}`;
23625
23737
  writeFileSync3(tmpPath, mintResult.token, { mode: 384 });
23626
- renameSync7(tmpPath, tokenPath);
23738
+ renameSync8(tmpPath, tokenPath);
23627
23739
  try {
23628
23740
  const uid = allocateAgentUid(agent);
23629
23741
  chownSync(tokenPath, uid, uid);
@@ -23682,9 +23794,9 @@ class VaultBroker {
23682
23794
  try {
23683
23795
  const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
23684
23796
  if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
23685
- const agentsDir = this.config ? resolveAgentsDir(this.config) : path3.join(os3.homedir(), ".switchroom", "agents");
23686
- const tokenPath = path3.join(agentsDir, row.agent_slug, ".vault-token");
23687
- if (existsSync11(tokenPath)) {
23797
+ const agentsDir = this.config ? resolveAgentsDir(this.config) : path4.join(os3.homedir(), ".switchroom", "agents");
23798
+ const tokenPath = path4.join(agentsDir, row.agent_slug, ".vault-token");
23799
+ if (existsSync12(tokenPath)) {
23688
23800
  try {
23689
23801
  unlinkSync6(tokenPath);
23690
23802
  } catch {}
@@ -23922,7 +24034,7 @@ class VaultBroker {
23922
24034
  const envPath = process.env.SWITCHROOM_VAULT_BROKER_AUTO_UNLOCK_PATH;
23923
24035
  const configuredPath = (envPath && envPath.length > 0 ? envPath : undefined) ?? this.config?.vault?.broker?.autoUnlockCredentialPath ?? DEFAULT_AUTO_UNLOCK_PATH;
23924
24036
  const filePath = resolvePath(configuredPath);
23925
- if (!existsSync11(filePath))
24037
+ if (!existsSync12(filePath))
23926
24038
  return false;
23927
24039
  try {
23928
24040
  if (statSync6(filePath).size === 0)
@@ -24003,15 +24115,15 @@ class VaultBroker {
24003
24115
  }
24004
24116
  }
24005
24117
  function detectVaultLayoutDrift(vaultPath) {
24006
- const dir = dirname5(vaultPath);
24118
+ const dir = dirname6(vaultPath);
24007
24119
  if (basename4(dir) !== "vault")
24008
24120
  return;
24009
24121
  if (basename4(vaultPath) !== "vault.enc")
24010
24122
  return;
24011
- const switchroomDir = dirname5(dir);
24123
+ const switchroomDir = dirname6(dir);
24012
24124
  if (basename4(switchroomDir) !== ".switchroom")
24013
24125
  return;
24014
- const home2 = dirname5(switchroomDir);
24126
+ const home2 = dirname6(switchroomDir);
24015
24127
  const result = inspectVaultLayout(home2);
24016
24128
  if (result.kind === "divergent") {
24017
24129
  throw new VaultError(`Vault layout divergence detected at boot: ${result.details.oldPath} and ${result.details.newPath} are both regular files with different content. An older switchroom CLI may have written to the legacy path after migration ran. Run \`switchroom apply\` from the host to surface the recovery recipe (state E refusal with literal \`mv\` commands). See docs/operators/state-e-recovery.md.`);
@@ -24037,7 +24149,7 @@ async function main() {
24037
24149
  const vaultPath = process.env.SWITCHROOM_VAULT_PATH;
24038
24150
  let perAgentTargets = [];
24039
24151
  try {
24040
- if (existsSync11(perAgentDir)) {
24152
+ if (existsSync12(perAgentDir)) {
24041
24153
  const entries = readdirSync4(perAgentDir, { withFileTypes: true });
24042
24154
  const flat = [];
24043
24155
  const subdirs = [];
@@ -24045,11 +24157,11 @@ async function main() {
24045
24157
  if (e.name.startsWith("."))
24046
24158
  continue;
24047
24159
  if ((e.isFile() || e.isSocket()) && e.name.endsWith(".sock")) {
24048
- flat.push(resolve8(perAgentDir, e.name));
24160
+ flat.push(resolve9(perAgentDir, e.name));
24049
24161
  continue;
24050
24162
  }
24051
24163
  if (e.isDirectory()) {
24052
- const candidate = resolve8(perAgentDir, e.name, "sock");
24164
+ const candidate = resolve9(perAgentDir, e.name, "sock");
24053
24165
  if (socketPathToAgent(candidate) !== null) {
24054
24166
  subdirs.push(candidate);
24055
24167
  }
@@ -24092,7 +24204,7 @@ async function main() {
24092
24204
  }
24093
24205
  const operatorUidStr = process.env.SWITCHROOM_BROKER_OPERATOR_UID;
24094
24206
  const operatorDir = "/run/switchroom/broker/operator";
24095
- if (operatorUidStr !== undefined && existsSync11(operatorDir)) {
24207
+ if (operatorUidStr !== undefined && existsSync12(operatorDir)) {
24096
24208
  const operatorUid = parseInt(operatorUidStr, 10);
24097
24209
  if (!Number.isFinite(operatorUid) || operatorUid <= 0) {
24098
24210
  process.stderr.write(`[vault-broker] SWITCHROOM_BROKER_OPERATOR_UID='${operatorUidStr}' is not a positive integer; skipping operator listener