openmausbot 0.1.95 → 0.1.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{DesktopViewer-BwWpc4RK.js → DesktopViewer-DsnA23KM.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-jsQkIqTn.js → abnfDiagram-O67JEVCF-Dkbd2hcj.js} +1 -1
- package/dist/assets/{arc-7kei4Hsu.js → arc-Duj5Coy2.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CRWDFDMq.js → architectureDiagram-NJMV4G6O-uYUsDawP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-CTKW_IKs.js → blockDiagram-BEXU5L5S-CWDlTpXe.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BBTW7-6M.js → c4Diagram-YGBWAQC7-B0t15PDc.js} +1 -1
- package/dist/assets/channel-BZxwC-Gw.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-M_Ox-MkD.js → chunk-2Q5K7J3B-BJSCL-HV.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-D2YiN-Ud.js → chunk-3FUC2YCW-D3_ILccd.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-BmVYJ0f8.js → chunk-5VM5RSS4-D3dmwT_S.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-BtiLVVnZ.js → chunk-GTNCS2PH-Br73HFPD.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-DFYSb6J6.js → chunk-GWA4HPMP-uSsjxubC.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-mknKBzgV.js → chunk-JWPE2WC7-C40VWFyr.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-DgSYGiOu.js → chunk-NETBCI7D-D6WNNYwU.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-C7ZvuguH.js → chunk-WEXAMYUT-DOEhq64P.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-CPqxINpA.js → chunk-XXDRQBXY-BqJEi6Pi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Cd6xfJaC.js → classDiagram-v2-NBCMYWYE-ByU_GnxT.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BSXY5qRh.js → cose-bilkent-JH36ORCC-D-Uglxqq.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-D_lhEMj1.js → cynefin-OW5HDTMX-CZWO7aYj.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Da9FrFwk.js → cynefinDiagram-VND7K2PF-C70-w0la.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-T77jMOXe.js → dagre-6A5THRUB-CnhjhExB.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Dkj96X5E.js → diagram-22UHCM2B-bnjWCNb4.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CMi9zlD_.js → diagram-3UASUU5V-A4uTWoFV.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-CXK6YUfS.js → diagram-ATOU4E4O-Zi6m3NUl.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-BE7B1u9Q.js → diagram-CDSNMT55-DtCcmd5k.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CCMQphKr.js → diagram-MLGK6HIB-DofuETS3.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-iHJ_-tGH.js → diagram-MPIPVDR6-Dw2T9PuU.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-DONsCWQ7.js → ebnfDiagram-ZINNZB2B-DbrwPiem.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-CU_WOaXk.js → elk-276RUBZZ-BvCt1dCb.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BeYwM04G.js → erDiagram-OPXOYQCR-Y-QKqPnU.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bdw9DqEl.js → flowDiagram-KWPJA3E3-CDkL1B5w.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-C9iwq3qm.js → ganttDiagram-FUAMR5RP-CKV5D2F6.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Dx-VWzUg.js → gitGraphDiagram-X574FWY7-nL9iFTCv.js} +1 -1
- package/dist/assets/{index-KygHRE3E.js → index-DreDnjOu.js} +1 -1
- package/dist/assets/index-Du7o6Dsf.js +610 -0
- package/dist/assets/index-KxG4bWSr.css +1 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-BG7uaEd4.js → infoDiagram-VRGFBTTK-C7aHjzsf.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-D72WAXnM.js → ishikawaDiagram-OU5B5YK6-KUZsNBTh.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DKH1gjYT.js → journeyDiagram-ZHPQQLJL-CGUuodFl.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-3upm9JUT.js → kanban-definition-PNTS6WVX-Cy6r868Z.js} +1 -1
- package/dist/assets/{linear-CcR4pc0-.js → linear-Bl669148.js} +1 -1
- package/dist/assets/{mermaid.core-C6yU5FSS.js → mermaid.core-86Xqz5Mr.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DOI2xEuv.js → mindmap-definition-NLK3R4M7-G4DzHLrd.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BVWJSgLd.js → pegDiagram-GJSIUBJH-K4eruzY0.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-jtbYezhq.js → pieDiagram-5QR66LMP-DYdR9PPq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-g0u8mtPX.js → quadrantDiagram-O4NWA36T-BCAh0nEy.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-k8689Wny.js → railroadDiagram-XR7U4H2S-p0dGRc4g.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-DsJ22PDt.js → requirementDiagram-PLB6GJNP-tLeBG0aL.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-CneWBRgG.js → sankeyDiagram-IPEJSGJF-QRuN16rV.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DWsFCWap.js → sequenceDiagram-PO4LG4MO-ueT1nVoa.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Bvf0YE3S.js → sizeCapture-INFHLROL-wRS2zWg2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-Bxb3Iei8.js → stateDiagram-v2-GCMORJYK-CYzA_hE1.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DLkk7W4A.js → swimlanes-2SLR337P-kDnv6LMc.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C7voMDle.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-KR9bIHYC.js → timeline-definition-EJHVYXUP-D1y2UbbH.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-DfsOrj8t.js → usecaseDiagram-POWQR4AR-IqB4v9HX.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-DRSTzG3h.js → vennDiagram-UO4OBE2U-BEi7KYVF.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-DtEdhPgT.js → wardleyDiagram-VNRHLVJA-Dkybo-Yf.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-B-XhTxjG.js → xychartDiagram-PMCCYNJV-BAPFJLpg.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +25 -12
- package/dist-server/companion/src/listener.js +39 -18
- package/dist-server/container-mcp.js +14 -27
- package/dist-server/drivers/agents-proxy.js +103 -44
- package/dist-server/harness-mcp-proxy.js +1 -2
- package/dist-server/index.js +2633 -3396
- package/dist-server/openmausbot.js +606 -463
- package/dist-server/pair-cli.js +606 -463
- package/dist-server/server/admin-activity.js +1 -4
- package/dist-server/server/auto-approve.js +19 -4
- package/dist-server/server/boat.js +4 -4
- package/dist-server/server/checkpoints.js +90 -148
- package/dist-server/server/chief-of-staff.js +7 -6
- package/dist-server/server/cli.js +33 -26
- package/dist-server/server/cloud-home-start.js +12 -15
- package/dist-server/server/cloud-home.js +0 -4
- package/dist-server/server/cloud-move-http.js +44 -30
- package/dist-server/server/cloud-move.js +20 -18
- package/dist-server/server/composio.js +10 -3
- package/dist-server/server/config.js +92 -64
- package/dist-server/server/container-computer.js +16 -5
- package/dist-server/server/contracts.js +0 -9
- package/dist-server/server/decider/jev.js +1 -4
- package/dist-server/server/decider/log.js +1 -3
- package/dist-server/server/decision-log.js +1 -4
- package/dist-server/server/delegations.js +3 -2
- package/dist-server/server/drivers/acp/core.js +4 -3
- package/dist-server/server/drivers/acp/opencode-go.js +17 -16
- package/dist-server/server/drivers/agents-call.js +46 -63
- package/dist-server/server/drivers/agents-catalog.js +66 -0
- package/dist-server/server/drivers/agents-proxy.js +2 -1
- package/dist-server/server/drivers/claude-login-auth.js +2 -11
- package/dist-server/server/drivers/codex-device-auth.js +3 -1
- package/dist-server/server/drivers/codex.js +5 -4
- package/dist-server/server/drivers/grok.js +1 -1
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -2
- package/dist-server/server/drivers/openai-chat.js +15 -5
- package/dist-server/server/drivers/prompt-split.js +4 -4
- package/dist-server/server/harness/bus.js +49 -0
- package/dist-server/server/harness-mcp-proxy.js +1 -1
- package/dist-server/server/index.js +448 -762
- package/dist-server/server/mcp-registry.js +35 -0
- package/dist-server/server/message-file.js +4 -4
- package/dist-server/server/peer-delivery.js +4 -19
- package/dist-server/server/peer-roster.js +30 -4
- package/dist-server/server/profile-versions.js +0 -31
- package/dist-server/server/provider-key-check.js +2 -1
- package/dist-server/server/restart.js +40 -0
- package/dist-server/server/room-handoffs.js +12 -0
- package/dist-server/server/routes/bot-memory.js +0 -24
- package/dist-server/server/routine-requests.js +1 -4
- package/dist-server/server/routines.js +5 -4
- package/dist-server/server/server-launcher.js +53 -0
- package/dist-server/server/store.js +1 -1
- package/dist-server/server/surface.js +23 -27
- package/dist-server/server/system-prompt.js +13 -3
- package/dist-server/server/team-manifest.js +1 -6
- package/dist-server/server/tunnel.js +4 -1
- package/dist-server/server/turn-images.js +1 -21
- package/dist-server/server/turn-resources.js +19 -150
- package/dist-server/server/usage-ledger.js +3 -1
- package/dist-server/server/workspace.js +2 -3
- package/dist-server/server-launcher.js +72 -0
- package/dist-server/shared/ask-question.js +1 -1
- package/dist-server/shared/cloud-computer.js +12 -0
- package/dist-server/shared/failed-turn.js +25 -0
- package/dist-server/shared/live-approval.js +2 -2
- package/dist-server/shared/package-format.js +3 -3
- package/dist-server/shared/pairing-link.js +144 -0
- package/dist-server/vps-container-mcp.js +14 -27
- package/dist-server/workspace-backup.worker.js +14 -27
- package/package.json +1 -1
- package/dist/assets/channel-DS2NbT5x.js +0 -1
- package/dist/assets/index-NF7XCHAy.css +0 -1
- package/dist/assets/index-X_5ydXIE.js +0 -610
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +0 -8
- package/dist-server/server/claim-idle.js +0 -33
- package/dist-server/server/cloud-overflow.js +0 -167
- package/dist-server/server/tightening-requests.js +0 -435
- package/dist-server/shared/tightening-request.js +0 -87
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import{c as r,s as e}from"./flowDiagram-KWPJA3E3-Bdw9DqEl.js";import{_ as a}from"./mermaid.core-C6yU5FSS.js";import"./chunk-5VM5RSS4-BmVYJ0f8.js";import"./chunk-XXDRQBXY-CPqxINpA.js";import"./chunk-WEXAMYUT-C7ZvuguH.js";import"./chunk-GWA4HPMP-DFYSb6J6.js";import"./channel-DS2NbT5x.js";import"./index-X_5ydXIE.js";var o=a(t=>`${e(t)}
|
|
2
|
-
.swimlane.cluster:not([data-color-id]) rect {
|
|
3
|
-
stroke: ${t.clusterBorder} !important;
|
|
4
|
-
}
|
|
5
|
-
[data-look="neo"].cluster rect {
|
|
6
|
-
filter: none;
|
|
7
|
-
}
|
|
8
|
-
`,"getStyles"),s=o,u=r({styles:s});export{u as diagram};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** Quiet-window policy for releasing a held seat while its turn lives.
|
|
2
|
-
*
|
|
3
|
-
* The policy is deliberately synchronous and clock-injected: like the
|
|
4
|
-
* claim map it serves, expiry is evaluated when a claim is touched
|
|
5
|
-
* (claim, blocker, owns) rather than by a timer, so the server's single
|
|
6
|
-
* thread decides every transition and tests pin time exactly. Slice
|
|
7
|
-
* #1655 reuses this window math for its own idle release; keep this
|
|
8
|
-
* module free of server imports. */
|
|
9
|
-
/** Default quiet window: no screen activity for this long releases the seat. */
|
|
10
|
-
export const COMPUTER_CLAIM_QUIET_MS = 90_000;
|
|
11
|
-
/** Default reclaim window: the previous holder re-claims directly for this long. */
|
|
12
|
-
export const COMPUTER_CLAIM_RECLAIM_MS = 10 * 60_000;
|
|
13
|
-
export class IdleReleasePolicy {
|
|
14
|
-
quietMs;
|
|
15
|
-
reclaimMs;
|
|
16
|
-
constructor(quietMs = COMPUTER_CLAIM_QUIET_MS, reclaimMs = COMPUTER_CLAIM_RECLAIM_MS) {
|
|
17
|
-
if (!Number.isFinite(quietMs) || quietMs <= 0)
|
|
18
|
-
throw new Error("Idle release quiet window must be positive");
|
|
19
|
-
if (!Number.isFinite(reclaimMs) || reclaimMs <= 0)
|
|
20
|
-
throw new Error("Idle release reclaim window must be positive");
|
|
21
|
-
this.quietMs = quietMs;
|
|
22
|
-
this.reclaimMs = reclaimMs;
|
|
23
|
-
}
|
|
24
|
-
/** True once the seat has been quiet for the full window. Screen-poller
|
|
25
|
-
* frames never refresh the activity clock, so they cannot keep a seat. */
|
|
26
|
-
quietElapsed(lastActivityAt, now) {
|
|
27
|
-
return now - lastActivityAt >= this.quietMs;
|
|
28
|
-
}
|
|
29
|
-
/** True while a released seat's previous holder still re-claims directly. */
|
|
30
|
-
reclaimLive(releasedAt, now) {
|
|
31
|
-
return now < releasedAt + this.reclaimMs;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/** Consent, cost, and idle-stop policy for overflowing a local computer
|
|
2
|
-
* wait onto a per-second-billed cloud seat (#1655).
|
|
3
|
-
*
|
|
4
|
-
* Like server/claim-idle.ts this module is deliberately synchronous and
|
|
5
|
-
* clock-injected: the server decides every transition on its single
|
|
6
|
-
* thread and tests pin time exactly. Nothing here starts or stops a
|
|
7
|
-
* machine — the wiring does, and only when the decision says to. Keep
|
|
8
|
-
* this module free of server imports. */
|
|
9
|
-
import { computerWaitDuration } from "./computer-wait.js";
|
|
10
|
-
/** Default idle stop: a cloud seat no real screen work has touched for
|
|
11
|
-
* this long is stopped (archived), because billing runs per second while
|
|
12
|
-
* it runs. "A few idle minutes" per the issue. */
|
|
13
|
-
export const CLOUD_SEAT_IDLE_STOP_MS = 5 * 60_000;
|
|
14
|
-
/** Retry cadence for an idle stop that failed (#1655): the first retry
|
|
15
|
-
* after one minute, doubling per failure up to ten. A failed stop keeps
|
|
16
|
-
* its lease — the seat still bills — so the sweep always retries instead
|
|
17
|
-
* of abandoning a running machine. */
|
|
18
|
-
export const CLOUD_SEAT_SLEEP_RETRY_MS = 60_000;
|
|
19
|
-
export const CLOUD_SEAT_SLEEP_RETRY_MAX_MS = 10 * 60_000;
|
|
20
|
-
export function cloudOverflowAction(situation) {
|
|
21
|
-
if (!situation.featureEnabled || !situation.cloudConfigured || situation.perSecondCostUsd === null)
|
|
22
|
-
return { kind: "off" };
|
|
23
|
-
if (situation.started)
|
|
24
|
-
return { kind: "none" };
|
|
25
|
-
if (situation.consented)
|
|
26
|
-
return { kind: "start" };
|
|
27
|
-
if (situation.offered)
|
|
28
|
-
return { kind: "none" };
|
|
29
|
-
return { kind: "offer" };
|
|
30
|
-
}
|
|
31
|
-
/** Per-conversation consent plus the offer bookkeeping that keeps the
|
|
32
|
-
* card to one per conversation until it is revoked. A thread the operator
|
|
33
|
-
* allowlisted in config carries standing consent and never needs the
|
|
34
|
-
* card. */
|
|
35
|
-
export class CloudOverflowConsent {
|
|
36
|
-
grants = new Map();
|
|
37
|
-
offers = new Map();
|
|
38
|
-
revocations = new Set();
|
|
39
|
-
/** Consent for this conversation. With `perSecondCostUsd`, a card
|
|
40
|
-
* consent counts only at the rate its card showed — an allowlisted
|
|
41
|
-
* thread carries the operator's standing consent, and the operator is
|
|
42
|
-
* also the one who sets the rate. */
|
|
43
|
-
consented(threadId, allowlistedThreads = new Set(), perSecondCostUsd) {
|
|
44
|
-
if (this.revocations.has(threadId))
|
|
45
|
-
return false;
|
|
46
|
-
if (allowlistedThreads.has(threadId))
|
|
47
|
-
return true;
|
|
48
|
-
const grant = this.grants.get(threadId);
|
|
49
|
-
if (!grant)
|
|
50
|
-
return false;
|
|
51
|
-
return perSecondCostUsd === undefined || grant.perSecondCostUsd === perSecondCostUsd;
|
|
52
|
-
}
|
|
53
|
-
/** `perSecondCostUsd` is the rate the answered card showed. */
|
|
54
|
-
grant(threadId, perSecondCostUsd, now = Date.now()) {
|
|
55
|
-
this.revocations.delete(threadId);
|
|
56
|
-
this.grants.set(threadId, { at: now, perSecondCostUsd });
|
|
57
|
-
}
|
|
58
|
-
/** Revoking also clears the offered mark, so a later wait in the same
|
|
59
|
-
* conversation may ask again instead of meeting silence. */
|
|
60
|
-
revoke(threadId) {
|
|
61
|
-
const had = this.grants.delete(threadId);
|
|
62
|
-
this.revocations.add(threadId);
|
|
63
|
-
this.offers.delete(threadId);
|
|
64
|
-
return had;
|
|
65
|
-
}
|
|
66
|
-
/** A card is outstanding for this conversation — at the given rate
|
|
67
|
-
* when one is supplied, so a config change re-offers with the new
|
|
68
|
-
* price instead of meeting silence at the old one. */
|
|
69
|
-
offered(threadId, perSecondCostUsd) {
|
|
70
|
-
const offer = this.offers.get(threadId);
|
|
71
|
-
if (!offer)
|
|
72
|
-
return false;
|
|
73
|
-
return perSecondCostUsd === undefined || offer.perSecondCostUsd === perSecondCostUsd;
|
|
74
|
-
}
|
|
75
|
-
/** The rate the outstanding card showed, or null when no card is out. */
|
|
76
|
-
offeredRate(threadId) {
|
|
77
|
-
return this.offers.get(threadId)?.perSecondCostUsd ?? null;
|
|
78
|
-
}
|
|
79
|
-
markOffered(threadId, perSecondCostUsd, now = Date.now()) {
|
|
80
|
-
this.offers.set(threadId, { at: now, perSecondCostUsd });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/** One running cloud seat started by overflow. `touch` is called only
|
|
84
|
-
* for real computer tool completions on that seat — screen-poller frames
|
|
85
|
-
* never reach it, exactly like #1653's activity clock, so preview
|
|
86
|
-
* traffic cannot keep a paid machine awake. */
|
|
87
|
-
export class CloudSeatLease {
|
|
88
|
-
botId;
|
|
89
|
-
threadId;
|
|
90
|
-
/** The claim generation that started the seat: a stop releases the Box
|
|
91
|
-
* claim for exactly this owner, never a newer turn's. */
|
|
92
|
-
generation;
|
|
93
|
-
startedAt;
|
|
94
|
-
idleStopMs;
|
|
95
|
-
lastActivityAt;
|
|
96
|
-
sleepRetryNotBefore = 0;
|
|
97
|
-
sleepRetryMs = CLOUD_SEAT_SLEEP_RETRY_MS;
|
|
98
|
-
constructor(init) {
|
|
99
|
-
const idleStopMs = init.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
100
|
-
if (!Number.isFinite(idleStopMs) || idleStopMs <= 0)
|
|
101
|
-
throw new Error("Cloud seat idle stop window must be positive");
|
|
102
|
-
this.botId = init.botId;
|
|
103
|
-
this.threadId = init.threadId;
|
|
104
|
-
this.generation = init.generation;
|
|
105
|
-
this.startedAt = init.now ?? Date.now();
|
|
106
|
-
this.lastActivityAt = this.startedAt;
|
|
107
|
-
this.idleStopMs = idleStopMs;
|
|
108
|
-
}
|
|
109
|
-
touch(now = Date.now()) {
|
|
110
|
-
this.lastActivityAt = now;
|
|
111
|
-
// Real work earns a fresh stop attempt: the backoff only paces
|
|
112
|
-
// retries against an idle seat that keeps failing to sleep.
|
|
113
|
-
this.sleepRetryNotBefore = 0;
|
|
114
|
-
this.sleepRetryMs = CLOUD_SEAT_SLEEP_RETRY_MS;
|
|
115
|
-
}
|
|
116
|
-
/** An idle stop may run now — true until a failure defers it. */
|
|
117
|
-
sleepDue(now = Date.now()) {
|
|
118
|
-
return now >= this.sleepRetryNotBefore;
|
|
119
|
-
}
|
|
120
|
-
/** The idle stop failed: keep the lease and back off, bounded, so the
|
|
121
|
-
* sweep retries gently instead of hammering a sick Box — and never
|
|
122
|
-
* abandons a seat that still bills. */
|
|
123
|
-
deferSleep(now = Date.now()) {
|
|
124
|
-
this.sleepRetryNotBefore = now + this.sleepRetryMs;
|
|
125
|
-
this.sleepRetryMs = Math.min(this.sleepRetryMs * 2, CLOUD_SEAT_SLEEP_RETRY_MAX_MS);
|
|
126
|
-
}
|
|
127
|
-
idleFor(now = Date.now()) {
|
|
128
|
-
return Math.max(0, now - this.lastActivityAt);
|
|
129
|
-
}
|
|
130
|
-
idleElapsed(now = Date.now()) {
|
|
131
|
-
return now - this.lastActivityAt >= this.idleStopMs;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/** Dollars per second at rate-card precision: enough places for small
|
|
135
|
-
* per-second rates to stay visible, trailing zeros trimmed but never
|
|
136
|
-
* past two decimals. */
|
|
137
|
-
export function formatPerSecondUsd(perSecondCostUsd) {
|
|
138
|
-
let text = perSecondCostUsd.toFixed(6).replace(/0+$/, "");
|
|
139
|
-
if (text.endsWith("."))
|
|
140
|
-
text += "00";
|
|
141
|
-
else {
|
|
142
|
-
const decimals = text.length - text.indexOf(".") - 1;
|
|
143
|
-
if (decimals < 2)
|
|
144
|
-
text += "0".repeat(2 - decimals);
|
|
145
|
-
}
|
|
146
|
-
return "$" + text;
|
|
147
|
-
}
|
|
148
|
-
/** The consent card (#1655): the per-second cost sits beside the local
|
|
149
|
-
* wait picture before any choice is made, and nothing starts without an
|
|
150
|
-
* explicit consent the person still has to give. */
|
|
151
|
-
export function cloudOverflowOfferText(opts) {
|
|
152
|
-
const idleStopMs = opts.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
153
|
-
const estimate = opts.waitEstimateMs === undefined ? "" : ` Recent local waits here have taken about ${computerWaitDuration(opts.waitEstimateMs)}.`;
|
|
154
|
-
return `This computer is busy. Its work can overflow to a cloud computer for ${formatPerSecondUsd(opts.perSecondCostUsd)} per second.${estimate} Nothing starts without your consent — allow cloud overflow for this conversation to start it; the cloud computer stops after ${computerWaitDuration(idleStopMs)} idle.`;
|
|
155
|
-
}
|
|
156
|
-
export function cloudSeatStartedText(opts) {
|
|
157
|
-
const idleStopMs = opts.idleStopMs ?? CLOUD_SEAT_IDLE_STOP_MS;
|
|
158
|
-
return `Cloud computer started at ${formatPerSecondUsd(opts.perSecondCostUsd)} per second. It stops automatically after ${computerWaitDuration(idleStopMs)} idle, and the local wait continues until this computer is free.`;
|
|
159
|
-
}
|
|
160
|
-
export function cloudSeatStoppedText(idleMs) {
|
|
161
|
-
return `Cloud computer stopped after ${computerWaitDuration(idleMs)} idle — billing pauses while it sleeps.`;
|
|
162
|
-
}
|
|
163
|
-
export function cloudOverflowConsentText(allowed, idleStopMs = CLOUD_SEAT_IDLE_STOP_MS) {
|
|
164
|
-
return allowed
|
|
165
|
-
? `Cloud overflow allowed for this conversation. A waiting turn may start the cloud computer; it stops after ${computerWaitDuration(idleStopMs)} idle.`
|
|
166
|
-
: "Cloud overflow consent revoked. Waits stay local, and no cloud computer starts without a new consent.";
|
|
167
|
-
}
|
|
@@ -1,435 +0,0 @@
|
|
|
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 { recordAuthorityChange } from "./profile-versions.js";
|
|
14
|
-
import { redactSecretsInText } from "./redact.js";
|
|
15
|
-
const MAX_REASON = 500;
|
|
16
|
-
const STALE = "This bot's authority changed after this card was prepared. Ask the bot to review it and propose again.";
|
|
17
|
-
const NO_SUCH_BOT = "That bot no longer exists";
|
|
18
|
-
const CHOOSE_ONE = "Choose at least one tightening intent: approvalMode, composio, browser, approvePeerComms, alwaysAllow, mcpServers, skills";
|
|
19
|
-
const INTENT_KEYS = [
|
|
20
|
-
"approvalMode",
|
|
21
|
-
"composio",
|
|
22
|
-
"browser",
|
|
23
|
-
"approvePeerComms",
|
|
24
|
-
"alwaysAllow",
|
|
25
|
-
"mcpServers",
|
|
26
|
-
"skills",
|
|
27
|
-
];
|
|
28
|
-
/** Direction is enforced by shape first: the payload cannot name "full" or
|
|
29
|
-
* "custom" as a target, cannot ask to switch composio/browser back on, and
|
|
30
|
-
* the list fields only carry removals. The state-relative remainder (an
|
|
31
|
-
* upward move inside ask/edits/auto, a grant not currently held) is checked
|
|
32
|
-
* by validateTightening against live state at propose and confirm. */
|
|
33
|
-
const intentsSchema = z.object({
|
|
34
|
-
approvalMode: z.enum(TIGHTENING_MODE_TARGETS, {
|
|
35
|
-
error: 'approvalMode may only be proposed as "ask", "edits", or "auto"; full and custom stay owner-only',
|
|
36
|
-
}).optional(),
|
|
37
|
-
composio: z.literal(false, { error: "composio may only be turned off" }).optional(),
|
|
38
|
-
browser: z.literal(false, { error: "browser may only be turned off" }).optional(),
|
|
39
|
-
approvePeerComms: z.literal(true, { error: "approvePeerComms may only be turned on" }).optional(),
|
|
40
|
-
alwaysAllow: z.array(z.string().min(1).max(200)).max(200).optional(),
|
|
41
|
-
mcpServers: z.array(z.string().min(1).max(200)).max(100).optional(),
|
|
42
|
-
skills: z.array(z.string().min(1).max(100)).max(100).optional(),
|
|
43
|
-
});
|
|
44
|
-
export class TighteningRequestError extends Error {
|
|
45
|
-
status;
|
|
46
|
-
constructor(message, status = 400) {
|
|
47
|
-
super(message);
|
|
48
|
-
this.name = "TighteningRequestError";
|
|
49
|
-
this.status = status;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
function reasonText(value) {
|
|
53
|
-
if (typeof value !== "string")
|
|
54
|
-
throw new TighteningRequestError("reason is required");
|
|
55
|
-
const trimmed = value.trim();
|
|
56
|
-
if (!trimmed)
|
|
57
|
-
throw new TighteningRequestError("reason is required");
|
|
58
|
-
if (trimmed.length > MAX_REASON) {
|
|
59
|
-
throw new TighteningRequestError(`reason must be ${MAX_REASON} characters or fewer`);
|
|
60
|
-
}
|
|
61
|
-
return redactSecretsInText(trimmed);
|
|
62
|
-
}
|
|
63
|
-
function parseIntents(input) {
|
|
64
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
65
|
-
throw new TighteningRequestError(CHOOSE_ONE);
|
|
66
|
-
}
|
|
67
|
-
const record = input;
|
|
68
|
-
for (const key of Object.keys(record)) {
|
|
69
|
-
if (!INTENT_KEYS.includes(key)) {
|
|
70
|
-
throw new TighteningRequestError(`unsupported tightening field: ${key}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
const parsed = intentsSchema.safeParse(input);
|
|
74
|
-
if (!parsed.success) {
|
|
75
|
-
throw new TighteningRequestError(parsed.error.issues[0]?.message ?? CHOOSE_ONE);
|
|
76
|
-
}
|
|
77
|
-
const intents = parsed.data;
|
|
78
|
-
// Copy through only the keys the caller sent, so "did this card touch
|
|
79
|
-
// approvalMode" stays answerable from the payload alone.
|
|
80
|
-
const present = {};
|
|
81
|
-
if (record.approvalMode !== undefined)
|
|
82
|
-
present.approvalMode = intents.approvalMode;
|
|
83
|
-
if (record.composio !== undefined)
|
|
84
|
-
present.composio = intents.composio;
|
|
85
|
-
if (record.browser !== undefined)
|
|
86
|
-
present.browser = intents.browser;
|
|
87
|
-
if (record.approvePeerComms !== undefined)
|
|
88
|
-
present.approvePeerComms = intents.approvePeerComms;
|
|
89
|
-
if (record.alwaysAllow !== undefined)
|
|
90
|
-
present.alwaysAllow = intents.alwaysAllow;
|
|
91
|
-
if (record.mcpServers !== undefined)
|
|
92
|
-
present.mcpServers = intents.mcpServers;
|
|
93
|
-
if (record.skills !== undefined)
|
|
94
|
-
present.skills = intents.skills;
|
|
95
|
-
if (Object.keys(present).length === 0)
|
|
96
|
-
throw new TighteningRequestError(CHOOSE_ONE);
|
|
97
|
-
return present;
|
|
98
|
-
}
|
|
99
|
-
/** The card revision: a hash of exactly the authority surface a tightening
|
|
100
|
-
* can touch, plus the private receipt (an older, interrupted card cannot
|
|
101
|
-
* apply twice). Sorted lists so an unrelated reorder is not a false stale. */
|
|
102
|
-
export function tighteningRevision(state, lastTighteningRequestId = "") {
|
|
103
|
-
return createHash("sha256")
|
|
104
|
-
.update(JSON.stringify({
|
|
105
|
-
approvalMode: state.approvalMode,
|
|
106
|
-
composio: state.composio,
|
|
107
|
-
browser: state.browser,
|
|
108
|
-
approvePeerComms: state.approvePeerComms,
|
|
109
|
-
alwaysAllow: [...state.alwaysAllow].sort(),
|
|
110
|
-
mcpServers: [...state.mcpServers].sort(),
|
|
111
|
-
skills: [...state.skills].sort(),
|
|
112
|
-
lastTighteningRequestId,
|
|
113
|
-
}), "utf8")
|
|
114
|
-
.digest("hex");
|
|
115
|
-
}
|
|
116
|
-
export function tighteningSnapshot(bot, extras) {
|
|
117
|
-
return {
|
|
118
|
-
approvalMode: approvalModeFor(bot),
|
|
119
|
-
composio: bot.composio !== false,
|
|
120
|
-
browser: bot.browser !== false,
|
|
121
|
-
approvePeerComms: bot.approvePeerComms === true,
|
|
122
|
-
alwaysAllow: bot.alwaysAllow ?? [],
|
|
123
|
-
mcpServers: extras.mcpServers ?? [...(bot.mcpServers ?? [])],
|
|
124
|
-
skills: extras.skills ?? [],
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/** Minimal card copy: the judged facts (field, before, after, names) plus
|
|
128
|
-
* the standing rule that only the owner can grant authority back. */
|
|
129
|
-
export function tighteningCardCopy(target, before, intents, reason) {
|
|
130
|
-
const checked = validateTightening(before, intents);
|
|
131
|
-
const after = checked.ok ? checked.after : before;
|
|
132
|
-
const title = target.crossBot ? `Tighten @${target.name}'s permissions?` : `Tighten ${target.name}'s permissions?`;
|
|
133
|
-
const lines = target.crossBot ? [`Whose permissions: @${target.name}`, `Why: ${reason}`] : [`Why: ${reason}`];
|
|
134
|
-
if (intents.approvalMode !== undefined)
|
|
135
|
-
lines.push(`Approvals: ${before.approvalMode} → ${intents.approvalMode}`);
|
|
136
|
-
if (intents.composio !== undefined)
|
|
137
|
-
lines.push("Connected apps: on → off");
|
|
138
|
-
if (intents.browser !== undefined)
|
|
139
|
-
lines.push("Browser: on → off");
|
|
140
|
-
if (intents.approvePeerComms !== undefined)
|
|
141
|
-
lines.push("Peer contact: no approval → ask first");
|
|
142
|
-
if (intents.alwaysAllow?.length)
|
|
143
|
-
lines.push(removalLine("Always-allow grants", "removed", before.alwaysAllow, after.alwaysAllow));
|
|
144
|
-
if (intents.mcpServers?.length)
|
|
145
|
-
lines.push(removalLine("MCP servers", "unmounted", before.mcpServers, after.mcpServers));
|
|
146
|
-
if (intents.skills?.length)
|
|
147
|
-
lines.push(removalLine("Skills", "disabled", before.skills, after.skills));
|
|
148
|
-
lines.push(`This reduces ${target.name}'s authority, and the reverse cannot be proposed back.`);
|
|
149
|
-
const fields = INTENT_KEYS.filter((key) => intents[key] !== undefined);
|
|
150
|
-
return { title, summary: `${title} · ${fields.join(", ")}`, detail: lines.join("\n") };
|
|
151
|
-
}
|
|
152
|
-
const REMOVED_NAME_CAP = 5;
|
|
153
|
-
/** One line per list field with the counts on both sides and the exact
|
|
154
|
-
* names that go away, capped with a count so a wide card still reads in
|
|
155
|
-
* one pass while the human approves precisely what disappears. */
|
|
156
|
-
function removalLine(label, verb, before, after) {
|
|
157
|
-
const removed = before.filter((name) => !after.includes(name));
|
|
158
|
-
const shown = removed.slice(0, REMOVED_NAME_CAP);
|
|
159
|
-
const more = removed.length - shown.length;
|
|
160
|
-
const names = shown.join(", ") + (more > 0 ? ` (+${more} more)` : "");
|
|
161
|
-
return `${label}: ${before.length} → ${after.length} — ${verb}: ${names}`;
|
|
162
|
-
}
|
|
163
|
-
export class TighteningRequestService {
|
|
164
|
-
store;
|
|
165
|
-
now;
|
|
166
|
-
canPersist;
|
|
167
|
-
autoApply;
|
|
168
|
-
mountedMcpServers;
|
|
169
|
-
enabledSkills;
|
|
170
|
-
disableSkill;
|
|
171
|
-
targetBusy;
|
|
172
|
-
/** Public: a caller's section membership can change between propose and
|
|
173
|
-
* confirm, and tests flip this mid-scenario to model that. */
|
|
174
|
-
validateTarget;
|
|
175
|
-
constructor(options) {
|
|
176
|
-
this.store = options.store;
|
|
177
|
-
this.now = options.now ?? Date.now;
|
|
178
|
-
this.canPersist = options.canPersist;
|
|
179
|
-
this.autoApply = options.autoApply;
|
|
180
|
-
this.mountedMcpServers = options.mountedMcpServers;
|
|
181
|
-
this.enabledSkills = options.enabledSkills;
|
|
182
|
-
this.disableSkill = options.disableSkill;
|
|
183
|
-
this.targetBusy = options.targetBusy;
|
|
184
|
-
this.validateTarget = options.validateTarget;
|
|
185
|
-
}
|
|
186
|
-
snapshotOf(bot) {
|
|
187
|
-
return tighteningSnapshot(bot, {
|
|
188
|
-
mcpServers: this.mountedMcpServers?.(bot),
|
|
189
|
-
skills: this.enabledSkills?.(bot.id) ?? [],
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
/** Disable every named skill, or report the first failure together with
|
|
193
|
-
* the names still outstanding. Shared by the fresh apply and the
|
|
194
|
-
* receipt-backed retry so both leave the same trail. */
|
|
195
|
-
applySkillDisables(botId, names) {
|
|
196
|
-
for (let index = 0; index < names.length; index += 1) {
|
|
197
|
-
const disabled = this.disableSkill(botId, names[index]);
|
|
198
|
-
if (!disabled.ok)
|
|
199
|
-
return { ok: false, error: disabled.error, outstanding: names.slice(index) };
|
|
200
|
-
}
|
|
201
|
-
return { ok: true };
|
|
202
|
-
}
|
|
203
|
-
propose(args) {
|
|
204
|
-
return this.prepare(args);
|
|
205
|
-
}
|
|
206
|
-
submit(args) {
|
|
207
|
-
const proposal = this.prepare(args, true);
|
|
208
|
-
return { ...proposal, state: proposal.result ? "applied" : "pending" };
|
|
209
|
-
}
|
|
210
|
-
prepare(args, submitted = false) {
|
|
211
|
-
const reason = reasonText(args.reason);
|
|
212
|
-
const intents = parseIntents(args.intents);
|
|
213
|
-
const targetBotId = args.targetBotId ?? args.botId;
|
|
214
|
-
const target = this.store.bot(targetBotId);
|
|
215
|
-
if (!target)
|
|
216
|
-
throw new TighteningRequestError(NO_SUCH_BOT, 404);
|
|
217
|
-
const crossBot = targetBotId !== args.botId;
|
|
218
|
-
if (crossBot && this.validateTarget) {
|
|
219
|
-
const refusal = this.validateTarget(args.botId, targetBotId);
|
|
220
|
-
if (refusal)
|
|
221
|
-
throw new TighteningRequestError(refusal, 403);
|
|
222
|
-
}
|
|
223
|
-
const before = this.snapshotOf(target);
|
|
224
|
-
const checked = validateTightening(before, intents);
|
|
225
|
-
if (!checked.ok)
|
|
226
|
-
throw new TighteningRequestError(checked.error, 400);
|
|
227
|
-
const requestId = newId();
|
|
228
|
-
const targetName = redactSecretsInText(target.name);
|
|
229
|
-
const payload = {
|
|
230
|
-
version: 1,
|
|
231
|
-
requestId,
|
|
232
|
-
botId: args.botId,
|
|
233
|
-
threadId: args.threadId,
|
|
234
|
-
targetBotId,
|
|
235
|
-
targetName,
|
|
236
|
-
createdAt: this.now(),
|
|
237
|
-
reason,
|
|
238
|
-
intents,
|
|
239
|
-
before,
|
|
240
|
-
expectedRevision: tighteningRevision(before, target.lastTighteningRequestId),
|
|
241
|
-
};
|
|
242
|
-
const copy = tighteningCardCopy({ name: targetName, crossBot }, before, intents, reason);
|
|
243
|
-
const persistence = this.canPersist?.(args.botId, args.threadId);
|
|
244
|
-
if (persistence && !persistence.ok) {
|
|
245
|
-
throw new TighteningRequestError(persistence.error, persistence.status);
|
|
246
|
-
}
|
|
247
|
-
const automatic = submitted && this.autoApply?.(args.botId, args.threadId) === true;
|
|
248
|
-
const messageInput = {
|
|
249
|
-
role: "bot",
|
|
250
|
-
kind: "options",
|
|
251
|
-
card: {
|
|
252
|
-
title: copy.title,
|
|
253
|
-
subtitle: copy.detail,
|
|
254
|
-
options: automatic ? [] : ["Confirm", "Cancel"],
|
|
255
|
-
...(automatic ? { dismissed: true } : {}),
|
|
256
|
-
requestId,
|
|
257
|
-
tool: "tighten_permissions",
|
|
258
|
-
tighteningRequest: payload,
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
if (args.from)
|
|
262
|
-
messageInput.from = args.from;
|
|
263
|
-
const message = this.store.appendMessage(args.threadId, messageInput);
|
|
264
|
-
const proposal = { requestId, messageId: message.id, title: copy.title, summary: copy.summary, detail: copy.detail };
|
|
265
|
-
if (!automatic)
|
|
266
|
-
return proposal;
|
|
267
|
-
// The hidden receipt is persisted before the authority changes. The same
|
|
268
|
-
// validation and durable commit marker serve both automatic and human decisions.
|
|
269
|
-
const result = this.resolve({ botId: args.botId, threadId: args.threadId, requestId, behavior: "allow" });
|
|
270
|
-
if (result.state === "applied")
|
|
271
|
-
return { ...proposal, result };
|
|
272
|
-
throw new TighteningRequestError(result.state === "invalid" ? result.error : "The tightening could not be applied", result.state === "invalid" ? result.status : 409);
|
|
273
|
-
}
|
|
274
|
-
/** Claims a tightening card even after it was settled, so a duplicate
|
|
275
|
-
* click never re-applies an already-applied reduction. */
|
|
276
|
-
resolve(args) {
|
|
277
|
-
const message = this.store
|
|
278
|
-
.messagesFor(args.threadId)
|
|
279
|
-
.find((candidate) => candidate.card?.requestId === args.requestId && candidate.card.tighteningRequest);
|
|
280
|
-
const card = message?.card;
|
|
281
|
-
const payload = card?.tighteningRequest;
|
|
282
|
-
if (!message || !card || !payload)
|
|
283
|
-
return { claimed: false, state: "not_found" };
|
|
284
|
-
if (payload.requestId !== card.requestId) {
|
|
285
|
-
return { claimed: true, state: "invalid", error: "This tightening request does not match its card", status: 409 };
|
|
286
|
-
}
|
|
287
|
-
if (args.behavior !== "allow" && args.behavior !== "deny") {
|
|
288
|
-
return { claimed: true, state: "invalid", error: "Tightening confirmations must be confirmed or cancelled", status: 400 };
|
|
289
|
-
}
|
|
290
|
-
if (payload.botId !== args.botId || payload.threadId !== args.threadId) {
|
|
291
|
-
return { claimed: true, state: "invalid", error: "This tightening request belongs to another conversation", status: 403 };
|
|
292
|
-
}
|
|
293
|
-
if (card.answered)
|
|
294
|
-
return { claimed: true, state: "already_settled", behavior: card.answered };
|
|
295
|
-
let emergency = false;
|
|
296
|
-
let outstandingSkills = [];
|
|
297
|
-
try {
|
|
298
|
-
const target = this.store.bot(payload.targetBotId);
|
|
299
|
-
// The authority change and receipt share one durable write. If saving
|
|
300
|
-
// the card failed afterward, a retry only settles it; it never
|
|
301
|
-
// reapplies authority — but it does finish any skills a failed
|
|
302
|
-
// attempt left enabled, so a partial disable can never strand the card.
|
|
303
|
-
if (target?.lastTighteningRequestId === payload.requestId) {
|
|
304
|
-
const enabled = new Set(this.enabledSkills?.(payload.targetBotId) ?? []);
|
|
305
|
-
const remaining = (payload.intents.skills ?? []).filter((name) => enabled.has(name));
|
|
306
|
-
if (remaining.length && !this.disableSkill) {
|
|
307
|
-
throw new TighteningRequestError("Skill changes are not available in this workspace", 400);
|
|
308
|
-
}
|
|
309
|
-
const finished = this.applySkillDisables(payload.targetBotId, remaining);
|
|
310
|
-
if (!finished.ok) {
|
|
311
|
-
outstandingSkills = finished.outstanding;
|
|
312
|
-
throw new TighteningRequestError(finished.error, 409);
|
|
313
|
-
}
|
|
314
|
-
const settled = this.store.patchMessage(args.threadId, message.id, {
|
|
315
|
-
card: { ...card, answered: "allow", held: undefined, tighteningRequest: { ...payload, appliedAt: payload.appliedAt ?? this.now() } },
|
|
316
|
-
});
|
|
317
|
-
if (!settled)
|
|
318
|
-
throw new TighteningRequestError("This tightening confirmation card is no longer available", 409);
|
|
319
|
-
// The first attempt committed the authority change and the durable
|
|
320
|
-
// receipt, then died before it could settle the card or record
|
|
321
|
-
// history. This retry settles the card, so it also writes the row
|
|
322
|
-
// that attempt never reached — from the card's own frozen inputs,
|
|
323
|
-
// never the live bot: the revision guard proved live state equaled
|
|
324
|
-
// payload.before at apply, so the validated post-card state is the
|
|
325
|
-
// committed change, while a live snapshot could fold in later owner
|
|
326
|
-
// edits and attribute them to this card.
|
|
327
|
-
const recorded = validateTightening(payload.before, payload.intents);
|
|
328
|
-
if (recorded.ok) {
|
|
329
|
-
recordAuthorityChange(payload.targetBotId, "bot", `card:${message.id}`, payload.before, recorded.after);
|
|
330
|
-
}
|
|
331
|
-
return { claimed: true, state: "already_settled", behavior: "allow" };
|
|
332
|
-
}
|
|
333
|
-
if (args.behavior === "deny") {
|
|
334
|
-
this.store.patchMessage(args.threadId, message.id, { card: { ...card, answered: "deny", held: undefined } });
|
|
335
|
-
return { claimed: true, state: "denied" };
|
|
336
|
-
}
|
|
337
|
-
if (!target)
|
|
338
|
-
throw new TighteningRequestError(NO_SUCH_BOT, 404);
|
|
339
|
-
const crossBot = payload.targetBotId !== payload.botId;
|
|
340
|
-
if (crossBot && this.validateTarget) {
|
|
341
|
-
const refusal = this.validateTarget(payload.botId, payload.targetBotId);
|
|
342
|
-
if (refusal)
|
|
343
|
-
throw new TighteningRequestError(refusal, 404);
|
|
344
|
-
}
|
|
345
|
-
const current = this.snapshotOf(target);
|
|
346
|
-
if (tighteningRevision(current, target.lastTighteningRequestId) !== payload.expectedRevision) {
|
|
347
|
-
throw new TighteningRequestError(STALE, 409);
|
|
348
|
-
}
|
|
349
|
-
// Direction is re-validated against live state, not trusted from the
|
|
350
|
-
// card: a card that sat open can only ever apply a reduction.
|
|
351
|
-
const checked = validateTightening(current, payload.intents);
|
|
352
|
-
if (!checked.ok)
|
|
353
|
-
throw new TighteningRequestError(checked.error, 409);
|
|
354
|
-
emergency = payload.intents.approvalMode !== undefined &&
|
|
355
|
-
(current.approvalMode === "full" || current.approvalMode === "custom") &&
|
|
356
|
-
payload.intents.approvalMode === "ask";
|
|
357
|
-
if (this.targetBusy?.(target.id) && !emergency) {
|
|
358
|
-
if (payload.intents.approvalMode !== undefined) {
|
|
359
|
-
throw new TighteningRequestError("Stop this bot's turn before changing its approval level", 409);
|
|
360
|
-
}
|
|
361
|
-
if (payload.intents.mcpServers !== undefined) {
|
|
362
|
-
throw new TighteningRequestError("Stop this bot's turns before changing its MCP servers", 409);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
// The capability check runs before any mutation: a workspace that
|
|
366
|
-
// cannot touch skills must not half-apply the card.
|
|
367
|
-
if (payload.intents.skills?.length && !this.disableSkill) {
|
|
368
|
-
throw new TighteningRequestError("Skill changes are not available in this workspace", 400);
|
|
369
|
-
}
|
|
370
|
-
const patch = { lastTighteningRequestId: payload.requestId };
|
|
371
|
-
if (payload.intents.approvalMode !== undefined) {
|
|
372
|
-
patch.approvalMode = payload.intents.approvalMode;
|
|
373
|
-
patch.autoApprove = payload.intents.approvalMode === "auto";
|
|
374
|
-
patch.approvalGrant = undefined;
|
|
375
|
-
}
|
|
376
|
-
if (payload.intents.composio !== undefined)
|
|
377
|
-
patch.composio = false;
|
|
378
|
-
if (payload.intents.browser !== undefined)
|
|
379
|
-
patch.browser = false;
|
|
380
|
-
if (payload.intents.approvePeerComms !== undefined)
|
|
381
|
-
patch.approvePeerComms = true;
|
|
382
|
-
if (payload.intents.alwaysAllow !== undefined)
|
|
383
|
-
patch.alwaysAllow = [...checked.after.alwaysAllow];
|
|
384
|
-
if (payload.intents.mcpServers !== undefined)
|
|
385
|
-
patch.mcpServers = [...checked.after.mcpServers];
|
|
386
|
-
if (!this.store.patchBot(target.id, patch))
|
|
387
|
-
throw new TighteningRequestError(NO_SUCH_BOT, 404);
|
|
388
|
-
// Skills disable AFTER the receipt write. Each disable persists on
|
|
389
|
-
// its own, so a failure midway can leave part of the list done — but
|
|
390
|
-
// the receipt is already durable, which turns every retry into the
|
|
391
|
-
// branch above: finish the remainder, never reapply authority, and
|
|
392
|
-
// never leave the card permanently stale.
|
|
393
|
-
const skills = this.applySkillDisables(target.id, payload.intents.skills ?? []);
|
|
394
|
-
if (!skills.ok) {
|
|
395
|
-
outstandingSkills = skills.outstanding;
|
|
396
|
-
throw new TighteningRequestError(skills.error, 409);
|
|
397
|
-
}
|
|
398
|
-
const appliedAt = this.now();
|
|
399
|
-
const settled = this.store.patchMessage(args.threadId, message.id, {
|
|
400
|
-
card: { ...card, answered: "allow", held: undefined, tighteningRequest: { ...payload, appliedAt } },
|
|
401
|
-
});
|
|
402
|
-
if (!settled)
|
|
403
|
-
throw new TighteningRequestError("This tightening confirmation card is no longer available", 409);
|
|
404
|
-
// Same audit trail as a profile card: the proposing bot is the actor,
|
|
405
|
-
// the card id is the via, and the rows land in the one History section
|
|
406
|
-
// manual edits already write to. Fire-and-forget like every history row.
|
|
407
|
-
recordAuthorityChange(payload.targetBotId, "bot", `card:${message.id}`, current, checked.after);
|
|
408
|
-
const fields = INTENT_KEYS.filter((key) => payload.intents[key] !== undefined);
|
|
409
|
-
return { claimed: true, state: "applied", targetBotId: target.id, fields, ...(emergency ? { emergencyStop: true } : {}) };
|
|
410
|
-
}
|
|
411
|
-
catch (error) {
|
|
412
|
-
const status = error instanceof TighteningRequestError ? error.status : 400;
|
|
413
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
414
|
-
const saved = this.store.bot(payload.targetBotId)?.lastTighteningRequestId === payload.requestId;
|
|
415
|
-
const notice = outstandingSkills.length
|
|
416
|
-
? `Permissions tightened. Confirm again to finish disabling: ${redactSecretsInText(outstandingSkills.join(", "))}`
|
|
417
|
-
: card.dismissed && card.options.length === 0
|
|
418
|
-
? "Permissions tightened. Recording the operation receipt could not finish; the changes will not be applied again."
|
|
419
|
-
: "Permissions tightened. Confirm again to finish recording this decision; the changes will not be applied again.";
|
|
420
|
-
try {
|
|
421
|
-
this.store.patchMessage(args.threadId, message.id, {
|
|
422
|
-
card: { ...card, held: saved ? notice : redactSecretsInText(detail).slice(0, 500) },
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
catch { /* The durable receipt still permits a safe retry. */ }
|
|
426
|
-
if (saved)
|
|
427
|
-
return {
|
|
428
|
-
claimed: true, state: "applied", targetBotId: payload.targetBotId,
|
|
429
|
-
fields: INTENT_KEYS.filter((key) => payload.intents[key] !== undefined),
|
|
430
|
-
...(emergency ? { emergencyStop: true } : {}),
|
|
431
|
-
};
|
|
432
|
-
return { claimed: true, state: "invalid", error: detail, status };
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|