openmausbot 0.1.88 → 0.1.90

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 (147) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{abnfDiagram-O67JEVCF-qd9VD3GV.js → abnfDiagram-O67JEVCF-DUyXd0kL.js} +1 -1
  3. package/dist/assets/{arc-3RwkUjRe.js → arc-q73qM-At.js} +1 -1
  4. package/dist/assets/{architectureDiagram-NJMV4G6O-CFKSJS7a.js → architectureDiagram-NJMV4G6O-D33O3Wur.js} +1 -1
  5. package/dist/assets/{blockDiagram-BEXU5L5S-CG1rEfWD.js → blockDiagram-BEXU5L5S-DLsJeoXC.js} +1 -1
  6. package/dist/assets/{c4Diagram-YGBWAQC7-CBW9N664.js → c4Diagram-YGBWAQC7-DnaljZnu.js} +1 -1
  7. package/dist/assets/channel-BCuyHjFV.js +1 -0
  8. package/dist/assets/{chunk-2Q5K7J3B-2mRfTksB.js → chunk-2Q5K7J3B-Da0h2i79.js} +1 -1
  9. package/dist/assets/{chunk-3FUC2YCW-Cv1dpErj.js → chunk-3FUC2YCW-CfzHi54f.js} +2 -2
  10. package/dist/assets/{chunk-5VM5RSS4-B7IACJp-.js → chunk-5VM5RSS4-LjE4d9Jf.js} +1 -1
  11. package/dist/assets/{chunk-GTNCS2PH-An0hZtYa.js → chunk-GTNCS2PH-DY-UGmvc.js} +1 -1
  12. package/dist/assets/{chunk-GWA4HPMP-BcJB2nEm.js → chunk-GWA4HPMP-BqeGhyhP.js} +1 -1
  13. package/dist/assets/{chunk-JWPE2WC7-CV_W7TN7.js → chunk-JWPE2WC7-BYa9EBAg.js} +1 -1
  14. package/dist/assets/{chunk-NETBCI7D-Bcs_kwnA.js → chunk-NETBCI7D-vTrDtWyb.js} +1 -1
  15. package/dist/assets/{chunk-WEXAMYUT-lrx_DxFP.js → chunk-WEXAMYUT-ic5tXadM.js} +1 -1
  16. package/dist/assets/{chunk-XXDRQBXY-DabHlLGM.js → chunk-XXDRQBXY-DvRCY-c6.js} +1 -1
  17. package/dist/assets/{classDiagram-v2-NBCMYWYE-BnHj6P7d.js → classDiagram-v2-NBCMYWYE-DVbmgz_L.js} +1 -1
  18. package/dist/assets/{cose-bilkent-JH36ORCC-CfLp6UDi.js → cose-bilkent-JH36ORCC-vdUs2n57.js} +1 -1
  19. package/dist/assets/{cynefin-OW5HDTMX-Dm4dbVA1.js → cynefin-OW5HDTMX-BvLGlV3U.js} +1 -1
  20. package/dist/assets/{cynefinDiagram-VND7K2PF-B9mH-PID.js → cynefinDiagram-VND7K2PF-C4GcJQDt.js} +1 -1
  21. package/dist/assets/{dagre-6A5THRUB-DDc_NVQJ.js → dagre-6A5THRUB-BMSaHpt_.js} +1 -1
  22. package/dist/assets/{diagram-22UHCM2B-lwr8Ip8-.js → diagram-22UHCM2B-D5b9UBhC.js} +1 -1
  23. package/dist/assets/{diagram-3UASUU5V-BLLe6dmI.js → diagram-3UASUU5V-CIKLlB7Y.js} +1 -1
  24. package/dist/assets/{diagram-ATOU4E4O-6eUIq4lz.js → diagram-ATOU4E4O-BzXb-7k7.js} +1 -1
  25. package/dist/assets/{diagram-CDSNMT55-O-pRN7VQ.js → diagram-CDSNMT55-HsTvJ7uT.js} +1 -1
  26. package/dist/assets/{diagram-MLGK6HIB-BietvGAI.js → diagram-MLGK6HIB-Db9ySNbQ.js} +1 -1
  27. package/dist/assets/{diagram-MPIPVDR6-Bd2Cl62X.js → diagram-MPIPVDR6-D6BKbpqY.js} +1 -1
  28. package/dist/assets/{ebnfDiagram-ZINNZB2B-B6iDy69W.js → ebnfDiagram-ZINNZB2B-Bq067L0t.js} +1 -1
  29. package/dist/assets/{elk-276RUBZZ-DQVHfKe0.js → elk-276RUBZZ-DhaD7fNJ.js} +1 -1
  30. package/dist/assets/{erDiagram-OPXOYQCR-BWdVp2tQ.js → erDiagram-OPXOYQCR-fwIhfz36.js} +1 -1
  31. package/dist/assets/{flowDiagram-KWPJA3E3-WLE6rKB4.js → flowDiagram-KWPJA3E3-DygIyu0V.js} +1 -1
  32. package/dist/assets/{ganttDiagram-FUAMR5RP-BbjS-4gn.js → ganttDiagram-FUAMR5RP-DPYfSO96.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-X574FWY7-Dpy2QSGq.js → gitGraphDiagram-X574FWY7-DhkHeCle.js} +1 -1
  34. package/dist/assets/index-CRvo-DS2.css +1 -0
  35. package/dist/assets/index-CWYGlA63.js +588 -0
  36. package/dist/assets/{index-CIaHn5NS.js → index-G1PHKGyh.js} +1 -1
  37. package/dist/assets/{infoDiagram-VRGFBTTK-D_jKWlKe.js → infoDiagram-VRGFBTTK-DstBLxME.js} +1 -1
  38. package/dist/assets/{ishikawaDiagram-OU5B5YK6-DLJohSuK.js → ishikawaDiagram-OU5B5YK6-CrIsGgch.js} +1 -1
  39. package/dist/assets/{journeyDiagram-ZHPQQLJL-DPOFj0qN.js → journeyDiagram-ZHPQQLJL-CYypGDbB.js} +1 -1
  40. package/dist/assets/{kanban-definition-PNTS6WVX-BI7v-xpK.js → kanban-definition-PNTS6WVX-DGjPt8E_.js} +1 -1
  41. package/dist/assets/{linear-C2pWPSxO.js → linear-Dwc3Hrru.js} +1 -1
  42. package/dist/assets/{mermaid.core-CR-j5WQU.js → mermaid.core-Dc4OePBL.js} +5 -5
  43. package/dist/assets/{mindmap-definition-NLK3R4M7-lz9GvXRh.js → mindmap-definition-NLK3R4M7-DV-vepj-.js} +1 -1
  44. package/dist/assets/{pegDiagram-GJSIUBJH-Dl4_ia61.js → pegDiagram-GJSIUBJH-lx5Kj-n5.js} +1 -1
  45. package/dist/assets/{pieDiagram-5QR66LMP-DWuT3n88.js → pieDiagram-5QR66LMP-Bq7Vmm15.js} +1 -1
  46. package/dist/assets/{quadrantDiagram-O4NWA36T-DXWe9Jx7.js → quadrantDiagram-O4NWA36T-DeqeqOBj.js} +1 -1
  47. package/dist/assets/{railroadDiagram-XR7U4H2S-COY1IBVY.js → railroadDiagram-XR7U4H2S-CXwRQzSC.js} +1 -1
  48. package/dist/assets/{requirementDiagram-PLB6GJNP-DnyVuhsQ.js → requirementDiagram-PLB6GJNP-8tAeoB88.js} +1 -1
  49. package/dist/assets/{sankeyDiagram-IPEJSGJF-BSc4NdGK.js → sankeyDiagram-IPEJSGJF-BG5zjGWU.js} +1 -1
  50. package/dist/assets/{sequenceDiagram-PO4LG4MO-kqEU2EGq.js → sequenceDiagram-PO4LG4MO-DKF3iLcw.js} +1 -1
  51. package/dist/assets/{sizeCapture-INFHLROL-BvB4bkWD.js → sizeCapture-INFHLROL-B-y1qUgT.js} +1 -1
  52. package/dist/assets/{stateDiagram-v2-GCMORJYK-BIrqNv7y.js → stateDiagram-v2-GCMORJYK-MkzPgxAc.js} +1 -1
  53. package/dist/assets/{swimlanes-2SLR337P-CFnalQUE.js → swimlanes-2SLR337P-DnbaOEwH.js} +1 -1
  54. package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +8 -0
  55. package/dist/assets/{timeline-definition-EJHVYXUP-CsSWJnSQ.js → timeline-definition-EJHVYXUP-Bb4xG_Bt.js} +1 -1
  56. package/dist/assets/{usecaseDiagram-POWQR4AR-DkAmJ1EE.js → usecaseDiagram-POWQR4AR-JgkR9pFf.js} +1 -1
  57. package/dist/assets/{vennDiagram-UO4OBE2U-C3vd9drL.js → vennDiagram-UO4OBE2U-CIPIrAYe.js} +1 -1
  58. package/dist/assets/{wardleyDiagram-VNRHLVJA-j26CM-pi.js → wardleyDiagram-VNRHLVJA-BjCLuUvJ.js} +1 -1
  59. package/dist/assets/{xychartDiagram-PMCCYNJV-BYu1Q_at.js → xychartDiagram-PMCCYNJV-DKgiaYxT.js} +1 -1
  60. package/dist/index.html +2 -2
  61. package/dist-server/cloud-home-start.js +14795 -0
  62. package/dist-server/container-mcp.js +60 -4
  63. package/dist-server/drivers/agents-proxy.js +20 -10
  64. package/dist-server/index.js +48924 -46153
  65. package/dist-server/openmausbot.js +544 -253
  66. package/dist-server/pair-cli.js +544 -253
  67. package/dist-server/permission-proxy.js +1 -1
  68. package/dist-server/server/admission.js +46 -0
  69. package/dist-server/server/automatic-recovery.js +15 -0
  70. package/dist-server/server/{box-create-idempotency.js → boat-create-idempotency.js} +32 -31
  71. package/dist-server/server/{box-delete-journal.js → boat-delete-journal.js} +26 -25
  72. package/dist-server/server/{box.js → boat.js} +330 -328
  73. package/dist-server/server/bot-profile.js +7 -1
  74. package/dist-server/server/channel-queue.js +58 -18
  75. package/dist-server/server/checkpoints.js +98 -52
  76. package/dist-server/server/claim-idle.js +33 -0
  77. package/dist-server/server/cli-api-setup.js +0 -1
  78. package/dist-server/server/cloud-backend.js +5 -5
  79. package/dist-server/server/cloud-home-start.js +103 -0
  80. package/dist-server/server/cloud-home.js +218 -0
  81. package/dist-server/server/cloud-overflow.js +167 -0
  82. package/dist-server/server/composio.js +5 -0
  83. package/dist-server/server/computer-observation.js +2 -2
  84. package/dist-server/server/config.js +124 -6
  85. package/dist-server/server/container-computer.js +23 -0
  86. package/dist-server/server/contracts.js +10 -0
  87. package/dist-server/server/control-client.js +2 -2
  88. package/dist-server/server/delegations.js +11 -3
  89. package/dist-server/server/drivers/acp/core.js +129 -21
  90. package/dist-server/server/drivers/acp/qwen.js +3 -0
  91. package/dist-server/server/drivers/agents-call.js +5 -1
  92. package/dist-server/server/drivers/agents-catalog.js +9 -3
  93. package/dist-server/server/drivers/{boxagent.js → boatagent.js} +14 -13
  94. package/dist-server/server/drivers/builtIn.js +2 -2
  95. package/dist-server/server/drivers/{chat-box-tools.js → chat-boat-tools.js} +3 -3
  96. package/dist-server/server/drivers/chat-mcp-tools.js +2 -2
  97. package/dist-server/server/drivers/claude.js +177 -13
  98. package/dist-server/server/drivers/native.js +1 -1
  99. package/dist-server/server/drivers/openai-chat.js +19 -4
  100. package/dist-server/server/drivers/pi.js +3 -3
  101. package/dist-server/server/harness-capabilities.js +18 -3
  102. package/dist-server/server/hosted-models.js +6 -1
  103. package/dist-server/server/index.js +1163 -530
  104. package/dist-server/server/local-vm-inventory.js +11 -1
  105. package/dist-server/server/local-vm-seat-pool.js +141 -0
  106. package/dist-server/server/managed-policy.js +4 -1
  107. package/dist-server/server/memory-capture.js +194 -0
  108. package/dist-server/server/memory-entries.js +76 -0
  109. package/dist-server/server/memory-organize.js +102 -0
  110. package/dist-server/server/memory-tidy.js +144 -0
  111. package/dist-server/server/memory-topics.js +169 -0
  112. package/dist-server/server/memory-upkeep.js +452 -0
  113. package/dist-server/server/message-db.js +49 -5
  114. package/dist-server/server/permission-proxy.js +3 -3
  115. package/dist-server/server/profile-learned.js +98 -0
  116. package/dist-server/server/profile-versions.js +45 -11
  117. package/dist-server/server/recall.js +172 -0
  118. package/dist-server/server/redact.js +1 -1
  119. package/dist-server/server/room-cwd.js +1 -1
  120. package/dist-server/server/room-handoffs.js +61 -6
  121. package/dist-server/server/routes/bot-memory.js +215 -0
  122. package/dist-server/server/routine-requests.js +27 -16
  123. package/dist-server/server/routines.js +10 -6
  124. package/dist-server/server/steer-queue.js +41 -10
  125. package/dist-server/server/store.js +25 -1
  126. package/dist-server/server/surface.js +1 -1
  127. package/dist-server/server/system-prompt.js +16 -5
  128. package/dist-server/server/team-computers.js +2 -2
  129. package/dist-server/server/tightening-requests.js +34 -4
  130. package/dist-server/server/tool-summary.js +2 -2
  131. package/dist-server/server/tts/elevenlabs.js +1 -1
  132. package/dist-server/server/turn-resources.js +87 -11
  133. package/dist-server/server/vps-computer.js +1 -1
  134. package/dist-server/server/webhooks.js +21 -3
  135. package/dist-server/server/workspace.js +57 -22
  136. package/dist-server/shared/ask-question.js +7 -5
  137. package/dist-server/shared/bot-avatar.js +19 -0
  138. package/dist-server/shared/credential-request.js +3 -3
  139. package/dist-server/shared/tool-surface.js +1 -1
  140. package/dist-server/src/lib/composer-attachments.js +26 -0
  141. package/dist-server/vps-container-mcp.js +60 -4
  142. package/dist-server/workspace-backup.worker.js +60 -4
  143. package/package.json +1 -1
  144. package/dist/assets/channel-CnRnO3tF.js +0 -1
  145. package/dist/assets/index-BIMdIbaw.css +0 -1
  146. package/dist/assets/index-CHnODCr2.js +0 -583
  147. package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +0 -8
