openmausbot 0.1.85 → 0.1.86

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 (193) hide show
  1. package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  2. package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  3. package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  4. package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  5. package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  6. package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  7. package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  8. package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  9. package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  10. package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  11. package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  12. package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  13. package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  14. package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  15. package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  16. package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  17. package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  18. package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  19. package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  20. package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  21. package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  22. package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  23. package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  24. package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  25. package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  26. package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  27. package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  28. package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  29. package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  30. package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  31. package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  32. package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  33. package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  34. package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  35. package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  36. package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  37. package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  38. package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  39. package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  40. package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  41. package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  42. package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  43. package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  44. package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  45. package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  46. package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  47. package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  48. package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  49. package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  50. package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  51. package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  52. package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  53. package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  54. package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  55. package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  56. package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  57. package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  58. package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  59. package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  60. package/dist/assets/abnfDiagram-O67JEVCF-DpUZNnFD.js +1 -0
  61. package/dist/assets/arc-DW7mrI4p.js +1 -0
  62. package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
  63. package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
  64. package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
  65. package/dist/assets/channel-BXFkgus9.js +1 -0
  66. package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
  67. package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
  68. package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
  69. package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
  70. package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
  71. package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
  72. package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
  73. package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
  74. package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
  75. package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
  76. package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
  77. package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
  78. package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
  79. package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
  80. package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
  81. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  82. package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
  83. package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
  84. package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
  85. package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
  86. package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
  87. package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
  88. package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
  89. package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
  90. package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
  91. package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
  92. package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
  93. package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
  94. package/dist/assets/graph-D8UmR1vK.js +1 -0
  95. package/dist/assets/index-BGgqlWPJ.css +1 -0
  96. package/dist/assets/index-BtHo8ZKl.js +577 -0
  97. package/dist/assets/{index-CTNNoqSJ.js → index-h9FFNwTu.js} +1 -1
  98. package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
  99. package/dist/assets/init-Gi6I4Gst.js +1 -0
  100. package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
  101. package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
  102. package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
  103. package/dist/assets/linear-3su2TG93.js +1 -0
  104. package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
  105. package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
  106. package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  107. package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
  108. package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
  109. package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
  110. package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
  111. package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
  112. package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
  113. package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
  114. package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
  115. package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
  116. package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
  117. package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
  118. package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
  119. package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
  120. package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
  121. package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
  122. package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
  123. package/dist/index.html +2 -2
  124. package/dist-server/container-mcp.js +19 -10
  125. package/dist-server/drivers/agents-proxy.js +16159 -1596
  126. package/dist-server/enterprise/LICENSE +32 -0
  127. package/dist-server/enterprise/server/index.js +5 -5
  128. package/dist-server/index.js +4779 -1667
  129. package/dist-server/mcp-server.js +3 -1
  130. package/dist-server/openmausbot.js +4679 -4266
  131. package/dist-server/pair-cli.js +4679 -4266
  132. package/dist-server/server/admin-activity.js +410 -0
  133. package/dist-server/server/bot-visibility.js +462 -0
  134. package/dist-server/server/box.js +2 -1
  135. package/dist-server/server/browser-runtime.js +1 -1
  136. package/dist-server/server/channel-queue.js +2 -0
  137. package/dist-server/server/cli-prompts.js +24 -2
  138. package/dist-server/server/cli.js +79 -10
  139. package/dist-server/server/composio.js +49 -11
  140. package/dist-server/server/config.js +79 -13
  141. package/dist-server/server/decision-log.js +209 -48
  142. package/dist-server/server/default-model-selection.js +20 -3
  143. package/dist-server/server/delegations.js +11 -9
  144. package/dist-server/server/drivers/acp/core.js +110 -22
  145. package/dist-server/server/drivers/acp/grok.js +4 -3
  146. package/dist-server/server/drivers/agents-call.js +1123 -0
  147. package/dist-server/server/drivers/agents-catalog.js +659 -0
  148. package/dist-server/server/drivers/agents-client.js +28 -0
  149. package/dist-server/server/drivers/agents-proxy.js +18 -1722
  150. package/dist-server/server/drivers/chat-mcp-tools.js +17 -2
  151. package/dist-server/server/drivers/claude.js +21 -6
  152. package/dist-server/server/drivers/codex-mcp-names.js +44 -0
  153. package/dist-server/server/drivers/codex.js +26 -4
  154. package/dist-server/server/drivers/grok.js +2 -1
  155. package/dist-server/server/drivers/openai-compat.js +15 -8
  156. package/dist-server/server/enterprise.js +97 -16
  157. package/dist-server/server/external-runtime.js +83 -0
  158. package/dist-server/server/guarded-requests.js +60 -0
  159. package/dist-server/server/harness/http.js +61 -0
  160. package/dist-server/server/hosted-models.js +151 -0
  161. package/dist-server/server/hosted-slack.js +11 -0
  162. package/dist-server/server/index.js +1718 -348
  163. package/dist-server/server/managed-desktop.js +79 -14
  164. package/dist-server/server/managed-policy.js +153 -0
  165. package/dist-server/server/message-db.js +28 -0
  166. package/dist-server/server/model-context-window.js +7 -0
  167. package/dist-server/server/model-prices.js +121 -0
  168. package/dist-server/server/notify.js +10 -1
  169. package/dist-server/server/peer-roster.js +13 -2
  170. package/dist-server/server/recent-work.js +15 -0
  171. package/dist-server/server/request-auth.js +96 -0
  172. package/dist-server/server/room-handoffs.js +25 -11
  173. package/dist-server/server/routes/hosted-slack.js +20 -0
  174. package/dist-server/server/routes/table.js +14 -0
  175. package/dist-server/server/routines.js +22 -0
  176. package/dist-server/server/spend.js +111 -16
  177. package/dist-server/server/steer-queue.js +6 -1
  178. package/dist-server/server/store.js +292 -21
  179. package/dist-server/server/team-backup.js +10 -1
  180. package/dist-server/server/thread-starters.js +53 -0
  181. package/dist-server/server/usage-ledger.js +31 -8
  182. package/dist-server/server/workspace-backup-policy.js +9 -2
  183. package/dist-server/server/workspace-backup.js +7 -4
  184. package/dist-server/shared/mention-boundary.js +15 -0
  185. package/dist-server/shared/routines.js +7 -1
  186. package/dist-server/shared/team-backup.js +2 -0
  187. package/dist-server/src/lib/composer-attachments.js +16 -0
  188. package/dist-server/vps-container-mcp.js +19 -10
  189. package/enterprise/README.md +33 -12
  190. package/enterprise/server/index.js +5 -5
  191. package/package.json +1 -1
  192. package/dist/assets/index-Ba9G44HI.js +0 -310
  193. package/dist/assets/index-CkRNp7wX.css +0 -1
