openmausbot 0.1.94 → 0.1.96

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 (188) hide show
  1. package/dist/assets/{DesktopViewer-bxM7NV3N.js → DesktopViewer-DsnA23KM.js} +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-BhMdJhRJ.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
  3. package/dist/assets/{arc-B5itKDcx.js → arc-Duj5Coy2.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-C5d7UY6U.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-BrZIhMmu.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-7I0ArfbD.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
  7. package/dist/assets/channel-BZxwC-Gw.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-BnEsyN36.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-BXATQ52s.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B0BpTkEx.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-BtLTz6FR.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-D6gcZOYt.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-DCLrosvf.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-B9JsknV9.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-CsoTxw7z.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-0F_frB0c.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-DkrpnflX.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-BNDwYBt9.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-JUkNr1Mk.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-DKeh9N4M.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-ve-6T_wX.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-BuJ2d8rT.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BgOs_Ni-.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-DMK6VmcP.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-DksASlcY.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-Bgn8I5k6.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-zK0jqO8N.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-vKrbzTwr.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-Bzv2mPNf.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-CvOyYuxu.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-CtbnDbc0.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-c4dMRYyG.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-B34uaQho.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
  34. package/dist/assets/{index-CPiqc_DN.js → index-DreDnjOu.js} +1 -1
  35. package/dist/assets/index-Du7o6Dsf.js +610 -0
  36. package/dist/assets/index-KxG4bWSr.css +1 -0
  37. package/dist/assets/{infoDiagram-VRGFBTTK-o64ma4CI.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DV4C9BAm.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-Bf0AHk1H.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-B1UMSKmE.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
  41. package/dist/assets/{linear-BpKhuyLm.js → linear-Bl669148.js} +1 -1
  42. package/dist/assets/{mermaid.core-B_BKNCw5.js → mermaid.core-86Xqz5Mr.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-DeARruT1.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-BOQEk_1D.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-EIfoiTZo.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-GgZwT-fi.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-D9rDQomj.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DAh1MIeo.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-CPr6Uz64.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-BU6VIdcm.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-qF2HO41r.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-DlJ8G4MD.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-DzYE8I54.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-B7wzEQ0A.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-BvGgrxIP.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-CEECdgQg.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-XQHA0P_P.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-B5mLcZI2.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +27 -13
  62. package/dist-server/companion/src/listener.js +39 -18
  63. package/dist-server/companion/src/routes.js +39 -0
  64. package/dist-server/container-mcp.js +16 -29
  65. package/dist-server/drivers/agents-proxy.js +161 -82
  66. package/dist-server/enterprise/server/index.js +2 -1
  67. package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +34 -19
  68. package/dist-server/index.js +4991 -4908
  69. package/dist-server/local-computer.js +2 -1
  70. package/dist-server/openmausbot.js +906 -1785
  71. package/dist-server/pair-cli.js +906 -1785
  72. package/dist-server/proxy-paths.js +2 -1
  73. package/dist-server/server/account-signin.js +0 -2
  74. package/dist-server/server/activity.js +201 -0
  75. package/dist-server/server/admin-activity.js +1 -4
  76. package/dist-server/server/auto-approve.js +25 -4
  77. package/dist-server/server/automatic-recovery.js +1 -1
  78. package/dist-server/server/boat-create-idempotency.js +0 -7
  79. package/dist-server/server/boat-delete-journal.js +0 -4
  80. package/dist-server/server/boat.js +4 -4
  81. package/dist-server/server/bot-package.js +1 -19
  82. package/dist-server/server/browser-runtime.js +1 -2
  83. package/dist-server/server/checkpoints.js +90 -148
  84. package/dist-server/server/chief-of-staff.js +9 -9
  85. package/dist-server/server/cli.js +33 -26
  86. package/dist-server/server/cloud-backend.js +0 -4
  87. package/dist-server/server/cloud-computer-tools.js +110 -0
  88. package/dist-server/server/cloud-home-start.js +12 -15
  89. package/dist-server/server/cloud-home.js +0 -5
  90. package/dist-server/server/cloud-lending.js +0 -3
  91. package/dist-server/server/cloud-move-http.js +44 -30
  92. package/dist-server/server/cloud-move.js +20 -18
  93. package/dist-server/server/composio.js +41 -5
  94. package/dist-server/server/computer-control.js +0 -15
  95. package/dist-server/server/computer-observation.js +0 -74
  96. package/dist-server/server/config.js +121 -65
  97. package/dist-server/server/container-computer.js +23 -24
  98. package/dist-server/server/control-client.js +4 -10
  99. package/dist-server/server/decider/jev.js +1 -4
  100. package/dist-server/server/decider/log.js +1 -3
  101. package/dist-server/server/decision-log.js +1 -4
  102. package/dist-server/server/delegations.js +3 -2
  103. package/dist-server/server/drivers/acp/core.js +11 -8
  104. package/dist-server/server/drivers/acp/opencode-go.js +17 -16
  105. package/dist-server/server/drivers/agents-call.js +88 -92
  106. package/dist-server/server/drivers/agents-catalog.js +90 -11
  107. package/dist-server/server/drivers/agents-proxy.js +10 -31
  108. package/dist-server/server/drivers/antigravity.js +1 -4
  109. package/dist-server/server/drivers/boatagent.js +23 -4
  110. package/dist-server/server/drivers/cerebras.js +3 -3
  111. package/dist-server/server/drivers/chat-mcp-tools.js +4 -7
  112. package/dist-server/server/drivers/claude-login-auth.js +2 -11
  113. package/dist-server/server/drivers/claude.js +16 -14
  114. package/dist-server/server/drivers/codex-device-auth.js +3 -1
  115. package/dist-server/server/drivers/codex.js +5 -4
  116. package/dist-server/server/drivers/grok.js +1 -1
  117. package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
  118. package/dist-server/server/drivers/openai-chat.js +18 -11
  119. package/dist-server/server/drivers/prompt-split.js +4 -4
  120. package/dist-server/server/drivers/retry.js +8 -4
  121. package/dist-server/server/engine-install.js +1 -5
  122. package/dist-server/server/harness/bus.js +49 -0
  123. package/dist-server/server/harness/registry.js +0 -1
  124. package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
  125. package/dist-server/server/index.js +1190 -971
  126. package/dist-server/server/lending-memory.js +12 -8
  127. package/dist-server/server/mcp-registry.js +35 -0
  128. package/dist-server/server/memory-entries.js +38 -8
  129. package/dist-server/server/memory-organize.js +1 -8
  130. package/dist-server/server/memory-topics.js +4 -4
  131. package/dist-server/server/memory-upkeep.js +13 -15
  132. package/dist-server/server/message-file.js +9 -13
  133. package/dist-server/server/outbound-counts.js +79 -0
  134. package/dist-server/server/outbound-requests.js +60 -0
  135. package/dist-server/server/package-import.js +1 -1
  136. package/dist-server/server/peer-delivery.js +5 -19
  137. package/dist-server/server/peer-roster.js +30 -4
  138. package/dist-server/server/profile-versions.js +0 -31
  139. package/dist-server/server/provider-key-check.js +2 -1
  140. package/dist-server/server/proxy-paths.js +2 -1
  141. package/dist-server/server/restart.js +40 -0
  142. package/dist-server/server/room-handoffs.js +17 -13
  143. package/dist-server/server/routes/bot-memory.js +0 -24
  144. package/dist-server/server/routine-requests.js +2 -5
  145. package/dist-server/server/routines.js +10 -5
  146. package/dist-server/server/server-launcher.js +53 -0
  147. package/dist-server/server/setup-mode.js +0 -2
  148. package/dist-server/server/skill-learn.js +0 -1
  149. package/dist-server/server/skill-library.js +0 -15
  150. package/dist-server/server/store.js +10 -2
  151. package/dist-server/server/surface.js +69 -28
  152. package/dist-server/server/system-prompt.js +17 -7
  153. package/dist-server/server/team-manifest.js +1 -6
  154. package/dist-server/server/team-memory.js +209 -0
  155. package/dist-server/server/thread-events.js +2 -2
  156. package/dist-server/server/tunnel.js +4 -1
  157. package/dist-server/server/turn-images.js +1 -21
  158. package/dist-server/server/turn-resources.js +19 -150
  159. package/dist-server/server/usage-ledger.js +3 -1
  160. package/dist-server/server/vps-computer.js +4 -14
  161. package/dist-server/server/workspace.js +87 -50
  162. package/dist-server/server-launcher.js +72 -0
  163. package/dist-server/shared/activity.js +3 -0
  164. package/dist-server/shared/ask-question.js +1 -1
  165. package/dist-server/shared/bot-avatar.js +8 -0
  166. package/dist-server/shared/cloud-computer.js +12 -0
  167. package/dist-server/shared/connector-scopes.js +78 -0
  168. package/dist-server/shared/failed-turn.js +25 -0
  169. package/dist-server/shared/live-approval.js +2 -2
  170. package/dist-server/shared/outbound.js +128 -0
  171. package/dist-server/shared/package-format.js +3 -3
  172. package/dist-server/shared/pairing-link.js +144 -0
  173. package/dist-server/shared/tool-surface.js +3 -2
  174. package/dist-server/vps-container-mcp.js +16 -29
  175. package/dist-server/workspace-backup.worker.js +14 -27
  176. package/enterprise/server/index.js +2 -1
  177. package/package.json +1 -1
  178. package/dist/assets/channel-CdJX6i8k.js +0 -1
  179. package/dist/assets/index-CPYrGsv1.js +0 -610
  180. package/dist/assets/index-CzT42GR8.css +0 -1
  181. package/dist/assets/swimlanesDiagram-TC7HE7FX-D-U4GtLQ.js +0 -8
  182. package/dist-server/server/claim-idle.js +0 -33
  183. package/dist-server/server/cloud-overflow.js +0 -167
  184. package/dist-server/server/drivers/chat-boat-tools.js +0 -90
  185. package/dist-server/server/drivers/grokagent.js +0 -4
  186. package/dist-server/server/openmaus-status-capsule.js +0 -329
  187. package/dist-server/server/tightening-requests.js +0 -435
  188. package/dist-server/shared/tightening-request.js +0 -87
@@ -1,435 +0,0 @@
1
- // propose_tightening: a bot (or a Chief for a section peer) proposes an
2
- // authority REDUCTION through a human-confirmed card. Same shape as
3
- // profile-requests.ts: everything is re-validated at confirm time — a card
4
- // can sit open for days, so the direction is checked against live state
5
- // again and the revision (a hash of the touched authority surface) must
6
- // still match. Escalation is inexpressible in the payload and rejected by
7
- // the validator; restoring authority stays owner-only in Edit Profile.
8
- import { createHash } from "node:crypto";
9
- import { z } from "zod";
10
- import { approvalModeFor } from "../shared/approval-mode.js";
11
- import { TIGHTENING_MODE_TARGETS, validateTightening, } from "../shared/tightening-request.js";
12
- import { newId } from "./contracts.js";
13
- import { recordAuthorityChange } from "./profile-versions.js";
14
- import { redactSecretsInText } from "./redact.js";
15
- const MAX_REASON = 500;
16
- const STALE = "This bot's authority changed after this card was prepared. Ask the bot to review it and propose again.";
17
- const NO_SUCH_BOT = "That bot no longer exists";
18
- const CHOOSE_ONE = "Choose at least one tightening intent: approvalMode, composio, browser, approvePeerComms, alwaysAllow, mcpServers, skills";
19
- const INTENT_KEYS = [
20
- "approvalMode",
21
- "composio",
22
- "browser",
23
- "approvePeerComms",
24
- "alwaysAllow",
25
- "mcpServers",
26
- "skills",
27
- ];
28
- /** Direction is enforced by shape first: the payload cannot name "full" or
29
- * "custom" as a target, cannot ask to switch composio/browser back on, and
30
- * the list fields only carry removals. The state-relative remainder (an
31
- * upward move inside ask/edits/auto, a grant not currently held) is checked
32
- * by validateTightening against live state at propose and confirm. */
33
- const intentsSchema = z.object({
34
- approvalMode: z.enum(TIGHTENING_MODE_TARGETS, {
35
- error: 'approvalMode may only be proposed as "ask", "edits", or "auto"; full and custom stay owner-only',
36
- }).optional(),
37
- composio: z.literal(false, { error: "composio may only be turned off" }).optional(),
38
- browser: z.literal(false, { error: "browser may only be turned off" }).optional(),
39
- approvePeerComms: z.literal(true, { error: "approvePeerComms may only be turned on" }).optional(),
40
- alwaysAllow: z.array(z.string().min(1).max(200)).max(200).optional(),
41
- mcpServers: z.array(z.string().min(1).max(200)).max(100).optional(),
42
- skills: z.array(z.string().min(1).max(100)).max(100).optional(),
43
- });
44
- export class TighteningRequestError extends Error {
45
- status;
46
- constructor(message, status = 400) {
47
- super(message);
48
- this.name = "TighteningRequestError";
49
- this.status = status;
50
- }
51
- }
52
- function reasonText(value) {
53
- if (typeof value !== "string")
54
- throw new TighteningRequestError("reason is required");
55
- const trimmed = value.trim();
56
- if (!trimmed)
57
- throw new TighteningRequestError("reason is required");
58
- if (trimmed.length > MAX_REASON) {
59
- throw new TighteningRequestError(`reason must be ${MAX_REASON} characters or fewer`);
60
- }
61
- return redactSecretsInText(trimmed);
62
- }
63
- function parseIntents(input) {
64
- if (typeof input !== "object" || input === null || Array.isArray(input)) {
65
- throw new TighteningRequestError(CHOOSE_ONE);
66
- }
67
- const record = input;
68
- for (const key of Object.keys(record)) {
69
- if (!INTENT_KEYS.includes(key)) {
70
- throw new TighteningRequestError(`unsupported tightening field: ${key}`);
71
- }
72
- }
73
- const parsed = intentsSchema.safeParse(input);
74
- if (!parsed.success) {
75
- throw new TighteningRequestError(parsed.error.issues[0]?.message ?? CHOOSE_ONE);
76
- }
77
- const intents = parsed.data;
78
- // Copy through only the keys the caller sent, so "did this card touch
79
- // approvalMode" stays answerable from the payload alone.
80
- const present = {};
81
- if (record.approvalMode !== undefined)
82
- present.approvalMode = intents.approvalMode;
83
- if (record.composio !== undefined)
84
- present.composio = intents.composio;
85
- if (record.browser !== undefined)
86
- present.browser = intents.browser;
87
- if (record.approvePeerComms !== undefined)
88
- present.approvePeerComms = intents.approvePeerComms;
89
- if (record.alwaysAllow !== undefined)
90
- present.alwaysAllow = intents.alwaysAllow;
91
- if (record.mcpServers !== undefined)
92
- present.mcpServers = intents.mcpServers;
93
- if (record.skills !== undefined)
94
- present.skills = intents.skills;
95
- if (Object.keys(present).length === 0)
96
- throw new TighteningRequestError(CHOOSE_ONE);
97
- return present;
98
- }
99
- /** The card revision: a hash of exactly the authority surface a tightening
100
- * can touch, plus the private receipt (an older, interrupted card cannot
101
- * apply twice). Sorted lists so an unrelated reorder is not a false stale. */
102
- export function tighteningRevision(state, lastTighteningRequestId = "") {
103
- return createHash("sha256")
104
- .update(JSON.stringify({
105
- approvalMode: state.approvalMode,
106
- composio: state.composio,
107
- browser: state.browser,
108
- approvePeerComms: state.approvePeerComms,
109
- alwaysAllow: [...state.alwaysAllow].sort(),
110
- mcpServers: [...state.mcpServers].sort(),
111
- skills: [...state.skills].sort(),
112
- lastTighteningRequestId,
113
- }), "utf8")
114
- .digest("hex");
115
- }
116
- export function tighteningSnapshot(bot, extras) {
117
- return {
118
- approvalMode: approvalModeFor(bot),
119
- composio: bot.composio !== false,
120
- browser: bot.browser !== false,
121
- approvePeerComms: bot.approvePeerComms === true,
122
- alwaysAllow: bot.alwaysAllow ?? [],
123
- mcpServers: extras.mcpServers ?? [...(bot.mcpServers ?? [])],
124
- skills: extras.skills ?? [],
125
- };
126
- }
127
- /** Minimal card copy: the judged facts (field, before, after, names) plus
128
- * the standing rule that only the owner can grant authority back. */
129
- export function tighteningCardCopy(target, before, intents, reason) {
130
- const checked = validateTightening(before, intents);
131
- const after = checked.ok ? checked.after : before;
132
- const title = target.crossBot ? `Tighten @${target.name}'s permissions?` : `Tighten ${target.name}'s permissions?`;
133
- const lines = target.crossBot ? [`Whose permissions: @${target.name}`, `Why: ${reason}`] : [`Why: ${reason}`];
134
- if (intents.approvalMode !== undefined)
135
- lines.push(`Approvals: ${before.approvalMode} → ${intents.approvalMode}`);
136
- if (intents.composio !== undefined)
137
- lines.push("Connected apps: on → off");
138
- if (intents.browser !== undefined)
139
- lines.push("Browser: on → off");
140
- if (intents.approvePeerComms !== undefined)
141
- lines.push("Peer contact: no approval → ask first");
142
- if (intents.alwaysAllow?.length)
143
- lines.push(removalLine("Always-allow grants", "removed", before.alwaysAllow, after.alwaysAllow));
144
- if (intents.mcpServers?.length)
145
- lines.push(removalLine("MCP servers", "unmounted", before.mcpServers, after.mcpServers));
146
- if (intents.skills?.length)
147
- lines.push(removalLine("Skills", "disabled", before.skills, after.skills));
148
- lines.push(`This reduces ${target.name}'s authority, and the reverse cannot be proposed back.`);
149
- const fields = INTENT_KEYS.filter((key) => intents[key] !== undefined);
150
- return { title, summary: `${title} · ${fields.join(", ")}`, detail: lines.join("\n") };
151
- }
152
- const REMOVED_NAME_CAP = 5;
153
- /** One line per list field with the counts on both sides and the exact
154
- * names that go away, capped with a count so a wide card still reads in
155
- * one pass while the human approves precisely what disappears. */
156
- function removalLine(label, verb, before, after) {
157
- const removed = before.filter((name) => !after.includes(name));
158
- const shown = removed.slice(0, REMOVED_NAME_CAP);
159
- const more = removed.length - shown.length;
160
- const names = shown.join(", ") + (more > 0 ? ` (+${more} more)` : "");
161
- return `${label}: ${before.length} → ${after.length} — ${verb}: ${names}`;
162
- }
163
- export class TighteningRequestService {
164
- store;
165
- now;
166
- canPersist;
167
- autoApply;
168
- mountedMcpServers;
169
- enabledSkills;
170
- disableSkill;
171
- targetBusy;
172
- /** Public: a caller's section membership can change between propose and
173
- * confirm, and tests flip this mid-scenario to model that. */
174
- validateTarget;
175
- constructor(options) {
176
- this.store = options.store;
177
- this.now = options.now ?? Date.now;
178
- this.canPersist = options.canPersist;
179
- this.autoApply = options.autoApply;
180
- this.mountedMcpServers = options.mountedMcpServers;
181
- this.enabledSkills = options.enabledSkills;
182
- this.disableSkill = options.disableSkill;
183
- this.targetBusy = options.targetBusy;
184
- this.validateTarget = options.validateTarget;
185
- }
186
- snapshotOf(bot) {
187
- return tighteningSnapshot(bot, {
188
- mcpServers: this.mountedMcpServers?.(bot),
189
- skills: this.enabledSkills?.(bot.id) ?? [],
190
- });
191
- }
192
- /** Disable every named skill, or report the first failure together with
193
- * the names still outstanding. Shared by the fresh apply and the
194
- * receipt-backed retry so both leave the same trail. */
195
- applySkillDisables(botId, names) {
196
- for (let index = 0; index < names.length; index += 1) {
197
- const disabled = this.disableSkill(botId, names[index]);
198
- if (!disabled.ok)
199
- return { ok: false, error: disabled.error, outstanding: names.slice(index) };
200
- }
201
- return { ok: true };
202
- }
203
- propose(args) {
204
- return this.prepare(args);
205
- }
206
- submit(args) {
207
- const proposal = this.prepare(args, true);
208
- return { ...proposal, state: proposal.result ? "applied" : "pending" };
209
- }
210
- prepare(args, submitted = false) {
211
- const reason = reasonText(args.reason);
212
- const intents = parseIntents(args.intents);
213
- const targetBotId = args.targetBotId ?? args.botId;
214
- const target = this.store.bot(targetBotId);
215
- if (!target)
216
- throw new TighteningRequestError(NO_SUCH_BOT, 404);
217
- const crossBot = targetBotId !== args.botId;
218
- if (crossBot && this.validateTarget) {
219
- const refusal = this.validateTarget(args.botId, targetBotId);
220
- if (refusal)
221
- throw new TighteningRequestError(refusal, 403);
222
- }
223
- const before = this.snapshotOf(target);
224
- const checked = validateTightening(before, intents);
225
- if (!checked.ok)
226
- throw new TighteningRequestError(checked.error, 400);
227
- const requestId = newId();
228
- const targetName = redactSecretsInText(target.name);
229
- const payload = {
230
- version: 1,
231
- requestId,
232
- botId: args.botId,
233
- threadId: args.threadId,
234
- targetBotId,
235
- targetName,
236
- createdAt: this.now(),
237
- reason,
238
- intents,
239
- before,
240
- expectedRevision: tighteningRevision(before, target.lastTighteningRequestId),
241
- };
242
- const copy = tighteningCardCopy({ name: targetName, crossBot }, before, intents, reason);
243
- const persistence = this.canPersist?.(args.botId, args.threadId);
244
- if (persistence && !persistence.ok) {
245
- throw new TighteningRequestError(persistence.error, persistence.status);
246
- }
247
- const automatic = submitted && this.autoApply?.(args.botId, args.threadId) === true;
248
- const messageInput = {
249
- role: "bot",
250
- kind: "options",
251
- card: {
252
- title: copy.title,
253
- subtitle: copy.detail,
254
- options: automatic ? [] : ["Confirm", "Cancel"],
255
- ...(automatic ? { dismissed: true } : {}),
256
- requestId,
257
- tool: "tighten_permissions",
258
- tighteningRequest: payload,
259
- },
260
- };
261
- if (args.from)
262
- messageInput.from = args.from;
263
- const message = this.store.appendMessage(args.threadId, messageInput);
264
- const proposal = { requestId, messageId: message.id, title: copy.title, summary: copy.summary, detail: copy.detail };
265
- if (!automatic)
266
- return proposal;
267
- // The hidden receipt is persisted before the authority changes. The same
268
- // validation and durable commit marker serve both automatic and human decisions.
269
- const result = this.resolve({ botId: args.botId, threadId: args.threadId, requestId, behavior: "allow" });
270
- if (result.state === "applied")
271
- return { ...proposal, result };
272
- throw new TighteningRequestError(result.state === "invalid" ? result.error : "The tightening could not be applied", result.state === "invalid" ? result.status : 409);
273
- }
274
- /** Claims a tightening card even after it was settled, so a duplicate
275
- * click never re-applies an already-applied reduction. */
276
- resolve(args) {
277
- const message = this.store
278
- .messagesFor(args.threadId)
279
- .find((candidate) => candidate.card?.requestId === args.requestId && candidate.card.tighteningRequest);
280
- const card = message?.card;
281
- const payload = card?.tighteningRequest;
282
- if (!message || !card || !payload)
283
- return { claimed: false, state: "not_found" };
284
- if (payload.requestId !== card.requestId) {
285
- return { claimed: true, state: "invalid", error: "This tightening request does not match its card", status: 409 };
286
- }
287
- if (args.behavior !== "allow" && args.behavior !== "deny") {
288
- return { claimed: true, state: "invalid", error: "Tightening confirmations must be confirmed or cancelled", status: 400 };
289
- }
290
- if (payload.botId !== args.botId || payload.threadId !== args.threadId) {
291
- return { claimed: true, state: "invalid", error: "This tightening request belongs to another conversation", status: 403 };
292
- }
293
- if (card.answered)
294
- return { claimed: true, state: "already_settled", behavior: card.answered };
295
- let emergency = false;
296
- let outstandingSkills = [];
297
- try {
298
- const target = this.store.bot(payload.targetBotId);
299
- // The authority change and receipt share one durable write. If saving
300
- // the card failed afterward, a retry only settles it; it never
301
- // reapplies authority — but it does finish any skills a failed
302
- // attempt left enabled, so a partial disable can never strand the card.
303
- if (target?.lastTighteningRequestId === payload.requestId) {
304
- const enabled = new Set(this.enabledSkills?.(payload.targetBotId) ?? []);
305
- const remaining = (payload.intents.skills ?? []).filter((name) => enabled.has(name));
306
- if (remaining.length && !this.disableSkill) {
307
- throw new TighteningRequestError("Skill changes are not available in this workspace", 400);
308
- }
309
- const finished = this.applySkillDisables(payload.targetBotId, remaining);
310
- if (!finished.ok) {
311
- outstandingSkills = finished.outstanding;
312
- throw new TighteningRequestError(finished.error, 409);
313
- }
314
- const settled = this.store.patchMessage(args.threadId, message.id, {
315
- card: { ...card, answered: "allow", held: undefined, tighteningRequest: { ...payload, appliedAt: payload.appliedAt ?? this.now() } },
316
- });
317
- if (!settled)
318
- throw new TighteningRequestError("This tightening confirmation card is no longer available", 409);
319
- // The first attempt committed the authority change and the durable
320
- // receipt, then died before it could settle the card or record
321
- // history. This retry settles the card, so it also writes the row
322
- // that attempt never reached — from the card's own frozen inputs,
323
- // never the live bot: the revision guard proved live state equaled
324
- // payload.before at apply, so the validated post-card state is the
325
- // committed change, while a live snapshot could fold in later owner
326
- // edits and attribute them to this card.
327
- const recorded = validateTightening(payload.before, payload.intents);
328
- if (recorded.ok) {
329
- recordAuthorityChange(payload.targetBotId, "bot", `card:${message.id}`, payload.before, recorded.after);
330
- }
331
- return { claimed: true, state: "already_settled", behavior: "allow" };
332
- }
333
- if (args.behavior === "deny") {
334
- this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
335
- return { claimed: true, state: "denied" };
336
- }
337
- if (!target)
338
- throw new TighteningRequestError(NO_SUCH_BOT, 404);
339
- const crossBot = payload.targetBotId !== payload.botId;
340
- if (crossBot && this.validateTarget) {
341
- const refusal = this.validateTarget(payload.botId, payload.targetBotId);
342
- if (refusal)
343
- throw new TighteningRequestError(refusal, 404);
344
- }
345
- const current = this.snapshotOf(target);
346
- if (tighteningRevision(current, target.lastTighteningRequestId) !== payload.expectedRevision) {
347
- throw new TighteningRequestError(STALE, 409);
348
- }
349
- // Direction is re-validated against live state, not trusted from the
350
- // card: a card that sat open can only ever apply a reduction.
351
- const checked = validateTightening(current, payload.intents);
352
- if (!checked.ok)
353
- throw new TighteningRequestError(checked.error, 409);
354
- emergency = payload.intents.approvalMode !== undefined &&
355
- (current.approvalMode === "full" || current.approvalMode === "custom") &&
356
- payload.intents.approvalMode === "ask";
357
- if (this.targetBusy?.(target.id) && !emergency) {
358
- if (payload.intents.approvalMode !== undefined) {
359
- throw new TighteningRequestError("Stop this bot's turn before changing its approval level", 409);
360
- }
361
- if (payload.intents.mcpServers !== undefined) {
362
- throw new TighteningRequestError("Stop this bot's turns before changing its MCP servers", 409);
363
- }
364
- }
365
- // The capability check runs before any mutation: a workspace that
366
- // cannot touch skills must not half-apply the card.
367
- if (payload.intents.skills?.length && !this.disableSkill) {
368
- throw new TighteningRequestError("Skill changes are not available in this workspace", 400);
369
- }
370
- const patch = { lastTighteningRequestId: payload.requestId };
371
- if (payload.intents.approvalMode !== undefined) {
372
- patch.approvalMode = payload.intents.approvalMode;
373
- patch.autoApprove = payload.intents.approvalMode === "auto";
374
- patch.approvalGrant = undefined;
375
- }
376
- if (payload.intents.composio !== undefined)
377
- patch.composio = false;
378
- if (payload.intents.browser !== undefined)
379
- patch.browser = false;
380
- if (payload.intents.approvePeerComms !== undefined)
381
- patch.approvePeerComms = true;
382
- if (payload.intents.alwaysAllow !== undefined)
383
- patch.alwaysAllow = [...checked.after.alwaysAllow];
384
- if (payload.intents.mcpServers !== undefined)
385
- patch.mcpServers = [...checked.after.mcpServers];
386
- if (!this.store.patchBot(target.id, patch))
387
- throw new TighteningRequestError(NO_SUCH_BOT, 404);
388
- // Skills disable AFTER the receipt write. Each disable persists on
389
- // its own, so a failure midway can leave part of the list done — but
390
- // the receipt is already durable, which turns every retry into the
391
- // branch above: finish the remainder, never reapply authority, and
392
- // never leave the card permanently stale.
393
- const skills = this.applySkillDisables(target.id, payload.intents.skills ?? []);
394
- if (!skills.ok) {
395
- outstandingSkills = skills.outstanding;
396
- throw new TighteningRequestError(skills.error, 409);
397
- }
398
- const appliedAt = this.now();
399
- const settled = this.store.patchMessage(args.threadId, message.id, {
400
- card: { ...card, answered: "allow", held: undefined, tighteningRequest: { ...payload, appliedAt } },
401
- });
402
- if (!settled)
403
- throw new TighteningRequestError("This tightening confirmation card is no longer available", 409);
404
- // Same audit trail as a profile card: the proposing bot is the actor,
405
- // the card id is the via, and the rows land in the one History section
406
- // manual edits already write to. Fire-and-forget like every history row.
407
- recordAuthorityChange(payload.targetBotId, "bot", `card:${message.id}`, current, checked.after);
408
- const fields = INTENT_KEYS.filter((key) => payload.intents[key] !== undefined);
409
- return { claimed: true, state: "applied", targetBotId: target.id, fields, ...(emergency ? { emergencyStop: true } : {}) };
410
- }
411
- catch (error) {
412
- const status = error instanceof TighteningRequestError ? error.status : 400;
413
- const detail = error instanceof Error ? error.message : String(error);
414
- const saved = this.store.bot(payload.targetBotId)?.lastTighteningRequestId === payload.requestId;
415
- const notice = outstandingSkills.length
416
- ? `Permissions tightened. Confirm again to finish disabling: ${redactSecretsInText(outstandingSkills.join(", "))}`
417
- : card.dismissed && card.options.length === 0
418
- ? "Permissions tightened. Recording the operation receipt could not finish; the changes will not be applied again."
419
- : "Permissions tightened. Confirm again to finish recording this decision; the changes will not be applied again.";
420
- try {
421
- this.store.patchMessage(args.threadId, message.id, {
422
- card: { ...card, held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
423
- });
424
- }
425
- catch { /* The durable receipt still permits a safe retry. */ }
426
- if (saved)
427
- return {
428
- claimed: true, state: "applied", targetBotId: payload.targetBotId,
429
- fields: INTENT_KEYS.filter((key) => payload.intents[key] !== undefined),
430
- ...(emergency ? { emergencyStop: true } : {}),
431
- };
432
- return { claimed: true, state: "invalid", error: detail, status };
433
- }
434
- }
435
- }
@@ -1,87 +0,0 @@
1
- /**
2
- * Durable payload carried by a tightening card (propose_tightening).
3
- *
4
- * Authority reductions a bot may propose for itself or (for a Chief) a
5
- * section peer. Direction is enforced by shape: the intents cannot express
6
- * an escalation — approvalMode targets stop below full and custom, the
7
- * booleans only carry their tightening value, and the list fields only
8
- * carry removals. `validateTightening` then checks the remaining
9
- * state-relative direction (an upward move inside ask/edits/auto, a grant
10
- * that is not currently held) against live bot state, at propose AND again
11
- * at confirm, so a card that sat open can never apply a transition that
12
- * stopped being a reduction. Escalation stays human-only in Edit Profile.
13
- */
14
- import { APPROVAL_MODES } from "./approval-mode.js";
15
- /** Targets a proposal may name. "full" and "custom" are owner-only. */
16
- export const TIGHTENING_MODE_TARGETS = ["ask", "edits", "auto"];
17
- /** Direction check against live state. Pure: the caller supplies the
18
- * snapshot, at propose and again at confirm. Every rejection names the
19
- * field and the allowed direction. */
20
- export function validateTightening(before, intents) {
21
- const after = {
22
- ...before,
23
- alwaysAllow: [...before.alwaysAllow],
24
- mcpServers: [...before.mcpServers],
25
- skills: [...before.skills],
26
- };
27
- if (intents.approvalMode !== undefined) {
28
- const to = intents.approvalMode;
29
- const from = before.approvalMode;
30
- const reduces = from === "custom"
31
- ? to === "ask"
32
- : APPROVAL_MODES.indexOf(to) < APPROVAL_MODES.indexOf(from);
33
- if (!reduces) {
34
- return {
35
- ok: false,
36
- error: `approvalMode can only tighten: down the selector order (ask, edits, auto, full), or custom to ask. "${to}" does not reduce "${from}".`,
37
- };
38
- }
39
- after.approvalMode = to;
40
- }
41
- if (intents.composio !== undefined) {
42
- if (!before.composio)
43
- return { ok: false, error: "composio is already off; it may only be turned off" };
44
- after.composio = false;
45
- }
46
- if (intents.browser !== undefined) {
47
- if (!before.browser)
48
- return { ok: false, error: "browser is already off; it may only be turned off" };
49
- after.browser = false;
50
- }
51
- if (intents.approvePeerComms !== undefined) {
52
- if (before.approvePeerComms)
53
- return { ok: false, error: "approvePeerComms is already on; it may only be turned on" };
54
- after.approvePeerComms = true;
55
- }
56
- if (intents.alwaysAllow !== undefined) {
57
- const removals = [...new Set(intents.alwaysAllow)];
58
- if (!removals.length)
59
- return { ok: false, error: "alwaysAllow must name at least one grant to remove" };
60
- const unknown = removals.filter((key) => !before.alwaysAllow.includes(key));
61
- if (unknown.length) {
62
- return { ok: false, error: `alwaysAllow entries may only be removed, never added; not currently granted: ${unknown.join(", ")}` };
63
- }
64
- after.alwaysAllow = before.alwaysAllow.filter((key) => !removals.includes(key));
65
- }
66
- if (intents.mcpServers !== undefined) {
67
- const removals = [...new Set(intents.mcpServers)];
68
- if (!removals.length)
69
- return { ok: false, error: "mcpServers must name at least one server to unmount" };
70
- const unknown = removals.filter((name) => !before.mcpServers.includes(name));
71
- if (unknown.length) {
72
- return { ok: false, error: `mcpServers may only unmount servers this bot currently mounts; not mounted: ${unknown.join(", ")}` };
73
- }
74
- after.mcpServers = before.mcpServers.filter((name) => !removals.includes(name));
75
- }
76
- if (intents.skills !== undefined) {
77
- const removals = [...new Set(intents.skills)];
78
- if (!removals.length)
79
- return { ok: false, error: "skills must name at least one skill to disable" };
80
- const unknown = removals.filter((name) => !before.skills.includes(name));
81
- if (unknown.length) {
82
- return { ok: false, error: `skills may only be disabled, never enabled; not currently enabled: ${unknown.join(", ")}` };
83
- }
84
- after.skills = before.skills.filter((name) => !removals.includes(name));
85
- }
86
- return { ok: true, after };
87
- }