switchroom 0.10.0 → 0.11.0

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 (46) hide show
  1. package/README.md +5 -4
  2. package/dist/cli/drive-write-pretool.mjs +5418 -0
  3. package/dist/cli/switchroom.js +201 -24
  4. package/package.json +1 -1
  5. package/telegram-plugin/admin-commands/dispatch.test.ts +1 -1
  6. package/telegram-plugin/admin-commands/index.ts +2 -0
  7. package/telegram-plugin/auth-snapshot-format.ts +612 -0
  8. package/telegram-plugin/auto-fallback-fleet.ts +215 -0
  9. package/telegram-plugin/auto-fallback.ts +28 -301
  10. package/telegram-plugin/dist/gateway/gateway.js +4407 -2252
  11. package/telegram-plugin/fleet-fallback-gate.ts +105 -0
  12. package/telegram-plugin/gateway/approval-callback.test.ts +104 -0
  13. package/telegram-plugin/gateway/approval-callback.ts +31 -3
  14. package/telegram-plugin/gateway/auth-command.ts +121 -10
  15. package/telegram-plugin/gateway/auth-status-adapter.ts +101 -0
  16. package/telegram-plugin/gateway/boot-card.ts +1 -1
  17. package/telegram-plugin/gateway/boot-probes.ts +6 -9
  18. package/telegram-plugin/gateway/diff-preview-card.test.ts +192 -0
  19. package/telegram-plugin/gateway/diff-preview-card.ts +170 -0
  20. package/telegram-plugin/gateway/drive-write-approval.test.ts +312 -0
  21. package/telegram-plugin/gateway/drive-write-approval.ts +243 -0
  22. package/telegram-plugin/gateway/folder-picker-handler.test.ts +314 -0
  23. package/telegram-plugin/gateway/folder-picker-handler.ts +348 -0
  24. package/telegram-plugin/gateway/gateway.ts +876 -173
  25. package/telegram-plugin/gateway/hostd-dispatch.ts +127 -0
  26. package/telegram-plugin/gateway/ipc-protocol.ts +83 -2
  27. package/telegram-plugin/gateway/ipc-server.ts +69 -0
  28. package/telegram-plugin/hooks/sandbox-hint-posttool.mjs +103 -12
  29. package/telegram-plugin/model-unavailable.ts +28 -12
  30. package/telegram-plugin/silence-poke.ts +153 -1
  31. package/telegram-plugin/tests/auth-command-format2.test.ts +156 -0
  32. package/telegram-plugin/tests/auth-snapshot-format.test.ts +429 -0
  33. package/telegram-plugin/tests/auth-status-adapter.test.ts +129 -0
  34. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +211 -0
  35. package/telegram-plugin/tests/auto-fallback.test.ts +60 -358
  36. package/telegram-plugin/tests/boot-probes.test.ts +16 -18
  37. package/telegram-plugin/tests/fleet-fallback-gate.test.ts +197 -0
  38. package/telegram-plugin/tests/model-unavailable.test.ts +30 -5
  39. package/telegram-plugin/tests/sandbox-hint-posttool.test.ts +212 -2
  40. package/telegram-plugin/tests/silence-poke.test.ts +237 -0
  41. package/telegram-plugin/tests/turn-flush-safety.test.ts +112 -0
  42. package/telegram-plugin/turn-flush-safety.ts +55 -1
  43. package/telegram-plugin/uat/SETUP.md +16 -12
  44. package/telegram-plugin/auto-fallback-dispatcher.ts +0 -68
  45. package/telegram-plugin/tests/auto-fallback-dispatcher.e2e.test.ts +0 -183
  46. package/telegram-plugin/tests/hostd-dispatch.test.ts +0 -129
@@ -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(`
@@ -25660,17 +25661,22 @@ function checkHindsightConsumer(config, opts) {
25660
25661
  ` + "then run `switchroom apply` to bind the per-consumer socket."
25661
25662
  };