@@ -0,0 +1,218 @@
1
+ // OMB Cloud Pro home machine: the boot contract, the Admin's signed pairing
2
+ // request, and the volume the machine lives on. docs/cloud-pro.md is the
3
+ // contract of record (and openmaus-cloud docs/consumer-cloud.md its Admin
4
+ // half); keep them in step.
5
+ //
6
+ // One Fly app per customer runs this server behind an edge proxy on
7
+ // 0.0.0.0:8080 (server/cloud-home-start.ts). The server itself still binds
8
+ // 127.0.0.1 and every request reaches it with X-Forwarded-* headers, so no
9
+ // request from the network is ever the loopback owner.
10
+ //
11
+ // The Admin never holds a session on the machine. It signs a request with
12
+ // the machine's bootstrap secret (HMAC-SHA256 over the method, path, a
13
+ // timestamp, a nonce and the body's hash); each valid request opens one
14
+ // ordinary pairing window (sessions.ts: single use, at most ten minutes),
15
+ // which the Admin hands to the person's signed-in desktop app.
16
+ //
17
+ // Cloud Pro includes no AI. The person signs in on the machine with their own
18
+ // Claude or ChatGPT account, or an API key, exactly as on any server; nothing
19
+ // on a Cloud home is ever routed to a platform model gateway.
20
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
21
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { writeFileAtomic } from "./atomic.js";
24
+ import { hostedWorkspaceConfigured } from "./enterprise.js";
25
+ import { formatPairingCode } from "./sessions.js";
26
+ export const CLOUD_HOME_CONTRACT_VERSION = 1;
27
+ /** Any of these switches the server into Cloud home mode; then all are required. */
28
+ export const CLOUD_HOME_KEYS = ["OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_BOOTSTRAP_SECRET"];
29
+ /** A platform model gateway's settings. A Cloud home never uses them: given
30
+ * any, it logs one warning, and neither the server nor anything it starts
31
+ * ever sees them. */
32
+ export const CLOUD_IGNORED_KEYS = ["OMB_HOSTED_MODEL_URL", "OMB_HOSTED_MODEL_TOKEN", "OMB_HOSTED_MODELS"];
33
+ export const CLOUD_PAIRING_PATH = "/api/cloud/pairing";
34
+ export const CLOUD_PAIRING_DEFAULT_TTL_S = 300;
35
+ export const CLOUD_PAIRING_MAX_TTL_S = 600;
36
+ /** How far a signed request's timestamp may be from this machine's clock. */
37
+ export const CLOUD_PAIRING_SKEW_S = 300;
38
+ /** How long a used nonce is refused. Longer than the whole accepted window. */
39
+ export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
40
+ const MAX_NONCES = 10_000;
41
+ export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
42
+ export function cloudHomeConfigured(env = process.env) {
43
+ return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
44
+ }
45
+ function invalid(why) {
46
+ throw new Error(`Cloud home configuration is invalid: ${why}.`);
47
+ }
48
+ function exactHttpsOrigin(raw, name) {
49
+ let url;
50
+ try {
51
+ if (!raw || raw !== raw.trim())
52
+ throw new Error();
53
+ url = new URL(raw);
54
+ }
55
+ catch {
56
+ return invalid(`${name} must be an https origin`);
57
+ }
58
+ if (url.protocol !== "https:" || url.username || url.password || (raw !== url.origin && raw !== `${url.origin}/`)) {
59
+ invalid(`${name} must be an exact https origin with no path`);
60
+ }
61
+ return url.origin;
62
+ }
63
+ /** Read the boot contract. Null on any machine that is not a Cloud home;
64
+ * throws (so the server refuses to start) on a partial or invalid one. */
65
+ export function cloudHomeConfiguration(env = process.env) {
66
+ if (!cloudHomeConfigured(env))
67
+ return null;
68
+ if (env.OMB_DESKTOP_PARENT === "1")
69
+ invalid("the desktop app cannot run as a Cloud home machine");
70
+ if (hostedWorkspaceConfigured(env))
71
+ invalid("a Cloud home is not a hosted team workspace; remove OMB_ADMIN_URL, OMB_ADMIN_WORKSPACE and OMB_ADMIN_MEMBERSHIP");
72
+ if (env.OMB_CLOUD_ROLE !== "home")
73
+ invalid('OMB_CLOUD_ROLE must be "home"; this image runs the home machine');
74
+ const machineId = env.OMB_CLOUD_MACHINE_ID ?? "";
75
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{2,127}$/.test(machineId))
76
+ invalid("OMB_CLOUD_MACHINE_ID must be the Admin's machine id (letters, digits, dashes)");
77
+ const adminOrigin = exactHttpsOrigin(env.OMB_CLOUD_ADMIN_URL, "OMB_CLOUD_ADMIN_URL");
78
+ const publicOrigin = exactHttpsOrigin(env.OMB_PUBLIC_URL, "OMB_PUBLIC_URL");
79
+ const bootstrapSecret = env.OMB_CLOUD_BOOTSTRAP_SECRET ?? "";
80
+ if (!/^[A-Za-z0-9_-]{43,128}$/.test(bootstrapSecret))
81
+ invalid("OMB_CLOUD_BOOTSTRAP_SECRET must be at least 256 bits of base64url");
82
+ const warnings = [];
83
+ const ignored = CLOUD_IGNORED_KEYS.filter((key) => env[key] !== undefined);
84
+ if (ignored.length)
85
+ warnings.push(`ignoring ${ignored.join(", ")}: Cloud Pro includes no AI; people sign in with their own Claude or ChatGPT account, or an API key`);
86
+ return { machineId, adminOrigin, publicOrigin, bootstrapSecret, warnings };
87
+ }
88
+ /** The environment without a platform gateway's settings (CLOUD_IGNORED_KEYS). */
89
+ export function withoutIgnoredCloudKeys(env) {
90
+ const kept = { ...env };
91
+ for (const key of CLOUD_IGNORED_KEYS)
92
+ delete kept[key];
93
+ return kept;
94
+ }
95
+ /** The Admin's side of the signature (openmaus-cloud cloudPairingSignature). */
96
+ export function cloudPairingSignature(secret, timestamp, nonce, body) {
97
+ const bodyHash = createHash("sha256").update(body).digest("base64url");
98
+ return createHmac("sha256", secret).update(`v1\n${timestamp}\n${nonce}\nPOST\n${CLOUD_PAIRING_PATH}\n${bodyHash}`).digest("base64url");
99
+ }
100
+ /** The Admin's signed request for one pairing window on this machine. */
101
+ export function createCloudPairing(options) {
102
+ const { secret, sessions } = options;
103
+ const now = options.now ?? Date.now;
104
+ const nonces = new Map();
105
+ const refused = (error) => ({ status: 401, body: { error } });
106
+ return {
107
+ /** Everything is checked before anything is opened: the signature (in
108
+ * constant time; a bad one counts toward the pairing lockout), then the
109
+ * clock, then the nonce, then the body. Nothing here is ever logged. */
110
+ handle(input) {
111
+ const lock = sessions.attemptAllowed(input.source);
112
+ if (!lock.ok)
113
+ return { status: 429, body: { error: "rate_limited", retryAfterSeconds: Math.ceil(lock.retryAfterMs / 1000) } };
114
+ const timestamp = input.timestamp ?? "", nonce = input.nonce ?? "";
115
+ const presented = /^v1=([A-Za-z0-9_-]{43})$/.exec(input.signature ?? "")?.[1];
116
+ const expected = Buffer.from(cloudPairingSignature(secret, timestamp, nonce, input.body), "base64url");
117
+ const given = presented ? Buffer.from(presented, "base64url") : Buffer.alloc(expected.length);
118
+ const matches = given.length === expected.length && timingSafeEqual(given, expected);
119
+ if (!presented || !matches || !/^\d{1,12}$/.test(timestamp) || !/^[A-Za-z0-9_-]{16,128}$/.test(nonce)) {
120
+ sessions.noteFailure(input.source);
121
+ return refused("invalid_signature");
122
+ }
123
+ sessions.clearFailures(input.source);
124
+ const at = now();
125
+ if (Math.abs(at / 1000 - Number(timestamp)) > CLOUD_PAIRING_SKEW_S)
126
+ return refused("stale_request");
127
+ for (const [seen, until] of nonces)
128
+ if (until <= at)
129
+ nonces.delete(seen);
130
+ if (nonces.has(nonce))
131
+ return refused("replayed_request");
132
+ if (nonces.size >= MAX_NONCES)
133
+ return { status: 503, body: { error: "busy" } };
134
+ nonces.set(nonce, at + CLOUD_PAIRING_NONCE_MS);
135
+ let parsed;
136
+ try {
137
+ parsed = input.body.length ? JSON.parse(input.body.toString("utf8")) : {};
138
+ }
139
+ catch {
140
+ return { status: 400, body: { error: "invalid_body" } };
141
+ }
142
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
143
+ return { status: 400, body: { error: "invalid_body" } };
144
+ const { label, ttlSeconds } = parsed;
145
+ // oxlint-disable-next-line no-control-regex
146
+ if (label !== undefined && (typeof label !== "string" || label.length > 80 || /[\x00-\x1f\x7f]/.test(label)))
147
+ return { status: 400, body: { error: "invalid_label" } };
148
+ if (ttlSeconds !== undefined && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1))
149
+ return { status: 400, body: { error: "invalid_ttl" } };
150
+ const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, CLOUD_PAIRING_MAX_TTL_S);
151
+ const opened = sessions.openPairing({
152
+ scopes: ["admin", "client"],
153
+ label: typeof label === "string" && label.trim() ? label.trim() : "OMB Cloud",
154
+ ttlMs: ttl * 1000,
155
+ });
156
+ return { status: 200, body: { code: formatPairingCode(opened.code), credential: opened.credential, expiresAt: opened.expiresAt } };
157
+ },
158
+ };
159
+ }
160
+ /** The exact bytes the Admin signed, bounded. */
161
+ export function readSignedBody(req, limit = 4096) {
162
+ return new Promise((resolve, reject) => {
163
+ const chunks = [];
164
+ let size = 0, done = false;
165
+ req.on("data", (chunk) => {
166
+ if (done)
167
+ return;
168
+ size += chunk.length;
169
+ if (size > limit) {
170
+ done = true;
171
+ reject(Object.assign(new Error("body too large"), { status: 413 }));
172
+ return;
173
+ }
174
+ chunks.push(chunk);
175
+ });
176
+ req.on("end", () => { if (!done) {
177
+ done = true;
178
+ resolve(Buffer.concat(chunks));
179
+ } });
180
+ req.on("error", (error) => { if (!done) {
181
+ done = true;
182
+ reject(Object.assign(error, { status: 400 }));
183
+ } });
184
+ });
185
+ }
186
+ // The image's home directory may carry the shell files useradd seeds; a
187
+ // fresh Fly volume carries lost+found. Anything else is someone's data.
188
+ const ADOPTABLE = new Set(["lost+found", ".bash_logout", ".bashrc", ".profile"]);
189
+ /** Bind the volume to this machine on first boot and refuse any other
190
+ * machine's volume afterwards. Never deletes or rewrites existing data. */
191
+ export function prepareCloudHomeVolume(home, machineId) {
192
+ mkdirSync(home, { recursive: true, mode: 0o700 });
193
+ const marker = join(home, CLOUD_HOME_MARKER);
194
+ if (existsSync(marker)) {
195
+ const stat = lstatSync(marker);
196
+ let saved;
197
+ try {
198
+ if (!stat.isFile() || stat.size > 4096)
199
+ throw new Error();
200
+ saved = JSON.parse(readFileSync(marker, "utf8"));
201
+ }
202
+ catch {
203
+ throw new Error("This volume's Cloud marker is unreadable. No data was changed.");
204
+ }
205
+ if (saved?.machine !== machineId)
206
+ throw new Error("This volume belongs to another Cloud machine. No data was changed.");
207
+ return "existing";
208
+ }
209
+ if (readdirSync(home).some((name) => !ADOPTABLE.has(name))) {
210
+ throw new Error("Refusing to adopt a data volume that was not made for this Cloud machine. No data was changed.");
211
+ }
212
+ writeFileAtomic(marker, JSON.stringify({ version: 1, machine: machineId }) + "\n", { mode: 0o600 });
213
+ return "new";
214
+ }
215
+ /** What the edge proxy may answer for: the machine's own public name. */
216
+ export function cloudHomeHost(config) {
217
+ return new URL(config.publicOrigin).hostname;
218
+ }
@@ -0,0 +1,167 @@
1
+ /** Consent, cost, and idle-stop policy for overflowing a local computer
2
+ * wait onto a per-second-billed cloud seat (#1655).
3
+ *
4
+ * Like server/claim-idle.ts this module is deliberately synchronous and
5
+ * clock-injected: the server decides every transition on its single
6
+ * thread and tests pin time exactly. Nothing here starts or stops a
7
+ * machine — the wiring does, and only when the decision says to. Keep
8
+ * this module free of server imports. */
9
+ import { computerWaitDuration } from "./computer-wait.js";
10
+ /** Default idle stop: a cloud seat no real screen work has touched for
11
+ * this long is stopped (archived), because billing runs per second while
12
+ * it runs. "A few idle minutes" per the issue. */
13
+ export const CLOUD_SEAT_IDLE_STOP_MS = 5 * 60_000;
14
+ /** Retry cadence for an idle stop that failed (#1655): the first retry
15
+ * after one minute, doubling per failure up to ten. A failed stop keeps
16
+ * its lease — the seat still bills — so the sweep always retries instead
17
+ * of abandoning a running machine. */
18
+ export const CLOUD_SEAT_SLEEP_RETRY_MS = 60_000;
19
+ export const CLOUD_SEAT_SLEEP_RETRY_MAX_MS = 10 * 60_000;
20
+ export function cloudOverflowAction(situation) {
21
+ if (!situation.featureEnabled || !situation.cloudConfigured || situation.perSecondCostUsd === null)
22
+ return { kind: "off" };
23
+ if (situation.started)
24
+ return { kind: "none" };
25
+ if (situation.consented)
26
+ return { kind: "start" };
27
+ if (situation.offered)
28
+ return { kind: "none" };
29
+ return { kind: "offer" };
30
+ }
31
+ /** Per-conversation consent plus the offer bookkeeping that keeps the
32
+ * card to one per conversation until it is revoked. A thread the operator
33
+ * allowlisted in config carries standing consent and never needs the
34
+ * card. */
35
+ export class CloudOverflowConsent {
36
+ grants = new Map();
37
+ offers = new Map();
38
+ revocations = new Set();
39
+ /** Consent for this conversation. With `perSecondCostUsd`, a card
40
+ * consent counts only at the rate its card showed — an allowlisted
41
+ * thread carries the operator's standing consent, and the operator is
42
+ * also the one who sets the rate. */
43
+ consented(threadId, allowlistedThreads = new Set(), perSecondCostUsd) {
44
+ if (this.revocations.has(threadId))
45
+ return false;
46
+ if (allowlistedThreads.has(threadId))
47
+ return true;
48
+ const grant = this.grants.get(threadId);
49
+ if (!grant)
50
+ return false;
51
+ return perSecondCostUsd === undefined || grant.perSecondCostUsd === perSecondCostUsd;
52
+ }
53
+ /** `perSecondCostUsd` is the rate the answered card showed. */
54
+ grant(threadId, perSecondCostUsd, now = Date.now()) {
55
+ this.revocations.delete(threadId);
56
+ this.grants.set(threadId, { at: now, perSecondCostUsd });
57
+ }
58
+ /** Revoking also clears the offered mark, so a later wait in the same
59
+ * conversation may ask again instead of meeting silence. */
60
+ revoke(threadId) {
61
+ const had = this.grants.delete(threadId);
62
+ this.revocations.add(threadId);
63
+ this.offers.delete(threadId);
64
+ return had;
65
+ }
66
+ /** A card is outstanding for this conversation — at the given rate
67
+ * when one is supplied, so a config change re-offers with the new
68
+ * price instead of meeting silence at the old one. */
69
+ offered(threadId, perSecondCostUsd) {
70
+ const offer = this.offers.get(threadId);
71
+ if (!offer)
72
+ return false;
73
+ return perSecondCostUsd === undefined || offer.perSecondCostUsd === perSecondCostUsd;
74
+ }
75
+ /** The rate the outstanding card showed, or null when no card is out. */
76
+ offeredRate(threadId) {
77
+ return this.offers.get(threadId)?.perSecondCostUsd ?? null;
78
+ }
79
+ markOffered(threadId, perSecondCostUsd, now = Date.now()) {
80
+ this.offers.set(threadId, { at: now, perSecondCostUsd });
81
+ }
82
+ }
83
+ /** One running cloud seat started by overflow. `touch` is called only
84
+ * for real computer tool completions on that seat — screen-poller frames
85
+ * never reach it, exactly like #1653's activity clock, so preview
86
+ * traffic cannot keep a paid machine awake. */
87
+ export class CloudSeatLease {
88
+ botId;
89
+ threadId;
90
+ /** The claim generation that started the seat: a stop releases the Box
91
+ * claim for exactly this owner, never a newer turn's. */
92
+ generation;
93
+ startedAt;
94
+ idleStopMs;
95
+ lastActivityAt;
96
+ sleepRetryNotBefore = 0;
97
+ sleepRetryMs = CLOUD_SEAT_SLEEP_RETRY_MS;
98
+ constructor(init) {
99
+ const idleStopMs = init.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
100
+ if (!Number.isFinite(idleStopMs) || idleStopMs <= 0)
101
+ throw new Error("Cloud seat idle stop window must be positive");
102
+ this.botId = init.botId;
103
+ this.threadId = init.threadId;
104
+ this.generation = init.generation;
105
+ this.startedAt = init.now ?? Date.now();
106
+ this.lastActivityAt = this.startedAt;
107
+ this.idleStopMs = idleStopMs;
108
+ }
109
+ touch(now = Date.now()) {
110
+ this.lastActivityAt = now;
111
+ // Real work earns a fresh stop attempt: the backoff only paces
112
+ // retries against an idle seat that keeps failing to sleep.
113
+ this.sleepRetryNotBefore = 0;
114
+ this.sleepRetryMs = CLOUD_SEAT_SLEEP_RETRY_MS;
115
+ }
116
+ /** An idle stop may run now — true until a failure defers it. */
117
+ sleepDue(now = Date.now()) {
118
+ return now >= this.sleepRetryNotBefore;
119
+ }
120
+ /** The idle stop failed: keep the lease and back off, bounded, so the
121
+ * sweep retries gently instead of hammering a sick Box — and never
122
+ * abandons a seat that still bills. */
123
+ deferSleep(now = Date.now()) {
124
+ this.sleepRetryNotBefore = now + this.sleepRetryMs;
125
+ this.sleepRetryMs = Math.min(this.sleepRetryMs * 2, CLOUD_SEAT_SLEEP_RETRY_MAX_MS);
126
+ }
127
+ idleFor(now = Date.now()) {
128
+ return Math.max(0, now - this.lastActivityAt);
129
+ }
130
+ idleElapsed(now = Date.now()) {
131
+ return now - this.lastActivityAt >= this.idleStopMs;
132
+ }
133
+ }
134
+ /** Dollars per second at rate-card precision: enough places for small
135
+ * per-second rates to stay visible, trailing zeros trimmed but never
136
+ * past two decimals. */
137
+ export function formatPerSecondUsd(perSecondCostUsd) {
138
+ let text = perSecondCostUsd.toFixed(6).replace(/0+$/, "");
139
+ if (text.endsWith("."))
140
+ text += "00";
141
+ else {
142
+ const decimals = text.length - text.indexOf(".") - 1;
143
+ if (decimals < 2)
144
+ text += "0".repeat(2 - decimals);
145
+ }
146
+ return "$" + text;
147
+ }
148
+ /** The consent card (#1655): the per-second cost sits beside the local
149
+ * wait picture before any choice is made, and nothing starts without an
150
+ * explicit consent the person still has to give. */
151
+ export function cloudOverflowOfferText(opts) {
152
+ const idleStopMs = opts.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
153
+ const estimate = opts.waitEstimateMs === undefined ? "" : ` Recent local waits here have taken about ${computerWaitDuration(opts.waitEstimateMs)}.`;
154
+ return `This computer is busy. Its work can overflow to a cloud computer for ${formatPerSecondUsd(opts.perSecondCostUsd)} per second.${estimate} Nothing starts without your consent — allow cloud overflow for this conversation to start it; the cloud computer stops after ${computerWaitDuration(idleStopMs)} idle.`;
155
+ }
156
+ export function cloudSeatStartedText(opts) {
157
+ const idleStopMs = opts.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
158
+ return `Cloud computer started at ${formatPerSecondUsd(opts.perSecondCostUsd)} per second. It stops automatically after ${computerWaitDuration(idleStopMs)} idle, and the local wait continues until this computer is free.`;
159
+ }
160
+ export function cloudSeatStoppedText(idleMs) {
161
+ return `Cloud computer stopped after ${computerWaitDuration(idleMs)} idle — billing pauses while it sleeps.`;
162
+ }
163
+ export function cloudOverflowConsentText(allowed, idleStopMs = CLOUD_SEAT_IDLE_STOP_MS) {
164
+ return allowed
165
+ ? `Cloud overflow allowed for this conversation. A waiting turn may start the cloud computer; it stops after ${computerWaitDuration(idleStopMs)} idle.`
166
+ : "Cloud overflow consent revoked. Waits stay local, and no cloud computer starts without a new consent.";
167
+ }
@@ -196,6 +196,11 @@ export function connectorAvailability(cfg, storeState = process.env.OMB_CREDENTI
196
196
  return "configured";
197
197
  return storeState === "unavailable" ? "unreadable" : "unconfigured";
198
198
  }
