@ricsam/r5d-worker 0.0.138 → 0.0.140
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/package.json +1 -1
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/runtime.mjs +44 -13
- package/dist/mjs/runtime/workspace/authority.mjs +10 -3
- package/dist/mjs/runtime/workspace/files.mjs +28 -0
- package/dist/types/runtime/workspace/files.d.ts +4 -0
- package/package.json +3 -3
package/dist/cjs/package.json
CHANGED
package/dist/mjs/main.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { startManagerRpc } from "./runtime/releases/rpc-main.mjs";
|
|
|
7
7
|
import { ManagerRpcConfig } from "./runtime/releases/rpc-protocol.mjs";
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
if (args.includes("--version")) {
|
|
10
|
-
console.log(`r5d-worker ${true ? "0.0.
|
|
10
|
+
console.log(`r5d-worker ${true ? "0.0.140" : "development"}`);
|
|
11
11
|
} else if (!args.length || args.includes("--help")) {
|
|
12
12
|
console.log(
|
|
13
13
|
"Usage: r5d-worker start --label <label> [--root <dir>] [--base-url <url>] [--token <worker-token>]\n r5d-worker executor /absolute/private/config.json\n r5d-worker manager /absolute/private/config.json\n\nRun independently supervised durable runtime services. Provision configuration with r5dinfra."
|
|
@@ -15,7 +15,7 @@ if (args.includes("--version")) {
|
|
|
15
15
|
} else if (args[0] === "start") {
|
|
16
16
|
const runtime = await startPersonalWorker(
|
|
17
17
|
parsePersonalWorkerOptions(args.slice(1)),
|
|
18
|
-
true ? "0.0.
|
|
18
|
+
true ? "0.0.140" : "development"
|
|
19
19
|
);
|
|
20
20
|
console.log(`Worker connected: ${runtime.resourceId}`);
|
|
21
21
|
let closing = false;
|
package/dist/mjs/package.json
CHANGED
|
@@ -35,6 +35,33 @@ const PersonalWorkspaceRequest = z.object({
|
|
|
35
35
|
command: z.record(z.string(), z.unknown()),
|
|
36
36
|
credentials: z.array(ExecutorCredential).max(32).optional()
|
|
37
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
|
+
}
|
|
38
65
|
async function openPersonalWorkerRuntime(options) {
|
|
39
66
|
const grant = PersonalWorkerGrant.parse(options.grant), root = path.resolve(options.root);
|
|
40
67
|
privateDirectory(root);
|
|
@@ -43,7 +70,7 @@ async function openPersonalWorkerRuntime(options) {
|
|
|
43
70
|
const credentialsRoot = path.join(root, "credentials"), workspaceRoot = path.join(root, "workspace", grant.installationId);
|
|
44
71
|
privateDirectory(credentialsRoot);
|
|
45
72
|
privateDirectory(workspaceRoot);
|
|
46
|
-
const token =
|
|
73
|
+
const token = await personalExecutorToken(root, grant), controllerId = `controller-${createHash("sha256").update(grant.instanceId).digest("hex").slice(0, 24)}`;
|
|
47
74
|
const daemon = await startHostExecutor({
|
|
48
75
|
installationId: grant.installationId,
|
|
49
76
|
workerId: grant.workerFence.resourceId,
|
|
@@ -62,16 +89,6 @@ async function openPersonalWorkerRuntime(options) {
|
|
|
62
89
|
cwdRoots: [workspaceRoot],
|
|
63
90
|
lanes: ["general", "utility", "control"],
|
|
64
91
|
credentialIds: []
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
id: grant.workerHello.instanceId,
|
|
68
|
-
role: "adapter",
|
|
69
|
-
ownerId: grant.workerHello.instanceId,
|
|
70
|
-
tokenHash: tokenHash(token),
|
|
71
|
-
userIds: [grant.userId],
|
|
72
|
-
cwdRoots: [workspaceRoot],
|
|
73
|
-
lanes: ["general", "utility", "control"],
|
|
74
|
-
credentialIds: []
|
|
75
92
|
}
|
|
76
93
|
]
|
|
77
94
|
});
|
|
@@ -105,7 +122,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
105
122
|
hello: grant.workerHello,
|
|
106
123
|
timeoutMs: 1e4
|
|
107
124
|
});
|
|
108
|
-
let current = grant;
|
|
125
|
+
let current = grant, installed = false, retirementPending = false;
|
|
109
126
|
const manifests = /* @__PURE__ */ new Map();
|
|
110
127
|
const manifestFile = path.join(root, "workbenches.json");
|
|
111
128
|
try {
|
|
@@ -118,12 +135,20 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
118
135
|
if (error.code !== "ENOENT") throw error;
|
|
119
136
|
}
|
|
120
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
|
+
}
|
|
121
145
|
async function renew(value) {
|
|
122
146
|
const next = PersonalWorkerGrant.parse(value);
|
|
123
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)
|
|
124
148
|
throw new Error("Personal worker resource identity changed; inspect retained keeper before rebinding");
|
|
125
149
|
let successor = client;
|
|
126
|
-
if (canonicalJson(next.workerHello) !== canonicalJson(current.workerHello)) {
|
|
150
|
+
if (!installed || canonicalJson(next.workerHello) !== canonicalJson(current.workerHello)) {
|
|
151
|
+
await retireObsoleteAdapters([next.workerHello.instanceId]);
|
|
127
152
|
await controller.registerAdapter({
|
|
128
153
|
installationId: grant.installationId,
|
|
129
154
|
workerId: grant.workerFence.resourceId,
|
|
@@ -149,8 +174,14 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
149
174
|
});
|
|
150
175
|
}
|
|
151
176
|
await controller.setAuthority(next.workerFence, next.leaseExpiresAt);
|
|
177
|
+
if (!installed || canonicalJson(next.workerHello) !== canonicalJson(current.workerHello)) retirementPending = true;
|
|
152
178
|
client = successor;
|
|
153
179
|
current = next;
|
|
180
|
+
installed = true;
|
|
181
|
+
if (retirementPending) {
|
|
182
|
+
await retireObsoleteAdapters([next.workerHello.instanceId]);
|
|
183
|
+
retirementPending = false;
|
|
184
|
+
}
|
|
154
185
|
}
|
|
155
186
|
await renew(grant);
|
|
156
187
|
const authority = await WorkspaceAuthority.open({
|
|
@@ -17,6 +17,7 @@ import { WorkspaceFileWrite, WorkspaceFileWriteLookup } from "./file-write.mjs";
|
|
|
17
17
|
import { WorkspaceStorageClient } from "./storage-client.mjs";
|
|
18
18
|
import {
|
|
19
19
|
durableJson,
|
|
20
|
+
ensureAuthorityGitRepositoryLayout,
|
|
20
21
|
git,
|
|
21
22
|
noSymlinkAncestors,
|
|
22
23
|
privateRoot,
|
|
@@ -86,7 +87,9 @@ class WorkspaceAuthority {
|
|
|
86
87
|
const state = JSON.parse((await readRegular(path.join(directory, "state.json"), 1024 * 1024)).toString());
|
|
87
88
|
if (typeof state.initialized !== "boolean" || !state.runs || !(state.head === null || GitOid.safeParse(state.head).success))
|
|
88
89
|
throw new WorkspaceError("invalid_state", "Invalid durable workspace state; maintenance required");
|
|
89
|
-
|
|
90
|
+
const repo = path.join(directory, "repo");
|
|
91
|
+
await ensureAuthorityGitRepositoryLayout(repo);
|
|
92
|
+
authority.benches.set(b.id, { config: b, state, directory, repo });
|
|
90
93
|
}
|
|
91
94
|
if (config.dynamicWorkbenches) {
|
|
92
95
|
for (const id of await fs.readdir(path.join(config.root, "state"))) {
|
|
@@ -98,7 +101,9 @@ class WorkspaceAuthority {
|
|
|
98
101
|
await noSymlinkAncestors(b.cwd);
|
|
99
102
|
const state = JSON.parse((await readRegular(path.join(directory, "state.json"), 1024 * 1024)).toString());
|
|
100
103
|
if (typeof state.initialized !== "boolean" || !state.runs || !(state.head === null || GitOid.safeParse(state.head).success)) throw new WorkspaceError("invalid_state", "Invalid dynamic workspace state");
|
|
101
|
-
|
|
104
|
+
const repo = path.join(directory, "repo");
|
|
105
|
+
await ensureAuthorityGitRepositoryLayout(repo);
|
|
106
|
+
authority.benches.set(id, { config: b, state, directory, repo });
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
return authority;
|
|
@@ -145,7 +150,9 @@ class WorkspaceAuthority {
|
|
|
145
150
|
await noSymlinkAncestors(config.cwd);
|
|
146
151
|
const state = JSON.parse((await readRegular(path.join(directory, "state.json"), 1024 * 1024)).toString());
|
|
147
152
|
if (typeof state.initialized !== "boolean" || !state.runs || !(state.head === null || GitOid.safeParse(state.head).success)) throw new WorkspaceError("invalid_state", "Invalid workspace state");
|
|
148
|
-
const
|
|
153
|
+
const repo = path.join(directory, "repo");
|
|
154
|
+
await ensureAuthorityGitRepositoryLayout(repo);
|
|
155
|
+
const bench = { config, state, directory, repo };
|
|
149
156
|
this.benches.set(config.id, bench);
|
|
150
157
|
return bench;
|
|
151
158
|
})();
|
|
@@ -35,6 +35,33 @@ async function readRegular(file, limit = 32 * 1024 * 1024) {
|
|
|
35
35
|
await handle.close();
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
async function ensureAuthorityGitRepositoryLayout(repo) {
|
|
39
|
+
await noSymlinkAncestors(repo);
|
|
40
|
+
const repository = await fs.lstat(repo);
|
|
41
|
+
if (!repository.isDirectory() || repository.isSymbolicLink()) throw new WorkspaceError("unsafe_git", "Authority Git repository is invalid");
|
|
42
|
+
await readRegular(path.join(repo, "HEAD"), 4096);
|
|
43
|
+
await readRegular(path.join(repo, "config"), 128 * 1024);
|
|
44
|
+
for (const relative of ["objects", "objects/info", "objects/pack", "refs", "refs/heads", "refs/tags"]) {
|
|
45
|
+
const target = path.join(repo, relative);
|
|
46
|
+
let created = false;
|
|
47
|
+
try {
|
|
48
|
+
await fs.mkdir(target, { mode: 448 });
|
|
49
|
+
created = true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code !== "EEXIST") throw error;
|
|
52
|
+
}
|
|
53
|
+
const entry = await fs.lstat(target);
|
|
54
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) throw new WorkspaceError("unsafe_git", "Authority Git structure is invalid");
|
|
55
|
+
if (created) {
|
|
56
|
+
const parent = await fs.open(path.dirname(target), constants.O_RDONLY);
|
|
57
|
+
try {
|
|
58
|
+
await parent.sync();
|
|
59
|
+
} finally {
|
|
60
|
+
await parent.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
38
65
|
async function durableJson(file, value) {
|
|
39
66
|
const temporary = `${file}.next`;
|
|
40
67
|
const handle = await fs.open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
@@ -295,6 +322,7 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
295
322
|
}
|
|
296
323
|
export {
|
|
297
324
|
durableJson,
|
|
325
|
+
ensureAuthorityGitRepositoryLayout,
|
|
298
326
|
git,
|
|
299
327
|
noSymlinkAncestors,
|
|
300
328
|
privateRoot,
|
|
@@ -2,6 +2,10 @@ export declare const sha256: (bytes: Buffer | string) => string;
|
|
|
2
2
|
export declare function privateRoot(root: string, installationId: string): Promise<void>;
|
|
3
3
|
export declare function noSymlinkAncestors(file: string): Promise<void>;
|
|
4
4
|
export declare function readRegular(file: string, limit?: number): Promise<Buffer>;
|
|
5
|
+
/** File-only migration capsules can omit Git's empty structural directories.
|
|
6
|
+
* Restore only that directory skeleton after confirming the authority-owned
|
|
7
|
+
* repository markers; refs, objects, indexes and configuration are untouched. */
|
|
8
|
+
export declare function ensureAuthorityGitRepositoryLayout(repo: string): Promise<void>;
|
|
5
9
|
export declare function durableJson(file: string, value: unknown): Promise<void>;
|
|
6
10
|
/** All Git operates in authority-created repos; NEVER workbench .git/config/index.
|
|
7
11
|
* hash-object --no-filters and update-index --index-info replace git add/checkout.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.140",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/mjs/main.mjs",
|
|
6
6
|
"module": "./dist/mjs/main.mjs",
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"r5d-worker": "dist/mjs/main.mjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@ricsam/r5d-api": "^0.0.
|
|
25
|
-
"@ricsam/r5dctl": "0.0.
|
|
24
|
+
"@ricsam/r5d-api": "^0.0.140",
|
|
25
|
+
"@ricsam/r5dctl": "0.0.140",
|
|
26
26
|
"node-pty": "1.1.0",
|
|
27
27
|
"zod": "^4.1.13",
|
|
28
28
|
"picomatch": "^4.0.3"
|