switchroom 0.17.5 → 0.17.10

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 (158) hide show
  1. package/dist/agent-scheduler/index.js +39 -5
  2. package/dist/auth-broker/index.js +386 -208
  3. package/dist/cli/notion-write-pretool.mjs +36 -3
  4. package/dist/cli/switchroom.js +1185 -585
  5. package/dist/host-control/main.js +149 -15
  6. package/dist/vault/approvals/kernel-server.js +141 -56
  7. package/dist/vault/broker/server.js +143 -58
  8. package/package.json +1 -1
  9. package/profiles/_base/start.sh.hbs +50 -6
  10. package/profiles/default/CLAUDE.md +116 -0
  11. package/skills/mental-model-curator/SKILL.md +162 -0
  12. package/telegram-plugin/auth-snapshot-format.ts +50 -2
  13. package/telegram-plugin/bridge/bridge.ts +80 -1
  14. package/telegram-plugin/bridge/ipc-client.ts +19 -0
  15. package/telegram-plugin/bridge/permission-ledger.ts +61 -0
  16. package/telegram-plugin/consolidation-legibility.ts +279 -0
  17. package/telegram-plugin/dist/bridge/bridge.js +85 -1
  18. package/telegram-plugin/dist/gateway/gateway.js +2597 -645
  19. package/telegram-plugin/dist/server.js +86 -2
  20. package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
  21. package/telegram-plugin/gateway/activity-card-store.ts +293 -0
  22. package/telegram-plugin/gateway/auth-command.ts +1 -1
  23. package/telegram-plugin/gateway/gateway.ts +1414 -119
  24. package/telegram-plugin/gateway/inbound-spool.ts +22 -0
  25. package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
  26. package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
  27. package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
  28. package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
  29. package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
  30. package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
  31. package/telegram-plugin/gateway/permission-rearm.ts +115 -0
  32. package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
  33. package/telegram-plugin/memory-legibility.ts +217 -0
  34. package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  35. package/telegram-plugin/scoped-approval.ts +59 -0
  36. package/telegram-plugin/silent-end.ts +78 -0
  37. package/telegram-plugin/subagent-watcher.ts +60 -6
  38. package/telegram-plugin/tests/activity-card-store.test.ts +436 -0
  39. package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
  40. package/telegram-plugin/tests/auth-snapshot-format.test.ts +75 -0
  41. package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
  42. package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
  43. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +33 -9
  44. package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
  45. package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
  46. package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
  47. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
  48. package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
  49. package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
  50. package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
  51. package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
  52. package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
  53. package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
  54. package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
  55. package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
  56. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
  57. package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
  58. package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
  59. package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
  60. package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
  61. package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
  62. package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
  63. package/telegram-plugin/tests/worktree-watch-cwds.test.ts +103 -0
  64. package/telegram-plugin/uat/assertions.ts +88 -4
  65. package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
  66. package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
  67. package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
  68. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
  69. package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
  70. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
  71. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
  72. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
  73. package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
  74. package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
  75. package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
  76. package/telegram-plugin/worktree-watch-cwds.ts +60 -0
  77. package/vendor/hindsight-memory/hooks/hooks.json +9 -0
  78. package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
  79. package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
  80. package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
  81. package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
  82. package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
  83. package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
  84. package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
  85. package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
  86. package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
  87. package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
  88. package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
  89. package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
  90. package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
  91. package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
  92. package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
  93. package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
  94. package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
  95. package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
  96. package/vendor/hindsight-memory/scripts/lib/config.py +37 -0
  97. package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
  98. package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
  99. package/vendor/hindsight-memory/scripts/recall.py +153 -4
  100. package/vendor/hindsight-memory/scripts/retain.py +17 -0
  101. package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
  102. package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
  103. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
  104. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
  105. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
  106. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
  107. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
  108. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
  109. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
  110. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
  111. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
  112. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
  113. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
  114. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
  115. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
  116. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
  117. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
  118. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
  119. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
  120. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
  121. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
  122. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
  123. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
  124. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
  125. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
  126. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
  127. package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
  128. package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
  129. package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
  130. package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
  131. package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
  132. package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
  133. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
  134. package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  135. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
  136. package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
  137. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
  138. package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
  139. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
  140. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
  141. package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
  142. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
  143. package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
  144. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  145. package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
  146. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
  147. package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
  148. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
  149. package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
  150. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  151. package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
  152. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
  153. package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
  154. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
  155. package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
  156. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
  157. package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
  158. package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
@@ -1,5 +1,34 @@
1
1
  import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
2
4
  var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ function __accessProp(key) {
8
+ return this[key];
9
+ }
10
+ var __toESMCache_node;
11
+ var __toESMCache_esm;
12
+ var __toESM = (mod, isNodeMode, target) => {
13
+ var canCache = mod != null && typeof mod === "object";
14
+ if (canCache) {
15
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
16
+ var cached = cache.get(mod);
17
+ if (cached)
18
+ return cached;
19
+ }
20
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
21
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
22
+ for (let key of __getOwnPropNames(mod))
23
+ if (!__hasOwnProp.call(to, key))
24
+ __defProp(to, key, {
25
+ get: __accessProp.bind(mod, key),
26
+ enumerable: true
27
+ });
28
+ if (canCache)
29
+ cache.set(mod, to);
30
+ return to;
31
+ };
3
32
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
33
  var __returnValue = (v) => v;
5
34
  function __exportSetter(name, newValue) {
@@ -14,6 +43,7 @@ var __export = (target, all) => {
14
43
  set: __exportSetter.bind(all, name)
15
44
  });
16
45
  };
46
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
18
48
 
19
49
  // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/nodes/identity.js
@@ -6931,60 +6961,99 @@ var require_public_api = __commonJS((exports) => {
6931
6961
  exports.stringify = stringify;
6932
6962
  });
6933
6963
 
