openmausbot 0.1.85 → 0.1.87
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.
- package/README.md +2 -2
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/abnfDiagram-O67JEVCF-BTiuL79F.js +1 -0
- package/dist/assets/arc-C88Blc2v.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-BZsJIZrb.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CVeMe9Ah.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-Cedk3Bhx.js +38 -0
- package/dist/assets/channel-taCNRlUU.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-D-VRqzTB.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BChov01Y.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-C-TfwvHy.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-Db7CGj5Y.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-CNvIfqKT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-C_bEp5tq.js +1 -0
- package/dist/assets/chunk-NETBCI7D-Coi4BOls.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-CzylEuL3.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-m7p7ztaD.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BMy6o9x2.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-BDQ__0_J.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-Djv4ghUb.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-BULrlGYH.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-D-jrT_hU.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-p2UhQmfL.js +200 -0
- package/dist/assets/diagram-3UASUU5V-DHvGWZ7q.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-ChhKDot-.js +3 -0
- package/dist/assets/diagram-CDSNMT55-B-T7A43R.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-CJm8ENNY.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-510tlza8.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-PkvRPHel.js +1 -0
- package/dist/assets/elk-276RUBZZ-BVBVPoX3.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-BgYMVm7L.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-BGqsnc_3.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-D_-UeReH.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-D0uooONx.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BwdMAQ6p.css +1 -0
- package/dist/assets/index-ByJennr7.js +581 -0
- package/dist/assets/{index-CTNNoqSJ.js → index-zRsJERGJ.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-B9e1z77f.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-BB4vNwvD.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-DpkosOTB.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-DbDP4kUu.js +89 -0
- package/dist/assets/linear-DBJGPmjT.js +1 -0
- package/dist/assets/mermaid.core-DWPRi-ue.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-nszBbJmx.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-CWykJWu_.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-g3Da-Vnv.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-Wzrlpzgh.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-moHTFMC0.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-CwfXdFH8.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-3ZROYvnq.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-E1TBebP8.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-DHKEU5pu.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-BguEj09D.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-C7IDYnhd.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-DCW-F5PW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-xBMFtWYV.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-B5nIkuH8.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DYFAhdbr.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-DeXwX6Cz.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/container-mcp.js +1047 -173
- package/dist-server/drivers/agents-proxy.js +16121 -1504
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +16134 -7446
- package/dist-server/local-computer-proxy.js +3 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +9353 -6124
- package/dist-server/pair-cli.js +9353 -6124
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/attachments.js +55 -0
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +59 -56
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +38 -2
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +81 -12
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +133 -17
- package/dist-server/server/connector-verdict.js +141 -0
- package/dist-server/server/container-computer.js +1 -1
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +29 -3
- package/dist-server/server/delegations.js +97 -44
- package/dist-server/server/drivers/acp/core.js +163 -33
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1162 -0
- package/dist-server/server/drivers/agents-catalog.js +689 -0
- package/dist-server/server/drivers/agents-client.js +28 -0
- package/dist-server/server/drivers/agents-proxy.js +18 -1722
- package/dist-server/server/drivers/boxagent.js +174 -36
- package/dist-server/server/drivers/builtIn.js +2 -0
- package/dist-server/server/drivers/chat-box-tools.js +90 -0
- package/dist-server/server/drivers/chat-images.js +54 -0
- package/dist-server/server/drivers/chat-mcp-tools.js +55 -10
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +28 -8
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +121 -34
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +155 -32
- package/dist-server/server/drivers/openai-compat.js +16 -8
- package/dist-server/server/drivers/permission-command.js +35 -0
- package/dist-server/server/drivers/phone-proxy.js +47 -2
- package/dist-server/server/drivers/pi.js +75 -4
- package/dist-server/server/drivers/prompt-split.js +87 -0
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/fleet.js +1 -1
- package/dist-server/server/group-thread-usage.js +103 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -0
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/hosted-models.js +151 -0
- package/dist-server/server/hosted-slack.js +11 -0
- package/dist-server/server/index.js +3091 -658
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +28 -0
- package/dist-server/server/model-context-window.js +7 -0
- package/dist-server/server/model-prices.js +121 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/notify.js +10 -1
- package/dist-server/server/org-library.js +1051 -0
- package/dist-server/server/package-export.js +627 -39
- package/dist-server/server/package-import.js +458 -0
- package/dist-server/server/package-parts.js +87 -0
- package/dist-server/server/peer-approval.js +16 -7
- package/dist-server/server/peer-roster.js +23 -2
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/recent-work.js +15 -0
- package/dist-server/server/request-auth.js +96 -0
- package/dist-server/server/room-handoffs.js +44 -14
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routine-requests.js +6 -3
- package/dist-server/server/routines.js +63 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skills.js +34 -61
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +9 -3
- package/dist-server/server/store.js +550 -35
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +12 -4
- package/dist-server/server/team-backup.js +16 -3
- package/dist-server/server/team-computers.js +10 -0
- package/dist-server/server/team-library.js +6 -2
- package/dist-server/server/team-manifest.js +4 -3
- package/dist-server/server/team-setup-requests.js +16 -5
- package/dist-server/server/thread-events.js +17 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/turn-context.js +2 -0
- package/dist-server/server/turn-watchdog.js +12 -3
- package/dist-server/server/usage-ledger.js +43 -9
- package/dist-server/server/vps-computer.js +139 -51
- package/dist-server/server/vps-container-mcp.js +7 -0
- package/dist-server/server/vps-ssh.js +36 -7
- package/dist-server/server/workspace-backup-policy.js +21 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +188 -12
- package/dist-server/shared/bot-profile.js +15 -0
- package/dist-server/shared/command-allowlist.js +1 -0
- package/dist-server/shared/credential-request.js +1 -1
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +9 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +1147 -179
- package/enterprise/README.md +33 -12
- package/enterprise/server/index.js +5 -5
- package/package.json +1 -1
- package/dist/assets/index-Ba9G44HI.js +0 -310
- package/dist/assets/index-CkRNp7wX.css +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// The stable/volatile prompt split, shared by the drivers that deliver it.
|
|
2
|
+
// The stable half is everything that must stay byte-identical for a
|
|
3
|
+
// provider's cached prefix (or a spawned CLI's session contract) to
|
|
4
|
+
// survive; the volatile half (memory, mentions, outstanding teammate
|
|
5
|
+
// work, recent work) legitimately changes mid-conversation and reaches
|
|
6
|
+
// the model inside the turn that changed it, after the cacheable prefix.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { DATA_DIR } from "../config.js";
|
|
11
|
+
import { writeFileAtomic } from "../atomic.js";
|
|
12
|
+
/** Read the split off a turn. Both halves must be present: a driver that
|
|
13
|
+
* receives only the unsplit system field was called through a path that
|
|
14
|
+
* never split it. */
|
|
15
|
+
export function promptHalves(turn) {
|
|
16
|
+
const { systemStable, systemVolatile } = turn;
|
|
17
|
+
if (typeof systemStable !== "string" || typeof systemVolatile !== "string") {
|
|
18
|
+
return { stable: null, volatile: "" };
|
|
19
|
+
}
|
|
20
|
+
return { stable: systemStable, volatile: systemVolatile };
|
|
21
|
+
}
|
|
22
|
+
export const VOLATILE_CONTEXT_NOTE_PREFIX = "Context from OpenMausBot updated since this conversation started; it replaces any earlier copy:";
|
|
23
|
+
export const VOLATILE_CONTEXT_CLEARED_NOTE = "The OpenMausBot context notes from earlier in this conversation (memory, mentions, outstanding teammate work) have been cleared; the standing instructions still apply.";
|
|
24
|
+
/** The labelled block that carries a changed volatile half inside a user
|
|
25
|
+
* turn. A half that is empty and always was needs no note; one that was
|
|
26
|
+
* cleared announces the removal so the model stops relying on it. */
|
|
27
|
+
export function volatileContextNote(volatile, hadVolatile) {
|
|
28
|
+
const text = volatile.trim();
|
|
29
|
+
if (text)
|
|
30
|
+
return VOLATILE_CONTEXT_NOTE_PREFIX + "\n\n" + text;
|
|
31
|
+
return hadVolatile ? VOLATILE_CONTEXT_CLEARED_NOTE : "";
|
|
32
|
+
}
|
|
33
|
+
/** Prepend a delivered note to the turn text: the one composition shape
|
|
34
|
+
* every split-aware driver uses. */
|
|
35
|
+
export function withContextNote(note, text) {
|
|
36
|
+
if (!note)
|
|
37
|
+
return text;
|
|
38
|
+
return text ? note + "\n\n" + text : note;
|
|
39
|
+
}
|
|
40
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
41
|
+
const EMPTY_FINGERPRINT = digest("");
|
|
42
|
+
export function promptSplitFingerprints(stable, volatile) {
|
|
43
|
+
return { stable: digest(stable), volatile: digest(volatile) };
|
|
44
|
+
}
|
|
45
|
+
const receiptPath = (scope, key) => join(DATA_DIR, "prompt-split", digest(JSON.stringify([scope, key])) + ".json");
|
|
46
|
+
/** Which halves a native session (an ACP session id, a pi session file)
|
|
47
|
+
* last carried. Unknown - never tracked, or tracked before this file
|
|
48
|
+
* existed - reads as null, and the caller re-delivers the full prompt
|
|
49
|
+
* once. */
|
|
50
|
+
export function readPromptSplitReceipt(scope, key) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = JSON.parse(readFileSync(receiptPath(scope, key), "utf8"));
|
|
53
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
54
|
+
const record = raw;
|
|
55
|
+
if (typeof record.stable === "string" && typeof record.volatile === "string") {
|
|
56
|
+
return { stable: record.stable, volatile: record.volatile };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* a missing or corrupt receipt is simply unknown */
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
export function writePromptSplitReceipt(scope, key, receipt) {
|
|
66
|
+
mkdirSync(join(DATA_DIR, "prompt-split"), { recursive: true });
|
|
67
|
+
writeFileAtomic(receiptPath(scope, key), JSON.stringify(receipt), { mode: 0o600 });
|
|
68
|
+
}
|
|
69
|
+
/** Compose the prompt for a session that persists its own history (ACP
|
|
70
|
+
* agents, pi): the full system block rides only when this native session
|
|
71
|
+
* has not carried it - or carried a different one, which re-delivers the
|
|
72
|
+
* current copy the way a pre-split turn always did - and a changed
|
|
73
|
+
* volatile half rides as a labelled note. Otherwise the turn text goes
|
|
74
|
+
* through bare, so an ordinary memory write neither appends a second copy
|
|
75
|
+
* of the prompt to the session nor re-prices its cached prefix.
|
|
76
|
+
* perTurnVolatile marks a turn whose volatile half describes this very
|
|
77
|
+
* turn (a mention): its note is delivered even when the text is unchanged. */
|
|
78
|
+
export function splitSessionPrompt(stable, volatile, previous, fullSystem, text, perTurnVolatile = false) {
|
|
79
|
+
const receipt = promptSplitFingerprints(stable, volatile);
|
|
80
|
+
if (previous === null || previous.stable !== receipt.stable) {
|
|
81
|
+
return { text: fullSystem ? fullSystem + "\n\n" + text : text, receipt };
|
|
82
|
+
}
|
|
83
|
+
const note = previous.volatile === receipt.volatile && !perTurnVolatile
|
|
84
|
+
? ""
|
|
85
|
+
: volatileContextNote(volatile, previous.volatile !== EMPTY_FINGERPRINT);
|
|
86
|
+
return { text: withContextNote(note, text), receipt };
|
|
87
|
+
}
|
|
@@ -8,10 +8,11 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
// Neutral hook point for the source-available enterprise layer.
|
|
10
10
|
//
|
|
11
|
-
// The layer lives in
|
|
11
|
+
// The layer lives in enterprise/ (its own LICENSE) and is loaded only if that
|
|
12
12
|
// folder is present: delete it and this file still compiles, the server still
|
|
13
13
|
// starts, and /api/edition reports the open-source edition. Core never imports
|
|
14
|
-
// the layer statically
|
|
14
|
+
// the layer statically; a build ships it as its own bundled file beside the
|
|
15
|
+
// server (see enterpriseLayerDirs for where it is looked up).
|
|
15
16
|
// The layer's only obligation is `register()`, which turns OMB_LICENSE_KEY into
|
|
16
17
|
// entitlements; core keeps the resulting status and answers `entitled()`.
|
|
17
18
|
import { existsSync } from "node:fs";
|
|
@@ -25,7 +26,28 @@ const layerSchema = z.object({
|
|
|
25
26
|
features: z.array(z.string().min(1)),
|
|
26
27
|
expiresAt: z.string().nullable(),
|
|
27
28
|
});
|
|
28
|
-
|
|
29
|
+
/** Days before expiry that the startup log, /api/edition and Settings start
|
|
30
|
+
* saying so, and days after it that the features keep working while the
|
|
31
|
+
* operator renews. The key and its verification are unchanged: the layer
|
|
32
|
+
* accepts a key that expired less than the grace period ago. */
|
|
33
|
+
export const LICENSE_WARN_DAYS = 30;
|
|
34
|
+
export const LICENSE_GRACE_DAYS = 7;
|
|
35
|
+
const DAY_MS = 24 * 60 * 60_000;
|
|
36
|
+
/** Where the layer may be, in order. An explicit OMB_ENTERPRISE_DIR is the
|
|
37
|
+
* only place looked at when set. Otherwise: beside the server root (a
|
|
38
|
+
* checkout, where server/ and enterprise/ are siblings, and the npm package,
|
|
39
|
+
* which copies the bundled layer to <package>/enterprise), then inside it
|
|
40
|
+
* (the Docker image and the packaged desktop ship dist-server/ alone, and
|
|
41
|
+
* scripts/bundle-server.mjs writes the layer to dist-server/enterprise). */
|
|
42
|
+
export function enterpriseLayerDirs(env = process.env, serverRoot = SERVER_ROOT) {
|
|
43
|
+
if (env.OMB_ENTERPRISE_DIR)
|
|
44
|
+
return [env.OMB_ENTERPRISE_DIR];
|
|
45
|
+
return [join(serverRoot, "..", "enterprise"), join(serverRoot, "enterprise")];
|
|
46
|
+
}
|
|
47
|
+
/** Source in a checkout, a compiled bundle in an image: same convention as proxy-paths.ts. */
|
|
48
|
+
function layerEntry(dir) {
|
|
49
|
+
return [join(dir, "server", "index.ts"), join(dir, "server", "index.js")].find((candidate) => existsSync(candidate));
|
|
50
|
+
}
|
|
29
51
|
let current = { edition: "oss", features: [] };
|
|
30
52
|
let workspaceAccessFactory;
|
|
31
53
|
export function hostedWorkspaceConfigured(env = process.env) {
|
|
@@ -53,6 +75,26 @@ export function hostedWorkspaceConfiguration(env = process.env) {
|
|
|
53
75
|
return null;
|
|
54
76
|
}
|
|
55
77
|
}
|
|
78
|
+
/** Who decides who may use this workspace, for Settings → People and
|
|
79
|
+
* Remote access. On a portal-membership workspace the organisation's Admin
|
|
80
|
+
* does, and `peopleUrl` opens its People page for this workspace (identifiers
|
|
81
|
+
* only; Admin authorizes its own visitor). A hosted workspace never issues
|
|
82
|
+
* pairing codes or email sign-in: people come in through the portal. */
|
|
83
|
+
export function workspaceMembership(env = process.env) {
|
|
84
|
+
const hosted = hostedWorkspaceConfiguration(env);
|
|
85
|
+
const pairingCodes = !hostedWorkspaceConfigured(env);
|
|
86
|
+
if (!hosted?.portalMembership)
|
|
87
|
+
return { authority: "local", pairingCodes };
|
|
88
|
+
const people = new URL("/people", hosted.admin);
|
|
89
|
+
people.search = new URLSearchParams({ workspace: hosted.workspace }).toString();
|
|
90
|
+
return { authority: "portal", pairingCodes, peopleUrl: people.href };
|
|
91
|
+
}
|
|
92
|
+
/** An operator may authorize new Full tasks only on a dedicated,
|
|
93
|
+
* portal-managed server. This is not an HTTP setting or a desktop grant. */
|
|
94
|
+
export function sharedWorkspaceFullAccessConfigured(env = process.env) {
|
|
95
|
+
return env.OMB_SHARED_WORKSPACE_FULL_ACCESS === "1" && env.OMB_DESKTOP_PARENT !== "1" &&
|
|
96
|
+
hostedWorkspaceConfiguration(env)?.portalMembership === true;
|
|
97
|
+
}
|
|
56
98
|
/** No enterprise import or portal dependency enters the core bundle. A
|
|
57
99
|
* configured server without this hook must refuse hosted access, not fall
|
|
58
100
|
* back to legacy email or QR credentials. */
|
|
@@ -69,12 +111,11 @@ function oss(notice) {
|
|
|
69
111
|
* degrades to the open-source edition with a notice that says what to fix. */
|
|
70
112
|
export async function loadEnterpriseLayer(options = {}) {
|
|
71
113
|
workspaceAccessFactory = undefined;
|
|
72
|
-
const
|
|
114
|
+
const dirs = options.dir ? [options.dir] : enterpriseLayerDirs(options.env, options.serverRoot);
|
|
73
115
|
const licenseKey = options.licenseKey ?? process.env.OMB_LICENSE_KEY;
|
|
74
|
-
|
|
75
|
-
const entry = [join(dir, "server", "index.ts"), join(dir, "server", "index.js")].find((candidate) => existsSync(candidate));
|
|
116
|
+
const entry = dirs.map(layerEntry).find((candidate) => candidate !== undefined);
|
|
76
117
|
if (!entry) {
|
|
77
|
-
return oss(licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${
|
|
118
|
+
return oss(licenseKey ? `OMB_LICENSE_KEY is set but no enterprise layer exists at ${dirs.join(" or ")}` : undefined);
|
|
78
119
|
}
|
|
79
120
|
try {
|
|
80
121
|
const loaded = await import(__rewriteRelativeImportExtension(pathToFileURL(entry).href));
|
|
@@ -86,7 +127,9 @@ export async function loadEnterpriseLayer(options = {}) {
|
|
|
86
127
|
throw new Error(`${entry} does not export register()`);
|
|
87
128
|
if (!licenseKey)
|
|
88
129
|
return oss("enterprise layer present but OMB_LICENSE_KEY is not set");
|
|
89
|
-
|
|
130
|
+
// graceDays is additive: a layer that ignores it refuses an expired key
|
|
131
|
+
// at startup exactly as before.
|
|
132
|
+
const registered = await register({ licenseKey, graceDays: LICENSE_GRACE_DAYS });
|
|
90
133
|
const layer = layerSchema.parse(registered);
|
|
91
134
|
current = {
|
|
92
135
|
edition: "enterprise",
|
|
@@ -100,27 +143,65 @@ export async function loadEnterpriseLayer(options = {}) {
|
|
|
100
143
|
return oss(`enterprise layer disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
101
144
|
}
|
|
102
145
|
}
|
|
146
|
+
/** When the key stops being valid, or null for perpetual / open-source. */
|
|
147
|
+
function expiryAt() {
|
|
148
|
+
if (current.edition !== "enterprise" || !current.expiresAt)
|
|
149
|
+
return null;
|
|
150
|
+
const at = new Date(current.expiresAt).getTime();
|
|
151
|
+
return Number.isFinite(at) ? at : null;
|
|
152
|
+
}
|
|
103
153
|
/** A license that expires while the server keeps running stops granting
|
|
104
|
-
* features at that moment, not at the next restart. */
|
|
154
|
+
* features once its grace period ends, at that moment, not at the next restart. */
|
|
105
155
|
function expired(now) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return now >= new Date(current.expiresAt).getTime();
|
|
156
|
+
const at = expiryAt();
|
|
157
|
+
return at !== null && now >= at + LICENSE_GRACE_DAYS * DAY_MS;
|
|
109
158
|
}
|
|
110
159
|
export function editionStatus(now = Date.now()) {
|
|
111
|
-
|
|
160
|
+
const at = expiryAt();
|
|
161
|
+
if (at === null)
|
|
112
162
|
return current;
|
|
163
|
+
if (expired(now)) {
|
|
164
|
+
return {
|
|
165
|
+
edition: "oss",
|
|
166
|
+
features: [],
|
|
167
|
+
notice: `enterprise layer disabled: OMB_LICENSE_KEY expired on ${current.expiresAt}; renew it to keep enterprise features`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const expiresInDays = Math.ceil((at - now) / DAY_MS);
|
|
171
|
+
if (now < at)
|
|
172
|
+
return { ...current, expiresInDays };
|
|
173
|
+
const graceEndsAt = new Date(at + LICENSE_GRACE_DAYS * DAY_MS).toISOString().slice(0, 10);
|
|
113
174
|
return {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
175
|
+
...current,
|
|
176
|
+
expiresInDays,
|
|
177
|
+
graceEndsAt,
|
|
178
|
+
notice: `OMB_LICENSE_KEY expired on ${current.expiresAt}; enterprise features keep working until ${graceEndsAt} while it is renewed`,
|
|
117
179
|
};
|
|
118
180
|
}
|
|
181
|
+
/** The edition as a non-admin session sees it: what is entitled, without
|
|
182
|
+
* the license's countdown, grace date or operator notice. */
|
|
183
|
+
export function editionForMembers(status) {
|
|
184
|
+
const { expiresInDays: _days, graceEndsAt: _grace, notice: _notice, ...shared } = status;
|
|
185
|
+
return shared;
|
|
186
|
+
}
|
|
119
187
|
/** Feature gates in core ask this and nothing else. Unknown ids are simply not
|
|
120
188
|
* granted, and the open-source edition always carries an empty feature list. */
|
|
121
189
|
export function entitled(feature, now = Date.now()) {
|
|
122
190
|
return !expired(now) && current.features.includes(feature);
|
|
123
191
|
}
|
|
192
|
+
/** The startup warning for a key that is about to lapse or already in its
|
|
193
|
+
* grace period; null when there is nothing to say. */
|
|
194
|
+
export function licenseWarning(status) {
|
|
195
|
+
if (status.edition !== "enterprise" || !status.expiresAt || status.expiresInDays === undefined)
|
|
196
|
+
return null;
|
|
197
|
+
if (status.graceEndsAt) {
|
|
198
|
+
return `OMB_LICENSE_KEY expired on ${status.expiresAt}; enterprise features keep working until ${status.graceEndsAt} — renew the key before then`;
|
|
199
|
+
}
|
|
200
|
+
if (status.expiresInDays > LICENSE_WARN_DAYS)
|
|
201
|
+
return null;
|
|
202
|
+
const days = status.expiresInDays === 1 ? "1 day" : `${status.expiresInDays} days`;
|
|
203
|
+
return `OMB_LICENSE_KEY expires on ${status.expiresAt} (in ${days}); renew it before then to keep enterprise features`;
|
|
204
|
+
}
|
|
124
205
|
/** One line for the startup log. */
|
|
125
206
|
export function describeEdition(status) {
|
|
126
207
|
if (status.edition === "enterprise") {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
const MAX_FILE_BYTES = 1_048_576;
|
|
4
|
+
const ID = /^[\w-]{1,128}$/;
|
|
5
|
+
const fingerprint = (token) => createHash("sha256").update(token).digest("hex");
|
|
6
|
+
const sameFingerprint = (left, right) => {
|
|
7
|
+
const a = Buffer.from(left);
|
|
8
|
+
const b = Buffer.from(right);
|
|
9
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
10
|
+
};
|
|
11
|
+
/** Read a bounded regular file through the descriptor whose permissions we
|
|
12
|
+
* checked. A pipe must not block the request loop, nor a pathname swap replace
|
|
13
|
+
* the checked file before the read. */
|
|
14
|
+
function registrations(file) {
|
|
15
|
+
let fd;
|
|
16
|
+
try {
|
|
17
|
+
fd = openSync(file, constants.O_RDONLY | (process.platform === "win32" ? 0 : constants.O_NOFOLLOW | constants.O_NONBLOCK));
|
|
18
|
+
const stat = fstatSync(fd);
|
|
19
|
+
if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
|
|
20
|
+
return [];
|
|
21
|
+
if (process.platform !== "win32" && (stat.mode & 0o077) !== 0) {
|
|
22
|
+
console.warn("[comms] ignoring external runtime credentials: use a private file (chmod 600)");
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const bytes = Buffer.alloc(stat.size + 1);
|
|
26
|
+
let count = 0;
|
|
27
|
+
for (;;) {
|
|
28
|
+
const read = readSync(fd, bytes, count, bytes.length - count, null);
|
|
29
|
+
if (!read)
|
|
30
|
+
break;
|
|
31
|
+
count += read;
|
|
32
|
+
if (count > stat.size)
|
|
33
|
+
return []; // changed while being read
|
|
34
|
+
}
|
|
35
|
+
const parsed = JSON.parse(bytes.subarray(0, count).toString("utf8"));
|
|
36
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
37
|
+
return [];
|
|
38
|
+
return Object.entries(parsed).flatMap(([botId, value]) => {
|
|
39
|
+
if (!ID.test(botId) || !value || typeof value !== "object" || Array.isArray(value))
|
|
40
|
+
return [];
|
|
41
|
+
const { token, threadId } = value;
|
|
42
|
+
if (typeof token !== "string" || token.length < 32 || token.length > 4096 || /\s/.test(token))
|
|
43
|
+
return [];
|
|
44
|
+
if (typeof threadId !== "string" || !ID.test(threadId))
|
|
45
|
+
return [];
|
|
46
|
+
return [{ botId, tokenHash: fingerprint(token), threadId }];
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (fd !== undefined)
|
|
54
|
+
closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolveRegistration(file, tokenHash, botForId) {
|
|
58
|
+
const matches = registrations(file).filter(entry => sameFingerprint(entry.tokenHash, tokenHash));
|
|
59
|
+
// A copied secret must not select a different bot based on JSON key order.
|
|
60
|
+
if (matches.length !== 1)
|
|
61
|
+
return null;
|
|
62
|
+
const registration = matches[0];
|
|
63
|
+
const bot = botForId(registration.botId);
|
|
64
|
+
if (!bot || bot.hidden)
|
|
65
|
+
return null;
|
|
66
|
+
// There is no durable "main thread": bot.threadId is the UI selection and
|
|
67
|
+
// new tasks are prepended. Every grant needs an explicit binding.
|
|
68
|
+
const task = bot.tasks?.find(candidate => candidate.threadId === registration.threadId);
|
|
69
|
+
if (!task || task.archivedAt !== undefined)
|
|
70
|
+
return null;
|
|
71
|
+
return { botId: bot.id, threadId: task.threadId, tokenHash };
|
|
72
|
+
}
|
|
73
|
+
export function authorizeExternalRuntime(file, header, botForId) {
|
|
74
|
+
if (typeof header !== "string" || !/^Bearer \S{32,4096}$/.test(header))
|
|
75
|
+
return null;
|
|
76
|
+
return resolveRegistration(file, fingerprint(header.slice(7)), botForId);
|
|
77
|
+
}
|
|
78
|
+
/** Re-check after every body/approval/poll wait. The captured identity is not
|
|
79
|
+
* a lease: token rotation, rebinding, deletion and archive take effect now. */
|
|
80
|
+
export function externalRuntimeIsActive(file, grant, botForId) {
|
|
81
|
+
const current = resolveRegistration(file, grant.tokenHash, botForId);
|
|
82
|
+
return current?.botId === grant.botId && current.threadId === grant.threadId;
|
|
83
|
+
}
|
|
@@ -250,7 +250,7 @@ export function initPlan(input) {
|
|
|
250
250
|
...(input.operator ? [{ kind: "run", argv: ["systemctl", "enable", "--now", "openmausbot-fleet.service"], why: `start the fleet agent for ${input.operator}` }] : []),
|
|
251
251
|
{ kind: "run", argv: ["systemctl", "reload", "caddy"], why: "start serving the workspaces folder" },
|
|
252
252
|
{ kind: "note", text: `point *.${registry.domain} at this server (a wildcard A/AAAA record); each workspace gets its own certificate when created` },
|
|
253
|
-
...(input.operator ? [{ kind: "note", text: `the
|
|
253
|
+
...(input.operator ? [{ kind: "note", text: `the installation running as ${input.operator} can now manage installations from Settings → Installations` }] : []),
|
|
254
254
|
];
|
|
255
255
|
return { steps, registry };
|
|
256
256
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
4
|
+
const valid = (row) => Boolean(row && typeof row.threadId === "string" && typeof row.botId === "string" && typeof row.at === "string" && Number.isFinite(Date.parse(row.at)) && finite(row.input) && finite(row.output));
|
|
5
|
+
function addRow(totals, row) {
|
|
6
|
+
const summary = totals.get(row.threadId) ?? { usage: { input: 0, output: 0, costUsd: null, turns: 0, cachedInput: 0 }, lastAt: -Infinity };
|
|
7
|
+
const total = summary.usage;
|
|
8
|
+
const input = Math.trunc(row.input), output = Math.trunc(row.output);
|
|
9
|
+
const cached = finite(row.cachedInput) ? Math.min(input, Math.trunc(row.cachedInput)) : undefined;
|
|
10
|
+
total.input += input;
|
|
11
|
+
total.output += output;
|
|
12
|
+
total.turns++;
|
|
13
|
+
if (cached === undefined)
|
|
14
|
+
delete total.cachedInput;
|
|
15
|
+
else if (total.cachedInput !== undefined)
|
|
16
|
+
total.cachedInput += cached;
|
|
17
|
+
if (finite(row.costUsd))
|
|
18
|
+
total.costUsd = (total.costUsd ?? 0) + row.costUsd;
|
|
19
|
+
const at = Date.parse(row.at);
|
|
20
|
+
if (at >= summary.lastAt) {
|
|
21
|
+
summary.lastAt = at;
|
|
22
|
+
total.lastTurn = { input, output, ...(cached === undefined ? {} : { cachedInput: cached }), costUsd: finite(row.costUsd) ? row.costUsd : null };
|
|
23
|
+
total.lastSpeaker = { botId: row.botId, name: typeof row.botName === "string" && row.botName ? row.botName : row.botId };
|
|
24
|
+
}
|
|
25
|
+
totals.set(row.threadId, summary);
|
|
26
|
+
}
|
|
27
|
+
/** Cache per-month/per-thread summaries, not the complete ledger in memory. */
|
|
28
|
+
export class GroupUsageReader {
|
|
29
|
+
files = new Map();
|
|
30
|
+
signature = "";
|
|
31
|
+
totals = new Map();
|
|
32
|
+
dataDir;
|
|
33
|
+
constructor(dataDir) { this.dataDir = dataDir; }
|
|
34
|
+
/** Refresh only at startup or after ledger persistence, never on an SSE lookup. */
|
|
35
|
+
refresh() {
|
|
36
|
+
const dir = join(this.dataDir, "usage");
|
|
37
|
+
let names;
|
|
38
|
+
try {
|
|
39
|
+
names = readdirSync(dir).filter(name => /^\d{4}-\d{2}\.jsonl$/.test(name)).sort();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT")
|
|
43
|
+
throw error;
|
|
44
|
+
names = [];
|
|
45
|
+
}
|
|
46
|
+
const files = names.map(name => {
|
|
47
|
+
const stat = statSync(join(dir, name));
|
|
48
|
+
return { name, stamp: `${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}:${stat.ino}` };
|
|
49
|
+
});
|
|
50
|
+
const signature = JSON.stringify(files);
|
|
51
|
+
if (signature !== this.signature) {
|
|
52
|
+
const next = new Map();
|
|
53
|
+
for (const { name, stamp } of files) {
|
|
54
|
+
let entry = this.files.get(name);
|
|
55
|
+
if (entry?.stamp !== stamp) {
|
|
56
|
+
const totals = new Map();
|
|
57
|
+
for (const line of readFileSync(join(dir, name), "utf8").split("\n")) {
|
|
58
|
+
try {
|
|
59
|
+
const row = JSON.parse(line);
|
|
60
|
+
if (valid(row))
|
|
61
|
+
addRow(totals, row);
|
|
62
|
+
}
|
|
63
|
+
catch { /* incomplete or damaged row */ }
|
|
64
|
+
}
|
|
65
|
+
entry = { stamp, totals };
|
|
66
|
+
}
|
|
67
|
+
next.set(name, entry);
|
|
68
|
+
}
|
|
69
|
+
const summaries = new Map();
|
|
70
|
+
for (const entry of next.values()) {
|
|
71
|
+
for (const [threadId, part] of entry.totals) {
|
|
72
|
+
const total = summaries.get(threadId);
|
|
73
|
+
if (!total)
|
|
74
|
+
summaries.set(threadId, structuredClone(part));
|
|
75
|
+
else {
|
|
76
|
+
total.usage.input += part.usage.input;
|
|
77
|
+
total.usage.output += part.usage.output;
|
|
78
|
+
total.usage.turns += part.usage.turns;
|
|
79
|
+
if (total.usage.cachedInput === undefined || part.usage.cachedInput === undefined)
|
|
80
|
+
delete total.usage.cachedInput;
|
|
81
|
+
else
|
|
82
|
+
total.usage.cachedInput += part.usage.cachedInput;
|
|
83
|
+
if (part.usage.costUsd !== null)
|
|
84
|
+
total.usage.costUsd = (total.usage.costUsd ?? 0) + part.usage.costUsd;
|
|
85
|
+
if (part.lastAt >= total.lastAt) {
|
|
86
|
+
total.lastAt = part.lastAt;
|
|
87
|
+
total.usage.lastTurn = part.usage.lastTurn;
|
|
88
|
+
total.usage.lastSpeaker = part.usage.lastSpeaker;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const totals = new Map([...summaries].map(([id, value]) => [id, value.usage]));
|
|
94
|
+
this.files = next;
|
|
95
|
+
this.totals = totals;
|
|
96
|
+
this.signature = signature;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
forThread(threadId) {
|
|
100
|
+
const usage = this.totals.get(threadId);
|
|
101
|
+
return usage ? structuredClone(usage) : undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function requestConflict(message = "The request no longer owns this conversation") {
|
|
2
|
+
return Object.assign(new Error(message), { status: 409, code: "guarded_request_changed" });
|
|
3
|
+
}
|
|
4
|
+
export function assertRequestTarget(current, expected) {
|
|
5
|
+
if (current.messageId !== expected.messageId || current.activeLeafId !== expected.expectedActiveLeafId ||
|
|
6
|
+
current.activeTurnId !== expected.expectedTurnId || current.executionId !== expected.expectedExecutionId)
|
|
7
|
+
throw requestConflict();
|
|
8
|
+
}
|
|
9
|
+
/** Never infer ownership from timestamps or the latest assistant text. */
|
|
10
|
+
export function guardedRequestPath(messages, activeLeafId, sendId) {
|
|
11
|
+
const sources = messages.filter(message => message.sendId === sendId);
|
|
12
|
+
const source = sources[0];
|
|
13
|
+
if (sources.length !== 1 || !source || source.role !== "user" || source.kind !== "text" || source.steered || source.peerAsk) {
|
|
14
|
+
throw requestConflict("No unique acknowledged user request matches this sendId");
|
|
15
|
+
}
|
|
16
|
+
const byId = new Map(messages.map(message => [message.id, message]));
|
|
17
|
+
const path = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
let id = activeLeafId;
|
|
20
|
+
while (id) {
|
|
21
|
+
if (seen.has(id))
|
|
22
|
+
throw requestConflict();
|
|
23
|
+
seen.add(id);
|
|
24
|
+
const message = byId.get(id);
|
|
25
|
+
if (!message)
|
|
26
|
+
throw requestConflict();
|
|
27
|
+
path.unshift(message);
|
|
28
|
+
if (message.id === source.id)
|
|
29
|
+
break;
|
|
30
|
+
if (path.length >= 500)
|
|
31
|
+
throw Object.assign(new Error("Open this request in the workspace; its transcript exceeds the response limit"), { status: 413 });
|
|
32
|
+
id = message.parentId ?? null;
|
|
33
|
+
}
|
|
34
|
+
if (path[0]?.id !== source.id || path.slice(1).some(message => message.role === "user"))
|
|
35
|
+
throw requestConflict();
|
|
36
|
+
// An abandoned sibling can hold a detached result or approval. A caller
|
|
37
|
+
// must never receive it as the answer to the current branch.
|
|
38
|
+
const ancestors = new Set(path.map(message => message.id));
|
|
39
|
+
if (messages.some(message => message.parentId && ancestors.has(message.parentId) && !seen.has(message.id)))
|
|
40
|
+
throw requestConflict();
|
|
41
|
+
return path;
|
|
42
|
+
}
|
|
43
|
+
/** Used only for an explicit control-plane card, never the latest user. */
|
|
44
|
+
export function requestSourceForCard(messages, messageId) {
|
|
45
|
+
const byId = new Map(messages.map(message => [message.id, message]));
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
let message = byId.get(messageId);
|
|
48
|
+
while (message && !seen.has(message.id)) {
|
|
49
|
+
seen.add(message.id);
|
|
50
|
+
if (message.role === "user")
|
|
51
|
+
return message.peerAsk || message.steered || message.requestCancelled ? undefined : message.id;
|
|
52
|
+
message = message.parentId ? byId.get(message.parentId) : undefined;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
export function requestNeedsInput(message) {
|
|
57
|
+
return Boolean((message.card?.requestId && !message.card.answered && !message.card.dismissed) ||
|
|
58
|
+
(message.connector && !message.connector.dismissed && !message.connector.resumed && message.connector.status !== "connected") ||
|
|
59
|
+
(message.secret && !message.secret.provided && !message.secret.dismissed));
|
|
60
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const jsonHooks = new WeakMap();
|
|
2
|
+
const parsedBodies = new WeakMap();
|
|
3
|
+
/** Pass every JSON body sent on this response through `hook` first (it may
|
|
4
|
+
* return a narrowed copy). Set once a request is authenticated: index.ts
|
|
5
|
+
* narrows what a member is sent and notes what an admin change answered.
|
|
6
|
+
* Hooks added later run after earlier ones. */
|
|
7
|
+
export function onJsonBody(res, hook) {
|
|
8
|
+
const previous = jsonHooks.get(res);
|
|
9
|
+
jsonHooks.set(res, previous ? (body) => hook(previous(body)) : hook);
|
|
10
|
+
}
|
|
11
|
+
/** The JSON body readBody parsed for this request, if it read one. */
|
|
12
|
+
export function parsedBodyOf(req) {
|
|
13
|
+
return parsedBodies.get(req);
|
|
14
|
+
}
|
|
15
|
+
export function json(res, status, body) {
|
|
16
|
+
const hook = jsonHooks.get(res);
|
|
17
|
+
const data = JSON.stringify(hook ? hook(body) : body);
|
|
18
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
19
|
+
res.end(data);
|
|
20
|
+
}
|
|
21
|
+
export function readBody(req, limit = 1_000_000) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let data = "";
|
|
24
|
+
let bytes = 0;
|
|
25
|
+
let done = false;
|
|
26
|
+
const fail = (status, msg) => {
|
|
27
|
+
if (done)
|
|
28
|
+
return;
|
|
29
|
+
done = true;
|
|
30
|
+
const err = Object.assign(new Error(msg), { status });
|
|
31
|
+
reject(err);
|
|
32
|
+
};
|
|
33
|
+
req.on("data", (c) => {
|
|
34
|
+
if (done)
|
|
35
|
+
return;
|
|
36
|
+
bytes += typeof c === "string" ? Buffer.byteLength(c) : c.length;
|
|
37
|
+
if (bytes > limit) {
|
|
38
|
+
// Keep draining the socket, but stop retaining attacker-controlled
|
|
39
|
+
// bytes. Destroying the request here prevents the caller from
|
|
40
|
+
// receiving the useful 413 response.
|
|
41
|
+
return fail(413, "body too large");
|
|
42
|
+
}
|
|
43
|
+
data += c;
|
|
44
|
+
});
|
|
45
|
+
req.on("end", () => {
|
|
46
|
+
if (done)
|
|
47
|
+
return;
|
|
48
|
+
let body;
|
|
49
|
+
try {
|
|
50
|
+
body = data ? JSON.parse(data) : {};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return fail(400, "invalid JSON body");
|
|
54
|
+
}
|
|
55
|
+
done = true;
|
|
56
|
+
parsedBodies.set(req, body);
|
|
57
|
+
resolve(body);
|
|
58
|
+
});
|
|
59
|
+
req.on("error", (e) => fail(400, e instanceof Error ? e.message : String(e)));
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -228,6 +228,7 @@ export class ProviderRegistry {
|
|
|
228
228
|
modelVariants: inst.adapter.capabilities.modelVariants === true,
|
|
229
229
|
queueing: inst.adapter.capabilities.queueing === true,
|
|
230
230
|
localComputerMcp: inst.adapter.capabilities.localComputerMcp === true,
|
|
231
|
+
cloudComputerMcp: inst.adapter.capabilities.cloudComputerMcp === true,
|
|
231
232
|
approvalReview: inst.reviewPermission !== undefined,
|
|
232
233
|
},
|
|
233
234
|
access: driver?.metadata.access ?? "subscription",
|