openmausbot 0.1.86 → 0.1.88
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 +4 -3
- package/dist/assets/{abnfDiagram-O67JEVCF-DpUZNnFD.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-DW7mrI4p.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CyeZ5WLZ.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CBVaNjO7.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-sf8d41uT.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-BOGvy7Y_.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BiF9zyiz.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-EFuZ1SBZ.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-pJLNSAT7.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-Cb-3_3IT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-DAf5aogn.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DR3kiwZz.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BmU8PeyM.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-Buh8g8OT.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BurfmMWM.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-KkGZUEkU.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-C3PpV96d.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Dt8JwJaI.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DkjN8266.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-DUAJS0JV.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Dw4KhF1a.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-Dd1gmQqf.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CjwGs0Yw.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-BL-69Zrq.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-5Zi2N7wo.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-D9NekWUX.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CkaSQjxw.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-CdAVBV8-.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bm_lWWH7.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-FZ8ejYWJ.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dlv9rizZ.js → gitGraphDiagram-X574FWY7-Dpy2QSGq.js} +1 -1
- package/dist/assets/index-BIMdIbaw.css +1 -0
- package/dist/assets/index-CHnODCr2.js +583 -0
- package/dist/assets/{index-h9FFNwTu.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-BdQ_gW5B.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-EwssEp2X.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-BX1G1vOd.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-BB4kAsyC.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-3su2TG93.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-CDD1E0s9.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Ba50MsNK.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-DZLkkZlM.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DaTz77Uq.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CpaiidpR.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-mN4JrMeI.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-BSNDRpg7.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-FngzGNug.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-CM4pCC3L.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CRICHXkw.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-oyTWpxH2.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-D9h9mPBq.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BJUlN9bW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-3uBNkbAb.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D2odalzT.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DHgFaB9S.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-w5GYM2h0.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/browser-proxy.js +2 -1
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +1032 -165
- package/dist-server/drivers/agents-proxy.js +338 -29
- package/dist-server/drivers/phone-proxy.js +33 -2
- package/dist-server/index.js +40590 -31811
- package/dist-server/local-computer-proxy.js +5 -3
- package/dist-server/local-computer.js +53 -17
- package/dist-server/mcp-gate.js +2 -0
- package/dist-server/mcp-server.js +5 -3
- package/dist-server/message-search.worker.js +52 -0
- package/dist-server/openmausbot.js +4457 -1399
- package/dist-server/pair-cli.js +4457 -1399
- package/dist-server/permission-proxy.js +29 -9
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +98 -1
- package/dist-server/server/auto-approve.js +4 -3
- package/dist-server/server/auto-vm-claims.js +1 -0
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/bot-package.js +18 -292
- package/dist-server/server/bot-profile.js +5 -17
- package/dist-server/server/box-create-idempotency.js +3 -3
- package/dist-server/server/box.js +57 -55
- package/dist-server/server/browser-proxy.js +3 -2
- package/dist-server/server/browser-runtime.js +37 -1
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/cli.js +2 -2
- package/dist-server/server/command-allowlist.js +154 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/computer-wait.js +22 -9
- package/dist-server/server/config.js +84 -12
- package/dist-server/server/connector-advertisement.js +165 -0
- package/dist-server/server/connector-proxy.js +14 -2
- package/dist-server/server/connector-verdict.js +168 -0
- package/dist-server/server/container-computer.js +61 -1
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/default-model-selection.js +9 -0
- package/dist-server/server/delegations.js +85 -34
- package/dist-server/server/drivers/acp/core.js +80 -20
- package/dist-server/server/drivers/acp/cursor.js +3 -7
- package/dist-server/server/drivers/acp/droid.js +4 -7
- package/dist-server/server/drivers/acp/gemini.js +4 -4
- package/dist-server/server/drivers/acp/grok.js +4 -9
- package/dist-server/server/drivers/acp/hermes.js +3 -5
- package/dist-server/server/drivers/acp/opencode-go.js +2 -5
- package/dist-server/server/drivers/acp/qwen.js +9 -15
- package/dist-server/server/drivers/agents-call.js +179 -13
- package/dist-server/server/drivers/agents-catalog.js +134 -10
- package/dist-server/server/drivers/boxagent.js +160 -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 +38 -8
- package/dist-server/server/drivers/chat-tool-approval.js +70 -18
- package/dist-server/server/drivers/claude.js +38 -19
- package/dist-server/server/drivers/codex-catalog.js +14 -6
- package/dist-server/server/drivers/codex-identity.js +2 -1
- package/dist-server/server/drivers/codex-instructions.js +48 -6
- package/dist-server/server/drivers/codex-release.js +48 -0
- package/dist-server/server/drivers/codex-tool-surface.js +20 -0
- package/dist-server/server/drivers/codex.js +102 -55
- package/dist-server/server/drivers/mistral.js +115 -0
- package/dist-server/server/drivers/openai-chat.js +180 -33
- package/dist-server/server/drivers/openai-compat.js +1 -0
- 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 +306 -28
- package/dist-server/server/drivers/prompt-split.js +110 -0
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -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 +2 -2
- package/dist-server/server/harness/registry.js +1 -0
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/hosted-models.js +1 -1
- package/dist-server/server/index.js +2396 -505
- package/dist-server/server/local-computer.js +72 -23
- package/dist-server/server/managed-desktop.js +1 -1
- package/dist-server/server/mcp-bridge.js +6 -6
- package/dist-server/server/message-db.js +22 -42
- package/dist-server/server/message-file.js +9 -1
- package/dist-server/server/message-search-query.js +43 -0
- package/dist-server/server/message-search-worker.js +80 -0
- package/dist-server/server/message-search.worker.js +21 -0
- package/dist-server/server/model-requests.js +248 -0
- package/dist-server/server/new-bot-defaults.js +115 -0
- package/dist-server/server/options-card.js +21 -0
- package/dist-server/server/org-library.js +1065 -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-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +67 -0
- package/dist-server/server/presets.js +492 -0
- package/dist-server/server/profile-requests.js +66 -39
- package/dist-server/server/profile-revision.js +9 -1
- package/dist-server/server/profile-versions.js +6 -3
- package/dist-server/server/provider-key-check.js +3 -2
- package/dist-server/server/published-teams.js +80 -0
- package/dist-server/server/room-handoffs.js +19 -3
- package/dist-server/server/routes/bot-presets.js +25 -0
- package/dist-server/server/routine-requests.js +35 -10
- package/dist-server/server/routines.js +41 -1
- package/dist-server/server/section-context.js +10 -0
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +35 -61
- package/dist-server/server/steer-queue.js +3 -2
- package/dist-server/server/store.js +291 -17
- package/dist-server/server/surface.js +10 -4
- package/dist-server/server/system-prompt.js +38 -6
- package/dist-server/server/team-backup.js +6 -2
- 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 +139 -27
- package/dist-server/server/thread-events.js +20 -2
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- 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 +19 -2
- 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 +12 -0
- package/dist-server/server/workspace-backup.js +42 -5
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/server/workspace.js +1 -1
- package/dist-server/shared/approval-mode.js +1 -1
- package/dist-server/shared/ask-question.js +207 -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 +6 -2
- package/dist-server/shared/line-diff.js +23 -0
- package/dist-server/shared/model-request.js +1 -0
- package/dist-server/shared/options-card.js +51 -0
- package/dist-server/shared/package-format.js +967 -0
- package/dist-server/shared/profile-request.js +5 -4
- package/dist-server/shared/skill-md.js +64 -0
- package/dist-server/shared/team-backup.js +7 -0
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/shared/wire.js +4 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +1132 -171
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/enterprise/README.md +1 -1
- package/package.json +1 -1
- package/dist/assets/channel-BXFkgus9.js +0 -1
- package/dist/assets/index-BGgqlWPJ.css +0 -1
- package/dist/assets/index-BtHo8ZKl.js +0 -577
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BWrvB2ak.js +0 -8
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// propose_profile: a bot proposes changes to its own name, title, description,
|
|
2
|
-
// SOUL.md, or
|
|
2
|
+
// SOUL.md, working folder, or alert/voice toggles; confirmed cards and Full Access submissions share
|
|
3
3
|
// the same validated commit path. Same
|
|
4
4
|
// shape as routine-requests.ts, much smaller: the profile commits in one
|
|
5
5
|
// store call, and staleness is a hash of the five fields instead of a
|
|
6
6
|
// scheduler revision. Everything here is re-validated at confirm time —
|
|
7
7
|
// a card can sit open for days.
|
|
8
|
-
import {
|
|
8
|
+
import { soulDiffLines } from "../shared/line-diff.js";
|
|
9
9
|
import { BOT_PROFILE_LIMITS } from "../shared/bot-profile.js";
|
|
10
10
|
import { PROFILE_REQUEST_FIELDS } from "../shared/profile-request.js";
|
|
11
11
|
import { parseBotProfilePatch } from "./bot-profile.js";
|
|
@@ -15,22 +15,32 @@ import { profileRevision, profileSnapshot } from "./profile-revision.js";
|
|
|
15
15
|
import { recordProfileChange } from "./profile-versions.js";
|
|
16
16
|
import { redactSecretsInText } from "./redact.js";
|
|
17
17
|
const MAX_REASON = 500;
|
|
18
|
-
const MAX_DIFF_LINES = 400;
|
|
19
18
|
const STALE = "This bot's profile changed after this card was prepared. Ask the bot to review it and propose again.";
|
|
20
19
|
const NO_SUCH_BOT = "That bot no longer exists";
|
|
21
20
|
const LABELS = {
|
|
22
21
|
name: "Name",
|
|
23
22
|
title: "Title",
|
|
24
23
|
description: "Description",
|
|
24
|
+
notifications: "Notifications",
|
|
25
|
+
speakReplies: "Speak replies",
|
|
25
26
|
};
|
|
26
27
|
const PRIVATE_WORKSPACE = "its private workspace";
|
|
27
|
-
const CHOOSE_ONE = "Choose at least one of name, title, description, soul, cwd";
|
|
28
|
+
const CHOOSE_ONE = "Choose at least one of name, title, description, soul, cwd, notifications, speakReplies";
|
|
29
|
+
const toggleText = (value) => (value ? "on" : "off");
|
|
30
|
+
/** The fields that change what a bot is told: every card field except the
|
|
31
|
+
* working folder and the two toggles. */
|
|
32
|
+
const TEXT_FIELDS = ["name", "title", "description", "soul"];
|
|
28
33
|
export class ProfileRequestError extends Error {
|
|
29
34
|
status;
|
|
30
|
-
|
|
35
|
+
/** True when no retry of this card can ever succeed — the proposal went
|
|
36
|
+
* stale (revision mismatch, target or folder gone) and the card must
|
|
37
|
+
* settle as expired instead of staying actionable. */
|
|
38
|
+
terminal;
|
|
39
|
+
constructor(message, status = 400, options) {
|
|
31
40
|
super(message);
|
|
32
41
|
this.name = "ProfileRequestError";
|
|
33
42
|
this.status = status;
|
|
43
|
+
this.terminal = options?.terminal === true;
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
function reasonText(value) {
|
|
@@ -44,8 +54,9 @@ function reasonText(value) {
|
|
|
44
54
|
}
|
|
45
55
|
return redactSecretsInText(trimmed);
|
|
46
56
|
}
|
|
47
|
-
/** `parseBotProfilePatch` would silently accept a key like `
|
|
48
|
-
* valid for the broader bot patch, not for a profile request
|
|
57
|
+
/** `parseBotProfilePatch` would silently accept a key like `voice` —
|
|
58
|
+
* valid for the broader bot patch, not for a profile request (a voice pick
|
|
59
|
+
* needs the harness catalog rendered on the card). Reject keys
|
|
49
60
|
* outside the proposable fields ourselves first, for exact copy. */
|
|
50
61
|
function parseChanges(input) {
|
|
51
62
|
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
@@ -84,6 +95,11 @@ function parseChanges(input) {
|
|
|
84
95
|
if (field === "cwd")
|
|
85
96
|
continue;
|
|
86
97
|
const value = parsed.patch[field];
|
|
98
|
+
if (field === "notifications" || field === "speakReplies") {
|
|
99
|
+
if (value !== undefined)
|
|
100
|
+
changes[field] = value;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
87
103
|
// This payload is hidden under the card's visible fields, so the store's
|
|
88
104
|
// shallow card redaction cannot reach it. Scrub before it is persisted.
|
|
89
105
|
if (typeof value !== "string")
|
|
@@ -125,36 +141,27 @@ export function profileCardCopy(target, fullProfile, before, changes, reason) {
|
|
|
125
141
|
continue;
|
|
126
142
|
if (changes[field] === undefined)
|
|
127
143
|
continue;
|
|
144
|
+
if (field === "notifications" || field === "speakReplies") {
|
|
145
|
+
lines.push(`${LABELS[field]}: ${toggleText(before[field])} → ${toggleText(changes[field])}`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
128
148
|
lines.push(`${LABELS[field]}: "${before[field] ?? ""}" → "${changes[field]}"`);
|
|
129
149
|
}
|
|
130
150
|
if (changes.cwd !== undefined) {
|
|
131
151
|
lines.push(`Working folder: ${before.cwd || PRIVATE_WORKSPACE} → ${changes.cwd || PRIVATE_WORKSPACE}`);
|
|
132
152
|
}
|
|
133
153
|
if (changes.soul !== undefined) {
|
|
134
|
-
|
|
135
|
-
const afterSoul = changes.soul;
|
|
136
|
-
const bytesBefore = Buffer.byteLength(beforeSoul, "utf8");
|
|
137
|
-
const bytesAfter = Buffer.byteLength(afterSoul, "utf8");
|
|
138
|
-
lines.push(`SOUL.md (${bytesBefore} → ${bytesAfter} bytes):`);
|
|
139
|
-
const diff = lineDiff(beforeSoul, afterSoul);
|
|
140
|
-
if (diff.length > MAX_DIFF_LINES) {
|
|
141
|
-
// Truncating a diff can hide the very instructions being approved.
|
|
142
|
-
// The full replacement is already byte-bounded by profile validation
|
|
143
|
-
// and is readable in the same generic card on desktop and phones.
|
|
144
|
-
lines.push("Large change — complete proposed instructions (replaces the current SOUL.md):");
|
|
145
|
-
lines.push(afterSoul || "(empty)");
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
lines.push(...diff);
|
|
149
|
-
}
|
|
154
|
+
lines.push(...soulDiffLines(before.soul ?? "", changes.soul));
|
|
150
155
|
}
|
|
151
156
|
// The closing line says the consequence of exactly what is on the card:
|
|
152
|
-
// a folder change moves where the bot's tools read and write; the
|
|
153
|
-
// fields change what it is told
|
|
154
|
-
|
|
157
|
+
// a folder change moves where the bot's tools read and write; the text
|
|
158
|
+
// fields change what it is told; a card of only toggles, only a folder,
|
|
159
|
+
// or both carries no instruction change at all. Either way nothing runs
|
|
160
|
+
// on confirm.
|
|
161
|
+
const changesText = TEXT_FIELDS.some((field) => changes[field] !== undefined);
|
|
155
162
|
if (changes.cwd !== undefined)
|
|
156
163
|
lines.push(`${target.name}'s tools will read and write files in that folder.`);
|
|
157
|
-
lines.push(
|
|
164
|
+
lines.push(changesText ? `Changes what ${target.name} is told on every turn. Nothing runs.` : "Nothing runs.");
|
|
158
165
|
const detail = lines.join("\n");
|
|
159
166
|
const fields = PROFILE_REQUEST_FIELDS.filter((field) => changes[field] !== undefined);
|
|
160
167
|
const summary = `${title} · ${fields.join(", ")}`;
|
|
@@ -199,13 +206,20 @@ export class ProfileRequestService {
|
|
|
199
206
|
const before = {};
|
|
200
207
|
const finalChanges = {};
|
|
201
208
|
for (const field of PROFILE_REQUEST_FIELDS) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
+
if (field === "notifications" || field === "speakReplies") {
|
|
210
|
+
const value = changes[field];
|
|
211
|
+
if (value === undefined || value === snapshot[field])
|
|
212
|
+
continue;
|
|
213
|
+
before[field] = snapshot[field];
|
|
214
|
+
finalChanges[field] = value;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const value = changes[field];
|
|
218
|
+
if (value === undefined || value === snapshot[field])
|
|
219
|
+
continue;
|
|
220
|
+
before[field] = redactSecretsInText(snapshot[field]);
|
|
221
|
+
finalChanges[field] = value;
|
|
222
|
+
}
|
|
209
223
|
}
|
|
210
224
|
if (Object.keys(finalChanges).length === 0) {
|
|
211
225
|
throw new ProfileRequestError("Nothing would change");
|
|
@@ -290,20 +304,29 @@ export class ProfileRequestService {
|
|
|
290
304
|
throw new ProfileRequestError("This profile confirmation card is no longer available", 409);
|
|
291
305
|
return { claimed: true, state: "already_settled", behavior: "allow" };
|
|
292
306
|
}
|
|
307
|
+
// An expired card is settled terminal state, not a decision waiting on
|
|
308
|
+
// a slower click: the proposal it carried can never be confirmed as
|
|
309
|
+
// prepared, even when the condition that expired it reverses (the
|
|
310
|
+
// revision matches again, the folder comes back). The committed
|
|
311
|
+
// receipt above still recovers a write that already happened; nothing
|
|
312
|
+
// past this point can.
|
|
313
|
+
if (card.expired) {
|
|
314
|
+
return { claimed: true, state: "invalid", error: "This profile request expired before it was confirmed. Ask for a fresh proposal.", status: 409 };
|
|
315
|
+
}
|
|
293
316
|
if (args.behavior === "deny") {
|
|
294
317
|
this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
|
|
295
318
|
return { claimed: true, state: "denied" };
|
|
296
319
|
}
|
|
297
320
|
if (!target)
|
|
298
|
-
throw new ProfileRequestError(NO_SUCH_BOT, 404);
|
|
321
|
+
throw new ProfileRequestError(NO_SUCH_BOT, 404, { terminal: true });
|
|
299
322
|
const crossBot = payload.targetBotId !== payload.botId;
|
|
300
323
|
if (crossBot && this.validateTarget) {
|
|
301
324
|
const refusal = this.validateTarget(payload.botId, payload.targetBotId);
|
|
302
325
|
if (refusal)
|
|
303
|
-
throw new ProfileRequestError(refusal, 404);
|
|
326
|
+
throw new ProfileRequestError(refusal, 404, { terminal: true });
|
|
304
327
|
}
|
|
305
328
|
if (profileRevision(target) !== payload.expectedRevision) {
|
|
306
|
-
throw new ProfileRequestError(STALE, 409);
|
|
329
|
+
throw new ProfileRequestError(STALE, 409, { terminal: true });
|
|
307
330
|
}
|
|
308
331
|
const { cwd, ...rest } = payload.changes;
|
|
309
332
|
const validated = Object.keys(rest).length ? parseChanges(rest) : {};
|
|
@@ -314,7 +337,7 @@ export class ProfileRequestService {
|
|
|
314
337
|
// applicable as prepared.
|
|
315
338
|
const checked = validateBotCwd(cwd || null);
|
|
316
339
|
if (!checked.ok)
|
|
317
|
-
throw new ProfileRequestError(checked.error, 409);
|
|
340
|
+
throw new ProfileRequestError(checked.error, 409, { terminal: true });
|
|
318
341
|
patch.cwd = checked.cwd ?? undefined;
|
|
319
342
|
}
|
|
320
343
|
if (!this.store.patchBotProfile(target.id, patch))
|
|
@@ -333,12 +356,16 @@ export class ProfileRequestService {
|
|
|
333
356
|
const status = error instanceof ProfileRequestError ? error.status : 400;
|
|
334
357
|
const detail = error instanceof Error ? error.message : String(error);
|
|
335
358
|
const saved = this.store.bot(payload.targetBotId)?.lastProfileRequestId === payload.requestId;
|
|
359
|
+
// A terminal failure (stale revision, target gone) can never be
|
|
360
|
+
// confirmed as prepared: settle the card as expired with its options
|
|
361
|
+
// removed so it stops looking actionable, and say so in one line.
|
|
362
|
+
const expired = !saved && error instanceof ProfileRequestError && error.terminal;
|
|
336
363
|
const notice = card.dismissed && card.options.length === 0
|
|
337
364
|
? "Profile saved. Recording the operation receipt could not finish; the changes will not be applied again."
|
|
338
365
|
: "Profile saved. Confirm again to finish recording this decision; the changes will not be applied again.";
|
|
339
366
|
try {
|
|
340
367
|
this.store.patchMessage(args.threadId, message.id, {
|
|
341
|
-
card: { ...card, held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
|
|
368
|
+
card: { ...card, ...(expired ? { expired: true, options: [] } : {}), held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
|
|
342
369
|
});
|
|
343
370
|
}
|
|
344
371
|
catch { /* The durable profile receipt still permits a safe retry. */ }
|
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
// routine's expectedUpdatedAt (bots have no updatedAt of their own).
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
export function profileSnapshot(bot) {
|
|
7
|
-
return {
|
|
7
|
+
return {
|
|
8
|
+
name: bot.name,
|
|
9
|
+
title: bot.title,
|
|
10
|
+
description: bot.description,
|
|
11
|
+
soul: bot.soul ?? "",
|
|
12
|
+
cwd: bot.cwd ?? "",
|
|
13
|
+
notifications: bot.notifications ?? true,
|
|
14
|
+
speakReplies: bot.speakReplies ?? false,
|
|
15
|
+
};
|
|
8
16
|
}
|
|
9
17
|
export function profileRevision(bot) {
|
|
10
18
|
// A later proposal can restore identical text. Keep its private receipt
|
|
@@ -60,11 +60,14 @@ async function writeRows(botId, rows) {
|
|
|
60
60
|
export function recordProfileChange(botId, actor, via, before, after) {
|
|
61
61
|
const at = Date.now();
|
|
62
62
|
const rows = [];
|
|
63
|
+
// Toggles land in history as the same on/off wording the card shows;
|
|
64
|
+
// every other proposable field is text.
|
|
65
|
+
const historyText = (value) => value === undefined ? "" : typeof value === "boolean" ? (value ? "on" : "off") : value;
|
|
63
66
|
for (const field of PROFILE_REQUEST_FIELDS) {
|
|
64
|
-
const b = before[field]
|
|
65
|
-
const a = after[field]
|
|
67
|
+
const b = before[field];
|
|
68
|
+
const a = after[field];
|
|
66
69
|
if (b !== a)
|
|
67
|
-
rows.push(rowFor(field, at, actor, via, b, a));
|
|
70
|
+
rows.push(rowFor(field, at, actor, via, historyText(b), historyText(a)));
|
|
68
71
|
}
|
|
69
72
|
if (!rows.length)
|
|
70
73
|
return;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai"];
|
|
1
|
+
export const PROVIDER_KEY_KINDS = ["anthropic", "openaiCompat", "xai", "mistral"];
|
|
2
2
|
const DEFAULT_URLS = {
|
|
3
3
|
anthropic: "https://api.anthropic.com",
|
|
4
4
|
openaiCompat: "https://openrouter.ai/api/v1",
|
|
5
5
|
xai: "https://api.x.ai/v1",
|
|
6
|
+
mistral: "https://api.mistral.ai/v1",
|
|
6
7
|
};
|
|
7
8
|
const MAX_MODELS = 5;
|
|
8
9
|
function isLoopback(hostname) {
|
|
@@ -18,7 +19,7 @@ export function providerModelsUrl(provider, base) {
|
|
|
18
19
|
}
|
|
19
20
|
function modelIds(body) {
|
|
20
21
|
const record = body && typeof body === "object" ? body : null;
|
|
21
|
-
const list = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
|
|
22
|
+
const list = Array.isArray(body) ? body : Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
|
|
22
23
|
const ids = [];
|
|
23
24
|
for (const entry of list) {
|
|
24
25
|
const item = entry && typeof entry === "object" ? entry : null;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// What this installation has shared, per team: the package id and the
|
|
2
|
+
// release last exported, plus the key each bot, group chat and routine was
|
|
3
|
+
// published under.
|
|
4
|
+
//
|
|
5
|
+
// Keys are a package's identity across releases (Admin refuses a release
|
|
6
|
+
// that silently drops one, and later updates match on them). They must not
|
|
7
|
+
// come from names once they exist: renaming a bot, a room, a routine or the
|
|
8
|
+
// team would otherwise read as "everything removed, everything added". So
|
|
9
|
+
// the first export records them here, keyed by record id, and every later
|
|
10
|
+
// export of the same team reuses them. Teams are name strings in OMB, so the
|
|
11
|
+
// team entry is renamed with the section (section-context.ts calls
|
|
12
|
+
// renamePublishedTeam from the one function every rename goes through).
|
|
13
|
+
//
|
|
14
|
+
// Server-private: nothing here is on the wire, and nothing here is a secret.
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
19
|
+
import { DATA_DIR } from "./config.js";
|
|
20
|
+
export const PUBLISHED_TEAMS_FILE = join(DATA_DIR, "published-teams.json");
|
|
21
|
+
const keyMap = z.record(z.string(), z.string());
|
|
22
|
+
const publishedTeamSchema = z.object({
|
|
23
|
+
packageId: z.string().min(1),
|
|
24
|
+
lastRelease: z.string().min(1),
|
|
25
|
+
keys: z.object({ bots: keyMap, rooms: keyMap, routines: keyMap }).default({ bots: {}, rooms: {}, routines: {} }),
|
|
26
|
+
});
|
|
27
|
+
const fileSchema = z.object({ version: z.literal(1), teams: z.record(z.string(), z.unknown()) });
|
|
28
|
+
function load() {
|
|
29
|
+
const teams = new Map();
|
|
30
|
+
if (!existsSync(PUBLISHED_TEAMS_FILE))
|
|
31
|
+
return teams;
|
|
32
|
+
try {
|
|
33
|
+
const file = fileSchema.parse(JSON.parse(readFileSync(PUBLISHED_TEAMS_FILE, "utf8")));
|
|
34
|
+
// Own entries only: a team may be called anything, including __proto__.
|
|
35
|
+
for (const [section, value] of Object.entries(file.teams)) {
|
|
36
|
+
const entry = publishedTeamSchema.safeParse(value);
|
|
37
|
+
if (entry.success)
|
|
38
|
+
teams.set(section, entry.data);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// An unreadable record only costs key stability on the next export,
|
|
43
|
+
// which then derives keys from names as a first export does.
|
|
44
|
+
}
|
|
45
|
+
return teams;
|
|
46
|
+
}
|
|
47
|
+
function save(teams) {
|
|
48
|
+
mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
|
|
49
|
+
const out = Object.create(null);
|
|
50
|
+
for (const [section, entry] of teams)
|
|
51
|
+
out[section] = entry;
|
|
52
|
+
writeFileAtomic(PUBLISHED_TEAMS_FILE, JSON.stringify({ version: 1, teams: out }, null, 2), { mode: 0o600 });
|
|
53
|
+
}
|
|
54
|
+
export function readPublishedTeam(section) {
|
|
55
|
+
return load().get(section) ?? null;
|
|
56
|
+
}
|
|
57
|
+
export function writePublishedTeam(section, entry) {
|
|
58
|
+
const teams = load();
|
|
59
|
+
teams.set(section, entry);
|
|
60
|
+
save(teams);
|
|
61
|
+
}
|
|
62
|
+
/** Follow a team rename; null forgets the team (it was deleted). */
|
|
63
|
+
export function renamePublishedTeam(name, nextName) {
|
|
64
|
+
const teams = load();
|
|
65
|
+
const entry = teams.get(name);
|
|
66
|
+
if (!entry || name === nextName)
|
|
67
|
+
return;
|
|
68
|
+
teams.delete(name);
|
|
69
|
+
if (nextName !== null)
|
|
70
|
+
teams.set(nextName, entry);
|
|
71
|
+
save(teams);
|
|
72
|
+
}
|
|
73
|
+
/** "1.2.3" → "1.2.4"; nothing published yet (or unreadable) starts at 1.0.0. */
|
|
74
|
+
export function nextPatchRelease(release) {
|
|
75
|
+
const match = release?.match(/^(\d{1,6})\.(\d{1,6})\.(\d{1,6})$/);
|
|
76
|
+
if (!match)
|
|
77
|
+
return "1.0.0";
|
|
78
|
+
const [major, minor, patch] = match.slice(1).map(Number);
|
|
79
|
+
return patch < 999_999 ? `${major}.${minor}.${patch + 1}` : `${major}.${minor + 1}.0`;
|
|
80
|
+
}
|
|
@@ -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,6 +247,14 @@ 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")
|
|
@@ -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
|
+
}
|
|
@@ -211,10 +211,15 @@ const routineRequestCardDataSchema = z.object({
|
|
|
211
211
|
}).strict();
|
|
212
212
|
export class RoutineRequestError extends Error {
|
|
213
213
|
status;
|
|
214
|
-
|
|
214
|
+
/** True when no retry of this card can ever succeed — the routine moved
|
|
215
|
+
* under the proposal — so the card must settle as expired rather than
|
|
216
|
+
* staying actionable. */
|
|
217
|
+
terminal;
|
|
218
|
+
constructor(message, status = 400, options) {
|
|
215
219
|
super(message);
|
|
216
220
|
this.name = "RoutineRequestError";
|
|
217
221
|
this.status = status;
|
|
222
|
+
this.terminal = options?.terminal === true;
|
|
218
223
|
}
|
|
219
224
|
}
|
|
220
225
|
function text(value, field, max) {
|
|
@@ -786,9 +791,9 @@ export function routineRequestFingerprint(payload, messageId) {
|
|
|
786
791
|
function verifyManageSnapshot(operation, manager, botId) {
|
|
787
792
|
const current = ownedRoutine(manager, operation.routineId, botId);
|
|
788
793
|
if (!current)
|
|
789
|
-
throw new RoutineRequestError("That routine no longer exists", 404);
|
|
794
|
+
throw new RoutineRequestError("That routine no longer exists", 404, { terminal: true });
|
|
790
795
|
if (current.updatedAt !== operation.expectedUpdatedAt) {
|
|
791
|
-
throw new RoutineRequestError("That routine changed after this confirmation card was prepared. Ask the bot to review it and propose the action again.", 409);
|
|
796
|
+
throw new RoutineRequestError("That routine changed after this confirmation card was prepared. Ask the bot to review it and propose the action again.", 409, { terminal: true });
|
|
792
797
|
}
|
|
793
798
|
return current;
|
|
794
799
|
}
|
|
@@ -828,7 +833,7 @@ function revalidateOperation(operation, manager, botId, now) {
|
|
|
828
833
|
if (duplicate) {
|
|
829
834
|
// The tool cannot choose a result destination. Return the existing ID,
|
|
830
835
|
// without moving its reports or treating a renamed request as new work.
|
|
831
|
-
throw new RoutineRequestError(`An enabled routine with the same instructions and execution settings already exists (${duplicate.id}). Use list_routines to review it, then update or run that routine instead.`, 409);
|
|
836
|
+
throw new RoutineRequestError(`An enabled routine with the same instructions and execution settings already exists (${duplicate.id}). Use list_routines to review it, then update or run that routine instead.`, 409, { terminal: true });
|
|
832
837
|
}
|
|
833
838
|
}
|
|
834
839
|
const current = operation.action === "create"
|
|
@@ -848,7 +853,7 @@ function revalidateOperation(operation, manager, botId, now) {
|
|
|
848
853
|
}
|
|
849
854
|
}
|
|
850
855
|
if (schedule?.type === "once" && schedule.at <= now) {
|
|
851
|
-
throw new RoutineRequestError("That one-time schedule is now in the past. Ask the bot to propose a new time.", 409);
|
|
856
|
+
throw new RoutineRequestError("That one-time schedule is now in the past. Ask the bot to propose a new time.", 409, { terminal: true });
|
|
852
857
|
}
|
|
853
858
|
if (schedule?.type === "interval") {
|
|
854
859
|
const base = operation.action === "create"
|
|
@@ -923,7 +928,7 @@ export class RoutineRequestService {
|
|
|
923
928
|
if (refusal)
|
|
924
929
|
throw new RoutineRequestError(refusal, 403);
|
|
925
930
|
}
|
|
926
|
-
await this.requireCloudReadiness(operation);
|
|
931
|
+
await this.requireCloudReadiness(operation, botId);
|
|
927
932
|
// The readiness probe is asynchronous. Another request can edit or
|
|
928
933
|
// delete the routine while it is in flight, so re-check the captured
|
|
929
934
|
// revision before rendering and persisting the confirmation snapshot.
|
|
@@ -1006,7 +1011,7 @@ export class RoutineRequestService {
|
|
|
1006
1011
|
}
|
|
1007
1012
|
throw new RoutineRequestError(result.state === "invalid" ? result.error : "The routine change could not be applied", result.state === "invalid" ? result.status : 409);
|
|
1008
1013
|
}
|
|
1009
|
-
async requireCloudReadiness(operation) {
|
|
1014
|
+
async requireCloudReadiness(operation, proposerBotId) {
|
|
1010
1015
|
if (!this.cloudReady || operation.action === "pause" || operation.action === "delete")
|
|
1011
1016
|
return;
|
|
1012
1017
|
const definition = effectiveDefinition(operation, this.routines);
|
|
@@ -1014,7 +1019,10 @@ export class RoutineRequestService {
|
|
|
1014
1019
|
return;
|
|
1015
1020
|
let readiness;
|
|
1016
1021
|
try {
|
|
1017
|
-
|
|
1022
|
+
const targetBotId = operation.action === "create"
|
|
1023
|
+
? operation.forBot?.botId ?? proposerBotId
|
|
1024
|
+
: this.routines.listRoutines().find(routine => routine.id === operation.routineId)?.botId ?? proposerBotId;
|
|
1025
|
+
readiness = await this.cloudReady(targetBotId);
|
|
1018
1026
|
}
|
|
1019
1027
|
catch (error) {
|
|
1020
1028
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -1104,6 +1112,19 @@ export class RoutineRequestService {
|
|
|
1104
1112
|
}
|
|
1105
1113
|
return this.settleApplied(args.threadId, message.id, card, payload, receipt.resultId, receipt.appliedAt);
|
|
1106
1114
|
}
|
|
1115
|
+
// An expired card is settled terminal state, not a decision waiting on
|
|
1116
|
+
// a slower click: the proposal it carried can never be confirmed as
|
|
1117
|
+
// prepared, even when the routine moves back under it. The committed
|
|
1118
|
+
// receipt above still recovers a write that already happened; nothing
|
|
1119
|
+
// past this point can.
|
|
1120
|
+
if (card.expired) {
|
|
1121
|
+
return {
|
|
1122
|
+
claimed: true,
|
|
1123
|
+
state: "invalid",
|
|
1124
|
+
error: "This routine request expired before it was confirmed. Ask for a fresh proposal.",
|
|
1125
|
+
status: 409,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1107
1128
|
if (args.behavior === "deny") {
|
|
1108
1129
|
this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
|
|
1109
1130
|
return { claimed: true, state: "denied" };
|
|
@@ -1112,7 +1133,7 @@ export class RoutineRequestService {
|
|
|
1112
1133
|
if (payload.operation.action === "create" && payload.operation.forBot && this.validateTarget) {
|
|
1113
1134
|
const refusal = this.validateTarget(payload.botId, payload.operation.forBot);
|
|
1114
1135
|
if (refusal)
|
|
1115
|
-
throw new RoutineRequestError(refusal, 404);
|
|
1136
|
+
throw new RoutineRequestError(refusal, 404, { terminal: true });
|
|
1116
1137
|
}
|
|
1117
1138
|
const resultId = this.apply(payload, message.id, fingerprint);
|
|
1118
1139
|
return this.settleApplied(args.threadId, message.id, card, payload, resultId);
|
|
@@ -1120,8 +1141,12 @@ export class RoutineRequestService {
|
|
|
1120
1141
|
catch (error) {
|
|
1121
1142
|
const status = error instanceof RoutineRequestError ? error.status : 400;
|
|
1122
1143
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1144
|
+
// A terminal failure (the routine moved under the proposal) can never
|
|
1145
|
+
// be confirmed as prepared: settle the card as expired with its
|
|
1146
|
+
// options removed so it stops looking actionable.
|
|
1147
|
+
const expired = error instanceof RoutineRequestError && error.terminal;
|
|
1123
1148
|
this.store.patchMessage(args.threadId, message.id, {
|
|
1124
|
-
card: { ...card, held: redactSecretsInText(detail).slice(0, 500) },
|
|
1149
|
+
card: { ...card, ...(expired ? { expired: true, options: [] } : {}), held: redactSecretsInText(detail).slice(0, 500) },
|
|
1125
1150
|
});
|
|
1126
1151
|
return {
|
|
1127
1152
|
claimed: true,
|
|
@@ -7,7 +7,24 @@ import { writeFileAtomic } from "./atomic.js";
|
|
|
7
7
|
import { redactSecretsInText } from "./redact.js";
|
|
8
8
|
import { normalizeCronSchedule, nextCronRuns } from "../shared/routine-schedule.js";
|
|
9
9
|
import { isRoutineProblemRun } from "../shared/routines.js";
|
|
10
|
+
import { ROUTINE_PARTS } from "./package-parts.js";
|
|
10
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
|
+
}
|
|
11
28
|
const webhookRunReceiptSchema = z.object({
|
|
12
29
|
webhookId: z.string().min(1).max(200),
|
|
13
30
|
deliveryId: z.string().min(1).max(200),
|
|
@@ -490,9 +507,12 @@ export class RoutineManager {
|
|
|
490
507
|
overlap: routine.overlap === "queue" ? "queue" : undefined,
|
|
491
508
|
skippedRuns: Number.isSafeInteger(routine.skippedRuns) && routine.skippedRuns > 0 ? routine.skippedRuns : undefined,
|
|
492
509
|
lastSkippedAt: Number.isSafeInteger(routine.lastSkippedAt) && routine.lastSkippedAt >= 0 && routine.lastSkippedAt <= MAX_DATE_MS ? routine.lastSkippedAt : undefined,
|
|
510
|
+
installedPackage: loadInstalledPackage(routine.installedPackage),
|
|
493
511
|
};
|
|
494
512
|
if (loaded.timeoutMinutes === undefined)
|
|
495
513
|
delete loaded.timeoutMinutes;
|
|
514
|
+
if (loaded.installedPackage === undefined)
|
|
515
|
+
delete loaded.installedPackage;
|
|
496
516
|
delete loaded.failureStreak;
|
|
497
517
|
return [loaded];
|
|
498
518
|
})
|
|
@@ -582,7 +602,27 @@ export class RoutineManager {
|
|
|
582
602
|
.sort((a, b) => (b.finishedAt ?? b.createdAt) - (a.finishedAt ?? a.createdAt) || b.createdAt - a.createdAt);
|
|
583
603
|
const success = outcomes.findIndex(run => run.status === "completed");
|
|
584
604
|
const failures = success < 0 ? outcomes.length : success;
|
|
585
|
-
|
|
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;
|
|
586
626
|
}
|
|
587
627
|
/** Whether this computer should stay awake for routines: a run is in
|
|
588
628
|
* flight, or an enabled routine is due within the horizon. The schedule
|
|
@@ -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)];
|