openmausbot 0.1.90 → 0.1.92
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-Do6lBa4C.js +5 -0
- package/dist/assets/{abnfDiagram-O67JEVCF-DUyXd0kL.js → abnfDiagram-O67JEVCF-Cd3X7Hxc.js} +1 -1
- package/dist/assets/{arc-q73qM-At.js → arc-DIYc7Ymx.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-D33O3Wur.js → architectureDiagram-NJMV4G6O-DBmRGVyP.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-DLsJeoXC.js → blockDiagram-BEXU5L5S-CPFePSEE.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-DnaljZnu.js → c4Diagram-YGBWAQC7-DD_DO1qy.js} +1 -1
- package/dist/assets/channel-Dtk5Mtic.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-Da0h2i79.js → chunk-2Q5K7J3B-BNEeNSjX.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-CfzHi54f.js → chunk-3FUC2YCW-BbinLaqH.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-LjE4d9Jf.js → chunk-5VM5RSS4-BBgYrgDR.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DY-UGmvc.js → chunk-GTNCS2PH-ezXj-kA-.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BqeGhyhP.js → chunk-GWA4HPMP-9FxZ3diV.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BYa9EBAg.js → chunk-JWPE2WC7-CO-40JpI.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-vTrDtWyb.js → chunk-NETBCI7D-BEhjslYE.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-ic5tXadM.js → chunk-WEXAMYUT-CNeq4n7_.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-DvRCY-c6.js → chunk-XXDRQBXY-OsCY7KSi.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DVbmgz_L.js → classDiagram-v2-NBCMYWYE-CloRUEtk.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-vdUs2n57.js → cose-bilkent-JH36ORCC-D1BGZhp4.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-BvLGlV3U.js → cynefin-OW5HDTMX-CUf4T2CN.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-C4GcJQDt.js → cynefinDiagram-VND7K2PF-DPEZrFGA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-BMSaHpt_.js → dagre-6A5THRUB-bDe6GIv5.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-D5b9UBhC.js → diagram-22UHCM2B-CdcUQz55.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-CIKLlB7Y.js → diagram-3UASUU5V-BZpqGg7c.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-BzXb-7k7.js → diagram-ATOU4E4O-DvGM6ugp.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-HsTvJ7uT.js → diagram-CDSNMT55-DSTmMm0s.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-Db9ySNbQ.js → diagram-MLGK6HIB-15cucnMn.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D6BKbpqY.js → diagram-MPIPVDR6-tMbuaE6n.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-Bq067L0t.js → ebnfDiagram-ZINNZB2B-BYxYzjHI.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DhaD7fNJ.js → elk-276RUBZZ-v6bj852W.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-fwIhfz36.js → erDiagram-OPXOYQCR-5lBBlnd2.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-DygIyu0V.js → flowDiagram-KWPJA3E3-CZyl1zIj.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DPYfSO96.js → ganttDiagram-FUAMR5RP-BiAI-ltZ.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-DhkHeCle.js → gitGraphDiagram-X574FWY7-BAZISkj3.js} +1 -1
- package/dist/assets/index-Cdmjrl28.css +1 -0
- package/dist/assets/{index-G1PHKGyh.js → index-DGBaEXrM.js} +1 -1
- package/dist/assets/index-DR7YPMkW.js +597 -0
- package/dist/assets/{infoDiagram-VRGFBTTK-DstBLxME.js → infoDiagram-VRGFBTTK-B6ooOy-Z.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-CrIsGgch.js → ishikawaDiagram-OU5B5YK6-BZ9ksi-B.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CYypGDbB.js → journeyDiagram-ZHPQQLJL-DWqlzIEe.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DGjPt8E_.js → kanban-definition-PNTS6WVX-JX6piDwL.js} +1 -1
- package/dist/assets/{linear-Dwc3Hrru.js → linear-CmOWzX0P.js} +1 -1
- package/dist/assets/{mermaid.core-Dc4OePBL.js → mermaid.core-CF1zwoQ-.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-DV-vepj-.js → mindmap-definition-NLK3R4M7-CGh2K4fW.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-lx5Kj-n5.js → pegDiagram-GJSIUBJH-CDwYN0s7.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-Bq7Vmm15.js → pieDiagram-5QR66LMP-1rfjJocB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-DeqeqOBj.js → quadrantDiagram-O4NWA36T-tKPvPiok.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-CXwRQzSC.js → railroadDiagram-XR7U4H2S-BHLm5vXR.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-8tAeoB88.js → requirementDiagram-PLB6GJNP-CB45STp7.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-BG5zjGWU.js → sankeyDiagram-IPEJSGJF-55QA_huC.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DKF3iLcw.js → sequenceDiagram-PO4LG4MO-BckFmQHn.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-B-y1qUgT.js → sizeCapture-INFHLROL-BFCOJF11.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-MkzPgxAc.js → stateDiagram-v2-GCMORJYK-CZvmiVVN.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-DnbaOEwH.js → swimlanes-2SLR337P-BpDUCEKy.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-M0GYW9Y3.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Bb4xG_Bt.js → timeline-definition-EJHVYXUP-BpxHq6IK.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-JgkR9pFf.js → usecaseDiagram-POWQR4AR-Cj66vm02.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-CIPIrAYe.js → vennDiagram-UO4OBE2U-C9nRkWE_.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-BjCLuUvJ.js → wardleyDiagram-VNRHLVJA-D-me_nWj.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-DKgiaYxT.js → xychartDiagram-PMCCYNJV-Dc_fL4mk.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/novnc-NOTICE.txt +118 -0
- package/dist-server/cloud-home-start.js +128 -15
- package/dist-server/container-mcp.js +277 -45
- package/dist-server/drivers/agents-proxy.js +16 -4
- package/dist-server/enterprise/server/index.js +5 -1
- package/dist-server/index.js +46703 -39955
- package/dist-server/local-computer-proxy.js +128 -7
- package/dist-server/mcp-gate.js +11 -5
- package/dist-server/model-catalog/models-dev.snapshot.json +7299 -0
- package/dist-server/openmausbot.js +4099 -1217
- package/dist-server/pair-cli.js +4099 -1217
- package/dist-server/server/boat.js +67 -22
- package/dist-server/server/bot-overview.js +7 -4
- package/dist-server/server/browser-engine.js +10 -1
- package/dist-server/server/browser-live.js +46 -2
- package/dist-server/server/browser-runtime.js +74 -8
- package/dist-server/server/browser-tool-shape.js +6 -4
- package/dist-server/server/chief-of-staff.js +1 -1
- package/dist-server/server/cloud-home-start.js +136 -26
- package/dist-server/server/cloud-home.js +64 -5
- package/dist-server/server/cloud-lending.js +167 -0
- package/dist-server/server/cloud-move-http.js +268 -0
- package/dist-server/server/cloud-move.js +418 -0
- package/dist-server/server/cloud-owner.js +196 -0
- package/dist-server/server/cloud-secrets-boot.js +6 -0
- package/dist-server/server/cloud-secrets.js +65 -0
- package/dist-server/server/config.js +215 -17
- package/dist-server/server/container-computer.js +3 -2
- package/dist-server/server/decider/index.js +238 -0
- package/dist-server/server/decider/jev.js +183 -0
- package/dist-server/server/decider/log.js +62 -0
- package/dist-server/server/decider/relay.js +52 -0
- package/dist-server/server/decider/room-routing.js +88 -0
- package/dist-server/server/decider/types.js +5 -0
- package/dist-server/server/desktop-viewer-targets.js +26 -0
- package/dist-server/server/drivers/acp/core.js +167 -21
- package/dist-server/server/drivers/acp/opencode-go.js +614 -85
- package/dist-server/server/drivers/agents-catalog.js +22 -6
- package/dist-server/server/drivers/antigravity-acp.js +69 -11
- package/dist-server/server/drivers/antigravity-temp.js +360 -0
- package/dist-server/server/drivers/antigravity.js +21 -0
- package/dist-server/server/drivers/boatagent.js +20 -12
- package/dist-server/server/drivers/chatgpt-plan-auth.js +481 -0
- package/dist-server/server/drivers/claude.js +313 -34
- package/dist-server/server/drivers/codex.js +228 -18
- package/dist-server/server/drivers/grok.js +2 -2
- package/dist-server/server/drivers/mistral.js +4 -3
- package/dist-server/server/drivers/openai-chat-protocol.js +4 -3
- package/dist-server/server/drivers/openai-chat.js +41 -3
- package/dist-server/server/drivers/openai-compat.js +73 -23
- package/dist-server/server/engine-install.js +46 -12
- package/dist-server/server/env-path.js +15 -3
- package/dist-server/server/group-goal-run.js +6 -4
- package/dist-server/server/harness/registry.js +2 -2
- package/dist-server/server/harness-capabilities.js +2 -0
- package/dist-server/server/included-services.js +70 -0
- package/dist-server/server/index.js +1753 -263
- package/dist-server/server/lending-memory.js +479 -0
- package/dist-server/server/local-vm-idle.js +24 -6
- package/dist-server/server/managed-desktop.js +3 -0
- package/dist-server/server/mcp-bridge.js +5 -3
- package/dist-server/server/mcp-http.js +6 -3
- package/dist-server/server/mcp-oauth-discovery.js +133 -0
- package/dist-server/server/mcp-oauth-store.js +70 -0
- package/dist-server/server/mcp-oauth.js +550 -0
- package/dist-server/server/mcp-probe.js +14 -1
- package/dist-server/server/mcp-registry.js +43 -0
- package/dist-server/server/memory-upkeep.js +94 -84
- package/dist-server/server/model-catalog/catalog.js +374 -0
- package/dist-server/server/model-catalog/presets.js +176 -0
- package/dist-server/server/model-catalog/trim.js +219 -0
- package/dist-server/server/model-context-window.js +4 -4
- package/dist-server/server/package-export.js +17 -10
- package/dist-server/server/package-import.js +6 -3
- package/dist-server/server/package-parts.js +3 -1
- package/dist-server/server/provider-key-check.js +4 -2
- package/dist-server/server/recent-work.js +1 -1
- package/dist-server/server/request-auth.js +25 -0
- package/dist-server/server/routes/antigravity-leftovers.js +45 -0
- package/dist-server/server/routes/bot-memory.js +31 -6
- package/dist-server/server/routes/decider.js +19 -0
- package/dist-server/server/routes/desktop-viewer.js +194 -0
- package/dist-server/server/routine-requests.js +1 -1
- package/dist-server/server/routines.js +1 -1
- package/dist-server/server/sessions.js +56 -9
- package/dist-server/server/shared-computers.js +116 -30
- package/dist-server/server/skills.js +25 -10
- package/dist-server/server/store.js +79 -124
- package/dist-server/server/surface.js +13 -0
- package/dist-server/server/system-prompt.js +11 -0
- package/dist-server/server/team-backup.js +6 -2
- package/dist-server/server/team-manifest.js +11 -3
- package/dist-server/server/thread-events.js +2 -0
- package/dist-server/server/thread-starters.js +33 -0
- package/dist-server/server/thread-title.js +46 -0
- package/dist-server/server/tts/elevenlabs.js +11 -6
- package/dist-server/server/tts/fish.js +4 -5
- package/dist-server/server/tts/index.js +20 -11
- package/dist-server/server/vps-computer.js +79 -12
- package/dist-server/server/webhooks.js +18 -1
- package/dist-server/server/workspace-backup-policy.js +25 -3
- package/dist-server/server/workspace-backup.js +28 -8
- package/dist-server/server/workspace.js +65 -15
- package/dist-server/shared/cloud-home.js +4 -0
- package/dist-server/shared/package-format.js +30 -4
- package/dist-server/shared/team-backup.js +20 -3
- package/dist-server/shared/workspace-backup-client.js +1 -0
- package/dist-server/src/lib/citations.js +177 -0
- package/dist-server/src/lib/composer-attachments.js +7 -1
- package/dist-server/vps-container-mcp.js +278 -45
- package/dist-server/workspace-backup.worker.js +184 -38
- package/enterprise/server/index.js +5 -1
- package/package.json +1 -1
- package/dist/assets/channel-BCuyHjFV.js +0 -1
- package/dist/assets/index-CRvo-DS2.css +0 -1
- package/dist/assets/index-CWYGlA63.js +0 -588
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BZQ07DGE.js +0 -8
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
// Entry point of the OMB Cloud Pro home image (deploy/fly/Dockerfile).
|
|
2
2
|
//
|
|
3
|
-
// Starts as root
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080.
|
|
7
|
-
// the only listener the network can reach, and it always
|
|
8
|
-
// X-Forwarded-*, so request-auth.ts never grants a remote
|
|
9
|
-
// trust. If either child exits, both stop and the machine
|
|
3
|
+
// Starts as root, hands a fresh Fly volume (mounted root-owned at /data) to
|
|
4
|
+
// the unprivileged `maus` user, and stays a small root supervisor of two
|
|
5
|
+
// children that run as `maus`: the OpenMausBot server on 127.0.0.1:8799
|
|
6
|
+
// (and its webhook receiver on :8800) and the Caddy edge on 0.0.0.0:8080.
|
|
7
|
+
// The edge is the only listener the network can reach, and it always
|
|
8
|
+
// forwards with X-Forwarded-*, so request-auth.ts never grants a remote
|
|
9
|
+
// request loopback trust. If either child exits, both stop and the machine
|
|
10
|
+
// restarts; the one exception is the server asking to be started again
|
|
11
|
+
// after a restore.
|
|
12
|
+
//
|
|
13
|
+
// The machine's secrets (the signing secret, the relay tokens) arrive as
|
|
14
|
+
// this process's environment. No child's environment ever carries them:
|
|
15
|
+
// /proc/<pid>/environ keeps a process's starting environment, readable by
|
|
16
|
+
// anything running as the same user (an engine's shell). The server gets
|
|
17
|
+
// them over an inherited pipe it reads first thing and closes
|
|
18
|
+
// (cloud-secrets.ts), and an environment built from an allow-list, so a
|
|
19
|
+
// secret the platform adds later never reaches it either. This process stays
|
|
20
|
+
// root, so its own environment and memory are out of `maus`'s reach, and it
|
|
21
|
+
// runs and trusts only code `maus` cannot change: the image's, never the
|
|
22
|
+
// volume's (codeTrustProblem).
|
|
10
23
|
import { spawn } from "node:child_process";
|
|
11
24
|
import { chownSync, readFileSync, statSync } from "node:fs";
|
|
12
25
|
import { dirname, join, posix, resolve } from "node:path";
|
|
13
26
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
-
import { cloudHomeConfiguration, cloudHomeHost, prepareCloudHomeVolume, withoutIgnoredCloudKeys } from "./cloud-home.js";
|
|
27
|
+
import { CLOUD_HOME_RESTART_EXIT_CODE, CLOUD_SECRETS_FD_ENV, cloudHomeConfiguration, cloudHomeHost, cloudHomeSecrets, prepareCloudHomeVolume, withoutCloudSecrets, withoutIgnoredCloudKeys, } from "./cloud-home.js";
|
|
15
28
|
const SERVICE_USER = "maus";
|
|
29
|
+
/** The descriptor the server reads its secrets from. */
|
|
30
|
+
const SECRETS_FD = 3;
|
|
16
31
|
/** uid/gid from /etc/passwd; Node has no getpwnam. */
|
|
17
32
|
export function passwdIds(passwd, name) {
|
|
18
33
|
for (const line of passwd.split("\n")) {
|
|
@@ -22,22 +37,85 @@ export function passwdIds(passwd, name) {
|
|
|
22
37
|
}
|
|
23
38
|
return null;
|
|
24
39
|
}
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
40
|
+
/** What the server child may be given from this process's environment, by
|
|
41
|
+
* name: the process basics, what the image sets, and the Cloud home's
|
|
42
|
+
* contract that is not secret. Anything else (a secret the platform adds
|
|
43
|
+
* later, a test's key, a platform gateway's settings) is left out. */
|
|
44
|
+
const SERVER_ENV_NAMES = new Set([
|
|
45
|
+
"PATH", "SHELL", "HOSTNAME", "LANG", "LANGUAGE", "TZ", "TERM", "TMPDIR", "NO_COLOR", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
46
|
+
"AGENT_BROWSER_EXECUTABLE_PATH", "OMB_STATIC_DIR", "OMB_DATA_DIR",
|
|
47
|
+
"OMB_CLOUD_ROLE", "OMB_CLOUD_MACHINE_ID", "OMB_CLOUD_ADMIN_URL", "OMB_CLOUD_IMAGE", "OMB_PUBLIC_URL", "OMB_WEBHOOK_PUBLIC_URL",
|
|
48
|
+
"OMB_CLOUD_BOAT_URL", "OMB_CLOUD_VOICE_URL", "OMB_CLOUD_DECIDER_URL", "OMB_TTS_DEFAULT_VOICE",
|
|
49
|
+
]);
|
|
50
|
+
export function serverEnvironmentAllowed(name) {
|
|
51
|
+
return SERVER_ENV_NAMES.has(name) || name.startsWith("LC_");
|
|
52
|
+
}
|
|
53
|
+
/** The server child's environment: the allowed part of the operator's
|
|
54
|
+
* contract (serverEnvironmentAllowed) plus fixed ports and paths (Linux
|
|
55
|
+
* paths inside the image, so POSIX joins on every host that builds them,
|
|
56
|
+
* tests included), never a platform gateway's settings and never a secret
|
|
57
|
+
* (`secrets`, handed over the pipe instead). `dropped` names what was left
|
|
58
|
+
* out, for the log. The edge child gets only what it needs to route. */
|
|
29
59
|
export function cloudHomeChildEnvironments(config, env, home) {
|
|
60
|
+
const offered = withoutCloudSecrets(withoutIgnoredCloudKeys(env));
|
|
61
|
+
const allowed = Object.fromEntries(Object.entries(offered).filter(([name, value]) => value !== undefined && serverEnvironmentAllowed(name)));
|
|
62
|
+
const dropped = Object.keys(offered).filter((name) => !serverEnvironmentAllowed(name) && name !== "HOME").sort();
|
|
30
63
|
const server = {
|
|
31
|
-
...
|
|
64
|
+
...allowed, HOME: home, OMB_DATA_DIR: env.OMB_DATA_DIR || posix.join(home, ".openmausbot"),
|
|
32
65
|
OMB_PORT: "8799", OMB_WEBHOOK_PORT: "8800", OMB_PUBLIC_URL: config.publicOrigin,
|
|
33
66
|
OMB_WEBHOOK_PUBLIC_URL: env.OMB_WEBHOOK_PUBLIC_URL || config.publicOrigin,
|
|
67
|
+
[CLOUD_SECRETS_FD_ENV]: String(SECRETS_FD),
|
|
34
68
|
};
|
|
35
69
|
const edge = {
|
|
36
70
|
PATH: env.PATH ?? "/usr/local/bin:/usr/bin:/bin", HOME: "/tmp/omb-edge",
|
|
37
71
|
XDG_DATA_HOME: "/tmp/omb-edge/data", XDG_CONFIG_HOME: "/tmp/omb-edge/config",
|
|
38
72
|
OMB_CLOUD_PUBLIC_HOST: cloudHomeHost(config),
|
|
39
73
|
};
|
|
40
|
-
return { server, edge };
|
|
74
|
+
return { server, edge, secrets: cloudHomeSecrets(env), dropped };
|
|
75
|
+
}
|
|
76
|
+
/** Why the root supervisor must not run or trust `files`, or null: each
|
|
77
|
+
* must be root's and not writable by anyone else (nor any folder above it),
|
|
78
|
+
* and none may live on the volume `home`, which `maus` owns. The image makes its code
|
|
79
|
+
* root's (deploy/fly/Dockerfile); a file `maus` could rewrite would run as
|
|
80
|
+
* root at the next start, or be handed the secrets. */
|
|
81
|
+
export function codeTrustProblem(files, home, stat = statSync) {
|
|
82
|
+
const volume = posix.join(home, "/");
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
if (file === home || file.startsWith(volume))
|
|
85
|
+
return `${file} is on the volume`;
|
|
86
|
+
for (let path = file;; path = dirname(path)) {
|
|
87
|
+
let owner;
|
|
88
|
+
try {
|
|
89
|
+
owner = stat(path);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return `${path} cannot be checked`;
|
|
93
|
+
}
|
|
94
|
+
if (owner.uid !== 0)
|
|
95
|
+
return `${path} is not root's`;
|
|
96
|
+
// A folder with the sticky bit (/tmp) lets nobody replace root's files.
|
|
97
|
+
if (owner.mode & 0o022 && !(path !== file && owner.mode & 0o1000))
|
|
98
|
+
return `${path} is writable by others than root`;
|
|
99
|
+
if (dirname(path) === path)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** Start the server with its secrets on an inherited pipe (never its
|
|
106
|
+
* environment), as `ids` when given. The pipe is written and closed at once;
|
|
107
|
+
* nothing else is ever sent on it. */
|
|
108
|
+
export function spawnWithSecrets(command, args, env, secrets, ids) {
|
|
109
|
+
const child = spawn(command, args, { env, stdio: ["inherit", "inherit", "inherit", "pipe"], ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) });
|
|
110
|
+
const pipe = child.stdio[SECRETS_FD];
|
|
111
|
+
pipe?.on("error", () => { });
|
|
112
|
+
pipe?.end(JSON.stringify(secrets));
|
|
113
|
+
return child;
|
|
114
|
+
}
|
|
115
|
+
/** What the launcher does when the server child exits: start it again only
|
|
116
|
+
* when it asked to (a committed restore), and only a few times in a row. */
|
|
117
|
+
export function serverExitAction(code, stopping, restarts) {
|
|
118
|
+
return !stopping && code === CLOUD_HOME_RESTART_EXIT_CODE && restarts < 5 ? "restart" : "stop";
|
|
41
119
|
}
|
|
42
120
|
export function startCloudHome(env = process.env) {
|
|
43
121
|
process.umask(0o077);
|
|
@@ -48,8 +126,11 @@ export function startCloudHome(env = process.env) {
|
|
|
48
126
|
for (const warning of config.warnings)
|
|
49
127
|
console.warn(`cloud home: ${warning}`);
|
|
50
128
|
const home = env.HOME || "/data";
|
|
129
|
+
// Who the children run as: `maus` when this starts as root (the image),
|
|
130
|
+
// else whoever started it (a test, a dev machine).
|
|
131
|
+
let ids = null;
|
|
51
132
|
if (process.getuid?.() === 0) {
|
|
52
|
-
|
|
133
|
+
ids = passwdIds(readFileSync("/etc/passwd", "utf8"), SERVICE_USER);
|
|
53
134
|
if (!ids)
|
|
54
135
|
throw new Error(`The ${SERVICE_USER} user is missing from this image.`);
|
|
55
136
|
// A new volume is a root-owned mount point. Only the mount point itself
|
|
@@ -58,14 +139,32 @@ export function startCloudHome(env = process.env) {
|
|
|
58
139
|
if (stat.uid !== ids.uid || stat.gid !== ids.gid)
|
|
59
140
|
chownSync(home, ids.uid, ids.gid);
|
|
60
141
|
process.setgroups?.([]);
|
|
61
|
-
|
|
62
|
-
process.
|
|
142
|
+
// The volume is prepared as `maus`, so what it creates is theirs.
|
|
143
|
+
process.setegid(ids.gid);
|
|
144
|
+
process.seteuid(ids.uid);
|
|
145
|
+
try {
|
|
146
|
+
prepareCloudHomeVolume(home, config.machineId);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
process.seteuid(0);
|
|
150
|
+
process.setegid(0);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
prepareCloudHomeVolume(home, config.machineId);
|
|
63
155
|
}
|
|
64
|
-
if (process.getuid?.() === 0)
|
|
65
|
-
throw new Error("The Cloud home must run as an unprivileged user.");
|
|
66
|
-
prepareCloudHomeVolume(home, config.machineId);
|
|
67
156
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
68
|
-
const
|
|
157
|
+
const edgeBin = env.OMB_CLOUD_EDGE_BIN || "/usr/local/bin/caddy";
|
|
158
|
+
const edgeConfig = env.OMB_CLOUD_EDGE_CONFIG || "/app/cloud/Caddyfile";
|
|
159
|
+
if (ids) {
|
|
160
|
+
const problem = codeTrustProblem([process.execPath, fileURLToPath(import.meta.url), join(here, "index.js"), edgeBin, edgeConfig], home);
|
|
161
|
+
if (problem)
|
|
162
|
+
throw new Error(`This image's code is not safe to run as root: ${problem}. Rebuild it from deploy/fly/Dockerfile.`);
|
|
163
|
+
}
|
|
164
|
+
const { server, edge, secrets, dropped } = cloudHomeChildEnvironments(config, env, home);
|
|
165
|
+
// Names only, never values: what the server does not get from here.
|
|
166
|
+
if (dropped.length)
|
|
167
|
+
console.log(`cloud home: not passed to the server: ${dropped.join(", ")}`);
|
|
69
168
|
const children = [];
|
|
70
169
|
let stopping = false;
|
|
71
170
|
const stop = (failed) => {
|
|
@@ -81,14 +180,25 @@ export function startCloudHome(env = process.env) {
|
|
|
81
180
|
child.kill("SIGKILL"); }, 20_000);
|
|
82
181
|
force.unref();
|
|
83
182
|
};
|
|
84
|
-
const
|
|
85
|
-
const child = spawn(command, args, { env: childEnv, stdio: "inherit" });
|
|
183
|
+
const watch = (child, again) => {
|
|
86
184
|
children.push(child);
|
|
87
185
|
child.once("error", () => stop(true));
|
|
88
|
-
child.once("exit", () =>
|
|
186
|
+
child.once("exit", (code) => {
|
|
187
|
+
children.splice(children.indexOf(child), 1);
|
|
188
|
+
if (!again?.(code))
|
|
189
|
+
stop(true);
|
|
190
|
+
});
|
|
89
191
|
};
|
|
90
|
-
|
|
91
|
-
|
|
192
|
+
let restarts = 0;
|
|
193
|
+
const runServer = () => watch(spawnWithSecrets(process.execPath, [join(here, "index.js")], server, secrets, ids), (code) => {
|
|
194
|
+
if (serverExitAction(code, stopping, restarts) !== "restart")
|
|
195
|
+
return false;
|
|
196
|
+
restarts++;
|
|
197
|
+
runServer();
|
|
198
|
+
return true;
|
|
199
|
+
});
|
|
200
|
+
runServer();
|
|
201
|
+
watch(spawn(edgeBin, ["run", "--config", edgeConfig, "--adapter", "caddyfile"], { env: edge, stdio: "inherit", ...(ids ? { uid: ids.uid, gid: ids.gid } : {}) }));
|
|
92
202
|
process.once("SIGTERM", () => stop(false));
|
|
93
203
|
process.once("SIGINT", () => stop(false));
|
|
94
204
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// OMB Cloud Pro home machine: the boot contract, the
|
|
2
|
-
// request, and the volume the machine lives on. docs/cloud-pro.md is the
|
|
1
|
+
// OMB Cloud Pro home machine: the boot contract, the places it offers, the
|
|
2
|
+
// Admin's signed pairing request, and the volume the machine lives on. docs/cloud-pro.md is the
|
|
3
3
|
// contract of record (and openmaus-cloud docs/consumer-cloud.md its Admin
|
|
4
4
|
// half); keep them in step.
|
|
5
5
|
//
|
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
// the machine's bootstrap secret (HMAC-SHA256 over the method, path, a
|
|
13
13
|
// timestamp, a nonce and the body's hash); each valid request opens one
|
|
14
14
|
// ordinary pairing window (sessions.ts: single use, at most ten minutes),
|
|
15
|
-
// which the Admin hands to the person's signed-in desktop app.
|
|
15
|
+
// which the Admin hands to the person's signed-in desktop app. A request for
|
|
16
|
+
// a browser sign-in (the Cloud page's "Use in your browser") names the
|
|
17
|
+
// account that owns this Cloud and opens a window only this machine's web
|
|
18
|
+
// page redeems, for at most two minutes, after showing whose Cloud it is.
|
|
16
19
|
//
|
|
17
20
|
// Cloud Pro includes no AI. The person signs in on the machine with their own
|
|
18
21
|
// Claude or ChatGPT account, or an API key, exactly as on any server; nothing
|
|
@@ -33,12 +36,20 @@ export const CLOUD_IGNORED_KEYS = ["OMB_HOSTED_MODEL_URL", "OMB_HOSTED_MODEL_TOK
|
|
|
33
36
|
export const CLOUD_PAIRING_PATH = "/api/cloud/pairing";
|
|
34
37
|
export const CLOUD_PAIRING_DEFAULT_TTL_S = 300;
|
|
35
38
|
export const CLOUD_PAIRING_MAX_TTL_S = 600;
|
|
39
|
+
/** A browser sign-in's owner: printable ASCII with no space, `<` or `>`, and exactly one `@`. */
|
|
40
|
+
const OWNER_EMAIL = /^[!-;=?A-~]{1,64}@[!-;=?A-~]{1,189}$/;
|
|
41
|
+
/** A browser sign-in is redeemed the moment its tab loads. */
|
|
42
|
+
export const CLOUD_BROWSER_SIGN_IN_MAX_TTL_S = 120;
|
|
36
43
|
/** How far a signed request's timestamp may be from this machine's clock. */
|
|
37
44
|
export const CLOUD_PAIRING_SKEW_S = 300;
|
|
38
45
|
/** How long a used nonce is refused. Longer than the whole accepted window. */
|
|
39
46
|
export const CLOUD_PAIRING_NONCE_MS = 10 * 60_000;
|
|
40
47
|
const MAX_NONCES = 10_000;
|
|
41
48
|
export const CLOUD_HOME_MARKER = ".omb-cloud-home.json";
|
|
49
|
+
/** The server exits with this after a restore commits (Move to Cloud): the
|
|
50
|
+
* launcher then starts it again, and startup installs the restore. Any other
|
|
51
|
+
* exit stops the machine for Fly to restart. */
|
|
52
|
+
export const CLOUD_HOME_RESTART_EXIT_CODE = 75;
|
|
42
53
|
export function cloudHomeConfigured(env = process.env) {
|
|
43
54
|
return CLOUD_HOME_KEYS.some((key) => env[key] !== undefined);
|
|
44
55
|
}
|
|
@@ -85,6 +96,9 @@ export function cloudHomeConfiguration(env = process.env) {
|
|
|
85
96
|
warnings.push(`ignoring ${ignored.join(", ")}: Cloud Pro includes no AI; people sign in with their own Claude or ChatGPT account, or an API key`);
|
|
86
97
|
return { machineId, adminOrigin, publicOrigin, bootstrapSecret, warnings };
|
|
87
98
|
}
|
|
99
|
+
// The secrets and their pipe live in cloud-secrets.ts, which the server
|
|
100
|
+
// reads before anything else (cloud-secrets-boot.ts).
|
|
101
|
+
export { CLOUD_HOME_SECRET_KEYS, CLOUD_SECRETS_FD_ENV, cloudHomeSecrets, takeCloudSecrets, withoutCloudSecrets } from "./cloud-secrets.js";
|
|
88
102
|
/** The environment without a platform gateway's settings (CLOUD_IGNORED_KEYS). */
|
|
89
103
|
export function withoutIgnoredCloudKeys(env) {
|
|
90
104
|
const kept = { ...env };
|
|
@@ -92,6 +106,37 @@ export function withoutIgnoredCloudKeys(env) {
|
|
|
92
106
|
delete kept[key];
|
|
93
107
|
return kept;
|
|
94
108
|
}
|
|
109
|
+
// The places a Cloud home offers live in shared/cloud-home.ts, so the app
|
|
110
|
+
// lists exactly what the server accepts.
|
|
111
|
+
export { cloudHomeOffersPlace } from "../shared/cloud-home.js";
|
|
112
|
+
/** Why a Cloud home refuses a place it never offers (no "this computer" of
|
|
113
|
+
* the person's, no Local VM), in the words the person reads; undefined for a
|
|
114
|
+
* place it offers. A turn's error shows 160 characters, so each fits. */
|
|
115
|
+
export function cloudHomePlaceRefusal(place) {
|
|
116
|
+
if (place === "local")
|
|
117
|
+
return "This computer isn't a place on your OMB Cloud: its bots run in the cloud. Set Works on to Auto, Cloud or Browser, or lend your Mac under Settings → OMB Cloud.";
|
|
118
|
+
if (place === "vm")
|
|
119
|
+
return "Bots on your OMB Cloud can't use a Local VM: the cloud machine has no container runtime. Set Works on to Auto, Cloud or Browser.";
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
/** What a turn is told when Cloud is chosen but no Boat account is set up (no
|
|
123
|
+
* key of the person's and no included Boat). A Cloud home has no Local VM to
|
|
124
|
+
* suggest instead. */
|
|
125
|
+
export function boatNotConfiguredMessage(cloudHome) {
|
|
126
|
+
return `Cloud Boat is not configured — add a Boat API key or choose ${cloudHome ? "Browser" : "Local VM"}`;
|
|
127
|
+
}
|
|
128
|
+
/** The Cloud's setup checklist (docs/cloud-pro.md) has a "try something"
|
|
129
|
+
* step that is done once a bot's turn finishes on the machine itself. The
|
|
130
|
+
* server records when, once, in this Cloud's own onboarding record: that
|
|
131
|
+
* section never travels with Move to Cloud (workspace-backup-policy.ts), so a
|
|
132
|
+
* moved-in history of turns does not count. Null when there is nothing to
|
|
133
|
+
* record: not a Cloud home, already recorded, a failed or stopped turn, or a
|
|
134
|
+
* thread that is no bot's conversation or room. */
|
|
135
|
+
export function firstCloudTurnPatch(turn) {
|
|
136
|
+
if (!turn.cloudHome || turn.recorded || !turn.ok || !turn.known)
|
|
137
|
+
return null;
|
|
138
|
+
return { onboarding: { firstTurnAt: (turn.now ?? new Date()).toISOString() } };
|
|
139
|
+
}
|
|
95
140
|
/** The Admin's side of the signature (openmaus-cloud cloudPairingSignature). */
|
|
96
141
|
export function cloudPairingSignature(secret, timestamp, nonce, body) {
|
|
97
142
|
const bodyHash = createHash("sha256").update(body).digest("base64url");
|
|
@@ -141,18 +186,32 @@ export function createCloudPairing(options) {
|
|
|
141
186
|
}
|
|
142
187
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
143
188
|
return { status: 400, body: { error: "invalid_body" } };
|
|
144
|
-
const { label, ttlSeconds } = parsed;
|
|
189
|
+
const { label, ttlSeconds, purpose, owner } = parsed;
|
|
145
190
|
// oxlint-disable-next-line no-control-regex
|
|
146
191
|
if (label !== undefined && (typeof label !== "string" || label.length > 80 || /[\x00-\x1f\x7f]/.test(label)))
|
|
147
192
|
return { status: 400, body: { error: "invalid_label" } };
|
|
148
193
|
if (ttlSeconds !== undefined && (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 1))
|
|
149
194
|
return { status: 400, body: { error: "invalid_ttl" } };
|
|
150
|
-
|
|
195
|
+
if (purpose !== undefined && purpose !== "browser")
|
|
196
|
+
return { status: 400, body: { error: "invalid_purpose" } };
|
|
197
|
+
const browser = purpose === "browser";
|
|
198
|
+
// A browser sign-in names its Cloud's owner (the account's email), which the sign-in page shows before the
|
|
199
|
+
// person continues: one address in printable ASCII, as the Admin's email schemas allow, with no `<` or `>`.
|
|
200
|
+
if ((browser || owner !== undefined) && (typeof owner !== "string" || owner.length > 254 || !OWNER_EMAIL.test(owner))) {
|
|
201
|
+
return { status: 400, body: { error: "invalid_owner" } };
|
|
202
|
+
}
|
|
203
|
+
const ttl = Math.min(ttlSeconds ?? CLOUD_PAIRING_DEFAULT_TTL_S, browser ? CLOUD_BROWSER_SIGN_IN_MAX_TTL_S : CLOUD_PAIRING_MAX_TTL_S);
|
|
151
204
|
const opened = sessions.openPairing({
|
|
152
205
|
scopes: ["admin", "client"],
|
|
153
206
|
label: typeof label === "string" && label.trim() ? label.trim() : "OMB Cloud",
|
|
154
207
|
ttlMs: ttl * 1000,
|
|
208
|
+
browser,
|
|
209
|
+
...(browser ? { owner: owner } : {}),
|
|
155
210
|
});
|
|
211
|
+
// A browser sign-in has no code to type: only its credential redeems it, and saying `purpose` back tells the
|
|
212
|
+
// Admin this machine made one (a machine from before this ignores `purpose` and opens an ordinary window).
|
|
213
|
+
if (browser)
|
|
214
|
+
return { status: 200, body: { credential: opened.credential, expiresAt: opened.expiresAt, purpose: "browser" } };
|
|
156
215
|
return { status: 200, body: { code: formatPairingCode(opened.code), credential: opened.credential, expiresAt: opened.expiresAt } };
|
|
157
216
|
},
|
|
158
217
|
};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Who may use a Mac lent to an OMB Cloud home (docs/cloud-pro.md, "Let my
|
|
2
|
+
// Cloud use this Mac"). A Cloud home is one person's server, but not every
|
|
3
|
+
// turn on it is theirs: a guest the owner paired, a webhook's payload, or a
|
|
4
|
+
// line someone else slipped into a running turn must never reach the Mac.
|
|
5
|
+
//
|
|
6
|
+
// A turn may use the lent Mac only when the harness can prove it acts for the
|
|
7
|
+
// owner:
|
|
8
|
+
// - a conversation the owner started from one of their own devices (a live
|
|
9
|
+
// session with admin scope), or
|
|
10
|
+
// - a scheduled run of a routine whose instructions the owner wrote (created
|
|
11
|
+
// or edited from one of those devices), or a run the owner started by hand;
|
|
12
|
+
// and the conversation was opened by them and holds nobody else's words,
|
|
13
|
+
// ever: a resumed session carries everything said in it, so one line (or
|
|
14
|
+
// card answer) from a guest, a teammate bot or a local process, before or
|
|
15
|
+
// during the turn, takes that conversation out of lending. The owner starts a new one to lend again.
|
|
16
|
+
// Webhook runs, guests, rooms, a bot's delegated or peer turn, and anything
|
|
17
|
+
// unprovable never can.
|
|
18
|
+
// Everything here is pure; server/index.ts supplies the records.
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
21
|
+
import { z } from "zod";
|
|
22
|
+
import { writeFileAtomic } from "./atomic.js";
|
|
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
|
+
export function cloudHomeLendingRefusal(turn) {
|
|
28
|
+
const { request } = turn;
|
|
29
|
+
if (!request?.messageId || request.stopped || !request.generations.has(turn.generation))
|
|
30
|
+
return "unproven";
|
|
31
|
+
const index = turn.thread.findIndex(line => line.id === request.messageId);
|
|
32
|
+
if (index < 0)
|
|
33
|
+
return "unproven";
|
|
34
|
+
let owner;
|
|
35
|
+
if (request.automation === undefined) {
|
|
36
|
+
owner = turn.ownerPerson(personOf(turn.thread[index]));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const run = turn.routineRun();
|
|
40
|
+
owner = request.automation !== "webhook" && Boolean(run) && run.triggerSource !== "webhook" &&
|
|
41
|
+
run.ownerAuthored && (run.triggerSource !== "manual" || run.ownerStarted);
|
|
42
|
+
}
|
|
43
|
+
if (!owner)
|
|
44
|
+
return "not-owner";
|
|
45
|
+
// A conversation someone else opened (and named) is theirs, whoever writes in it.
|
|
46
|
+
if (turn.starters?.some((person) => person !== undefined && !turn.ownerPerson(person)))
|
|
47
|
+
return "someone-else";
|
|
48
|
+
if (turn.reportsFromOthers)
|
|
49
|
+
return "someone-else";
|
|
50
|
+
// Nothing anyone else wrote (a guest, a teammate bot, a local process) may
|
|
51
|
+
// be anywhere in this conversation, before or after the request, as a line
|
|
52
|
+
// (sent, queued, steered or handed in) or as the answer to a card. A
|
|
53
|
+
// routine's own prompt line names nobody; its provenance was proven above.
|
|
54
|
+
const clean = ownerOnlyConversation(turn.thread, turn.ownerPerson, turn.cardAnswerer, request.automation !== undefined ? request.messageId : undefined);
|
|
55
|
+
return clean ? null : "someone-else";
|
|
56
|
+
}
|
|
57
|
+
/** A conversation only the owner has written in: every user line is theirs
|
|
58
|
+
* and every card someone answered was answered by them (`exempt`: one line,
|
|
59
|
+
* a routine's own prompt, that names nobody). The same test decides which of
|
|
60
|
+
* a bot's other conversations may feed a turn on a Cloud home (recall, the
|
|
61
|
+
* recent-work brief, memory capture, the daily log). */
|
|
62
|
+
export function ownerOnlyConversation(thread, ownerPerson, cardAnswerer, exempt) {
|
|
63
|
+
return thread.every(line => (line.role !== "user" || line.id === exempt || ownerPerson(personOf(line))) &&
|
|
64
|
+
(!answeredByPerson(line.card) || ownerPerson(cardAnswerer(line.card))));
|
|
65
|
+
}
|
|
66
|
+
/** A card someone answered: a person's verdict or words. The harness's own
|
|
67
|
+
* settlements (automatic approvals, a turn that ended) are marked dismissed
|
|
68
|
+
* or unavailable and carry nobody's words. */
|
|
69
|
+
function answeredByPerson(card) {
|
|
70
|
+
if (!card)
|
|
71
|
+
return false;
|
|
72
|
+
return (Boolean(card.answered) && card.answered !== "unavailable" && card.dismissed !== true) || Boolean(card.answeredText);
|
|
73
|
+
}
|
|
74
|
+
/** What a routine run does and when, reduced to what its author decides:
|
|
75
|
+
* the instructions, where and on what it runs, and its schedule. The
|
|
76
|
+
* scheduler never rewrites these (it does rewrite a routine's results thread
|
|
77
|
+
* on each run, so that is guarded by who edits the routine instead: see the
|
|
78
|
+
* routines PATCH route). An edit by anyone else changes the fingerprint. */
|
|
79
|
+
export function routineFingerprint(routine) {
|
|
80
|
+
const attachments = (routine.attachments ?? []).map(attachment => [attachment.id, attachment.path]);
|
|
81
|
+
return createHash("sha256").update(JSON.stringify([
|
|
82
|
+
routine.prompt ?? "", routine.target ?? null, routine.botId ?? "", routine.groupId ?? null, attachments,
|
|
83
|
+
routine.schedule ?? null, routine.runOn ?? null,
|
|
84
|
+
])).digest("hex");
|
|
85
|
+
}
|
|
86
|
+
const authorsFile = z.object({
|
|
87
|
+
version: z.literal(1),
|
|
88
|
+
routines: z.record(z.string().max(128), z.string().regex(/^[a-f0-9]{64}$/)),
|
|
89
|
+
/** Whoever last wrote each routine (a person key), owner or not. */
|
|
90
|
+
writers: z.record(z.string().max(128), z.string().regex(/^p_[\w-]{22}$/)).optional(),
|
|
91
|
+
}).strict();
|
|
92
|
+
/** Which routines the owner wrote, as the fingerprint of what they wrote.
|
|
93
|
+
* Persisted beside the routines (owner-only). A missing or damaged file
|
|
94
|
+
* means none: it never grants anything by being absent. */
|
|
95
|
+
export function createCloudRoutineAuthors(file) {
|
|
96
|
+
let routines = {};
|
|
97
|
+
let writers = {};
|
|
98
|
+
try {
|
|
99
|
+
if (existsSync(file)) {
|
|
100
|
+
const stat = lstatSync(file);
|
|
101
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.size <= 1_000_000) {
|
|
102
|
+
const parsed = authorsFile.parse(JSON.parse(readFileSync(file, "utf8")));
|
|
103
|
+
routines = parsed.routines;
|
|
104
|
+
writers = parsed.writers ?? {};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
routines = {};
|
|
110
|
+
writers = {};
|
|
111
|
+
}
|
|
112
|
+
const save = () => writeFileAtomic(file, JSON.stringify({ version: 1, routines, writers }), { mode: 0o600 });
|
|
113
|
+
return {
|
|
114
|
+
/** The owner wrote this routine as it stands now. */
|
|
115
|
+
record(id, routine) { routines = { ...routines, [id]: routineFingerprint(routine) }; save(); },
|
|
116
|
+
/** The owner's authorship ends; whoever wrote it last is still named. */
|
|
117
|
+
forget(id) { if (Object.hasOwn(routines, id)) {
|
|
118
|
+
const next = { ...routines };
|
|
119
|
+
delete next[id];
|
|
120
|
+
routines = next;
|
|
121
|
+
save();
|
|
122
|
+
} },
|
|
123
|
+
/** Someone (a person key) created or edited this routine. */
|
|
124
|
+
wrote(id, person) { if (writers[id] !== person) {
|
|
125
|
+
writers = { ...writers, [id]: person };
|
|
126
|
+
save();
|
|
127
|
+
} },
|
|
128
|
+
/** Who last wrote this routine, when that is known. */
|
|
129
|
+
writer(id) { return Object.hasOwn(writers, id) ? writers[id] : undefined; },
|
|
130
|
+
/** The routine is gone: nothing about it is kept. */
|
|
131
|
+
remove(id) {
|
|
132
|
+
if (!Object.hasOwn(routines, id) && !Object.hasOwn(writers, id))
|
|
133
|
+
return;
|
|
134
|
+
const nextRoutines = { ...routines };
|
|
135
|
+
delete nextRoutines[id];
|
|
136
|
+
routines = nextRoutines;
|
|
137
|
+
const nextWriters = { ...writers };
|
|
138
|
+
delete nextWriters[id];
|
|
139
|
+
writers = nextWriters;
|
|
140
|
+
save();
|
|
141
|
+
},
|
|
142
|
+
/** Whether this routine, as it stands (or as a run snapshotted it), is
|
|
143
|
+
* exactly what the owner wrote. */
|
|
144
|
+
authored(id, routine) { return Object.hasOwn(routines, id) && routines[id] === routineFingerprint(routine); },
|
|
145
|
+
/** Whether the owner's fingerprint is on record for this routine at all,
|
|
146
|
+
* matching what it runs now or not. */
|
|
147
|
+
recorded(id) { return Object.hasOwn(routines, id); },
|
|
148
|
+
/** Everyone named as a writer (server/cloud-owner.ts). */
|
|
149
|
+
people() { return new Set(Object.values(writers)); },
|
|
150
|
+
/** Name `to` as the writer wherever `move` accepts the writer: a one-time
|
|
151
|
+
* migration (server/cloud-owner.ts). The routines that changed. */
|
|
152
|
+
reassign(move, to) {
|
|
153
|
+
const moved = [];
|
|
154
|
+
const next = { ...writers };
|
|
155
|
+
for (const [id, person] of Object.entries(writers))
|
|
156
|
+
if (person !== to && move(person)) {
|
|
157
|
+
next[id] = to;
|
|
158
|
+
moved.push(id);
|
|
159
|
+
}
|
|
160
|
+
if (moved.length) {
|
|
161
|
+
writers = next;
|
|
162
|
+
save();
|
|
163
|
+
}
|
|
164
|
+
return moved;
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|