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
|
@@ -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;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { peerName } from "./peer-roster.js";
|
|
2
|
+
// All three use createOpenAIChatRuntime and get history via SendTurnInput.transcript.
|
|
3
|
+
export const NATIVELY_REPLAYING_DRIVER_KINDS = ["grok", "openai-compat", "minimax"];
|
|
2
4
|
/** Does this engine need the thread replayed to it? True when a DIFFERENT
|
|
3
5
|
* instance ran the last turn here — a cursor of our own is not enough,
|
|
4
6
|
* because it only proves we once had a session covering some prefix of the
|
|
@@ -23,10 +23,10 @@ export class TurnWatchdog {
|
|
|
23
23
|
this.timer = null;
|
|
24
24
|
this.turns.clear();
|
|
25
25
|
}
|
|
26
|
-
/** A turn was
|
|
26
|
+
/** A turn was admitted on this thread. */
|
|
27
27
|
watch(threadId, botId) {
|
|
28
28
|
const at = this.now();
|
|
29
|
-
this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false });
|
|
29
|
+
this.turns.set(threadId, { threadId, botId, startedAt: at, lastEventAt: at, waitingOnHuman: false, waitingOnComputer: false });
|
|
30
30
|
}
|
|
31
31
|
/** Any provider event for the thread proves the turn is alive. */
|
|
32
32
|
touch(threadId) {
|
|
@@ -43,6 +43,15 @@ export class TurnWatchdog {
|
|
|
43
43
|
turn.waitingOnHuman = waiting;
|
|
44
44
|
turn.lastEventAt = this.now();
|
|
45
45
|
}
|
|
46
|
+
/** Shared-computer waits have their own bounded deadline. Waiting for
|
|
47
|
+
* another turn to release the computer is not a silent provider stall. */
|
|
48
|
+
setWaitingOnComputer(threadId, waiting) {
|
|
49
|
+
const turn = this.turns.get(threadId);
|
|
50
|
+
if (!turn)
|
|
51
|
+
return;
|
|
52
|
+
turn.waitingOnComputer = waiting;
|
|
53
|
+
turn.lastEventAt = this.now();
|
|
54
|
+
}
|
|
46
55
|
/** The turn settled normally — stop watching it. */
|
|
47
56
|
settle(threadId) {
|
|
48
57
|
this.turns.delete(threadId);
|
|
@@ -54,7 +63,7 @@ export class TurnWatchdog {
|
|
|
54
63
|
sweep() {
|
|
55
64
|
const at = this.now();
|
|
56
65
|
for (const turn of this.turns.values()) {
|
|
57
|
-
if (turn.waitingOnHuman)
|
|
66
|
+
if (turn.waitingOnHuman || turn.waitingOnComputer)
|
|
58
67
|
continue;
|
|
59
68
|
if (at - turn.lastEventAt < this.opts.stallMs)
|
|
60
69
|
continue;
|
|
@@ -11,13 +11,21 @@ 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;
|
|
18
18
|
const writeQueues = new Map();
|
|
19
19
|
const clean = (value) => typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
20
20
|
const finiteOrNull = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
21
|
+
const promptBytesOf = (value) => {
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
return null;
|
|
24
|
+
const record = value;
|
|
25
|
+
return typeof record.stable === "number" && typeof record.volatile === "number"
|
|
26
|
+
? { stable: clean(record.stable), volatile: clean(record.volatile) }
|
|
27
|
+
: null;
|
|
28
|
+
};
|
|
21
29
|
function monthKey(at) {
|
|
22
30
|
return `${at.getUTCFullYear()}-${String(at.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
23
31
|
}
|
|
@@ -34,12 +42,15 @@ export function usageRowKey(row) {
|
|
|
34
42
|
/** Append one settled turn. Fire-and-forget (see the module comment): the
|
|
35
43
|
* returned promise never rejects, and says whether the row reached disk. */
|
|
36
44
|
export function appendUsage(dataDir, row) {
|
|
45
|
+
const { promptBytes: rawPromptBytes, ...rest } = row;
|
|
46
|
+
const promptBytes = promptBytesOf(rawPromptBytes);
|
|
37
47
|
const record = {
|
|
38
|
-
...
|
|
48
|
+
...rest,
|
|
39
49
|
at: row.at ?? new Date().toISOString(),
|
|
40
50
|
input: clean(row.input),
|
|
41
51
|
output: clean(row.output),
|
|
42
52
|
...(typeof row.cachedInput === "number" ? { cachedInput: clean(row.cachedInput) } : {}),
|
|
53
|
+
...(promptBytes ? { promptBytes } : {}),
|
|
43
54
|
costUsd: finiteOrNull(row.costUsd),
|
|
44
55
|
};
|
|
45
56
|
// A cost is labelled with where it came from; an unpriced row carries no label.
|
|
@@ -173,6 +184,12 @@ function groupOf(row, groupBy) {
|
|
|
173
184
|
return { key: triggerKey(row.trigger), label: triggerLabel(row.trigger) };
|
|
174
185
|
case "day":
|
|
175
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" };
|
|
176
193
|
default:
|
|
177
194
|
return { key: `engine:${row.driverKind}`, label: row.driverKind };
|
|
178
195
|
}
|