6964
+ // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/index.js
6965
+ var require_dist = __commonJS((exports) => {
6966
+ var composer = require_composer();
6967
+ var Document = require_Document();
6968
+ var Schema = require_Schema();
6969
+ var errors = require_errors();
6970
+ var Alias = require_Alias();
6971
+ var identity = require_identity();
6972
+ var Pair = require_Pair();
6973
+ var Scalar = require_Scalar();
6974
+ var YAMLMap = require_YAMLMap();
6975
+ var YAMLSeq = require_YAMLSeq();
6976
+ var cst = require_cst();
6977
+ var lexer = require_lexer();
6978
+ var lineCounter = require_line_counter();
6979
+ var parser = require_parser();
6980
+ var publicApi = require_public_api();
6981
+ var visit = require_visit();
6982
+ exports.Composer = composer.Composer;
6983
+ exports.Document = Document.Document;
6984
+ exports.Schema = Schema.Schema;
6985
+ exports.YAMLError = errors.YAMLError;
6986
+ exports.YAMLParseError = errors.YAMLParseError;
6987
+ exports.YAMLWarning = errors.YAMLWarning;
6988
+ exports.Alias = Alias.Alias;
6989
+ exports.isAlias = identity.isAlias;
6990
+ exports.isCollection = identity.isCollection;
6991
+ exports.isDocument = identity.isDocument;
6992
+ exports.isMap = identity.isMap;
6993
+ exports.isNode = identity.isNode;
6994
+ exports.isPair = identity.isPair;
6995
+ exports.isScalar = identity.isScalar;
6996
+ exports.isSeq = identity.isSeq;
6997
+ exports.Pair = Pair.Pair;
6998
+ exports.Scalar = Scalar.Scalar;
6999
+ exports.YAMLMap = YAMLMap.YAMLMap;
7000
+ exports.YAMLSeq = YAMLSeq.YAMLSeq;
7001
+ exports.CST = cst;
7002
+ exports.Lexer = lexer.Lexer;
7003
+ exports.LineCounter = lineCounter.LineCounter;
7004
+ exports.Parser = parser.Parser;
7005
+ exports.parse = publicApi.parse;
7006
+ exports.parseAllDocuments = publicApi.parseAllDocuments;
7007
+ exports.parseDocument = publicApi.parseDocument;
7008
+ exports.stringify = publicApi.stringify;
7009
+ exports.visit = visit.visit;
7010
+ exports.visitAsync = visit.visitAsync;
7011
+ });
7012
+
7013
+ // src/util/atomic.ts
7014
+ import { randomBytes } from "node:crypto";
7015
+ import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
7016
+ function atomicWriteFileSync(destPath, contents, mode = 384) {
7017
+ const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
7018
+ const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
7019
+ let fd = null;
7020
+ try {
7021
+ fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
7022
+ writeSync(fd, buf, 0, buf.length, 0);
7023
+ fsyncSync(fd);
7024
+ closeSync(fd);
7025
+ fd = null;
7026
+ renameSync(tmp, destPath);
7027
+ } catch (err) {
7028
+ if (fd !== null) {
7029
+ try {
7030
+ closeSync(fd);
7031
+ } catch {}
7032
+ }
7033
+ try {
7034
+ rmSync(tmp, { force: true });
7035
+ } catch {}
7036
+ throw err;
7037
+ }
7038
+ }
7039
+ function atomicWriteJsonSync(destPath, value, mode = 384) {
7040
+ atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
7041
+ `, mode);
7042
+ }
7043
+ var TMP_OPEN_FLAGS;
7044
+ var init_atomic = __esm(() => {
7045
+ TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
7046
+ });
7047
+
6934
7048
  // src/auth/broker/index.ts
6935
7049
  import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
6936
7050
 
6937
7051
  // src/config/loader.ts
7052
+ var import_yaml2 = __toESM(require_dist(), 1);
6938
7053
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
6939
7054
  import { homedir } from "node:os";
6940
7055
  import { resolve as resolve3 } from "node:path";
6941
7056
 
6942
- // node_modules/.bun/yaml@2.8.3/node_modules/yaml/dist/index.js
6943
- var composer = require_composer();
6944
- var Document = require_Document();
6945
- var Schema = require_Schema();
6946
- var errors = require_errors();
6947
- var Alias = require_Alias();
6948
- var identity = require_identity();
6949
- var Pair = require_Pair();
6950
- var Scalar = require_Scalar();
6951
- var YAMLMap = require_YAMLMap();
6952
- var YAMLSeq = require_YAMLSeq();
6953
- var cst = require_cst();
6954
- var lexer = require_lexer();
6955
- var lineCounter = require_line_counter();
6956
- var parser = require_parser();
6957
- var publicApi = require_public_api();
6958
- var visit = require_visit();
6959
- var $Composer = composer.Composer;
6960
- var $Document = Document.Document;
6961
- var $Schema = Schema.Schema;
6962
- var $YAMLError = errors.YAMLError;
6963
- var $YAMLParseError = errors.YAMLParseError;
6964
- var $YAMLWarning = errors.YAMLWarning;
6965
- var $Alias = Alias.Alias;
6966
- var $isAlias = identity.isAlias;
6967
- var $isCollection = identity.isCollection;
6968
- var $isDocument = identity.isDocument;
6969
- var $isMap = identity.isMap;
6970
- var $isNode = identity.isNode;
6971
- var $isPair = identity.isPair;
6972
- var $isScalar = identity.isScalar;
6973
- var $isSeq = identity.isSeq;
6974
- var $Pair = Pair.Pair;
6975
- var $Scalar = Scalar.Scalar;
6976
- var $YAMLMap = YAMLMap.YAMLMap;
6977
- var $YAMLSeq = YAMLSeq.YAMLSeq;
6978
- var $Lexer = lexer.Lexer;
6979
- var $LineCounter = lineCounter.LineCounter;
6980
- var $Parser = parser.Parser;
6981
- var $parse = publicApi.parse;
6982
- var $parseAllDocuments = publicApi.parseAllDocuments;
6983
- var $parseDocument = publicApi.parseDocument;
6984
- var $stringify = publicApi.stringify;
6985
- var $visit = visit.visit;
6986
- var $visitAsync = visit.visitAsync;
6987
-
6988
7057
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
6989
7058
  var exports_external = {};
6990
7059
  __export(exports_external, {
@@ -11070,8 +11139,35 @@ var AgentMemorySchema = exports_external.object({
11070
11139
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11071
11140
  file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11072
11141
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11073
- bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11074
- retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
11142
+ bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize " + "results. NOTE: this is an alias for the Hindsight engine's " + "`reflect_mission` field (verified live: switchroom's bank_mission " + "lands in `config.reflect_mission`). Prefer `reflect_mission` going " + "forward; `bank_mission` is retained for back-compat. If both are " + "set, `reflect_mission` wins. Cascade: override."),
11143
+ reflect_mission: exports_external.string().optional().describe("Mission/context steering Hindsight Reflect operations (the bank's " + "'who am I / what matters' framing applied during recall). The " + "engine-accurate name for what `bank_mission` sets. Cascade: override."),
11144
+ retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain. Cascade: override."),
11145
+ mental_models: exports_external.array(exports_external.object({
11146
+ name: exports_external.string().min(1).describe("Stable model name (identity key for idempotent ensure). Two " + "declarations with the same name in one agent are rejected."),
11147
+ source_query: exports_external.string().min(1).max(2000).describe("The reflection query the model answers, semantically " + "refreshed from the bank's content. Capped at 2000 chars — a " + "standing reflection query, not a document; the ceiling also " + "bounds what an agent-proposed model can smuggle past the " + "operator approval card."),
11148
+ refresh_after_consolidation: exports_external.boolean().optional().describe("Refresh this model after each consolidation. Defaults OFF — " + "refresh adds bounded background model-spend + timeout risk " + "(RFC Phase 5), so it is opt-in per model."),
11149
+ max_tokens: exports_external.number().int().positive().max(8192).optional().describe("Cap on the synthesized model's token size. Upper-bounded at " + "8192 — a mental model is a standing summary, not a corpus; the " + "ceiling also caps what an agent-proposed model can request.")
11150
+ })).superRefine((models, ctx) => {
11151
+ const seen = new Set;
11152
+ for (let i = 0;i < models.length; i++) {
11153
+ const key = models[i].name;
11154
+ if (seen.has(key)) {
11155
+ ctx.addIssue({
11156
+ code: exports_external.ZodIssueCode.custom,
11157
+ path: [i, "name"],
11158
+ message: `duplicate mental_models name "${key}" — model names must be ` + `unique within an agent (they are the idempotent-ensure key)`
11159
+ });
11160
+ }
11161
+ seen.add(key);
11162
+ }
11163
+ }).optional().describe("Operator-declared, per-specialist Hindsight mental models (RFC " + "Phase 5). Named, opt-in curated reflections this agent's bank should " + "carry — e.g. a coach's 'training-plan-state' or a lawyer's " + "'open-matters'. Ensured idempotently at scaffold/reconcile: NOTHING " + "is created unless declared here (zero declarations = zero models, " + "matching post-#2447 behaviour), and no fixed identity model is " + "reintroduced — 'who the user is' stays owned by dedicated profile " + "banks (users.*.profile_bank), never a per-agent model. Per-agent " + "ONLY: intentionally not accepted at the defaults/profile tier, so a " + "model can never be fleet-seeded — each specialist opts in on its own " + "(the invariant-clean inverse of the retired blind auto-seeding)."),
11164
+ observations_mission: exports_external.string().optional().describe("Steers what the observation-consolidation LLM synthesises from raw " + "facts (the higher-order 'what patterns matter' lens). Cascade: override."),
11165
+ disposition: exports_external.object({
11166
+ skepticism: exports_external.number().int().min(1).max(5).optional().describe("How much the bank doubts unverified claims (1-5; engine default 3)."),
11167
+ literalism: exports_external.number().int().min(1).max(5).optional().describe("How literally the bank reads statements vs inferring intent (1-5; engine default 3)."),
11168
+ empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
11169
+ }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis — a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
11170
+ directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true — Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never …", "from now on …", ' + `"stop doing …", a stated preference, "that's wrong, it's …") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex — the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
11075
11171
  recall: exports_external.object({
11076
11172
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
11077
11173
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
@@ -11336,6 +11432,7 @@ var profileFields = {
11336
11432
  auto_recall: exports_external.boolean().optional(),
11337
11433
  file: exports_external.boolean().optional(),
11338
11434
  isolation: exports_external.enum(["default", "strict"]).optional(),
11435
+ directive_capture_nudge: exports_external.boolean().optional(),
11339
11436
  recall: exports_external.object({
11340
11437
  max_memories: exports_external.number().int().min(0).optional(),
11341
11438
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
@@ -11591,7 +11688,7 @@ var SwitchroomConfigSchema = exports_external.object({
11591
11688
  name: exports_external.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/, {
11592
11689
  message: "Consumer name must be a path-safe slug (letters, digits, underscore, hyphen)"
11593
11690
  }).describe("Socket-path identity; binds at /run/switchroom/auth-broker/<name>/sock"),
11594
- account: exports_external.string().min(1).describe("Pinned account label for this consumer. `get-credentials` returns " + "this account's credentials; `mark-exhausted` from this consumer " + "only affects this account."),
11691
+ account: exports_external.string().min(1).optional().describe("Optional pinned account label for this consumer. When set, " + "`get-credentials` serves this account (with automatic failover " + "while it is quota-exhausted) and `mark-exhausted` from this " + "consumer attributes to it — use a pin for quota isolation. " + "When omitted, the consumer follows the fleet `auth.active` " + "exactly like an agent: same account swaps, same failover."),
11595
11692
  uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root)."),
11596
11693
  mirror_dir: exports_external.string().optional().describe("Optional host-side directory path. When set, the broker actively " + "writes the consumer's effective-account `.credentials.json` mirror " + "here — in addition to serving creds on demand via `get-credentials`. " + "Use this to eliminate the pull-latency gap: without a mirror the " + "consumer only gets failover creds at its next scheduled re-fetch " + "(up to 30 min). With a mirror the broker pushes failover creds " + "immediately when it detects exhaustion (consumer-quota-sensor tick, " + "or a mark-exhausted RPC on the pinned account). The directory must " + "be accessible to the broker container (bind-mounted from the host) " + "and to the consumer container; the broker writes " + "`<mirror_dir>/.credentials.json` atomically. Chown is attempted to " + "`uid` (default 0) — swallowed when CAP_CHOWN is absent.")
11597
11694
  })).optional().describe("Non-agent peers that hold a broker socket (RFC H §4.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation."),
@@ -11687,6 +11784,7 @@ function resolveDualPath(pathStr) {
11687
11784
  }
11688
11785
 
11689
11786
  // src/config/overlay-loader.ts
11787
+ var import_yaml = __toESM(require_dist(), 1);
11690
11788
  import { existsSync as existsSync2, readFileSync, readdirSync, statSync } from "node:fs";
11691
11789
  import { basename, resolve as resolve2 } from "node:path";
11692
11790
 
@@ -11762,7 +11860,7 @@ function applyAgentOverlays(config) {
11762
11860
  for (const file of files) {
11763
11861
  try {
11764
11862
  const raw = readFileSync(file, "utf-8");
11765
- const parsed = $parse(raw);
11863
+ const parsed = import_yaml.parse(raw);
11766
11864
  const doc = OverlayDocSchema.parse(parsed);
11767
11865
  const title = deriveOverlayTitle(raw, basename(file));
11768
11866
  for (const entry of doc.schedule ?? []) {
@@ -11803,7 +11901,7 @@ function applyAgentOverlays(config) {
11803
11901
  for (const file of skillFiles) {
11804
11902
  try {
11805
11903
  const raw = readFileSync(file, "utf-8");
11806
- const parsed = $parse(raw);
11904
+ const parsed = import_yaml.parse(raw);
11807
11905
  const doc = OverlayDocSchema.parse(parsed);
11808
11906
  for (const skillName of doc.skills ?? []) {
11809
11907
  if (seen.has(skillName))
@@ -11979,6 +12077,11 @@ function mergeAgentConfig(defaultsIn, agentIn) {
11979
12077
  continue;
11980
12078
  if (k === "recall" && base.recall && typeof v === "object" && v !== null && !Array.isArray(v)) {
11981
12079
  combined[k] = { ...base.recall, ...v };
12080
+ } else if (k === "disposition" && base.disposition && typeof v === "object" && v !== null && !Array.isArray(v)) {
12081
+ combined[k] = {
12082
+ ...base.disposition,
12083
+ ...v
12084
+ };
11982
12085
  } else {
11983
12086
  combined[k] = v;
11984
12087
  }
@@ -12320,7 +12423,7 @@ function loadConfig(configPath) {
12320
12423
  }
12321
12424
  let parsed;
12322
12425
  try {
12323
- parsed = $parse(raw);
12426
+ parsed = import_yaml2.parse(raw);
12324
12427
  } catch (err) {
12325
12428
  throw new ConfigError(`Invalid YAML in ${filePath}`, [
12326
12429
  ` ${err.message}`
@@ -12440,8 +12543,31 @@ function isReservedAgentName(name) {
12440
12543
  return RESERVED_AGENT_NAMES.has(name);
12441
12544
  }
12442
12545
 
12546
+ // src/setup/hindsight.ts
12547
+ var HINDSIGHT_DEFAULT_API_PORT = 18888;
12548
+ var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
12549
+ var HINDSIGHT_DEFAULT_API_BASE_URL = HINDSIGHT_DEFAULT_MCP_URL.replace(/\/mcp\/?$/, "");
12550
+ var HINDSIGHT_CONSUMER_NAME = "hindsight";
12551
+ var HINDSIGHT_IMAGE_REPO = "ghcr.io/switchroom/switchroom-hindsight";
12552
+ var HINDSIGHT_IMAGE = `${HINDSIGHT_IMAGE_REPO}:latest`;
12553
+ var HINDSIGHT_BROKER_SOCK_VOLUME = `auth-broker-${HINDSIGHT_CONSUMER_NAME}-sock`;
12554
+ var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
12555
+ var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
12556
+
12443
12557
  // src/memory/hindsight.ts
12444
12558
  var DEFAULT_RETAIN_MISSION = "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.";
12559
+ var PROFILE_MEMORY_DEFAULTS = {
12560
+ "health-coach": {
12561
+ disposition: { skepticism: 2, literalism: 2, empathy: 5 },
12562
+ observations_mission: "Synthesise the person's wellbeing patterns, motivations, and emotional " + "context — how habits, setbacks, and encouragement connect over time."
12563
+ },
12564
+ "executive-assistant": {
12565
+ disposition: { skepticism: 4, literalism: 4, empathy: 3 }
12566
+ },
12567
+ coding: {
12568
+ disposition: { skepticism: 4, literalism: 5, empathy: 2 }
12569
+ }
12570
+ };
12445
12571
 
12446
12572
  // src/agents/reconcile-default-skills.ts
12447
12573
  var warnedMissingPool = new Set;
@@ -12657,37 +12783,8 @@ function resolveConsumerProbeIntervalMs(env) {
12657
12783
  return DEFAULT_CONSUMER_PROBE_INTERVAL_MS;
12658
12784
  }
12659
12785
 
12660
- // src/util/atomic.ts
12661
- import { randomBytes } from "node:crypto";
12662
- import { closeSync, constants, fsyncSync, openSync, renameSync, rmSync, writeSync } from "node:fs";
12663
- var TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
12664
- function atomicWriteFileSync(destPath, contents, mode = 384) {
12665
- const tmp = `${destPath}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
12666
- const buf = typeof contents === "string" ? Buffer.from(contents, "utf-8") : contents;
12667
- let fd = null;
12668
- try {
12669
- fd = openSync(tmp, TMP_OPEN_FLAGS, mode);
12670
- writeSync(fd, buf, 0, buf.length, 0);
12671
- fsyncSync(fd);
12672
- closeSync(fd);
12673
- fd = null;
12674
- renameSync(tmp, destPath);
12675
- } catch (err) {
12676
- if (fd !== null) {
12677
- try {
12678
- closeSync(fd);
12679
- } catch {}
12680
- }
12681
- try {
12682
- rmSync(tmp, { force: true });
12683
- } catch {}
12684
- throw err;
12685
- }
12686
- }
12687
- function atomicWriteJsonSync(destPath, value, mode = 384) {
12688
- atomicWriteFileSync(destPath, JSON.stringify(value, null, 2) + `
12689
- `, mode);
12690
- }
12786
+ // src/auth/broker/server.ts
12787
+ init_atomic();
12691
12788
 
12692
12789
  // src/auth/account-store.ts
12693
12790
  import {
@@ -13341,6 +13438,7 @@ function resolveMicrosoftClientId(configClientId) {
13341
13438
  }
13342
13439
 
13343
13440
  // src/auth/broker/google-storage.ts
13441
+ init_atomic();
13344
13442
  import { existsSync as existsSync5, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync3 } from "node:fs";
13345
13443
  import { dirname, join as join2, resolve as resolve5 } from "node:path";
13346
13444
  function normalizeGoogleAccountForStorage(account) {
@@ -13409,6 +13507,7 @@ function listGoogleAccounts(stateDir) {
13409
13507
  }
13410
13508
 
13411
13509
  // src/auth/broker/microsoft-storage.ts
13510
+ init_atomic();
13412
13511
  import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, rmSync as rmSync4, statSync as statSync4 } from "node:fs";
13413
13512
  import { dirname as dirname2, join as join3, resolve as resolve6 } from "node:path";
13414
13513
  function normalizeMicrosoftAccountForStorage(account) {
@@ -13527,19 +13626,19 @@ function classify(socketPath, config) {
13527
13626
  return null;
13528
13627
  }
13529
13628
  function validateConsumerNames(config) {
13530
- const errors3 = [];
13629
+ const errors2 = [];
13531
13630
  for (const c of config.consumers) {
13532
13631
  if (RESERVED_NAMES.has(c)) {
13533
- errors3.push(`consumer name '${c}' is reserved`);
13632
+ errors2.push(`consumer name '${c}' is reserved`);
13534
13633
  }
13535
13634
  if (config.agents.includes(c)) {
13536
- errors3.push(`consumer name '${c}' collides with an agent name`);
13635
+ errors2.push(`consumer name '${c}' collides with an agent name`);
13537
13636
  }
13538
13637
  if (config.adminAgents.includes(c)) {
13539
- errors3.push(`consumer name '${c}' is an admin agent (consumers cannot be admins)`);
13638
+ errors2.push(`consumer name '${c}' is an admin agent (consumers cannot be admins)`);
13540
13639
  }
13541
13640
  }
13542
- return errors3;
13641
+ return errors2;
13543
13642
  }
13544
13643
 
13545
13644
  // src/auth/broker/protocol.ts
@@ -13910,10 +14009,12 @@ class AuthBroker {
13910
14009
  refreshInFlight = new Set;
13911
14010
  consumerLastSeen = {};
13912
14011
  capChownWarned = false;
14012
+ yamlActive;
13913
14013
  closed = false;
13914
14014
  constructor(config, opts = {}) {
13915
14015
  this.opts = opts;
13916
14016
  this.config = config;
14017
+ this.yamlActive = config.auth?.active;
13917
14018
  this.home = opts.home;
13918
14019
  this.now = opts.now ?? nowMs;
13919
14020
  this.operatorUid = opts.operatorUid;
@@ -14040,6 +14141,8 @@ class AuthBroker {
14040
14141
  this.assertConfigConsistent(config);
14041
14142
  const prev = this.config;
14042
14143
  this.config = config;
14144
+ this.yamlActive = config.auth?.active;
14145
+ this.applyActiveOverride();
14043
14146
  const wanted = new Set;
14044
14147
  for (const name of Object.keys(config.agents ?? {})) {
14045
14148
  wanted.add(this.agentSocketPath(name));
@@ -14115,7 +14218,7 @@ class AuthBroker {
14115
14218
  const sockPath = this.operatorSocketPath();
14116
14219
  await this.bindListener(sockPath, operatorUid, 384, { kind: "operator" });
14117
14220
  }
14118
- async bindListener(sockPath, targetUid, sockMode, identity2) {
14221
+ async bindListener(sockPath, targetUid, sockMode, identity) {
14119
14222
  const dir = dirname3(sockPath);
14120
14223
  if (existsSync7(dir)) {
14121
14224
  try {
@@ -14134,7 +14237,7 @@ class AuthBroker {
14134
14237
  }
14135
14238
  await new Promise((resolveP, rejectP) => {
14136
14239
  const server = net.createServer((sock) => {
14137
- this.handleConnection(sock, sockPath, identity2);
14240
+ this.handleConnection(sock, sockPath, identity);
14138
14241
  });
14139
14242
  server.on("error", (err) => rejectP(err));
14140
14243
  server.listen(sockPath, () => {
@@ -14147,7 +14250,7 @@ class AuthBroker {
14147
14250
  try {
14148
14251
  chownSync2(dir, targetUid, targetUid);
14149
14252
  } catch {}
14150
- this.listeners.set(sockPath, { server, identity: identity2, socketPath: sockPath });
14253
+ this.listeners.set(sockPath, { server, identity, socketPath: sockPath });
14151
14254
  resolveP();
14152
14255
  });
14153
14256
  });
@@ -14191,7 +14294,7 @@ class AuthBroker {
14191
14294
  socket.write(encodeError(reqId, "INVALID_ARGS", err.message));
14192
14295
  return;
14193
14296
  }
14194
- const identity2 = classify(sockPath, configToShape(this.config)) ?? boundIdentity;
14297
+ const identity = classify(sockPath, configToShape(this.config)) ?? boundIdentity;
14195
14298
  try {
14196
14299
  switch (req.op) {
14197
14300
  case "get-credentials": {
@@ -14201,22 +14304,22 @@ class AuthBroker {
14201
14304
  break;
14202
14305
  }
14203
14306
  if (provider === "anthropic") {
14204
- await this.opGetCredentials(socket, reqId, identity2);
14307
+ await this.opGetCredentials(socket, reqId, identity);
14205
14308
  break;
14206
14309
  }
14207
14310
  if (provider === "google") {
14208
- await this.opGoogleGetCredentials(socket, reqId, identity2);
14311
+ await this.opGoogleGetCredentials(socket, reqId, identity);
14209
14312
  break;
14210
14313
  }
14211
14314
  if (provider === "microsoft") {
14212
- await this.opMicrosoftGetCredentials(socket, reqId, identity2);
14315
+ await this.opMicrosoftGetCredentials(socket, reqId, identity);
14213
14316
  break;
14214
14317
  }
14215
14318
  socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in get-credentials dispatch`));
14216
14319
  break;
14217
14320
  }
14218
14321
  case "list-state":
14219
- await this.opListState(socket, reqId, identity2);
14322
+ await this.opListState(socket, reqId, identity);
14220
14323
  break;
14221
14324
  case "set-active": {
14222
14325
  const provider = req.provider ?? "anthropic";
@@ -14224,11 +14327,11 @@ class AuthBroker {
14224
14327
  socket.write(encodeError(reqId, "INVALID_ARGS", `set-active is Anthropic-only — Google's account-active model is per-agent via google_accounts.enabled_for[]`));
14225
14328
  break;
14226
14329
  }
14227
- await this.opSetActive(socket, reqId, identity2, req.account);
14330
+ await this.opSetActive(socket, reqId, identity, req.account);
14228
14331
  break;
14229
14332
  }
14230
14333
  case "mark-exhausted":
14231
- await this.opMarkExhausted(socket, reqId, identity2, req.until);
14334
+ await this.opMarkExhausted(socket, reqId, identity, req.until);
14232
14335
  break;
14233
14336
  case "refresh-account": {
14234
14337
  const provider = req.provider ?? "anthropic";
@@ -14237,7 +14340,7 @@ class AuthBroker {
14237
14340
  break;
14238
14341
  }
14239
14342
  if (provider === "anthropic") {
14240
- await this.opRefreshAccount(socket, reqId, identity2, req.account);
14343
+ await this.opRefreshAccount(socket, reqId, identity, req.account);
14241
14344
  break;
14242
14345
  }
14243
14346
  socket.write(encodeError(reqId, "INVALID_ARGS", `refresh-account dispatch through provider '${provider}' storage path lands in Phase 3b.2c (vault-broker-mediated, not direct-to-disk like Anthropic)`));
@@ -14256,17 +14359,17 @@ class AuthBroker {
14256
14359
  }
14257
14360
  if (provider === "anthropic") {
14258
14361
  const anthropicCreds = req.credentials;
14259
- await this.opAddAccount(socket, reqId, identity2, req.label, anthropicCreds, req.replace ?? false);
14362
+ await this.opAddAccount(socket, reqId, identity, req.label, anthropicCreds, req.replace ?? false);
14260
14363
  break;
14261
14364
  }
14262
14365
  if (provider === "google") {
14263
14366
  const googleCreds = req.credentials;
14264
- await this.opGoogleAddAccount(socket, reqId, identity2, req.label, googleCreds, req.replace ?? false);
14367
+ await this.opGoogleAddAccount(socket, reqId, identity, req.label, googleCreds, req.replace ?? false);
14265
14368
  break;
14266
14369
  }
14267
14370
  if (provider === "microsoft") {
14268
14371
  const microsoftCreds = req.credentials;
14269
- await this.opMicrosoftAddAccount(socket, reqId, identity2, req.label, microsoftCreds, req.replace ?? false);
14372
+ await this.opMicrosoftAddAccount(socket, reqId, identity, req.label, microsoftCreds, req.replace ?? false);
14270
14373
  break;
14271
14374
  }
14272
14375
  socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in add-account dispatch`));
@@ -14279,69 +14382,71 @@ class AuthBroker {
14279
14382
  break;
14280
14383
  }
14281
14384
  if (provider === "anthropic") {
14282
- await this.opRmAccount(socket, reqId, identity2, req.label);
14385
+ await this.opRmAccount(socket, reqId, identity, req.label);
14283
14386
  break;
14284
14387
  }
14285
14388
  if (provider === "google") {
14286
- await this.opGoogleRmAccount(socket, reqId, identity2, req.label);
14389
+ await this.opGoogleRmAccount(socket, reqId, identity, req.label);
14287
14390
  break;
14288
14391
  }
14289
14392
  if (provider === "microsoft") {
14290
- await this.opMicrosoftRmAccount(socket, reqId, identity2, req.label);
14393
+ await this.opMicrosoftRmAccount(socket, reqId, identity, req.label);
14291
14394
  break;
14292
14395
  }
14293
14396
  socket.write(encodeError(reqId, "INTERNAL", `unhandled provider '${provider}' in rm-account dispatch`));
14294
14397
  break;
14295
14398
  }
14296
14399
  case "set-override":
14297
- await this.opSetOverride(socket, reqId, identity2, req.agent, req.account);
14400
+ await this.opSetOverride(socket, reqId, identity, req.agent, req.account);
14298
14401
  break;
14299
14402
  case "list-google-accounts":
14300
- await this.opListGoogleAccounts(socket, reqId, identity2);
14403
+ await this.opListGoogleAccounts(socket, reqId, identity);
14301
14404
  break;
14302
14405
  case "list-microsoft-accounts":
14303
- await this.opListMicrosoftAccounts(socket, reqId, identity2);
14406
+ await this.opListMicrosoftAccounts(socket, reqId, identity);
14304
14407
  break;
14305
14408
  case "probe-quota":
14306
- await this.opProbeQuota(socket, reqId, identity2, req.accounts, req.timeoutMs, req.forceLive);
14409
+ await this.opProbeQuota(socket, reqId, identity, req.accounts, req.timeoutMs, req.forceLive);
14307
14410
  break;
14308
14411
  case "claim-notification":
14309
- this.opClaimNotification(socket, reqId, identity2, req.key, req.windowMs);
14412
+ this.opClaimNotification(socket, reqId, identity, req.key, req.windowMs);
14310
14413
  break;
14311
14414
  }
14312
14415
  } catch (err) {
14313
14416
  socket.write(encodeError(reqId, "INTERNAL", err.message));
14314
14417
  }
14315
14418
  }
14316
- isAdmin(identity2) {
14317
- if (identity2.kind === "operator")
14419
+ isAdmin(identity) {
14420
+ if (identity.kind === "operator")
14318
14421
  return true;
14319
- if (identity2.kind === "agent")
14320
- return identity2.admin;
14422
+ if (identity.kind === "agent")
14423
+ return identity.admin;
14321
14424
  return false;
14322
14425
  }
14323
14426
  respondForbidden(socket, id, why) {
14324
14427
  socket.write(encodeError(id, "FORBIDDEN", why));
14325
14428
  }
14326
- callerAccount(identity2) {
14429
+ callerAccount(identity) {
14327
14430
  const auth = this.config.auth;
14328
14431
  if (!auth)
14329
14432
  return null;
14330
- if (identity2.kind === "operator")
14433
+ if (identity.kind === "operator")
14331
14434
  return auth.active ?? null;
14332
- if (identity2.kind === "consumer") {
14333
- const c = (auth.consumers ?? []).find((x) => x.name === identity2.name);
14334
- return c?.account ?? null;
14435
+ if (identity.kind === "consumer") {
14436
+ const c = (auth.consumers ?? []).find((x) => x.name === identity.name);
14437
+ if (!c)
14438
+ return null;
14439
+ return c.account ?? auth.active ?? null;
14335
14440
  }
14336
- const agent = (this.config.agents ?? {})[identity2.name];
14441
+ const agent = (this.config.agents ?? {})[identity.name];
14337
14442
  const override = agent?.auth?.override;
14338
14443
  if (override)
14339
14444
  return override;
14340
14445
  return auth.active ?? null;
14341
14446
  }
14342
- servingAccount(identity2) {
14343
- const account = this.callerAccount(identity2);
14344
- if (identity2.kind === "operator")
14447
+ servingAccount(identity) {
14448
+ const account = this.callerAccount(identity);
14449
+ if (identity.kind === "operator")
14345
14450
  return account;
14346
14451
  return this.accountWithFailover(account);
14347
14452
  }
@@ -14433,24 +14538,24 @@ class AuthBroker {
14433
14538
  }
14434
14539
  return account;
14435
14540
  }
14436
- async opGetCredentials(socket, id, identity2) {
14437
- const account = this.servingAccount(identity2);
14541
+ async opGetCredentials(socket, id, identity) {
14542
+ const account = this.servingAccount(identity);
14438
14543
  if (!account) {
14439
- this.audit({ op: "get-credentials", identity: identity2, accountKind: "claude", ok: false, error: "no-active-account" });
14544
+ this.audit({ op: "get-credentials", identity, accountKind: "claude", ok: false, error: "no-active-account" });
14440
14545
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", "no active account configured"));
14441
14546
  return;
14442
14547
  }
14443
14548
  const creds = readAccountCredentials(account, this.home);
14444
14549
  if (!creds) {
14445
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "claude", ok: false, error: "missing-credentials" });
14550
+ this.audit({ op: "get-credentials", identity, account, accountKind: "claude", ok: false, error: "missing-credentials" });
14446
14551
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `no credentials for account '${account}'`));
14447
14552
  return;
14448
14553
  }
14449
14554
  const expiresAt = creds.claudeAiOauth?.expiresAt;
14450
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "claude", ok: true });
14555
+ this.audit({ op: "get-credentials", identity, account, accountKind: "claude", ok: true });
14451
14556
  socket.write(encodeSuccess(id, { account, credentials: creds, expiresAt }));
14452
14557
  }
14453
- async opListState(socket, id, identity2) {
14558
+ async opListState(socket, id, identity) {
14454
14559
  const auth = this.config.auth ?? {};
14455
14560
  const accounts = listAccounts(this.home).map((label) => {
14456
14561
  const creds = readAccountCredentials(label, this.home);
@@ -14475,11 +14580,11 @@ class AuthBroker {
14475
14580
  });
14476
14581
  const consumers = (auth.consumers ?? []).map((c) => ({
14477
14582
  name: c.name,
14478
- account: c.account,
14583
+ account: c.account ?? auth.active ?? "",
14479
14584
  last_seen_at: this.consumerLastSeen[c.name] ?? null
14480
14585
  }));
14481
- const active_overage_serving = this.isActiveOverageServing(this.callerAccount(identity2));
14482
- this.audit({ op: "list-state", identity: identity2, ok: true });
14586
+ const active_overage_serving = this.isActiveOverageServing(this.callerAccount(identity));
14587
+ this.audit({ op: "list-state", identity, ok: true });
14483
14588
  socket.write(encodeSuccess(id, {
14484
14589
  active: auth.active ?? "",
14485
14590
  fallback_order: auth.fallback_order ?? [],
@@ -14489,7 +14594,7 @@ class AuthBroker {
14489
14594
  active_overage_serving
14490
14595
  }));
14491
14596
  }
14492
- async opListGoogleAccounts(socket, id, identity2) {
14597
+ async opListGoogleAccounts(socket, id, identity) {
14493
14598
  const accounts = listGoogleAccounts(this.stateDir).map((account) => {
14494
14599
  const creds = readGoogleAccountCredentials(this.stateDir, account);
14495
14600
  if (!creds)
@@ -14501,10 +14606,10 @@ class AuthBroker {
14501
14606
  clientId: creds.googleOauth.clientId
14502
14607
  };
14503
14608
  }).filter((entry) => entry !== null).sort((a, b) => a.account.localeCompare(b.account));
14504
- this.audit({ op: "list-google-accounts", identity: identity2, accountKind: "google", ok: true });
14609
+ this.audit({ op: "list-google-accounts", identity, accountKind: "google", ok: true });
14505
14610
  socket.write(encodeSuccess(id, { accounts }));
14506
14611
  }
14507
- async opProbeQuota(socket, id, identity2, accounts, timeoutMs, forceLive) {
14612
+ async opProbeQuota(socket, id, identity, accounts, timeoutMs, forceLive) {
14508
14613
  const ttlMs = forceLive ? 0 : quotaProbeTtlMs();
14509
14614
  const results = await Promise.all(accounts.map(async (label) => {
14510
14615
  const cached = this.lastQuotaCache[label];
@@ -14521,13 +14626,13 @@ class AuthBroker {
14521
14626
  ok: false,
14522
14627
  reason: "no credentials for account in broker store"
14523
14628
  };
14524
- this.audit({ op: "probe-quota", identity: identity2, account: label, accountKind: "claude", ok: false, error: "missing-credentials" });
14629
+ this.audit({ op: "probe-quota", identity, account: label, accountKind: "claude", ok: false, error: "missing-credentials" });
14525
14630
  return { label, result: result2 };
14526
14631
  }
14527
14632
  const result = await this.probeQuotaSingleFlight(label, token, timeoutMs);
14528
14633
  this.audit({
14529
14634
  op: "probe-quota",
14530
- identity: identity2,
14635
+ identity,
14531
14636
  account: label,
14532
14637
  accountKind: "claude",
14533
14638
  ok: result.ok,
@@ -14594,10 +14699,20 @@ class AuthBroker {
14594
14699
  continue;
14595
14700
  }
14596
14701
  this.cacheQuotaSnapshot(label, result);
14702
+ const active = this.config.auth?.active;
14703
+ if (label !== active)
14704
+ continue;
14705
+ const decision = quotaIndicatesExhaustion(result, this.isOverageAllowed(label));
14706
+ if (!decision.exhausted)
14707
+ continue;
14708
+ process.stdout.write(`auth-broker: fleet-quota-probe shows ACTIVE ${label} exhausted — proactive failover
14709
+ `);
14710
+ this.audit({ op: "mark-exhausted", identity: { kind: "operator" }, account: label, accountKind: "claude", ok: true });
14711
+ await this.markExhaustedAndRoll(label, decision.until ?? undefined, { kind: "operator" });
14597
14712
  }
14598
14713
  }
14599
14714
  async consumerQuotaProbeTick() {
14600
- const accounts = Array.from(new Set((this.config.auth?.consumers ?? []).map((c) => c.account)));
14715
+ const accounts = Array.from(new Set((this.config.auth?.consumers ?? []).map((c) => c.account).filter((a) => typeof a === "string" && a.length > 0)));
14601
14716
  for (const label of accounts) {
14602
14717
  const creds = readAccountCredentials(label, this.home);
14603
14718
  const token = creds?.claudeAiOauth?.accessToken;
@@ -14638,14 +14753,14 @@ class AuthBroker {
14638
14753
  this.fanoutToAffectedConsumers(label);
14639
14754
  }
14640
14755
  }
14641
- async opSetActive(socket, id, identity2, account) {
14642
- if (!this.isAdmin(identity2)) {
14643
- this.audit({ op: "set-active", identity: identity2, account, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14756
+ async opSetActive(socket, id, identity, account) {
14757
+ if (!this.isAdmin(identity)) {
14758
+ this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14644
14759
  this.respondForbidden(socket, id, "set-active requires admin");
14645
14760
  return;
14646
14761
  }
14647
14762
  if (!accountExists(account, this.home)) {
14648
- this.audit({ op: "set-active", identity: identity2, account, accountKind: "claude", ok: false, error: "ACCOUNT_NOT_FOUND" });
14763
+ this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: false, error: "ACCOUNT_NOT_FOUND" });
14649
14764
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
14650
14765
  return;
14651
14766
  }
@@ -14654,18 +14769,24 @@ class AuthBroker {
14654
14769
  auth: { ...this.config.auth ?? {}, active: account }
14655
14770
  };
14656
14771
  this.config = cfg;
14772
+ this.persistActiveOverride(account);
14657
14773
  const fanned = this.fanoutToAffectedAgents(account);
14658
14774
  this.fanoutAllConsumers();
14659
- this.audit({ op: "set-active", identity: identity2, account, accountKind: "claude", ok: true });
14775
+ this.audit({ op: "set-active", identity, account, accountKind: "claude", ok: true });
14660
14776
  socket.write(encodeSuccess(id, { active: account, fanned }));
14661
14777
  }
14662
- async opMarkExhausted(socket, id, identity2, until) {
14663
- const account = this.callerAccount(identity2);
14778
+ async opMarkExhausted(socket, id, identity, until) {
14779
+ const account = this.callerAccount(identity);
14664
14780
  if (!account) {
14665
- this.audit({ op: "mark-exhausted", identity: identity2, accountKind: "claude", ok: false, error: "no-active-account" });
14781
+ this.audit({ op: "mark-exhausted", identity, accountKind: "claude", ok: false, error: "no-active-account" });
14666
14782
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", "no active account configured"));
14667
14783
  return;
14668
14784
  }
14785
+ const { rolled, rolledTo } = await this.markExhaustedAndRoll(account, until, identity);
14786
+ this.audit({ op: "mark-exhausted", identity, account, accountKind: "claude", ok: true });
14787
+ socket.write(encodeSuccess(id, { account, rolled, rolledTo }));
14788
+ }
14789
+ async markExhaustedAndRoll(account, until, identity) {
14669
14790
  const now = this.now();
14670
14791
  const exhaustedUntil = clampMarkExpiry({
14671
14792
  proposedUntil: until ?? now + MARK_EXHAUSTED_DEFAULT_MS,
@@ -14678,10 +14799,28 @@ class AuthBroker {
14678
14799
  const rolledTo = await this.nextHealthyAccountLive(account, this.config.auth?.fallback_order ?? []);
14679
14800
  const rolled = this.fanoutFailoverTo(account, rolledTo);
14680
14801
  this.fanoutToAffectedConsumers(account);
14681
- this.audit({ op: "mark-exhausted", identity: identity2, account, accountKind: "claude", ok: true });
14682
- socket.write(encodeSuccess(id, { account, rolled, rolledTo }));
14802
+ if (rolledTo && this.config.auth?.active === account && this.exhaustionLiveCorroborated(account)) {
14803
+ this.config = {
14804
+ ...this.config,
14805
+ auth: { ...this.config.auth ?? {}, active: rolledTo }
14806
+ };
14807
+ this.persistActiveOverride(rolledTo);
14808
+ this.fanoutAllConsumers();
14809
+ this.audit({ op: "auto-promote-active", identity, account: rolledTo, accountKind: "claude", ok: true });
14810
+ process.stdout.write(`auth-broker: auto-promoted auth.active ${account} → ${rolledTo} ` + `(${account} exhausted until ${new Date(exhaustedUntil).toISOString()}) — persisted
14811
+ `);
14812
+ }
14813
+ return { rolled, rolledTo };
14814
+ }
14815
+ exhaustionLiveCorroborated(account) {
14816
+ const snapshot = this.lastQuotaCache[account];
14817
+ if (!snapshot || !snapshotFresh(snapshot, this.now(), MARK_EXHAUSTED_DEFAULT_MS))
14818
+ return false;
14819
+ if (!snapshotWalled(snapshot))
14820
+ return false;
14821
+ return !overageLiftsWall(snapshot, this.isOverageAllowed(account));
14683
14822
  }
14684
- opClaimNotification(socket, id, identity2, key, windowMs) {
14823
+ opClaimNotification(socket, id, identity, key, windowMs) {
14685
14824
  const now = this.now();
14686
14825
  const prev = this.notificationClaims[key];
14687
14826
  const granted = prev === undefined || now - prev >= windowMs;
@@ -14692,35 +14831,35 @@ class AuthBroker {
14692
14831
  delete this.notificationClaims[k];
14693
14832
  }
14694
14833
  this.persistNotificationClaims();
14695
- this.audit({ op: "claim-notification", identity: identity2, account: key, ok: true });
14834
+ this.audit({ op: "claim-notification", identity, account: key, ok: true });
14696
14835
  }
14697
14836
  socket.write(encodeSuccess(id, { granted }));
14698
14837
  }
14699
- async opRefreshAccount(socket, id, identity2, account) {
14700
- if (!this.isAdmin(identity2)) {
14701
- this.audit({ op: "refresh-account", identity: identity2, account, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14838
+ async opRefreshAccount(socket, id, identity, account) {
14839
+ if (!this.isAdmin(identity)) {
14840
+ this.audit({ op: "refresh-account", identity, account, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14702
14841
  this.respondForbidden(socket, id, "refresh-account requires admin");
14703
14842
  return;
14704
14843
  }
14705
14844
  if (!accountExists(account, this.home)) {
14706
- this.audit({ op: "refresh-account", identity: identity2, account, accountKind: "claude", ok: false, error: "ACCOUNT_NOT_FOUND" });
14845
+ this.audit({ op: "refresh-account", identity, account, accountKind: "claude", ok: false, error: "ACCOUNT_NOT_FOUND" });
14707
14846
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `account '${account}' not found`));
14708
14847
  return;
14709
14848
  }
14710
14849
  const result = await this.refreshOneAccount(account, true);
14711
14850
  if (result.kind === "failed") {
14712
- this.audit({ op: "refresh-account", identity: identity2, account, accountKind: "claude", ok: false, error: result.error });
14851
+ this.audit({ op: "refresh-account", identity, account, accountKind: "claude", ok: false, error: result.error });
14713
14852
  socket.write(encodeError(id, "REFRESH_FAILED", result.error));
14714
14853
  return;
14715
14854
  }
14716
14855
  const creds = readAccountCredentials(account, this.home);
14717
14856
  const expiresAt = creds?.claudeAiOauth?.expiresAt;
14718
- this.audit({ op: "refresh-account", identity: identity2, account, accountKind: "claude", ok: true });
14857
+ this.audit({ op: "refresh-account", identity, account, accountKind: "claude", ok: true });
14719
14858
  socket.write(encodeSuccess(id, { account, expiresAt }));
14720
14859
  }
14721
- async opAddAccount(socket, id, identity2, label, credentials, replace) {
14722
- if (!this.isAdmin(identity2)) {
14723
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14860
+ async opAddAccount(socket, id, identity, label, credentials, replace) {
14861
+ if (!this.isAdmin(identity)) {
14862
+ this.audit({ op: "add-account", identity, account: label, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14724
14863
  this.respondForbidden(socket, id, "add-account requires admin");
14725
14864
  return;
14726
14865
  }
@@ -14731,7 +14870,7 @@ class AuthBroker {
14731
14870
  return;
14732
14871
  }
14733
14872
  if (accountExists(label, this.home) && !replace) {
14734
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "claude", ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
14873
+ this.audit({ op: "add-account", identity, account: label, accountKind: "claude", ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
14735
14874
  socket.write(encodeError(id, "ACCOUNT_ALREADY_EXISTS", `account '${label}' already exists; pass replace:true to overwrite`));
14736
14875
  return;
14737
14876
  }
@@ -14749,12 +14888,12 @@ class AuthBroker {
14749
14888
  this.persistShaIndex();
14750
14889
  this.fanoutToAffectedAgents(label);
14751
14890
  const expiresAt = credentials.claudeAiOauth?.expiresAt;
14752
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "claude", ok: true, replace });
14891
+ this.audit({ op: "add-account", identity, account: label, accountKind: "claude", ok: true, replace });
14753
14892
  socket.write(encodeSuccess(id, { label, expiresAt }));
14754
14893
  }
14755
- async opRmAccount(socket, id, identity2, label) {
14756
- if (!this.isAdmin(identity2)) {
14757
- this.audit({ op: "rm-account", identity: identity2, account: label, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14894
+ async opRmAccount(socket, id, identity, label) {
14895
+ if (!this.isAdmin(identity)) {
14896
+ this.audit({ op: "rm-account", identity, account: label, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14758
14897
  this.respondForbidden(socket, id, "rm-account requires admin");
14759
14898
  return;
14760
14899
  }
@@ -14787,42 +14926,42 @@ class AuthBroker {
14787
14926
  this.persistQuota();
14788
14927
  this.persistThresholdViolations();
14789
14928
  this.persistLastQuotaCache();
14790
- this.audit({ op: "rm-account", identity: identity2, account: label, accountKind: "claude", ok: true });
14929
+ this.audit({ op: "rm-account", identity, account: label, accountKind: "claude", ok: true });
14791
14930
  socket.write(encodeSuccess(id, { label }));
14792
14931
  }
14793
- async opGoogleGetCredentials(socket, id, identity2) {
14794
- if (identity2.kind !== "agent") {
14795
- socket.write(encodeError(id, "INVALID_ARGS", `Google get-credentials is per-agent only (caller kind '${identity2.kind}' not supported); use the agent's per-agent socket bind`));
14932
+ async opGoogleGetCredentials(socket, id, identity) {
14933
+ if (identity.kind !== "agent") {
14934
+ socket.write(encodeError(id, "INVALID_ARGS", `Google get-credentials is per-agent only (caller kind '${identity.kind}' not supported); use the agent's per-agent socket bind`));
14796
14935
  return;
14797
14936
  }
14798
- const agentName = identity2.name;
14937
+ const agentName = identity.name;
14799
14938
  const agent = (this.config.agents ?? {})[agentName];
14800
14939
  const account = agent?.google_workspace?.account;
14801
14940
  if (!account) {
14802
- this.audit({ op: "get-credentials", identity: identity2, accountKind: "google", ok: false, error: "no-google-account-configured" });
14941
+ this.audit({ op: "get-credentials", identity, accountKind: "google", ok: false, error: "no-google-account-configured" });
14803
14942
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' has no google_workspace.account configured in switchroom.yaml`));
14804
14943
  return;
14805
14944
  }
14806
14945
  const ga = this.config.google_accounts;
14807
14946
  const enabledFor = ga?.[account]?.enabled_for ?? [];
14808
14947
  if (!enabledFor.includes(agentName)) {
14809
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "google", ok: false, error: "acl-deny" });
14948
+ this.audit({ op: "get-credentials", identity, account, accountKind: "google", ok: false, error: "acl-deny" });
14810
14949
  socket.write(encodeError(id, "FORBIDDEN", `agent '${agentName}' not in google_accounts['${account}'].enabled_for[] — operator must run \`switchroom auth google enable ${account} ${agentName}\``));
14811
14950
  return;
14812
14951
  }
14813
14952
  const creds = readGoogleAccountCredentials(this.stateDir, account);
14814
14953
  if (!creds) {
14815
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "google", ok: false, error: "missing-credentials" });
14954
+ this.audit({ op: "get-credentials", identity, account, accountKind: "google", ok: false, error: "missing-credentials" });
14816
14955
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `no Google credentials for account '${account}' — operator must run \`switchroom auth google account add ${account}\``));
14817
14956
  return;
14818
14957
  }
14819
14958
  const expiresAt = creds.googleOauth?.expiresAt;
14820
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "google", ok: true });
14959
+ this.audit({ op: "get-credentials", identity, account, accountKind: "google", ok: true });
14821
14960
  socket.write(encodeSuccess(id, { account, credentials: creds, expiresAt }));
14822
14961
  }
14823
- async opGoogleAddAccount(socket, id, identity2, label, credentials, replace) {
14824
- if (!this.isAdmin(identity2)) {
14825
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "google", ok: false, error: "FORBIDDEN" });
14962
+ async opGoogleAddAccount(socket, id, identity, label, credentials, replace) {
14963
+ if (!this.isAdmin(identity)) {
14964
+ this.audit({ op: "add-account", identity, account: label, accountKind: "google", ok: false, error: "FORBIDDEN" });
14826
14965
  this.respondForbidden(socket, id, "add-account requires admin");
14827
14966
  return;
14828
14967
  }
@@ -14833,7 +14972,7 @@ class AuthBroker {
14833
14972
  return;
14834
14973
  }
14835
14974
  if (googleAccountExists(this.stateDir, label) && !replace) {
14836
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "google", ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
14975
+ this.audit({ op: "add-account", identity, account: label, accountKind: "google", ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
14837
14976
  socket.write(encodeError(id, "ACCOUNT_ALREADY_EXISTS", `google account '${label}' already exists; pass replace:true to overwrite`));
14838
14977
  return;
14839
14978
  }
@@ -14844,12 +14983,12 @@ class AuthBroker {
14844
14983
  return;
14845
14984
  }
14846
14985
  const expiresAt = credentials.googleOauth?.expiresAt;
14847
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "google", ok: true, replace });
14986
+ this.audit({ op: "add-account", identity, account: label, accountKind: "google", ok: true, replace });
14848
14987
  socket.write(encodeSuccess(id, { label, expiresAt }));
14849
14988
  }
14850
- async opGoogleRmAccount(socket, id, identity2, label) {
14851
- if (!this.isAdmin(identity2)) {
14852
- this.audit({ op: "rm-account", identity: identity2, account: label, accountKind: "google", ok: false, error: "FORBIDDEN" });
14989
+ async opGoogleRmAccount(socket, id, identity, label) {
14990
+ if (!this.isAdmin(identity)) {
14991
+ this.audit({ op: "rm-account", identity, account: label, accountKind: "google", ok: false, error: "FORBIDDEN" });
14853
14992
  this.respondForbidden(socket, id, "rm-account requires admin");
14854
14993
  return;
14855
14994
  }
@@ -14875,42 +15014,42 @@ class AuthBroker {
14875
15014
  socket.write(encodeError(id, "INTERNAL", err.message));
14876
15015
  return;
14877
15016
  }
14878
- this.audit({ op: "rm-account", identity: identity2, account: label, accountKind: "google", ok: true });
15017
+ this.audit({ op: "rm-account", identity, account: label, accountKind: "google", ok: true });
14879
15018
  socket.write(encodeSuccess(id, { label }));
14880
15019
  }
14881
- async opMicrosoftGetCredentials(socket, id, identity2) {
14882
- if (identity2.kind !== "agent") {
14883
- socket.write(encodeError(id, "INVALID_ARGS", `Microsoft get-credentials is per-agent only (caller kind '${identity2.kind}' not supported); use the agent's per-agent socket bind`));
15020
+ async opMicrosoftGetCredentials(socket, id, identity) {
15021
+ if (identity.kind !== "agent") {
15022
+ socket.write(encodeError(id, "INVALID_ARGS", `Microsoft get-credentials is per-agent only (caller kind '${identity.kind}' not supported); use the agent's per-agent socket bind`));
14884
15023
  return;
14885
15024
  }
14886
- const agentName = identity2.name;
15025
+ const agentName = identity.name;
14887
15026
  const agent = (this.config.agents ?? {})[agentName];
14888
15027
  const account = agent?.microsoft_workspace?.account;
14889
15028
  if (!account) {
14890
- this.audit({ op: "get-credentials", identity: identity2, accountKind: "microsoft", ok: false, error: "no-microsoft-account-configured" });
15029
+ this.audit({ op: "get-credentials", identity, accountKind: "microsoft", ok: false, error: "no-microsoft-account-configured" });
14891
15030
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `agent '${agentName}' has no microsoft_workspace.account configured in switchroom.yaml`));
14892
15031
  return;
14893
15032
  }
14894
15033
  const ma = this.config.microsoft_accounts;
14895
15034
  const enabledFor = ma?.[account]?.enabled_for ?? [];
14896
15035
  if (!enabledFor.includes(agentName)) {
14897
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "microsoft", ok: false, error: "acl-deny" });
15036
+ this.audit({ op: "get-credentials", identity, account, accountKind: "microsoft", ok: false, error: "acl-deny" });
14898
15037
  socket.write(encodeError(id, "FORBIDDEN", `agent '${agentName}' not in microsoft_accounts['${account}'].enabled_for[] — operator must run \`switchroom auth microsoft enable ${account} ${agentName}\``));
14899
15038
  return;
14900
15039
  }
14901
15040
  const creds = readMicrosoftAccountCredentials(this.stateDir, account);
14902
15041
  if (!creds) {
14903
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "microsoft", ok: false, error: "missing-credentials" });
15042
+ this.audit({ op: "get-credentials", identity, account, accountKind: "microsoft", ok: false, error: "missing-credentials" });
14904
15043
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", `no Microsoft credentials for account '${account}' — operator must run \`switchroom auth microsoft account add ${account}\``));
14905
15044
  return;
14906
15045
  }
14907
15046
  const expiresAt = creds.microsoftOauth?.expiresAt;
14908
- this.audit({ op: "get-credentials", identity: identity2, account, accountKind: "microsoft", ok: true });
15047
+ this.audit({ op: "get-credentials", identity, account, accountKind: "microsoft", ok: true });
14909
15048
  socket.write(encodeSuccess(id, { account, credentials: creds, expiresAt }));
14910
15049
  }
14911
- async opMicrosoftAddAccount(socket, id, identity2, label, credentials, replace) {
14912
- if (!this.isAdmin(identity2)) {
14913
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "microsoft", ok: false, error: "FORBIDDEN" });
15050
+ async opMicrosoftAddAccount(socket, id, identity, label, credentials, replace) {
15051
+ if (!this.isAdmin(identity)) {
15052
+ this.audit({ op: "add-account", identity, account: label, accountKind: "microsoft", ok: false, error: "FORBIDDEN" });
14914
15053
  this.respondForbidden(socket, id, "add-account requires admin");
14915
15054
  return;
14916
15055
  }
@@ -14921,7 +15060,7 @@ class AuthBroker {
14921
15060
  return;
14922
15061
  }
14923
15062
  if (microsoftAccountExists(this.stateDir, label) && !replace) {
14924
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "microsoft", ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
15063
+ this.audit({ op: "add-account", identity, account: label, accountKind: "microsoft", ok: false, error: "ACCOUNT_ALREADY_EXISTS" });
14925
15064
  socket.write(encodeError(id, "ACCOUNT_ALREADY_EXISTS", `microsoft account '${label}' already exists; pass replace:true to overwrite`));
14926
15065
  return;
14927
15066
  }
@@ -14932,12 +15071,12 @@ class AuthBroker {
14932
15071
  return;
14933
15072
  }
14934
15073
  const expiresAt = credentials.microsoftOauth?.expiresAt;
14935
- this.audit({ op: "add-account", identity: identity2, account: label, accountKind: "microsoft", ok: true, replace });
15074
+ this.audit({ op: "add-account", identity, account: label, accountKind: "microsoft", ok: true, replace });
14936
15075
  socket.write(encodeSuccess(id, { label, expiresAt }));
14937
15076
  }
14938
- async opMicrosoftRmAccount(socket, id, identity2, label) {
14939
- if (!this.isAdmin(identity2)) {
14940
- this.audit({ op: "rm-account", identity: identity2, account: label, accountKind: "microsoft", ok: false, error: "FORBIDDEN" });
15077
+ async opMicrosoftRmAccount(socket, id, identity, label) {
15078
+ if (!this.isAdmin(identity)) {
15079
+ this.audit({ op: "rm-account", identity, account: label, accountKind: "microsoft", ok: false, error: "FORBIDDEN" });
14941
15080
  this.respondForbidden(socket, id, "rm-account requires admin");
14942
15081
  return;
14943
15082
  }
@@ -14963,10 +15102,10 @@ class AuthBroker {
14963
15102
  socket.write(encodeError(id, "INTERNAL", err.message));
14964
15103
  return;
14965
15104
  }
14966
- this.audit({ op: "rm-account", identity: identity2, account: label, accountKind: "microsoft", ok: true });
15105
+ this.audit({ op: "rm-account", identity, account: label, accountKind: "microsoft", ok: true });
14967
15106
  socket.write(encodeSuccess(id, { label }));
14968
15107
  }
14969
- async opListMicrosoftAccounts(socket, id, identity2) {
15108
+ async opListMicrosoftAccounts(socket, id, identity) {
14970
15109
  const accounts = listMicrosoftAccounts(this.stateDir).map((account) => {
14971
15110
  const creds = readMicrosoftAccountCredentials(this.stateDir, account);
14972
15111
  if (!creds)
@@ -14979,12 +15118,12 @@ class AuthBroker {
14979
15118
  accountType: creds.microsoftOauth.accountType
14980
15119
  };
14981
15120
  }).filter((entry) => entry !== null).sort((a, b) => a.account.localeCompare(b.account));
14982
- this.audit({ op: "list-microsoft-accounts", identity: identity2, accountKind: "microsoft", ok: true });
15121
+ this.audit({ op: "list-microsoft-accounts", identity, accountKind: "microsoft", ok: true });
14983
15122
  socket.write(encodeSuccess(id, { accounts }));
14984
15123
  }
14985
- async opSetOverride(socket, id, identity2, agentName, account) {
14986
- if (!this.isAdmin(identity2)) {
14987
- this.audit({ op: "set-override", identity: identity2, account: account ?? undefined, accountKind: "claude", ok: false, error: "FORBIDDEN" });
15124
+ async opSetOverride(socket, id, identity, agentName, account) {
15125
+ if (!this.isAdmin(identity)) {
15126
+ this.audit({ op: "set-override", identity, account: account ?? undefined, accountKind: "claude", ok: false, error: "FORBIDDEN" });
14988
15127
  this.respondForbidden(socket, id, "set-override requires admin");
14989
15128
  return;
14990
15129
  }
@@ -15006,7 +15145,7 @@ class AuthBroker {
15006
15145
  agents[agentName] = { ...cur, auth };
15007
15146
  this.config = { ...this.config, agents };
15008
15147
  this.fanoutForAgent(agentName);
15009
- this.audit({ op: "set-override", identity: identity2, account: account ?? undefined, accountKind: "claude", ok: true });
15148
+ this.audit({ op: "set-override", identity, account: account ?? undefined, accountKind: "claude", ok: true });
15010
15149
  socket.write(encodeSuccess(id, { agent: agentName, account }));
15011
15150
  }
15012
15151
  async refreshTick() {
@@ -15187,12 +15326,15 @@ class AuthBroker {
15187
15326
  for (const consumer of this.config.auth?.consumers ?? []) {
15188
15327
  if (!consumer.mirror_dir)
15189
15328
  continue;
15190
- const isPinned = consumer.account === label;
15329
+ const bound = consumer.account ?? this.config.auth?.active;
15330
+ const isPinned = bound === label;
15191
15331
  const effective = this.servingAccountForConsumer(consumer.name);
15192
15332
  const isEffective = effective === label;
15193
15333
  if (!isPinned && !isEffective)
15194
15334
  continue;
15195
- const toMirror = effective ?? consumer.account;
15335
+ const toMirror = effective ?? bound;
15336
+ if (toMirror == null)
15337
+ continue;
15196
15338
  if (this.mirrorAccountToConsumer(toMirror, consumer)) {
15197
15339
  fanned.push(consumer.name);
15198
15340
  }
@@ -15216,7 +15358,7 @@ class AuthBroker {
15216
15358
  const c = (this.config.auth?.consumers ?? []).find((x) => x.name === name);
15217
15359
  if (!c)
15218
15360
  return null;
15219
- return this.accountWithFailover(c.account);
15361
+ return this.accountWithFailover(c.account ?? this.config.auth?.active);
15220
15362
  }
15221
15363
  mirrorAccountToConsumer(label, consumer) {
15222
15364
  const mirrorDir = consumer.mirror_dir;
@@ -15362,6 +15504,42 @@ class AuthBroker {
15362
15504
  this.thresholdViolations = this.readJson("threshold-violations.json") ?? {};
15363
15505
  this.notificationClaims = this.readJson("notification-claims.json") ?? {};
15364
15506
  this.lastQuotaCache = this.readJson("last-quota.json") ?? {};
15507
+ this.applyActiveOverride();
15508
+ }
15509
+ applyActiveOverride() {
15510
+ const ov = this.readJson("active-override.json");
15511
+ if (!ov || typeof ov.active !== "string" || ov.active.length === 0)
15512
+ return;
15513
+ const yamlActive = this.yamlActive ?? null;
15514
+ if ((ov.yaml_active_at_write ?? null) !== yamlActive) {
15515
+ process.stdout.write(`auth-broker: active-override dropped — yaml auth.active changed since the swap ` + `(${ov.yaml_active_at_write ?? "unset"} → ${yamlActive ?? "unset"}); yaml wins
15516
+ `);
15517
+ try {
15518
+ unlinkSync(join4(this.stateDir, "active-override.json"));
15519
+ } catch {}
15520
+ return;
15521
+ }
15522
+ if (!accountExists(ov.active, this.home)) {
15523
+ process.stdout.write(`auth-broker: active-override ignored — account '${ov.active}' not found on disk
15524
+ `);
15525
+ return;
15526
+ }
15527
+ if (ov.active === this.config.auth?.active)
15528
+ return;
15529
+ this.config = {
15530
+ ...this.config,
15531
+ auth: { ...this.config.auth ?? {}, active: ov.active }
15532
+ };
15533
+ process.stdout.write(`auth-broker: active-override applied — auth.active ${yamlActive ?? "unset"} → ${ov.active} ` + `(persisted swap survives restarts)
15534
+ `);
15535
+ }
15536
+ persistActiveOverride(active) {
15537
+ const entry = {
15538
+ active,
15539
+ yaml_active_at_write: this.yamlActive ?? null,
15540
+ updated_at: this.now()
15541
+ };
15542
+ atomicWriteJsonSync(join4(this.stateDir, "active-override.json"), entry, 384);
15365
15543
  }
15366
15544
  readJson(name) {
15367
15545
  const p = join4(this.stateDir, name);