switchroom 0.10.0 → 0.11.1

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 (52) hide show
  1. package/README.md +5 -4
  2. package/dist/agent-scheduler/index.js +2 -2
  3. package/dist/auth-broker/index.js +125 -3
  4. package/dist/cli/drive-write-pretool.mjs +5436 -0
  5. package/dist/cli/switchroom.js +231 -29
  6. package/dist/host-control/main.js +2 -2
  7. package/dist/vault/approvals/kernel-server.js +2 -2
  8. package/dist/vault/broker/server.js +2 -2
  9. package/package.json +1 -1
  10. package/telegram-plugin/admin-commands/dispatch.test.ts +1 -1
  11. package/telegram-plugin/admin-commands/index.ts +2 -0
  12. package/telegram-plugin/auth-snapshot-format.ts +612 -0
  13. package/telegram-plugin/auto-fallback-fleet.ts +215 -0
  14. package/telegram-plugin/auto-fallback.ts +28 -301
  15. package/telegram-plugin/dist/gateway/gateway.js +4314 -2143
  16. package/telegram-plugin/fleet-fallback-gate.ts +105 -0
  17. package/telegram-plugin/gateway/approval-callback.test.ts +104 -0
  18. package/telegram-plugin/gateway/approval-callback.ts +31 -3
  19. package/telegram-plugin/gateway/auth-broker-client.ts +2 -0
  20. package/telegram-plugin/gateway/auth-command.ts +131 -10
  21. package/telegram-plugin/gateway/auth-status-adapter.ts +101 -0
  22. package/telegram-plugin/gateway/boot-card.ts +1 -1
  23. package/telegram-plugin/gateway/boot-probes.ts +6 -9
  24. package/telegram-plugin/gateway/diff-preview-card.test.ts +192 -0
  25. package/telegram-plugin/gateway/diff-preview-card.ts +170 -0
  26. package/telegram-plugin/gateway/drive-write-approval.test.ts +312 -0
  27. package/telegram-plugin/gateway/drive-write-approval.ts +243 -0
  28. package/telegram-plugin/gateway/folder-picker-handler.test.ts +314 -0
  29. package/telegram-plugin/gateway/folder-picker-handler.ts +348 -0
  30. package/telegram-plugin/gateway/gateway.ts +903 -173
  31. package/telegram-plugin/gateway/hostd-dispatch.ts +137 -2
  32. package/telegram-plugin/gateway/ipc-protocol.ts +83 -2
  33. package/telegram-plugin/gateway/ipc-server.ts +69 -0
  34. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +103 -12
  35. package/telegram-plugin/model-unavailable.ts +28 -12
  36. package/telegram-plugin/silence-poke.ts +153 -1
  37. package/telegram-plugin/tests/auth-command-format2.test.ts +156 -0
  38. package/telegram-plugin/tests/auth-snapshot-format.test.ts +429 -0
  39. package/telegram-plugin/tests/auth-status-adapter.test.ts +129 -0
  40. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +211 -0
  41. package/telegram-plugin/tests/auto-fallback.test.ts +60 -358
  42. package/telegram-plugin/tests/boot-probes.test.ts +16 -18
  43. package/telegram-plugin/tests/fleet-fallback-gate.test.ts +197 -0
  44. package/telegram-plugin/tests/model-unavailable.test.ts +30 -5
  45. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +212 -2
  46. package/telegram-plugin/tests/silence-poke.test.ts +237 -0
  47. package/telegram-plugin/tests/turn-flush-safety.test.ts +112 -0
  48. package/telegram-plugin/turn-flush-safety.ts +55 -1
  49. package/telegram-plugin/uat/SETUP.md +16 -12
  50. package/telegram-plugin/auto-fallback-dispatcher.ts +0 -68
  51. package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +0 -183
  52. package/telegram-plugin/tests/hostd-dispatch.test.ts +0 -129
@@ -13850,7 +13850,7 @@ var init_schema = __esm(() => {
13850
13850
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
13851
13851
  });