199
+ export function connectorSetup(cfg, desktopManaged = process.env.OMB_DESKTOP_PARENT === "1") {
200
+ if (configured(cfg))
201
+ return "ready";
202
+ return desktopManaged ? "service-unavailable" : "needs-setup";
203
+ }
199
204
  async function brokerRequest(path, init) {
200
205
  const broker = brokerAccess();
201
206
  if (!broker)
@@ -99,7 +99,7 @@ export class ObservationCoordinator {
99
99
  this.metrics.retries += 1;
100
100
  }
101
101
  /** canonicalFrame must describe the full screenshot, even when the image
102
- * returned to the model is cropped. A box-provided full-frame hash works. */
102
+ * returned to the model is cropped. A boat-provided full-frame hash works. */
103
103
  observeFrame(canonicalFrame, crop) {
104
104
  this.metrics.screenshotsCaptured += 1;
105
105
  const hash = canonicalFrame
@@ -107,7 +107,7 @@ export class ObservationCoordinator {
107
107
  : null;
108
108
  const view = crop ? `${crop.x},${crop.y},${crop.width},${crop.height}` : "full";
109
109
  const signature = hash ? `${hash}:${view}` : null;
110
- // If the box cannot provide a full-frame hash, fail open and send the
110
+ // If the boat cannot provide a full-frame hash, fail open and send the
111
111
  // valid image. Suppressing a possibly-new crop would be worse.
112
112
  const changed = signature === null || signature !== this.lastObservation;
113
113
  if (signature)