openmausbot 0.1.85 → 0.1.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-DpUZNnFD.js +1 -0
- package/dist/assets/arc-DW7mrI4p.js +1 -0
- package/dist/assets/architectureDiagram-NJMV4G6O-CyeZ5WLZ.js +36 -0
- package/dist/assets/blockDiagram-BEXU5L5S-CBVaNjO7.js +140 -0
- package/dist/assets/c4Diagram-YGBWAQC7-sf8d41uT.js +38 -0
- package/dist/assets/channel-BXFkgus9.js +1 -0
- package/dist/assets/chunk-2Q5K7J3B-BOGvy7Y_.js +1 -0
- package/dist/assets/chunk-3FUC2YCW-BiF9zyiz.js +2 -0
- package/dist/assets/chunk-5VM5RSS4-EFuZ1SBZ.js +15 -0
- package/dist/assets/chunk-GTNCS2PH-pJLNSAT7.js +88 -0
- package/dist/assets/chunk-GWA4HPMP-Cb-3_3IT.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DAf5aogn.js +1 -0
- package/dist/assets/chunk-NETBCI7D-DR3kiwZz.js +1 -0
- package/dist/assets/chunk-WEXAMYUT-BmU8PeyM.js +1 -0
- package/dist/assets/chunk-XXDRQBXY-Buh8g8OT.js +1 -0
- package/dist/assets/classDiagram-v2-NBCMYWYE-BurfmMWM.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-KkGZUEkU.js +1 -0
- package/dist/assets/cynefin-OW5HDTMX-C3PpV96d.js +166 -0
- package/dist/assets/cynefinDiagram-VND7K2PF-Dt8JwJaI.js +62 -0
- package/dist/assets/cytoscape.esm-BB4DxJjf.js +321 -0
- package/dist/assets/dagre-6A5THRUB-DkjN8266.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-22UHCM2B-DUAJS0JV.js +200 -0
- package/dist/assets/diagram-3UASUU5V-Dw4KhF1a.js +24 -0
- package/dist/assets/diagram-ATOU4E4O-Dd1gmQqf.js +3 -0
- package/dist/assets/diagram-CDSNMT55-CjwGs0Yw.js +30 -0
- package/dist/assets/diagram-MLGK6HIB-BL-69Zrq.js +24 -0
- package/dist/assets/diagram-MPIPVDR6-5Zi2N7wo.js +41 -0
- package/dist/assets/ebnfDiagram-ZINNZB2B-D9NekWUX.js +1 -0
- package/dist/assets/elk-276RUBZZ-CkaSQjxw.js +27 -0
- package/dist/assets/erDiagram-OPXOYQCR-CdAVBV8-.js +99 -0
- package/dist/assets/flowDiagram-KWPJA3E3-Bm_lWWH7.js +213 -0
- package/dist/assets/ganttDiagram-FUAMR5RP-FZ8ejYWJ.js +292 -0
- package/dist/assets/gitGraphDiagram-X574FWY7-Dlv9rizZ.js +106 -0
- package/dist/assets/graph-D8UmR1vK.js +1 -0
- package/dist/assets/index-BGgqlWPJ.css +1 -0
- package/dist/assets/index-BtHo8ZKl.js +577 -0
- package/dist/assets/{index-CTNNoqSJ.js → index-h9FFNwTu.js} +1 -1
- package/dist/assets/infoDiagram-VRGFBTTK-BdQ_gW5B.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-OU5B5YK6-EwssEp2X.js +70 -0
- package/dist/assets/journeyDiagram-ZHPQQLJL-BX1G1vOd.js +139 -0
- package/dist/assets/kanban-definition-PNTS6WVX-BB4kAsyC.js +89 -0
- package/dist/assets/linear-3su2TG93.js +1 -0
- package/dist/assets/mermaid.core-CDD1E0s9.js +311 -0
- package/dist/assets/mindmap-definition-NLK3R4M7-Ba50MsNK.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pegDiagram-GJSIUBJH-DZLkkZlM.js +1 -0
- package/dist/assets/pieDiagram-5QR66LMP-DaTz77Uq.js +39 -0
- package/dist/assets/quadrantDiagram-O4NWA36T-CpaiidpR.js +7 -0
- package/dist/assets/railroadDiagram-XR7U4H2S-mN4JrMeI.js +1 -0
- package/dist/assets/requirementDiagram-PLB6GJNP-BSNDRpg7.js +84 -0
- package/dist/assets/sankeyDiagram-IPEJSGJF-FngzGNug.js +40 -0
- package/dist/assets/sequenceDiagram-PO4LG4MO-CM4pCC3L.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-CRICHXkw.js +1 -0
- package/dist/assets/stateDiagram-v2-GCMORJYK-oyTWpxH2.js +291 -0
- package/dist/assets/swimlanes-2SLR337P-D9h9mPBq.js +1 -0
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +8 -0
- package/dist/assets/timeline-definition-EJHVYXUP-BJUlN9bW.js +120 -0
- package/dist/assets/usecaseDiagram-POWQR4AR-3uBNkbAb.js +370 -0
- package/dist/assets/vennDiagram-UO4OBE2U-D2odalzT.js +34 -0
- package/dist/assets/wardleyDiagram-VNRHLVJA-DHgFaB9S.js +78 -0
- package/dist/assets/xychartDiagram-PMCCYNJV-w5GYM2h0.js +7 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +19 -10
- package/dist-server/drivers/agents-proxy.js +16159 -1596
- package/dist-server/enterprise/LICENSE +32 -0
- package/dist-server/enterprise/server/index.js +5 -5
- package/dist-server/index.js +4779 -1667
- package/dist-server/mcp-server.js +3 -1
- package/dist-server/openmausbot.js +4679 -4266
- package/dist-server/pair-cli.js +4679 -4266
- package/dist-server/server/admin-activity.js +410 -0
- package/dist-server/server/bot-visibility.js +462 -0
- package/dist-server/server/box.js +2 -1
- package/dist-server/server/browser-runtime.js +1 -1
- package/dist-server/server/channel-queue.js +2 -0
- package/dist-server/server/cli-prompts.js +24 -2
- package/dist-server/server/cli.js +79 -10
- package/dist-server/server/composio.js +49 -11
- package/dist-server/server/config.js +79 -13
- package/dist-server/server/decision-log.js +209 -48
- package/dist-server/server/default-model-selection.js +20 -3
- package/dist-server/server/delegations.js +11 -9
- package/dist-server/server/drivers/acp/core.js +110 -22
- package/dist-server/server/drivers/acp/grok.js +4 -3
- package/dist-server/server/drivers/agents-call.js +1123 -0
- package/dist-server/server/drivers/agents-catalog.js +659 -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/chat-mcp-tools.js +17 -2
- package/dist-server/server/drivers/claude.js +21 -6
- package/dist-server/server/drivers/codex-mcp-names.js +44 -0
- package/dist-server/server/drivers/codex.js +26 -4
- package/dist-server/server/drivers/grok.js +2 -1
- package/dist-server/server/drivers/openai-compat.js +15 -8
- package/dist-server/server/enterprise.js +97 -16
- package/dist-server/server/external-runtime.js +83 -0
- package/dist-server/server/guarded-requests.js +60 -0
- package/dist-server/server/harness/http.js +61 -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 +1718 -348
- package/dist-server/server/managed-desktop.js +79 -14
- package/dist-server/server/managed-policy.js +153 -0
- 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/notify.js +10 -1
- package/dist-server/server/peer-roster.js +13 -2
- 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 +25 -11
- package/dist-server/server/routes/hosted-slack.js +20 -0
- package/dist-server/server/routes/table.js +14 -0
- package/dist-server/server/routines.js +22 -0
- package/dist-server/server/spend.js +111 -16
- package/dist-server/server/steer-queue.js +6 -1
- package/dist-server/server/store.js +292 -21
- package/dist-server/server/team-backup.js +10 -1
- package/dist-server/server/thread-starters.js +53 -0
- package/dist-server/server/usage-ledger.js +31 -8
- package/dist-server/server/workspace-backup-policy.js +9 -2
- package/dist-server/server/workspace-backup.js +7 -4
- package/dist-server/shared/mention-boundary.js +15 -0
- package/dist-server/shared/routines.js +7 -1
- package/dist-server/shared/team-backup.js +2 -0
- package/dist-server/src/lib/composer-attachments.js +16 -0
- package/dist-server/vps-container-mcp.js +19 -10
- 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
|
@@ -2,6 +2,83 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { isIP } from "node:net";
|
|
3
3
|
import { denyReason as companionDenial } from "../companion/src/routes.js";
|
|
4
4
|
const LOOPBACK_SCOPES = ["admin", "client"];
|
|
5
|
+
const SERVICE_SCOPES = ["client"];
|
|
6
|
+
/** What a `service`-trust loopback caller may reach, and nothing else:
|
|
7
|
+
*
|
|
8
|
+
* - liveness and identity: health, who-am-I, edition and brand;
|
|
9
|
+
* - the cloud Slack worker (openmaus-cloud server/slack-worker.ts), which
|
|
10
|
+
* shares the workspace's network namespace and reads the bot list, a
|
|
11
|
+
* thread's messages and a bot's PNG picture, creates a thread, sends
|
|
12
|
+
* through the guarded route, watches and stops its exact request,
|
|
13
|
+
* withdraws a queued line, and declines a card (the handler refuses any
|
|
14
|
+
* other answer from this caller);
|
|
15
|
+
* - the turn-scoped bot capability routes, whose own bearer token is the
|
|
16
|
+
* authorization (checked in the handler), and the test-only capability
|
|
17
|
+
* mint that exists only when its private key is set.
|
|
18
|
+
*
|
|
19
|
+
* Residual risk, not closed here: a bot's shell is a loopback caller too, so
|
|
20
|
+
* it can do everything the worker does. It can post into any bot's thread
|
|
21
|
+
* through the guarded route, including an existing Full-access thread
|
|
22
|
+
* (`expectedApprovalMode: "full"`), and while the operator's shared Full
|
|
23
|
+
* access is on it can open new Full-access threads; either way work runs with
|
|
24
|
+
* Full access and no card, whoever asked the bot. It can also stop a request
|
|
25
|
+
* and decline a card. It can no longer change settings, keys, instances, MCP
|
|
26
|
+
* servers, webhooks, sessions, people, budgets or fleet, loosen a bot's
|
|
27
|
+
* permissions, or approve anything. The planned fix is a relay token that only
|
|
28
|
+
* the Slack worker holds, so these routes stop answering session-less
|
|
29
|
+
* loopback at all. */
|
|
30
|
+
export const SERVICE_ALLOW = [
|
|
31
|
+
{ methods: ["GET"], path: /^\/api\/health$/ },
|
|
32
|
+
{ methods: ["GET"], path: /^\/api\/auth\/session$/ },
|
|
33
|
+
{ methods: ["GET"], path: /^\/api\/edition$/ },
|
|
34
|
+
{ methods: ["GET"], path: /^\/api\/brand$/ },
|
|
35
|
+
{ methods: ["GET"], path: /^\/api\/bots$/ },
|
|
36
|
+
{ methods: ["GET"], path: /^\/api\/threads\/[\w-]+\/messages$/ },
|
|
37
|
+
{ methods: ["GET"], path: /^\/api\/attachments\/[\w.-]+$/ },
|
|
38
|
+
{ methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/tasks$/ },
|
|
39
|
+
{ methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/messages\/guarded$/ },
|
|
40
|
+
{ methods: ["GET"], path: /^\/api\/bots\/[\w-]+\/requests\/[\w-]+$/ },
|
|
41
|
+
{ methods: ["POST"], path: /^\/api\/bots\/[\w-]+\/requests\/[\w-]+\/interrupt$/ },
|
|
42
|
+
{ methods: ["DELETE"], path: /^\/api\/bots\/[\w-]+\/queue\/[\w-]+$/ },
|
|
43
|
+
{ methods: ["POST"], path: /^\/api\/threads\/[\w-]+\/respond$/ }, // decline only: see the handler
|
|
44
|
+
{ methods: ["GET", "POST", "PUT", "PATCH", "DELETE"], path: /^\/api\/internal\// }, // capability bearer checked in the handler
|
|
45
|
+
{ methods: ["POST"], path: /^\/api\/testing\/internal-capability$/ }, // exists only with its private key
|
|
46
|
+
];
|
|
47
|
+
export function serviceAllowed(method, path) {
|
|
48
|
+
const upper = method.toUpperCase();
|
|
49
|
+
return SERVICE_ALLOW.some((rule) => rule.methods.includes(upper) && rule.path.test(path));
|
|
50
|
+
}
|
|
51
|
+
/** Pick the loopback trust for this process, and say why for the startup log.
|
|
52
|
+
*
|
|
53
|
+
* A packaged desktop keeps `owner`: its mutations already need Electron's
|
|
54
|
+
* per-launch capability, and only its owner uses the machine. Elsewhere the
|
|
55
|
+
* operator may set OMB_LOOPBACK_TRUST=owner|service. Without it a hosted
|
|
56
|
+
* workspace (any OMB_ADMIN_* setting, even an incomplete one) defaults to
|
|
57
|
+
* `service` — shared-workspace Full access is only honoured there, so it needs
|
|
58
|
+
* no rule of its own — and a headless self-hosted server keeps `owner`. A
|
|
59
|
+
* value that is neither fails closed. */
|
|
60
|
+
export function resolveLoopbackTrust(input) {
|
|
61
|
+
const raw = (input.env ?? process.env).OMB_LOOPBACK_TRUST;
|
|
62
|
+
const requested = raw?.trim().toLowerCase();
|
|
63
|
+
if (input.desktopManaged) {
|
|
64
|
+
return { trust: "owner", reason: "desktop app", ...(raw !== undefined ? { warning: "OMB_LOOPBACK_TRUST is ignored in the desktop app" } : {}) };
|
|
65
|
+
}
|
|
66
|
+
if (requested === "owner" || requested === "service") {
|
|
67
|
+
return {
|
|
68
|
+
trust: requested,
|
|
69
|
+
reason: "OMB_LOOPBACK_TRUST",
|
|
70
|
+
...(requested === "owner" && input.hostedWorkspace
|
|
71
|
+
? { warning: "OMB_LOOPBACK_TRUST=owner on a shared workspace: every bot's shell can change settings and approve cards as the owner" }
|
|
72
|
+
: {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (raw !== undefined && requested !== "") {
|
|
76
|
+
return { trust: "service", reason: "OMB_LOOPBACK_TRUST", warning: `OMB_LOOPBACK_TRUST="${raw.replace(/[^\w.-]/g, "").slice(0, 40)}" is not owner or service; using service` };
|
|
77
|
+
}
|
|
78
|
+
if (input.hostedWorkspace)
|
|
79
|
+
return { trust: "service", reason: "hosted workspace" };
|
|
80
|
+
return { trust: "owner", reason: "self-hosted default" };
|
|
81
|
+
}
|
|
5
82
|
export function isLoopbackHost(host) {
|
|
6
83
|
if (!host)
|
|
7
84
|
return false;
|
|
@@ -170,6 +247,8 @@ export const CLIENT_ALLOW = [
|
|
|
170
247
|
// reads: fleet, transcripts, search (no secrets in any of these)
|
|
171
248
|
{ methods: ["GET"], path: /^\/api\/bots$/ },
|
|
172
249
|
{ methods: ["GET"], path: /^\/api\/team-map$/ },
|
|
250
|
+
// a link into the organisation's Admin: identifiers only, and Admin authorizes its own visitor
|
|
251
|
+
{ methods: ["GET"], path: /^\/api\/bots\/[\w-]+\/slack-management$/ },
|
|
173
252
|
{ methods: ["GET"], path: /^\/api\/search$/ },
|
|
174
253
|
{ methods: ["GET"], path: /^\/api\/threads\/[\w-]+\/messages$/ },
|
|
175
254
|
{ methods: ["GET"], path: /^\/api\/threads\/[\w-]+\/messages\/[\w-]+\/image$/ },
|
|
@@ -219,6 +298,7 @@ export const CLIENT_ALLOW = [
|
|
|
219
298
|
{ methods: ["PATCH", "DELETE"], path: /^\/api\/routines\/[\w-]+$/ },
|
|
220
299
|
{ methods: ["POST"], path: /^\/api\/routines\/[\w-]+\/run$/ },
|
|
221
300
|
{ methods: ["POST"], path: /^\/api\/routine-runs\/[\w-]+\/(?:cancel|seen)$/ },
|
|
301
|
+
{ methods: ["POST"], path: /^\/api\/routine-runs\/seen-all$/ },
|
|
222
302
|
// webhook list is secret-free; creating or rotating one is not
|
|
223
303
|
{ methods: ["GET"], path: /^\/api\/webhooks$/ },
|
|
224
304
|
// configured-or-not booleans; the handler strips the few identifying fields for clients
|
|
@@ -255,6 +335,9 @@ export function clientGroupPatchViolation(body) {
|
|
|
255
335
|
return key;
|
|
256
336
|
return null;
|
|
257
337
|
}
|
|
338
|
+
const CLI_OWNER_HEADER = "x-openmausbot-cli-owner";
|
|
339
|
+
/** The only routes the serving CLI's secret opens: its pairing code. */
|
|
340
|
+
const CLI_OWNER_ROUTE = /^\/api\/auth\/pairing$/;
|
|
258
341
|
const DESKTOP_OWNER_HEADER = "x-openmausbot-desktop-owner";
|
|
259
342
|
function mutatingPublicRoute(method, path) {
|
|
260
343
|
const upper = method.toUpperCase();
|
|
@@ -343,6 +426,19 @@ export function resolveRequestAuth(req, options) {
|
|
|
343
426
|
!secureTokenMatch(headerValue(req.headers[DESKTOP_OWNER_HEADER]), options.loopbackMutationToken)) {
|
|
344
427
|
return deny(403, "forbidden: this change must come from the desktop app or a paired device");
|
|
345
428
|
}
|
|
429
|
+
if (options.loopbackMutationToken === undefined && options.loopbackTrust === "service") {
|
|
430
|
+
// The CLI that started this server may still print a pairing code.
|
|
431
|
+
if (options.cliOwnerToken && CLI_OWNER_ROUTE.test(path) && ["GET", "POST"].includes(method.toUpperCase()) &&
|
|
432
|
+
secureTokenMatch(headerValue(req.headers[CLI_OWNER_HEADER]), options.cliOwnerToken)) {
|
|
433
|
+
return { auth: { kind: "loopback", scopes: LOOPBACK_SCOPES }, status: 401, error: "" };
|
|
434
|
+
}
|
|
435
|
+
// On a shared server "local" includes every bot's shell. Default deny:
|
|
436
|
+
// only the service routes, never an admin change, without a session.
|
|
437
|
+
if (!serviceAllowed(method, path)) {
|
|
438
|
+
return deny(403, "forbidden: on this shared server a local request without a session may only use the service routes; sign in to change settings, people or access");
|
|
439
|
+
}
|
|
440
|
+
return { auth: { kind: "loopback", scopes: SERVICE_SCOPES, trust: "service" }, status: 401, error: "" };
|
|
441
|
+
}
|
|
346
442
|
return { auth: { kind: "loopback", scopes: LOOPBACK_SCOPES }, status: 401, error: "" };
|
|
347
443
|
}
|
|
348
444
|
if (proxied) {
|
|
@@ -243,12 +243,13 @@ export class RoomHandoffs {
|
|
|
243
243
|
const node = this.nodes.get(generation);
|
|
244
244
|
if (!node || node.status !== "source")
|
|
245
245
|
return;
|
|
246
|
+
// An accepted assignment belongs to the queue, not the provider that
|
|
247
|
+
// submitted it. A failed/expired source turn must not erase that work.
|
|
248
|
+
// Explicit Stop, deletion and revoked routes still cancel separately.
|
|
246
249
|
if (!ok)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.publish(node);
|
|
251
|
-
}
|
|
250
|
+
node.result = "The originating turn ended before its teammates returned.";
|
|
251
|
+
node.status = "waiting";
|
|
252
|
+
this.publish(node);
|
|
252
253
|
}
|
|
253
254
|
cancelTree(node, reason, status = "cancelled") {
|
|
254
255
|
for (const child of this.children(node.id))
|
|
@@ -347,8 +348,10 @@ export class RoomHandoffs {
|
|
|
347
348
|
}
|
|
348
349
|
if (n.status !== "queued" && n.status !== "resume")
|
|
349
350
|
continue;
|
|
350
|
-
//
|
|
351
|
-
|
|
351
|
+
// Independent conversations can start as soon as work is accepted.
|
|
352
|
+
// Same-room speakers still serialize; never overlap their shared chat.
|
|
353
|
+
if (parent && (parent.status === "source" || parent.status === "running") &&
|
|
354
|
+
(parent.threadId === n.threadId || (n.groupId && n.groupId === parent.groupId)))
|
|
352
355
|
continue;
|
|
353
356
|
// A stopped source stops waiting; only work that never started is
|
|
354
357
|
// dropped with it. A teammate mid-turn keeps its process and reports.
|
|
@@ -381,10 +384,10 @@ export class RoomHandoffs {
|
|
|
381
384
|
if (terminal(n))
|
|
382
385
|
return;
|
|
383
386
|
n.result = result.text.slice(0, 12_000);
|
|
384
|
-
if (
|
|
385
|
-
this.cancelTree(n, n.result || "Room agent failed", "failed");
|
|
386
|
-
else if (this.children(n.id).length > childCount)
|
|
387
|
+
if (this.children(n.id).length > childCount)
|
|
387
388
|
n.status = "waiting";
|
|
389
|
+
else if (!result.ok)
|
|
390
|
+
this.cancelTree(n, n.result || "Room agent failed", "failed");
|
|
388
391
|
else
|
|
389
392
|
n.status = "completed";
|
|
390
393
|
// Close the paused span with the settlement itself: work enqueued
|
|
@@ -392,7 +395,18 @@ export class RoomHandoffs {
|
|
|
392
395
|
// budget, not the still-open pause's overstated runway.
|
|
393
396
|
this.trackExecutionPauses();
|
|
394
397
|
this.publish(n);
|
|
395
|
-
}).catch(e => {
|
|
398
|
+
}).catch(e => {
|
|
399
|
+
if (terminal(n))
|
|
400
|
+
return;
|
|
401
|
+
n.result = String(e).slice(0, 1000);
|
|
402
|
+
if (this.children(n.id).length > childCount) {
|
|
403
|
+
n.status = "waiting";
|
|
404
|
+
this.trackExecutionPauses();
|
|
405
|
+
this.publish(n);
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
this.cancelTree(n, n.result, "failed");
|
|
409
|
+
})
|
|
396
410
|
.finally(() => this.controllers.delete(n.id));
|
|
397
411
|
}
|
|
398
412
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// GET /api/bots/:id/slack-management: where, in the organisation's Admin, this
|
|
2
|
+
// bot's Slack app is managed. A deep link only: this server holds no Slack
|
|
3
|
+
// logic, contacts nobody, and the link carries identifiers, never a credential.
|
|
4
|
+
// Client-scoped in server/request-auth.ts, so a member reading Bot Settings
|
|
5
|
+
// gets the same link; Admin authorizes whoever opens it.
|
|
6
|
+
import { hostedSlackManagement } from "../hosted-slack.js";
|
|
7
|
+
import { PASS } from "./table.js";
|
|
8
|
+
export function createHostedSlackRoutes(deps) {
|
|
9
|
+
return async ({ res, path, method, json }) => {
|
|
10
|
+
const m = path.match(/^\/api\/bots\/([\w-]+)\/slack-management$/);
|
|
11
|
+
if (!m || method !== "GET")
|
|
12
|
+
return PASS;
|
|
13
|
+
const bot = deps.bot(m[1]);
|
|
14
|
+
if (!bot || bot.hidden)
|
|
15
|
+
return json(res, 404, { error: "no such bot" });
|
|
16
|
+
res.setHeader("cache-control", "no-store");
|
|
17
|
+
// The id in the link is the stored bot's, not the text from the request.
|
|
18
|
+
return json(res, 200, hostedSlackManagement(bot.id, deps.hostedReady(), deps.env));
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** "Not my route": the next handler, then index.ts's inline routes, get a turn. */
|
|
2
|
+
export const PASS = Symbol("route.pass");
|
|
3
|
+
export const ROUTES = [];
|
|
4
|
+
/** Runs handlers in order until one answers; true means stop routing. A
|
|
5
|
+
* handler that wrote a response but returned PASS by mistake still counts as
|
|
6
|
+
* answered, so the request can never reach a second handler. */
|
|
7
|
+
export async function dispatchRoutes(routes, ctx) {
|
|
8
|
+
for (const route of routes) {
|
|
9
|
+
const out = await route(ctx);
|
|
10
|
+
if (out !== PASS || ctx.res.headersSent || ctx.res.writableEnded)
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
@@ -6,6 +6,7 @@ import { DATA_DIR } from "./config.js";
|
|
|
6
6
|
import { writeFileAtomic } from "./atomic.js";
|
|
7
7
|
import { redactSecretsInText } from "./redact.js";
|
|
8
8
|
import { normalizeCronSchedule, nextCronRuns } from "../shared/routine-schedule.js";
|
|
9
|
+
import { isRoutineProblemRun } from "../shared/routines.js";
|
|
9
10
|
const persistedSourceThreadId = z.string().trim().min(1).optional().catch(undefined);
|
|
10
11
|
const webhookRunReceiptSchema = z.object({
|
|
11
12
|
webhookId: z.string().min(1).max(200),
|
|
@@ -1063,6 +1064,27 @@ export class RoutineManager {
|
|
|
1063
1064
|
}
|
|
1064
1065
|
return cloneRun(run);
|
|
1065
1066
|
}
|
|
1067
|
+
/** Clear every failure indicator at once: stamp seenAt on each unseen
|
|
1068
|
+
* failed/missed run in one committed save, then emit the updated runs so
|
|
1069
|
+
* connected clients drop their dots immediately. A failed save rolls the
|
|
1070
|
+
* stamps back so a retry still finds the unseen runs. */
|
|
1071
|
+
markAllSeen() {
|
|
1072
|
+
if (!this.runs.some((run) => !run.seenAt && isRoutineProblemRun(run)))
|
|
1073
|
+
return [];
|
|
1074
|
+
const stampAt = this.now();
|
|
1075
|
+
const updated = [];
|
|
1076
|
+
this.commitMutation(() => {
|
|
1077
|
+
for (const run of this.runs) {
|
|
1078
|
+
if (run.seenAt || !isRoutineProblemRun(run))
|
|
1079
|
+
continue;
|
|
1080
|
+
run.seenAt = stampAt;
|
|
1081
|
+
updated.push(run);
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
for (const run of updated)
|
|
1085
|
+
this.emitRun(run);
|
|
1086
|
+
return updated.map(cloneRun);
|
|
1087
|
+
}
|
|
1066
1088
|
get isTicking() { return this.ticking; }
|
|
1067
1089
|
start() {
|
|
1068
1090
|
if (this.timer)
|
|
@@ -1,33 +1,76 @@
|
|
|
1
|
+
// Spend limits over the usage ledger: what this workspace has spent this
|
|
2
|
+
// month against its cap, the refusal a turn gets once the cap is reached,
|
|
3
|
+
// and the one-per-month notices when the month crosses the warning
|
|
4
|
+
// threshold and the cap. The figure is every cost in the ledger: what
|
|
5
|
+
// engines reported (on a workspace of personal subscriptions, their
|
|
6
|
+
// equivalents too) plus the estimates booked for engines that report tokens
|
|
7
|
+
// but no price (server/model-prices.ts). A turn on an unpriced model counts
|
|
8
|
+
// nothing. Enforced only with the `budgets` entitlement; without it the
|
|
9
|
+
// setting is inert.
|
|
10
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
1
12
|
import { entitled } from "./enterprise.js";
|
|
2
|
-
import { readUsage } from "./usage-ledger.js";
|
|
13
|
+
import { readUsage, usageRowKey } from "./usage-ledger.js";
|
|
3
14
|
const DEFAULT_WARN_AT_PERCENT = 80;
|
|
4
15
|
const CACHE_MS = 15_000;
|
|
5
16
|
// Every turn start asks; reading the month file each time would be silly.
|
|
17
|
+
// A read keeps the keys of the rows it summed, so a booked row can be told
|
|
18
|
+
// apart from the same row read back later.
|
|
6
19
|
const cache = new Map();
|
|
20
|
+
// Booked turns whose ledger row may not be on disk yet: dataDir → row key →
|
|
21
|
+
// cost. The append is asynchronous, and a turn that ran longer than CACHE_MS
|
|
22
|
+
// would otherwise be re-read out of the month before its own row landed.
|
|
23
|
+
const unwritten = new Map();
|
|
7
24
|
function monthOf(now) {
|
|
8
25
|
return now.toISOString().slice(0, 7);
|
|
9
26
|
}
|
|
10
|
-
|
|
27
|
+
const costOf = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
|
|
28
|
+
/** Reported and estimated cost this month so far: the ledger file (read at
|
|
29
|
+
* most every CACHE_MS) plus turns booked since whose rows have not landed. */
|
|
11
30
|
export function monthToDateSpend(dataDir, now = new Date()) {
|
|
12
31
|
const month = monthOf(now);
|
|
13
|
-
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return spentUsd;
|
|
20
|
-
}
|
|
21
|
-
/** Called right after a turn is booked, so the next check sees it without
|
|
22
|
-
* waiting for the ledger's append to land or the cache to expire. */
|
|
23
|
-
export function noteSpend(dataDir, costUsd, now = new Date()) {
|
|
24
|
-
const hit = cache.get(dataDir);
|
|
25
|
-
if (hit && hit.month === monthOf(now) && typeof costUsd === "number" && Number.isFinite(costUsd) && costUsd > 0) {
|
|
26
|
-
hit.spentUsd += costUsd;
|
|
32
|
+
let read = cache.get(dataDir);
|
|
33
|
+
if (!read || read.month !== month || now.getTime() - read.at >= CACHE_MS) {
|
|
34
|
+
const from = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
35
|
+
const rows = readUsage(dataDir, { from, to: now });
|
|
36
|
+
read = { at: now.getTime(), month, fileUsd: rows.reduce((sum, row) => sum + costOf(row.costUsd), 0), keys: new Set(rows.map(usageRowKey)) };
|
|
37
|
+
cache.set(dataDir, read);
|
|
27
38
|
}
|
|
39
|
+
let pending = 0;
|
|
40
|
+
for (const [key, entry] of unwritten.get(dataDir) ?? []) {
|
|
41
|
+
if (entry.month === month && !read.keys.has(key))
|
|
42
|
+
pending += entry.costUsd;
|
|
43
|
+
}
|
|
44
|
+
return read.fileUsd + pending;
|
|
45
|
+
}
|
|
46
|
+
/** Called as a turn is booked, with the ledger's write for its row
|
|
47
|
+
* (appendUsage's promise). The cost counts from memory at once; when the
|
|
48
|
+
* row lands it moves into the cached read, and a re-read that already sees
|
|
49
|
+
* the row never counts it twice. A write that failed stops counting, as
|
|
50
|
+
* the ledger has no row for it. */
|
|
51
|
+
export function noteSpend(dataDir, row, written) {
|
|
52
|
+
const costUsd = costOf(row.costUsd);
|
|
53
|
+
if (!costUsd)
|
|
54
|
+
return;
|
|
55
|
+
const key = usageRowKey(row);
|
|
56
|
+
const month = row.at.slice(0, 7);
|
|
57
|
+
const entries = unwritten.get(dataDir) ?? new Map();
|
|
58
|
+
unwritten.set(dataDir, entries);
|
|
59
|
+
entries.set(key, { month, costUsd });
|
|
60
|
+
void written.then((landed) => {
|
|
61
|
+
entries.delete(key);
|
|
62
|
+
if (entries.size === 0 && unwritten.get(dataDir) === entries)
|
|
63
|
+
unwritten.delete(dataDir);
|
|
64
|
+
const read = cache.get(dataDir);
|
|
65
|
+
if (landed && read && read.month === month && !read.keys.has(key)) {
|
|
66
|
+
read.fileUsd += costUsd;
|
|
67
|
+
read.keys.add(key);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
28
70
|
}
|
|
29
71
|
export function resetSpendCacheForTests() {
|
|
30
72
|
cache.clear();
|
|
73
|
+
unwritten.clear();
|
|
31
74
|
}
|
|
32
75
|
/** The cap and where the month stands against it; null when there is no
|
|
33
76
|
* enforceable cap (no entitlement, or none set). */
|
|
@@ -59,3 +102,55 @@ export function assertWithinBudget(cfg, dataDir, now = new Date(), isEntitled =
|
|
|
59
102
|
return;
|
|
60
103
|
throw Object.assign(new Error(`this workspace has reached its monthly spend limit of $${usd(state.monthlyUsd)} — an admin can raise it under Settings → Usage`), { status: 409, code: "spend_cap" });
|
|
61
104
|
}
|
|
105
|
+
const ALERTS_FILE = "alerts.json";
|
|
106
|
+
function readMarks(dataDir) {
|
|
107
|
+
try {
|
|
108
|
+
const value = JSON.parse(readFileSync(join(dataDir, "usage", ALERTS_FILE), "utf8"));
|
|
109
|
+
if (typeof value !== "object" || value === null)
|
|
110
|
+
return null;
|
|
111
|
+
const marks = value;
|
|
112
|
+
return typeof marks.month === "string" && typeof marks.monthlyUsd === "number" ? marks : null;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** The notice this booking should raise, if any: the cap the first time the
|
|
119
|
+
* month reaches it, the warning the first time it crosses the threshold, and
|
|
120
|
+
* nothing again for either that month. A new month, or a different cap, is a
|
|
121
|
+
* new budget and starts over. Marks live beside the ledger so a restart does
|
|
122
|
+
* not repeat them; if they cannot be written the notice is still sent once
|
|
123
|
+
* for this run. */
|
|
124
|
+
export function takeSpendAlert(dataDir, state) {
|
|
125
|
+
if (!state || (!state.warn && !state.exceeded))
|
|
126
|
+
return null;
|
|
127
|
+
const saved = readMarks(dataDir) ?? memoryMarks.get(dataDir) ?? null;
|
|
128
|
+
const marks = saved && saved.month === state.month && saved.monthlyUsd === state.monthlyUsd
|
|
129
|
+
? saved
|
|
130
|
+
: { month: state.month, monthlyUsd: state.monthlyUsd };
|
|
131
|
+
const alert = state.exceeded && !marks.cap ? "cap" : state.warn && !marks.warn && !marks.cap ? "warn" : null;
|
|
132
|
+
if (!alert)
|
|
133
|
+
return null;
|
|
134
|
+
// Reaching the cap also answers the warning: one notice, the stronger one.
|
|
135
|
+
const next = { ...marks, warn: true, ...(alert === "cap" ? { cap: true } : {}) };
|
|
136
|
+
memoryMarks.set(dataDir, next);
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(join(dataDir, "usage"), { recursive: true, mode: 0o700 });
|
|
139
|
+
writeFileSync(join(dataDir, "usage", ALERTS_FILE), JSON.stringify(next), { mode: 0o600 });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
/* bookkeeping must never take down the turn */
|
|
143
|
+
}
|
|
144
|
+
return alert;
|
|
145
|
+
}
|
|
146
|
+
const memoryMarks = new Map();
|
|
147
|
+
export function resetSpendAlertsForTests() {
|
|
148
|
+
memoryMarks.clear();
|
|
149
|
+
}
|
|
150
|
+
/** The notice's words. English like the server's other notification titles. */
|
|
151
|
+
export function spendAlertText(alert, state) {
|
|
152
|
+
const spent = `$${state.spentUsd.toFixed(2)} of $${usd(state.monthlyUsd)} spent this month (${state.month})`;
|
|
153
|
+
return alert === "cap"
|
|
154
|
+
? { title: "Monthly spend limit reached", body: `${spent}. New turns are refused until an admin raises the limit under Settings → Usage.` }
|
|
155
|
+
: { title: `Spend is at ${state.percent}% of the monthly limit`, body: `${spent}. New turns stop when the limit is reached.` };
|
|
156
|
+
}
|
|
@@ -72,6 +72,8 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
|
72
72
|
reason: options.reason,
|
|
73
73
|
unattended: options.unattended,
|
|
74
74
|
peerAsk: options.peerAsk,
|
|
75
|
+
sender: options.sender,
|
|
76
|
+
trigger: options.trigger,
|
|
75
77
|
};
|
|
76
78
|
saveChatFollowup({ id, kind: "bot", ownerId: botId, threadId, payload: item });
|
|
77
79
|
entry.items.push(item);
|
|
@@ -140,6 +142,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
140
142
|
sendId: item.sendId,
|
|
141
143
|
queueId: item.messageId,
|
|
142
144
|
peerAsk: item.peerAsk,
|
|
145
|
+
sender: item.sender,
|
|
143
146
|
}));
|
|
144
147
|
}
|
|
145
148
|
const last = appended.at(-1);
|
|
@@ -153,7 +156,9 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
153
156
|
const running = run(entry.botId, threadId, prompt, last, appended.map((message) => message.id),
|
|
154
157
|
// one unattended line makes the whole drained turn unattended: a
|
|
155
158
|
// person's words in the same queue cannot re-attend a bot's own
|
|
156
|
-
entry.items.some((item) => item.unattended === true)
|
|
159
|
+
entry.items.some((item) => item.unattended === true),
|
|
160
|
+
// the first waiting line is the one that starts this turn
|
|
161
|
+
{ trigger: entry.items[0].trigger, sender: entry.items[0].sender, peerAsk: entry.items[0].peerAsk });
|
|
157
162
|
void Promise.resolve(running).then(() => settleChatFollowups(ids, null), () => settleChatFollowups(ids, "interrupted")).catch((error) => console.warn("steer-queue: could not settle durable follow-up", error));
|
|
158
163
|
}
|
|
159
164
|
}
|