@@ -0,0 +1,410 @@
1
+ // The admin activity log: who changed what on this server — settings,
2
+ // people, sessions, webhooks, MCP servers, engines, bots, budgets and bot
3
+ // visibility — written beside the decision log and kept as long.
4
+ //
5
+ // One file per UTC month under <data>/admin-activity/YYYY-MM.ndjson, 0600,
6
+ // pruned by the decision log's retention window (decision-log.ts
7
+ // `decisions.retentionDays` / OMB_DECISION_RETENTION_DAYS), so one setting
8
+ // keeps both. Each row names who acted (a signed-in session, the owner on
9
+ // this machine, a local service, or the command line), what changed (the
10
+ // setting paths), and the values before and after — redacted: a value under
11
+ // a key that names a credential, and every value in a headers or environment
12
+ // map, is written as "[hidden]", and every other string goes through the
13
+ // same content redaction the decision log uses. Nothing here is sent
14
+ // anywhere; the organisation's cloud Admin keeps its own log.
15
+ //
16
+ // Fire-and-forget, like the decision log: a full disk must not undo a
17
+ // setting that already took effect.
18
+ import { readdirSync, readFileSync } from "node:fs";
19
+ import { appendFile, mkdir } from "node:fs/promises";
20
+ import { basename, join } from "node:path";
21
+ import { actorLabel, boundRetentionDays, pruneMonthFiles } from "./decision-log.js";
22
+ import { redactSecrets } from "./redact.js";
23
+ import { csvCell } from "./usage-ledger.js";
24
+ export const ADMIN_ACTIVITY_CATEGORIES = ["config", "people", "session", "webhook", "mcp", "engine", "bot", "budget", "visibility"];
25
+ const DIR = "admin-activity";
26
+ const MONTH_FILE = /^(\d{4})-(\d{2})\.ndjson$/;
27
+ const PRUNE_EVERY_MS = 60 * 60_000;
28
+ const writeQueues = new Map();
29
+ const lastPrune = new Map();
30
+ function monthKey(at) {
31
+ return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
32
+ }
33
+ export function adminActivityFileFor(dataDir, at) {
34
+ return join(dataDir, DIR, `${monthKey(at)}.ndjson`);
35
+ }
36
+ async function writeRow(dataDir, row) {
37
+ const at = new Date(row.at);
38
+ await mkdir(join(dataDir, DIR), { recursive: true, mode: 0o700 });
39
+ await appendFile(adminActivityFileFor(dataDir, at), JSON.stringify(row) + "\n", { mode: 0o600 });
40
+ if (at.getTime() - (lastPrune.get(dataDir) ?? 0) >= PRUNE_EVERY_MS) {
41
+ lastPrune.set(dataDir, at.getTime());
42
+ await pruneMonthFiles(join(dataDir, DIR), boundRetentionDays(), at);
43
+ }
44
+ }
45
+ /** Append one row. Its values are redacted here, at the moment of writing. */
46
+ export function appendAdminAction(dataDir, row) {
47
+ const record = {
48
+ ...row,
49
+ at: row.at ?? new Date().toISOString(),
50
+ ...(row.before ? { before: auditValues(row.before) } : {}),
51
+ ...(row.after ? { after: auditValues(row.after) } : {}),
52
+ };
53
+ const previous = writeQueues.get(dataDir) ?? Promise.resolve();
54
+ const queued = previous.then(() => writeRow(dataDir, record)).catch(() => {
55
+ /* an audit log must never take down the change it records */
56
+ });
57
+ writeQueues.set(dataDir, queued);
58
+ void queued.finally(() => {
59
+ if (writeQueues.get(dataDir) === queued)
60
+ writeQueues.delete(dataDir);
61
+ });
62
+ }
63
+ /** Delete the months the retention window no longer covers. Never throws.
64
+ * Writing prunes too; the server also runs this on a timer for quiet days. */
65
+ export function pruneAdminActivity(dataDir, days = boundRetentionDays(), now = new Date()) {
66
+ return pruneMonthFiles(join(dataDir, DIR), days, now);
67
+ }
68
+ /** Test/shutdown seam: wait for every queued row to reach disk. */
69
+ export async function flushAdminActivity(dataDir) {
70
+ await writeQueues.get(dataDir);
71
+ }
72
+ // ── redaction ───────────────────────────────────────────────────────────
73
+ /** Name parts that mark a credential (case-insensitive substring), as in
74
+ * redact.ts, plus `key` standing alone or as a suffix (apiKey, xai_key). */
75
+ const SECRET_PARTS = ["token", "secret", "password", "passwd", "passphrase", "credential", "authorization", "cookie", "bearer", "apikey", "api_key"];
76
+ /** Maps whose every value may be a credential: keep the names, hide values. */
77
+ const OPAQUE_MAP = /^(?:headers|env|environment)$/i;
78
+ const MAX_VALUE_CHARS = 2_000;
79
+ const HIDDEN = "[hidden]";
80
+ function isSecretName(name) {
81
+ const lower = name.toLowerCase();
82
+ // Counts, not credentials: maxTokens, contextTokens.
83
+ if (lower.endsWith("tokens"))
84
+ return false;
85
+ if (SECRET_PARTS.some((part) => lower.includes(part)))
86
+ return true;
87
+ return /(?:^|[_.-])keys?$/.test(lower) || /[a-z]Keys?$/.test(name);
88
+ }
89
+ /** A command-line flag or URL parameter that names a credential:
90
+ * `--api-key`, `--token=…`, `?key=`, `&access_token=`, `?sig=`. */
91
+ function isSecretParameter(name) {
92
+ // `-k` is the usual short form of --key (curl-style CLIs and many MCP servers).
93
+ if (name === "-k" || name === "-K")
94
+ return true;
95
+ const bare = name.replace(/^-+/, "");
96
+ return isSecretName(bare) || /^(?:auth|sig|signature|pass|pwd|code)$/i.test(bare);
97
+ }
98
+ /** A path segment that looks like a key rather than a name: it holds a run
99
+ * of 16 or more letters and digits, mixed (Zapier-style `/s/<key>/sse`, a
100
+ * Slack hook's last part) — a readable slug like `getting-started-2024` or a
101
+ * UUID does not. */
102
+ function keyLikeSegment(segment) {
103
+ return (segment.match(/[A-Za-z0-9]{16,}/g) ?? []).some((run) => /\d/.test(run) && /[A-Za-z]/.test(run));
104
+ }
105
+ /** In a URL: credentials before the host (`user:pass@`, or a bare token
106
+ * `TOKEN@`), key-like path segments, and query or fragment values whose
107
+ * names mark a credential. */
108
+ function maskUrlSecrets(text) {
109
+ return text.replace(/\b([a-z][\w+.-]*:\/\/)([^\s/?#]*)([^\s?#]*)([^\s]*)/gi, (_all, scheme, authority, path, rest) => {
110
+ const at = authority.lastIndexOf("@");
111
+ const host = at >= 0 ? `${HIDDEN}@${authority.slice(at + 1)}` : authority;
112
+ const maskedPath = path.split("/").map((segment) => (keyLikeSegment(segment) ? HIDDEN : segment)).join("/");
113
+ const maskedRest = rest.replace(/([?&;#])([^=&#\s]+)=([^&#\s]+)/g, (all, separator, name) => (isSecretParameter(name) ? `${separator}${name}=${HIDDEN}` : all));
114
+ return `${scheme}${host}${maskedPath}${maskedRest}`;
115
+ });
116
+ }
117
+ /** An argument list: the value after a credential flag, or after its `=`. */
118
+ function maskArgs(items) {
119
+ return items.map((item, index) => {
120
+ const previous = items[index - 1];
121
+ if (typeof item !== "string")
122
+ return item;
123
+ if (typeof previous === "string" && /^--?[\w-]+$/.test(previous) && isSecretParameter(previous))
124
+ return HIDDEN;
125
+ const inline = /^(--?[\w-]+)=(.+)$/.exec(item);
126
+ return inline && isSecretParameter(inline[1]) ? `${inline[1]}=${HIDDEN}` : item;
127
+ });
128
+ }
129
+ function scrub(value, depth, hideAll) {
130
+ if (value === undefined || value === null || value === "")
131
+ return value;
132
+ if (hideAll && typeof value !== "object")
133
+ return HIDDEN;
134
+ if (typeof value === "string")
135
+ return redactSecrets(maskUrlSecrets(value));
136
+ if (typeof value !== "object")
137
+ return value;
138
+ if (depth > 6)
139
+ return "[…]";
140
+ if (Array.isArray(value))
141
+ return maskArgs(value.slice(0, 100)).map((item) => scrub(item, depth + 1, hideAll));
142
+ const out = {};
143
+ for (const [key, child] of Object.entries(value)) {
144
+ if (isSecretName(key) && child !== undefined && child !== null && child !== "")
145
+ out[key] = HIDDEN;
146
+ else
147
+ out[key] = scrub(child, depth + 1, hideAll || OPAQUE_MAP.test(key));
148
+ }
149
+ return out;
150
+ }
151
+ /** A path's last segment decides, too: `anthropic.key` is a key. A value
152
+ * too large to be useful in an audit row is summarised, not stored. */
153
+ export function auditValues(values) {
154
+ const out = {};
155
+ for (const [path, value] of Object.entries(values)) {
156
+ const segments = path.split(".");
157
+ const last = segments.at(-1) ?? path;
158
+ const opaque = segments.slice(0, -1).some((segment) => OPAQUE_MAP.test(segment));
159
+ const scrubbed = (isSecretName(last) || opaque) && value !== undefined && value !== null && value !== ""
160
+ ? HIDDEN
161
+ : scrub(value, 0, OPAQUE_MAP.test(last));
162
+ const text = JSON.stringify(scrubbed);
163
+ out[path] = text !== undefined && text.length > MAX_VALUE_CHARS ? "[large value]" : scrubbed ?? null;
164
+ }
165
+ return out;
166
+ }
167
+ // ── diffs ───────────────────────────────────────────────────────────────
168
+ const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
169
+ /** Key-order-insensitive JSON, for comparing two readings of one setting. */
170
+ function stable(value) {
171
+ if (Array.isArray(value))
172
+ return `[${value.map(stable).join(",")}]`;
173
+ if (isPlainObject(value))
174
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stable(value[key])}`).join(",")}}`;
175
+ return JSON.stringify(value) ?? "undefined";
176
+ }
177
+ /** Dot paths (up to `depth` segments) whose values differ. A section that
178
+ * appears or disappears is followed into, so a new key reads as the key. */
179
+ export function changedPaths(before, after, depth = 3, prefix = "") {
180
+ if (stable(before) === stable(after))
181
+ return [];
182
+ if (before === undefined && isPlainObject(after))
183
+ before = {};
184
+ if (after === undefined && isPlainObject(before))
185
+ after = {};
186
+ if (depth > 0 && isPlainObject(before) && isPlainObject(after)) {
187
+ const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
188
+ return keys.flatMap((key) => changedPaths(before[key], after[key], depth - 1, prefix ? `${prefix}.${key}` : key));
189
+ }
190
+ return [prefix || "(all)"];
191
+ }
192
+ export function valueAt(source, path) {
193
+ let value = source;
194
+ for (const segment of path.split(".")) {
195
+ if (!isPlainObject(value))
196
+ return undefined;
197
+ value = value[segment];
198
+ }
199
+ return value;
200
+ }
201
+ const pick = (source, paths) => Object.fromEntries(paths.map((path) => [path, valueAt(source, path) ?? null]));
202
+ /** Which log category a top-level config.json key belongs to. */
203
+ const CONFIG_CATEGORY = {
204
+ signIn: "people",
205
+ budgets: "budget",
206
+ billing: "budget",
207
+ mcpServers: "mcp",
208
+ instances: "engine",
209
+ anthropic: "engine",
210
+ openaiCompat: "engine",
211
+ xai: "engine",
212
+ opencodeGo: "engine",
213
+ defaultModelSelection: "engine",
214
+ };
215
+ /** The rows one change to config.json makes: one per category it touched,
216
+ * each naming the paths that changed and their redacted values. */
217
+ export function configChangeRows(before, after) {
218
+ const paths = changedPaths(isPlainObject(before) ? before : {}, isPlainObject(after) ? after : {});
219
+ const byCategory = new Map();
220
+ for (const path of paths) {
221
+ const category = CONFIG_CATEGORY[path.split(".")[0]] ?? "config";
222
+ byCategory.set(category, [...(byCategory.get(category) ?? []), path]);
223
+ }
224
+ return [...byCategory].map(([category, changed]) => ({
225
+ category,
226
+ action: `${category}.update`,
227
+ target: { kind: "settings" },
228
+ changed,
229
+ before: pick(before, changed),
230
+ after: pick(after, changed),
231
+ }));
232
+ }
233
+ /** The fields of a bot an audit follows: what it may do, where, and who may
234
+ * see it. Display fields (colour, unread, pins) are not audited. */
235
+ export const BOT_AUDIT_FIELDS = [
236
+ "approvalMode", "autoApprove", "alwaysAllow", "peers", "approvePeerComms", "computer", "cloudBackend", "autoStartVps",
237
+ "cwd", "composio", "browser", "browserProfile", "mcpServers", "chiefOfStaff", "managedSections", "section",
238
+ "parkDirectMessages", "hidden", "modelSelection", "visibility",
239
+ ];
240
+ export function botAuditSnapshot(bot) {
241
+ if (!bot)
242
+ return null;
243
+ // "everyone" and no field are one audience.
244
+ const present = (field) => bot[field] !== undefined && !(field === "visibility" && bot[field] === "everyone");
245
+ return Object.fromEntries(BOT_AUDIT_FIELDS.filter(present).map((field) => [field, structuredClone(bot[field])]));
246
+ }
247
+ /** A bot's audited fields that changed: a `visibility.update` row when its
248
+ * audience changed, a `bot.update` row for everything else. `only` limits
249
+ * the comparison to the fields a request named. */
250
+ export function botChangeRows(target, before, after, only) {
251
+ if (!before || !after)
252
+ return [];
253
+ const changed = BOT_AUDIT_FIELDS.filter((field) => (!only || only.includes(field)) && stable(before[field]) !== stable(after[field]));
254
+ const rows = [];
255
+ const bot = { kind: "bot", id: target.id, ...(target.name ? { name: target.name } : {}) };
256
+ if (changed.includes("visibility")) {
257
+ // An absent audience is the default: everyone.
258
+ const audience = (value) => value ?? "everyone";
259
+ rows.push({
260
+ category: "visibility", action: "visibility.update", target: bot, changed: ["visibility"],
261
+ before: { visibility: audience(before.visibility) }, after: { visibility: audience(after.visibility) },
262
+ });
263
+ }
264
+ const rest = changed.filter((field) => field !== "visibility");
265
+ if (rest.length)
266
+ rows.push({ category: "bot", action: "bot.update", target: bot, changed: rest, before: pick(before, rest), after: pick(after, rest) });
267
+ return rows;
268
+ }
269
+ /** An email sign-in list that lets more than one person in: any member, a
270
+ * second admin, or a whole @domain. */
271
+ export function sharedSignIn(lists) {
272
+ return lists.members.length > 0 || lists.admins.length > 1 || [...lists.admins, ...lists.members].some((entry) => entry.trim().startsWith("@"));
273
+ }
274
+ /** The sign-in lists in a config.json reading. */
275
+ export function signInListsOf(config) {
276
+ const signIn = isPlainObject(config) && isPlainObject(config.signIn) ? config.signIn : {};
277
+ const list = (value) => (Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : []);
278
+ return { admins: list(signIn.admins), members: list(signIn.members) };
279
+ }
280
+ // ── reading ─────────────────────────────────────────────────────────────
281
+ const isRow = (value) => isPlainObject(value) &&
282
+ typeof value.at === "string" &&
283
+ typeof value.category === "string" &&
284
+ typeof value.action === "string" &&
285
+ isPlainObject(value.actor);
286
+ function readRows(path) {
287
+ let text;
288
+ try {
289
+ text = readFileSync(path, "utf8");
290
+ }
291
+ catch {
292
+ return [];
293
+ }
294
+ const rows = [];
295
+ for (const line of text.split("\n")) {
296
+ if (!line)
297
+ continue;
298
+ try {
299
+ const value = JSON.parse(line);
300
+ if (isRow(value))
301
+ rows.push(value);
302
+ }
303
+ catch {
304
+ /* a line torn mid-write */
305
+ }
306
+ }
307
+ return rows;
308
+ }
309
+ /** Every row inside the range, oldest first. */
310
+ export function readAdminActivityRange(dataDir, range) {
311
+ let months = [];
312
+ try {
313
+ months = readdirSync(join(dataDir, DIR)).filter((name) => MONTH_FILE.test(name)).sort();
314
+ }
315
+ catch {
316
+ return [];
317
+ }
318
+ const first = monthKey(range.from);
319
+ const last = monthKey(range.to);
320
+ const from = range.from.getTime();
321
+ const to = range.to.getTime();
322
+ const rows = [];
323
+ for (const name of months) {
324
+ const month = basename(name, ".ndjson");
325
+ if (month < first || month > last)
326
+ continue;
327
+ for (const row of readRows(join(dataDir, DIR, name))) {
328
+ const at = Date.parse(row.at);
329
+ if (at >= from && at <= to)
330
+ rows.push(row);
331
+ }
332
+ }
333
+ return rows;
334
+ }
335
+ // ── the Activity view: admin actions and approvals together ─────────────
336
+ export function adminActorLabel(actor) {
337
+ if (actor?.kind === "cli")
338
+ return "Command line";
339
+ return actorLabel(actor);
340
+ }
341
+ export function parseActivityWhat(value) {
342
+ if (!value)
343
+ return "all";
344
+ if (value === "all" || value === "approvals" || value === "decisions")
345
+ return value;
346
+ return ADMIN_ACTIVITY_CATEGORIES.includes(value) ? value : null;
347
+ }
348
+ /** Both logs as one list, newest first, narrowed by who (a case-insensitive
349
+ * part of the name, email or label shown) and what. */
350
+ export function activityEntries(decisions, actions, filter) {
351
+ const who = filter.who?.trim().toLowerCase() ?? "";
352
+ const entries = [];
353
+ const wantDecision = (row) => filter.what === "decisions" || ((filter.what === "all" || filter.what === "approvals") && row.source === "user");
354
+ for (const row of decisions) {
355
+ if (!wantDecision(row))
356
+ continue;
357
+ const clean = redactSecrets(row);
358
+ entries.push({
359
+ type: "approval",
360
+ at: clean.at,
361
+ who: clean.actor ? actorLabel(clean.actor) : "",
362
+ what: clean.decision,
363
+ source: clean.source,
364
+ ...(clean.botName || clean.botId ? { bot: clean.botName || clean.botId } : {}),
365
+ ...(clean.tool ? { tool: clean.tool } : {}),
366
+ ...(clean.summary ? { summary: clean.summary } : {}),
367
+ threadId: clean.threadId,
368
+ ...(clean.requestId ? { requestId: clean.requestId } : {}),
369
+ });
370
+ }
371
+ if (filter.what !== "approvals" && filter.what !== "decisions") {
372
+ for (const row of actions) {
373
+ if (filter.what !== "all" && row.category !== filter.what)
374
+ continue;
375
+ entries.push({
376
+ type: "admin",
377
+ at: row.at,
378
+ who: adminActorLabel(row.actor),
379
+ what: row.category,
380
+ action: row.action,
381
+ ...(row.target ? { target: row.target } : {}),
382
+ ...(row.changed ? { changed: row.changed } : {}),
383
+ ...(row.before ? { before: row.before } : {}),
384
+ ...(row.after ? { after: row.after } : {}),
385
+ });
386
+ }
387
+ }
388
+ const matched = who ? entries.filter((entry) => entry.who.toLowerCase().includes(who)) : entries;
389
+ return matched.sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
390
+ }
391
+ /** One line per entry. Cells go through the usage CSV's formula
392
+ * neutralising; values were redacted when the rows were written. */
393
+ export function activityCsv(entries) {
394
+ const header = ["time", "type", "who", "what", "action", "target", "changed", "before", "after", "bot", "tool", "summary", "thread"];
395
+ const lines = [header.join(",")];
396
+ for (const entry of entries) {
397
+ const cells = entry.type === "approval"
398
+ ? [entry.at, "approval", entry.who, entry.what, entry.source, "", "", "", "", entry.bot ?? "", entry.tool ?? "", entry.summary ?? "", entry.threadId]
399
+ : [
400
+ entry.at, "admin", entry.who, entry.what, entry.action,
401
+ entry.target ? entry.target.name ?? entry.target.id ?? entry.target.kind : "",
402
+ (entry.changed ?? []).join(" "),
403
+ entry.before ? JSON.stringify(entry.before) : "",
404
+ entry.after ? JSON.stringify(entry.after) : "",
405
+ "", "", "", "",
406
+ ];
407
+ lines.push(cells.map(csvCell).join(","));
408
+ }
409
+ return lines.join("\n") + "\n";
410
+ }