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
|
@@ -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) {
|
|
@@ -6,6 +6,7 @@ const nodeSchema = z.object({
|
|
|
6
6
|
id: z.string(), rootId: z.string(), parentId: z.string().optional(),
|
|
7
7
|
groupId: z.string().optional(), threadId: z.string(), botId: z.string(),
|
|
8
8
|
key: z.string(), text: z.string(), createdAt: z.number(),
|
|
9
|
+
requestBatchKey: z.string().optional(),
|
|
9
10
|
status: z.enum(["source", "queued", "running", "waiting", "resume", "completed", "failed", "cancelled"]),
|
|
10
11
|
result: z.string().default(""), reported: z.boolean().default(false),
|
|
11
12
|
executions: z.number().int().nonnegative().default(0), startedAt: z.number().optional(),
|
|
@@ -159,7 +160,7 @@ export class RoomHandoffs {
|
|
|
159
160
|
const root = this.root(n);
|
|
160
161
|
return `Room handoff hard cap exhausted: node was ${n.status} after ${duration(this.now() - root.createdAt)} of the ${duration(this.limits.hardCapMs)} wall-clock cap`;
|
|
161
162
|
}
|
|
162
|
-
enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "") {
|
|
163
|
+
enqueue(source, generation, parentId, target, key, text, approvalGranted = false, rework = false, sourceText = "", requestBatchKey) {
|
|
163
164
|
if (this.loadError)
|
|
164
165
|
throw new Error(this.loadError);
|
|
165
166
|
let parent = parentId ? this.nodes.get(parentId) : this.nodes.get(generation);
|
|
@@ -178,10 +179,17 @@ export class RoomHandoffs {
|
|
|
178
179
|
const existing = this.children(parent.id).find(n => n.key === key);
|
|
179
180
|
if (existing) {
|
|
180
181
|
if (existing.groupId !== target.groupId || existing.botId !== target.botId || existing.text !== text ||
|
|
181
|
-
existing.kind !== kind)
|
|
182
|
+
existing.kind !== kind || existing.requestBatchKey !== requestBatchKey)
|
|
182
183
|
throw new Error("request_key was already used for different work");
|
|
183
184
|
return { node: existing, duplicate: true };
|
|
184
185
|
}
|
|
186
|
+
if (requestBatchKey && target.groupId) {
|
|
187
|
+
const batch = this.children(parent.id).filter(n => n.requestBatchKey === requestBatchKey && n.groupId === target.groupId);
|
|
188
|
+
if (batch.some(n => n.text !== text || n.threadId !== target.threadId))
|
|
189
|
+
throw new Error("request_key was already used for different room work");
|
|
190
|
+
if (batch.some(n => n.startedAt !== undefined))
|
|
191
|
+
throw new Error("This shared room request has already started; use a new request_key for additional recipients");
|
|
192
|
+
}
|
|
185
193
|
if (!rework && this.children(parent.id).some(n => n.kind === kind &&
|
|
186
194
|
n.groupId === target.groupId && n.botId === target.botId && n.status === "completed")) {
|
|
187
195
|
throw new Error("This agent already completed your assignment. Do not send acknowledgements or approvals as new work. Finish with your decision; results return automatically. Only use rework=true for concrete additional work.");
|
|
@@ -221,7 +229,7 @@ export class RoomHandoffs {
|
|
|
221
229
|
}
|
|
222
230
|
const node = { ...target, id: randomUUID(), rootId: parent.rootId, parentId: parent.id,
|
|
223
231
|
key, text, createdAt: this.now(), status: "queued", result: "", reported: false, executions: 0, approvalGranted,
|
|
224
|
-
kind };
|
|
232
|
+
kind, ...(target.groupId && requestBatchKey ? { requestBatchKey } : {}) };
|
|
225
233
|
const problem = this.hooks.validate(node, parent);
|
|
226
234
|
if (problem)
|
|
227
235
|
throw new Error(problem);
|
|
@@ -239,16 +247,25 @@ export class RoomHandoffs {
|
|
|
239
247
|
}
|
|
240
248
|
return { node, duplicate: false };
|
|
241
249
|
}
|
|
250
|
+
/** A room brief is displayed once; each recipient keeps its own execution and result. */
|
|
251
|
+
sharedRequest(node) {
|
|
252
|
+
const batch = node.groupId && node.requestBatchKey && node.parentId
|
|
253
|
+
? this.children(node.parentId).filter(n => n.requestBatchKey === node.requestBatchKey &&
|
|
254
|
+
n.groupId === node.groupId && n.threadId === node.threadId && n.text === node.text)
|
|
255
|
+
: [node];
|
|
256
|
+
return { id: batch[0]?.id ?? node.id, botIds: batch.map(n => n.botId) };
|
|
257
|
+
}
|
|
242
258
|
sourceSettled(generation, ok) {
|
|
243
259
|
const node = this.nodes.get(generation);
|
|
244
260
|
if (!node || node.status !== "source")
|
|
245
261
|
return;
|
|
262
|
+
// An accepted assignment belongs to the queue, not the provider that
|
|
263
|
+
// submitted it. A failed/expired source turn must not erase that work.
|
|
264
|
+
// Explicit Stop, deletion and revoked routes still cancel separately.
|
|
246
265
|
if (!ok)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.publish(node);
|
|
251
|
-
}
|
|
266
|
+
node.result = "The originating turn ended before its teammates returned.";
|
|
267
|
+
node.status = "waiting";
|
|
268
|
+
this.publish(node);
|
|
252
269
|
}
|
|
253
270
|
cancelTree(node, reason, status = "cancelled") {
|
|
254
271
|
for (const child of this.children(node.id))
|
|
@@ -347,8 +364,10 @@ export class RoomHandoffs {
|
|
|
347
364
|
}
|
|
348
365
|
if (n.status !== "queued" && n.status !== "resume")
|
|
349
366
|
continue;
|
|
350
|
-
//
|
|
351
|
-
|
|
367
|
+
// Independent conversations can start as soon as work is accepted.
|
|
368
|
+
// Same-room speakers still serialize; never overlap their shared chat.
|
|
369
|
+
if (parent && (parent.status === "source" || parent.status === "running") &&
|
|
370
|
+
(parent.threadId === n.threadId || (n.groupId && n.groupId === parent.groupId)))
|
|
352
371
|
continue;
|
|
353
372
|
// A stopped source stops waiting; only work that never started is
|
|
354
373
|
// dropped with it. A teammate mid-turn keeps its process and reports.
|
|
@@ -381,10 +400,10 @@ export class RoomHandoffs {
|
|
|
381
400
|
if (terminal(n))
|
|
382
401
|
return;
|
|
383
402
|
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)
|
|
403
|
+
if (this.children(n.id).length > childCount)
|
|
387
404
|
n.status = "waiting";
|
|
405
|
+
else if (!result.ok)
|
|
406
|
+
this.cancelTree(n, n.result || "Room agent failed", "failed");
|
|
388
407
|
else
|
|
389
408
|
n.status = "completed";
|
|
390
409
|
// Close the paused span with the settlement itself: work enqueued
|
|
@@ -392,7 +411,18 @@ export class RoomHandoffs {
|
|
|
392
411
|
// budget, not the still-open pause's overstated runway.
|
|
393
412
|
this.trackExecutionPauses();
|
|
394
413
|
this.publish(n);
|
|
395
|
-
}).catch(e => {
|
|
414
|
+
}).catch(e => {
|
|
415
|
+
if (terminal(n))
|
|
416
|
+
return;
|
|
417
|
+
n.result = String(e).slice(0, 1000);
|
|
418
|
+
if (this.children(n.id).length > childCount) {
|
|
419
|
+
n.status = "waiting";
|
|
420
|
+
this.trackExecutionPauses();
|
|
421
|
+
this.publish(n);
|
|
422
|
+
}
|
|
423
|
+
else
|
|
424
|
+
this.cancelTree(n, n.result, "failed");
|
|
425
|
+
})
|
|
396
426
|
.finally(() => this.controllers.delete(n.id));
|
|
397
427
|
}
|
|
398
428
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Preset bots for New bot (server/presets.ts has the store and the rules).
|
|
2
|
+
// GET lists what New bot offers: the organization's presets first, then
|
|
3
|
+
// imported files', never one from a withdrawn release. DELETE removes an
|
|
4
|
+
// imported preset (the person's own click; no confirm step). Organization
|
|
5
|
+
// presets are Admin's to remove. Both are admin-scoped by default in
|
|
6
|
+
// server/request-auth.ts, like the New bot defaults: a preset adds skills
|
|
7
|
+
// and starter notes to the bot made from it.
|
|
8
|
+
import { listBotPresets, ORG_PRESET_REMOVE_MESSAGE, PRESET_UNAVAILABLE_MESSAGE } from "../presets.js";
|
|
9
|
+
import { PASS } from "./table.js";
|
|
10
|
+
export function createBotPresetRoutes(deps) {
|
|
11
|
+
return async ({ res, path, method, json }) => {
|
|
12
|
+
if (path === "/api/bot-presets" && method === "GET") {
|
|
13
|
+
return json(res, 200, { presets: listBotPresets(deps.presets, deps.orgStatuses()) });
|
|
14
|
+
}
|
|
15
|
+
const m = path.match(/^\/api\/bot-presets\/([\w-]+)$/);
|
|
16
|
+
if (!m || method !== "DELETE")
|
|
17
|
+
return PASS;
|
|
18
|
+
const removed = deps.presets.removeFilePreset(m[1]);
|
|
19
|
+
if (removed === "not_found")
|
|
20
|
+
return json(res, 404, { error: PRESET_UNAVAILABLE_MESSAGE });
|
|
21
|
+
if (removed === "organization")
|
|
22
|
+
return json(res, 409, { error: ORG_PRESET_REMOVE_MESSAGE });
|
|
23
|
+
return json(res, 200, { ok: true });
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -923,7 +923,7 @@ export class RoutineRequestService {
|
|
|
923
923
|
if (refusal)
|
|
924
924
|
throw new RoutineRequestError(refusal, 403);
|
|
925
925
|
}
|
|
926
|
-
await this.requireCloudReadiness(operation);
|
|
926
|
+
await this.requireCloudReadiness(operation, botId);
|
|
927
927
|
// The readiness probe is asynchronous. Another request can edit or
|
|
928
928
|
// delete the routine while it is in flight, so re-check the captured
|
|
929
929
|
// revision before rendering and persisting the confirmation snapshot.
|
|
@@ -1006,7 +1006,7 @@ export class RoutineRequestService {
|
|
|
1006
1006
|
}
|
|
1007
1007
|
throw new RoutineRequestError(result.state === "invalid" ? result.error : "The routine change could not be applied", result.state === "invalid" ? result.status : 409);
|
|
1008
1008
|
}
|
|
1009
|
-
async requireCloudReadiness(operation) {
|
|
1009
|
+
async requireCloudReadiness(operation, proposerBotId) {
|
|
1010
1010
|
if (!this.cloudReady || operation.action === "pause" || operation.action === "delete")
|
|
1011
1011
|
return;
|
|
1012
1012
|
const definition = effectiveDefinition(operation, this.routines);
|
|
@@ -1014,7 +1014,10 @@ export class RoutineRequestService {
|
|
|
1014
1014
|
return;
|
|
1015
1015
|
let readiness;
|
|
1016
1016
|
try {
|
|
1017
|
-
|
|
1017
|
+
const targetBotId = operation.action === "create"
|
|
1018
|
+
? operation.forBot?.botId ?? proposerBotId
|
|
1019
|
+
: this.routines.listRoutines().find(routine => routine.id === operation.routineId)?.botId ?? proposerBotId;
|
|
1020
|
+
readiness = await this.cloudReady(targetBotId);
|
|
1018
1021
|
}
|
|
1019
1022
|
catch (error) {
|
|
1020
1023
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -6,7 +6,25 @@ 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";
|
|
10
|
+
import { ROUTINE_PARTS } from "./package-parts.js";
|
|
9
11
|
const persistedSourceThreadId = z.string().trim().min(1).optional().catch(undefined);
|
|
12
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
13
|
+
function loadInstalledPackage(value) {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return undefined;
|
|
16
|
+
const stamp = value;
|
|
17
|
+
if (typeof stamp.installId !== "string" || !/^[a-f0-9]{32}$/.test(stamp.installId) || typeof stamp.key !== "string" || !stamp.key || stamp.key.length > 80)
|
|
18
|
+
return undefined;
|
|
19
|
+
const parts = stamp.parts;
|
|
20
|
+
if (!parts || typeof parts !== "object" || !ROUTINE_PARTS.every((part) => HASH.test(String(parts[part]?.r)) && HASH.test(String(parts[part]?.w))))
|
|
21
|
+
return undefined;
|
|
22
|
+
return {
|
|
23
|
+
installId: stamp.installId,
|
|
24
|
+
key: stamp.key,
|
|
25
|
+
parts: Object.fromEntries(ROUTINE_PARTS.map((part) => [part, { r: parts[part].r, w: parts[part].w }])),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
10
28
|
const webhookRunReceiptSchema = z.object({
|
|
11
29
|
webhookId: z.string().min(1).max(200),
|
|
12
30
|
deliveryId: z.string().min(1).max(200),
|
|
@@ -489,9 +507,12 @@ export class RoutineManager {
|
|
|
489
507
|
overlap: routine.overlap === "queue" ? "queue" : undefined,
|
|
490
508
|
skippedRuns: Number.isSafeInteger(routine.skippedRuns) && routine.skippedRuns > 0 ? routine.skippedRuns : undefined,
|
|
491
509
|
lastSkippedAt: Number.isSafeInteger(routine.lastSkippedAt) && routine.lastSkippedAt >= 0 && routine.lastSkippedAt <= MAX_DATE_MS ? routine.lastSkippedAt : undefined,
|
|
510
|
+
installedPackage: loadInstalledPackage(routine.installedPackage),
|
|
492
511
|
};
|
|
493
512
|
if (loaded.timeoutMinutes === undefined)
|
|
494
513
|
delete loaded.timeoutMinutes;
|
|
514
|
+
if (loaded.installedPackage === undefined)
|
|
515
|
+
delete loaded.installedPackage;
|
|
495
516
|
delete loaded.failureStreak;
|
|
496
517
|
return [loaded];
|
|
497
518
|
})
|
|
@@ -581,7 +602,27 @@ export class RoutineManager {
|
|
|
581
602
|
.sort((a, b) => (b.finishedAt ?? b.createdAt) - (a.finishedAt ?? a.createdAt) || b.createdAt - a.createdAt);
|
|
582
603
|
const success = outcomes.findIndex(run => run.status === "completed");
|
|
583
604
|
const failures = success < 0 ? outcomes.length : success;
|
|
584
|
-
|
|
605
|
+
const { installedPackage: _installedPackage, ...visible } = cloneRoutine(routine);
|
|
606
|
+
return { ...visible, ...(failures ? { failureStreak: failures } : {}) };
|
|
607
|
+
}
|
|
608
|
+
/** Routines added from the organization's library, with their stamps. */
|
|
609
|
+
packageStamps() {
|
|
610
|
+
return this.routines.flatMap((routine) => routine.installedPackage
|
|
611
|
+
? [{ routineId: routine.id, botId: routine.botId, enabled: routine.enabled, stamp: structuredClone(routine.installedPackage) }]
|
|
612
|
+
: []);
|
|
613
|
+
}
|
|
614
|
+
/** Record which organization install a just-created routine belongs to. */
|
|
615
|
+
stampInstalledPackage(id, stamp) {
|
|
616
|
+
const clean = loadInstalledPackage(stamp);
|
|
617
|
+
if (!clean)
|
|
618
|
+
throw new Error("Invalid routine package stamp");
|
|
619
|
+
if (!this.routines.some((routine) => routine.id === id))
|
|
620
|
+
return false;
|
|
621
|
+
this.commitMutation(() => {
|
|
622
|
+
const routine = this.routines.find((candidate) => candidate.id === id);
|
|
623
|
+
routine.installedPackage = clean;
|
|
624
|
+
});
|
|
625
|
+
return true;
|
|
585
626
|
}
|
|
586
627
|
/** Whether this computer should stay awake for routines: a run is in
|
|
587
628
|
* flight, or an enabled routine is due within the horizon. The schedule
|
|
@@ -1063,6 +1104,27 @@ export class RoutineManager {
|
|
|
1063
1104
|
}
|
|
1064
1105
|
return cloneRun(run);
|
|
1065
1106
|
}
|
|
1107
|
+
/** Clear every failure indicator at once: stamp seenAt on each unseen
|
|
1108
|
+
* failed/missed run in one committed save, then emit the updated runs so
|
|
1109
|
+
* connected clients drop their dots immediately. A failed save rolls the
|
|
1110
|
+
* stamps back so a retry still finds the unseen runs. */
|
|
1111
|
+
markAllSeen() {
|
|
1112
|
+
if (!this.runs.some((run) => !run.seenAt && isRoutineProblemRun(run)))
|
|
1113
|
+
return [];
|
|
1114
|
+
const stampAt = this.now();
|
|
1115
|
+
const updated = [];
|
|
1116
|
+
this.commitMutation(() => {
|
|
1117
|
+
for (const run of this.runs) {
|
|
1118
|
+
if (run.seenAt || !isRoutineProblemRun(run))
|
|
1119
|
+
continue;
|
|
1120
|
+
run.seenAt = stampAt;
|
|
1121
|
+
updated.push(run);
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
for (const run of updated)
|
|
1125
|
+
this.emitRun(run);
|
|
1126
|
+
return updated.map(cloneRun);
|
|
1127
|
+
}
|
|
1066
1128
|
get isTicking() { return this.ticking; }
|
|
1067
1129
|
start() {
|
|
1068
1130
|
if (this.timer)
|
|
@@ -9,6 +9,7 @@ import { join } from "node:path";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { writeFileAtomic } from "./atomic.js";
|
|
11
11
|
import { DATA_DIR } from "./config.js";
|
|
12
|
+
import { renamePublishedTeam } from "./published-teams.js";
|
|
12
13
|
export const SECTION_CONTEXT_MAX_BYTES = 24_000;
|
|
13
14
|
export const SECTION_CONTEXTS_FILE = join(DATA_DIR, "section-contexts.json");
|
|
14
15
|
const sectionContextRecordSchema = z.object({ text: z.string(), updatedAt: z.number().finite() });
|
|
@@ -94,6 +95,15 @@ export function changeEmptySection(name, nextName) {
|
|
|
94
95
|
if (nextName !== null && context)
|
|
95
96
|
data.contexts[nextName] = context;
|
|
96
97
|
saveFile(data);
|
|
98
|
+
// Every team rename and removal funnels through here, so the team's
|
|
99
|
+
// published package id and keys follow it (published-teams.ts). The team
|
|
100
|
+
// itself is already renamed; a failure here costs only key stability.
|
|
101
|
+
try {
|
|
102
|
+
renamePublishedTeam(name, nextName);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.warn(`[teams] Could not move the shared-package record for this team: ${error.message}`);
|
|
106
|
+
}
|
|
97
107
|
}
|
|
98
108
|
export function readSectionContext(section) {
|
|
99
109
|
const record = loadFile().contexts[sectionContextKey(section)];
|
|
@@ -34,14 +34,11 @@ import { DATA_DIR } from "./config.js";
|
|
|
34
34
|
import { redactSecretsInText } from "./redact.js";
|
|
35
35
|
import { LEARN_SOURCE_PREFIX } from "./skill-learn.js";
|
|
36
36
|
import { workspaceDir } from "./workspace.js";
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export
|
|
42
|
-
export const DESCRIPTION_MAX = 1024;
|
|
43
|
-
/** One SKILL.md may be at most this large; the spec recommends <5k tokens. */
|
|
44
|
-
export const SKILL_FILE_MAX_BYTES = 256 * 1024;
|
|
37
|
+
import { isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES } from "../shared/skill-md.js";
|
|
38
|
+
// SKILL.md parsing and the static scan live in shared/ so the package
|
|
39
|
+
// format (Admin and the renderer included) validates skills with the exact
|
|
40
|
+
// same rules; re-exported here under their historical path.
|
|
41
|
+
export { DESCRIPTION_MAX, isSkillName, parseSkillMd, scanSkillText, SKILL_FILE_MAX_BYTES, SKILL_NAME_MAX, } from "../shared/skill-md.js";
|
|
45
42
|
/** Index budget: name+description lines only, ~100 tokens per skill. */
|
|
46
43
|
export const INDEX_MAX_SKILLS = 30;
|
|
47
44
|
export const INDEX_MAX_BYTES = 4_000;
|
|
@@ -51,58 +48,6 @@ export const STAGED_GIST_MAX = 240;
|
|
|
51
48
|
/** Learned skills are duplicated onto their durable review card. Keep that
|
|
52
49
|
* exact review payload bounded while leaving fetched skill imports unchanged. */
|
|
53
50
|
export const STAGED_SKILL_FILE_MAX_BYTES = 32 * 1024;
|
|
54
|
-
export function isSkillName(name) {
|
|
55
|
-
return name.length >= 1 && name.length <= SKILL_NAME_MAX && SKILL_NAME.test(name);
|
|
56
|
-
}
|
|
57
|
-
/** Minimal frontmatter reader for the two required keys plus the two we
|
|
58
|
-
* display. Deliberately not a YAML engine: values are single-line strings in
|
|
59
|
-
* every skill the spec's own examples show, and a parser that cannot
|
|
60
|
-
* evaluate anchors or tags cannot be surprised by them. */
|
|
61
|
-
export function parseSkillMd(raw) {
|
|
62
|
-
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
63
|
-
if (!match)
|
|
64
|
-
return { error: "SKILL.md has no YAML frontmatter (--- block) at the top" };
|
|
65
|
-
const fields = {};
|
|
66
|
-
for (const line of match[1].split(/\r?\n/)) {
|
|
67
|
-
const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
|
|
68
|
-
if (!kv)
|
|
69
|
-
continue;
|
|
70
|
-
fields[kv[1].toLowerCase()] = kv[2].replace(/^["']|["']$/g, "").trim();
|
|
71
|
-
}
|
|
72
|
-
const name = fields.name ?? "";
|
|
73
|
-
const description = fields.description ?? "";
|
|
74
|
-
if (!isSkillName(name)) {
|
|
75
|
-
return { error: `frontmatter name ${JSON.stringify(name)} is not a valid skill name (lowercase, hyphens, max ${SKILL_NAME_MAX})` };
|
|
76
|
-
}
|
|
77
|
-
if (!description || description.length > DESCRIPTION_MAX) {
|
|
78
|
-
return { error: `frontmatter description is required and must be at most ${DESCRIPTION_MAX} characters` };
|
|
79
|
-
}
|
|
80
|
-
return {
|
|
81
|
-
name,
|
|
82
|
-
description,
|
|
83
|
-
license: fields.license || undefined,
|
|
84
|
-
compatibility: fields.compatibility || undefined,
|
|
85
|
-
body: match[2] ?? "",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
/** Static red flags before a human review. Presence is a warning shown in
|
|
89
|
-
* the review screen, never a silent rejection — the reviewer decides. These
|
|
90
|
-
* are the three patterns the public registry audits actually caught. */
|
|
91
|
-
export function scanSkillText(raw) {
|
|
92
|
-
const warnings = [];
|
|
93
|
-
if (/[A-Za-z0-9+/]{120,}={0,2}/.test(raw)) {
|
|
94
|
-
warnings.push("contains a long base64-looking blob — a common wrapper for hidden instructions or payloads");
|
|
95
|
-
}
|
|
96
|
-
if (/\b(curl|wget)\b[^\n]{0,200}\|\s*(ba|z|da)?sh\b/.test(raw)) {
|
|
97
|
-
warnings.push("pipes a download straight into a shell (curl|sh) — never enable without understanding why");
|
|
98
|
-
}
|
|
99
|
-
// zero-width and bidi-control characters hide text from the reviewer while
|
|
100
|
-
// the model still reads it — the invisible-instruction trick
|
|
101
|
-
if (/[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/.test(raw)) {
|
|
102
|
-
warnings.push("contains invisible Unicode characters (zero-width or bidi controls) — text you cannot see");
|
|
103
|
-
}
|
|
104
|
-
return warnings;
|
|
105
|
-
}
|
|
106
51
|
const skillManifestEntrySchema = z.object({
|
|
107
52
|
description: z.string(),
|
|
108
53
|
enabled: z.boolean(),
|
|
@@ -115,6 +60,13 @@ const skillManifestEntrySchema = z.object({
|
|
|
115
60
|
skippedFiles: z.array(z.string()),
|
|
116
61
|
appliedStageId: z.string().optional(),
|
|
117
62
|
storageRevision: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
63
|
+
package: z.object({
|
|
64
|
+
installId: z.string().regex(/^[a-f0-9]{32}$/),
|
|
65
|
+
key: z.string().min(1).max(64),
|
|
66
|
+
release: z.string().min(1).max(40),
|
|
67
|
+
r: z.string().regex(/^[a-f0-9]{64}$/),
|
|
68
|
+
w: z.string().regex(/^[a-f0-9]{64}$/),
|
|
69
|
+
}).optional(),
|
|
118
70
|
});
|
|
119
71
|
const skillManifestSchema = z.record(z.string(), skillManifestEntrySchema);
|
|
120
72
|
const managedLinksSchema = z.array(z.string());
|
|
@@ -498,7 +450,7 @@ function skillContentMatches(botId, name, entry) {
|
|
|
498
450
|
}
|
|
499
451
|
}
|
|
500
452
|
function skillListing(botId, name, entry) {
|
|
501
|
-
const { appliedStageId, storageRevision: _storageRevision, ...visible } = entry;
|
|
453
|
+
const { appliedStageId, storageRevision: _storageRevision, package: _package, ...visible } = entry;
|
|
502
454
|
const intact = skillContentMatches(botId, name, entry);
|
|
503
455
|
return {
|
|
504
456
|
name,
|
|
@@ -563,6 +515,26 @@ export function installSkill(botId, source, files) {
|
|
|
563
515
|
return prepared;
|
|
564
516
|
return installPreparedSkill(botId, source, prepared, { enabled: false });
|
|
565
517
|
}
|
|
518
|
+
/** The organization library's path: a skill the organization's Admin
|
|
519
|
+
* published arrives switched ON (the Admin is the review), with the install
|
|
520
|
+
* stamp that lets the library find it again. `source` is chosen by the
|
|
521
|
+
* caller (`org:<ref>@<release>`), never read from the package. */
|
|
522
|
+
export function installOrgSkill(botId, source, skillMd, stamp) {
|
|
523
|
+
if (!source.startsWith("org:"))
|
|
524
|
+
return { error: "organization skills need an org: source" };
|
|
525
|
+
const prepared = preparedSkillFiles([{ path: "SKILL.md", content: skillMd }]);
|
|
526
|
+
if ("error" in prepared)
|
|
527
|
+
return prepared;
|
|
528
|
+
if (prepared.parsed.name !== stamp.key)
|
|
529
|
+
return { error: `the skill is named "${prepared.parsed.name}", not "${stamp.key}"` };
|
|
530
|
+
return installPreparedSkill(botId, source, prepared, { enabled: true, package: { ...stamp } });
|
|
531
|
+
}
|
|
532
|
+
/** A bot's skills that an organization install added, with their stamps. */
|
|
533
|
+
export function skillPackageStamps(botId) {
|
|
534
|
+
return Object.entries(readManifest(botId)).flatMap(([name, entry]) => entry.package
|
|
535
|
+
? [{ name, enabled: entry.enabled, stamp: { ...entry.package } }]
|
|
536
|
+
: []);
|
|
537
|
+
}
|
|
566
538
|
export function setSkillEnabled(botId, name, enabled) {
|
|
567
539
|
if (!isSkillName(name))
|
|
568
540
|
return { error: "invalid skill name" };
|
|
@@ -936,6 +908,7 @@ function installPreparedSkill(botId, source, prepared, options) {
|
|
|
936
908
|
warnings: prepared.warnings,
|
|
937
909
|
skippedFiles: prepared.skippedFiles,
|
|
938
910
|
appliedStageId: options.appliedStageId,
|
|
911
|
+
...(options.package ? { package: options.package } : {}),
|
|
939
912
|
};
|
|
940
913
|
const root = ensureSkillsRoot(botId);
|
|
941
914
|
if (!root)
|