@ricsam/r5d-worker 0.0.136 → 0.0.138
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 -7537
- 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 +328 -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 +17 -0
- package/dist/types/runtime/daemon.d.ts +8 -0
- package/dist/types/runtime/executor.d.ts +35 -0
- package/dist/types/runtime/index.d.ts +5 -0
- package/dist/types/runtime/protocol.d.ts +544 -0
- package/dist/types/runtime/pty.d.ts +30 -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/storage.d.ts +21 -0
- package/dist/types/runtime/test-harness.d.ts +53 -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 -7511
- 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 -3450
- package/dist/cjs/workspace-hydration-ledger.cjs +0 -66
- package/dist/cjs/workspace-hydration-merge.cjs +0 -433
- 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 -467
- 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 -3407
- package/dist/mjs/workspace-hydration-ledger.mjs +0 -42
- package/dist/mjs/workspace-hydration-merge.mjs +0 -399
- 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 -433
- 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-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 -288
- package/dist/types/workspace-filesystem-jobs.d.ts +0 -7
- package/dist/types/workspace-git-sync.d.ts +0 -495
- package/dist/types/workspace-hydration-ledger.d.ts +0 -43
- package/dist/types/workspace-hydration-merge.d.ts +0 -95
- 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 -60
- 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/adapter.d.ts} +0 -0
- /package/dist/types/{recovery-journal-stall-fixture.d.ts → runtime/releases/main.d.ts} +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
ExecutorError,
|
|
8
|
+
isStartNonAdmissionCode,
|
|
9
|
+
MAX_FRAME_BYTES,
|
|
10
|
+
ExecutorCommand
|
|
11
|
+
} from "../protocol.mjs";
|
|
12
|
+
import { canonicalDirectory } from "./artifacts.mjs";
|
|
13
|
+
import { digest } from "./grants.mjs";
|
|
14
|
+
import { DeliveryReceipt, ManagerRpcClientConfig } from "./rpc-protocol.mjs";
|
|
15
|
+
class ManagerRpcClient {
|
|
16
|
+
config;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = ManagerRpcClientConfig.parse(config);
|
|
19
|
+
}
|
|
20
|
+
request(command) {
|
|
21
|
+
const c = this.config;
|
|
22
|
+
canonicalDirectory(dirname(c.socketPath));
|
|
23
|
+
const st = lstatSync(c.socketPath), requestId = randomUUID();
|
|
24
|
+
if (!st.isSocket() || st.uid !== process.getuid?.() || st.mode & 63)
|
|
25
|
+
return Promise.reject(new ExecutorError("insecure_manager_socket"));
|
|
26
|
+
const frame = JSON.stringify({
|
|
27
|
+
protocol: 1,
|
|
28
|
+
requestId,
|
|
29
|
+
installationId: c.installationId,
|
|
30
|
+
workerId: c.workerId,
|
|
31
|
+
auth: { principalId: c.principalId, token: c.token },
|
|
32
|
+
command
|
|
33
|
+
}) + "\n";
|
|
34
|
+
if (Buffer.byteLength(frame) > MAX_FRAME_BYTES) return Promise.reject(new ExecutorError("request_too_large"));
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const socket = createConnection(c.socketPath);
|
|
37
|
+
let bytes = Buffer.alloc(0), done = false;
|
|
38
|
+
const finish = (error, result) => {
|
|
39
|
+
if (done) return;
|
|
40
|
+
done = true;
|
|
41
|
+
socket.destroy();
|
|
42
|
+
if (error) reject(error);
|
|
43
|
+
else resolve(result);
|
|
44
|
+
};
|
|
45
|
+
socket.setTimeout(c.timeoutMs, () => finish(new ExecutorError("manager_transport_unknown")));
|
|
46
|
+
socket.once("error", () => finish(new ExecutorError("manager_transport_unknown")));
|
|
47
|
+
socket.once("close", () => finish(new ExecutorError("manager_transport_unknown")));
|
|
48
|
+
socket.once("connect", () => socket.write(frame));
|
|
49
|
+
socket.on("data", (chunk) => {
|
|
50
|
+
bytes = Buffer.concat([bytes, chunk]);
|
|
51
|
+
if (bytes.length > MAX_FRAME_BYTES) {
|
|
52
|
+
finish(new ExecutorError("manager_response_invalid"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!bytes.includes(10)) return;
|
|
56
|
+
try {
|
|
57
|
+
const response = z.discriminatedUnion("ok", [
|
|
58
|
+
z.object({ requestId: z.literal(requestId), ok: z.literal(true), result: z.unknown() }).strict(),
|
|
59
|
+
z.object({
|
|
60
|
+
requestId: z.literal(requestId),
|
|
61
|
+
ok: z.literal(false),
|
|
62
|
+
error: z.object({ code: z.string(), rejectedBeforeAdmission: z.boolean() }).strict()
|
|
63
|
+
}).strict()
|
|
64
|
+
]).parse(JSON.parse(bytes.toString("utf8")));
|
|
65
|
+
if (response.ok === true && Object.hasOwn(response, "result")) finish(void 0, response.result);
|
|
66
|
+
else
|
|
67
|
+
finish(
|
|
68
|
+
new ExecutorError(
|
|
69
|
+
response.ok === false ? response.error.code : "manager_response_invalid",
|
|
70
|
+
void 0,
|
|
71
|
+
response.ok === false && response.error.rejectedBeforeAdmission && command.method === "execute" && command.command.method === "start" && [
|
|
72
|
+
"candidate_not_ready",
|
|
73
|
+
"manager_delivery_busy",
|
|
74
|
+
"delivery_retention_full",
|
|
75
|
+
"release_manager_closed",
|
|
76
|
+
"manager_ipc_busy"
|
|
77
|
+
].includes(response.error.code)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
} catch {
|
|
81
|
+
finish(new ExecutorError("manager_response_invalid"));
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
probe() {
|
|
87
|
+
return this.request({ method: "probe", adapterId: this.config.adapterId, hello: this.config.hello });
|
|
88
|
+
}
|
|
89
|
+
async execute(callId, command) {
|
|
90
|
+
return DeliveryReceipt.parse(
|
|
91
|
+
await this.request({
|
|
92
|
+
method: "execute",
|
|
93
|
+
callId,
|
|
94
|
+
adapterId: this.config.adapterId,
|
|
95
|
+
hello: this.config.hello,
|
|
96
|
+
command: ExecutorCommand.parse(command)
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
async observe(command) {
|
|
101
|
+
return DeliveryReceipt.parse(
|
|
102
|
+
await this.request({
|
|
103
|
+
method: "observe",
|
|
104
|
+
adapterId: this.config.adapterId,
|
|
105
|
+
hello: this.config.hello,
|
|
106
|
+
command: ExecutorCommand.parse(command)
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
async receipt(callId) {
|
|
111
|
+
return DeliveryReceipt.parse(await this.request({ method: "receipt", callId }));
|
|
112
|
+
}
|
|
113
|
+
async acknowledge(callId, responseDigest, ackToken) {
|
|
114
|
+
return z.object({ callId: z.literal(callId), responseDigest: z.literal(responseDigest), acknowledged: z.literal(true) }).strict().parse(await this.request({ method: "acknowledge", callId, responseDigest, ackToken }));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
class ManagedExecutorClient {
|
|
118
|
+
constructor(config, receiptSink, onAcknowledged) {
|
|
119
|
+
this.receiptSink = receiptSink;
|
|
120
|
+
this.onAcknowledged = onAcknowledged;
|
|
121
|
+
this.rpc = new ManagerRpcClient(config);
|
|
122
|
+
}
|
|
123
|
+
receiptSink;
|
|
124
|
+
onAcknowledged;
|
|
125
|
+
rpc;
|
|
126
|
+
get config() {
|
|
127
|
+
return this.rpc.config;
|
|
128
|
+
}
|
|
129
|
+
status() {
|
|
130
|
+
return this.rpc.probe();
|
|
131
|
+
}
|
|
132
|
+
async request(command) {
|
|
133
|
+
if (command.method === "status") return this.status();
|
|
134
|
+
if (!["start", "poll", "write", "resize", "cancel"].includes(command.method)) throw new ExecutorError("forbidden");
|
|
135
|
+
command = ExecutorCommand.parse(command);
|
|
136
|
+
const callId = command.method === "poll" ? null : `delivery-${digest(command)}`;
|
|
137
|
+
let receipt;
|
|
138
|
+
try {
|
|
139
|
+
receipt = callId === null ? await this.rpc.observe(command) : await this.rpc.execute(callId, command);
|
|
140
|
+
} catch (error2) {
|
|
141
|
+
if (command.method === "start" && error2 instanceof ExecutorError && error2.rejectedBeforeAdmission)
|
|
142
|
+
throw new ExecutorError("capacity_busy", void 0, true);
|
|
143
|
+
throw error2;
|
|
144
|
+
}
|
|
145
|
+
const identity = command.method === "start" ? command.operation : "run" in command ? command.run : null;
|
|
146
|
+
if ((callId !== null ? receipt.callId !== callId || receipt.deliveryKind !== "mutation" : !/^observation-[1-9][0-9]*$/.test(receipt.callId) || receipt.deliveryKind !== "observation") || receipt.adapterId !== this.config.adapterId || receipt.requesterId !== this.config.principalId || receipt.userId !== identity?.userId || receipt.sessionId !== identity?.sessionId || receipt.operationId !== identity?.operationId || receipt.commandDigest !== digest(command) || receipt.installationId !== this.config.installationId || receipt.workerId !== this.config.workerId || receipt.responseDigest !== digest({ callId: receipt.callId, commandDigest: receipt.commandDigest, state: receipt.state, outcome: receipt.outcome }))
|
|
147
|
+
throw new ExecutorError("manager_response_invalid");
|
|
148
|
+
if (this.receiptSink) await this.receiptSink(receipt);
|
|
149
|
+
if (receipt.state !== "completed" || receipt.outcome === null) throw new ExecutorError("manager_delivery_unknown");
|
|
150
|
+
if (this.receiptSink) {
|
|
151
|
+
try {
|
|
152
|
+
await this.rpc.acknowledge(receipt.callId, receipt.responseDigest, receipt.ackToken);
|
|
153
|
+
await this.onAcknowledged?.(receipt);
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (receipt.outcome.ok) return receipt.outcome.result;
|
|
158
|
+
const error = receipt.outcome.error;
|
|
159
|
+
throw new ExecutorError(
|
|
160
|
+
error.code,
|
|
161
|
+
void 0,
|
|
162
|
+
command.method === "start" && isStartNonAdmissionCode(error.code) && error.rejectedBeforeAdmission
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
start(operation) {
|
|
166
|
+
return this.request({ method: "start", operation });
|
|
167
|
+
}
|
|
168
|
+
poll(run, stdoutOffset = 0, stderrOffset = 0, maxBytes = 32768) {
|
|
169
|
+
return this.request({ method: "poll", run, stdoutOffset, stderrOffset, maxBytes });
|
|
170
|
+
}
|
|
171
|
+
write(run, actionId, data, eof = false) {
|
|
172
|
+
return this.request({ method: "write", run, actionId, data, eof });
|
|
173
|
+
}
|
|
174
|
+
resize(run, actionId, cols, rows) {
|
|
175
|
+
return this.request({ method: "resize", run, actionId, cols, rows });
|
|
176
|
+
}
|
|
177
|
+
cancel(run, actionId) {
|
|
178
|
+
return this.request({ method: "cancel", run, actionId });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
ManagedExecutorClient,
|
|
183
|
+
ManagerRpcClient
|
|
184
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { chmodSync, constants, closeSync, fstatSync, lstatSync, openSync, readSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
6
|
+
import { ExecutorError, MAX_FRAME_BYTES } from "../protocol.mjs";
|
|
7
|
+
import { WorkerAdapterReleases } from "./manager.mjs";
|
|
8
|
+
import { bytesHash, canonicalDirectory } from "./artifacts.mjs";
|
|
9
|
+
import { ManagerRequest, ManagerRpcConfig } from "./rpc-protocol.mjs";
|
|
10
|
+
function readManagerPrivateJson(path) {
|
|
11
|
+
canonicalDirectory(dirname(path));
|
|
12
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
13
|
+
try {
|
|
14
|
+
const st = fstatSync(fd);
|
|
15
|
+
if (!st.isFile() || st.nlink !== 1 || st.uid !== process.getuid?.() || st.mode & 63 || st.size > MAX_FRAME_BYTES)
|
|
16
|
+
throw new ExecutorError("insecure_manager_config");
|
|
17
|
+
const bytes = Buffer.alloc(st.size + 1);
|
|
18
|
+
let offset = 0;
|
|
19
|
+
while (offset < bytes.length) {
|
|
20
|
+
const n = readSync(fd, bytes, offset, bytes.length - offset, null);
|
|
21
|
+
if (!n) break;
|
|
22
|
+
offset += n;
|
|
23
|
+
}
|
|
24
|
+
const after = fstatSync(fd);
|
|
25
|
+
if (offset !== st.size || after.size !== st.size || after.mtimeMs !== st.mtimeMs || after.ctimeMs !== st.ctimeMs)
|
|
26
|
+
throw new ExecutorError("manager_config_changed");
|
|
27
|
+
return JSON.parse(bytes.subarray(0, offset).toString("utf8"));
|
|
28
|
+
} finally {
|
|
29
|
+
closeSync(fd);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function startManagerRpc(input) {
|
|
33
|
+
const config = ManagerRpcConfig.parse(input), manager = new WorkerAdapterReleases(config.manager);
|
|
34
|
+
const socketPath = join(manager.config.stateDir, "manager.sock"), sockets = /* @__PURE__ */ new Set();
|
|
35
|
+
let pending = 0;
|
|
36
|
+
const server = createServer((socket) => {
|
|
37
|
+
if (sockets.size >= 48) {
|
|
38
|
+
socket.destroy();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
sockets.add(socket);
|
|
42
|
+
socket.once("close", () => sockets.delete(socket));
|
|
43
|
+
socket.on("error", () => {
|
|
44
|
+
});
|
|
45
|
+
socket.setTimeout(15e3, () => socket.destroy());
|
|
46
|
+
let bytes = Buffer.alloc(0), received = false;
|
|
47
|
+
socket.on("data", (chunk) => {
|
|
48
|
+
if (received) {
|
|
49
|
+
socket.destroy();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (bytes.length + chunk.length > MAX_FRAME_BYTES) {
|
|
53
|
+
socket.destroy();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
bytes = Buffer.concat([bytes, chunk]);
|
|
57
|
+
const end = bytes.indexOf(10);
|
|
58
|
+
if (end < 0) return;
|
|
59
|
+
received = true;
|
|
60
|
+
if (end !== bytes.length - 1) {
|
|
61
|
+
socket.destroy();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
let requestId = null;
|
|
65
|
+
const execute = async () => {
|
|
66
|
+
const raw = JSON.parse(bytes.subarray(0, end).toString("utf8"));
|
|
67
|
+
requestId = RuntimeId.parse(raw?.requestId);
|
|
68
|
+
const request = ManagerRequest.parse(raw);
|
|
69
|
+
const principal = config.principals.find((p) => p.id === request.auth.principalId);
|
|
70
|
+
if (!timingSafeEqual(
|
|
71
|
+
Buffer.from(bytesHash(Buffer.from(request.auth.token)), "hex"),
|
|
72
|
+
Buffer.from(principal?.tokenHash ?? "0".repeat(64), "hex")
|
|
73
|
+
) || !principal)
|
|
74
|
+
throw new ExecutorError("unauthenticated");
|
|
75
|
+
if (request.installationId !== manager.config.installationId || request.workerId !== manager.config.workerId)
|
|
76
|
+
throw new ExecutorError("wrong_installation");
|
|
77
|
+
const command = request.command;
|
|
78
|
+
const sessionAllowed = (userId, sessionId) => principal.sessions.some((scope) => scope.userId === userId && scope.sessionId === sessionId) || principal.role === "workspace" && principal.userIds?.includes(userId) === true;
|
|
79
|
+
if (command.method === "probe") return manager.probe(command.adapterId, command.hello);
|
|
80
|
+
if (command.method === "execute" || command.method === "observe") {
|
|
81
|
+
if (principal.role !== "workspace") throw new ExecutorError("forbidden");
|
|
82
|
+
const operation = command.command.method === "start" ? command.command.operation : "run" in command.command ? command.command.run : null;
|
|
83
|
+
if (!operation?.sessionId || !sessionAllowed(operation.userId, operation.sessionId)) throw new ExecutorError("forbidden");
|
|
84
|
+
if (pending > (command.command.method === "cancel" ? 44 : 40))
|
|
85
|
+
throw new ExecutorError("manager_ipc_busy", void 0, command.command.method === "start");
|
|
86
|
+
return command.method === "observe" ? manager.observe(command.adapterId, command.hello, principal.id, command.command) : manager.deliver(command.callId, command.adapterId, command.hello, principal.id, command.command);
|
|
87
|
+
}
|
|
88
|
+
if (command.method === "receipt" || command.method === "acknowledge") {
|
|
89
|
+
if (principal.role !== "workspace") throw new ExecutorError("forbidden");
|
|
90
|
+
const receipt = manager.delivery(command.callId);
|
|
91
|
+
if (receipt && (receipt.requesterId !== principal.id || !sessionAllowed(receipt.userId, receipt.sessionId)))
|
|
92
|
+
throw new ExecutorError("delivery_not_found");
|
|
93
|
+
if (command.method === "receipt") {
|
|
94
|
+
if (!receipt) throw new ExecutorError("delivery_not_found");
|
|
95
|
+
return receipt;
|
|
96
|
+
}
|
|
97
|
+
return manager.acknowledgeDelivery(command.callId, command.responseDigest, command.ackToken, principal.id);
|
|
98
|
+
}
|
|
99
|
+
if (principal.role !== "admin") throw new ExecutorError("forbidden");
|
|
100
|
+
switch (command.method) {
|
|
101
|
+
case "stage":
|
|
102
|
+
return manager.stage(command);
|
|
103
|
+
case "launch":
|
|
104
|
+
return manager.launch(command.adapterId);
|
|
105
|
+
case "stop-adapter":
|
|
106
|
+
return manager.stopAdapter(command.adapterId);
|
|
107
|
+
case "promote":
|
|
108
|
+
return manager.promote(command.adapterId, command.fence, command.validUntil);
|
|
109
|
+
case "retire":
|
|
110
|
+
return manager.retire(command.adapterId, command.retirementIntentDigest);
|
|
111
|
+
case "settle-unknown":
|
|
112
|
+
return manager.settleUnknownDelivery(command.callId, command.evidenceDigest);
|
|
113
|
+
case "reconcile":
|
|
114
|
+
return manager.reconcile();
|
|
115
|
+
case "status":
|
|
116
|
+
return {
|
|
117
|
+
managerInvocation: manager.invocation,
|
|
118
|
+
releases: manager.list().map((r) => ({
|
|
119
|
+
principalId: r.principalId,
|
|
120
|
+
phase: r.phase,
|
|
121
|
+
retirementCompletion: r.retirementCompletion,
|
|
122
|
+
hello: r.manifest.hello,
|
|
123
|
+
artifactDigest: r.artifactDigest,
|
|
124
|
+
drain: manager.deliveryDrain(r.principalId)
|
|
125
|
+
})),
|
|
126
|
+
transitions: manager.transitions()
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
pending++;
|
|
131
|
+
void execute().then(
|
|
132
|
+
(result) => {
|
|
133
|
+
const frame = JSON.stringify({ requestId, ok: true, result: result ?? null }) + "\n";
|
|
134
|
+
if (Buffer.byteLength(frame) > MAX_FRAME_BYTES) socket.destroy();
|
|
135
|
+
else socket.end(frame);
|
|
136
|
+
},
|
|
137
|
+
(error) => socket.end(
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
requestId,
|
|
140
|
+
ok: false,
|
|
141
|
+
error: {
|
|
142
|
+
code: error instanceof ExecutorError ? error.code : "invalid_request",
|
|
143
|
+
rejectedBeforeAdmission: error instanceof ExecutorError && error.rejectedBeforeAdmission
|
|
144
|
+
}
|
|
145
|
+
}) + "\n"
|
|
146
|
+
)
|
|
147
|
+
).finally(() => pending--);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
manager.bindRpcPrincipals(config.principals);
|
|
152
|
+
canonicalDirectory(manager.config.stateDir);
|
|
153
|
+
try {
|
|
154
|
+
const st = lstatSync(socketPath);
|
|
155
|
+
if (!st.isSocket() || st.uid !== process.getuid?.()) throw new ExecutorError("unsafe_manager_socket");
|
|
156
|
+
unlinkSync(socketPath);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error.code !== "ENOENT") throw error;
|
|
159
|
+
}
|
|
160
|
+
await new Promise((resolve, reject) => {
|
|
161
|
+
server.once("error", reject);
|
|
162
|
+
server.listen(socketPath, () => {
|
|
163
|
+
server.off("error", reject);
|
|
164
|
+
resolve();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
chmodSync(socketPath, 384);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
server.close();
|
|
170
|
+
await manager.close();
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
let closing;
|
|
174
|
+
return {
|
|
175
|
+
manager,
|
|
176
|
+
socketPath,
|
|
177
|
+
get pending() {
|
|
178
|
+
return pending;
|
|
179
|
+
},
|
|
180
|
+
close() {
|
|
181
|
+
return closing ??= (async () => {
|
|
182
|
+
for (const socket of sockets) socket.destroy();
|
|
183
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
184
|
+
await manager.close();
|
|
185
|
+
})();
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (import.meta.main) {
|
|
190
|
+
try {
|
|
191
|
+
if (process.argv.length !== 3) throw new ExecutorError("private_config_required");
|
|
192
|
+
const service = await startManagerRpc(ManagerRpcConfig.parse(readManagerPrivateJson(process.argv[2])));
|
|
193
|
+
console.log(JSON.stringify({ ready: true, pid: process.pid, protocol: 1 }));
|
|
194
|
+
let stopping = false;
|
|
195
|
+
const stop = () => {
|
|
196
|
+
if (stopping) return;
|
|
197
|
+
stopping = true;
|
|
198
|
+
void service.close().then(
|
|
199
|
+
() => process.exit(0),
|
|
200
|
+
() => process.exit(1)
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
process.once("SIGTERM", stop);
|
|
204
|
+
process.once("SIGINT", stop);
|
|
205
|
+
} catch {
|
|
206
|
+
console.error("Private adapter manager startup failed");
|
|
207
|
+
process.exitCode = 1;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
readManagerPrivateJson,
|
|
212
|
+
startManagerRpc
|
|
213
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { RuntimeHello, RuntimeId, OwnershipFence } from "@ricsam/r5d-api/runtime-protocol";
|
|
3
|
+
import { AdapterConfig, AdapterGrant, ExecutorCommand } from "../protocol.mjs";
|
|
4
|
+
import { AdapterManifest } from "./artifacts.mjs";
|
|
5
|
+
import { ReleaseManagerConfig } from "./manager.mjs";
|
|
6
|
+
const Absolute = z.string().startsWith("/").max(4096);
|
|
7
|
+
const ManagerRpcConfig = z.object({
|
|
8
|
+
manager: ReleaseManagerConfig,
|
|
9
|
+
principals: z.array(
|
|
10
|
+
z.object({
|
|
11
|
+
id: RuntimeId,
|
|
12
|
+
tokenHash: z.string().regex(/^[a-f0-9]{64}$/),
|
|
13
|
+
role: z.enum(["admin", "probe", "workspace"]),
|
|
14
|
+
sessions: z.array(z.object({ userId: RuntimeId, sessionId: RuntimeId }).strict()).max(100),
|
|
15
|
+
// Only the trusted catalog-authorizing workspace keeper receives these grants.
|
|
16
|
+
userIds: z.array(RuntimeId).max(1e3).optional()
|
|
17
|
+
}).strict()
|
|
18
|
+
).min(1).max(100)
|
|
19
|
+
}).strict().refine(
|
|
20
|
+
(v) => new Set(v.principals.map((p) => p.id)).size === v.principals.length && new Set(v.principals.map((p) => p.tokenHash)).size === v.principals.length,
|
|
21
|
+
"Duplicate RPC principal/credential"
|
|
22
|
+
);
|
|
23
|
+
const ManagerRpcClientConfig = z.object({
|
|
24
|
+
socketPath: Absolute,
|
|
25
|
+
installationId: RuntimeId,
|
|
26
|
+
workerId: RuntimeId,
|
|
27
|
+
principalId: RuntimeId,
|
|
28
|
+
token: z.string().min(32).max(512),
|
|
29
|
+
adapterId: RuntimeId,
|
|
30
|
+
hello: RuntimeHello,
|
|
31
|
+
timeoutMs: z.number().int().min(100).max(6e4).default(1e4)
|
|
32
|
+
}).strict();
|
|
33
|
+
const ManagerCommand = z.discriminatedUnion("method", [
|
|
34
|
+
z.object({ method: z.literal("probe"), adapterId: RuntimeId, hello: RuntimeHello }).strict(),
|
|
35
|
+
z.object({ method: z.literal("execute"), adapterId: RuntimeId, hello: RuntimeHello, callId: RuntimeId, command: ExecutorCommand }).strict(),
|
|
36
|
+
z.object({ method: z.literal("observe"), adapterId: RuntimeId, hello: RuntimeHello, command: ExecutorCommand }).strict(),
|
|
37
|
+
z.object({ method: z.literal("receipt"), callId: RuntimeId }).strict(),
|
|
38
|
+
z.object({
|
|
39
|
+
method: z.literal("acknowledge"),
|
|
40
|
+
callId: RuntimeId,
|
|
41
|
+
responseDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
42
|
+
ackToken: z.string().regex(/^[a-f0-9]{64}$/)
|
|
43
|
+
}).strict(),
|
|
44
|
+
z.object({
|
|
45
|
+
method: z.literal("stage"),
|
|
46
|
+
source: Absolute,
|
|
47
|
+
manifest: AdapterManifest,
|
|
48
|
+
approvalDigest: z.string(),
|
|
49
|
+
grant: AdapterGrant,
|
|
50
|
+
adapter: AdapterConfig
|
|
51
|
+
}).strict(),
|
|
52
|
+
z.object({ method: z.literal("launch"), adapterId: RuntimeId }).strict(),
|
|
53
|
+
z.object({ method: z.literal("stop-adapter"), adapterId: RuntimeId }).strict(),
|
|
54
|
+
z.object({ method: z.literal("promote"), adapterId: RuntimeId, fence: OwnershipFence, validUntil: z.number().int().positive() }).strict(),
|
|
55
|
+
z.object({
|
|
56
|
+
method: z.literal("retire"),
|
|
57
|
+
adapterId: RuntimeId,
|
|
58
|
+
retirementIntentDigest: z.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
59
|
+
}).strict(),
|
|
60
|
+
z.object({ method: z.literal("settle-unknown"), callId: RuntimeId, evidenceDigest: z.string().regex(/^[a-f0-9]{64}$/) }).strict(),
|
|
61
|
+
z.object({ method: z.literal("status") }).strict(),
|
|
62
|
+
z.object({ method: z.literal("reconcile") }).strict()
|
|
63
|
+
]);
|
|
64
|
+
const ManagerRequest = z.object({
|
|
65
|
+
protocol: z.literal(1),
|
|
66
|
+
requestId: RuntimeId,
|
|
67
|
+
installationId: RuntimeId,
|
|
68
|
+
workerId: RuntimeId,
|
|
69
|
+
auth: z.object({ principalId: RuntimeId, token: z.string().min(32).max(512) }).strict(),
|
|
70
|
+
command: ManagerCommand
|
|
71
|
+
}).strict();
|
|
72
|
+
const DeliveryOutcome = z.discriminatedUnion("ok", [
|
|
73
|
+
z.object({ ok: z.literal(true), result: z.unknown() }).strict(),
|
|
74
|
+
z.object({ ok: z.literal(false), error: z.object({ code: z.string(), rejectedBeforeAdmission: z.boolean() }).strict() }).strict()
|
|
75
|
+
]);
|
|
76
|
+
const DeliveryReceipt = z.object({
|
|
77
|
+
callId: RuntimeId,
|
|
78
|
+
deliveryKind: z.enum(["mutation", "observation"]),
|
|
79
|
+
ackToken: z.string().regex(/^[a-f0-9]{64}$/),
|
|
80
|
+
installationId: RuntimeId,
|
|
81
|
+
workerId: RuntimeId,
|
|
82
|
+
adapterId: RuntimeId,
|
|
83
|
+
requesterId: RuntimeId,
|
|
84
|
+
userId: RuntimeId,
|
|
85
|
+
sessionId: RuntimeId,
|
|
86
|
+
operationId: RuntimeId,
|
|
87
|
+
commandDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
88
|
+
state: z.enum(["pending", "completed", "unknown"]),
|
|
89
|
+
outcome: DeliveryOutcome.nullable(),
|
|
90
|
+
responseDigest: z.string().regex(/^[a-f0-9]{64}$/),
|
|
91
|
+
acknowledged: z.boolean()
|
|
92
|
+
}).strict().superRefine((receipt, ctx) => {
|
|
93
|
+
if (receipt.state === "completed" !== (receipt.outcome !== null) || receipt.outcome?.ok === true && !Object.hasOwn(receipt.outcome, "result"))
|
|
94
|
+
ctx.addIssue({ code: "custom", message: "Delivery state/payload mismatch" });
|
|
95
|
+
});
|
|
96
|
+
export {
|
|
97
|
+
DeliveryOutcome,
|
|
98
|
+
DeliveryReceipt,
|
|
99
|
+
ManagerCommand,
|
|
100
|
+
ManagerRequest,
|
|
101
|
+
ManagerRpcClientConfig,
|
|
102
|
+
ManagerRpcConfig
|
|
103
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
fstatSync,
|
|
6
|
+
fsyncSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeSync
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { ExecutorError } from "./protocol.mjs";
|
|
17
|
+
function privateDirectory(path) {
|
|
18
|
+
mkdirSync(path, { mode: 448, recursive: true });
|
|
19
|
+
const stat = lstatSync(path);
|
|
20
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || stat.mode & 63)
|
|
21
|
+
throw new ExecutorError("insecure_directory");
|
|
22
|
+
}
|
|
23
|
+
function readPrivateJson(path) {
|
|
24
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
25
|
+
try {
|
|
26
|
+
const stat = fstatSync(fd);
|
|
27
|
+
if (!stat.isFile() || stat.uid !== process.getuid?.() || stat.mode & 63 || stat.size > 1024 * 1024)
|
|
28
|
+
throw new ExecutorError("insecure_config");
|
|
29
|
+
return JSON.parse(readFileSync(fd, "utf8"));
|
|
30
|
+
} finally {
|
|
31
|
+
closeSync(fd);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function privateDatabase(path) {
|
|
35
|
+
try {
|
|
36
|
+
closeSync(openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code !== "EEXIST") throw error;
|
|
39
|
+
}
|
|
40
|
+
for (const file of [path, `${path}-journal`, `${path}-wal`, `${path}-shm`]) {
|
|
41
|
+
try {
|
|
42
|
+
const stat = lstatSync(file);
|
|
43
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.mode & 63)
|
|
44
|
+
throw new ExecutorError("insecure_store_file");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== "ENOENT") throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return new Database(path);
|
|
50
|
+
}
|
|
51
|
+
class ExecutorStore {
|
|
52
|
+
constructor(root, installationId, workerId) {
|
|
53
|
+
this.root = root;
|
|
54
|
+
privateDirectory(root);
|
|
55
|
+
this.lock = privateDatabase(join(root, "authority.lock.sqlite"));
|
|
56
|
+
try {
|
|
57
|
+
this.lock.exec(
|
|
58
|
+
"PRAGMA busy_timeout=0; PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE; CREATE TABLE IF NOT EXISTS keeper_lock (id INTEGER)"
|
|
59
|
+
);
|
|
60
|
+
} catch {
|
|
61
|
+
this.lock.close();
|
|
62
|
+
throw new ExecutorError("executor_already_running");
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
this.db = privateDatabase(join(root, "receipts.sqlite"));
|
|
66
|
+
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON;
|
|
67
|
+
CREATE TABLE IF NOT EXISTS identity (id INTEGER PRIMARY KEY CHECK (id=1), installation TEXT NOT NULL, worker TEXT NOT NULL, version INTEGER NOT NULL);
|
|
68
|
+
CREATE TABLE IF NOT EXISTS authority (id INTEGER PRIMARY KEY CHECK (id=1), fence TEXT NOT NULL, valid_until INTEGER NOT NULL);
|
|
69
|
+
CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, receipt TEXT NOT NULL);
|
|
70
|
+
CREATE TABLE IF NOT EXISTS actions (run_id TEXT NOT NULL REFERENCES runs(id), id TEXT NOT NULL, hash TEXT NOT NULL, state TEXT NOT NULL, PRIMARY KEY(run_id,id));`);
|
|
71
|
+
const identity = this.db.query("SELECT * FROM identity WHERE id=1").get();
|
|
72
|
+
if (identity && (identity.installation !== installationId || identity.worker !== workerId || identity.version !== 1))
|
|
73
|
+
throw new ExecutorError("wrong_store_identity");
|
|
74
|
+
if (!identity) this.db.query("INSERT INTO identity VALUES (1,?,?,1)").run(installationId, workerId);
|
|
75
|
+
privateDirectory(join(root, "output"));
|
|
76
|
+
privateDirectory(join(root, "secrets"));
|
|
77
|
+
for (const receipt of this.all()) {
|
|
78
|
+
if (receipt.state === "accepted" || receipt.state === "running") {
|
|
79
|
+
receipt.state = "unknown";
|
|
80
|
+
receipt.reason = "executor_restarted_reconciliation_required";
|
|
81
|
+
this.put(receipt);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
this.db.exec("UPDATE actions SET state='unknown' WHERE state='accepted'; UPDATE authority SET valid_until=0");
|
|
85
|
+
rmSync(join(root, "secrets"), { recursive: true });
|
|
86
|
+
privateDirectory(join(root, "secrets"));
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.db?.close();
|
|
89
|
+
this.lock.close();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
root;
|
|
94
|
+
db;
|
|
95
|
+
lock;
|
|
96
|
+
all() {
|
|
97
|
+
return this.db.query("SELECT receipt FROM runs").all().map((row) => JSON.parse(row.receipt));
|
|
98
|
+
}
|
|
99
|
+
get(id) {
|
|
100
|
+
const row = this.db.query("SELECT receipt FROM runs WHERE id=?").get(id);
|
|
101
|
+
return row ? JSON.parse(row.receipt) : null;
|
|
102
|
+
}
|
|
103
|
+
put(receipt) {
|
|
104
|
+
this.db.query("INSERT INTO runs VALUES (?,?) ON CONFLICT(id) DO UPDATE SET receipt=excluded.receipt").run(receipt.operationId, JSON.stringify(receipt));
|
|
105
|
+
}
|
|
106
|
+
outputPath(id, stream) {
|
|
107
|
+
return join(this.root, "output", `${id}.${stream}`);
|
|
108
|
+
}
|
|
109
|
+
append(id, stream, data) {
|
|
110
|
+
const fd = openSync(
|
|
111
|
+
this.outputPath(id, stream),
|
|
112
|
+
constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW,
|
|
113
|
+
384
|
|
114
|
+
);
|
|
115
|
+
try {
|
|
116
|
+
let offset = 0;
|
|
117
|
+
while (offset < data.length) offset += writeSync(fd, data, offset);
|
|
118
|
+
fsyncSync(fd);
|
|
119
|
+
const directory = openSync(join(this.root, "output"), constants.O_RDONLY | constants.O_DIRECTORY);
|
|
120
|
+
try {
|
|
121
|
+
fsyncSync(directory);
|
|
122
|
+
} finally {
|
|
123
|
+
closeSync(directory);
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
closeSync(fd);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
read(id, stream, offset, end, max) {
|
|
130
|
+
if (offset > end) throw new ExecutorError("invalid_offset");
|
|
131
|
+
const buffer = Buffer.alloc(Math.min(end - offset, max));
|
|
132
|
+
if (buffer.length) {
|
|
133
|
+
const fd = openSync(this.outputPath(id, stream), constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
134
|
+
try {
|
|
135
|
+
let n = 0;
|
|
136
|
+
while (n < buffer.length) {
|
|
137
|
+
const read = readSync(fd, buffer, n, buffer.length - n, offset + n);
|
|
138
|
+
if (!read) throw new ExecutorError("output_truncated");
|
|
139
|
+
n += read;
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
closeSync(fd);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { base64: buffer.toString("base64"), nextOffset: offset + buffer.length };
|
|
146
|
+
}
|
|
147
|
+
close() {
|
|
148
|
+
this.db.close();
|
|
149
|
+
this.lock.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export {
|
|
153
|
+
ExecutorStore,
|
|
154
|
+
privateDirectory,
|
|
155
|
+
readPrivateJson
|
|
156
|
+
};
|