@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
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class PersonalActionScheduler {
|
|
2
|
+
constructor(execute, failed, limit = 8) {
|
|
3
|
+
this.execute = execute;
|
|
4
|
+
this.failed = failed;
|
|
5
|
+
this.limit = limit;
|
|
6
|
+
}
|
|
7
|
+
execute;
|
|
8
|
+
failed;
|
|
9
|
+
limit;
|
|
10
|
+
active = /* @__PURE__ */ new Map();
|
|
11
|
+
accept(actions) {
|
|
12
|
+
for (const action of actions) {
|
|
13
|
+
const previous = this.active.get(action.id);
|
|
14
|
+
if (previous) {
|
|
15
|
+
if (previous.hash !== action.requestHash) throw new Error("Personal action identity changed while active");
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const request = action.request;
|
|
19
|
+
const lane = request.kind === "tcp" && typeof request.command?.streamId === "string" ? `tcp:${request.sessionId}:${request.command.streamId}` : request.kind === "inspect" && ["processes", "status"].includes(request.command?.method ?? "") ? `inspect:${request.sessionId}` : "mutation";
|
|
20
|
+
if (this.active.size >= this.limit || [...this.active.values()].some((value) => value.lane === lane)) continue;
|
|
21
|
+
const task = Promise.resolve().then(() => this.execute(action)).catch(this.failed).finally(() => this.active.delete(action.id));
|
|
22
|
+
this.active.set(action.id, { hash: action.requestHash, lane, task });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async drain() {
|
|
26
|
+
await Promise.all([...this.active.values()].map((value) => value.task));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
PersonalActionScheduler
|
|
31
|
+
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { promises as fs, openSync, closeSync } from "node:fs";
|
|
6
|
+
import { Database } from "bun:sqlite";
|
|
7
|
+
import { canonicalJson } from "@ricsam/r5d-api/runtime-protocol";
|
|
8
|
+
import { privateDirectory, readPrivateJson } from "../runtime/storage.mjs";
|
|
9
|
+
import { openPersonalWorkerRuntime, PersonalWorkerGrant } from "./runtime.mjs";
|
|
10
|
+
import { installCliUpdate, readInstalledCliVersion } from "../cli-update.mjs";
|
|
11
|
+
import { WorkspaceError } from "../runtime/workspace/contracts.mjs";
|
|
12
|
+
import { PersonalActionScheduler } from "./action-scheduler.mjs";
|
|
13
|
+
class PersonalResponseError extends Error {
|
|
14
|
+
constructor(code, status, rejectedBeforeAdmission) {
|
|
15
|
+
super(`Personal worker request failed (${status})`);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.rejectedBeforeAdmission = rejectedBeforeAdmission;
|
|
19
|
+
}
|
|
20
|
+
code;
|
|
21
|
+
status;
|
|
22
|
+
rejectedBeforeAdmission;
|
|
23
|
+
}
|
|
24
|
+
const storageErrorCodes = /* @__PURE__ */ new Set(["invalid_input", "conflict", "not_found", "busy", "stale_fence", "unknown", "unsafe_path", "git_failed"]);
|
|
25
|
+
function parsePersonalWorkerOptions(args, environment = process.env) {
|
|
26
|
+
const values = {};
|
|
27
|
+
const allowed = /* @__PURE__ */ new Set(["label", "base-url", "token", "api-key", "config", "root"]);
|
|
28
|
+
for (let index = 0; index < args.length; index++) {
|
|
29
|
+
const arg = args[index];
|
|
30
|
+
const match = /^--([^=]+)(?:=(.*))?$/.exec(arg);
|
|
31
|
+
if (!match || !allowed.has(match[1])) throw new Error("Unknown worker option");
|
|
32
|
+
const key = match[1];
|
|
33
|
+
if (key in values) throw new Error("Duplicate worker option");
|
|
34
|
+
const value = match[2] ?? args[++index];
|
|
35
|
+
if (!value || value.startsWith("--")) throw new Error(`Missing value for --${key}`);
|
|
36
|
+
values[key] = value;
|
|
37
|
+
}
|
|
38
|
+
const configPath = values.config ?? path.join(os.homedir(), ".config/r5d/r5dctl/config.json");
|
|
39
|
+
let saved = {};
|
|
40
|
+
try {
|
|
41
|
+
saved = readPrivateJson(configPath);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error.code !== "ENOENT") throw error;
|
|
44
|
+
}
|
|
45
|
+
const string = (value) => typeof value === "string" ? value : void 0;
|
|
46
|
+
const label = values.label;
|
|
47
|
+
if (!label || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,62}$/.test(label))
|
|
48
|
+
throw new Error("A worker --label is required (letters, numbers, dots, underscores or hyphens)");
|
|
49
|
+
const origin = new URL(values["base-url"] ?? environment.R5D_BASE_URL ?? string(saved.baseUrl) ?? "https://r5d.dev");
|
|
50
|
+
if (origin.username || origin.password || origin.search || origin.hash || origin.pathname !== "/" || origin.protocol !== "https:" && !(origin.protocol === "http:" && ["127.0.0.1", "localhost", "[::1]"].includes(origin.hostname)))
|
|
51
|
+
throw new Error("Worker base URL must use HTTPS or local loopback");
|
|
52
|
+
const credential = values.token ?? values["api-key"] ?? environment.R5D_TOKEN ?? environment.R5D_API_KEY ?? string(saved.token) ?? string(saved.apiKey);
|
|
53
|
+
if (!credential) throw new Error(`Authenticate this worker with: r5dctl auth login --worker-label ${label}`);
|
|
54
|
+
return {
|
|
55
|
+
label,
|
|
56
|
+
baseUrl: origin.origin,
|
|
57
|
+
credential,
|
|
58
|
+
root: path.resolve(values.root ?? environment.R5D_ROOT ?? path.join(os.homedir(), ".r5d"))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function safeError(error) {
|
|
62
|
+
const value = error.code;
|
|
63
|
+
return typeof value === "string" && /^[a-zA-Z0-9_-]{1,100}$/.test(value) ? value : "personal_operation_unknown";
|
|
64
|
+
}
|
|
65
|
+
async function startPersonalWorker(options, version) {
|
|
66
|
+
const key = createHash("sha256").update(canonicalJson([options.baseUrl, options.label])).digest("hex").slice(0, 32), root = path.join(options.root, "personal", key);
|
|
67
|
+
privateDirectory(root);
|
|
68
|
+
const identityFile = path.join(root, "identity.json");
|
|
69
|
+
let identity;
|
|
70
|
+
try {
|
|
71
|
+
identity = readPrivateJson(identityFile);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== "ENOENT") throw error;
|
|
74
|
+
identity = { resourceId: `worker-${randomUUID()}`, instanceId: `host-${randomUUID()}` };
|
|
75
|
+
const handle = await fs.open(identityFile, "wx", 384);
|
|
76
|
+
try {
|
|
77
|
+
await handle.writeFile(canonicalJson(identity));
|
|
78
|
+
await handle.sync();
|
|
79
|
+
} finally {
|
|
80
|
+
await handle.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const endpoint = `/api/personal/resources/worker/${encodeURIComponent(identity.resourceId)}`;
|
|
84
|
+
async function request(pathname, body) {
|
|
85
|
+
const response = await fetch(options.baseUrl + pathname, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: { authorization: `Bearer ${options.credential}`, "content-type": "application/json" },
|
|
88
|
+
redirect: "error",
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
signal: AbortSignal.timeout(3e4)
|
|
91
|
+
});
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new PersonalResponseError(typeof data.error === "string" ? data.error : "personal_transport_rejected", response.status, data.rejectedBeforeAdmission === true);
|
|
95
|
+
}
|
|
96
|
+
return data;
|
|
97
|
+
}
|
|
98
|
+
const metadata = () => ({
|
|
99
|
+
version,
|
|
100
|
+
r5dctlVersion: readInstalledCliVersion("r5dctl"),
|
|
101
|
+
platform: process.platform,
|
|
102
|
+
arch: process.arch,
|
|
103
|
+
hostname: os.hostname(),
|
|
104
|
+
capabilities: { updateClis: true, durableWorkspace: true }
|
|
105
|
+
});
|
|
106
|
+
const grant = PersonalWorkerGrant.parse(
|
|
107
|
+
await request("/api/personal/resources/register", { kind: "worker", ...identity, label: options.label, metadata: metadata() })
|
|
108
|
+
);
|
|
109
|
+
let cliEntrypoint = options.cliEntrypoint ?? Bun.which("r5dctl");
|
|
110
|
+
if (!cliEntrypoint) {
|
|
111
|
+
try {
|
|
112
|
+
const module = fileURLToPath(import.meta.resolve("@ricsam/r5dctl/cli"));
|
|
113
|
+
cliEntrypoint = path.join(path.dirname(module), path.extname(module) === ".cjs" ? "main.cjs" : "main.mjs");
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!cliEntrypoint) throw new Error("Install @ricsam/r5dctl before starting this worker");
|
|
118
|
+
const runtime = await openPersonalWorkerRuntime({
|
|
119
|
+
root,
|
|
120
|
+
cliEntrypoint,
|
|
121
|
+
grant,
|
|
122
|
+
storage: (sessionId) => async (storageRequest) => {
|
|
123
|
+
try {
|
|
124
|
+
const response = await request(`${endpoint}/storage`, {
|
|
125
|
+
instanceId: identity.instanceId,
|
|
126
|
+
request: { sessionId, envelope: { protocol: 1, installationId: grant.installationId, request: storageRequest } }
|
|
127
|
+
});
|
|
128
|
+
if (!response || !("result" in response)) throw new Error("Invalid scoped storage response");
|
|
129
|
+
return response.result;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (error instanceof PersonalResponseError && storageErrorCodes.has(error.code))
|
|
132
|
+
throw new WorkspaceError(error.code, "Scoped storage rejected the request; retain its original operation identity", error.rejectedBeforeAdmission);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
const ledgerFile = path.join(root, "relay.sqlite");
|
|
138
|
+
try {
|
|
139
|
+
closeSync(openSync(ledgerFile, "wx", 384));
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code !== "EEXIST") throw error;
|
|
142
|
+
}
|
|
143
|
+
const ledger = new Database(ledgerFile);
|
|
144
|
+
ledger.exec(
|
|
145
|
+
"PRAGMA journal_mode=WAL;PRAGMA synchronous=FULL;CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY,hash TEXT NOT NULL,state TEXT NOT NULL,result TEXT)"
|
|
146
|
+
);
|
|
147
|
+
let stopped = false, metadataAt = 0;
|
|
148
|
+
async function action(input) {
|
|
149
|
+
if (createHash("sha256").update(canonicalJson(input.request)).digest("hex") !== input.requestHash)
|
|
150
|
+
throw new Error("Personal action digest mismatch");
|
|
151
|
+
const previous = ledger.query("SELECT * FROM actions WHERE id=?").get(input.id);
|
|
152
|
+
if (previous && previous.hash !== input.requestHash) throw new Error("Personal action identity changed");
|
|
153
|
+
let state = previous?.state, result = previous?.result ? JSON.parse(previous.result) : void 0;
|
|
154
|
+
if (!previous || state === "pending") {
|
|
155
|
+
if (!previous) ledger.query("INSERT INTO actions VALUES(?,?,'pending',NULL)").run(input.id, input.requestHash);
|
|
156
|
+
try {
|
|
157
|
+
if (input.request.kind === "update-clis") {
|
|
158
|
+
const command = input.request.command;
|
|
159
|
+
if (input.request.userId !== grant.userId || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(command.version))
|
|
160
|
+
throw new Error("Invalid CLI update");
|
|
161
|
+
if (previous) throw new Error("Inspect interrupted CLI installation before retrying");
|
|
162
|
+
result = await installCliUpdate({
|
|
163
|
+
workerPackageSpec: `@ricsam/r5d-worker@${command.version}`,
|
|
164
|
+
r5dctlPackageSpec: `@ricsam/r5dctl@${command.version}`,
|
|
165
|
+
targetWorkerVersion: command.version,
|
|
166
|
+
targetR5dctlVersion: command.version
|
|
167
|
+
});
|
|
168
|
+
} else result = await runtime.dispatch(input.request);
|
|
169
|
+
state = "completed";
|
|
170
|
+
} catch (error) {
|
|
171
|
+
state = error.rejectedBeforeAdmission === true ? "rejected" : "unknown";
|
|
172
|
+
result = { error: safeError(error), ...error.rejectedBeforeAdmission === true ? { rejectedBeforeAdmission: true } : {} };
|
|
173
|
+
}
|
|
174
|
+
ledger.query("UPDATE actions SET state=?,result=? WHERE id=?").run(state, canonicalJson(result), input.id);
|
|
175
|
+
}
|
|
176
|
+
await request(`${endpoint}/ack`, { instanceId: identity.instanceId, id: input.id, requestHash: input.requestHash, state, result });
|
|
177
|
+
}
|
|
178
|
+
const scheduler = new PersonalActionScheduler(action, (error) => {
|
|
179
|
+
process.stderr.write(`[r5d-worker] ${safeError(error)}; retaining original action for reconnect
|
|
180
|
+
`);
|
|
181
|
+
});
|
|
182
|
+
const loop = (async () => {
|
|
183
|
+
while (!stopped) {
|
|
184
|
+
try {
|
|
185
|
+
const sendMetadata = Date.now() - metadataAt > 6e4;
|
|
186
|
+
const response = await request(`${endpoint}/poll`, {
|
|
187
|
+
instanceId: identity.instanceId,
|
|
188
|
+
...sendMetadata ? { metadata: metadata() } : {}
|
|
189
|
+
});
|
|
190
|
+
if (sendMetadata) metadataAt = Date.now();
|
|
191
|
+
await runtime.renew({ ...grant, ...response });
|
|
192
|
+
scheduler.accept(response.actions ?? []);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
process.stderr.write(`[r5d-worker] ${safeError(error)}; reconnecting without replacing the local keeper
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
await Bun.sleep(250);
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
return {
|
|
201
|
+
resourceId: identity.resourceId,
|
|
202
|
+
root,
|
|
203
|
+
async close() {
|
|
204
|
+
stopped = true;
|
|
205
|
+
await loop;
|
|
206
|
+
await scheduler.drain();
|
|
207
|
+
await runtime.close();
|
|
208
|
+
ledger.close();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export {
|
|
213
|
+
parsePersonalWorkerOptions,
|
|
214
|
+
startPersonalWorker
|
|
215
|
+
};
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { PersonalTcpManager } from "./tcp.mjs";
|
|
3
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { RuntimeHello, RuntimeId, OwnershipFence, OperationEnvelope, canonicalJson } from "@ricsam/r5d-api/runtime-protocol";
|
|
7
|
+
import { startHostExecutor } from "../runtime/daemon.mjs";
|
|
8
|
+
import { HostExecutorClient } from "../runtime/client.mjs";
|
|
9
|
+
import { tokenHash } from "../runtime/executor.mjs";
|
|
10
|
+
import { EXECUTOR_CAPABILITY, ExecutorCredential } from "../runtime/protocol.mjs";
|
|
11
|
+
import { privateDirectory, readPrivateJson } from "../runtime/storage.mjs";
|
|
12
|
+
import { WorkspaceAuthority } from "../runtime/workspace/authority.mjs";
|
|
13
|
+
import { ApprovedWorkbench, WorkspaceError } from "../runtime/workspace/contracts.mjs";
|
|
14
|
+
import { WorkspaceStorageClient } from "../runtime/workspace/storage-client.mjs";
|
|
15
|
+
import { BranchName, StorageId } from "../runtime/workspace/storage-wire.mjs";
|
|
16
|
+
const PersonalWorkerGrant = z.object({
|
|
17
|
+
installationId: RuntimeId,
|
|
18
|
+
userId: RuntimeId,
|
|
19
|
+
resourceId: RuntimeId,
|
|
20
|
+
instanceId: RuntimeId,
|
|
21
|
+
label: z.string().optional(),
|
|
22
|
+
workerFence: OwnershipFence,
|
|
23
|
+
workerHello: RuntimeHello,
|
|
24
|
+
leaseExpiresAt: z.coerce.number().int().positive()
|
|
25
|
+
}).passthrough();
|
|
26
|
+
const Workbench = z.object({ id: StorageId, repositoryId: StorageId, branch: BranchName, sessionId: RuntimeId, sharedSessionId: RuntimeId.optional() }).strict();
|
|
27
|
+
const PersonalWorkspaceRequest = z.object({
|
|
28
|
+
protocol: z.literal(1),
|
|
29
|
+
userId: RuntimeId,
|
|
30
|
+
sessionId: RuntimeId,
|
|
31
|
+
workbench: Workbench,
|
|
32
|
+
sourceWorkbench: Workbench.optional(),
|
|
33
|
+
sharedWorkbenches: z.array(Workbench).max(32).optional(),
|
|
34
|
+
kind: z.enum(["inspect", "terminal", "product", "agent", "tcp"]),
|
|
35
|
+
command: z.record(z.string(), z.unknown()),
|
|
36
|
+
credentials: z.array(ExecutorCredential).max(32).optional()
|
|
37
|
+
}).strict();
|
|
38
|
+
async function personalExecutorToken(root, grant) {
|
|
39
|
+
const identity = { installationId: grant.installationId, userId: grant.userId, resourceId: grant.resourceId, instanceId: grant.instanceId, workerId: grant.workerFence.resourceId };
|
|
40
|
+
const file = path.join(root, "executor-identity.json");
|
|
41
|
+
try {
|
|
42
|
+
const handle = await fs.open(file, "wx", 384);
|
|
43
|
+
try {
|
|
44
|
+
await handle.writeFile(canonicalJson({ version: 1, ...identity, token: randomBytes(32).toString("hex") }));
|
|
45
|
+
await handle.sync();
|
|
46
|
+
} finally {
|
|
47
|
+
await handle.close();
|
|
48
|
+
}
|
|
49
|
+
const directory = await fs.open(root, "r");
|
|
50
|
+
try {
|
|
51
|
+
await directory.sync();
|
|
52
|
+
} finally {
|
|
53
|
+
await directory.close();
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error.code !== "EEXIST") throw error;
|
|
57
|
+
}
|
|
58
|
+
const stat = await fs.lstat(file);
|
|
59
|
+
if (stat.nlink !== 1) throw new Error("Personal executor identity must be a private single-link file");
|
|
60
|
+
const stored = z.object({ version: z.literal(1), installationId: RuntimeId, userId: RuntimeId, resourceId: RuntimeId, instanceId: RuntimeId, workerId: RuntimeId, token: z.string().regex(/^[a-f0-9]{64}$/) }).strict().parse(readPrivateJson(file));
|
|
61
|
+
const { token, version: _, ...bound } = stored;
|
|
62
|
+
if (canonicalJson(bound) !== canonicalJson(identity)) throw new Error("Personal executor identity changed; retain the original keeper");
|
|
63
|
+
return token;
|
|
64
|
+
}
|
|
65
|
+
async function openPersonalWorkerRuntime(options) {
|
|
66
|
+
const grant = PersonalWorkerGrant.parse(options.grant), root = path.resolve(options.root);
|
|
67
|
+
privateDirectory(root);
|
|
68
|
+
if (grant.workerFence.installationId !== grant.installationId || grant.workerHello.installationId !== grant.installationId || grant.workerHello.instanceId !== grant.workerFence.ownerId || grant.workerHello.role !== "worker-adapter")
|
|
69
|
+
throw new Error("Invalid personal worker grant");
|
|
70
|
+
const credentialsRoot = path.join(root, "credentials"), workspaceRoot = path.join(root, "workspace", grant.installationId);
|
|
71
|
+
privateDirectory(credentialsRoot);
|
|
72
|
+
privateDirectory(workspaceRoot);
|
|
73
|
+
const token = await personalExecutorToken(root, grant), controllerId = `controller-${createHash("sha256").update(grant.instanceId).digest("hex").slice(0, 24)}`;
|
|
74
|
+
const daemon = await startHostExecutor({
|
|
75
|
+
installationId: grant.installationId,
|
|
76
|
+
workerId: grant.workerFence.resourceId,
|
|
77
|
+
instanceId: `keeper-${grant.resourceId}`,
|
|
78
|
+
releaseId: "personal-keeper-v1",
|
|
79
|
+
stateDir: path.join(root, "executor"),
|
|
80
|
+
productCredentialDirectory: credentialsRoot,
|
|
81
|
+
capacity: { general: 8, utility: 8, control: 4 },
|
|
82
|
+
principals: [
|
|
83
|
+
{
|
|
84
|
+
id: controllerId,
|
|
85
|
+
role: "controller",
|
|
86
|
+
ownerId: controllerId,
|
|
87
|
+
tokenHash: tokenHash(token),
|
|
88
|
+
userIds: [grant.userId],
|
|
89
|
+
cwdRoots: [workspaceRoot],
|
|
90
|
+
lanes: ["general", "utility", "control"],
|
|
91
|
+
credentialIds: []
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
try {
|
|
96
|
+
let cliDirectory;
|
|
97
|
+
if (options.cliEntrypoint) {
|
|
98
|
+
const cli = await fs.realpath(options.cliEntrypoint), executable = await fs.realpath(process.execPath), stat = await fs.lstat(cli);
|
|
99
|
+
if (!stat.isFile() || stat.mode & 18 || ![0, process.getuid?.()].includes(stat.uid))
|
|
100
|
+
throw new Error("Untrusted installed r5dctl entrypoint");
|
|
101
|
+
cliDirectory = path.join(root, "bin");
|
|
102
|
+
privateDirectory(cliDirectory);
|
|
103
|
+
const quote = (value) => "'" + value.replace(/'/g, "'\\''") + "'";
|
|
104
|
+
const wrapper = `#!/bin/sh
|
|
105
|
+
exec ${quote(executable)} ${quote(cli)} "$@"
|
|
106
|
+
`;
|
|
107
|
+
const file = path.join(cliDirectory, "r5dctl"), temporary = `${file}.${randomBytes(8).toString("hex")}.next`;
|
|
108
|
+
await fs.writeFile(temporary, wrapper, { mode: 448, flag: "wx" });
|
|
109
|
+
await fs.rename(temporary, file);
|
|
110
|
+
}
|
|
111
|
+
const controller = new HostExecutorClient({
|
|
112
|
+
socketPath: daemon.socketPath,
|
|
113
|
+
principalId: controllerId,
|
|
114
|
+
token,
|
|
115
|
+
hello: { ...grant.workerHello, instanceId: controllerId, role: "scheduler", capabilities: [EXECUTOR_CAPABILITY] },
|
|
116
|
+
timeoutMs: 1e4
|
|
117
|
+
});
|
|
118
|
+
let client = new HostExecutorClient({
|
|
119
|
+
socketPath: daemon.socketPath,
|
|
120
|
+
principalId: grant.workerHello.instanceId,
|
|
121
|
+
token,
|
|
122
|
+
hello: grant.workerHello,
|
|
123
|
+
timeoutMs: 1e4
|
|
124
|
+
});
|
|
125
|
+
let current = grant, installed = false, retirementPending = false;
|
|
126
|
+
const manifests = /* @__PURE__ */ new Map();
|
|
127
|
+
const manifestFile = path.join(root, "workbenches.json");
|
|
128
|
+
try {
|
|
129
|
+
const rows = z.array(ApprovedWorkbench).parse(readPrivateJson(manifestFile));
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
if (row.userId !== grant.userId || !row.cwd.startsWith(workspaceRoot + path.sep)) throw new Error("Invalid retained workbench");
|
|
132
|
+
manifests.set(row.sessionId, row);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code !== "ENOENT") throw error;
|
|
136
|
+
}
|
|
137
|
+
const artifactDigest = createHash("sha256").update(await fs.readFile(new URL(import.meta.url))).digest("hex");
|
|
138
|
+
async function retireObsoleteAdapters(retain) {
|
|
139
|
+
const status = await controller.status();
|
|
140
|
+
for (const adapter of status.adapters) {
|
|
141
|
+
if (adapter.state === "registered" && adapter.ownerId !== status.authority?.fence.ownerId && !retain.includes(adapter.ownerId))
|
|
142
|
+
await controller.retireAdapter(adapter.principalId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function renew(value) {
|
|
146
|
+
const next = PersonalWorkerGrant.parse(value);
|
|
147
|
+
if (next.instanceId !== grant.instanceId || next.installationId !== grant.installationId || next.resourceId !== grant.resourceId || next.userId !== grant.userId || next.workerFence.installationId !== grant.installationId || next.workerHello.installationId !== grant.installationId || next.workerHello.role !== "worker-adapter" || next.workerFence.resourceId !== grant.workerFence.resourceId || next.workerHello.instanceId !== next.workerFence.ownerId)
|
|
148
|
+
throw new Error("Personal worker resource identity changed; inspect retained keeper before rebinding");
|
|
149
|
+
let successor = client;
|
|
150
|
+
if (!installed || canonicalJson(next.workerHello) !== canonicalJson(current.workerHello)) {
|
|
151
|
+
await retireObsoleteAdapters([next.workerHello.instanceId]);
|
|
152
|
+
await controller.registerAdapter({
|
|
153
|
+
installationId: grant.installationId,
|
|
154
|
+
workerId: grant.workerFence.resourceId,
|
|
155
|
+
hello: next.workerHello,
|
|
156
|
+
artifactDigest,
|
|
157
|
+
principal: {
|
|
158
|
+
id: next.workerHello.instanceId,
|
|
159
|
+
role: "adapter",
|
|
160
|
+
ownerId: next.workerHello.instanceId,
|
|
161
|
+
tokenHash: tokenHash(token),
|
|
162
|
+
userIds: [grant.userId],
|
|
163
|
+
cwdRoots: [workspaceRoot],
|
|
164
|
+
lanes: ["general", "utility", "control"],
|
|
165
|
+
credentialIds: []
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
successor = new HostExecutorClient({
|
|
169
|
+
socketPath: daemon.socketPath,
|
|
170
|
+
principalId: next.workerHello.instanceId,
|
|
171
|
+
token,
|
|
172
|
+
hello: next.workerHello,
|
|
173
|
+
timeoutMs: 1e4
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
await controller.setAuthority(next.workerFence, next.leaseExpiresAt);
|
|
177
|
+
if (!installed || canonicalJson(next.workerHello) !== canonicalJson(current.workerHello)) retirementPending = true;
|
|
178
|
+
client = successor;
|
|
179
|
+
current = next;
|
|
180
|
+
installed = true;
|
|
181
|
+
if (retirementPending) {
|
|
182
|
+
await retireObsoleteAdapters([next.workerHello.instanceId]);
|
|
183
|
+
retirementPending = false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
await renew(grant);
|
|
187
|
+
const authority = await WorkspaceAuthority.open({
|
|
188
|
+
config: { installationId: grant.installationId, root: workspaceRoot, dynamicWorkbenches: true, workbenches: [] },
|
|
189
|
+
executor: async () => ({ client, workerFence: current.workerFence }),
|
|
190
|
+
resolveWorkbench: async (identity) => identity.userId === grant.userId ? manifests.get(identity.sessionId) ?? null : null,
|
|
191
|
+
storage: async (identity) => new WorkspaceStorageClient(grant.installationId, options.storage(identity.sessionId), async () => current.workerFence)
|
|
192
|
+
});
|
|
193
|
+
async function approve(input) {
|
|
194
|
+
const row = ApprovedWorkbench.parse({
|
|
195
|
+
...input,
|
|
196
|
+
userId: grant.userId,
|
|
197
|
+
cwd: path.join(workspaceRoot, "workbenches", input.id),
|
|
198
|
+
nonmutatingArgv: []
|
|
199
|
+
});
|
|
200
|
+
const previous = manifests.get(input.sessionId);
|
|
201
|
+
if (previous && canonicalJson(previous) !== canonicalJson(row)) throw new Error("Personal workbench identity changed");
|
|
202
|
+
if (!previous) {
|
|
203
|
+
manifests.set(input.sessionId, row);
|
|
204
|
+
const tmp = `${manifestFile}.${randomBytes(8).toString("hex")}.next`;
|
|
205
|
+
const handle = await fs.open(tmp, "wx", 384);
|
|
206
|
+
try {
|
|
207
|
+
await handle.writeFile(canonicalJson([...manifests.values()]));
|
|
208
|
+
await handle.sync();
|
|
209
|
+
} finally {
|
|
210
|
+
await handle.close();
|
|
211
|
+
}
|
|
212
|
+
await fs.rename(tmp, manifestFile);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function installCredentials(values) {
|
|
216
|
+
for (const source of values) {
|
|
217
|
+
const credential = cliDirectory ? { ...source, env: { ...source.env, PATH: `${cliDirectory}:/usr/local/bin:/usr/bin:/bin` } } : source;
|
|
218
|
+
if (credential.installationId !== grant.installationId || credential.userId !== grant.userId || !/^product-[a-f0-9]{40}$/.test(credential.id))
|
|
219
|
+
throw new Error("Invalid personal credential scope");
|
|
220
|
+
const destination = path.join(credentialsRoot, `${credential.id}-${credential.generation}.json`), data = canonicalJson(credential);
|
|
221
|
+
try {
|
|
222
|
+
const handle = await fs.open(destination, "wx", 384);
|
|
223
|
+
try {
|
|
224
|
+
await handle.writeFile(data);
|
|
225
|
+
await handle.sync();
|
|
226
|
+
} finally {
|
|
227
|
+
await handle.close();
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (error.code !== "EEXIST" || canonicalJson(readPrivateJson(destination)) !== data) throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const tcp = new PersonalTcpManager(path.join(root, "tcp"));
|
|
235
|
+
async function dispatch(raw) {
|
|
236
|
+
const request = PersonalWorkspaceRequest.parse(raw);
|
|
237
|
+
if (request.userId !== grant.userId || request.workbench.sessionId !== request.sessionId)
|
|
238
|
+
throw new Error("Wrong personal worker principal");
|
|
239
|
+
if (request.kind === "tcp") return tcp.dispatch(request.sessionId, request.command);
|
|
240
|
+
for (const parent of request.sharedWorkbenches ?? []) await approve(parent);
|
|
241
|
+
await approve(request.workbench);
|
|
242
|
+
if (request.sourceWorkbench) await approve(request.sourceWorkbench);
|
|
243
|
+
await installCredentials(request.credentials ?? []);
|
|
244
|
+
const identity = { userId: grant.userId, sessionId: request.sessionId }, command = request.command, run = (operationId) => ({ ...identity, operationId: RuntimeId.parse(operationId), fence: current.workerFence });
|
|
245
|
+
if (request.kind === "inspect") {
|
|
246
|
+
if (command.method === "fileWriteResult") return authority.fileWriteResult(identity, { id: command.id });
|
|
247
|
+
if (command.method === "processes") return authority.processes(identity);
|
|
248
|
+
if (command.method === "status") return { ...await authority.status(identity), workerLabel: grant.label ?? grant.resourceId };
|
|
249
|
+
await authority.ensureHydrated(identity);
|
|
250
|
+
if (command.method === "file") return authority.readFile(identity, command.path);
|
|
251
|
+
if (command.method === "git") return authority.inspectGit(identity, { ...command, method: command.action });
|
|
252
|
+
if (command.method === "fileHistoryDates") return authority.fileHistoryDates(identity, command.path);
|
|
253
|
+
if (command.method === "directory" || command.method === "raw") return authority.inspectFiles(identity, command);
|
|
254
|
+
}
|
|
255
|
+
if (request.kind === "terminal") {
|
|
256
|
+
await authority.ensureHydrated(identity);
|
|
257
|
+
if (command.method === "open" || command.method === "start") {
|
|
258
|
+
const argv = command.method === "open" ? ["/bin/bash", "--noprofile", "--norc", ...command.command ? ["-c", command.command] : []] : ["/bin/sh", "-c", command.command];
|
|
259
|
+
return {
|
|
260
|
+
...await authority.start(
|
|
261
|
+
identity,
|
|
262
|
+
OperationEnvelope.parse({
|
|
263
|
+
protocol: 1,
|
|
264
|
+
installationId: grant.installationId,
|
|
265
|
+
operationId: `terminal:${command.id}`,
|
|
266
|
+
...identity,
|
|
267
|
+
kind: "host.shell",
|
|
268
|
+
fence: current.workerFence,
|
|
269
|
+
payload: {
|
|
270
|
+
argv,
|
|
271
|
+
cwd: manifests.get(identity.sessionId).cwd,
|
|
272
|
+
...command.method === "open" ? { interactive: true, pty: { cols: command.cols, rows: command.rows } } : {},
|
|
273
|
+
credentials: (request.credentials ?? []).map(({ id, generation }) => ({ id, generation }))
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
),
|
|
277
|
+
workerLabel: grant.label ?? grant.resourceId
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const ref = run(command.operationId ?? `terminal:${command.runId}`);
|
|
281
|
+
if (command.method === "poll" || command.method === "processPoll")
|
|
282
|
+
return authority.poll(identity, ref, command.stdoutOffset ?? 0, command.stderrOffset ?? 0, command.maxBytes ?? 32768);
|
|
283
|
+
if (command.method === "write") return authority.write(identity, ref, command.id, command.data, command.eof ?? false);
|
|
284
|
+
if (command.method === "resize") return authority.resize(identity, ref, command.id, command.cols, command.rows);
|
|
285
|
+
if (command.method === "cancel" || command.method === "processCancel") return authority.cancel(identity, ref, command.id);
|
|
286
|
+
}
|
|
287
|
+
if (request.kind === "product") {
|
|
288
|
+
if (command.method === "fileWrite") {
|
|
289
|
+
await authority.ensureHydrated(identity);
|
|
290
|
+
return authority.fileWrite(identity, command);
|
|
291
|
+
}
|
|
292
|
+
if (command.method === "materializeArtifact") return authority.materializeArtifact(identity, command);
|
|
293
|
+
if (command.method === "seed") return authority.seedReadme(identity, command.readme);
|
|
294
|
+
if (command.method === "hydrate") return authority.hydrate(identity, command.expectedBase);
|
|
295
|
+
if (command.method === "import") return authority.importRepository(identity, command);
|
|
296
|
+
if (command.method === "clone")
|
|
297
|
+
return authority.cloneBranch(identity, { userId: grant.userId, sessionId: command.sourceSessionId }, command);
|
|
298
|
+
if (command.method === "archive") return authority.archiveBranch(identity, command);
|
|
299
|
+
if (command.method === "commit") return authority.commit(identity, command);
|
|
300
|
+
if (command.method === "push") return authority.push(identity, command);
|
|
301
|
+
if (command.method === "reset") return authority.reset(identity, command);
|
|
302
|
+
if (command.method === "publish") return authority.publish(identity);
|
|
303
|
+
if (command.method === "reconcilePublication") return authority.reconcilePublication(identity);
|
|
304
|
+
}
|
|
305
|
+
if (request.kind === "agent") {
|
|
306
|
+
if (command.method === "status") return { ...await authority.status(identity), workerLabel: grant.label ?? grant.resourceId };
|
|
307
|
+
if (command.method === "ensureHydrated") {
|
|
308
|
+
await authority.ensureHydrated(identity);
|
|
309
|
+
return { ready: true };
|
|
310
|
+
}
|
|
311
|
+
if (command.method === "hydrate") return authority.hydrate(identity, command.expectedBase);
|
|
312
|
+
if (command.method === "seedReadme") return authority.seedReadme(identity, command.readme);
|
|
313
|
+
if (command.method === "publish") return authority.publish(identity);
|
|
314
|
+
if (command.method === "reconcilePublication") return authority.reconcilePublication(identity);
|
|
315
|
+
if (command.method === "start") {
|
|
316
|
+
const operation = OperationEnvelope.parse(command.operation);
|
|
317
|
+
if (operation.userId !== grant.userId || operation.sessionId !== identity.sessionId)
|
|
318
|
+
throw new Error("Wrong personal shell principal");
|
|
319
|
+
return authority.start(identity, {
|
|
320
|
+
...operation,
|
|
321
|
+
fence: current.workerFence,
|
|
322
|
+
payload: {
|
|
323
|
+
...operation.payload,
|
|
324
|
+
cwd: manifests.get(identity.sessionId).cwd,
|
|
325
|
+
credentials: (request.credentials ?? []).map(({ id, generation }) => ({ id, generation }))
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (command.method === "poll")
|
|
330
|
+
return authority.poll(identity, run(command.run.operationId), command.stdoutOffset, command.stderrOffset, command.maxBytes);
|
|
331
|
+
if (command.method === "write")
|
|
332
|
+
return authority.write(identity, run(command.run.operationId), command.actionId, command.data, command.eof);
|
|
333
|
+
if (command.method === "resize")
|
|
334
|
+
return authority.resize(identity, run(command.run.operationId), command.actionId, command.cols, command.rows);
|
|
335
|
+
if (command.method === "cancel") return authority.cancel(identity, run(command.run.operationId), command.actionId);
|
|
336
|
+
}
|
|
337
|
+
throw new WorkspaceError("invalid_request", "Unsupported personal workspace operation");
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
grant,
|
|
341
|
+
renew,
|
|
342
|
+
dispatch,
|
|
343
|
+
authority,
|
|
344
|
+
async close() {
|
|
345
|
+
await tcp.close();
|
|
346
|
+
await authority.close();
|
|
347
|
+
await daemon.close();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
} catch (error) {
|
|
351
|
+
await daemon.close();
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
export {
|
|
356
|
+
PersonalWorkerGrant,
|
|
357
|
+
PersonalWorkspaceRequest,
|
|
358
|
+
openPersonalWorkerRuntime
|
|
359
|
+
};
|