openmausbot 0.1.93 → 0.1.95
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-CKNR3SEW.js → DesktopViewer-BwWpc4RK.js} +1 -1
- package/dist/assets/{abnfDiagram-O67JEVCF-Ckrm1I1X.js → abnfDiagram-O67JEVCF-jsQkIqTn.js} +1 -1
- package/dist/assets/{arc-BBdBuCgB.js → arc-7kei4Hsu.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CDdA3Hjs.js → architectureDiagram-NJMV4G6O-CRWDFDMq.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-AUrsx1YR.js → blockDiagram-BEXU5L5S-CTKW_IKs.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-BEqQ0Unh.js → c4Diagram-YGBWAQC7-BBTW7-6M.js} +1 -1
- package/dist/assets/channel-DS2NbT5x.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CEEuvd4C.js → chunk-2Q5K7J3B-M_Ox-MkD.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-DyANCiHl.js → chunk-3FUC2YCW-D2YiN-Ud.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-Dx_Slpw9.js → chunk-5VM5RSS4-BmVYJ0f8.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-Dl7h7iiD.js → chunk-GTNCS2PH-BtiLVVnZ.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BLx07SPb.js → chunk-GWA4HPMP-DFYSb6J6.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BMxDj3n7.js → chunk-JWPE2WC7-mknKBzgV.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CN66PT4H.js → chunk-NETBCI7D-DgSYGiOu.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-6HwhsDKw.js → chunk-WEXAMYUT-C7ZvuguH.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-gWnLt7SN.js → chunk-XXDRQBXY-CPqxINpA.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-CMS5l2ea.js → classDiagram-v2-NBCMYWYE-Cd6xfJaC.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-CV7sQwIG.js → cose-bilkent-JH36ORCC-BSXY5qRh.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-CSyl-7vl.js → cynefin-OW5HDTMX-D_lhEMj1.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-BCLKL6az.js → cynefinDiagram-VND7K2PF-Da9FrFwk.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DiaZs6S1.js → dagre-6A5THRUB-T77jMOXe.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-BeTtmt3I.js → diagram-22UHCM2B-Dkj96X5E.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CsdBG-QQ.js → diagram-3UASUU5V-CMi9zlD_.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BczvdbR1.js → diagram-ATOU4E4O-CXK6YUfS.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DHTTTMhi.js → diagram-CDSNMT55-BE7B1u9Q.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-RTjHyd8z.js → diagram-MLGK6HIB-CCMQphKr.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-BDenHEH_.js → diagram-MPIPVDR6-iHJ_-tGH.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-BSiRSbtL.js → ebnfDiagram-ZINNZB2B-DONsCWQ7.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-BCAElss-.js → elk-276RUBZZ-CU_WOaXk.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-C4_1ZPJe.js → erDiagram-OPXOYQCR-BeYwM04G.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-Bcb4Z75S.js → flowDiagram-KWPJA3E3-Bdw9DqEl.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-BiPOzxzb.js → ganttDiagram-FUAMR5RP-C9iwq3qm.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-B1wtF3ZS.js → gitGraphDiagram-X574FWY7-Dx-VWzUg.js} +1 -1
- package/dist/assets/{index-BkYwilkG.js → index-KygHRE3E.js} +1 -1
- package/dist/assets/index-NF7XCHAy.css +1 -0
- package/dist/assets/index-X_5ydXIE.js +610 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-A19No71U.js → infoDiagram-VRGFBTTK-BG7uaEd4.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BQsqJ_cV.js → ishikawaDiagram-OU5B5YK6-D72WAXnM.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-DAO0pVan.js → journeyDiagram-ZHPQQLJL-DKH1gjYT.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-NepmKEe2.js → kanban-definition-PNTS6WVX-3upm9JUT.js} +1 -1
- package/dist/assets/{linear-bq1PTDTW.js → linear-CcR4pc0-.js} +1 -1
- package/dist/assets/{mermaid.core-s4LTbQle.js → mermaid.core-C6yU5FSS.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-LtdbgHFH.js → mindmap-definition-NLK3R4M7-DOI2xEuv.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-tdbaCxiK.js → pegDiagram-GJSIUBJH-BVWJSgLd.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-DvkbpW8l.js → pieDiagram-5QR66LMP-jtbYezhq.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Do4KaH-E.js → quadrantDiagram-O4NWA36T-g0u8mtPX.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BQWULOAs.js → railroadDiagram-XR7U4H2S-k8689Wny.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-w9kei1FU.js → requirementDiagram-PLB6GJNP-DsJ22PDt.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-D2gRy0e8.js → sankeyDiagram-IPEJSGJF-CneWBRgG.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-a2sO5_Eo.js → sequenceDiagram-PO4LG4MO-DWsFCWap.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CiPJmFEB.js → sizeCapture-INFHLROL-Bvf0YE3S.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DAkUUlL7.js → stateDiagram-v2-GCMORJYK-Bxb3Iei8.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DANf2R8Y.js → swimlanes-2SLR337P-DLkk7W4A.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-C6bZtBXk.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-BKpQmzrT.js → timeline-definition-EJHVYXUP-KR9bIHYC.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BtI7qLN-.js → usecaseDiagram-POWQR4AR-DfsOrj8t.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-gXJXrAG_.js → vennDiagram-UO4OBE2U-DRSTzG3h.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-GU8Po5-n.js → wardleyDiagram-VNRHLVJA-DtEdhPgT.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BF4J30g6.js → xychartDiagram-PMCCYNJV-B-XhTxjG.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/cloud-home-start.js +3 -1
- package/dist-server/companion/src/routes.js +60 -0
- package/dist-server/container-mcp.js +94 -77
- package/dist-server/drivers/agents-proxy.js +96 -39
- package/dist-server/drivers/pi-mcp-extension.ts +245 -299
- package/dist-server/enterprise/server/index.js +3 -1
- package/dist-server/{browser-proxy.js → harness-mcp-proxy.js} +35 -19
- package/dist-server/index.js +18242 -13645
- package/dist-server/local-computer.js +3 -1
- package/dist-server/mcp-gate.js +147 -19
- package/dist-server/mcp-remote-proxy.js +437 -0
- package/dist-server/openmausbot.js +9618 -10058
- package/dist-server/pair-cli.js +9618 -10058
- package/dist-server/permission-proxy.js +65 -1
- package/dist-server/proxy-paths.js +3 -1
- package/dist-server/server/account-signin.js +0 -2
- package/dist-server/server/activity.js +201 -0
- package/dist-server/server/auto-approve.js +39 -11
- package/dist-server/server/automatic-recovery.js +1 -1
- package/dist-server/server/boat-create-idempotency.js +0 -7
- package/dist-server/server/boat-delete-journal.js +0 -4
- package/dist-server/server/bot-package.js +1 -19
- package/dist-server/server/browser-live.js +14 -5
- package/dist-server/server/browser-runtime.js +7 -2
- package/dist-server/server/channel-queue.js +4 -1
- package/dist-server/server/chief-of-staff.js +2 -3
- package/dist-server/server/cli.js +32 -12
- package/dist-server/server/cloud-backend.js +0 -4
- package/dist-server/server/cloud-computer-tools.js +110 -0
- package/dist-server/server/cloud-home.js +0 -1
- package/dist-server/server/cloud-lending.js +0 -3
- package/dist-server/server/cloud-move-http.js +9 -3
- package/dist-server/server/cloud-move.js +18 -0
- package/dist-server/server/composio.js +31 -2
- package/dist-server/server/computer-control.js +0 -15
- package/dist-server/server/computer-observation.js +0 -74
- package/dist-server/server/computer-wait.js +33 -13
- package/dist-server/server/config.js +73 -11
- package/dist-server/server/container-computer.js +7 -19
- package/dist-server/server/contracts.js +9 -0
- package/dist-server/server/control-client.js +4 -10
- package/dist-server/server/decision-log.js +9 -6
- package/dist-server/server/delegations.js +47 -22
- package/dist-server/server/drivers/acp/core.js +64 -13
- package/dist-server/server/drivers/acp/grok.js +208 -3
- package/dist-server/server/drivers/agents-call.js +70 -29
- package/dist-server/server/drivers/agents-catalog.js +34 -12
- package/dist-server/server/drivers/agents-proxy.js +8 -30
- package/dist-server/server/drivers/antigravity.js +1 -4
- package/dist-server/server/drivers/boatagent.js +25 -4
- package/dist-server/server/drivers/cerebras.js +3 -3
- package/dist-server/server/drivers/chat-mcp-tools.js +29 -19
- package/dist-server/server/drivers/claude.js +33 -19
- package/dist-server/server/drivers/codex.js +84 -10
- package/dist-server/server/drivers/openai-chat.js +14 -9
- package/dist-server/server/drivers/pi.js +48 -4
- package/dist-server/server/drivers/retry.js +8 -4
- package/dist-server/server/engine-install.js +1 -5
- package/dist-server/server/environment.js +3 -0
- package/dist-server/server/guarded-requests.js +2 -1
- package/dist-server/server/harness/registry.js +0 -1
- package/dist-server/server/{browser-proxy.js → harness-mcp-proxy.js} +40 -20
- package/dist-server/server/index.js +1929 -507
- package/dist-server/server/lending-memory.js +12 -8
- package/dist-server/server/live-call-controller.js +868 -0
- package/dist-server/server/live-call.js +216 -0
- package/dist-server/server/mcp-gate-config.js +43 -12
- package/dist-server/server/mcp-gate.js +127 -25
- package/dist-server/server/mcp-http.js +46 -8
- package/dist-server/server/mcp-remote-proxy.js +108 -0
- package/dist-server/server/memory-entries.js +38 -8
- package/dist-server/server/memory-organize.js +1 -8
- package/dist-server/server/memory-topics.js +4 -4
- package/dist-server/server/memory-upkeep.js +13 -15
- package/dist-server/server/message-file.js +5 -9
- package/dist-server/server/new-bot-defaults.js +3 -0
- package/dist-server/server/outbound-counts.js +79 -0
- package/dist-server/server/outbound-requests.js +60 -0
- package/dist-server/server/package-import.js +1 -1
- package/dist-server/server/peer-delivery.js +1 -0
- package/dist-server/server/peer-provenance.js +4 -2
- package/dist-server/server/permission-proxy.js +15 -1
- package/dist-server/server/plan-usage.js +797 -0
- package/dist-server/server/proxy-paths.js +3 -1
- package/dist-server/server/redact.js +2 -0
- package/dist-server/server/request-auth.js +3 -2
- package/dist-server/server/room-handoffs.js +5 -13
- package/dist-server/server/routes/live.js +98 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +5 -1
- package/dist-server/server/setup-mode.js +0 -2
- package/dist-server/server/skill-learn.js +0 -1
- package/dist-server/server/skill-library.js +195 -3
- package/dist-server/server/skills-library-migration.js +235 -0
- package/dist-server/server/skills.js +66 -3
- package/dist-server/server/steer-queue.js +13 -1
- package/dist-server/server/store.js +41 -5
- package/dist-server/server/surface.js +74 -23
- package/dist-server/server/system-prompt.js +4 -4
- package/dist-server/server/team-backup.js +2 -1
- package/dist-server/server/team-memory.js +209 -0
- package/dist-server/server/thread-events.js +3 -3
- package/dist-server/server/tunnel.js +20 -1
- package/dist-server/server/turn-log.js +10 -0
- package/dist-server/server/turn-resources.js +124 -0
- package/dist-server/server/vps-computer.js +4 -14
- package/dist-server/server/workspace.js +85 -47
- package/dist-server/shared/activity.js +3 -0
- package/dist-server/shared/ask-question.js +13 -0
- package/dist-server/shared/bot-avatar.js +8 -0
- package/dist-server/shared/call-consent.js +28 -0
- package/dist-server/shared/connector-scopes.js +78 -0
- package/dist-server/shared/live-approval.js +124 -0
- package/dist-server/shared/live-call.js +176 -0
- package/dist-server/shared/outbound.js +128 -0
- package/dist-server/shared/skill-md.js +10 -0
- package/dist-server/shared/team-backup.js +3 -0
- package/dist-server/shared/tool-scope-support.js +25 -0
- package/dist-server/shared/tool-scope.js +136 -0
- package/dist-server/shared/tool-surface.js +3 -2
- package/dist-server/src/lib/thread-refs.js +406 -0
- package/dist-server/vps-container-mcp.js +94 -77
- package/dist-server/workspace-backup.worker.js +89 -100
- package/enterprise/server/index.js +3 -1
- package/package.json +1 -1
- package/dist/assets/channel-BU94VpxE.js +0 -1
- package/dist/assets/index-Biji-bfL.css +0 -1
- package/dist/assets/index-DZlhoES0.js +0 -597
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CdHxG8aM.js +0 -8
- package/dist-server/server/drivers/chat-boat-tools.js +0 -90
- package/dist-server/server/drivers/grokagent.js +0 -4
- package/dist-server/server/openmaus-status-capsule.js +0 -329
|
@@ -812,7 +812,7 @@ export async function runBrowser(options, io = defaultIo()) {
|
|
|
812
812
|
io.error("on Linux, install Chrome's system libraries with `sudo openmausbot browser install --with-deps`, then retry `openmausbot browser install` as the user running serve");
|
|
813
813
|
return 1;
|
|
814
814
|
}
|
|
815
|
-
io.log("browser installed for this user and data directory; run serve as the same user, then enable it under Settings →
|
|
815
|
+
io.log("browser installed for this user and data directory; run serve as the same user, then enable it under Settings → Computers and per bot");
|
|
816
816
|
if (process.platform === "linux" && options.withDeps)
|
|
817
817
|
io.log("if serve runs as another user, run `openmausbot browser install` from that user's login shell too");
|
|
818
818
|
return 0;
|
|
@@ -834,8 +834,13 @@ export function serverEntry(here = HERE) {
|
|
|
834
834
|
}
|
|
835
835
|
/** Everything `--tunnel` needs before the server starts, or the one reason
|
|
836
836
|
* it cannot have it. Fails closed: no silent fallback to a local-only server. */
|
|
837
|
-
async function planTunnel(options, log) {
|
|
837
|
+
async function planTunnel(options, log, recovery) {
|
|
838
838
|
let access = null;
|
|
839
|
+
let account = null;
|
|
840
|
+
const fail = (error) => {
|
|
841
|
+
account?.service.dispose();
|
|
842
|
+
return { error };
|
|
843
|
+
};
|
|
839
844
|
const credential = fleetCredential();
|
|
840
845
|
if (credential) {
|
|
841
846
|
// A fleet-started container: the credential is the whole identity.
|
|
@@ -848,36 +853,41 @@ async function planTunnel(options, log) {
|
|
|
848
853
|
}
|
|
849
854
|
}
|
|
850
855
|
else {
|
|
851
|
-
|
|
856
|
+
account = createTunnelAccount({ dataDir: options.dataDir, version: serverVersion(), recovery });
|
|
852
857
|
if (account.credentials.status === "unavailable")
|
|
853
|
-
return
|
|
858
|
+
return fail(`${account.credentials.file} exists but could not be read; fix or remove it`);
|
|
854
859
|
if (!describeTunnelAccount(account.credentials.read()).email) {
|
|
855
|
-
return
|
|
860
|
+
return fail("no account on this machine yet: run `openmausbot login` first, then `openmausbot serve --tunnel`");
|
|
856
861
|
}
|
|
857
862
|
// A fresh connector token when the control plane answers; the saved one otherwise.
|
|
858
863
|
try {
|
|
859
864
|
const state = await account.service.retry();
|
|
860
|
-
|
|
861
|
-
|
|
865
|
+
const saved = tunnelAccess(account.credentials.read()) !== null;
|
|
866
|
+
// Said even when a saved address exists: an expired sign-in means this
|
|
867
|
+
// machine can no longer repair that address on its own.
|
|
868
|
+
if (state.status === "signed-out")
|
|
869
|
+
log(`tunnel: the sign-in on this machine expired; run \`openmausbot login\` to renew it${saved ? " (serving the saved address until then)" : ""}`);
|
|
870
|
+
else if (state.message)
|
|
871
|
+
log(`tunnel: ${state.message}${saved ? " Serving the saved address for now." : ""}`);
|
|
862
872
|
}
|
|
863
873
|
catch (error) {
|
|
864
874
|
log(`tunnel: control plane not reachable right now (${message(error)}); using the saved address`);
|
|
865
875
|
}
|
|
866
876
|
access = tunnelAccess(account.credentials.read());
|
|
867
877
|
if (!access)
|
|
868
|
-
return
|
|
878
|
+
return fail("this machine has no public address; run `openmausbot login` again");
|
|
869
879
|
}
|
|
870
880
|
let binary;
|
|
871
881
|
try {
|
|
872
882
|
binary = await ensureCloudflared({ dataDir: options.dataDir, log });
|
|
873
883
|
}
|
|
874
884
|
catch (error) {
|
|
875
|
-
return
|
|
885
|
+
return fail(`--tunnel: ${message(error)}`);
|
|
876
886
|
}
|
|
877
887
|
const guardian = guardianEntry();
|
|
878
888
|
if (!guardian)
|
|
879
|
-
return
|
|
880
|
-
return { access, binary, guardian, origin: createTunnelOrigin() };
|
|
889
|
+
return fail("--tunnel: the connector guardian is missing from this install");
|
|
890
|
+
return { access, binary, guardian, origin: createTunnelOrigin(), account };
|
|
881
891
|
}
|
|
882
892
|
export async function runServe(options, log = console.log) {
|
|
883
893
|
const { browserEngineStatus, describeBrowserEngine } = await import("./browser-engine.js");
|
|
@@ -896,8 +906,11 @@ export async function runServe(options, log = console.log) {
|
|
|
896
906
|
tailscale = probe.status;
|
|
897
907
|
}
|
|
898
908
|
let plan = null;
|
|
909
|
+
// The tunnel this process serves, once it runs; the account service's
|
|
910
|
+
// background recovery reads it (see TunnelRecovery).
|
|
911
|
+
const serving = { tunnel: null };
|
|
899
912
|
if (options.tunnel) {
|
|
900
|
-
const planned = await planTunnel(options, log);
|
|
913
|
+
const planned = await planTunnel(options, log, { running: () => serving.tunnel });
|
|
901
914
|
if ("error" in planned) {
|
|
902
915
|
console.error(planned.error);
|
|
903
916
|
return 1;
|
|
@@ -991,6 +1004,7 @@ export async function runServe(options, log = console.log) {
|
|
|
991
1004
|
catch (error) {
|
|
992
1005
|
if ((tailscaleServing || (startupCancelled && tailscaleAttempted)) && tailscale)
|
|
993
1006
|
await tailscaleServeOff(tailscale).catch(() => undefined);
|
|
1007
|
+
plan?.account?.service.dispose();
|
|
994
1008
|
if (plan)
|
|
995
1009
|
cleanupTunnelOrigin(plan.origin);
|
|
996
1010
|
if (error instanceof SetupCancelled) {
|
|
@@ -1015,6 +1029,8 @@ export async function runServe(options, log = console.log) {
|
|
|
1015
1029
|
let stopping = null;
|
|
1016
1030
|
const stop = () => {
|
|
1017
1031
|
stopping ??= (async () => {
|
|
1032
|
+
serving.tunnel = null;
|
|
1033
|
+
plan?.account?.service.dispose();
|
|
1018
1034
|
// The gateway and the edge stop accepting before the server they forward to goes away.
|
|
1019
1035
|
if (tunnel)
|
|
1020
1036
|
await tunnel.stop().catch(() => undefined);
|
|
@@ -1083,6 +1099,10 @@ export async function runServe(options, log = console.log) {
|
|
|
1083
1099
|
onState: (state) => log(describeTunnelState(state, plan.access.endpoint)),
|
|
1084
1100
|
});
|
|
1085
1101
|
tunnel.started.catch((error) => log(`tunnel: ${message(error)}`));
|
|
1102
|
+
// From here on the endpoint is checked every 15 minutes; one reclaimed
|
|
1103
|
+
// while this machine was offline is re-created behind the same address.
|
|
1104
|
+
serving.tunnel = tunnel;
|
|
1105
|
+
void plan.account?.service.restore().catch(() => undefined);
|
|
1086
1106
|
}
|
|
1087
1107
|
log("");
|
|
1088
1108
|
log(`OpenMausBot is running on http://127.0.0.1:${options.port}${publicUrl ? `, reachable at ${publicUrl}` : ""}`);
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
export const CLOUD_BACKEND_CHANGE_ERROR = "stop the active turn before changing the cloud backend";
|
|
2
|
-
export const VPS_ALIAS_CHANGE_ERROR = "stop the active VPS turn before changing the SSH config alias";
|
|
3
2
|
export const BOAT_ACCOUNT_RESOURCES_ERROR = "remove this installation's cloud computers before changing or clearing the Boat account";
|
|
4
3
|
export const VPS_ALIAS_RESOURCES_ERROR = "remove this installation's VPS computers before changing or clearing the SSH config alias";
|
|
5
4
|
export function cloudBackendChangeError(botBusy, activeVpsThread) {
|
|
6
5
|
return botBusy || activeVpsThread ? CLOUD_BACKEND_CHANGE_ERROR : null;
|
|
7
6
|
}
|
|
8
|
-
export function vpsAliasChangeError(currentAlias, nextAlias, activeVpsThread) {
|
|
9
|
-
return activeVpsThread && currentAlias !== nextAlias ? VPS_ALIAS_CHANGE_ERROR : null;
|
|
10
|
-
}
|
|
11
7
|
/** An old Boat account may be detached only when it owns no local resources.
|
|
12
8
|
* Token rotation is safe when the replacement credential proves access to
|
|
13
9
|
* the exact same provider identities. */
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { isolatedRemoteCommand, runCommand, screenshotBoat } from "./boat.js";
|
|
2
|
+
import { CONTROL_REFUSAL_PLAIN } from "./control-client.js";
|
|
3
|
+
import { compileToolSchema } from "./drivers/chat-mcp-tools.js";
|
|
4
|
+
const coordinate = { type: "integer", minimum: 0, maximum: 32767 };
|
|
5
|
+
const tool = (name, description, properties = {}, required = []) => ({ name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } });
|
|
6
|
+
export const CLOUD_COMPUTER_TOOLS = [
|
|
7
|
+
tool("screenshot", "See the assigned cloud desktop at native pixel resolution."),
|
|
8
|
+
tool("get_screen_size", "Get the cloud desktop width and height."),
|
|
9
|
+
tool("click", "Click a point on the cloud desktop.", { x: coordinate, y: coordinate, button: { type: "string", enum: ["left", "middle", "right"] }, count: { type: "integer", minimum: 1, maximum: 3 } }, ["x", "y"]),
|
|
10
|
+
tool("move", "Move the cloud mouse pointer.", { x: coordinate, y: coordinate }, ["x", "y"]),
|
|
11
|
+
tool("drag", "Drag on the cloud desktop.", { x: coordinate, y: coordinate, to_x: coordinate, to_y: coordinate }, ["x", "y", "to_x", "to_y"]),
|
|
12
|
+
tool("type_text", "Type Unicode text into the focused cloud application.", { text: { type: "string", maxLength: 4000 } }, ["text"]),
|
|
13
|
+
tool("key_press", "Press an X11 key or shortcut.", { key: { type: "string", pattern: "^[A-Za-z0-9_+]+$", minLength: 1, maxLength: 100 } }, ["key"]),
|
|
14
|
+
tool("scroll", "Scroll at a point on the cloud desktop.", { x: coordinate, y: coordinate, direction: { type: "string", enum: ["up", "down", "left", "right"] }, amount: { type: "integer", minimum: 1, maximum: 30 } }, ["x", "y", "direction"]),
|
|
15
|
+
tool("open_url", "Request opening an HTTP or HTTPS URL in the cloud desktop browser; inspect the screen to confirm it loaded.", { url: { type: "string", pattern: "^https?://", maxLength: 2000 } }, ["url"]),
|
|
16
|
+
tool("exec", "Run a shell command on the assigned cloud computer.", { command: { type: "string", minLength: 1, maxLength: 4000 } }, ["command"]),
|
|
17
|
+
];
|
|
18
|
+
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
19
|
+
const buttons = { left: 1, middle: 2, right: 3 };
|
|
20
|
+
const scrollButtons = { up: 4, down: 5, left: 6, right: 7 };
|
|
21
|
+
let validators;
|
|
22
|
+
/** Why a call cannot run, or null. Checked before anything else touches the
|
|
23
|
+
* call: x and y reach the remote shell unquoted, so only the advertised
|
|
24
|
+
* integer schema stands between a model's arguments and a command line. */
|
|
25
|
+
export function cloudComputerCallProblem(name, args) {
|
|
26
|
+
validators ??= new Map(CLOUD_COMPUTER_TOOLS.map(entry => [entry.name, compileToolSchema(entry.inputSchema)]));
|
|
27
|
+
const validate = typeof name === "string" ? validators.get(name) : undefined;
|
|
28
|
+
if (!validate)
|
|
29
|
+
return "Unknown cloud computer tool. Use one of the advertised computer tools.";
|
|
30
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || !validate(args)) {
|
|
31
|
+
return "Tool arguments do not match the advertised input schema; use its required fields and types.";
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** One tool call on the given Boat with the server's own Boat account. The
|
|
36
|
+
* caller has already checked the arguments and the person's control. */
|
|
37
|
+
export async function runCloudComputerTool(cfg, boxId, name, args, signal) {
|
|
38
|
+
const problem = cloudComputerCallProblem(name, args);
|
|
39
|
+
if (problem)
|
|
40
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
if (name === "screenshot") {
|
|
43
|
+
const shot = await screenshotBoat(cfg, "", boxId, { signal, nativeSize: true });
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
return { content: [{ type: "image", mimeType: "image/jpeg", data: shot.png }] };
|
|
46
|
+
}
|
|
47
|
+
const a = args;
|
|
48
|
+
const mouse = `xdotool mousemove --sync ${a.x} ${a.y}`;
|
|
49
|
+
let command;
|
|
50
|
+
switch (name) {
|
|
51
|
+
case "get_screen_size":
|
|
52
|
+
command = "xdotool getdisplaygeometry";
|
|
53
|
+
break;
|
|
54
|
+
case "click":
|
|
55
|
+
command = `${mouse} click --repeat ${a.count ?? 1} --delay 100 ${buttons[String(a.button ?? "left")]}`;
|
|
56
|
+
break;
|
|
57
|
+
case "move":
|
|
58
|
+
command = mouse;
|
|
59
|
+
break;
|
|
60
|
+
case "drag":
|
|
61
|
+
command = `${mouse} mousedown 1 mousemove --sync ${a.to_x} ${a.to_y} mouseup 1`;
|
|
62
|
+
break;
|
|
63
|
+
case "type_text":
|
|
64
|
+
command = `printf %s ${quote(Buffer.from(String(a.text)).toString("base64"))} | base64 -d | xclip -selection clipboard && xdotool key --clearmodifiers ctrl+v`;
|
|
65
|
+
break;
|
|
66
|
+
case "key_press":
|
|
67
|
+
command = `xdotool key --clearmodifiers ${quote(String(a.key))}`;
|
|
68
|
+
break;
|
|
69
|
+
case "scroll":
|
|
70
|
+
command = `${mouse} click --repeat ${a.amount ?? 3} --delay 80 ${scrollButtons[String(a.direction)]}`;
|
|
71
|
+
break;
|
|
72
|
+
case "open_url":
|
|
73
|
+
command = `nohup xdg-open ${quote(String(a.url))} >/dev/null 2>&1 </dev/null &`;
|
|
74
|
+
break;
|
|
75
|
+
case "exec":
|
|
76
|
+
command = String(a.command);
|
|
77
|
+
break;
|
|
78
|
+
default: return { isError: true, content: [{ type: "text", text: "Unknown cloud computer tool." }] };
|
|
79
|
+
}
|
|
80
|
+
const result = await runCommand(cfg, boxId, isolatedRemoteCommand(command), { signal });
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
if (name === "open_url" && result.ok)
|
|
83
|
+
return { content: [{ type: "text", text: "Browser launch requested. Page loading is not confirmed; inspect the screen before continuing." }] };
|
|
84
|
+
return { isError: !result.ok, content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
85
|
+
}
|
|
86
|
+
/** One JSON-RPC request from harness-mcp-proxy, answered with its MCP
|
|
87
|
+
* `result`. The order is the safety argument: arguments are checked before
|
|
88
|
+
* the control gate (which may claim the seat), a person's control refuses
|
|
89
|
+
* the call before anything reaches the Boat, and the turn's capability must
|
|
90
|
+
* still be live before and after the Boat acts. */
|
|
91
|
+
export async function cloudComputerRpc(request, deps) {
|
|
92
|
+
if (request?.method === "tools/list") {
|
|
93
|
+
deps.assertActive();
|
|
94
|
+
return { tools: CLOUD_COMPUTER_TOOLS };
|
|
95
|
+
}
|
|
96
|
+
if (request?.method !== "tools/call")
|
|
97
|
+
throw Object.assign(new Error("unsupported computer method"), { status: 400 });
|
|
98
|
+
const params = (request.params && typeof request.params === "object" ? request.params : {});
|
|
99
|
+
const args = params.arguments ?? {};
|
|
100
|
+
const problem = cloudComputerCallProblem(params.name, args);
|
|
101
|
+
if (problem)
|
|
102
|
+
return { isError: true, content: [{ type: "text", text: problem }] };
|
|
103
|
+
const control = await deps.gate();
|
|
104
|
+
deps.assertActive();
|
|
105
|
+
if (control.held)
|
|
106
|
+
return { isError: true, content: [{ type: "text", text: control.blockedReason || CONTROL_REFUSAL_PLAIN }] };
|
|
107
|
+
const result = await runCloudComputerTool(deps.cfg, deps.boxId, params.name, args, deps.signal);
|
|
108
|
+
deps.assertActive();
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
@@ -26,7 +26,6 @@ import { join } from "node:path";
|
|
|
26
26
|
import { writeFileAtomic } from "./atomic.js";
|
|
27
27
|
import { hostedWorkspaceConfigured } from "./enterprise.js";
|
|
28
28
|
import { formatPairingCode } from "./sessions.js";
|
|
29
|
-
export const CLOUD_HOME_CONTRACT_VERSION = 1;
|
|
30
29
|
/** Any of these switches the server into Cloud home mode; then all are required. */
|
|
31
30
|
export const CLOUD_HOME_KEYS = ["OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_BOOTSTRAP_SECRET"];
|
|
32
31
|
/** A platform model gateway's settings. A Cloud home never uses them: given
|
|
@@ -21,9 +21,6 @@ import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
import { writeFileAtomic } from "./atomic.js";
|
|
23
23
|
const personOf = (line) => line?.role === "user" && !line.peerAsk ? line.sender?.id : undefined;
|
|
24
|
-
export function cloudHomeTurnMayLend(turn) {
|
|
25
|
-
return cloudHomeLendingRefusal(turn) === null;
|
|
26
|
-
}
|
|
27
24
|
export function cloudHomeLendingRefusal(turn) {
|
|
28
25
|
const { request } = turn;
|
|
29
26
|
if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { backupsBytes, beginUpload, CLOUD_MOVE_MAX_BYTES, CLOUD_MOVE_MAX_PART_BYTES, CLOUD_MOVE_PART_BYTES, completedUpload, discardNextPreviousCloud, discardUpload, forgetMoveRestore, freeVolumeBytes, isEmptyWorkspace, moveSpaceNeeded, noteMoveRestore, prepareNextPreviousCloud, previousCloud, previousCloudArchiveBytes, removeMoveFiles, stagePreviousCloud, tidyCloudMoveStorage, uploadStatus, validUploadDeclaration, workspaceContents, workspaceMoveSize, writeUploadPart, } from "./cloud-move.js";
|
|
2
|
+
import { backupsBytes, beginUpload, CLOUD_MOVE_MAX_BYTES, CLOUD_MOVE_MAX_PART_BYTES, CLOUD_MOVE_PART_BYTES, completedUpload, discardNextPreviousCloud, discardUpload, enabledRoutineCount, forgetMoveRestore, freeVolumeBytes, isEmptyWorkspace, moveSpaceNeeded, noteMoveRestore, prepareNextPreviousCloud, previousCloud, previousCloudArchiveBytes, removeMoveFiles, stagePreviousCloud, tidyCloudMoveStorage, totalVolumeBytes, uploadStatus, validUploadDeclaration, workspaceContents, workspaceMoveSize, writeUploadPart, } from "./cloud-move.js";
|
|
3
3
|
import { commitPendingWorkspaceRestore, createWorkspaceBackup, stageWorkspaceBackup } from "./workspace-backup.js";
|
|
4
4
|
export const CLOUD_MOVE_PREFIX = "/api/cloud-move";
|
|
5
5
|
const failure = (message, status) => Object.assign(new Error(message), { status });
|
|
6
6
|
const HELD_CACHE_MS = 30_000;
|
|
7
7
|
export function createCloudMoveRoutes(options) {
|
|
8
8
|
const freeBytes = options.freeBytes ?? freeVolumeBytes;
|
|
9
|
+
const volumeBytes = (path) => { try {
|
|
10
|
+
return (options.volumeBytes ?? totalVolumeBytes)(path);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
} };
|
|
9
15
|
// A move's restore that startup has installed keeps no safety copy or
|
|
10
16
|
// staged files; an abandoned upload goes after a day.
|
|
11
17
|
if (options.cloudHome) {
|
|
@@ -113,7 +119,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
113
119
|
if (!auth.scopes.includes("admin"))
|
|
114
120
|
throw failure("Only the owner can move a workspace.", 403);
|
|
115
121
|
if (method === "GET" && path === `${CLOUD_MOVE_PREFIX}/estimate`) {
|
|
116
|
-
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
|
|
122
|
+
json(res, 200, { ...workspaceContents(options.dataDir), ...workspaceMoveSize(options.dataDir), routines: enabledRoutineCount(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES });
|
|
117
123
|
return true;
|
|
118
124
|
}
|
|
119
125
|
if (!options.cloudHome)
|
|
@@ -126,7 +132,7 @@ export function createCloudMoveRoutes(options) {
|
|
|
126
132
|
if (!held || held.at + HELD_CACHE_MS < Date.now())
|
|
127
133
|
held = { at: Date.now(), bytes: backupsBytes(options.dataDir) };
|
|
128
134
|
json(res, 200, {
|
|
129
|
-
contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
|
|
135
|
+
contents, empty: isEmptyWorkspace(contents), freeBytes: freeBytes(options.dataDir), volumeBytes: volumeBytes(options.dataDir), maxBytes: CLOUD_MOVE_MAX_BYTES,
|
|
130
136
|
partBytes: CLOUD_MOVE_PART_BYTES, upload: uploadStatus(options.dataDir), previous: previousCloud(options.dataDir), job,
|
|
131
137
|
// What backups and the previous Cloud hold on the volume.
|
|
132
138
|
heldBytes: held.bytes,
|
|
@@ -96,6 +96,24 @@ export function freeVolumeBytes(path) {
|
|
|
96
96
|
const free = disk.bavail * disk.bsize;
|
|
97
97
|
return free > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(free);
|
|
98
98
|
}
|
|
99
|
+
/** The whole volume, so the app can tell how far a disk that grows may still grow. */
|
|
100
|
+
export function totalVolumeBytes(path) {
|
|
101
|
+
const disk = statfsSync(path, { bigint: true });
|
|
102
|
+
const total = disk.blocks * disk.bsize;
|
|
103
|
+
return total > BigInt(Number.MAX_SAFE_INTEGER) ? Number.MAX_SAFE_INTEGER : Number(total);
|
|
104
|
+
}
|
|
105
|
+
/** Routines switched on in this workspace. A move brings them paused, so the
|
|
106
|
+
* app says how many to turn on there. */
|
|
107
|
+
export function enabledRoutineCount(dataDir) {
|
|
108
|
+
try {
|
|
109
|
+
const value = JSON.parse(readFileSync(join(dataDir, "routines.json"), "utf8"));
|
|
110
|
+
const routines = value && typeof value === "object" ? value.routines : undefined;
|
|
111
|
+
return Array.isArray(routines) ? routines.filter((routine) => routine && typeof routine === "object" && routine.enabled !== false).length : 0;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
99
117
|
/** Peak extra space a move of `upload` bytes needs: the upload, its decrypted
|
|
100
118
|
* copy and its staged files exist together while it is checked; the restore
|
|
101
119
|
* then installs from the staged copy. Replacing a workspace first backs it up
|
|
@@ -244,9 +244,35 @@ function trustedAuthUrl(value, slug) {
|
|
|
244
244
|
}
|
|
245
245
|
return url.toString();
|
|
246
246
|
}
|
|
247
|
+
/** Composio's own hosts are always trusted. A backend the operator pointed
|
|
248
|
+
* the app at explicitly (OMB_COMPOSIO_API — a dev or test stub) may hand back
|
|
249
|
+
* a Session on its own origin, since the API itself was already trusted that
|
|
250
|
+
* far; any other host is refused. */
|
|
251
|
+
export function trustedSessionMcpUrl(value) {
|
|
252
|
+
let mcp;
|
|
253
|
+
try {
|
|
254
|
+
mcp = new URL(value);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
if (mcp.protocol === "https:" && (mcp.hostname === "composio.dev" || mcp.hostname.endsWith(".composio.dev")))
|
|
260
|
+
return true;
|
|
261
|
+
if (mcp.protocol !== "https:" && !(mcp.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(mcp.hostname)))
|
|
262
|
+
return false;
|
|
263
|
+
const override = process.env.OMB_COMPOSIO_API;
|
|
264
|
+
if (!override)
|
|
265
|
+
return false;
|
|
266
|
+
try {
|
|
267
|
+
return new URL(override).origin === mcp.origin;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
247
273
|
function parseSessionResponse(session) {
|
|
248
274
|
const mcp = new URL(session.mcp.url);
|
|
249
|
-
if (
|
|
275
|
+
if (!trustedSessionMcpUrl(session.mcp.url)) {
|
|
250
276
|
throw new Error("Composio returned an untrusted Session MCP URL");
|
|
251
277
|
}
|
|
252
278
|
return { ...session, mcp: { ...session.mcp, url: mcp.toString() } };
|
|
@@ -497,7 +523,7 @@ export async function mcpIntegration(cfg, context) {
|
|
|
497
523
|
},
|
|
498
524
|
};
|
|
499
525
|
}
|
|
500
|
-
export async function relayMcp(cfg, payload, transportSessionId) {
|
|
526
|
+
export async function relayMcp(cfg, payload, transportSessionId, beforeSend) {
|
|
501
527
|
const apiKey = projectApiKey(cfg);
|
|
502
528
|
let url;
|
|
503
529
|
let identity;
|
|
@@ -529,6 +555,9 @@ export async function relayMcp(cfg, payload, transportSessionId) {
|
|
|
529
555
|
if (forwardedTransportSessionId) {
|
|
530
556
|
headers.set("mcp-session-id", forwardedTransportSessionId);
|
|
531
557
|
}
|
|
558
|
+
// Session discovery can await network I/O. Turn authority and mutable
|
|
559
|
+
// permissions must be checked after it, immediately before dispatch.
|
|
560
|
+
beforeSend?.();
|
|
532
561
|
const response = await fetch(url, {
|
|
533
562
|
method: "POST",
|
|
534
563
|
headers,
|
|
@@ -153,18 +153,3 @@ export class ComputerControl {
|
|
|
153
153
|
return snapshot;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
/** Safely executes an async tool or desktop control call, catching runtime exceptions gracefully. */
|
|
157
|
-
export async function executeToolSafely(toolName, fn) {
|
|
158
|
-
try {
|
|
159
|
-
const data = await fn();
|
|
160
|
-
return { success: true, data };
|
|
161
|
-
}
|
|
162
|
-
catch (err) {
|
|
163
|
-
const errorMessage = err?.message || String(err) || 'Unknown execution error';
|
|
164
|
-
console.error(`[Tool Execution Error] Name: ${toolName} | Details: ${errorMessage}`);
|
|
165
|
-
return {
|
|
166
|
-
success: false,
|
|
167
|
-
error: `Tool '${toolName}' failed to execute: ${errorMessage}`
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
@@ -10,80 +10,6 @@ export const emptyObservationMetrics = () => ({
|
|
|
10
10
|
verificationSuccesses: 0,
|
|
11
11
|
verificationFailures: 0,
|
|
12
12
|
});
|
|
13
|
-
export function normalizeCrop(raw, maxWidth, maxHeight) {
|
|
14
|
-
if (!raw || typeof raw !== "object")
|
|
15
|
-
return null;
|
|
16
|
-
const value = raw;
|
|
17
|
-
const x = Math.round(Number(value.x));
|
|
18
|
-
const y = Math.round(Number(value.y));
|
|
19
|
-
const width = Math.round(Number(value.width));
|
|
20
|
-
const height = Math.round(Number(value.height));
|
|
21
|
-
if (![x, y, width, height, maxWidth, maxHeight].every(Number.isFinite) ||
|
|
22
|
-
maxWidth <= 0 ||
|
|
23
|
-
maxHeight <= 0 ||
|
|
24
|
-
x < 0 ||
|
|
25
|
-
y < 0 ||
|
|
26
|
-
width < 32 ||
|
|
27
|
-
height < 32) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
if (x + width > maxWidth || y + height > maxHeight)
|
|
31
|
-
return null;
|
|
32
|
-
return { x, y, width, height };
|
|
33
|
-
}
|
|
34
|
-
/** Canonical value for internal navigation checks. Credentials are never
|
|
35
|
-
* needed for equality and are removed here; query and fragment remain so
|
|
36
|
-
* two distinct application states cannot verify as the same destination. */
|
|
37
|
-
export function normalizeBrowserUrl(value) {
|
|
38
|
-
if (typeof value !== "string" || !value || value.length > 8_192)
|
|
39
|
-
return null;
|
|
40
|
-
try {
|
|
41
|
-
const url = new URL(value);
|
|
42
|
-
if (!/^https?:$/.test(url.protocol))
|
|
43
|
-
return null;
|
|
44
|
-
url.username = "";
|
|
45
|
-
url.password = "";
|
|
46
|
-
return url.toString();
|
|
47
|
-
}
|
|
48
|
-
catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/** Removes credentials, query, and fragment before browser state reaches a model or log. */
|
|
53
|
-
export function safeBrowserUrl(value) {
|
|
54
|
-
const normalized = normalizeBrowserUrl(value);
|
|
55
|
-
if (!normalized)
|
|
56
|
-
return null;
|
|
57
|
-
const url = new URL(normalized);
|
|
58
|
-
url.search = "";
|
|
59
|
-
url.hash = "";
|
|
60
|
-
const safe = url.toString();
|
|
61
|
-
return safe.length <= 2_048 ? safe : null;
|
|
62
|
-
}
|
|
63
|
-
/** Parses Chrome's /json/list response into a small, safe structured observation. */
|
|
64
|
-
export function parseBrowserTargets(raw) {
|
|
65
|
-
if (raw.length > 1_000_000)
|
|
66
|
-
return [];
|
|
67
|
-
try {
|
|
68
|
-
const parsed = JSON.parse(raw);
|
|
69
|
-
if (!Array.isArray(parsed))
|
|
70
|
-
return [];
|
|
71
|
-
return parsed.slice(0, 20).flatMap((item) => {
|
|
72
|
-
if (!item || typeof item !== "object")
|
|
73
|
-
return [];
|
|
74
|
-
const value = item;
|
|
75
|
-
const comparisonUrl = normalizeBrowserUrl(value.url);
|
|
76
|
-
const url = safeBrowserUrl(value.url);
|
|
77
|
-
if (value.type !== "page" || !url || !comparisonUrl || typeof value.id !== "string")
|
|
78
|
-
return [];
|
|
79
|
-
const title = typeof value.title === "string" ? value.title.replace(/\s+/g, " ").trim().slice(0, 200) : "";
|
|
80
|
-
return [{ id: value.id.slice(0, 100), title, url, comparisonUrl }];
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
13
|
/**
|
|
88
14
|
* Keeps observations cheap without claiming the screen is immutable. Every
|
|
89
15
|
* requested observation still captures fresh pixels (pages can change without
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
//
|
|
3
3
|
// The chip used to read "Waiting for computer — X / Y is using it; will
|
|
4
4
|
// continue automatically", which people read as an error about a machine
|
|
5
|
-
// they could not see. It is a queue position: this turn
|
|
6
|
-
// turn on one desktop
|
|
7
|
-
// that, name the holder as a bot running a
|
|
8
|
-
// on every surface (chat chip, room chip,
|
|
9
|
-
// who has read it once recognises it everywhere.
|
|
5
|
+
// they could not see. It is a queue position: this turn sits behind a named
|
|
6
|
+
// turn on one desktop — and every turn queued ahead of it — and starts on
|
|
7
|
+
// its own when they finish. Say that, name the holder as a bot running a
|
|
8
|
+
// thread, and keep the same words on every surface (chat chip, room chip,
|
|
9
|
+
// the gate's refusal) so a person who has read it once recognises it everywhere.
|
|
10
10
|
/** Whole minutes, or seconds under one minute, singular at exactly one. */
|
|
11
11
|
const minutes = (ms) => {
|
|
12
12
|
const value = ms >= 60_000 ? Math.round(ms / 60_000) : Math.round(ms / 1000);
|
|
@@ -19,11 +19,30 @@ export function computerWaitDuration(ms) {
|
|
|
19
19
|
return ms < 1_000 ? "under a second" : minutes(ms);
|
|
20
20
|
}
|
|
21
21
|
const holderPhrase = (holder) => holder.task ? `${holder.name} is running ${holder.task}` : `${holder.name} is using it`;
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const ordinal = (position) => {
|
|
23
|
+
const mod100 = position % 100;
|
|
24
|
+
const mod10 = position % 10;
|
|
25
|
+
const suffix = mod100 >= 11 && mod100 <= 13 ? "th" : mod10 === 1 ? "st" : mod10 === 2 ? "nd" : mod10 === 3 ? "rd" : "th";
|
|
26
|
+
return `${position}${suffix}`;
|
|
27
|
+
};
|
|
28
|
+
/** The chip a turn shows while it is queued behind another turn's desktop:
|
|
29
|
+
* its stable queue position, the holder, and — only once waits here have
|
|
30
|
+
* history — about how long they have been taking. */
|
|
31
|
+
export function computerWaitingText(holder, queue) {
|
|
32
|
+
const position = queue?.position && queue.position > 0 ? queue.position : undefined;
|
|
33
|
+
const place = position ? ` — ${ordinal(position)} in queue` : "";
|
|
34
|
+
const estimate = queue?.estimateMs !== undefined ? `; recent waits here have taken ${computerWaitDuration(queue.estimateMs)}` : "";
|
|
35
|
+
// Behind more than the holder, that turn ending is not this turn's start:
|
|
36
|
+
// the released seat goes to the front waiter first, so this turn starts
|
|
37
|
+
// only once every turn ahead of it has finished (#1652).
|
|
38
|
+
const starts = position && position > 1
|
|
39
|
+
? "Starts automatically when the turns ahead finish"
|
|
40
|
+
: holder
|
|
41
|
+
? "Starts automatically when that finishes"
|
|
42
|
+
: "Starts automatically when it is free";
|
|
24
43
|
if (!holder)
|
|
25
|
-
return
|
|
26
|
-
return `Waiting for its turn on this computer — ${holderPhrase(holder)}.
|
|
44
|
+
return `Waiting for its turn on this computer${place}. ${starts}${estimate}.`;
|
|
45
|
+
return `Waiting for its turn on this computer${place} — ${holderPhrase(holder)}. ${starts}${estimate}.`;
|
|
27
46
|
}
|
|
28
47
|
/** The resolution appended once the wait landed and this turn holds the
|
|
29
48
|
* desktop: how long it waited, and who held it. The waiting chip stays as
|
|
@@ -38,9 +57,10 @@ export function computerStoppedWaitingText(holder, waitedMs) {
|
|
|
38
57
|
const who = holder ? ` — ${holderPhrase(holder)}` : "";
|
|
39
58
|
return `Stopped waiting for the computer after ${computerWaitDuration(waitedMs)}${who}.`;
|
|
40
59
|
}
|
|
41
|
-
/** The
|
|
42
|
-
*
|
|
43
|
-
|
|
60
|
+
/** The resolution appended when the wait ceiling parks the turn (#1651):
|
|
61
|
+
* still names who holds it, but the work is not lost — the turn settled and
|
|
62
|
+
* resumes on its own when the desktop frees, so nobody has to fix it. */
|
|
63
|
+
export function computerParkedText(holder, ceilingMs) {
|
|
44
64
|
const who = holder ? ` — ${holderPhrase(holder).replace(" is running ", " is still running ").replace(" is using it", " is still using it")}` : "";
|
|
45
|
-
return `Computer
|
|
65
|
+
return `Computer still busy after ${minutes(ceilingMs)}${who}. Parked — it continues automatically when the computer is free.`;
|
|
46
66
|
}
|