13852
13852
  HostControlConfigSchema = exports_external.object({
13853
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Since Phase 2 (#1175 PR \u03b3) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled \u2014 replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
13853
+ enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` \u2014 " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C \u00a75.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
13854
13854
  });
13855
13855
  SwitchroomConfigSchema = exports_external.object({
13856
13856
  switchroom: exports_external.object({
@@ -13876,7 +13876,7 @@ var init_schema = __esm(() => {
13876
13876
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key \u2014 use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
13877
13877
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
13878
13878
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
13879
- host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
13879
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
13880
13880
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
13881
13881
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
13882
13882
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -22669,7 +22669,7 @@ function generateCompose(opts) {
22669
22669
  const buildContext = opts.buildContext;
22670
22670
  const homePrefix = opts.homeDir ?? "${HOME}";
22671
22671
  const containerNamePrefix = opts.containerNamePrefix ?? "switchroom";
22672
- const hostControlEnabled = config.host_control?.enabled === true;
22672
+ const hostControlEnabled = config.host_control?.enabled !== false;
22673
22673
  const hostHomeForChecks = opts.homeDir ?? process.env.HOME ?? "";
22674
22674
  const switchroomConfigPath = opts.switchroomConfigPath;
22675
22675
  const bundledSkillsPoolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
@@ -22860,6 +22860,7 @@ function generateCompose(opts) {
22860
22860
  }
22861
22861
  for (const c of authConsumers) {
22862
22862
  lines.push(` auth-broker-${c.name}-sock:`);
22863
+ lines.push(` name: auth-broker-${c.name}-sock`);
22863
22864
  }
22864
22865
  lines.push("");
22865
22866
  return lines.join(`
@@ -22951,6 +22952,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
22951
22952
  if (existsSync13(`${hostHomeForChecks}/.switchroom/vault-audit.log`)) {
22952
22953
  lines.push(` - ${homePrefix}/.switchroom/vault-audit.log:/state/agent/home/.switchroom/vault-audit.log:ro`);
22953
22954
  }
22955
+ if (existsSync13(`${hostHomeForChecks}/.switchroom/host-control-audit.log`)) {
22956
+ lines.push(` - ${homePrefix}/.switchroom/host-control-audit.log:/state/agent/home/.switchroom/host-control-audit.log:ro`);
22957
+ }
22954
22958
  if (hostControlEnabled && existsSync13(`${hostHomeForChecks}/.switchroom/hostd/${a.name}`)) {
22955
22959
  lines.push(` - ${homePrefix}/.switchroom/hostd/${a.name}:/run/switchroom/hostd/${a.name}`);
22956
22960
  }
@@ -25660,17 +25664,22 @@ function checkHindsightConsumer(config, opts) {
25660
25664
  ` + "then run `switchroom apply` to bind the per-consumer socket."
25661
25665
  };
25662
25666
  }
25663
- const socketProbe = opts?.socketProbe ?? ((p) => existsSync22(p));
25664
- const home2 = process.env.HOME ?? "";
25665
- const dockerVolPath = `/var/lib/docker/volumes/switchroom_auth-broker-hindsight-sock/_data/sock`;
25666
- const altVolPath = join17(home2, ".local", "share", "docker", "volumes", "switchroom_auth-broker-hindsight-sock", "_data", "sock");
25667
- const present = socketProbe(dockerVolPath) || socketProbe(altVolPath);
25668
- if (!present) {
25667
+ const probe2 = opts?.socketProbe ?? probeAuthBrokerSocket;
25668
+ const state = probe2(entry.name);
25669
+ if (state === "unreachable") {
25669
25670
  return {
25670
25671
  name: "hindsight consumer",
25671
25672
  status: "warn",
25672
- detail: `auth.consumers[hindsight] -> ${entry.account} (uid ${entry.uid ?? 0}); ` + `socket not yet bound on disk`,
25673
- fix: "Run `switchroom apply` to bring up the auth-broker singleton " + "and bind the per-consumer socket. The hindsight container will " + "fetch credentials from it via `get-credentials` at boot."
25673
+ detail: `auth.consumers[hindsight] -> ${entry.account} (uid ${entry.uid ?? 0}); ` + `couldn't query auth-broker container (not running / docker unavailable)`,
25674
+ fix: "Check `auth-broker: service health` row above; if the broker is " + "down, `switchroom apply` will bring it back and bind the socket."
25675
+ };
25676
+ }
25677
+ if (state === "missing") {
25678
+ return {
25679
+ name: "hindsight consumer",
25680
+ status: "warn",
25681
+ detail: `auth.consumers[hindsight] -> ${entry.account} (uid ${entry.uid ?? 0}); ` + `auth-broker is running but socket not bound at /run/switchroom/auth-broker/${entry.name}/sock`,
25682
+ fix: "Run `switchroom apply` to refresh compose and rebind per-consumer sockets."
25674
25683
  };
25675
25684
  }
25676
25685
  return {
@@ -25679,6 +25688,17 @@ function checkHindsightConsumer(config, opts) {
25679
25688
  detail: `auth.consumers[hindsight] -> ${entry.account} (uid ${entry.uid ?? 0})`
25680
25689
  };
25681
25690
  }
25691
+ function probeAuthBrokerSocket(consumerName) {
25692
+ const containerPath = `/run/switchroom/auth-broker/${consumerName}/sock`;
25693
+ const r = spawnSync3("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
25694
+ if (r.error || r.status === null)
25695
+ return "unreachable";
25696
+ if (r.status === 0)
25697
+ return "present";
25698
+ if (r.status >= 125)
25699
+ return "unreachable";
25700
+ return "missing";
25701
+ }
25682
25702
  async function checkHindsight(config) {
25683
25703
  const memoryBackend = config.memory?.backend;
25684
25704
  if (memoryBackend !== "hindsight") {
@@ -27041,7 +27061,7 @@ function decodeResponse2(line) {
27041
27061
  }
27042
27062
  return ResponseSchema2.parse(parsed);
27043
27063
  }
27044
- var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
27064
+ var MAX_FRAME_BYTES2, PROTOCOL_VERSION = 1, ProviderNameSchema, GetCredentialsRequestSchema, ListStateRequestSchema, SetActiveRequestSchema, MarkExhaustedRequestSchema, RefreshAccountRequestSchema, AnthropicCredentialsSchema, GoogleCredentialsSchema, ProviderCredentialsSchema, AddAccountRequestSchema, RmAccountRequestSchema, SetOverrideRequestSchema, ListGoogleAccountsRequestSchema, ProbeQuotaRequestSchema, RequestSchema2, GetCredentialsDataSchema, AccountStateSchema, AgentStateSchema, ConsumerStateSchema, ListStateDataSchema, SetActiveDataSchema, MarkExhaustedDataSchema, RefreshAccountDataSchema, AddAccountDataSchema, RmAccountDataSchema, SetOverrideDataSchema, GoogleAccountStateSchema, ListGoogleAccountsDataSchema, ErrorBodySchema, SuccessResponseSchema, ErrorResponseSchema2, ResponseSchema2;
27045
27065
  var init_protocol2 = __esm(() => {
27046
27066
  init_zod();
27047
27067
  MAX_FRAME_BYTES2 = 64 * 1024;
@@ -27130,6 +27150,13 @@ var init_protocol2 = __esm(() => {
27130
27150
  op: exports_external.literal("list-google-accounts"),
27131
27151
  id: exports_external.string().min(1)
27132
27152
  });
27153
+ ProbeQuotaRequestSchema = exports_external.object({
27154
+ v: exports_external.literal(PROTOCOL_VERSION),
27155
+ op: exports_external.literal("probe-quota"),
27156
+ id: exports_external.string().min(1),
27157
+ accounts: exports_external.array(exports_external.string().min(1)).min(1).max(32),
27158
+ timeoutMs: exports_external.number().int().positive().max(60000).optional()
27159
+ });
27133
27160
  RequestSchema2 = exports_external.discriminatedUnion("op", [
27134
27161
  GetCredentialsRequestSchema,
27135
27162
  ListStateRequestSchema,
@@ -27139,7 +27166,8 @@ var init_protocol2 = __esm(() => {
27139
27166
  AddAccountRequestSchema,
27140
27167
  RmAccountRequestSchema,
27141
27168
  SetOverrideRequestSchema,
27142
- ListGoogleAccountsRequestSchema
27169
+ ListGoogleAccountsRequestSchema,
27170
+ ProbeQuotaRequestSchema
27143
27171
  ]);
27144
27172
  GetCredentialsDataSchema = exports_external.object({
27145
27173
  account: exports_external.string(),
@@ -27308,6 +27336,16 @@ class AuthBrokerClient {
27308
27336
  });
27309
27337
  return data;
27310
27338
  }
27339
+ async probeQuota(accounts, timeoutMs) {
27340
+ const data = await this.send({
27341
+ v: PROTOCOL_VERSION,
27342
+ id: randomUUID2(),
27343
+ op: "probe-quota",
27344
+ accounts: [...accounts],
27345
+ ...timeoutMs !== undefined ? { timeoutMs } : {}
27346
+ });
27347
+ return data;
27348
+ }
27311
27349
  async setActive(account) {
27312
27350
  const data = await this.send({
27313
27351
  v: PROTOCOL_VERSION,
@@ -27911,14 +27949,14 @@ var init_oauth = __esm(() => {
27911
27949
 
27912
27950
  // src/drive/grants.ts
27913
27951
  function scopeFor(target, action) {
27914
- const writePrefix = action === "write" ? "write:" : "";
27952
+ const actionPrefix = action === "read" ? "" : `${action}:`;
27915
27953
  switch (target.kind) {
27916
27954
  case "all":
27917
- return `doc:gdrive:${writePrefix}**`;
27955
+ return `doc:gdrive:${actionPrefix}**`;
27918
27956
  case "folder":
27919
- return `doc:gdrive:${writePrefix}folder/${target.folder_id}/**`;
27957
+ return `doc:gdrive:${actionPrefix}folder/${target.folder_id}/**`;
27920
27958
  case "doc":
27921
- return `doc:gdrive:${writePrefix}${target.doc_id}`;
27959
+ return `doc:gdrive:${actionPrefix}${target.doc_id}`;
27922
27960
  }
27923
27961
  }
27924
27962
 
@@ -45296,8 +45334,8 @@ var {
45296
45334
  } = import__.default;
45297
45335
 
45298
45336
  // src/build-info.ts
45299
- var VERSION = "0.10.0";
45300
- var COMMIT_SHA = "a0ee1201";
45337
+ var VERSION = "0.11.1";
45338
+ var COMMIT_SHA = "f5d84dfb";
45301
45339
 
45302
45340
  // src/cli/deprecated.ts
45303
45341
  init_source();
@@ -47207,6 +47245,7 @@ Don't wait for a slash command. Don't ask permission. Memory work is table stake
47207
47245
  }
47208
47246
  var DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin";
47209
47247
  var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
47248
+ var DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks";
47210
47249
  var DOCKER_BIN_PATH = "/opt/switchroom/bin";
47211
47250
  var DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml";
47212
47251
  function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchroomConfig, userIdOverride, switchroomConfigPath) {
@@ -47735,6 +47774,16 @@ function buildSettingsHooksBlock(p) {
47735
47774
  }
47736
47775
  ]
47737
47776
  },
47777
+ {
47778
+ matcher: "^mcp__google-workspace__",
47779
+ hooks: [
47780
+ {
47781
+ type: "command",
47782
+ command: wrap("hook:drive-write-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "drive-write-pretool.mjs")}"`),
47783
+ timeout: 5 * 60 + 30
47784
+ }
47785
+ ]
47786
+ },
47738
47787
  {
47739
47788
  hooks: [
47740
47789
  {
@@ -47757,7 +47806,7 @@ function buildSettingsHooksBlock(p) {
47757
47806
  ]
47758
47807
  },
47759
47808
  {
47760
- matcher: ".*",
47809
+ matcher: "^(Edit|MultiEdit|Write|NotebookEdit|Bash|mcp__.*)$",
47761
47810
  hooks: [
47762
47811
  {
47763
47812
  type: "command",
@@ -53214,6 +53263,10 @@ async function ensureHostMountSources(config) {
53214
53263
  if (!existsSync23(auditLogPath)) {
53215
53264
  writeFileSync13(auditLogPath, "", { mode: 420 });
53216
53265
  }
53266
+ const hostdAuditLogPath = join18(home2, ".switchroom", "host-control-audit.log");
53267
+ if (!existsSync23(hostdAuditLogPath)) {
53268
+ writeFileSync13(hostdAuditLogPath, "", { mode: 420 });
53269
+ }
53217
53270
  }
53218
53271
  function detectComposeV2() {
53219
53272
  try {
@@ -64973,6 +65026,8 @@ var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
64973
65026
  var HINDSIGHT_CONSUMER_NAME = "hindsight";
64974
65027
  var HINDSIGHT_DEFAULT_UID = 11000;
64975
65028
  var HINDSIGHT_IMAGE = "ghcr.io/switchroom/switchroom-hindsight:latest";
65029
+ var HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-4-6";
65030
+ var HINDSIGHT_DEFAULT_MCP_STATELESS = true;
64976
65031
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
64977
65032
  function isPortFree(port) {
64978
65033
  return new Promise((resolve28) => {
@@ -65040,7 +65095,11 @@ function startHindsight(ports) {
65040
65095
  "-e",
65041
65096
  `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
65042
65097
  "-e",
65043
- "HINDSIGHT_API_LLM_PROVIDER=claude-code"
65098
+ "HINDSIGHT_API_LLM_PROVIDER=claude-code",
65099
+ "-e",
65100
+ `HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
65101
+ "-e",
65102
+ `HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`
65044
65103
  ];
65045
65104
  const args = [
65046
65105
  "run",
@@ -65058,7 +65117,7 @@ function startHindsight(ports) {
65058
65117
  "-v",
65059
65118
  `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
65060
65119
  "--tmpfs",
65061
- "/run/claude-creds:rw,mode=0700",
65120
+ `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
65062
65121
  ...envArgs,
65063
65122
  HINDSIGHT_IMAGE
65064
65123
  ];
@@ -65093,11 +65152,13 @@ function generateHindsightComposeSnippet() {
65093
65152
  " environment:",
65094
65153
  ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
65095
65154
  " - HINDSIGHT_API_LLM_PROVIDER=claude-code",
65155
+ ` - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
65156
+ ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
65096
65157
  " volumes:",
65097
65158
  " - switchroom-hindsight-data:/home/hindsight/.pg0",
65098
65159
  ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
65099
65160
  " tmpfs:",
65100
- " - /run/claude-creds:rw,mode=0700",
65161
+ ` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
65101
65162
  " restart: unless-stopped",
65102
65163
  "",
65103
65164
  "volumes:",
@@ -72574,10 +72635,109 @@ function registerMigrateCommand(program3) {
72574
72635
  // src/cli/hostd.ts
72575
72636
  init_source();
72576
72637
  init_helpers();
72577
- import { existsSync as existsSync65, mkdirSync as mkdirSync35, readdirSync as readdirSync26, writeFileSync as writeFileSync31, statSync as statSync26, copyFileSync as copyFileSync11 } from "node:fs";
72638
+ import { existsSync as existsSync65, mkdirSync as mkdirSync35, readdirSync as readdirSync26, readFileSync as readFileSync57, writeFileSync as writeFileSync31, statSync as statSync26, copyFileSync as copyFileSync11 } from "node:fs";
72639
+ import { homedir as homedir28 } from "node:os";
72640
+ import { join as join54 } from "node:path";
72641
+ import { spawnSync as spawnSync9 } from "node:child_process";
72642
+
72643
+ // src/host-control/audit-reader.ts
72578
72644
  import { homedir as homedir27 } from "node:os";
72579
72645
  import { join as join53 } from "node:path";
72580
- import { spawnSync as spawnSync9 } from "node:child_process";
72646
+ function defaultAuditLogPath2(home2 = homedir27()) {
72647
+ return join53(home2, ".switchroom", "host-control-audit.log");
72648
+ }
72649
+ function parseAuditLine2(line) {
72650
+ const trimmed = line.trim();
72651
+ if (trimmed.length === 0)
72652
+ return null;
72653
+ let obj;
72654
+ try {
72655
+ obj = JSON.parse(trimmed);
72656
+ } catch {
72657
+ return null;
72658
+ }
72659
+ if (typeof obj !== "object" || obj === null)
72660
+ return null;
72661
+ const o = obj;
72662
+ if (typeof o.ts !== "string" || typeof o.op !== "string")
72663
+ return null;
72664
+ if (typeof o.request_id !== "string" || typeof o.result !== "string")
72665
+ return null;
72666
+ if (typeof o.duration_ms !== "number")
72667
+ return null;
72668
+ const callerRaw = o.caller;
72669
+ let caller;
72670
+ if (callerRaw && callerRaw.kind === "agent" && typeof callerRaw.name === "string") {
72671
+ caller = { kind: "agent", name: callerRaw.name };
72672
+ } else if (callerRaw && callerRaw.kind === "operator") {
72673
+ caller = { kind: "operator" };
72674
+ } else {
72675
+ return null;
72676
+ }
72677
+ const exit_code = o.exit_code === null || typeof o.exit_code === "number" ? o.exit_code : null;
72678
+ const entry = {
72679
+ ts: o.ts,
72680
+ op: o.op,
72681
+ caller,
72682
+ request_id: o.request_id,
72683
+ result: o.result,
72684
+ exit_code,
72685
+ duration_ms: o.duration_ms
72686
+ };
72687
+ if (typeof o.error === "string")
72688
+ entry.error = o.error;
72689
+ return entry;
72690
+ }
72691
+ function filterEntries(entries, filters) {
72692
+ return entries.filter((e) => {
72693
+ if (filters.agent != null) {
72694
+ if (e.caller.kind !== "agent")
72695
+ return false;
72696
+ if (e.caller.name !== filters.agent)
72697
+ return false;
72698
+ }
72699
+ if (filters.op != null && e.op !== filters.op)
72700
+ return false;
72701
+ if (filters.errorOnly) {
72702
+ if (e.result !== "error" && e.result !== "denied")
72703
+ return false;
72704
+ }
72705
+ return true;
72706
+ });
72707
+ }
72708
+ function readAndFilter(raw, filters, limit) {
72709
+ const lines = raw.split(`
72710
+ `);
72711
+ const parsed = [];
72712
+ for (const line of lines) {
72713
+ const e = parseAuditLine2(line);
72714
+ if (e != null)
72715
+ parsed.push(e);
72716
+ }
72717
+ const filtered = filterEntries(parsed, filters);
72718
+ return filtered.slice(-Math.max(1, limit));
72719
+ }
72720
+ function shortCaller(caller) {
72721
+ return caller.kind === "agent" ? caller.name : "operator";
72722
+ }
72723
+ function shortTs(ts) {
72724
+ return ts.replace("T", " ").replace(/\.\d+Z$/, "").slice(0, 19);
72725
+ }
72726
+ function formatForCli(entries) {
72727
+ const out = [];
72728
+ for (const e of entries) {
72729
+ const ts = shortTs(e.ts).padEnd(20);
72730
+ const caller = shortCaller(e.caller).padEnd(15);
72731
+ const op = e.op.padEnd(16);
72732
+ const result = e.result.padEnd(10);
72733
+ const exit = e.exit_code == null ? " -" : String(e.exit_code).padStart(3);
72734
+ const ms = `${e.duration_ms}ms`.padStart(8);
72735
+ out.push(`${ts} ${caller} ${op} ${result} ${exit} ${ms}`);
72736
+ }
72737
+ return out;
72738
+ }
72739
+
72740
+ // src/cli/hostd.ts
72581
72741
  var DEFAULT_IMAGE_TAG = "latest";
72582
72742
  var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
72583
72743
  function renderHostdComposeFile(opts) {
@@ -72660,10 +72820,10 @@ networks:
72660
72820
  `;
72661
72821
  }
72662
72822
  function hostdDir() {
72663
- return join53(homedir27(), ".switchroom", "hostd");
72823
+ return join54(homedir28(), ".switchroom", "hostd");
72664
72824
  }
72665
72825
  function hostdComposePath() {
72666
- return join53(hostdDir(), "docker-compose.yml");
72826
+ return join54(hostdDir(), "docker-compose.yml");
72667
72827
  }
72668
72828
  function backupExistingCompose() {
72669
72829
  const p = hostdComposePath();
@@ -72702,7 +72862,7 @@ async function doInstall(opts, program3) {
72702
72862
  const composePath = hostdComposePath();
72703
72863
  mkdirSync35(dir, { recursive: true });
72704
72864
  const yaml = renderHostdComposeFile({
72705
- hostHome: homedir27(),
72865
+ hostHome: homedir28(),
72706
72866
  imageTag: opts.tag ?? DEFAULT_IMAGE_TAG
72707
72867
  });
72708
72868
  if (opts.dryRun) {
@@ -72770,7 +72930,7 @@ function doStatus() {
72770
72930
  for (const name of readdirSync26(dir)) {
72771
72931
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
72772
72932
  continue;
72773
- const sockPath = join53(dir, name, "sock");
72933
+ const sockPath = join54(dir, name, "sock");
72774
72934
  if (existsSync65(sockPath)) {
72775
72935
  const st = statSync26(sockPath);
72776
72936
  if ((st.mode & 61440) === 49152) {
@@ -72812,6 +72972,48 @@ function registerHostdCommand(program3) {
72812
72972
  }));
72813
72973
  hostd.command("status").description("Show daemon state and bound sockets").action(() => doStatus());
72814
72974
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
72975
+ 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("--path <file>", "Override audit log path (for debugging)").action((opts) => {
72976
+ const logPath = opts.path ?? defaultAuditLogPath2();
72977
+ if (!existsSync65(logPath)) {
72978
+ console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
72979
+ The log is created when hostd handles its first privileged-verb request.`));
72980
+ return;
72981
+ }
72982
+ const raw = readFileSync57(logPath, "utf-8");
72983
+ const limit = Math.max(1, parseInt(opts.tail ?? "50", 10) || 50);
72984
+ const filters = {
72985
+ agent: opts.agent,
72986
+ op: opts.op,
72987
+ errorOnly: !!opts.error
72988
+ };
72989
+ const entries = readAndFilter(raw, filters, limit);
72990
+ if (entries.length === 0) {
72991
+ const parts = [];
72992
+ if (opts.agent)
72993
+ parts.push(`agent=${opts.agent}`);
72994
+ if (opts.op)
72995
+ parts.push(`op=${opts.op}`);
72996
+ if (opts.error)
72997
+ parts.push("errors-only");
72998
+ const desc = parts.length > 0 ? ` matching ${parts.join(", ")}` : "";
72999
+ console.log(source_default.dim(`No hostd audit entries${desc}.`));
73000
+ return;
73001
+ }
73002
+ const header = "ts".padEnd(20) + " " + "caller".padEnd(15) + " " + "op".padEnd(16) + " " + "result".padEnd(10) + " " + "exit".padStart(3) + " " + "dur".padStart(8);
73003
+ console.log(source_default.dim(header));
73004
+ console.log(source_default.dim("\u2500".repeat(header.length)));
73005
+ for (const line of formatForCli(entries)) {
73006
+ if (line.includes(" error ") || line.includes(" denied ")) {
73007
+ console.log(source_default.red(line));
73008
+ } else if (line.includes(" started ")) {
73009
+ console.log(source_default.yellow(line));
73010
+ } else {
73011
+ console.log(line);
73012
+ }
73013
+ }
73014
+ console.log();
73015
+ console.log(source_default.dim(`${entries.length} entr${entries.length === 1 ? "y" : "ies"} shown` + (entries.length === limit ? ` (--tail ${limit})` : "") + ` \u00b7 log: ${logPath}`));
73016
+ });
72815
73017
  }
72816
73018
 
72817
73019
  // src/cli/index.ts
@@ -11286,7 +11286,7 @@ var QuotaConfigSchema = exports_external.object({
11286
11286
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11287
11287
  });
11288
11288
  var HostControlConfigSchema = exports_external.object({
11289
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
11289
+ enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
11290
11290
  });
11291
11291
  var SwitchroomConfigSchema = exports_external.object({
11292
11292
  switchroom: exports_external.object({
@@ -11312,7 +11312,7 @@ var SwitchroomConfigSchema = exports_external.object({
11312
11312
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11313
11313
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11314
11314
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11315
- host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
11315
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11316
11316
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11317
11317
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11318
11318
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -11278,7 +11278,7 @@ var init_schema = __esm(() => {
11278
11278
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11279
11279
  });
11280
11280
  HostControlConfigSchema = exports_external.object({
11281
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
11281
+ enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
11282
11282
  });
11283
11283
  SwitchroomConfigSchema = exports_external.object({
11284
11284
  switchroom: exports_external.object({
@@ -11304,7 +11304,7 @@ var init_schema = __esm(() => {
11304
11304
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11305
11305
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11306
11306
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11307
- host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
11307
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11308
11308
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11309
11309
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11310
11310
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -11278,7 +11278,7 @@ var init_schema = __esm(() => {
11278
11278
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11279
11279
  });
11280
11280
  HostControlConfigSchema = exports_external.object({
11281
- enabled: exports_external.boolean().optional().describe("Opt-in to the host-control daemon. Default: false. " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Since Phase 2 (#1175 PR γ) the gateway's /restart, /new, /reset, " + "and /update apply slash-commands automatically dispatch through " + "hostd when enabled — replacing the in-container " + "`spawnSwitchroomDetached` shellout that requires docker access. " + "Set enabled: true on docker-mode installs to make those verbs work " + "(they otherwise fail because the agent container has no docker " + "binary/socket).")
11281
+ enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3).")
11282
11282
  });
11283
11283
  SwitchroomConfigSchema = exports_external.object({
11284
11284
  switchroom: exports_external.object({
@@ -11304,7 +11304,7 @@ var init_schema = __esm(() => {
11304
11304
  drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
11305
11305
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11306
11306
  quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
11307
- host_control: HostControlConfigSchema.optional().describe("Optional host-control daemon configuration. See RFC C " + "(docs/rfcs/host-control-daemon.md) and the field-level help on " + "`enabled` for the Phase 1 scope."),
11307
+ host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
11308
11308
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11309
11309
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11310
11310
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
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": {
@@ -41,7 +41,7 @@ describe('parseCommandName', () => {
41
41
 
42
42
  describe('ADMIN_COMMAND_NAMES', () => {
43
43
  it('contains the fleet-management admin commands', () => {
44
- const required = ['agents', 'logs', 'restart', 'update', 'reconcile', 'stop', 'agentstart', 'grant', 'dangerous', 'permissions', 'vault']
44
+ const required = ['agents', 'logs', 'restart', 'update', 'reconcile', 'stop', 'agentstart', 'grant', 'dangerous', 'permissions', 'vault', 'audit']
45
45
  for (const cmd of required) {
46
46
  expect(ADMIN_COMMAND_NAMES.has(cmd)).toBe(true)
47
47
  }
@@ -61,6 +61,8 @@ export const ADMIN_COMMAND_NAMES = new Set<string>([
61
61
  // Per-agent ops that read shared fleet state via the switchroom CLI
62
62
  'memory',
63
63
  'topics',
64
+ // Observability of privileged-verb history
65
+ 'audit',
64
66
  ])
65
67
 
66
68
  /**