openmausbot 0.1.87 → 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 +2 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-BTiuL79F.js → abnfDiagram-O67JEVCF-qd9VD3GV.js} +1 -1
- package/dist/assets/{arc-C88Blc2v.js → arc-3RwkUjRe.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-BZsJIZrb.js → architectureDiagram-NJMV4G6O-CFKSJS7a.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CVeMe9Ah.js → blockDiagram-BEXU5L5S-CG1rEfWD.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-Cedk3Bhx.js → c4Diagram-YGBWAQC7-CBW9N664.js} +1 -1
- package/dist/assets/channel-CnRnO3tF.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-D-VRqzTB.js → chunk-2Q5K7J3B-2mRfTksB.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-BChov01Y.js → chunk-3FUC2YCW-Cv1dpErj.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-C-TfwvHy.js → chunk-5VM5RSS4-B7IACJp-.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Db7CGj5Y.js → chunk-GTNCS2PH-An0hZtYa.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CNvIfqKT.js → chunk-GWA4HPMP-BcJB2nEm.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-C_bEp5tq.js → chunk-JWPE2WC7-CV_W7TN7.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Coi4BOls.js → chunk-NETBCI7D-Bcs_kwnA.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-CzylEuL3.js → chunk-WEXAMYUT-lrx_DxFP.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-m7p7ztaD.js → chunk-XXDRQBXY-DabHlLGM.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-BMy6o9x2.js → classDiagram-v2-NBCMYWYE-BnHj6P7d.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BDQ__0_J.js → cose-bilkent-JH36ORCC-CfLp6UDi.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-Djv4ghUb.js → cynefin-OW5HDTMX-Dm4dbVA1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BULrlGYH.js → cynefinDiagram-VND7K2PF-B9mH-PID.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-D-jrT_hU.js → dagre-6A5THRUB-DDc_NVQJ.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-p2UhQmfL.js → diagram-22UHCM2B-lwr8Ip8-.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-DHvGWZ7q.js → diagram-3UASUU5V-BLLe6dmI.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-ChhKDot-.js → diagram-ATOU4E4O-6eUIq4lz.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-B-T7A43R.js → diagram-CDSNMT55-O-pRN7VQ.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CJm8ENNY.js → diagram-MLGK6HIB-BietvGAI.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-510tlza8.js → diagram-MPIPVDR6-Bd2Cl62X.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-PkvRPHel.js → ebnfDiagram-ZINNZB2B-B6iDy69W.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BVBVPoX3.js → elk-276RUBZZ-DQVHfKe0.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BgYMVm7L.js → erDiagram-OPXOYQCR-BWdVp2tQ.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-BGqsnc_3.js → flowDiagram-KWPJA3E3-WLE6rKB4.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-D_-UeReH.js → ganttDiagram-FUAMR5RP-BbjS-4gn.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-D0uooONx.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-zRsJERGJ.js → index-CIaHn5NS.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-B9e1z77f.js → infoDiagram-VRGFBTTK-D_jKWlKe.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BB4vNwvD.js → ishikawaDiagram-OU5B5YK6-DLJohSuK.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DpkosOTB.js → journeyDiagram-ZHPQQLJL-DPOFj0qN.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DbDP4kUu.js → kanban-definition-PNTS6WVX-BI7v-xpK.js} +1 -1
- package/dist/assets/{linear-DBJGPmjT.js → linear-C2pWPSxO.js} +1 -1
- package/dist/assets/{mermaid.core-DWPRi-ue.js → mermaid.core-CR-j5WQU.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-nszBbJmx.js → mindmap-definition-NLK3R4M7-lz9GvXRh.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CWykJWu_.js → pegDiagram-GJSIUBJH-Dl4_ia61.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-g3Da-Vnv.js → pieDiagram-5QR66LMP-DWuT3n88.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Wzrlpzgh.js → quadrantDiagram-O4NWA36T-DXWe9Jx7.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-moHTFMC0.js → railroadDiagram-XR7U4H2S-COY1IBVY.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-CwfXdFH8.js → requirementDiagram-PLB6GJNP-DnyVuhsQ.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-3ZROYvnq.js → sankeyDiagram-IPEJSGJF-BSc4NdGK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-E1TBebP8.js → sequenceDiagram-PO4LG4MO-kqEU2EGq.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-DHKEU5pu.js → sizeCapture-INFHLROL-BvB4bkWD.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-BguEj09D.js → stateDiagram-v2-GCMORJYK-BIrqNv7y.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-C7IDYnhd.js → swimlanes-2SLR337P-CFnalQUE.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C4a8qNGj.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DCW-F5PW.js → timeline-definition-EJHVYXUP-CsSWJnSQ.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-xBMFtWYV.js → usecaseDiagram-POWQR4AR-DkAmJ1EE.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-B5nIkuH8.js → vennDiagram-UO4OBE2U-C3vd9drL.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DYFAhdbr.js → wardleyDiagram-VNRHLVJA-j26CM-pi.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DeXwX6Cz.js → xychartDiagram-PMCCYNJV-BYu1Q_at.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/companion/src/routes.js +8 -1
- package/dist-server/connector-proxy.js +105 -1
- package/dist-server/container-mcp.js +3 -1
- package/dist-server/drivers/agents-proxy.js +277 -22
- package/dist-server/index.js +23150 -19947
- package/dist-server/local-computer-proxy.js +2 -0
- 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 +540 -298
- package/dist-server/pair-cli.js +540 -298
- package/dist-server/server/admission.js +81 -0
- package/dist-server/server/aside-queue.js +294 -0
- package/dist-server/server/attachments.js +43 -1
- package/dist-server/server/bot-attachment.js +107 -0
- package/dist-server/server/bot-overview.js +19 -0
- package/dist-server/server/cli-api-setup.js +1 -0
- package/dist-server/server/composio.js +297 -1
- package/dist-server/server/config.js +25 -3
- 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 +31 -4
- package/dist-server/server/container-computer.js +60 -0
- package/dist-server/server/contracts.js +22 -0
- package/dist-server/server/drivers/acp/core.js +24 -6
- 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 +137 -10
- package/dist-server/server/drivers/agents-catalog.js +100 -6
- package/dist-server/server/drivers/boxagent.js +2 -16
- package/dist-server/server/drivers/claude.js +31 -17
- 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-release.js +48 -0
- package/dist-server/server/drivers/codex.js +7 -25
- package/dist-server/server/drivers/openai-chat.js +26 -2
- package/dist-server/server/drivers/pi.js +237 -30
- package/dist-server/server/drivers/prompt-split.js +32 -9
- package/dist-server/server/drivers/session-idle.js +29 -0
- package/dist-server/server/env-path.js +15 -0
- package/dist-server/server/guarded-requests.js +2 -2
- package/dist-server/server/harness-capabilities.js +46 -0
- package/dist-server/server/index.js +1037 -209
- 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/options-card.js +21 -0
- package/dist-server/server/org-library.js +15 -1
- package/dist-server/server/peer-delivery.js +60 -0
- package/dist-server/server/peer-roster.js +57 -0
- package/dist-server/server/presets.js +32 -32
- 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/routine-requests.js +29 -7
- package/dist-server/server/skill-fetch.js +72 -14
- package/dist-server/server/skills.js +1 -0
- package/dist-server/server/store.js +32 -2
- package/dist-server/server/system-prompt.js +26 -2
- package/dist-server/server/team-setup-requests.js +124 -23
- package/dist-server/server/thread-events.js +3 -1
- package/dist-server/server/tightening-requests.js +405 -0
- package/dist-server/server/tts/chatterbox.js +27 -5
- package/dist-server/server/usage-ledger.js +7 -1
- package/dist-server/server/workspace-backup.js +40 -3
- package/dist-server/server/workspace-backup.worker.js +10 -0
- package/dist-server/shared/ask-question.js +19 -0
- package/dist-server/shared/credential-request.js +5 -1
- 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/profile-request.js +5 -4
- package/dist-server/shared/tightening-request.js +87 -0
- package/dist-server/src/lib/composer-attachments.js +25 -3
- package/dist-server/vps-container-mcp.js +3 -1
- package/dist-server/workspace-backup.worker.js +19474 -0
- package/package.json +1 -1
- package/dist/assets/channel-taCNRlUU.js +0 -1
- package/dist/assets/index-BwdMAQ6p.css +0 -1
- package/dist/assets/index-ByJennr7.js +0 -581
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BpEdoB2e.js +0 -8
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
// propose_tightening: a bot (or a Chief for a section peer) proposes an
|
|
2
|
+
// authority REDUCTION through a human-confirmed card. Same shape as
|
|
3
|
+
// profile-requests.ts: everything is re-validated at confirm time — a card
|
|
4
|
+
// can sit open for days, so the direction is checked against live state
|
|
5
|
+
// again and the revision (a hash of the touched authority surface) must
|
|
6
|
+
// still match. Escalation is inexpressible in the payload and rejected by
|
|
7
|
+
// the validator; restoring authority stays owner-only in Edit Profile.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { approvalModeFor } from "../shared/approval-mode.js";
|
|
11
|
+
import { TIGHTENING_MODE_TARGETS, validateTightening, } from "../shared/tightening-request.js";
|
|
12
|
+
import { newId } from "./contracts.js";
|
|
13
|
+
import { redactSecretsInText } from "./redact.js";
|
|
14
|
+
const MAX_REASON = 500;
|
|
15
|
+
const STALE = "This bot's authority changed after this card was prepared. Ask the bot to review it and propose again.";
|
|
16
|
+
const NO_SUCH_BOT = "That bot no longer exists";
|
|
17
|
+
const CHOOSE_ONE = "Choose at least one tightening intent: approvalMode, composio, browser, approvePeerComms, alwaysAllow, mcpServers, skills";
|
|
18
|
+
const INTENT_KEYS = [
|
|
19
|
+
"approvalMode",
|
|
20
|
+
"composio",
|
|
21
|
+
"browser",
|
|
22
|
+
"approvePeerComms",
|
|
23
|
+
"alwaysAllow",
|
|
24
|
+
"mcpServers",
|
|
25
|
+
"skills",
|
|
26
|
+
];
|
|
27
|
+
/** Direction is enforced by shape first: the payload cannot name "full" or
|
|
28
|
+
* "custom" as a target, cannot ask to switch composio/browser back on, and
|
|
29
|
+
* the list fields only carry removals. The state-relative remainder (an
|
|
30
|
+
* upward move inside ask/edits/auto, a grant not currently held) is checked
|
|
31
|
+
* by validateTightening against live state at propose and confirm. */
|
|
32
|
+
const intentsSchema = z.object({
|
|
33
|
+
approvalMode: z.enum(TIGHTENING_MODE_TARGETS, {
|
|
34
|
+
error: 'approvalMode may only be proposed as "ask", "edits", or "auto"; full and custom stay owner-only',
|
|
35
|
+
}).optional(),
|
|
36
|
+
composio: z.literal(false, { error: "composio may only be turned off" }).optional(),
|
|
37
|
+
browser: z.literal(false, { error: "browser may only be turned off" }).optional(),
|
|
38
|
+
approvePeerComms: z.literal(true, { error: "approvePeerComms may only be turned on" }).optional(),
|
|
39
|
+
alwaysAllow: z.array(z.string().min(1).max(200)).max(200).optional(),
|
|
40
|
+
mcpServers: z.array(z.string().min(1).max(200)).max(100).optional(),
|
|
41
|
+
skills: z.array(z.string().min(1).max(100)).max(100).optional(),
|
|
42
|
+
});
|
|
43
|
+
export class TighteningRequestError extends Error {
|
|
44
|
+
status;
|
|
45
|
+
constructor(message, status = 400) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "TighteningRequestError";
|
|
48
|
+
this.status = status;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function reasonText(value) {
|
|
52
|
+
if (typeof value !== "string")
|
|
53
|
+
throw new TighteningRequestError("reason is required");
|
|
54
|
+
const trimmed = value.trim();
|
|
55
|
+
if (!trimmed)
|
|
56
|
+
throw new TighteningRequestError("reason is required");
|
|
57
|
+
if (trimmed.length > MAX_REASON) {
|
|
58
|
+
throw new TighteningRequestError(`reason must be ${MAX_REASON} characters or fewer`);
|
|
59
|
+
}
|
|
60
|
+
return redactSecretsInText(trimmed);
|
|
61
|
+
}
|
|
62
|
+
function parseIntents(input) {
|
|
63
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
64
|
+
throw new TighteningRequestError(CHOOSE_ONE);
|
|
65
|
+
}
|
|
66
|
+
const record = input;
|
|
67
|
+
for (const key of Object.keys(record)) {
|
|
68
|
+
if (!INTENT_KEYS.includes(key)) {
|
|
69
|
+
throw new TighteningRequestError(`unsupported tightening field: ${key}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const parsed = intentsSchema.safeParse(input);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
throw new TighteningRequestError(parsed.error.issues[0]?.message ?? CHOOSE_ONE);
|
|
75
|
+
}
|
|
76
|
+
const intents = parsed.data;
|
|
77
|
+
// Copy through only the keys the caller sent, so "did this card touch
|
|
78
|
+
// approvalMode" stays answerable from the payload alone.
|
|
79
|
+
const present = {};
|
|
80
|
+
if (record.approvalMode !== undefined)
|
|
81
|
+
present.approvalMode = intents.approvalMode;
|
|
82
|
+
if (record.composio !== undefined)
|
|
83
|
+
present.composio = intents.composio;
|
|
84
|
+
if (record.browser !== undefined)
|
|
85
|
+
present.browser = intents.browser;
|
|
86
|
+
if (record.approvePeerComms !== undefined)
|
|
87
|
+
present.approvePeerComms = intents.approvePeerComms;
|
|
88
|
+
if (record.alwaysAllow !== undefined)
|
|
89
|
+
present.alwaysAllow = intents.alwaysAllow;
|
|
90
|
+
if (record.mcpServers !== undefined)
|
|
91
|
+
present.mcpServers = intents.mcpServers;
|
|
92
|
+
if (record.skills !== undefined)
|
|
93
|
+
present.skills = intents.skills;
|
|
94
|
+
if (Object.keys(present).length === 0)
|
|
95
|
+
throw new TighteningRequestError(CHOOSE_ONE);
|
|
96
|
+
return present;
|
|
97
|
+
}
|
|
98
|
+
/** The card revision: a hash of exactly the authority surface a tightening
|
|
99
|
+
* can touch, plus the private receipt (an older, interrupted card cannot
|
|
100
|
+
* apply twice). Sorted lists so an unrelated reorder is not a false stale. */
|
|
101
|
+
export function tighteningRevision(state, lastTighteningRequestId = "") {
|
|
102
|
+
return createHash("sha256")
|
|
103
|
+
.update(JSON.stringify({
|
|
104
|
+
approvalMode: state.approvalMode,
|
|
105
|
+
composio: state.composio,
|
|
106
|
+
browser: state.browser,
|
|
107
|
+
approvePeerComms: state.approvePeerComms,
|
|
108
|
+
alwaysAllow: [...state.alwaysAllow].sort(),
|
|
109
|
+
mcpServers: [...state.mcpServers].sort(),
|
|
110
|
+
skills: [...state.skills].sort(),
|
|
111
|
+
lastTighteningRequestId,
|
|
112
|
+
}), "utf8")
|
|
113
|
+
.digest("hex");
|
|
114
|
+
}
|
|
115
|
+
export function tighteningSnapshot(bot, extras) {
|
|
116
|
+
return {
|
|
117
|
+
approvalMode: approvalModeFor(bot),
|
|
118
|
+
composio: bot.composio !== false,
|
|
119
|
+
browser: bot.browser !== false,
|
|
120
|
+
approvePeerComms: bot.approvePeerComms === true,
|
|
121
|
+
alwaysAllow: bot.alwaysAllow ?? [],
|
|
122
|
+
mcpServers: extras.mcpServers ?? [...(bot.mcpServers ?? [])],
|
|
123
|
+
skills: extras.skills ?? [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Minimal card copy: the judged facts (field, before, after, names) plus
|
|
127
|
+
* the standing rule that only the owner can grant authority back. */
|
|
128
|
+
export function tighteningCardCopy(target, before, intents, reason) {
|
|
129
|
+
const title = target.crossBot ? `Tighten @${target.name}'s permissions?` : `Tighten ${target.name}'s permissions?`;
|
|
130
|
+
const lines = target.crossBot ? [`Whose permissions: @${target.name}`, `Why: ${reason}`] : [`Why: ${reason}`];
|
|
131
|
+
if (intents.approvalMode !== undefined)
|
|
132
|
+
lines.push(`Approvals: ${before.approvalMode} → ${intents.approvalMode}`);
|
|
133
|
+
if (intents.composio !== undefined)
|
|
134
|
+
lines.push("Connected apps: on → off");
|
|
135
|
+
if (intents.browser !== undefined)
|
|
136
|
+
lines.push("Browser: on → off");
|
|
137
|
+
if (intents.approvePeerComms !== undefined)
|
|
138
|
+
lines.push("Peer contact: no approval → ask first");
|
|
139
|
+
if (intents.alwaysAllow?.length)
|
|
140
|
+
lines.push(`Always-allow grants: remove ${intents.alwaysAllow.join(", ")}`);
|
|
141
|
+
if (intents.mcpServers?.length)
|
|
142
|
+
lines.push(`MCP servers: unmount ${intents.mcpServers.join(", ")}`);
|
|
143
|
+
if (intents.skills?.length)
|
|
144
|
+
lines.push(`Skills: disable ${intents.skills.join(", ")}`);
|
|
145
|
+
lines.push(`This reduces ${target.name}'s authority. Only the owner can grant it back.`);
|
|
146
|
+
const fields = INTENT_KEYS.filter((key) => intents[key] !== undefined);
|
|
147
|
+
return { title, summary: `${title} · ${fields.join(", ")}`, detail: lines.join("\n") };
|
|
148
|
+
}
|
|
149
|
+
export class TighteningRequestService {
|
|
150
|
+
store;
|
|
151
|
+
now;
|
|
152
|
+
canPersist;
|
|
153
|
+
autoApply;
|
|
154
|
+
mountedMcpServers;
|
|
155
|
+
enabledSkills;
|
|
156
|
+
disableSkill;
|
|
157
|
+
targetBusy;
|
|
158
|
+
/** Public: a caller's section membership can change between propose and
|
|
159
|
+
* confirm, and tests flip this mid-scenario to model that. */
|
|
160
|
+
validateTarget;
|
|
161
|
+
constructor(options) {
|
|
162
|
+
this.store = options.store;
|
|
163
|
+
this.now = options.now ?? Date.now;
|
|
164
|
+
this.canPersist = options.canPersist;
|
|
165
|
+
this.autoApply = options.autoApply;
|
|
166
|
+
this.mountedMcpServers = options.mountedMcpServers;
|
|
167
|
+
this.enabledSkills = options.enabledSkills;
|
|
168
|
+
this.disableSkill = options.disableSkill;
|
|
169
|
+
this.targetBusy = options.targetBusy;
|
|
170
|
+
this.validateTarget = options.validateTarget;
|
|
171
|
+
}
|
|
172
|
+
snapshotOf(bot) {
|
|
173
|
+
return tighteningSnapshot(bot, {
|
|
174
|
+
mcpServers: this.mountedMcpServers?.(bot),
|
|
175
|
+
skills: this.enabledSkills?.(bot.id) ?? [],
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Disable every named skill, or report the first failure together with
|
|
179
|
+
* the names still outstanding. Shared by the fresh apply and the
|
|
180
|
+
* receipt-backed retry so both leave the same trail. */
|
|
181
|
+
applySkillDisables(botId, names) {
|
|
182
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
183
|
+
const disabled = this.disableSkill(botId, names[index]);
|
|
184
|
+
if (!disabled.ok)
|
|
185
|
+
return { ok: false, error: disabled.error, outstanding: names.slice(index) };
|
|
186
|
+
}
|
|
187
|
+
return { ok: true };
|
|
188
|
+
}
|
|
189
|
+
propose(args) {
|
|
190
|
+
return this.prepare(args);
|
|
191
|
+
}
|
|
192
|
+
submit(args) {
|
|
193
|
+
const proposal = this.prepare(args, true);
|
|
194
|
+
return { ...proposal, state: proposal.result ? "applied" : "pending" };
|
|
195
|
+
}
|
|
196
|
+
prepare(args, submitted = false) {
|
|
197
|
+
const reason = reasonText(args.reason);
|
|
198
|
+
const intents = parseIntents(args.intents);
|
|
199
|
+
const targetBotId = args.targetBotId ?? args.botId;
|
|
200
|
+
const target = this.store.bot(targetBotId);
|
|
201
|
+
if (!target)
|
|
202
|
+
throw new TighteningRequestError(NO_SUCH_BOT, 404);
|
|
203
|
+
const crossBot = targetBotId !== args.botId;
|
|
204
|
+
if (crossBot && this.validateTarget) {
|
|
205
|
+
const refusal = this.validateTarget(args.botId, targetBotId);
|
|
206
|
+
if (refusal)
|
|
207
|
+
throw new TighteningRequestError(refusal, 403);
|
|
208
|
+
}
|
|
209
|
+
const before = this.snapshotOf(target);
|
|
210
|
+
const checked = validateTightening(before, intents);
|
|
211
|
+
if (!checked.ok)
|
|
212
|
+
throw new TighteningRequestError(checked.error, 400);
|
|
213
|
+
const requestId = newId();
|
|
214
|
+
const targetName = redactSecretsInText(target.name);
|
|
215
|
+
const payload = {
|
|
216
|
+
version: 1,
|
|
217
|
+
requestId,
|
|
218
|
+
botId: args.botId,
|
|
219
|
+
threadId: args.threadId,
|
|
220
|
+
targetBotId,
|
|
221
|
+
targetName,
|
|
222
|
+
createdAt: this.now(),
|
|
223
|
+
reason,
|
|
224
|
+
intents,
|
|
225
|
+
before,
|
|
226
|
+
expectedRevision: tighteningRevision(before, target.lastTighteningRequestId),
|
|
227
|
+
};
|
|
228
|
+
const copy = tighteningCardCopy({ name: targetName, crossBot }, before, intents, reason);
|
|
229
|
+
const persistence = this.canPersist?.(args.botId, args.threadId);
|
|
230
|
+
if (persistence && !persistence.ok) {
|
|
231
|
+
throw new TighteningRequestError(persistence.error, persistence.status);
|
|
232
|
+
}
|
|
233
|
+
const automatic = submitted && this.autoApply?.(args.botId, args.threadId) === true;
|
|
234
|
+
const messageInput = {
|
|
235
|
+
role: "bot",
|
|
236
|
+
kind: "options",
|
|
237
|
+
card: {
|
|
238
|
+
title: copy.title,
|
|
239
|
+
subtitle: copy.detail,
|
|
240
|
+
options: automatic ? [] : ["Confirm", "Cancel"],
|
|
241
|
+
...(automatic ? { dismissed: true } : {}),
|
|
242
|
+
requestId,
|
|
243
|
+
tool: "tighten_permissions",
|
|
244
|
+
tighteningRequest: payload,
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
if (args.from)
|
|
248
|
+
messageInput.from = args.from;
|
|
249
|
+
const message = this.store.appendMessage(args.threadId, messageInput);
|
|
250
|
+
const proposal = { requestId, messageId: message.id, title: copy.title, summary: copy.summary, detail: copy.detail };
|
|
251
|
+
if (!automatic)
|
|
252
|
+
return proposal;
|
|
253
|
+
// The hidden receipt is persisted before the authority changes. The same
|
|
254
|
+
// validation and durable commit marker serve both automatic and human decisions.
|
|
255
|
+
const result = this.resolve({ botId: args.botId, threadId: args.threadId, requestId, behavior: "allow" });
|
|
256
|
+
if (result.state === "applied")
|
|
257
|
+
return { ...proposal, result };
|
|
258
|
+
throw new TighteningRequestError(result.state === "invalid" ? result.error : "The tightening could not be applied", result.state === "invalid" ? result.status : 409);
|
|
259
|
+
}
|
|
260
|
+
/** Claims a tightening card even after it was settled, so a duplicate
|
|
261
|
+
* click never re-applies an already-applied reduction. */
|
|
262
|
+
resolve(args) {
|
|
263
|
+
const message = this.store
|
|
264
|
+
.messagesFor(args.threadId)
|
|
265
|
+
.find((candidate) => candidate.card?.requestId === args.requestId && candidate.card.tighteningRequest);
|
|
266
|
+
const card = message?.card;
|
|
267
|
+
const payload = card?.tighteningRequest;
|
|
268
|
+
if (!message || !card || !payload)
|
|
269
|
+
return { claimed: false, state: "not_found" };
|
|
270
|
+
if (payload.requestId !== card.requestId) {
|
|
271
|
+
return { claimed: true, state: "invalid", error: "This tightening request does not match its card", status: 409 };
|
|
272
|
+
}
|
|
273
|
+
if (args.behavior !== "allow" && args.behavior !== "deny") {
|
|
274
|
+
return { claimed: true, state: "invalid", error: "Tightening confirmations must be confirmed or cancelled", status: 400 };
|
|
275
|
+
}
|
|
276
|
+
if (payload.botId !== args.botId || payload.threadId !== args.threadId) {
|
|
277
|
+
return { claimed: true, state: "invalid", error: "This tightening request belongs to another conversation", status: 403 };
|
|
278
|
+
}
|
|
279
|
+
if (card.answered)
|
|
280
|
+
return { claimed: true, state: "already_settled", behavior: card.answered };
|
|
281
|
+
let emergency = false;
|
|
282
|
+
let outstandingSkills = [];
|
|
283
|
+
try {
|
|
284
|
+
const target = this.store.bot(payload.targetBotId);
|
|
285
|
+
// The authority change and receipt share one durable write. If saving
|
|
286
|
+
// the card failed afterward, a retry only settles it; it never
|
|
287
|
+
// reapplies authority — but it does finish any skills a failed
|
|
288
|
+
// attempt left enabled, so a partial disable can never strand the card.
|
|
289
|
+
if (target?.lastTighteningRequestId === payload.requestId) {
|
|
290
|
+
const enabled = new Set(this.enabledSkills?.(payload.targetBotId) ?? []);
|
|
291
|
+
const remaining = (payload.intents.skills ?? []).filter((name) => enabled.has(name));
|
|
292
|
+
if (remaining.length && !this.disableSkill) {
|
|
293
|
+
throw new TighteningRequestError("Skill changes are not available in this workspace", 400);
|
|
294
|
+
}
|
|
295
|
+
const finished = this.applySkillDisables(payload.targetBotId, remaining);
|
|
296
|
+
if (!finished.ok) {
|
|
297
|
+
outstandingSkills = finished.outstanding;
|
|
298
|
+
throw new TighteningRequestError(finished.error, 409);
|
|
299
|
+
}
|
|
300
|
+
const settled = this.store.patchMessage(args.threadId, message.id, {
|
|
301
|
+
card: { ...card, answered: "allow", held: undefined, tighteningRequest: { ...payload, appliedAt: payload.appliedAt ?? this.now() } },
|
|
302
|
+
});
|
|
303
|
+
if (!settled)
|
|
304
|
+
throw new TighteningRequestError("This tightening confirmation card is no longer available", 409);
|
|
305
|
+
return { claimed: true, state: "already_settled", behavior: "allow" };
|
|
306
|
+
}
|
|
307
|
+
if (args.behavior === "deny") {
|
|
308
|
+
this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
|
|
309
|
+
return { claimed: true, state: "denied" };
|
|
310
|
+
}
|
|
311
|
+
if (!target)
|
|
312
|
+
throw new TighteningRequestError(NO_SUCH_BOT, 404);
|
|
313
|
+
const crossBot = payload.targetBotId !== payload.botId;
|
|
314
|
+
if (crossBot && this.validateTarget) {
|
|
315
|
+
const refusal = this.validateTarget(payload.botId, payload.targetBotId);
|
|
316
|
+
if (refusal)
|
|
317
|
+
throw new TighteningRequestError(refusal, 404);
|
|
318
|
+
}
|
|
319
|
+
const current = this.snapshotOf(target);
|
|
320
|
+
if (tighteningRevision(current, target.lastTighteningRequestId) !== payload.expectedRevision) {
|
|
321
|
+
throw new TighteningRequestError(STALE, 409);
|
|
322
|
+
}
|
|
323
|
+
// Direction is re-validated against live state, not trusted from the
|
|
324
|
+
// card: a card that sat open can only ever apply a reduction.
|
|
325
|
+
const checked = validateTightening(current, payload.intents);
|
|
326
|
+
if (!checked.ok)
|
|
327
|
+
throw new TighteningRequestError(checked.error, 409);
|
|
328
|
+
emergency = payload.intents.approvalMode !== undefined &&
|
|
329
|
+
(current.approvalMode === "full" || current.approvalMode === "custom") &&
|
|
330
|
+
payload.intents.approvalMode === "ask";
|
|
331
|
+
if (this.targetBusy?.(target.id) && !emergency) {
|
|
332
|
+
if (payload.intents.approvalMode !== undefined) {
|
|
333
|
+
throw new TighteningRequestError("Stop this bot's turn before changing its approval level", 409);
|
|
334
|
+
}
|
|
335
|
+
if (payload.intents.mcpServers !== undefined) {
|
|
336
|
+
throw new TighteningRequestError("Stop this bot's turns before changing its MCP servers", 409);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// The capability check runs before any mutation: a workspace that
|
|
340
|
+
// cannot touch skills must not half-apply the card.
|
|
341
|
+
if (payload.intents.skills?.length && !this.disableSkill) {
|
|
342
|
+
throw new TighteningRequestError("Skill changes are not available in this workspace", 400);
|
|
343
|
+
}
|
|
344
|
+
const patch = { lastTighteningRequestId: payload.requestId };
|
|
345
|
+
if (payload.intents.approvalMode !== undefined) {
|
|
346
|
+
patch.approvalMode = payload.intents.approvalMode;
|
|
347
|
+
patch.autoApprove = payload.intents.approvalMode === "auto";
|
|
348
|
+
patch.approvalGrant = undefined;
|
|
349
|
+
}
|
|
350
|
+
if (payload.intents.composio !== undefined)
|
|
351
|
+
patch.composio = false;
|
|
352
|
+
if (payload.intents.browser !== undefined)
|
|
353
|
+
patch.browser = false;
|
|
354
|
+
if (payload.intents.approvePeerComms !== undefined)
|
|
355
|
+
patch.approvePeerComms = true;
|
|
356
|
+
if (payload.intents.alwaysAllow !== undefined)
|
|
357
|
+
patch.alwaysAllow = [...checked.after.alwaysAllow];
|
|
358
|
+
if (payload.intents.mcpServers !== undefined)
|
|
359
|
+
patch.mcpServers = [...checked.after.mcpServers];
|
|
360
|
+
if (!this.store.patchBot(target.id, patch))
|
|
361
|
+
throw new TighteningRequestError(NO_SUCH_BOT, 404);
|
|
362
|
+
// Skills disable AFTER the receipt write. Each disable persists on
|
|
363
|
+
// its own, so a failure midway can leave part of the list done — but
|
|
364
|
+
// the receipt is already durable, which turns every retry into the
|
|
365
|
+
// branch above: finish the remainder, never reapply authority, and
|
|
366
|
+
// never leave the card permanently stale.
|
|
367
|
+
const skills = this.applySkillDisables(target.id, payload.intents.skills ?? []);
|
|
368
|
+
if (!skills.ok) {
|
|
369
|
+
outstandingSkills = skills.outstanding;
|
|
370
|
+
throw new TighteningRequestError(skills.error, 409);
|
|
371
|
+
}
|
|
372
|
+
const appliedAt = this.now();
|
|
373
|
+
const settled = this.store.patchMessage(args.threadId, message.id, {
|
|
374
|
+
card: { ...card, answered: "allow", held: undefined, tighteningRequest: { ...payload, appliedAt } },
|
|
375
|
+
});
|
|
376
|
+
if (!settled)
|
|
377
|
+
throw new TighteningRequestError("This tightening confirmation card is no longer available", 409);
|
|
378
|
+
const fields = INTENT_KEYS.filter((key) => payload.intents[key] !== undefined);
|
|
379
|
+
return { claimed: true, state: "applied", targetBotId: target.id, fields, ...(emergency ? { emergencyStop: true } : {}) };
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const status = error instanceof TighteningRequestError ? error.status : 400;
|
|
383
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
384
|
+
const saved = this.store.bot(payload.targetBotId)?.lastTighteningRequestId === payload.requestId;
|
|
385
|
+
const notice = outstandingSkills.length
|
|
386
|
+
? `Permissions tightened. Confirm again to finish disabling: ${redactSecretsInText(outstandingSkills.join(", "))}`
|
|
387
|
+
: card.dismissed && card.options.length === 0
|
|
388
|
+
? "Permissions tightened. Recording the operation receipt could not finish; the changes will not be applied again."
|
|
389
|
+
: "Permissions tightened. Confirm again to finish recording this decision; the changes will not be applied again.";
|
|
390
|
+
try {
|
|
391
|
+
this.store.patchMessage(args.threadId, message.id, {
|
|
392
|
+
card: { ...card, held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
catch { /* The durable receipt still permits a safe retry. */ }
|
|
396
|
+
if (saved)
|
|
397
|
+
return {
|
|
398
|
+
claimed: true, state: "applied", targetBotId: payload.targetBotId,
|
|
399
|
+
fields: INTENT_KEYS.filter((key) => payload.intents[key] !== undefined),
|
|
400
|
+
...(emergency ? { emergencyStop: true } : {}),
|
|
401
|
+
};
|
|
402
|
+
return { claimed: true, state: "invalid", error: detail, status };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
@@ -44,11 +44,33 @@ async function request(url, init, what, baseUrl) {
|
|
|
44
44
|
}
|
|
45
45
|
export async function listChatterboxVoices(baseUrl) {
|
|
46
46
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
try {
|
|
48
|
+
const res = await request(`${apiRoot(baseUrl)}/models`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) }, "listing voices", baseUrl);
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const body = await safeJson(res);
|
|
51
|
+
const voices = (body?.data ?? [])
|
|
52
|
+
.map((m) => ({ id: String(m?.id ?? ""), label: String(m?.id ?? "") }))
|
|
53
|
+
.filter((v) => v.id);
|
|
54
|
+
if (voices.length)
|
|
55
|
+
return voices;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// /v1/models missing or unreachable — try /v1/audio/voices next
|
|
60
|
+
}
|
|
61
|
+
// Some servers (e.g. Chatterbox-TTS-Server) list voices here instead.
|
|
62
|
+
const voicesRes = await request(`${apiRoot(baseUrl)}/audio/voices`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) }, "listing voices", baseUrl);
|
|
63
|
+
if (voicesRes.ok) {
|
|
64
|
+
const body = await safeJson(voicesRes);
|
|
65
|
+
const voices = (body?.voices ?? [])
|
|
66
|
+
.map((entry) => {
|
|
67
|
+
// Chatterbox-TTS-Server currently returns filename strings; docs (and
|
|
68
|
+
// /get_predefined_voices) describe { filename, display_name } objects.
|
|
69
|
+
if (typeof entry === "string")
|
|
70
|
+
return { id: entry, label: entry };
|
|
71
|
+
const id = String(entry?.filename ?? entry?.id ?? "");
|
|
72
|
+
return { id, label: String(entry?.display_name ?? entry?.name ?? id) };
|
|
73
|
+
})
|
|
52
74
|
.filter((v) => v.id);
|
|
53
75
|
if (voices.length)
|
|
54
76
|
return voices;
|
|
@@ -11,7 +11,7 @@ import { readFileSync } from "node:fs";
|
|
|
11
11
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { billableFor } from "./prices.js";
|
|
14
|
-
export const USAGE_GROUPINGS = ["bot", "model", "user", "day", "engine"];
|
|
14
|
+
export const USAGE_GROUPINGS = ["bot", "model", "user", "day", "engine", "routine"];
|
|
15
15
|
const DIR = "usage";
|
|
16
16
|
const MAX_RANGE_DAYS = 366;
|
|
17
17
|
const DAY_MS = 24 * 60 * 60_000;
|
|
@@ -184,6 +184,12 @@ function groupOf(row, groupBy) {
|
|
|
184
184
|
return { key: triggerKey(row.trigger), label: triggerLabel(row.trigger) };
|
|
185
185
|
case "day":
|
|
186
186
|
return { key: `day:${row.at.slice(0, 10)}`, label: row.at.slice(0, 10) };
|
|
187
|
+
case "routine":
|
|
188
|
+
// Turns a routine started are grouped by routine; everything else
|
|
189
|
+
// (people, the owner, other bots) shares one "manual" bucket.
|
|
190
|
+
return row.trigger.kind === "routine"
|
|
191
|
+
? { key: triggerKey(row.trigger), label: row.trigger.label ?? row.trigger.routineId ?? "unknown" }
|
|
192
|
+
: { key: "manual", label: "Not from a routine" };
|
|
187
193
|
default:
|
|
188
194
|
return { key: `engine:${row.driverKind}`, label: row.driverKind };
|
|
189
195
|
}
|
|
@@ -6,6 +6,7 @@ import { isAbsolute, join, parse, posix, relative, resolve, win32 } from "node:p
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { backup, DatabaseSync } from "node:sqlite";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { Worker } from "node:worker_threads";
|
|
9
10
|
import * as tar from "tar";
|
|
10
11
|
import { fromMarkdown } from "mdast-util-from-markdown";
|
|
11
12
|
import { writeFileAtomic } from "./atomic.js";
|
|
@@ -77,8 +78,7 @@ function jobPath(dataDir, id) {
|
|
|
77
78
|
}
|
|
78
79
|
return path;
|
|
79
80
|
}
|
|
80
|
-
function newJob(dataDir) {
|
|
81
|
-
const id = randomUUID();
|
|
81
|
+
function newJob(dataDir, id = randomUUID()) {
|
|
82
82
|
const directory = jobPath(dataDir, id);
|
|
83
83
|
mkdirSync(directory, { mode: 0o700 });
|
|
84
84
|
return { id, directory };
|
|
@@ -265,13 +265,50 @@ function databaseCounts(path) {
|
|
|
265
265
|
db.close();
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
/** The existing maintenance gate owns exclusivity until this worker has exited.
|
|
269
|
+
* Keep synchronous file validation/fsync intact, but off the request thread. */
|
|
268
270
|
export async function createWorkspaceBackup(dataDir, options) {
|
|
271
|
+
const source = new URL("./workspace-backup.worker.ts", import.meta.url);
|
|
272
|
+
// Assign ownership before the worker can create staging, including crashes
|
|
273
|
+
// too early to send a job-created message back to this process.
|
|
274
|
+
const jobId = randomUUID();
|
|
275
|
+
let worker;
|
|
276
|
+
let completed = false;
|
|
277
|
+
try {
|
|
278
|
+
worker = new Worker(existsSync(source) ? source : new URL("./workspace-backup.worker.js", import.meta.url), {
|
|
279
|
+
workerData: { dataDir, options, jobId }, execArgv: [],
|
|
280
|
+
});
|
|
281
|
+
const result = await new Promise((resolveBackup, reject) => {
|
|
282
|
+
const failed = () => reject(new Error("The backup worker stopped before completing. Try again."));
|
|
283
|
+
worker.once("message", (reply) => {
|
|
284
|
+
if (reply.result)
|
|
285
|
+
resolveBackup(reply.result);
|
|
286
|
+
else if (reply.error)
|
|
287
|
+
reject(new Error(reply.error));
|
|
288
|
+
else
|
|
289
|
+
failed();
|
|
290
|
+
});
|
|
291
|
+
worker.once("error", failed);
|
|
292
|
+
worker.once("exit", failed);
|
|
293
|
+
});
|
|
294
|
+
completed = true;
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await worker?.terminate();
|
|
299
|
+
// Reuse the guarded deletion path: never sweep other jobs or recovery.
|
|
300
|
+
if (!completed && entryExists(join(dataDir, ".backups", jobId)))
|
|
301
|
+
removeWorkspaceBackupJob(dataDir, jobId);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Worker implementation; application callers use createWorkspaceBackup. */
|
|
305
|
+
export async function createWorkspaceBackupSnapshot(dataDir, options, jobId) {
|
|
269
306
|
if (Object.hasOwn(options, "credentials"))
|
|
270
307
|
throw new Error("Workspace backups do not transfer credentials.");
|
|
271
308
|
assertLocalAuthOutsideSnapshot(dataDir);
|
|
272
309
|
const salt = randomBytes(16);
|
|
273
310
|
const key = await passwordKey(options.password, salt);
|
|
274
|
-
const job = newJob(dataDir);
|
|
311
|
+
const job = newJob(dataDir, jobId);
|
|
275
312
|
const snapshot = join(job.directory, "snapshot");
|
|
276
313
|
folder(join(snapshot, "data"));
|
|
277
314
|
const entries = [];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
2
|
+
import { createWorkspaceBackupSnapshot } from "./workspace-backup.js";
|
|
3
|
+
const { dataDir, options, jobId } = workerData;
|
|
4
|
+
try {
|
|
5
|
+
// Same snapshot and cleanup implementation; never send password/options back.
|
|
6
|
+
parentPort.postMessage({ result: await createWorkspaceBackupSnapshot(dataDir, options, jobId) });
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
parentPort.postMessage({ error: error instanceof Error ? error.message : "The workspace backup could not finish." });
|
|
10
|
+
}
|
|
@@ -124,6 +124,25 @@ export function stripOmbAskBlock(output) {
|
|
|
124
124
|
return output;
|
|
125
125
|
return output.replace(OMB_ASK_FENCE_GLOBAL, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
126
126
|
}
|
|
127
|
+
/** The prompt contract that teaches a model the turn-held ask transport: end
|
|
128
|
+
* the reply with a fenced `omb-ask` block carrying the questions, and the
|
|
129
|
+
* answers come back on the next prompt. A harness that cannot pause mid-run
|
|
130
|
+
* (the BoxAgent transport) appends this to every prompt; the caps in the text
|
|
131
|
+
* are the constants above, so the taught contract and the parser cannot
|
|
132
|
+
* drift apart. */
|
|
133
|
+
export function ombAskProtocolPrompt() {
|
|
134
|
+
return [
|
|
135
|
+
"",
|
|
136
|
+
"## Asking the person a question",
|
|
137
|
+
"When a decision belongs to the person, end your reply with a fenced block exactly like this:",
|
|
138
|
+
"",
|
|
139
|
+
"```omb-ask",
|
|
140
|
+
'{"questions":[{"question":"Ship the release now?","header":"Release","options":[{"label":"Ship now"},{"label":"Wait for the QA signoff"}]}]}',
|
|
141
|
+
"```",
|
|
142
|
+
"",
|
|
143
|
+
`The block must be the last thing in your reply. You may ask up to ${MAX_QUESTIONS} questions at once, each with up to ${MAX_OPTIONS} options; the person can always answer in their own words. Their answers arrive on your next prompt as \`Q:\`/\`A:\` lines — never invent them.`,
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
127
146
|
/** The one line the card subtitle and a spoken prompt show. */
|
|
128
147
|
export function askQuestionSummary(questions) {
|
|
129
148
|
const first = questions[0]?.question ?? "";
|
|
@@ -76,11 +76,15 @@ export function credentialIsConfigured(config, id) {
|
|
|
76
76
|
return Boolean(config.imageGen?.key);
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
/** A still-pending credential card for this target — the set a newer
|
|
80
|
+
* request supersedes. Provided, dismissed, and already-superseded cards
|
|
81
|
+
* keep their settled state untouched. */
|
|
82
|
+
export function isPendingCredentialRequest(message, target, requestingBotId, roomThread) {
|
|
80
83
|
return (message.kind === "secret" &&
|
|
81
84
|
message.secret?.target === target &&
|
|
82
85
|
message.secret.provided !== true &&
|
|
83
86
|
message.secret.dismissed !== true &&
|
|
87
|
+
message.secret.superseded !== true &&
|
|
84
88
|
(!roomThread || message.from?.botId === requestingBotId));
|
|
85
89
|
}
|
|
86
90
|
export function credentialResumeOutcome(state) {
|
|
@@ -40,3 +40,26 @@ export function lineDiff(before, after) {
|
|
|
40
40
|
out.push(`+${b[j++]}`);
|
|
41
41
|
return out;
|
|
42
42
|
}
|
|
43
|
+
/** Cap on diff lines rendered inside an approval card. Past this the card
|
|
44
|
+
* shows the complete proposed document instead of a truncated diff, because
|
|
45
|
+
* truncation can hide the very instructions being approved. */
|
|
46
|
+
export const MAX_DIFF_LINES = 400;
|
|
47
|
+
/** The SOUL.md review block shared by the profile and team-setup cards:
|
|
48
|
+
* byte counts, a line diff for reviewable changes, and the complete
|
|
49
|
+
* replacement when the diff would outrun the card. The full replacement is
|
|
50
|
+
* already byte-bounded by profile validation and is readable in the same
|
|
51
|
+
* generic card on desktop and phones. */
|
|
52
|
+
export function soulDiffLines(before, after) {
|
|
53
|
+
const encoder = new TextEncoder();
|
|
54
|
+
const bytesBefore = encoder.encode(before).length;
|
|
55
|
+
const bytesAfter = encoder.encode(after).length;
|
|
56
|
+
const diff = lineDiff(before, after);
|
|
57
|
+
if (diff.length > MAX_DIFF_LINES) {
|
|
58
|
+
return [
|
|
59
|
+
`SOUL.md (${bytesBefore} → ${bytesAfter} bytes):`,
|
|
60
|
+
"Large change — complete proposed instructions (replaces the current SOUL.md):",
|
|
61
|
+
after || "(empty)",
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
return [`SOUL.md (${bytesBefore} → ${bytesAfter} bytes):`, ...diff];
|
|
65
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|