@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,217 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { closeSync, constants, lstatSync, openSync } from "node:fs";
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { RuntimeId } from "@ricsam/r5d-api/runtime-protocol";
|
|
8
|
+
import { privateDirectory } from "../runtime/storage.mjs";
|
|
9
|
+
const Cursor = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
10
|
+
const Command = z.discriminatedUnion("method", [
|
|
11
|
+
z.object({ method: z.literal("connect"), streamId: RuntimeId, id: RuntimeId.optional(), port: z.number().int().min(1).max(65535) }).strict(),
|
|
12
|
+
z.object({
|
|
13
|
+
method: z.literal("write"),
|
|
14
|
+
streamId: RuntimeId,
|
|
15
|
+
id: RuntimeId.optional(),
|
|
16
|
+
offset: Cursor,
|
|
17
|
+
base64: z.string().max(44e3),
|
|
18
|
+
end: z.boolean().optional()
|
|
19
|
+
}).strict(),
|
|
20
|
+
z.object({ method: z.literal("read"), streamId: RuntimeId, id: RuntimeId.optional(), after: Cursor }).strict(),
|
|
21
|
+
z.object({ method: z.literal("close"), streamId: RuntimeId, id: RuntimeId.optional() }).strict()
|
|
22
|
+
]);
|
|
23
|
+
class PersonalTcpError extends Error {
|
|
24
|
+
constructor(code) {
|
|
25
|
+
super(code);
|
|
26
|
+
this.code = code;
|
|
27
|
+
}
|
|
28
|
+
code;
|
|
29
|
+
}
|
|
30
|
+
class PersonalTcpManager {
|
|
31
|
+
constructor(directory, authorizeConnect) {
|
|
32
|
+
this.authorizeConnect = authorizeConnect;
|
|
33
|
+
privateDirectory(directory);
|
|
34
|
+
const file = path.join(directory, "streams.sqlite");
|
|
35
|
+
try {
|
|
36
|
+
closeSync(openSync(file, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 384));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
if (error.code !== "EEXIST") throw error;
|
|
39
|
+
}
|
|
40
|
+
for (const candidate of [file, `${file}-wal`, `${file}-shm`]) {
|
|
41
|
+
try {
|
|
42
|
+
const stat = lstatSync(candidate);
|
|
43
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.uid !== process.getuid?.() || stat.mode & 63)
|
|
44
|
+
throw new PersonalTcpError("insecure_tcp_store");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code !== "ENOENT") throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
this.db = new Database(file);
|
|
50
|
+
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL;
|
|
51
|
+
CREATE TABLE IF NOT EXISTS streams(id TEXT PRIMARY KEY,session TEXT NOT NULL,port INTEGER NOT NULL,state TEXT NOT NULL,write_offset INTEGER NOT NULL DEFAULT 0,read_offset INTEGER NOT NULL DEFAULT 0,ack_offset INTEGER NOT NULL DEFAULT 0,eof INTEGER NOT NULL DEFAULT 0);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS writes(stream TEXT NOT NULL,offset INTEGER NOT NULL,hash TEXT NOT NULL,next_offset INTEGER NOT NULL,state TEXT NOT NULL,PRIMARY KEY(stream,offset));
|
|
53
|
+
CREATE TABLE IF NOT EXISTS chunks(stream TEXT NOT NULL,offset INTEGER NOT NULL,data BLOB NOT NULL,PRIMARY KEY(stream,offset));
|
|
54
|
+
CREATE TABLE IF NOT EXISTS reads(id TEXT PRIMARY KEY,stream TEXT NOT NULL,after_offset INTEGER NOT NULL,result TEXT NOT NULL);
|
|
55
|
+
UPDATE streams SET state='unknown' WHERE state IN ('connecting','open');
|
|
56
|
+
UPDATE writes SET state='unknown' WHERE state='pending';`);
|
|
57
|
+
}
|
|
58
|
+
authorizeConnect;
|
|
59
|
+
db;
|
|
60
|
+
sockets = /* @__PURE__ */ new Map();
|
|
61
|
+
stream(sessionId, id) {
|
|
62
|
+
const row = this.db.query("SELECT * FROM streams WHERE id=?").get(id);
|
|
63
|
+
if (!row || row.session !== sessionId) throw new PersonalTcpError("tcp_stream_not_found");
|
|
64
|
+
return row;
|
|
65
|
+
}
|
|
66
|
+
async dispatch(sessionId, raw) {
|
|
67
|
+
RuntimeId.parse(sessionId);
|
|
68
|
+
const command = Command.parse(raw), id = command.streamId;
|
|
69
|
+
if (command.method === "connect") {
|
|
70
|
+
const previous2 = this.db.query("SELECT * FROM streams WHERE id=?").get(id);
|
|
71
|
+
if (previous2) {
|
|
72
|
+
if (previous2.session !== sessionId || previous2.port !== command.port) throw new PersonalTcpError("tcp_stream_identity_changed");
|
|
73
|
+
if (previous2.state !== "open" || !this.sockets.has(id)) throw new PersonalTcpError("tcp_stream_unknown_or_closed");
|
|
74
|
+
return { streamId: id, connected: true };
|
|
75
|
+
}
|
|
76
|
+
if (this.sockets.size >= 16) throw new PersonalTcpError("tcp_stream_capacity");
|
|
77
|
+
await this.authorizeConnect?.(sessionId, command.port);
|
|
78
|
+
if (this.db.query("SELECT id FROM streams WHERE id=?").get(id)) return this.dispatch(sessionId, raw);
|
|
79
|
+
this.db.query("INSERT INTO streams(id,session,port,state) VALUES(?,?,?,'connecting')").run(id, sessionId, command.port);
|
|
80
|
+
const socket = new net.Socket({ allowHalfOpen: true });
|
|
81
|
+
socket.setTimeout(5 * 60 * 1e3, () => socket.destroy(new PersonalTcpError("tcp_stream_idle")));
|
|
82
|
+
this.sockets.set(id, socket);
|
|
83
|
+
socket.on("data", (data) => {
|
|
84
|
+
const row2 = this.stream(sessionId, id);
|
|
85
|
+
if (row2.read_offset - row2.ack_offset + data.length > 8 * 1024 * 1024) {
|
|
86
|
+
this.db.query("UPDATE streams SET state='unknown' WHERE id=?").run(id);
|
|
87
|
+
socket.destroy();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
this.db.transaction(() => {
|
|
91
|
+
this.db.query("INSERT INTO chunks VALUES(?,?,?)").run(id, row2.read_offset, data);
|
|
92
|
+
this.db.query("UPDATE streams SET read_offset=read_offset+? WHERE id=?").run(data.length, id);
|
|
93
|
+
})();
|
|
94
|
+
});
|
|
95
|
+
socket.on("end", () => this.db.query("UPDATE streams SET eof=1 WHERE id=?").run(id));
|
|
96
|
+
socket.on("error", () => this.db.query("UPDATE streams SET state='unknown' WHERE id=?").run(id));
|
|
97
|
+
socket.on("close", () => {
|
|
98
|
+
this.sockets.delete(id);
|
|
99
|
+
this.db.query("UPDATE streams SET state=CASE WHEN state='unknown' THEN state ELSE 'closed' END,eof=1 WHERE id=?").run(id);
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
await new Promise((resolve, reject) => {
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
socket.destroy();
|
|
105
|
+
reject(new PersonalTcpError("tcp_connect_timeout"));
|
|
106
|
+
}, 1e4);
|
|
107
|
+
socket.once("connect", () => {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
resolve();
|
|
110
|
+
});
|
|
111
|
+
socket.once("error", () => {
|
|
112
|
+
clearTimeout(timer);
|
|
113
|
+
reject(new PersonalTcpError("tcp_connect_failed"));
|
|
114
|
+
});
|
|
115
|
+
socket.connect({
|
|
116
|
+
host: "localhost",
|
|
117
|
+
port: command.port,
|
|
118
|
+
autoSelectFamily: true,
|
|
119
|
+
lookup: (_host, _options, callback) => callback(null, [
|
|
120
|
+
{ address: "::1", family: 6 },
|
|
121
|
+
{ address: "127.0.0.1", family: 4 }
|
|
122
|
+
])
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
this.db.query("UPDATE streams SET state='open' WHERE id=?").run(id);
|
|
126
|
+
return { streamId: id, connected: true };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
this.db.query("UPDATE streams SET state='unknown' WHERE id=?").run(id);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const row = this.stream(sessionId, id);
|
|
133
|
+
if (command.method === "close") {
|
|
134
|
+
this.db.query("UPDATE streams SET state='closed',eof=1 WHERE id=?").run(id);
|
|
135
|
+
this.sockets.get(id)?.destroy();
|
|
136
|
+
return { closed: true };
|
|
137
|
+
}
|
|
138
|
+
if (command.method === "read") {
|
|
139
|
+
if (command.id) {
|
|
140
|
+
const previous2 = this.db.query("SELECT stream,after_offset,result FROM reads WHERE id=?").get(command.id);
|
|
141
|
+
if (previous2) {
|
|
142
|
+
if (previous2.stream !== id || previous2.after_offset !== command.after) throw new PersonalTcpError("tcp_read_identity_changed");
|
|
143
|
+
return JSON.parse(previous2.result);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (command.after < row.ack_offset || command.after > row.read_offset) throw new PersonalTcpError("tcp_read_cursor_invalid");
|
|
147
|
+
const chunks = this.db.query("SELECT offset,data FROM chunks WHERE stream=? AND offset+length(data)>? ORDER BY offset").all(id, command.after);
|
|
148
|
+
let remaining = 32768;
|
|
149
|
+
const selected = [];
|
|
150
|
+
for (const chunk of chunks) {
|
|
151
|
+
const value = Buffer.from(chunk.data).subarray(
|
|
152
|
+
Math.max(0, command.after - chunk.offset),
|
|
153
|
+
Math.max(0, command.after - chunk.offset) + remaining
|
|
154
|
+
);
|
|
155
|
+
selected.push(value);
|
|
156
|
+
remaining -= value.length;
|
|
157
|
+
if (!remaining) break;
|
|
158
|
+
}
|
|
159
|
+
this.db.transaction(() => {
|
|
160
|
+
this.db.query("UPDATE streams SET ack_offset=? WHERE id=?").run(command.after, id);
|
|
161
|
+
this.db.query("DELETE FROM chunks WHERE stream=? AND offset+length(data)<=?").run(id, command.after);
|
|
162
|
+
})();
|
|
163
|
+
const data = Buffer.concat(selected), nextOffset2 = command.after + data.length;
|
|
164
|
+
if (row.state === "unknown" && !data.length) throw new PersonalTcpError("tcp_stream_unknown");
|
|
165
|
+
const result = { base64: data.toString("base64"), nextOffset: nextOffset2, eof: !!row.eof && nextOffset2 === row.read_offset };
|
|
166
|
+
if (command.id) this.db.query("INSERT INTO reads VALUES(?,?,?,?)").run(command.id, id, command.after, JSON.stringify(result));
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
const bytes = Buffer.from(command.base64, "base64");
|
|
170
|
+
if (bytes.length > 32768 || bytes.toString("base64") !== command.base64 || !bytes.length && !command.end)
|
|
171
|
+
throw new PersonalTcpError("tcp_invalid_bytes");
|
|
172
|
+
const hash = createHash("sha256").update(bytes).update(command.end ? ":end" : ":more").digest("hex");
|
|
173
|
+
const previous = this.db.query("SELECT * FROM writes WHERE stream=? AND offset=?").get(id, command.offset);
|
|
174
|
+
if (previous) {
|
|
175
|
+
if (previous.hash !== hash) throw new PersonalTcpError("tcp_write_identity_changed");
|
|
176
|
+
if (previous.state !== "completed") throw new PersonalTcpError("tcp_write_unknown");
|
|
177
|
+
return { nextOffset: previous.next_offset };
|
|
178
|
+
}
|
|
179
|
+
if (row.state !== "open" || !this.sockets.has(id)) throw new PersonalTcpError("tcp_stream_unknown_or_closed");
|
|
180
|
+
if (command.offset !== row.write_offset) throw new PersonalTcpError("tcp_write_cursor_invalid");
|
|
181
|
+
const nextOffset = command.offset + bytes.length;
|
|
182
|
+
this.db.query("INSERT INTO writes VALUES(?,?,?,?,'pending')").run(id, command.offset, hash, nextOffset);
|
|
183
|
+
try {
|
|
184
|
+
const socket = this.sockets.get(id);
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
socket.write(bytes, (error) => {
|
|
187
|
+
if (error) reject(new PersonalTcpError("tcp_write_unknown"));
|
|
188
|
+
else resolve();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
if (command.end) socket.end();
|
|
192
|
+
this.db.transaction(() => {
|
|
193
|
+
this.db.query("UPDATE writes SET state='completed' WHERE stream=? AND offset=?").run(id, command.offset);
|
|
194
|
+
this.db.query("UPDATE streams SET write_offset=? WHERE id=?").run(nextOffset, id);
|
|
195
|
+
})();
|
|
196
|
+
return { nextOffset };
|
|
197
|
+
} catch (error) {
|
|
198
|
+
this.db.query("UPDATE writes SET state='unknown' WHERE stream=? AND offset=?").run(id, command.offset);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async close() {
|
|
203
|
+
await Promise.all(
|
|
204
|
+
[...this.sockets.values()].map(
|
|
205
|
+
(socket) => new Promise((resolve) => {
|
|
206
|
+
socket.once("close", resolve);
|
|
207
|
+
socket.destroy();
|
|
208
|
+
})
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
this.db.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
PersonalTcpError,
|
|
216
|
+
PersonalTcpManager
|
|
217
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { HostExecutorClient } from "./client.mjs";
|
|
2
|
+
import { AdapterConfig, ExecutorCommand, ExecutorError, MAX_FRAME_BYTES } from "./protocol.mjs";
|
|
3
|
+
import { readPrivateJson } from "./storage.mjs";
|
|
4
|
+
if (import.meta.main) {
|
|
5
|
+
const path = process.argv[2];
|
|
6
|
+
if (!path || process.argv.length !== 3) throw new Error("Usage: bun adapter.ts /absolute/private/adapter-config.json");
|
|
7
|
+
const client = new HostExecutorClient(AdapterConfig.parse(readPrivateJson(path)));
|
|
8
|
+
let buffer = Buffer.alloc(0);
|
|
9
|
+
process.stdin.on("data", (chunk) => {
|
|
10
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
11
|
+
if (buffer.length > MAX_FRAME_BYTES) process.exit(1);
|
|
12
|
+
while (buffer.includes(10)) {
|
|
13
|
+
const newline = buffer.indexOf(10);
|
|
14
|
+
const line = buffer.subarray(0, newline);
|
|
15
|
+
buffer = buffer.subarray(newline + 1);
|
|
16
|
+
void (async () => {
|
|
17
|
+
let id;
|
|
18
|
+
let method;
|
|
19
|
+
try {
|
|
20
|
+
const message = JSON.parse(line.toString("utf8"));
|
|
21
|
+
if (typeof message.id !== "string" || message.id.length > 200) throw new ExecutorError("invalid_request");
|
|
22
|
+
id = message.id;
|
|
23
|
+
const command = ExecutorCommand.parse(message.command);
|
|
24
|
+
method = command.method;
|
|
25
|
+
const result = await client.request(command);
|
|
26
|
+
process.stdout.write(JSON.stringify({ id, ok: true, result }) + "\n");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
process.stdout.write(
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
id,
|
|
31
|
+
ok: false,
|
|
32
|
+
error: {
|
|
33
|
+
code: error instanceof ExecutorError ? error.code : "invalid_request",
|
|
34
|
+
...method === "start" && error instanceof ExecutorError && error.rejectedBeforeAdmission && (error.code === "stale_fence" || error.code === "capacity_busy") ? { rejectedBeforeAdmission: true } : {}
|
|
35
|
+
}
|
|
36
|
+
}) + "\n"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import {
|
|
5
|
+
AdapterConfig,
|
|
6
|
+
ExecutorError,
|
|
7
|
+
isStartNonAdmissionCode,
|
|
8
|
+
MAX_FRAME_BYTES
|
|
9
|
+
} from "./protocol.mjs";
|
|
10
|
+
class HostExecutorClient {
|
|
11
|
+
config;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.config = AdapterConfig.parse(config);
|
|
14
|
+
}
|
|
15
|
+
request(command) {
|
|
16
|
+
const stat = lstatSync(this.config.socketPath);
|
|
17
|
+
if (!stat.isSocket() || stat.uid !== process.getuid?.() || stat.mode & 63) throw new ExecutorError("insecure_socket");
|
|
18
|
+
const request = JSON.stringify({
|
|
19
|
+
protocol: 1,
|
|
20
|
+
requestId: randomUUID(),
|
|
21
|
+
auth: { principalId: this.config.principalId, token: this.config.token },
|
|
22
|
+
hello: this.config.hello,
|
|
23
|
+
command
|
|
24
|
+
}) + "\n";
|
|
25
|
+
if (Buffer.byteLength(request) > MAX_FRAME_BYTES) throw new ExecutorError("request_too_large");
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const socket = createConnection(this.config.socketPath);
|
|
28
|
+
let buffer = Buffer.alloc(0);
|
|
29
|
+
let settled = false;
|
|
30
|
+
const finish = (error, value) => {
|
|
31
|
+
if (settled) return;
|
|
32
|
+
settled = true;
|
|
33
|
+
socket.destroy();
|
|
34
|
+
if (error) reject(error);
|
|
35
|
+
else resolve(value);
|
|
36
|
+
};
|
|
37
|
+
socket.setTimeout(this.config.timeoutMs, () => finish(new ExecutorError("transport_outcome_unknown")));
|
|
38
|
+
socket.once("connect", () => socket.write(request));
|
|
39
|
+
socket.on("error", () => finish(new ExecutorError("transport_outcome_unknown")));
|
|
40
|
+
socket.once("close", () => finish(new ExecutorError("transport_outcome_unknown")));
|
|
41
|
+
socket.on("data", (chunk) => {
|
|
42
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
43
|
+
if (buffer.length > MAX_FRAME_BYTES) {
|
|
44
|
+
finish(new ExecutorError("invalid_response"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!buffer.includes(10)) return;
|
|
48
|
+
try {
|
|
49
|
+
const response = JSON.parse(buffer.toString("utf8"));
|
|
50
|
+
if (response.ok === true) finish(void 0, response.result);
|
|
51
|
+
else
|
|
52
|
+
finish(
|
|
53
|
+
new ExecutorError(
|
|
54
|
+
response.error?.code ?? "invalid_response",
|
|
55
|
+
void 0,
|
|
56
|
+
response.ok === false && command.method === "start" && isStartNonAdmissionCode(response.error?.code) && response.error?.rejectedBeforeAdmission === true
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
} catch {
|
|
60
|
+
finish(new ExecutorError("invalid_response"));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
status() {
|
|
66
|
+
return this.request({ method: "status" });
|
|
67
|
+
}
|
|
68
|
+
registerAdapter(grant) {
|
|
69
|
+
return this.request({ method: "adapter.register", grant });
|
|
70
|
+
}
|
|
71
|
+
retireAdapter(principalId) {
|
|
72
|
+
return this.request({ method: "adapter.retire", principalId });
|
|
73
|
+
}
|
|
74
|
+
setAuthority(fence, validUntil) {
|
|
75
|
+
return this.request({ method: "authority", fence, validUntil });
|
|
76
|
+
}
|
|
77
|
+
start(operation) {
|
|
78
|
+
return this.request({ method: "start", operation });
|
|
79
|
+
}
|
|
80
|
+
poll(run, stdoutOffset = 0, stderrOffset = 0, maxBytes = 32768) {
|
|
81
|
+
return this.request({ method: "poll", run, stdoutOffset, stderrOffset, maxBytes });
|
|
82
|
+
}
|
|
83
|
+
write(run, actionId, data, eof = false) {
|
|
84
|
+
return this.request({ method: "write", run, actionId, data, eof });
|
|
85
|
+
}
|
|
86
|
+
resize(run, actionId, cols, rows) {
|
|
87
|
+
return this.request({ method: "resize", run, actionId, cols, rows });
|
|
88
|
+
}
|
|
89
|
+
cancel(run, actionId) {
|
|
90
|
+
return this.request({ method: "cancel", run, actionId });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
HostExecutorClient
|
|
95
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { HostExecutor } from "./executor.mjs";
|
|
5
|
+
import { ExecutorConfig, ExecutorError, MAX_FRAME_BYTES } from "./protocol.mjs";
|
|
6
|
+
import { readPrivateJson } from "./storage.mjs";
|
|
7
|
+
import { preflightNativePty } from "./pty.mjs";
|
|
8
|
+
async function startHostExecutor(input) {
|
|
9
|
+
if (process.platform === "win32") throw new ExecutorError("unsupported_platform");
|
|
10
|
+
const config = ExecutorConfig.parse(input);
|
|
11
|
+
let pty;
|
|
12
|
+
try {
|
|
13
|
+
pty = await import("node-pty");
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
if (pty) preflightNativePty();
|
|
17
|
+
const executor = new HostExecutor(config, pty);
|
|
18
|
+
const socketPath = join(config.stateDir, "executor.sock");
|
|
19
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
20
|
+
let closing;
|
|
21
|
+
let pending = 0;
|
|
22
|
+
const server = createServer((socket) => {
|
|
23
|
+
if (sockets.size >= 72) {
|
|
24
|
+
socket.destroy();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
sockets.add(socket);
|
|
28
|
+
socket.on("close", () => sockets.delete(socket));
|
|
29
|
+
socket.on("error", () => {
|
|
30
|
+
});
|
|
31
|
+
socket.setTimeout(1e4, () => socket.destroy());
|
|
32
|
+
let bytes = Buffer.alloc(0);
|
|
33
|
+
let received = false;
|
|
34
|
+
socket.on("data", (chunk) => {
|
|
35
|
+
if (received) {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
bytes = Buffer.concat([bytes, chunk]);
|
|
40
|
+
if (bytes.length > MAX_FRAME_BYTES) {
|
|
41
|
+
socket.destroy();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const newline = bytes.indexOf(10);
|
|
45
|
+
if (newline < 0) return;
|
|
46
|
+
received = true;
|
|
47
|
+
if (newline !== bytes.length - 1) {
|
|
48
|
+
socket.destroy();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let request;
|
|
52
|
+
try {
|
|
53
|
+
request = JSON.parse(bytes.subarray(0, newline).toString("utf8"));
|
|
54
|
+
} catch {
|
|
55
|
+
socket.end(JSON.stringify({ ok: false, error: { code: "invalid_request" } }) + "\n");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const authorityHint = request?.command?.method === "authority";
|
|
59
|
+
if (pending >= 72 || pending >= 64 && !authorityHint) {
|
|
60
|
+
socket.end(JSON.stringify({ ok: false, error: { code: "ipc_busy" } }) + "\n");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
pending++;
|
|
64
|
+
void executor.handle(request).then(
|
|
65
|
+
(result) => socket.end(JSON.stringify({ ok: true, result }) + "\n"),
|
|
66
|
+
(error) => socket.end(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
ok: false,
|
|
69
|
+
error: {
|
|
70
|
+
code: error instanceof ExecutorError ? error.code : "invalid_request",
|
|
71
|
+
...request?.command?.method === "start" && error instanceof ExecutorError && error.rejectedBeforeAdmission && (error.code === "stale_fence" || error.code === "capacity_busy") ? { rejectedBeforeAdmission: true } : {}
|
|
72
|
+
}
|
|
73
|
+
}) + "\n"
|
|
74
|
+
)
|
|
75
|
+
).finally(() => {
|
|
76
|
+
pending--;
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
try {
|
|
82
|
+
const stat = lstatSync(socketPath);
|
|
83
|
+
if (!stat.isSocket() || stat.uid !== process.getuid?.()) throw new ExecutorError("unsafe_socket_path");
|
|
84
|
+
unlinkSync(socketPath);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code !== "ENOENT") throw error;
|
|
87
|
+
}
|
|
88
|
+
await new Promise((resolve, reject) => {
|
|
89
|
+
server.once("error", reject);
|
|
90
|
+
server.listen(socketPath, () => {
|
|
91
|
+
server.removeListener("error", reject);
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
chmodSync(socketPath, 384);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
await executor.close();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
executor,
|
|
102
|
+
socketPath,
|
|
103
|
+
close() {
|
|
104
|
+
return closing ??= (async () => {
|
|
105
|
+
for (const socket of sockets) socket.destroy();
|
|
106
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
107
|
+
await executor.close();
|
|
108
|
+
})();
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (import.meta.main) {
|
|
113
|
+
const path = process.argv[2];
|
|
114
|
+
if (!path || process.argv.length !== 3) throw new Error("Usage: bun daemon.ts /absolute/private/executor-config.json");
|
|
115
|
+
const daemon = await startHostExecutor(ExecutorConfig.parse(readPrivateJson(path)));
|
|
116
|
+
process.stdout.write(JSON.stringify({ ready: true, pid: process.pid, protocol: 1 }) + "\n");
|
|
117
|
+
let stopping = false;
|
|
118
|
+
const stop = () => {
|
|
119
|
+
if (stopping) return;
|
|
120
|
+
stopping = true;
|
|
121
|
+
void daemon.close().then(
|
|
122
|
+
() => process.exit(0),
|
|
123
|
+
() => process.exit(1)
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
process.on("SIGTERM", stop);
|
|
127
|
+
process.on("SIGINT", stop);
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
startHostExecutor
|
|
131
|
+
};
|