@ricsam/r5d-worker 0.0.137 → 0.0.139
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -94
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +46 -7543
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/action-scheduler.mjs +31 -0
- package/dist/mjs/personal/client.mjs +215 -0
- package/dist/mjs/personal/runtime.mjs +359 -0
- package/dist/mjs/personal/tcp.mjs +217 -0
- package/dist/mjs/runtime/adapter.mjs +42 -0
- package/dist/mjs/runtime/client.mjs +95 -0
- package/dist/mjs/runtime/daemon.mjs +131 -0
- package/dist/mjs/runtime/executor.mjs +436 -0
- package/dist/mjs/runtime/index.mjs +15 -0
- package/dist/mjs/runtime/protocol.mjs +139 -0
- package/dist/mjs/runtime/pty-host.mjs +156 -0
- package/dist/mjs/runtime/pty.mjs +127 -0
- package/dist/mjs/runtime/releases/artifacts.mjs +201 -0
- package/dist/mjs/runtime/releases/grants.mjs +93 -0
- package/dist/mjs/runtime/releases/main.mjs +101 -0
- package/dist/mjs/runtime/releases/manager.mjs +541 -0
- package/dist/mjs/runtime/releases/process.mjs +88 -0
- package/dist/mjs/runtime/releases/retirement.mjs +18 -0
- package/dist/mjs/runtime/releases/rpc-client.mjs +184 -0
- package/dist/mjs/runtime/releases/rpc-main.mjs +213 -0
- package/dist/mjs/runtime/releases/rpc-protocol.mjs +103 -0
- package/dist/mjs/runtime/storage.mjs +156 -0
- package/dist/mjs/runtime/workspace/artifacts.mjs +121 -0
- package/dist/mjs/runtime/workspace/authority.mjs +1355 -0
- package/dist/mjs/runtime/workspace/contracts.mjs +60 -0
- package/dist/mjs/runtime/workspace/file-write.mjs +23 -0
- package/dist/mjs/runtime/workspace/files.mjs +308 -0
- package/dist/mjs/runtime/workspace/http-body.mjs +45 -0
- package/dist/mjs/runtime/workspace/storage-client.mjs +59 -0
- package/dist/mjs/runtime/workspace/storage-wire.mjs +53 -0
- package/dist/types/main.d.ts +0 -1167
- package/dist/types/personal/action-scheduler.d.ts +23 -0
- package/dist/types/personal/client.d.ts +14 -0
- package/dist/types/personal/runtime.d.ts +121 -0
- package/dist/types/personal/tcp.d.ts +14 -0
- package/dist/types/runtime/client.d.ts +3 -1
- package/dist/types/runtime/executor.d.ts +6 -1
- package/dist/types/runtime/protocol.d.ts +176 -4
- package/dist/types/runtime/pty.d.ts +1 -0
- package/dist/types/runtime/releases/artifacts.d.ts +51 -0
- package/dist/types/runtime/releases/grants.d.ts +16 -0
- package/dist/types/runtime/releases/manager.d.ts +145 -0
- package/dist/types/runtime/releases/process.d.ts +14 -0
- package/dist/types/runtime/releases/retirement.d.ts +15 -0
- package/dist/types/runtime/releases/rpc-client.d.ts +52 -0
- package/dist/types/runtime/releases/rpc-main.d.ts +10 -0
- package/dist/types/runtime/releases/rpc-protocol.d.ts +1139 -0
- package/dist/types/runtime/workspace/artifacts.d.ts +30 -0
- package/dist/types/runtime/workspace/authority.d.ts +338 -0
- package/dist/types/runtime/workspace/contracts.d.ts +86 -0
- package/dist/types/runtime/workspace/file-write.d.ts +14 -0
- package/dist/types/runtime/workspace/files.d.ts +21 -0
- package/dist/types/runtime/workspace/http-body.d.ts +9 -0
- package/dist/types/runtime/workspace/storage-client.d.ts +23 -0
- package/dist/types/runtime/workspace/storage-wire.d.ts +138 -0
- package/package.json +8 -6
- package/dist/cjs/atomic-rename.cjs +0 -303
- package/dist/cjs/browser-upload.cjs +0 -73
- package/dist/cjs/cli-update.cjs +0 -108
- package/dist/cjs/command-launcher.cjs +0 -423
- package/dist/cjs/control-command-policy.cjs +0 -177
- package/dist/cjs/git-blob-hash.cjs +0 -41
- package/dist/cjs/git-identity.cjs +0 -39
- package/dist/cjs/git-process-environment.cjs +0 -133
- package/dist/cjs/inline-output.cjs +0 -62
- package/dist/cjs/main.cjs +0 -7517
- package/dist/cjs/managed-paths.cjs +0 -215
- package/dist/cjs/plan-lint.cjs +0 -165
- package/dist/cjs/plan-parser.cjs +0 -127
- package/dist/cjs/plan-paths.cjs +0 -53
- package/dist/cjs/port-forward-client.cjs +0 -206
- package/dist/cjs/process-tree.cjs +0 -117
- package/dist/cjs/project-checkout-garbage.cjs +0 -219
- package/dist/cjs/project-mirror-fetch-policy.cjs +0 -82
- package/dist/cjs/project-mirror-refs-token.cjs +0 -85
- package/dist/cjs/project-workspace-state.cjs +0 -972
- package/dist/cjs/project-worktrees.cjs +0 -1713
- package/dist/cjs/pty-output-coalescer.cjs +0 -87
- package/dist/cjs/recovery-journal-protocol.cjs +0 -56
- package/dist/cjs/recovery-journal-runtime.cjs +0 -133
- package/dist/cjs/recovery-journal-thread.cjs +0 -9
- package/dist/cjs/recovery-journal.cjs +0 -742
- package/dist/cjs/recovery-output-window.cjs +0 -77
- package/dist/cjs/recovery-protocol.cjs +0 -31
- package/dist/cjs/recovery-store.cjs +0 -252
- package/dist/cjs/registry-auth.cjs +0 -310
- package/dist/cjs/repository-transition-policy.cjs +0 -49
- package/dist/cjs/runtime-version.cjs +0 -88
- package/dist/cjs/session-file-mutations.cjs +0 -61
- package/dist/cjs/supervisor.cjs +0 -147
- package/dist/cjs/three-way-merge.cjs +0 -346
- package/dist/cjs/working-tree-mirror.cjs +0 -1872
- package/dist/cjs/workspace-automatic-sync-policy.cjs +0 -79
- package/dist/cjs/workspace-branch-incarnation-policy.cjs +0 -37
- package/dist/cjs/workspace-command-sync-policy.cjs +0 -112
- package/dist/cjs/workspace-command-targets.cjs +0 -63
- package/dist/cjs/workspace-filesystem-executor-thread.cjs +0 -36
- package/dist/cjs/workspace-filesystem-executor.cjs +0 -327
- package/dist/cjs/workspace-filesystem-job-types.cjs +0 -144
- package/dist/cjs/workspace-filesystem-jobs.cjs +0 -59
- package/dist/cjs/workspace-git-sync.cjs +0 -3595
- package/dist/cjs/workspace-hydration-ledger.cjs +0 -83
- package/dist/cjs/workspace-hydration-merge.cjs +0 -448
- package/dist/cjs/workspace-hydration-recovery-state.cjs +0 -53
- package/dist/cjs/workspace-incident-state.cjs +0 -37
- package/dist/cjs/workspace-merge-projection.cjs +0 -628
- package/dist/cjs/workspace-mount-boundary.cjs +0 -71
- package/dist/cjs/workspace-mount-hold-fence.cjs +0 -120
- package/dist/cjs/workspace-mutation-gate.cjs +0 -127
- package/dist/cjs/workspace-path-move.cjs +0 -195
- package/dist/cjs/workspace-preserve-only-policy.cjs +0 -36
- package/dist/cjs/workspace-project-config-policy.cjs +0 -53
- package/dist/cjs/workspace-projection-ledger.cjs +0 -81
- package/dist/cjs/workspace-publication-evidence.cjs +0 -46
- package/dist/cjs/workspace-sync-coalescer.cjs +0 -58
- package/dist/mjs/atomic-rename.mjs +0 -261
- package/dist/mjs/browser-upload.mjs +0 -42
- package/dist/mjs/command-launcher.mjs +0 -393
- package/dist/mjs/control-command-policy.mjs +0 -146
- package/dist/mjs/git-blob-hash.mjs +0 -16
- package/dist/mjs/git-identity.mjs +0 -15
- package/dist/mjs/git-process-environment.mjs +0 -105
- package/dist/mjs/inline-output.mjs +0 -37
- package/dist/mjs/managed-paths.mjs +0 -174
- package/dist/mjs/plan-lint.mjs +0 -129
- package/dist/mjs/plan-parser.mjs +0 -94
- package/dist/mjs/plan-paths.mjs +0 -25
- package/dist/mjs/port-forward-client.mjs +0 -172
- package/dist/mjs/project-checkout-garbage.mjs +0 -178
- package/dist/mjs/project-mirror-fetch-policy.mjs +0 -56
- package/dist/mjs/project-mirror-refs-token.mjs +0 -53
- package/dist/mjs/project-workspace-state.mjs +0 -937
- package/dist/mjs/project-worktrees.mjs +0 -1657
- package/dist/mjs/pty-output-coalescer.mjs +0 -61
- package/dist/mjs/recovery-journal-protocol.mjs +0 -30
- package/dist/mjs/recovery-journal-runtime.mjs +0 -112
- package/dist/mjs/recovery-journal-thread.mjs +0 -8
- package/dist/mjs/recovery-journal.mjs +0 -694
- package/dist/mjs/recovery-output-window.mjs +0 -53
- package/dist/mjs/recovery-protocol.mjs +0 -6
- package/dist/mjs/recovery-store.mjs +0 -217
- package/dist/mjs/registry-auth.mjs +0 -269
- package/dist/mjs/repository-transition-policy.mjs +0 -22
- package/dist/mjs/runtime-version.mjs +0 -54
- package/dist/mjs/session-file-mutations.mjs +0 -37
- package/dist/mjs/supervisor.mjs +0 -114
- package/dist/mjs/three-way-merge.mjs +0 -318
- package/dist/mjs/working-tree-mirror.mjs +0 -1817
- package/dist/mjs/workspace-automatic-sync-policy.mjs +0 -48
- package/dist/mjs/workspace-branch-incarnation-policy.mjs +0 -13
- package/dist/mjs/workspace-command-sync-policy.mjs +0 -84
- package/dist/mjs/workspace-command-targets.mjs +0 -37
- package/dist/mjs/workspace-filesystem-executor-thread.mjs +0 -38
- package/dist/mjs/workspace-filesystem-executor.mjs +0 -287
- package/dist/mjs/workspace-filesystem-job-types.mjs +0 -116
- package/dist/mjs/workspace-filesystem-jobs.mjs +0 -55
- package/dist/mjs/workspace-git-sync.mjs +0 -3553
- package/dist/mjs/workspace-hydration-ledger.mjs +0 -58
- package/dist/mjs/workspace-hydration-merge.mjs +0 -414
- package/dist/mjs/workspace-hydration-recovery-state.mjs +0 -29
- package/dist/mjs/workspace-incident-state.mjs +0 -12
- package/dist/mjs/workspace-merge-projection.mjs +0 -594
- package/dist/mjs/workspace-mount-boundary.mjs +0 -37
- package/dist/mjs/workspace-mount-hold-fence.mjs +0 -95
- package/dist/mjs/workspace-mutation-gate.mjs +0 -103
- package/dist/mjs/workspace-path-move.mjs +0 -160
- package/dist/mjs/workspace-preserve-only-policy.mjs +0 -11
- package/dist/mjs/workspace-project-config-policy.mjs +0 -27
- package/dist/mjs/workspace-projection-ledger.mjs +0 -57
- package/dist/mjs/workspace-publication-evidence.mjs +0 -21
- package/dist/mjs/workspace-sync-coalescer.mjs +0 -34
- package/dist/types/atomic-rename.d.ts +0 -78
- package/dist/types/browser-upload.d.ts +0 -7
- package/dist/types/command-launcher.d.ts +0 -104
- package/dist/types/control-command-policy.d.ts +0 -53
- package/dist/types/git-blob-hash.d.ts +0 -10
- package/dist/types/git-identity.d.ts +0 -9
- package/dist/types/git-process-environment.d.ts +0 -9
- package/dist/types/inline-output.d.ts +0 -3
- package/dist/types/managed-paths.d.ts +0 -19
- package/dist/types/plan-lint.d.ts +0 -16
- package/dist/types/plan-parser.d.ts +0 -38
- package/dist/types/plan-paths.d.ts +0 -36
- package/dist/types/port-forward-client.d.ts +0 -8
- package/dist/types/project-checkout-garbage.d.ts +0 -86
- package/dist/types/project-mirror-fetch-policy.d.ts +0 -84
- package/dist/types/project-mirror-refs-token.d.ts +0 -53
- package/dist/types/project-workspace-state.d.ts +0 -188
- package/dist/types/project-worktrees.d.ts +0 -397
- package/dist/types/pty-output-coalescer.d.ts +0 -20
- package/dist/types/recovery-journal-protocol.d.ts +0 -35
- package/dist/types/recovery-journal-runtime.d.ts +0 -12
- package/dist/types/recovery-journal-stall-fixture.d.ts +0 -1
- package/dist/types/recovery-journal-thread.d.ts +0 -1
- package/dist/types/recovery-journal.d.ts +0 -248
- package/dist/types/recovery-output-window.d.ts +0 -19
- package/dist/types/recovery-protocol.d.ts +0 -59
- package/dist/types/recovery-store.d.ts +0 -52
- package/dist/types/registry-auth.d.ts +0 -47
- package/dist/types/repository-transition-policy.d.ts +0 -26
- package/dist/types/runtime-version.d.ts +0 -1
- package/dist/types/session-file-mutations.d.ts +0 -22
- package/dist/types/supervisor-daemonized-fixture.d.ts +0 -1
- package/dist/types/supervisor-launcher-cleanup-fixture.d.ts +0 -1
- package/dist/types/supervisor-signal-fixture.d.ts +0 -1
- package/dist/types/supervisor.d.ts +0 -23
- package/dist/types/three-way-merge.d.ts +0 -77
- package/dist/types/working-tree-mirror.d.ts +0 -424
- package/dist/types/workspace-automatic-sync-policy.d.ts +0 -57
- package/dist/types/workspace-branch-incarnation-policy.d.ts +0 -14
- package/dist/types/workspace-command-sync-policy.d.ts +0 -57
- package/dist/types/workspace-command-targets.d.ts +0 -37
- package/dist/types/workspace-filesystem-executor-thread.d.ts +0 -1
- package/dist/types/workspace-filesystem-executor.d.ts +0 -123
- package/dist/types/workspace-filesystem-job-types.d.ts +0 -302
- package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
- package/dist/types/workspace-git-sync.d.ts +0 -514
- package/dist/types/workspace-hydration-ledger.d.ts +0 -76
- package/dist/types/workspace-hydration-merge.d.ts +0 -103
- package/dist/types/workspace-hydration-recovery-state.d.ts +0 -10
- package/dist/types/workspace-incident-state.d.ts +0 -19
- package/dist/types/workspace-merge-projection.d.ts +0 -69
- package/dist/types/workspace-mount-boundary.d.ts +0 -10
- package/dist/types/workspace-mount-hold-fence.d.ts +0 -42
- package/dist/types/workspace-mutation-gate.d.ts +0 -42
- package/dist/types/workspace-path-move.d.ts +0 -21
- package/dist/types/workspace-preserve-only-policy.d.ts +0 -15
- package/dist/types/workspace-project-config-policy.d.ts +0 -48
- package/dist/types/workspace-projection-ledger.d.ts +0 -59
- package/dist/types/workspace-publication-evidence.d.ts +0 -17
- package/dist/types/workspace-sync-coalescer.d.ts +0 -28
- /package/dist/types/{credential-authority-lock-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// node-pty's Unix FD integration needs Node, not Bun. This helper belongs to the
|
|
2
|
+
// keeper lifetime, never the adapter. Its IPC is inherited anonymous pipes only.
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import { Buffer } from "node:buffer";
|
|
5
|
+
import { write, constants, openSync, closeSync, lstatSync, fstatSync, fchmodSync, readSync, realpathSync } from "node:fs";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
10
|
+
import pty from "node-pty";
|
|
11
|
+
import readline from "node:readline";
|
|
12
|
+
|
|
13
|
+
/** node-pty 1.1.0 ships Darwin's spawn-helper without executable permission. */
|
|
14
|
+
export function ensureDarwinSpawnHelperPermissions({ platform = process.platform, arch = process.arch, currentUid = process.getuid?.(), requirePty = createRequire(import.meta.url) } = {}) {
|
|
15
|
+
if (platform !== "darwin") return false;
|
|
16
|
+
if (!["x64", "arm64"].includes(arch) || !Number.isSafeInteger(currentUid)) throw new Error("pty_native_helper_unavailable");
|
|
17
|
+
const manifest = requirePty("node-pty/package.json");
|
|
18
|
+
if (manifest.name !== "node-pty" || manifest.version !== "1.1.0") throw new Error("pty_native_helper_version");
|
|
19
|
+
const packageRoot = path.dirname(requirePty.resolve("node-pty/package.json"));
|
|
20
|
+
const library = path.dirname(requirePty.resolve("node-pty/lib/unixTerminal.js"));
|
|
21
|
+
const selected = requirePty("node-pty/lib/utils.js").loadNativeModule("pty");
|
|
22
|
+
const directory = path.resolve(library, selected.dir);
|
|
23
|
+
const allowed = ["build/Release", "build/Debug", `prebuilds/darwin-${arch}`].flatMap(relative => [path.join(packageRoot, relative), path.join(packageRoot, "lib", relative)]);
|
|
24
|
+
if (!allowed.includes(directory)) throw new Error("pty_native_helper_path");
|
|
25
|
+
const filename = path.join(directory, "spawn-helper");
|
|
26
|
+
let ancestor = path.parse(filename).root;
|
|
27
|
+
for (const segment of path.dirname(filename).slice(ancestor.length).split(path.sep).filter(Boolean)) {
|
|
28
|
+
ancestor = path.join(ancestor, segment);
|
|
29
|
+
const info = lstatSync(ancestor);
|
|
30
|
+
if (!info.isDirectory() || info.isSymbolicLink() || ![0, currentUid].includes(info.uid) || ((info.mode & 0o022) && !(info.mode & 0o1000))) throw new Error("pty_native_helper_unsafe");
|
|
31
|
+
}
|
|
32
|
+
const fd = openSync(filename, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
33
|
+
try {
|
|
34
|
+
const before = fstatSync(fd);
|
|
35
|
+
if (!before.isFile() || before.nlink !== 1 || ![0, currentUid].includes(before.uid) || (before.mode & 0o7022) || before.size < 1 || before.size > 1024 * 1024) throw new Error("pty_native_helper_unsafe");
|
|
36
|
+
const target = before.mode | ((before.mode & 0o444) >> 2);
|
|
37
|
+
if (!(target & 0o111)) throw new Error("pty_native_helper_not_executable");
|
|
38
|
+
if (target === before.mode) return false;
|
|
39
|
+
if (before.uid !== currentUid) throw new Error("pty_native_helper_not_owned");
|
|
40
|
+
const bytes = Buffer.alloc(before.size);
|
|
41
|
+
if (readSync(fd, bytes, 0, bytes.length, 0) !== bytes.length) throw new Error("pty_native_helper_changed");
|
|
42
|
+
const named = lstatSync(filename);
|
|
43
|
+
if (named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink()) throw new Error("pty_native_helper_changed");
|
|
44
|
+
fchmodSync(fd, target & 0o777);
|
|
45
|
+
const after = fstatSync(fd), sameName = lstatSync(filename), sameBytes = Buffer.alloc(before.size);
|
|
46
|
+
if (after.dev !== before.dev || after.ino !== before.ino || after.uid !== before.uid || after.nlink !== 1 || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.mode !== target || sameName.dev !== before.dev || sameName.ino !== before.ino || readSync(fd, sameBytes, 0, sameBytes.length, 0) !== sameBytes.length || !bytes.equals(sameBytes)) throw new Error("pty_native_helper_changed");
|
|
47
|
+
return true;
|
|
48
|
+
} finally { closeSync(fd); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function startPtyHost() {
|
|
52
|
+
try { ensureDarwinSpawnHelperPermissions(); }
|
|
53
|
+
catch {
|
|
54
|
+
process.stderr.write("pty_native_helper_unavailable: use a supported, user-owned node-pty 1.1.0 installation with executable spawn-helper\n");
|
|
55
|
+
process.exit(78);
|
|
56
|
+
}
|
|
57
|
+
if (process.argv[2] === "--preflight") { process.stdout.write("pty-preflight-ok\n"); return; }
|
|
58
|
+
|
|
59
|
+
let terminal;
|
|
60
|
+
let initialized = false;
|
|
61
|
+
let terminalExited = false;
|
|
62
|
+
const send = (message) => process.stdout.write(JSON.stringify(message) + "\n");
|
|
63
|
+
const input = readline.createInterface({ input: process.stdin });
|
|
64
|
+
let serial = Promise.resolve();
|
|
65
|
+
input.on("line", (line) => {
|
|
66
|
+
serial = serial.then(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const message = JSON.parse(line);
|
|
69
|
+
if (!initialized) {
|
|
70
|
+
if (message.method !== "start") throw new Error("Expected initialization");
|
|
71
|
+
initialized = true;
|
|
72
|
+
terminal = pty.spawn(message.argv[0], message.argv.slice(1), {
|
|
73
|
+
cwd: message.cwd,
|
|
74
|
+
env: message.env,
|
|
75
|
+
name: "xterm-256color",
|
|
76
|
+
cols: message.cols,
|
|
77
|
+
rows: message.rows,
|
|
78
|
+
encoding: null,
|
|
79
|
+
});
|
|
80
|
+
// node-pty 1.1.x UnixTerminal exposes fd. Fail closed if this pinned native
|
|
81
|
+
// contract changes; its public write() only enqueues and cannot ACK delivery.
|
|
82
|
+
if (!Number.isSafeInteger(terminal.fd) || terminal.fd < 0) throw new Error("PTY descriptor unavailable");
|
|
83
|
+
send({ event: "ready", pid: terminal.pid });
|
|
84
|
+
terminal.onData((data) => {
|
|
85
|
+
if (!Buffer.isBuffer(data)) {
|
|
86
|
+
shutdown();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!send({ event: "data", data: data.toString("base64") })) terminal.pause();
|
|
90
|
+
});
|
|
91
|
+
process.stdout.on("drain", () => {
|
|
92
|
+
if (!terminalExited) terminal.resume();
|
|
93
|
+
});
|
|
94
|
+
terminal.onExit(({ exitCode, signal }) => {
|
|
95
|
+
terminalExited = true;
|
|
96
|
+
input.close();
|
|
97
|
+
process.stdin.destroy();
|
|
98
|
+
// Flush all queued output and the final event before process exit.
|
|
99
|
+
process.stdout.end(JSON.stringify({ event: "exit", exitCode, signal }) + "\n", () => process.exit(0));
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (terminalExited) return;
|
|
104
|
+
try {
|
|
105
|
+
if (message.method === "write") await writeTerminal(message.data);
|
|
106
|
+
else if (message.method === "resize") terminal.resize(message.cols, message.rows);
|
|
107
|
+
else throw new Error("Unknown method");
|
|
108
|
+
if (!terminalExited) send({ event: "ack", id: message.id });
|
|
109
|
+
} catch {
|
|
110
|
+
if (!terminalExited) send({ event: "action-error", id: message.id });
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
shutdown();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
async function writeTerminal(data) {
|
|
118
|
+
const buffer = Buffer.from(data, "utf8");
|
|
119
|
+
let offset = 0;
|
|
120
|
+
const deadline = Date.now() + 1500;
|
|
121
|
+
while (offset < buffer.length) {
|
|
122
|
+
if (terminalExited || Date.now() > deadline) throw new Error("Unknown input outcome");
|
|
123
|
+
try {
|
|
124
|
+
offset += await new Promise((resolve, reject) =>
|
|
125
|
+
write(terminal.fd, buffer, offset, buffer.length - offset, (error, count) => (error ? reject(error) : resolve(count))),
|
|
126
|
+
);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error.code !== "EAGAIN" && error.code !== "EINTR") throw error;
|
|
129
|
+
await delay(5);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function shutdown() {
|
|
134
|
+
if (terminal && !terminalExited) {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(-terminal.pid, "SIGKILL");
|
|
137
|
+
} catch {
|
|
138
|
+
/* Already gone. */
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
terminal.kill("SIGKILL");
|
|
142
|
+
} catch {
|
|
143
|
+
/* Already gone. */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
input.on("close", () => {
|
|
149
|
+
if (!terminalExited) shutdown();
|
|
150
|
+
});
|
|
151
|
+
process.on("SIGTERM", shutdown);
|
|
152
|
+
process.on("SIGINT", shutdown);
|
|
153
|
+
process.stdout.on("error", shutdown);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) startPtyHost();
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { ExecutorError } from "./protocol.mjs";
|
|
4
|
+
const nodeExecutable = typeof R5D_PACKAGED_NODE !== "undefined" && R5D_PACKAGED_NODE ? fileURLToPath(new URL("./bin/node", import.meta.url)) : Bun.which("node") ?? "node";
|
|
5
|
+
function preflightNativePty() {
|
|
6
|
+
if (process.platform !== "darwin") return;
|
|
7
|
+
const result = spawnSync(nodeExecutable, [fileURLToPath(new URL("./pty-host.mjs", import.meta.url)), "--preflight"], { encoding: "utf8", timeout: 5e3, env: { PATH: "/usr/local/bin:/usr/bin:/bin" } });
|
|
8
|
+
if (result.status !== 0 || typeof result.stdout !== "string" || result.stdout.trim() !== "pty-preflight-ok") throw new ExecutorError("pty_native_helper_unavailable", "Native PTY helper is unavailable; use a supported, user-owned node-pty 1.1.0 installation with executable spawn-helper");
|
|
9
|
+
}
|
|
10
|
+
class PtySession {
|
|
11
|
+
pid = 0;
|
|
12
|
+
sequence = 0;
|
|
13
|
+
pending = /* @__PURE__ */ new Map();
|
|
14
|
+
child;
|
|
15
|
+
onDataCallback;
|
|
16
|
+
onExitCallback;
|
|
17
|
+
buffered = [];
|
|
18
|
+
exit;
|
|
19
|
+
ready;
|
|
20
|
+
constructor(input) {
|
|
21
|
+
this.child = spawn(nodeExecutable, [fileURLToPath(new URL("./pty-host.mjs", import.meta.url))], {
|
|
22
|
+
env: { PATH: "/usr/local/bin:/usr/bin:/bin", HOME: input.cwd },
|
|
23
|
+
stdio: "pipe"
|
|
24
|
+
});
|
|
25
|
+
this.ready = new Promise((resolve, reject) => {
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
this.child.kill("SIGKILL");
|
|
28
|
+
reject(new ExecutorError("pty_start_timeout"));
|
|
29
|
+
}, 5e3);
|
|
30
|
+
let buffer = "";
|
|
31
|
+
const fail = () => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
reject(new ExecutorError("pty_helper_failed"));
|
|
34
|
+
for (const pending of this.pending.values()) pending.reject(new ExecutorError("pty_helper_failed"));
|
|
35
|
+
this.pending.clear();
|
|
36
|
+
this.deliverExit({ exitCode: -1, signal: 0 });
|
|
37
|
+
};
|
|
38
|
+
this.child.on("error", fail);
|
|
39
|
+
this.child.stdin.on("error", fail);
|
|
40
|
+
this.child.stdout.setEncoding("utf8");
|
|
41
|
+
this.child.stdout.on("data", (data) => {
|
|
42
|
+
buffer += data;
|
|
43
|
+
if (buffer.length > 1024 * 1024) {
|
|
44
|
+
this.child.kill("SIGKILL");
|
|
45
|
+
fail();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
while (buffer.includes("\n")) {
|
|
49
|
+
const n = buffer.indexOf("\n");
|
|
50
|
+
const event = JSON.parse(buffer.slice(0, n));
|
|
51
|
+
buffer = buffer.slice(n + 1);
|
|
52
|
+
if (event.event === "ready") {
|
|
53
|
+
this.pid = event.pid;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve();
|
|
56
|
+
} else if (event.event === "data") {
|
|
57
|
+
const bytes = Buffer.from(event.data, "base64");
|
|
58
|
+
if (this.onDataCallback) this.onDataCallback(bytes);
|
|
59
|
+
else this.buffered.push(bytes);
|
|
60
|
+
} else if (event.event === "exit") this.deliverExit({ exitCode: event.exitCode, signal: event.signal ?? 0 });
|
|
61
|
+
else if (event.event === "ack") {
|
|
62
|
+
this.pending.get(event.id)?.resolve();
|
|
63
|
+
this.pending.delete(event.id);
|
|
64
|
+
} else if (event.event === "action-error") {
|
|
65
|
+
this.pending.get(event.id)?.reject(new ExecutorError("pty_write_unknown"));
|
|
66
|
+
this.pending.delete(event.id);
|
|
67
|
+
} else fail();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
this.child.once("close", () => {
|
|
71
|
+
if (!this.exit) fail();
|
|
72
|
+
});
|
|
73
|
+
this.child.stdin.write(JSON.stringify({ method: "start", ...input }) + "\n");
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
deliverExit(event) {
|
|
77
|
+
if (this.exit) return;
|
|
78
|
+
this.exit = event;
|
|
79
|
+
this.onExitCallback?.(event);
|
|
80
|
+
}
|
|
81
|
+
onData(callback) {
|
|
82
|
+
this.onDataCallback = callback;
|
|
83
|
+
for (const data of this.buffered.splice(0)) callback(data);
|
|
84
|
+
}
|
|
85
|
+
onExit(callback) {
|
|
86
|
+
this.onExitCallback = callback;
|
|
87
|
+
if (this.exit) callback(this.exit);
|
|
88
|
+
}
|
|
89
|
+
request(message) {
|
|
90
|
+
if (this.exit) return Promise.reject(new ExecutorError("pty_exited"));
|
|
91
|
+
const id = ++this.sequence;
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
const timer = setTimeout(() => {
|
|
94
|
+
this.pending.delete(id);
|
|
95
|
+
reject(new ExecutorError("pty_write_unknown"));
|
|
96
|
+
}, 2e3);
|
|
97
|
+
this.pending.set(id, {
|
|
98
|
+
resolve: () => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
resolve();
|
|
101
|
+
},
|
|
102
|
+
reject: (error) => {
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
reject(error);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
this.child.stdin.write(JSON.stringify({ id, ...message }) + "\n");
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
write(data) {
|
|
111
|
+
return this.request({ method: "write", data });
|
|
112
|
+
}
|
|
113
|
+
resize(cols, rows) {
|
|
114
|
+
return this.request({ method: "resize", cols, rows });
|
|
115
|
+
}
|
|
116
|
+
kill(signal = "SIGTERM") {
|
|
117
|
+
if (!Number.isSafeInteger(this.pid) || this.pid <= 1 || this.exit) return;
|
|
118
|
+
try {
|
|
119
|
+
process.kill(-this.pid, signal);
|
|
120
|
+
} catch {
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
PtySession,
|
|
126
|
+
preflightNativePty
|
|
127
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
fstatSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
realpathSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { RuntimeHello, RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
22
|
+
import { ExecutorError } from "../protocol.mjs";
|
|
23
|
+
import { privateDirectory } from "../storage.mjs";
|
|
24
|
+
import { digest } from "./grants.mjs";
|
|
25
|
+
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
|
|
26
|
+
const memberPath = z.string().min(1).max(512).refine(
|
|
27
|
+
(path) => path.split("/").every((part) => /^[a-zA-Z0-9_.-]+$/.test(part) && part !== "." && part !== ".."),
|
|
28
|
+
"Unsafe artifact member"
|
|
29
|
+
);
|
|
30
|
+
const AdapterManifest = z.object({
|
|
31
|
+
version: z.literal(1),
|
|
32
|
+
installationId: RuntimeId,
|
|
33
|
+
workerId: RuntimeId,
|
|
34
|
+
hello: RuntimeHello,
|
|
35
|
+
entrypoint: memberPath,
|
|
36
|
+
files: z.array(
|
|
37
|
+
z.object({
|
|
38
|
+
path: memberPath,
|
|
39
|
+
sha256,
|
|
40
|
+
size: z.number().int().min(0).max(64 * 1024 * 1024),
|
|
41
|
+
executable: z.boolean()
|
|
42
|
+
}).strict()
|
|
43
|
+
).min(1).max(4096)
|
|
44
|
+
}).strict().superRefine((manifest, ctx) => {
|
|
45
|
+
if (manifest.hello.installationId !== manifest.installationId || manifest.hello.role !== "worker-adapter" || new Set(manifest.files.map((file) => file.path)).size !== manifest.files.length || !manifest.files.some((file) => file.path === manifest.entrypoint) || manifest.files.reduce((total, file) => total + file.size, 0) > 256 * 1024 * 1024)
|
|
46
|
+
ctx.addIssue({ code: "custom", message: "Invalid adapter inventory or identity" });
|
|
47
|
+
});
|
|
48
|
+
const bytesHash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
49
|
+
function within(root, path) {
|
|
50
|
+
const rel = relative(root, path);
|
|
51
|
+
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith("../");
|
|
52
|
+
}
|
|
53
|
+
function canonicalDirectory(path) {
|
|
54
|
+
if (!isAbsolute(path) || resolve(path) !== path) throw new ExecutorError("unsafe_release_path");
|
|
55
|
+
let cursor = path;
|
|
56
|
+
while (true) {
|
|
57
|
+
const stat = lstatSync(cursor);
|
|
58
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 18) !== 0 && !(stat.mode & 512))
|
|
59
|
+
throw new ExecutorError("unsafe_release_path");
|
|
60
|
+
if (cursor === dirname(cursor)) break;
|
|
61
|
+
cursor = dirname(cursor);
|
|
62
|
+
}
|
|
63
|
+
if (realpathSync(path) !== path) throw new ExecutorError("unsafe_release_path");
|
|
64
|
+
return path;
|
|
65
|
+
}
|
|
66
|
+
function outsideWorkspaces(path, workspaceRoots) {
|
|
67
|
+
canonicalDirectory(path);
|
|
68
|
+
for (const root of workspaceRoots) {
|
|
69
|
+
const workspace = realpathSync(root);
|
|
70
|
+
if (within(workspace, path) || within(path, workspace)) throw new ExecutorError("release_inside_workspace");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function syncDirectory(path) {
|
|
74
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
75
|
+
try {
|
|
76
|
+
fsyncSync(fd);
|
|
77
|
+
} finally {
|
|
78
|
+
closeSync(fd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function readMember(path, immutable) {
|
|
82
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
83
|
+
try {
|
|
84
|
+
const stat = fstatSync(fd);
|
|
85
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.size > 64 * 1024 * 1024 || stat.mode & (immutable ? 191 : 18))
|
|
86
|
+
throw new ExecutorError("unsafe_artifact_member");
|
|
87
|
+
return readFileSync(fd);
|
|
88
|
+
} finally {
|
|
89
|
+
closeSync(fd);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function inventory(root, immutable) {
|
|
93
|
+
const files = [];
|
|
94
|
+
let entries = 0;
|
|
95
|
+
function walk(dir) {
|
|
96
|
+
const directory = lstatSync(dir);
|
|
97
|
+
if (immutable && (directory.uid !== process.getuid?.() || (directory.mode & 511) !== 320))
|
|
98
|
+
throw new ExecutorError("artifact_mode_mismatch");
|
|
99
|
+
if (++entries > 8192) throw new ExecutorError("artifact_inventory_mismatch");
|
|
100
|
+
if (files.length > 4096) throw new ExecutorError("artifact_inventory_mismatch");
|
|
101
|
+
for (const name of readdirSync(dir)) {
|
|
102
|
+
const path = join(dir, name), stat = lstatSync(path);
|
|
103
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && !stat.isFile()) throw new ExecutorError("unsafe_artifact_member");
|
|
104
|
+
if (stat.isDirectory()) {
|
|
105
|
+
canonicalDirectory(path);
|
|
106
|
+
walk(path);
|
|
107
|
+
} else files.push(relative(root, path));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
walk(root);
|
|
111
|
+
return files.sort();
|
|
112
|
+
}
|
|
113
|
+
function verifyTree(root, manifest, immutable) {
|
|
114
|
+
canonicalDirectory(root);
|
|
115
|
+
if (JSON.stringify(inventory(root, immutable)) !== JSON.stringify(manifest.files.map((file) => file.path).sort()))
|
|
116
|
+
throw new ExecutorError("artifact_inventory_mismatch");
|
|
117
|
+
for (const file of manifest.files) {
|
|
118
|
+
const bytes = readMember(join(root, file.path), immutable);
|
|
119
|
+
if (bytes.length !== file.size || bytesHash(bytes) !== file.sha256) throw new ExecutorError("artifact_digest_mismatch");
|
|
120
|
+
if (immutable && (lstatSync(join(root, file.path)).mode & 511) !== (file.executable ? 320 : 256))
|
|
121
|
+
throw new ExecutorError("artifact_mode_mismatch");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
class ImmutableAdapterArtifacts {
|
|
125
|
+
constructor(root, installationId, workerId, workspaceRoots) {
|
|
126
|
+
this.root = root;
|
|
127
|
+
this.installationId = installationId;
|
|
128
|
+
this.workerId = workerId;
|
|
129
|
+
this.workspaceRoots = workspaceRoots;
|
|
130
|
+
privateDirectory(root);
|
|
131
|
+
outsideWorkspaces(root, workspaceRoots);
|
|
132
|
+
}
|
|
133
|
+
root;
|
|
134
|
+
installationId;
|
|
135
|
+
workerId;
|
|
136
|
+
workspaceRoots;
|
|
137
|
+
approved(input, approvalDigest) {
|
|
138
|
+
const manifest = AdapterManifest.parse(input);
|
|
139
|
+
if (manifest.installationId !== this.installationId || manifest.workerId !== this.workerId) throw new ExecutorError("wrong_resource");
|
|
140
|
+
if (digest(manifest) !== sha256.parse(approvalDigest)) throw new ExecutorError("unapproved_artifact");
|
|
141
|
+
return manifest;
|
|
142
|
+
}
|
|
143
|
+
stage(source, input, approvalDigest) {
|
|
144
|
+
const manifest = this.approved(input, approvalDigest);
|
|
145
|
+
outsideWorkspaces(source, this.workspaceRoots);
|
|
146
|
+
verifyTree(source, manifest, false);
|
|
147
|
+
const destination = join(this.root, approvalDigest);
|
|
148
|
+
if (existsSync(destination)) return this.verify(manifest, approvalDigest);
|
|
149
|
+
const temporary = join(this.root, `.staging-${randomUUID()}`);
|
|
150
|
+
privateDirectory(temporary);
|
|
151
|
+
try {
|
|
152
|
+
for (const file of manifest.files) {
|
|
153
|
+
const target = join(temporary, file.path);
|
|
154
|
+
mkdirSync(dirname(target), { recursive: true, mode: 448 });
|
|
155
|
+
const bytes = readMember(join(source, file.path), false);
|
|
156
|
+
if (bytes.length !== file.size || bytesHash(bytes) !== file.sha256) throw new ExecutorError("artifact_digest_mismatch");
|
|
157
|
+
const fd = openSync(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
158
|
+
try {
|
|
159
|
+
writeFileSync(fd, bytes);
|
|
160
|
+
fsyncSync(fd);
|
|
161
|
+
} finally {
|
|
162
|
+
closeSync(fd);
|
|
163
|
+
}
|
|
164
|
+
chmodSync(target, file.executable ? 320 : 256);
|
|
165
|
+
}
|
|
166
|
+
const seal = (dir) => {
|
|
167
|
+
for (const name of readdirSync(dir)) if (lstatSync(join(dir, name)).isDirectory()) seal(join(dir, name));
|
|
168
|
+
chmodSync(dir, 320);
|
|
169
|
+
syncDirectory(dir);
|
|
170
|
+
};
|
|
171
|
+
seal(temporary);
|
|
172
|
+
verifyTree(temporary, manifest, true);
|
|
173
|
+
renameSync(temporary, destination);
|
|
174
|
+
syncDirectory(this.root);
|
|
175
|
+
return destination;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (existsSync(temporary)) {
|
|
178
|
+
const unseal = (dir) => {
|
|
179
|
+
chmodSync(dir, 448);
|
|
180
|
+
for (const name of readdirSync(dir)) if (lstatSync(join(dir, name)).isDirectory()) unseal(join(dir, name));
|
|
181
|
+
};
|
|
182
|
+
unseal(temporary);
|
|
183
|
+
rmSync(temporary, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
verify(input, approvalDigest) {
|
|
189
|
+
const manifest = this.approved(input, approvalDigest), path = join(this.root, approvalDigest);
|
|
190
|
+
verifyTree(path, manifest, true);
|
|
191
|
+
return path;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
export {
|
|
195
|
+
AdapterManifest,
|
|
196
|
+
ImmutableAdapterArtifacts,
|
|
197
|
+
bytesHash,
|
|
198
|
+
canonicalDirectory,
|
|
199
|
+
outsideWorkspaces,
|
|
200
|
+
within
|
|
201
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative } from "node:path";
|
|
4
|
+
import { canonicalJson, negotiateProtocol } from "@ricsam/r5d-api/runtime-protocol";
|
|
5
|
+
import {
|
|
6
|
+
AdapterGrant,
|
|
7
|
+
ExecutorError,
|
|
8
|
+
EXECUTOR_CAPABILITY
|
|
9
|
+
} from "../protocol.mjs";
|
|
10
|
+
const digest = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
|
|
11
|
+
class AdapterGrantRegistry {
|
|
12
|
+
constructor(db, config) {
|
|
13
|
+
this.db = db;
|
|
14
|
+
this.config = config;
|
|
15
|
+
db.exec(`CREATE TABLE IF NOT EXISTS adapter_grants (
|
|
16
|
+
id TEXT PRIMARY KEY, owner TEXT NOT NULL UNIQUE, release TEXT NOT NULL UNIQUE,
|
|
17
|
+
grant_json TEXT NOT NULL, digest TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('registered','retired')))`);
|
|
18
|
+
for (const row of this.rows()) {
|
|
19
|
+
const grant = AdapterGrant.parse(JSON.parse(row.grant_json));
|
|
20
|
+
if (digest(grant) !== row.digest || grant.installationId !== config.installationId || grant.workerId !== config.workerId || config.principals.some((p) => p.id === grant.principal.id || p.ownerId === grant.principal.ownerId))
|
|
21
|
+
throw new ExecutorError("grant_store_conflict");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
db;
|
|
25
|
+
config;
|
|
26
|
+
rows() {
|
|
27
|
+
return this.db.query("SELECT * FROM adapter_grants").all();
|
|
28
|
+
}
|
|
29
|
+
find(id) {
|
|
30
|
+
const row = this.rows().find((row2) => row2.id === id && row2.state === "registered");
|
|
31
|
+
return row ? AdapterGrant.parse(JSON.parse(row.grant_json)) : void 0;
|
|
32
|
+
}
|
|
33
|
+
hasOwner(owner) {
|
|
34
|
+
return this.rows().some(
|
|
35
|
+
(row) => row.state === "registered" && AdapterGrant.parse(JSON.parse(row.grant_json)).principal.ownerId === owner
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
list() {
|
|
39
|
+
return this.rows().map((row) => {
|
|
40
|
+
const grant = AdapterGrant.parse(JSON.parse(row.grant_json));
|
|
41
|
+
return {
|
|
42
|
+
principalId: row.id,
|
|
43
|
+
ownerId: grant.principal.ownerId,
|
|
44
|
+
releaseId: grant.hello.releaseId,
|
|
45
|
+
artifactDigest: grant.artifactDigest,
|
|
46
|
+
grantDigest: row.digest,
|
|
47
|
+
state: row.state
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
register(controller, input, keeperHello) {
|
|
52
|
+
if (controller.role !== "controller") throw new ExecutorError("forbidden");
|
|
53
|
+
const grant = AdapterGrant.parse(input), p = grant.principal;
|
|
54
|
+
if (grant.installationId !== this.config.installationId || grant.workerId !== this.config.workerId)
|
|
55
|
+
throw new ExecutorError("wrong_resource");
|
|
56
|
+
negotiateProtocol(keeperHello, grant.hello, [EXECUTOR_CAPABILITY]);
|
|
57
|
+
if (grant.hello.role !== "worker-adapter" || grant.hello.instanceId !== p.ownerId) throw new ExecutorError("wrong_principal");
|
|
58
|
+
if (p.userIds.some((id) => !controller.userIds.includes(id)) || p.lanes.some((lane) => !controller.lanes.includes(lane)) || p.credentialIds.some((id) => !controller.credentialIds.includes(id)) || p.cwdRoots.some(
|
|
59
|
+
(root) => !controller.cwdRoots.some((allowed) => {
|
|
60
|
+
const rel = relative(realpathSync(allowed), realpathSync(root));
|
|
61
|
+
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith("../");
|
|
62
|
+
})
|
|
63
|
+
))
|
|
64
|
+
throw new ExecutorError("grant_scope_exceeded");
|
|
65
|
+
const hash = digest(grant), rows = this.rows(), old = rows.find((row) => row.id === p.id);
|
|
66
|
+
if (old) {
|
|
67
|
+
if (old.digest !== hash) throw new ExecutorError("grant_identity_conflict");
|
|
68
|
+
if (old.state === "retired") throw new ExecutorError("adapter_retired");
|
|
69
|
+
return this.list().find((r) => r.principalId === p.id);
|
|
70
|
+
}
|
|
71
|
+
if (this.config.principals.some((s) => s.id === p.id || s.ownerId === p.ownerId) || rows.some((row) => {
|
|
72
|
+
const prior = AdapterGrant.parse(JSON.parse(row.grant_json));
|
|
73
|
+
return prior.principal.ownerId === p.ownerId || prior.hello.releaseId === grant.hello.releaseId;
|
|
74
|
+
}))
|
|
75
|
+
throw new ExecutorError("grant_identity_conflict");
|
|
76
|
+
if (rows.filter((row) => row.state === "registered").length + this.config.principals.filter((p2) => p2.role === "adapter").length >= 2)
|
|
77
|
+
throw new ExecutorError("adapter_retention_blocked");
|
|
78
|
+
this.db.query("INSERT INTO adapter_grants VALUES (?,?,?,?,?,'registered')").run(p.id, p.ownerId, grant.hello.releaseId, canonicalJson(grant), hash);
|
|
79
|
+
return this.list().find((r) => r.principalId === p.id);
|
|
80
|
+
}
|
|
81
|
+
retire(controller, id, authoritativeOwner) {
|
|
82
|
+
if (controller.role !== "controller") throw new ExecutorError("forbidden");
|
|
83
|
+
const row = this.list().find((row2) => row2.principalId === id);
|
|
84
|
+
if (!row) throw new ExecutorError("adapter_not_found");
|
|
85
|
+
if (row.ownerId === authoritativeOwner) throw new ExecutorError("adapter_is_authority");
|
|
86
|
+
this.db.query("UPDATE adapter_grants SET state='retired' WHERE id=?").run(id);
|
|
87
|
+
return { ...row, state: "retired" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
AdapterGrantRegistry,
|
|
92
|
+
digest
|
|
93
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OwnershipFence, RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
|
+
import { AdapterConfig, AdapterGrant, ExecutorCommand, ExecutorError, MAX_FRAME_BYTES } from "../protocol.mjs";
|
|
4
|
+
import { readPrivateJson } from "../storage.mjs";
|
|
5
|
+
import { AdapterManifest } from "./artifacts.mjs";
|
|
6
|
+
import { ReleaseManagerConfig, WorkerAdapterReleases } from "./manager.mjs";
|
|
7
|
+
const Command = z.discriminatedUnion("method", [
|
|
8
|
+
z.object({ method: z.literal("status") }).strict(),
|
|
9
|
+
z.object({
|
|
10
|
+
method: z.literal("stage"),
|
|
11
|
+
source: z.string(),
|
|
12
|
+
manifest: AdapterManifest,
|
|
13
|
+
approvalDigest: z.string(),
|
|
14
|
+
grant: AdapterGrant,
|
|
15
|
+
adapter: AdapterConfig
|
|
16
|
+
}).strict(),
|
|
17
|
+
z.object({ method: z.literal("launch"), principalId: RuntimeId }).strict(),
|
|
18
|
+
z.object({ method: z.literal("stop-adapter"), principalId: RuntimeId }).strict(),
|
|
19
|
+
z.object({ method: z.literal("promote"), principalId: RuntimeId, fence: OwnershipFence, validUntil: z.number().int().positive() }).strict(),
|
|
20
|
+
z.object({ method: z.literal("retire"), principalId: RuntimeId }).strict(),
|
|
21
|
+
z.object({ method: z.literal("request"), callId: RuntimeId, principalId: RuntimeId, command: ExecutorCommand }).strict(),
|
|
22
|
+
z.object({ method: z.literal("reconcile-call"), callId: RuntimeId, principalId: RuntimeId, command: ExecutorCommand }).strict(),
|
|
23
|
+
z.object({ method: z.literal("acknowledge"), callId: RuntimeId }).strict(),
|
|
24
|
+
z.object({ method: z.literal("reconcile") }).strict()
|
|
25
|
+
]);
|
|
26
|
+
if (import.meta.main) {
|
|
27
|
+
const path = process.argv[2];
|
|
28
|
+
if (!path || process.argv.length !== 3) throw new Error("Usage: bun releases/main.ts /absolute/private/release-manager.json");
|
|
29
|
+
const manager = new WorkerAdapterReleases(ReleaseManagerConfig.parse(readPrivateJson(path)));
|
|
30
|
+
let buffer = Buffer.alloc(0), tail = Promise.resolve(), stopping = false;
|
|
31
|
+
async function handle(line) {
|
|
32
|
+
let id;
|
|
33
|
+
try {
|
|
34
|
+
const message = z.object({ id: RuntimeId, command: Command }).strict().parse(JSON.parse(line.toString("utf8")));
|
|
35
|
+
id = message.id;
|
|
36
|
+
const command = message.command;
|
|
37
|
+
let result;
|
|
38
|
+
switch (command.method) {
|
|
39
|
+
case "status":
|
|
40
|
+
result = { releases: manager.list(), calls: manager.calls(), transitions: manager.transitions() };
|
|
41
|
+
break;
|
|
42
|
+
case "stage":
|
|
43
|
+
result = await manager.stage(command);
|
|
44
|
+
break;
|
|
45
|
+
case "launch":
|
|
46
|
+
result = await manager.launch(command.principalId);
|
|
47
|
+
break;
|
|
48
|
+
case "stop-adapter":
|
|
49
|
+
result = await manager.stopAdapter(command.principalId);
|
|
50
|
+
break;
|
|
51
|
+
case "promote":
|
|
52
|
+
result = await manager.promote(command.principalId, command.fence, command.validUntil);
|
|
53
|
+
break;
|
|
54
|
+
case "retire":
|
|
55
|
+
result = await manager.retire(command.principalId);
|
|
56
|
+
break;
|
|
57
|
+
case "request":
|
|
58
|
+
result = await manager.request(command.callId, command.principalId, command.command);
|
|
59
|
+
break;
|
|
60
|
+
case "reconcile-call":
|
|
61
|
+
result = await manager.reconcileCall(command.callId, command.principalId, command.command);
|
|
62
|
+
break;
|
|
63
|
+
case "acknowledge":
|
|
64
|
+
result = await manager.acknowledge(command.callId);
|
|
65
|
+
break;
|
|
66
|
+
case "reconcile":
|
|
67
|
+
result = await manager.reconcile();
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(JSON.stringify({ id, ok: true, result: result ?? null }) + "\n");
|
|
71
|
+
} catch (error) {
|
|
72
|
+
process.stdout.write(
|
|
73
|
+
JSON.stringify({ id, ok: false, error: { code: error instanceof ExecutorError ? error.code : "invalid_request" } }) + "\n"
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const stop = () => {
|
|
78
|
+
if (stopping) return;
|
|
79
|
+
stopping = true;
|
|
80
|
+
void tail.then(() => manager.close()).then(
|
|
81
|
+
() => process.exit(0),
|
|
82
|
+
() => process.exit(1)
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
process.stdin.on("data", (chunk) => {
|
|
86
|
+
if (stopping) return;
|
|
87
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
88
|
+
if (buffer.length > MAX_FRAME_BYTES) {
|
|
89
|
+
stop();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
while (buffer.includes(10)) {
|
|
93
|
+
const end = buffer.indexOf(10), line = buffer.subarray(0, end);
|
|
94
|
+
buffer = buffer.subarray(end + 1);
|
|
95
|
+
tail = tail.then(() => handle(line));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
process.stdin.once("end", stop);
|
|
99
|
+
process.on("SIGTERM", stop);
|
|
100
|
+
process.on("SIGINT", stop);
|
|
101
|
+
}
|