@ricsam/r5d-worker 0.0.145 → 0.0.147
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 +2 -0
- 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/cli-entrypoint.mjs +59 -0
- package/dist/mjs/personal/client.mjs +6 -13
- package/dist/mjs/runtime/workspace/authority.mjs +13 -20
- package/dist/mjs/runtime/workspace/files.mjs +98 -19
- package/dist/types/personal/cli-entrypoint.d.ts +13 -0
- package/dist/types/runtime/workspace/files.d.ts +2 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -8,3 +8,5 @@ r5d-worker manager /absolute/private/manager-config.json
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
The executor retains processes and durable receipts independently of application releases. The manager authenticates the approved workspace keeper and retains old adapters until their delivery receipts are settled. Configuration and host roots belong exclusively to one installation. Never share an executor's state directory, credentials or workspace root between environments.
|
|
11
|
+
|
|
12
|
+
The published worker has an exact-version dependency on `@ricsam/r5dctl`. A personal worker resolves that bundled CLI before the host `PATH`, verifies that its package version equals the running worker version, and exposes it to agent shells through a private worker-owned wrapper. Each worker remains bound to one server origin and installation, so stage, production, and review workers cannot accidentally select another environment's global CLI or credentials.
|
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.147" : "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.147" : "development"
|
|
19
19
|
);
|
|
20
20
|
console.log(`Worker connected: ${runtime.resourceId}`);
|
|
21
21
|
let closing = false;
|
package/dist/mjs/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
const PACKAGE_NAME = "@ricsam/r5dctl";
|
|
5
|
+
function packageMain(moduleUrl) {
|
|
6
|
+
const module = fileURLToPath(moduleUrl);
|
|
7
|
+
return path.join(path.dirname(module), path.extname(module) === ".cjs" ? "main.cjs" : "main.mjs");
|
|
8
|
+
}
|
|
9
|
+
async function inspectPackage(candidate) {
|
|
10
|
+
const entrypoint = await fs.realpath(candidate);
|
|
11
|
+
const entrypointStat = await fs.lstat(entrypoint);
|
|
12
|
+
if (!entrypointStat.isFile() || entrypointStat.nlink !== 1 || entrypointStat.mode & 18 || ![0, process.getuid?.()].includes(entrypointStat.uid))
|
|
13
|
+
throw new Error("Untrusted installed r5dctl entrypoint");
|
|
14
|
+
let directory = path.dirname(entrypoint);
|
|
15
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
16
|
+
const manifest = path.join(directory, "package.json");
|
|
17
|
+
try {
|
|
18
|
+
const stat = await fs.lstat(manifest);
|
|
19
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.mode & 18 || ![0, process.getuid?.()].includes(stat.uid))
|
|
20
|
+
throw new Error("Untrusted installed r5dctl package manifest");
|
|
21
|
+
const parsed = JSON.parse(await fs.readFile(manifest, "utf8"));
|
|
22
|
+
if (parsed?.name === PACKAGE_NAME) {
|
|
23
|
+
if (typeof parsed.version !== "string" || !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?$/.test(parsed.version))
|
|
24
|
+
throw new Error("Invalid installed r5dctl package version");
|
|
25
|
+
return { entrypoint, version: parsed.version };
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code !== "ENOENT") throw error;
|
|
29
|
+
}
|
|
30
|
+
const parent = path.dirname(directory);
|
|
31
|
+
if (parent === directory) break;
|
|
32
|
+
directory = parent;
|
|
33
|
+
}
|
|
34
|
+
throw new Error("Resolved r5dctl entrypoint is not inside an @ricsam/r5dctl package");
|
|
35
|
+
}
|
|
36
|
+
async function resolvePersonalCliEntrypoint(explicit, workerVersion, dependencies = {}) {
|
|
37
|
+
let source, candidate;
|
|
38
|
+
if (explicit) {
|
|
39
|
+
source = "explicit";
|
|
40
|
+
candidate = explicit;
|
|
41
|
+
} else {
|
|
42
|
+
try {
|
|
43
|
+
const resolvePackage = dependencies.resolvePackage ?? ((specifier) => import.meta.resolve(specifier));
|
|
44
|
+
candidate = packageMain(resolvePackage(`${PACKAGE_NAME}/cli`));
|
|
45
|
+
source = "bundled";
|
|
46
|
+
} catch {
|
|
47
|
+
source = "path";
|
|
48
|
+
candidate = (dependencies.which ?? Bun.which)("r5dctl");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!candidate) throw new Error("Install @ricsam/r5dctl before starting this worker");
|
|
52
|
+
const inspected = await inspectPackage(candidate);
|
|
53
|
+
if (workerVersion !== "development" && inspected.version !== workerVersion)
|
|
54
|
+
throw new Error(`r5d-worker ${workerVersion} requires its bundled r5dctl ${workerVersion}; resolved ${inspected.version}`);
|
|
55
|
+
return { ...inspected, source };
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
resolvePersonalCliEntrypoint
|
|
59
|
+
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
2
|
import os from "node:os";
|
|
4
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
4
|
import { promises as fs, openSync, closeSync } from "node:fs";
|
|
@@ -7,9 +6,10 @@ import { Database } from "bun:sqlite";
|
|
|
7
6
|
import { canonicalJson } from "@ricsam/r5d-api/runtime-protocol";
|
|
8
7
|
import { privateDirectory, readPrivateJson } from "../runtime/storage.mjs";
|
|
9
8
|
import { openPersonalWorkerRuntime, PersonalWorkerGrant } from "./runtime.mjs";
|
|
10
|
-
import { installCliUpdate
|
|
9
|
+
import { installCliUpdate } from "../cli-update.mjs";
|
|
11
10
|
import { WorkspaceError } from "../runtime/workspace/contracts.mjs";
|
|
12
11
|
import { PersonalActionScheduler } from "./action-scheduler.mjs";
|
|
12
|
+
import { resolvePersonalCliEntrypoint } from "./cli-entrypoint.mjs";
|
|
13
13
|
class PersonalResponseError extends Error {
|
|
14
14
|
constructor(code, status, rejectedBeforeAdmission) {
|
|
15
15
|
super(`Personal worker request failed (${status})`);
|
|
@@ -95,9 +95,11 @@ async function startPersonalWorker(options, version) {
|
|
|
95
95
|
}
|
|
96
96
|
return data;
|
|
97
97
|
}
|
|
98
|
+
const cli = await resolvePersonalCliEntrypoint(options.cliEntrypoint, version);
|
|
98
99
|
const metadata = () => ({
|
|
99
100
|
version,
|
|
100
|
-
r5dctlVersion:
|
|
101
|
+
r5dctlVersion: cli.version,
|
|
102
|
+
r5dctlSource: cli.source,
|
|
101
103
|
platform: process.platform,
|
|
102
104
|
arch: process.arch,
|
|
103
105
|
hostname: os.hostname(),
|
|
@@ -106,18 +108,9 @@ async function startPersonalWorker(options, version) {
|
|
|
106
108
|
const grant = PersonalWorkerGrant.parse(
|
|
107
109
|
await request("/api/personal/resources/register", { kind: "worker", ...identity, label: options.label, metadata: metadata() })
|
|
108
110
|
);
|
|
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
111
|
const runtime = await openPersonalWorkerRuntime({
|
|
119
112
|
root,
|
|
120
|
-
cliEntrypoint,
|
|
113
|
+
cliEntrypoint: cli.entrypoint,
|
|
121
114
|
grant,
|
|
122
115
|
storage: (sessionId) => async (storageRequest) => {
|
|
123
116
|
try {
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
durableJson,
|
|
20
20
|
ensureAuthorityGitRepositoryLayout,
|
|
21
21
|
git,
|
|
22
|
+
materializeTree,
|
|
22
23
|
noSymlinkAncestors,
|
|
23
24
|
privateRoot,
|
|
24
25
|
readHostRegular,
|
|
@@ -542,15 +543,7 @@ class WorkspaceAuthority {
|
|
|
542
543
|
const entries = await selectedTree(b.repo, head);
|
|
543
544
|
const staged = path.join(b.directory, `hydrate-${randomUUID()}`);
|
|
544
545
|
await fs.mkdir(staged, { mode: 448 });
|
|
545
|
-
|
|
546
|
-
for (const entry of entries) {
|
|
547
|
-
const bytes = await git(b.repo, ["cat-file", "blob", entry.oid]);
|
|
548
|
-
sourceBytes(bytes, entry.file);
|
|
549
|
-
if ((total += bytes.length) > 128 * 1024 * 1024) throw new WorkspaceError("too_large", "Hydrated tree exceeds limit");
|
|
550
|
-
const target = path.join(staged, entry.file);
|
|
551
|
-
await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
|
|
552
|
-
await fs.writeFile(target, bytes, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
|
|
553
|
-
}
|
|
546
|
+
await materializeTree(b.repo, staged, entries, 128 * 1024 * 1024, "Hydrated tree exceeds limit");
|
|
554
547
|
await verifyIgnore(b.repo, staged);
|
|
555
548
|
const latest = await storage.read({
|
|
556
549
|
method: "repository.get",
|
|
@@ -573,14 +566,16 @@ class WorkspaceAuthority {
|
|
|
573
566
|
} else {
|
|
574
567
|
await this.cleanRefreshBase(b, expectedBase);
|
|
575
568
|
const original = await selectedTree(b.repo, expectedBase);
|
|
576
|
-
const originalPaths = new
|
|
569
|
+
const originalPaths = new Map(original.map((entry) => [entry.file, entry]));
|
|
577
570
|
for (const entry of entries) {
|
|
578
571
|
const target = path.join(b.config.cwd, entry.file);
|
|
579
572
|
const existing = await fs.lstat(target).catch((error) => {
|
|
580
573
|
if (error.code === "ENOENT") return null;
|
|
581
574
|
throw error;
|
|
582
575
|
});
|
|
583
|
-
|
|
576
|
+
const originalEntry = originalPaths.get(entry.file);
|
|
577
|
+
const expectedType = entry.mode === "160000" ? existing?.isDirectory() : existing?.isFile();
|
|
578
|
+
if (existing && (!originalEntry || originalEntry.mode === "160000" !== (entry.mode === "160000") || !expectedType || existing.isSymbolicLink()))
|
|
584
579
|
throw new WorkspaceError("refresh_collision", "Canonical source would replace retained local content");
|
|
585
580
|
let parent = path.dirname(target);
|
|
586
581
|
while (parent !== b.config.cwd) {
|
|
@@ -625,6 +620,7 @@ class WorkspaceAuthority {
|
|
|
625
620
|
changed(path.dirname(destination), retained);
|
|
626
621
|
}
|
|
627
622
|
for (const entry of original) {
|
|
623
|
+
if (entry.mode === "160000") continue;
|
|
628
624
|
const destination = path.join(retained, "source", entry.file);
|
|
629
625
|
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 448 });
|
|
630
626
|
await persistFile(path.join(b.config.cwd, entry.file));
|
|
@@ -634,6 +630,11 @@ class WorkspaceAuthority {
|
|
|
634
630
|
}
|
|
635
631
|
for (const entry of entries) {
|
|
636
632
|
const destination = path.join(b.config.cwd, entry.file);
|
|
633
|
+
if (entry.mode === "160000") {
|
|
634
|
+
await fs.mkdir(destination, { recursive: true, mode: 448 });
|
|
635
|
+
changed(destination, b.config.cwd);
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
637
638
|
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 448 });
|
|
638
639
|
await persistFile(path.join(staged, entry.file));
|
|
639
640
|
await fs.rename(path.join(staged, entry.file), destination);
|
|
@@ -1001,15 +1002,7 @@ class WorkspaceAuthority {
|
|
|
1001
1002
|
const entries = await selectedTree(b.repo, head);
|
|
1002
1003
|
const staged = path.join(b.directory, `import-${randomUUID()}`);
|
|
1003
1004
|
await fs.mkdir(staged, { mode: 448 });
|
|
1004
|
-
|
|
1005
|
-
for (const entry of entries) {
|
|
1006
|
-
const bytes2 = await git(b.repo, ["cat-file", "blob", entry.oid]);
|
|
1007
|
-
sourceBytes(bytes2, entry.file);
|
|
1008
|
-
if ((total += bytes2.length) > STORAGE_LIMITS.blobBytes) throw new WorkspaceError("too_large", "Imported tree exceeds limit");
|
|
1009
|
-
const destination = path.join(staged, entry.file);
|
|
1010
|
-
await fs.mkdir(path.dirname(destination), { recursive: true, mode: 448 });
|
|
1011
|
-
await fs.writeFile(destination, bytes2, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
|
|
1012
|
-
}
|
|
1005
|
+
await materializeTree(b.repo, staged, entries, STORAGE_LIMITS.blobBytes, "Imported tree exceeds limit");
|
|
1013
1006
|
const ignore = path.join(staged, ".gitignore");
|
|
1014
1007
|
try {
|
|
1015
1008
|
await verifyIgnore(b.repo, staged);
|
|
@@ -225,17 +225,34 @@ function sourceBytes(bytes, file) {
|
|
|
225
225
|
async function selectedTree(repo, commit) {
|
|
226
226
|
if (!/^[0-9a-f]{40}$/.test(commit) || (await git(repo, ["cat-file", "-t", commit])).toString().trim() !== "commit")
|
|
227
227
|
throw new WorkspaceError("invalid_tip", "Selected tip must be a SHA-1 commit");
|
|
228
|
-
const raw = await git(repo, ["ls-tree", "-
|
|
228
|
+
const raw = await git(repo, ["ls-tree", "-r", "-z", "--full-tree", commit]);
|
|
229
229
|
if (!Buffer.from(raw.toString("utf8")).equals(raw)) throw new WorkspaceError("unsafe_path", "Non-UTF8 tree names unsupported");
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
230
|
+
const records = raw.toString("utf8").split("\0").filter(Boolean);
|
|
231
|
+
if (records.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source entries");
|
|
232
|
+
const entries = [];
|
|
233
|
+
for (const record of records) {
|
|
234
|
+
const match = /^(?:(100644|100755) blob|(160000) commit) ([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
235
|
+
if (!match) throw new WorkspaceError("unsafe_tree", "Symlinks and special tree modes unsupported");
|
|
236
|
+
sourcePath(match[4]);
|
|
237
|
+
entries.push({ file: match[4], mode: match[1] ?? match[2], oid: match[3] });
|
|
238
|
+
}
|
|
237
239
|
return entries;
|
|
238
240
|
}
|
|
241
|
+
async function materializeTree(repo, destination, entries, limit, message) {
|
|
242
|
+
let total = 0;
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const target = path.join(destination, entry.file);
|
|
245
|
+
if (entry.mode === "160000") {
|
|
246
|
+
await fs.mkdir(target, { recursive: true, mode: 448 });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const bytes = await git(repo, ["cat-file", "blob", entry.oid]);
|
|
250
|
+
sourceBytes(bytes, entry.file);
|
|
251
|
+
if ((total += bytes.length) > limit) throw new WorkspaceError("too_large", message);
|
|
252
|
+
await fs.mkdir(path.dirname(target), { recursive: true, mode: 448 });
|
|
253
|
+
await fs.writeFile(target, bytes, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
239
256
|
async function verifyIgnore(repo, cwd) {
|
|
240
257
|
await readRegular(path.join(cwd, ".gitignore"), 64 * 1024);
|
|
241
258
|
const probes = [
|
|
@@ -261,6 +278,7 @@ async function verifyIgnore(repo, cwd) {
|
|
|
261
278
|
}
|
|
262
279
|
async function snapshotTree(repo, cwd, canonicalHead) {
|
|
263
280
|
const metadata = path.join(cwd, ".git");
|
|
281
|
+
let workbenchIndex = null;
|
|
264
282
|
if (await fs.lstat(metadata).then(
|
|
265
283
|
() => true,
|
|
266
284
|
(e) => {
|
|
@@ -305,24 +323,84 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
305
323
|
await fs.writeFile(copy, await readRegular(index, 8 * 1024 * 1024), { mode: 384 });
|
|
306
324
|
if ((await git(repo, ["ls-files", "--unmerged", "-z"], void 0, copy)).length)
|
|
307
325
|
throw new WorkspaceError("workbench_conflict", "Unmerged index entries retained; resolve explicitly before publication");
|
|
326
|
+
workbenchIndex = copy;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const canonical = canonicalHead ? await selectedTree(repo, canonicalHead) : [];
|
|
330
|
+
const indexedGitlinks = /* @__PURE__ */ new Map();
|
|
331
|
+
if (workbenchIndex) {
|
|
332
|
+
const indexed = await git(repo, ["ls-files", "--stage", "-z"], void 0, workbenchIndex);
|
|
333
|
+
if (!Buffer.from(indexed.toString()).equals(indexed)) throw new WorkspaceError("unsafe_path", "Non-UTF8 index names unsupported");
|
|
334
|
+
for (const record of indexed.toString().split("\0").filter(Boolean)) {
|
|
335
|
+
const match = /^160000 ([0-9a-f]{40}) 0\t(.+)$/.exec(record);
|
|
336
|
+
if (record.startsWith("160000 ") && !match) throw new WorkspaceError("unsafe_tree", "Malformed gitlink index entry");
|
|
337
|
+
if (!match) continue;
|
|
338
|
+
sourcePath(match[2]);
|
|
339
|
+
indexedGitlinks.set(match[2], match[1]);
|
|
308
340
|
}
|
|
309
341
|
}
|
|
342
|
+
const canonicalGitlinks = new Map(canonical.filter((entry) => entry.mode === "160000").map((entry) => [entry.file, entry.oid]));
|
|
343
|
+
const gitlinks = new Map([...canonicalGitlinks, ...indexedGitlinks]);
|
|
344
|
+
for (const file of [...gitlinks.keys()]) {
|
|
345
|
+
const entry = await fs.lstat(path.join(cwd, file)).catch((error) => {
|
|
346
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return null;
|
|
347
|
+
throw error;
|
|
348
|
+
});
|
|
349
|
+
if (!entry) gitlinks.delete(file);
|
|
350
|
+
else if (!entry.isDirectory() || entry.isSymbolicLink()) gitlinks.delete(file);
|
|
351
|
+
}
|
|
352
|
+
const gitlinkRoots = [];
|
|
353
|
+
for (const file of [...gitlinks.keys()].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))) {
|
|
354
|
+
let parent = path.posix.dirname(file);
|
|
355
|
+
let nested = false;
|
|
356
|
+
while (parent !== ".") {
|
|
357
|
+
if (gitlinks.has(parent)) {
|
|
358
|
+
nested = true;
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
parent = path.posix.dirname(parent);
|
|
362
|
+
}
|
|
363
|
+
if (nested) gitlinks.delete(file);
|
|
364
|
+
else gitlinkRoots.push(file);
|
|
365
|
+
}
|
|
310
366
|
await verifyIgnore(repo, cwd);
|
|
311
367
|
await git(repo, ["read-tree", "--empty"]);
|
|
312
|
-
const listing = await git(repo, [
|
|
368
|
+
const listing = await git(repo, [
|
|
369
|
+
`--work-tree=${cwd}`,
|
|
370
|
+
"ls-files",
|
|
371
|
+
"--others",
|
|
372
|
+
"--exclude-standard",
|
|
373
|
+
"-z",
|
|
374
|
+
"--",
|
|
375
|
+
".",
|
|
376
|
+
...gitlinkRoots.map((file) => `:(exclude,literal)${file}`)
|
|
377
|
+
]);
|
|
313
378
|
if (!Buffer.from(listing.toString()).equals(listing)) throw new WorkspaceError("unsafe_path", "Non-UTF8 names unsupported");
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (
|
|
379
|
+
const beneathGitlink = (file) => {
|
|
380
|
+
let current = file;
|
|
381
|
+
while (true) {
|
|
382
|
+
if (gitlinks.has(current)) return true;
|
|
383
|
+
const parent = path.posix.dirname(current);
|
|
384
|
+
if (parent === "." || parent === current) return false;
|
|
385
|
+
current = parent;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
const tracked = canonical.filter((entry) => entry.mode !== "160000").map((entry) => entry.file);
|
|
389
|
+
const existingTracked = [];
|
|
390
|
+
for (const file of tracked) {
|
|
391
|
+
const exists = await fs.lstat(path.join(cwd, file)).then(() => true, (error) => {
|
|
392
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return false;
|
|
318
393
|
throw error;
|
|
319
394
|
});
|
|
320
|
-
if (exists)
|
|
395
|
+
if (exists) existingTracked.push(file);
|
|
321
396
|
}
|
|
322
|
-
const files = [.../* @__PURE__ */ new Set([...listing.toString().split("\0").filter(Boolean), ...
|
|
323
|
-
if (files.length > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source files");
|
|
397
|
+
const files = [.../* @__PURE__ */ new Set([...listing.toString().split("\0").filter(Boolean), ...existingTracked])].filter((file) => !beneathGitlink(file)).sort();
|
|
398
|
+
if (files.length + gitlinks.size > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source files");
|
|
324
399
|
let size = 0;
|
|
325
|
-
const records = []
|
|
400
|
+
const records = [...gitlinks].map(([file, oid]) => ({
|
|
401
|
+
file,
|
|
402
|
+
value: `160000 ${oid} ${file}\0`
|
|
403
|
+
}));
|
|
326
404
|
for (const file of files) {
|
|
327
405
|
sourcePath(file);
|
|
328
406
|
const bytes = await readRegular(path.join(cwd, file));
|
|
@@ -330,16 +408,17 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
330
408
|
if ((size += bytes.length) > 128 * 1024 * 1024) throw new WorkspaceError("too_large", "Source snapshot exceeds 128 MiB");
|
|
331
409
|
const st = await fs.lstat(path.join(cwd, file));
|
|
332
410
|
const oid = (await git(repo, ["hash-object", "-w", "--stdin", "--no-filters"], bytes)).toString().trim();
|
|
333
|
-
records.push(`${st.mode & 73 ? "100755" : "100644"} ${oid} ${file}\0`);
|
|
411
|
+
records.push({ file, value: `${st.mode & 73 ? "100755" : "100644"} ${oid} ${file}\0` });
|
|
334
412
|
}
|
|
335
413
|
if (!files.includes(".gitignore")) throw new WorkspaceError("ignore_policy", "The .gitignore policy itself must be published");
|
|
336
|
-
await git(repo, ["update-index", "-z", "--index-info"], records.join(""));
|
|
414
|
+
await git(repo, ["update-index", "-z", "--index-info"], records.sort((a, b) => a.file.localeCompare(b.file)).map((record) => record.value).join(""));
|
|
337
415
|
return (await git(repo, ["write-tree"])).toString().trim();
|
|
338
416
|
}
|
|
339
417
|
export {
|
|
340
418
|
durableJson,
|
|
341
419
|
ensureAuthorityGitRepositoryLayout,
|
|
342
420
|
git,
|
|
421
|
+
materializeTree,
|
|
343
422
|
noSymlinkAncestors,
|
|
344
423
|
privateRoot,
|
|
345
424
|
readHostRegular,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type ResolutionDependencies = {
|
|
2
|
+
resolvePackage?: (specifier: string) => string;
|
|
3
|
+
which?: (command: string) => string | null;
|
|
4
|
+
};
|
|
5
|
+
export type PersonalCliEntrypoint = {
|
|
6
|
+
entrypoint: string;
|
|
7
|
+
version: string;
|
|
8
|
+
source: "explicit" | "bundled" | "path";
|
|
9
|
+
};
|
|
10
|
+
/** Resolve the CLI that agent shells receive. Published workers always prefer
|
|
11
|
+
* their exact npm dependency; PATH is only a development/source fallback. */
|
|
12
|
+
export declare function resolvePersonalCliEntrypoint(explicit: string | undefined, workerVersion: string, dependencies?: ResolutionDependencies): Promise<PersonalCliEntrypoint>;
|
|
13
|
+
export {};
|
|
@@ -20,9 +20,10 @@ export declare function sourcePath(file: string): void;
|
|
|
20
20
|
export declare function sourceBytes(bytes: Buffer, file?: string): void;
|
|
21
21
|
export type TreeEntry = {
|
|
22
22
|
file: string;
|
|
23
|
-
mode:
|
|
23
|
+
mode: "100644" | "100755" | "160000";
|
|
24
24
|
oid: string;
|
|
25
25
|
};
|
|
26
26
|
export declare function selectedTree(repo: string, commit: string): Promise<TreeEntry[]>;
|
|
27
|
+
export declare function materializeTree(repo: string, destination: string, entries: TreeEntry[], limit: number, message: string): Promise<void>;
|
|
27
28
|
export declare function verifyIgnore(repo: string, cwd: string): Promise<void>;
|
|
28
29
|
export declare function snapshotTree(repo: string, cwd: string, canonicalHead?: string | null): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ricsam/r5d-worker",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.147",
|
|
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.147",
|
|
25
|
+
"@ricsam/r5dctl": "0.0.147",
|
|
26
26
|
"node-pty": "1.1.0",
|
|
27
27
|
"zod": "^4.1.13",
|
|
28
28
|
"picomatch": "^4.0.3"
|