25662
25663
  }
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) {
25664
+ const probe2 = opts?.socketProbe ?? probeAuthBrokerSocket;
25665
+ const state = probe2(entry.name);
25666
+ if (state === "unreachable") {
25667
+ return {
25668
+ name: "hindsight consumer",
25669
+ status: "warn",
25670
+ detail: `auth.consumers[hindsight] -> ${entry.account} (uid ${entry.uid ?? 0}); ` + `couldn't query auth-broker container (not running / docker unavailable)`,
25671
+ fix: "Check `auth-broker: service health` row above; if the broker is " + "down, `switchroom apply` will bring it back and bind the socket."
25672
+ };
25673
+ }
25674
+ if (state === "missing") {
25669
25675
  return {
25670
25676
  name: "hindsight consumer",
25671
25677
  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."
25678
+ 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`,
25679
+ fix: "Run `switchroom apply` to refresh compose and rebind per-consumer sockets."
25674
25680
  };
25675
25681
  }
25676
25682
  return {
@@ -25679,6 +25685,17 @@ function checkHindsightConsumer(config, opts) {
25679
25685
  detail: `auth.consumers[hindsight] -> ${entry.account} (uid ${entry.uid ?? 0})`
25680
25686
  };
25681
25687
  }
25688
+ function probeAuthBrokerSocket(consumerName) {
25689
+ const containerPath = `/run/switchroom/auth-broker/${consumerName}/sock`;
25690
+ const r = spawnSync3("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
25691
+ if (r.error || r.status === null)
25692
+ return "unreachable";
25693
+ if (r.status === 0)
25694
+ return "present";
25695
+ if (r.status >= 125)
25696
+ return "unreachable";
25697
+ return "missing";
25698
+ }
25682
25699
  async function checkHindsight(config) {
25683
25700
  const memoryBackend = config.memory?.backend;
25684
25701
  if (memoryBackend !== "hindsight") {
@@ -27911,14 +27928,14 @@ var init_oauth = __esm(() => {
27911
27928
 
27912
27929
  // src/drive/grants.ts
27913
27930
  function scopeFor(target, action) {
27914
- const writePrefix = action === "write" ? "write:" : "";
27931
+ const actionPrefix = action === "read" ? "" : `${action}:`;
27915
27932
  switch (target.kind) {
27916
27933
  case "all":
27917
- return `doc:gdrive:${writePrefix}**`;
27934
+ return `doc:gdrive:${actionPrefix}**`;
27918
27935
  case "folder":
27919
- return `doc:gdrive:${writePrefix}folder/${target.folder_id}/**`;
27936
+ return `doc:gdrive:${actionPrefix}folder/${target.folder_id}/**`;
27920
27937
  case "doc":
27921
- return `doc:gdrive:${writePrefix}${target.doc_id}`;
27938
+ return `doc:gdrive:${actionPrefix}${target.doc_id}`;
27922
27939
  }
27923
27940
  }
27924
27941
 
@@ -45296,8 +45313,8 @@ var {
45296
45313
  } = import__.default;
45297
45314
 
45298
45315
  // src/build-info.ts
45299
- var VERSION = "0.10.0";
45300
- var COMMIT_SHA = "a0ee1201";
45316
+ var VERSION = "0.11.0";
45317
+ var COMMIT_SHA = "abff20c7";
45301
45318
 
45302
45319
  // src/cli/deprecated.ts
45303
45320
  init_source();
@@ -47207,6 +47224,7 @@ Don't wait for a slash command. Don't ask permission. Memory work is table stake
47207
47224
  }
47208
47225
  var DOCKER_TELEGRAM_PLUGIN_PATH = "/opt/switchroom/telegram-plugin";
47209
47226
  var DOCKER_HOOKS_PATH = `${DOCKER_TELEGRAM_PLUGIN_PATH}/hooks`;
47227
+ var DOCKER_BUNDLED_HOOKS_PATH = "/opt/switchroom/hooks";
47210
47228
  var DOCKER_BIN_PATH = "/opt/switchroom/bin";
47211
47229
  var DOCKER_CONFIG_PATH = "/state/config/switchroom.yaml";
47212
47230
  function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchroomConfig, userIdOverride, switchroomConfigPath) {
@@ -47735,6 +47753,16 @@ function buildSettingsHooksBlock(p) {
47735
47753
  }
47736
47754
  ]
47737
47755
  },
47756
+ {
47757
+ matcher: "^mcp__google-workspace__",
47758
+ hooks: [
47759
+ {
47760
+ type: "command",
47761
+ command: wrap("hook:drive-write-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "drive-write-pretool.mjs")}"`),
47762
+ timeout: 5 * 60 + 30
47763
+ }
47764
+ ]
47765
+ },
47738
47766
  {
47739
47767
  hooks: [
47740
47768
  {
@@ -47757,7 +47785,7 @@ function buildSettingsHooksBlock(p) {
47757
47785
  ]
47758
47786
  },
47759
47787
  {
47760
- matcher: ".*",
47788
+ matcher: "^(Edit|MultiEdit|Write|NotebookEdit|Bash|mcp__.*)$",
47761
47789
  hooks: [
47762
47790
  {
47763
47791
  type: "command",
@@ -64973,6 +65001,8 @@ var HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE = 1000;
64973
65001
  var HINDSIGHT_CONSUMER_NAME = "hindsight";
64974
65002
  var HINDSIGHT_DEFAULT_UID = 11000;
64975
65003
  var HINDSIGHT_IMAGE = "ghcr.io/switchroom/switchroom-hindsight:latest";
65004
+ var HINDSIGHT_DEFAULT_MODEL = "claude-sonnet-4-6";
65005
+ var HINDSIGHT_DEFAULT_MCP_STATELESS = true;
64976
65006
  var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
64977
65007
  function isPortFree(port) {
64978
65008
  return new Promise((resolve28) => {
@@ -65040,7 +65070,11 @@ function startHindsight(ports) {
65040
65070
  "-e",
65041
65071
  `HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
65042
65072
  "-e",
65043
- "HINDSIGHT_API_LLM_PROVIDER=claude-code"
65073
+ "HINDSIGHT_API_LLM_PROVIDER=claude-code",
65074
+ "-e",
65075
+ `HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
65076
+ "-e",
65077
+ `HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`
65044
65078
  ];
65045
65079
  const args = [
65046
65080
  "run",
@@ -65058,7 +65092,7 @@ function startHindsight(ports) {
65058
65092
  "-v",
65059
65093
  `${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
65060
65094
  "--tmpfs",
65061
- "/run/claude-creds:rw,mode=0700",
65095
+ `/run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
65062
65096
  ...envArgs,
65063
65097
  HINDSIGHT_IMAGE
65064
65098
  ];
@@ -65093,11 +65127,13 @@ function generateHindsightComposeSnippet() {
65093
65127
  " environment:",
65094
65128
  ` - HINDSIGHT_API_MAX_OBSERVATIONS_PER_SCOPE=${HINDSIGHT_DEFAULT_MAX_OBSERVATIONS_PER_SCOPE}`,
65095
65129
  " - HINDSIGHT_API_LLM_PROVIDER=claude-code",
65130
+ ` - HINDSIGHT_API_LLM_MODEL=${HINDSIGHT_DEFAULT_MODEL}`,
65131
+ ` - HINDSIGHT_API_MCP_STATELESS=${HINDSIGHT_DEFAULT_MCP_STATELESS}`,
65096
65132
  " volumes:",
65097
65133
  " - switchroom-hindsight-data:/home/hindsight/.pg0",
65098
65134
  ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
65099
65135
  " tmpfs:",
65100
- " - /run/claude-creds:rw,mode=0700",
65136
+ ` - /run/claude-creds:rw,mode=0700,uid=${HINDSIGHT_DEFAULT_UID},gid=${HINDSIGHT_DEFAULT_UID}`,
65101
65137
  " restart: unless-stopped",
65102
65138
  "",
65103
65139
  "volumes:",
@@ -72574,10 +72610,109 @@ function registerMigrateCommand(program3) {
72574
72610
  // src/cli/hostd.ts
72575
72611
  init_source();
72576
72612
  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";
72613
+ 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";
72614
+ import { homedir as homedir28 } from "node:os";
72615
+ import { join as join54 } from "node:path";
72616
+ import { spawnSync as spawnSync9 } from "node:child_process";
72617
+
72618
+ // src/host-control/audit-reader.ts
72578
72619
  import { homedir as homedir27 } from "node:os";
72579
72620
  import { join as join53 } from "node:path";
72580
- import { spawnSync as spawnSync9 } from "node:child_process";
72621
+ function defaultAuditLogPath2(home2 = homedir27()) {
72622
+ return join53(home2, ".switchroom", "host-control-audit.log");
72623
+ }
72624
+ function parseAuditLine2(line) {
72625
+ const trimmed = line.trim();
72626
+ if (trimmed.length === 0)
72627
+ return null;
72628
+ let obj;
72629
+ try {
72630
+ obj = JSON.parse(trimmed);
72631
+ } catch {
72632
+ return null;
72633
+ }
72634
+ if (typeof obj !== "object" || obj === null)
72635
+ return null;
72636
+ const o = obj;
72637
+ if (typeof o.ts !== "string" || typeof o.op !== "string")
72638
+ return null;
72639
+ if (typeof o.request_id !== "string" || typeof o.result !== "string")
72640
+ return null;
72641
+ if (typeof o.duration_ms !== "number")
72642
+ return null;
72643
+ const callerRaw = o.caller;
72644
+ let caller;
72645
+ if (callerRaw && callerRaw.kind === "agent" && typeof callerRaw.name === "string") {
72646
+ caller = { kind: "agent", name: callerRaw.name };
72647
+ } else if (callerRaw && callerRaw.kind === "operator") {
72648
+ caller = { kind: "operator" };
72649
+ } else {
72650
+ return null;
72651
+ }
72652
+ const exit_code = o.exit_code === null || typeof o.exit_code === "number" ? o.exit_code : null;
72653
+ const entry = {
72654
+ ts: o.ts,
72655
+ op: o.op,
72656
+ caller,
72657
+ request_id: o.request_id,
72658
+ result: o.result,
72659
+ exit_code,
72660
+ duration_ms: o.duration_ms
72661
+ };
72662
+ if (typeof o.error === "string")
72663
+ entry.error = o.error;
72664
+ return entry;
72665
+ }
72666
+ function filterEntries(entries, filters) {
72667
+ return entries.filter((e) => {
72668
+ if (filters.agent != null) {
72669
+ if (e.caller.kind !== "agent")
72670
+ return false;
72671
+ if (e.caller.name !== filters.agent)
72672
+ return false;
72673
+ }
72674
+ if (filters.op != null && e.op !== filters.op)
72675
+ return false;
72676
+ if (filters.errorOnly) {
72677
+ if (e.result !== "error" && e.result !== "denied")
72678
+ return false;
72679
+ }
72680
+ return true;
72681
+ });
72682
+ }
72683
+ function readAndFilter(raw, filters, limit) {
72684
+ const lines = raw.split(`
72685
+ `);
72686
+ const parsed = [];
72687
+ for (const line of lines) {
72688
+ const e = parseAuditLine2(line);
72689
+ if (e != null)
72690
+ parsed.push(e);
72691
+ }
72692
+ const filtered = filterEntries(parsed, filters);
72693
+ return filtered.slice(-Math.max(1, limit));
72694
+ }
72695
+ function shortCaller(caller) {
72696
+ return caller.kind === "agent" ? caller.name : "operator";
72697
+ }
72698
+ function shortTs(ts) {
72699
+ return ts.replace("T", " ").replace(/\.\d+Z$/, "").slice(0, 19);
72700
+ }
72701
+ function formatForCli(entries) {
72702
+ const out = [];
72703
+ for (const e of entries) {
72704
+ const ts = shortTs(e.ts).padEnd(20);
72705
+ const caller = shortCaller(e.caller).padEnd(15);
72706
+ const op = e.op.padEnd(16);
72707
+ const result = e.result.padEnd(10);
72708
+ const exit = e.exit_code == null ? " -" : String(e.exit_code).padStart(3);
72709
+ const ms = `${e.duration_ms}ms`.padStart(8);
72710
+ out.push(`${ts} ${caller} ${op} ${result} ${exit} ${ms}`);
72711
+ }
72712
+ return out;
72713
+ }
72714
+
72715
+ // src/cli/hostd.ts
72581
72716
  var DEFAULT_IMAGE_TAG = "latest";
72582
72717
  var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
72583
72718
  function renderHostdComposeFile(opts) {
@@ -72660,10 +72795,10 @@ networks:
72660
72795
  `;
72661
72796
  }
72662
72797
  function hostdDir() {
72663
- return join53(homedir27(), ".switchroom", "hostd");
72798
+ return join54(homedir28(), ".switchroom", "hostd");
72664
72799
  }
72665
72800
  function hostdComposePath() {
72666
- return join53(hostdDir(), "docker-compose.yml");
72801
+ return join54(hostdDir(), "docker-compose.yml");
72667
72802
  }
72668
72803
  function backupExistingCompose() {
72669
72804
  const p = hostdComposePath();
@@ -72702,7 +72837,7 @@ async function doInstall(opts, program3) {
72702
72837
  const composePath = hostdComposePath();
72703
72838
  mkdirSync35(dir, { recursive: true });
72704
72839
  const yaml = renderHostdComposeFile({
72705
- hostHome: homedir27(),
72840
+ hostHome: homedir28(),
72706
72841
  imageTag: opts.tag ?? DEFAULT_IMAGE_TAG
72707
72842
  });
72708
72843
  if (opts.dryRun) {
@@ -72770,7 +72905,7 @@ function doStatus() {
72770
72905
  for (const name of readdirSync26(dir)) {
72771
72906
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
72772
72907
  continue;
72773
- const sockPath = join53(dir, name, "sock");
72908
+ const sockPath = join54(dir, name, "sock");
72774
72909
  if (existsSync65(sockPath)) {
72775
72910
  const st = statSync26(sockPath);
72776
72911
  if ((st.mode & 61440) === 49152) {
@@ -72812,6 +72947,48 @@ function registerHostdCommand(program3) {
72812
72947
  }));
72813
72948
  hostd.command("status").description("Show daemon state and bound sockets").action(() => doStatus());
72814
72949
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
72950
+ 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) => {
72951
+ const logPath = opts.path ?? defaultAuditLogPath2();
72952
+ if (!existsSync65(logPath)) {
72953
+ console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
72954
+ The log is created when hostd handles its first privileged-verb request.`));
72955
+ return;
72956
+ }
72957
+ const raw = readFileSync57(logPath, "utf-8");
72958
+ const limit = Math.max(1, parseInt(opts.tail ?? "50", 10) || 50);
72959
+ const filters = {
72960
+ agent: opts.agent,
72961
+ op: opts.op,
72962
+ errorOnly: !!opts.error
72963
+ };
72964
+ const entries = readAndFilter(raw, filters, limit);
72965
+ if (entries.length === 0) {
72966
+ const parts = [];
72967
+ if (opts.agent)
72968
+ parts.push(`agent=${opts.agent}`);
72969
+ if (opts.op)
72970
+ parts.push(`op=${opts.op}`);
72971
+ if (opts.error)
72972
+ parts.push("errors-only");
72973
+ const desc = parts.length > 0 ? ` matching ${parts.join(", ")}` : "";
72974
+ console.log(source_default.dim(`No hostd audit entries${desc}.`));
72975
+ return;
72976
+ }
72977
+ const header = "ts".padEnd(20) + " " + "caller".padEnd(15) + " " + "op".padEnd(16) + " " + "result".padEnd(10) + " " + "exit".padStart(3) + " " + "dur".padStart(8);
72978
+ console.log(source_default.dim(header));
72979
+ console.log(source_default.dim("\u2500".repeat(header.length)));
72980
+ for (const line of formatForCli(entries)) {
72981
+ if (line.includes(" error ") || line.includes(" denied ")) {
72982
+ console.log(source_default.red(line));
72983
+ } else if (line.includes(" started ")) {
72984
+ console.log(source_default.yellow(line));
72985
+ } else {
72986
+ console.log(line);
72987
+ }
72988
+ }
72989
+ console.log();
72990
+ console.log(source_default.dim(`${entries.length} entr${entries.length === 1 ? "y" : "ies"} shown` + (entries.length === limit ? ` (--tail ${limit})` : "") + ` \u00b7 log: ${logPath}`));
72991
+ });
72815
72992
  }
72816
72993
 
72817
72994
  // src/cli/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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
  /**