@ricsam/r5d-worker 0.0.157 → 0.0.159
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/internal-r5dctl.cjs +1 -1
- package/dist/mjs/main.mjs +2 -2
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/personal/client.mjs +3 -0
- package/dist/mjs/personal/runtime.mjs +56 -7
- package/dist/mjs/runtime/workspace/authority.mjs +29 -31
- package/dist/mjs/runtime/workspace/contracts.mjs +0 -21
- package/dist/mjs/runtime/workspace/files.mjs +13 -35
- package/dist/types/personal/runtime.d.ts +5 -0
- package/dist/types/runtime/workspace/authority.d.ts +6 -3
- package/dist/types/runtime/workspace/contracts.d.ts +0 -1
- package/dist/types/runtime/workspace/files.d.ts +1 -2
- package/package.json +2 -2
package/dist/cjs/package.json
CHANGED
|
@@ -20605,7 +20605,7 @@ function resolveEntrypointPath(entrypoint) {
|
|
|
20605
20605
|
}
|
|
20606
20606
|
}
|
|
20607
20607
|
function getR5dctlVersion() {
|
|
20608
|
-
if (true) return "0.0.
|
|
20608
|
+
if (true) return "0.0.159";
|
|
20609
20609
|
const entrypoint = process.argv[1] ? resolveEntrypointPath(process.argv[1]) : null;
|
|
20610
20610
|
let current = entrypoint ? import_node_path2.default.dirname(entrypoint) : process.cwd();
|
|
20611
20611
|
for (let index = 0; index < 12; index += 1) {
|
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.159" : "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.159" : "development"
|
|
19
19
|
);
|
|
20
20
|
console.log(`Worker connected: ${runtime.resourceId}`);
|
|
21
21
|
let closing = false;
|
package/dist/mjs/package.json
CHANGED
|
@@ -125,6 +125,9 @@ async function startPersonalWorker(options, version) {
|
|
|
125
125
|
throw new WorkspaceError(error.code, "Scoped storage rejected the request; retain its original operation identity", error.rejectedBeforeAdmission);
|
|
126
126
|
throw error;
|
|
127
127
|
}
|
|
128
|
+
},
|
|
129
|
+
publicationReport: async (report) => {
|
|
130
|
+
await request(`${endpoint}/publication`, { instanceId: identity.instanceId, report });
|
|
128
131
|
}
|
|
129
132
|
});
|
|
130
133
|
const ledgerFile = path.join(root, "relay.sqlite");
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { PersonalTcpManager } from "./tcp.mjs";
|
|
3
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { promises as fs } from "node:fs";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { RuntimeHello, RuntimeId, OwnershipFence, OperationEnvelope, canonicalJson } from "@ricsam/r5d-api/runtime-protocol";
|
|
6
|
+
import { RuntimeHello, RuntimeId, OwnershipFence, OperationEnvelope, WorkspacePublicationReport, canonicalJson } from "@ricsam/r5d-api/runtime-protocol";
|
|
7
7
|
import { startHostExecutor } from "../runtime/daemon.mjs";
|
|
8
8
|
import { HostExecutorClient } from "../runtime/client.mjs";
|
|
9
9
|
import { tokenHash } from "../runtime/executor.mjs";
|
|
@@ -42,7 +42,8 @@ const PersonalWorkspaceRequest = z.object({
|
|
|
42
42
|
sharedWorkbenches: z.array(Workbench).max(32).optional(),
|
|
43
43
|
kind: z.enum(["inspect", "terminal", "product", "agent", "tcp"]),
|
|
44
44
|
command: z.record(z.string(), z.unknown()),
|
|
45
|
-
credentials: z.array(ExecutorCredential).max(32).optional()
|
|
45
|
+
credentials: z.array(ExecutorCredential).max(32).optional(),
|
|
46
|
+
remediation: z.literal(true).optional()
|
|
46
47
|
}).strict();
|
|
47
48
|
async function personalExecutorToken(root, grant) {
|
|
48
49
|
const identity = { installationId: grant.installationId, userId: grant.userId, resourceId: grant.resourceId, instanceId: grant.instanceId, workerId: grant.workerFence.resourceId };
|
|
@@ -252,6 +253,17 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
}
|
|
256
|
+
const blockedPublicationCodes = /* @__PURE__ */ new Set([
|
|
257
|
+
"secret_or_runtime_path",
|
|
258
|
+
"secret_content",
|
|
259
|
+
"unsafe_file",
|
|
260
|
+
"unsafe_git",
|
|
261
|
+
"unsafe_path",
|
|
262
|
+
"unsafe_tree",
|
|
263
|
+
"too_large"
|
|
264
|
+
]);
|
|
265
|
+
const conflictPublicationCodes = /* @__PURE__ */ new Set(["conflict", "workbench_conflict"]);
|
|
266
|
+
const failedPublicationAttempts = /* @__PURE__ */ new Map();
|
|
255
267
|
let publication = null, closing = false;
|
|
256
268
|
const synchronize = () => {
|
|
257
269
|
if (publication) return publication;
|
|
@@ -263,10 +275,43 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
263
275
|
if (!manifest || manifest.rootProfile !== "project") continue;
|
|
264
276
|
try {
|
|
265
277
|
const result = await authority.publish({ userId: grant.userId, sessionId });
|
|
278
|
+
failedPublicationAttempts.delete(workbenchId);
|
|
266
279
|
results.push({ workbenchId, ...result });
|
|
267
280
|
} catch (error) {
|
|
268
281
|
const code = error instanceof WorkspaceError ? error.code : "publication_failed";
|
|
269
282
|
results.push({ workbenchId, error: code });
|
|
283
|
+
if (code !== "not_initialized" && options.publicationReport) {
|
|
284
|
+
let startingHead = null;
|
|
285
|
+
try {
|
|
286
|
+
const status = await authority.status({ userId: grant.userId, sessionId });
|
|
287
|
+
startingHead = status.head;
|
|
288
|
+
} catch {
|
|
289
|
+
}
|
|
290
|
+
const outcome = conflictPublicationCodes.has(code) ? "conflict_blocked" : blockedPublicationCodes.has(code) ? "large_diff_blocked" : "failed";
|
|
291
|
+
const fingerprint = canonicalJson({ code, startingHead, outcome });
|
|
292
|
+
let attempt = failedPublicationAttempts.get(workbenchId);
|
|
293
|
+
if (!attempt || attempt.fingerprint !== fingerprint) {
|
|
294
|
+
attempt = { fingerprint, attemptId: randomUUID() };
|
|
295
|
+
failedPublicationAttempts.set(workbenchId, attempt);
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await options.publicationReport(WorkspacePublicationReport.parse({
|
|
299
|
+
protocol: 1,
|
|
300
|
+
attemptId: attempt.attemptId,
|
|
301
|
+
sessionId,
|
|
302
|
+
workbenchId,
|
|
303
|
+
repositoryId: manifest.repositoryId,
|
|
304
|
+
branch: manifest.branch,
|
|
305
|
+
outcome,
|
|
306
|
+
startingHead,
|
|
307
|
+
diffSizeBytes: outcome === "large_diff_blocked" && code === "too_large" ? 5 * 1024 * 1024 + 1 : 0,
|
|
308
|
+
error: code
|
|
309
|
+
}));
|
|
310
|
+
} catch (reportError) {
|
|
311
|
+
process.stderr.write(`[r5d-worker] publication report deferred: ${reportError instanceof WorkspaceError ? reportError.code : "publication_report_failed"}
|
|
312
|
+
`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
270
315
|
}
|
|
271
316
|
}
|
|
272
317
|
return results;
|
|
@@ -335,7 +380,8 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
335
380
|
...command.method === "open" ? { interactive: true, pty: { cols: command.cols, rows: command.rows } } : {},
|
|
336
381
|
credentials: (request.credentials ?? []).map(({ id, generation }) => ({ id, generation }))
|
|
337
382
|
}
|
|
338
|
-
})
|
|
383
|
+
}),
|
|
384
|
+
request.remediation === true
|
|
339
385
|
),
|
|
340
386
|
workerLabel: grant.label ?? grant.resourceId
|
|
341
387
|
};
|
|
@@ -350,7 +396,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
350
396
|
if (request.kind === "product") {
|
|
351
397
|
if (command.method === "fileWrite") {
|
|
352
398
|
await authority.ensureHydrated(identity);
|
|
353
|
-
return authority.fileWrite(identity, command);
|
|
399
|
+
return authority.fileWrite(identity, command, request.remediation === true);
|
|
354
400
|
}
|
|
355
401
|
if (command.method === "materializeArtifact") return authority.materializeArtifact(identity, command);
|
|
356
402
|
if (command.method === "seed") return authority.seedReadme(identity, command.readme);
|
|
@@ -362,7 +408,10 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
362
408
|
if (command.method === "commit") return authority.commit(identity, command);
|
|
363
409
|
if (command.method === "push") return authority.push(identity, command);
|
|
364
410
|
if (command.method === "reset") return authority.reset(identity, command);
|
|
365
|
-
if (command.method === "publish") return authority.publish(identity
|
|
411
|
+
if (command.method === "publish") return authority.publish(identity, {
|
|
412
|
+
allowLargeDiff: request.remediation === true && command.allowLargeDiff === true,
|
|
413
|
+
allowBlockedConflict: request.remediation === true
|
|
414
|
+
});
|
|
366
415
|
if (command.method === "reconcilePublication") return authority.reconcilePublication(identity);
|
|
367
416
|
}
|
|
368
417
|
if (request.kind === "agent") {
|
|
@@ -387,7 +436,7 @@ exec ${quote(executable)} ${quote(cli)} "$@"
|
|
|
387
436
|
cwd: manifests.get(routes.get(identity.sessionId)).cwd,
|
|
388
437
|
credentials: (request.credentials ?? []).map(({ id, generation }) => ({ id, generation }))
|
|
389
438
|
}
|
|
390
|
-
});
|
|
439
|
+
}, request.remediation === true);
|
|
391
440
|
}
|
|
392
441
|
if (command.method === "poll")
|
|
393
442
|
return authority.poll(identity, run(command.run.operationId), command.stdoutOffset, command.stderrOffset, command.maxBytes);
|
|
@@ -9,8 +9,7 @@ import { ExecutorError, isDefiniteStartRejection, ShellPayload } from "../protoc
|
|
|
9
9
|
import {
|
|
10
10
|
ApprovedWorkbench,
|
|
11
11
|
WorkspaceConfig,
|
|
12
|
-
WorkspaceError
|
|
13
|
-
DEFAULT_WORKSPACE_IGNORE
|
|
12
|
+
WorkspaceError
|
|
14
13
|
} from "./contracts.mjs";
|
|
15
14
|
import { SessionArtifactStore, SessionArtifactChunk, RESERVED_ARTIFACT_ENV } from "./artifacts.mjs";
|
|
16
15
|
import { WorkspaceFileWrite, WorkspaceFileWriteLookup } from "./file-write.mjs";
|
|
@@ -27,8 +26,7 @@ import {
|
|
|
27
26
|
selectedTree,
|
|
28
27
|
sha256,
|
|
29
28
|
snapshotTree,
|
|
30
|
-
sourceBytes
|
|
31
|
-
verifyIgnore
|
|
29
|
+
sourceBytes
|
|
32
30
|
} from "./files.mjs";
|
|
33
31
|
const GITHUB_CREDENTIAL_HELPER = '!f() { if [ "$1" = get ] && [ -n "$R5D_GIT_CREDENTIAL" ]; then git credential-store --file="$R5D_GIT_CREDENTIAL" get; else return 0; fi; }; f';
|
|
34
32
|
class WorkspaceAuthority {
|
|
@@ -554,7 +552,6 @@ class WorkspaceAuthority {
|
|
|
554
552
|
const staged = path.join(b.directory, `hydrate-${randomUUID()}`);
|
|
555
553
|
await fs.mkdir(staged, { mode: 448 });
|
|
556
554
|
await materializeTree(b.repo, staged, entries, 128 * 1024 * 1024, "Hydrated tree exceeds limit");
|
|
557
|
-
await verifyIgnore(b.repo, staged);
|
|
558
555
|
const latest = await storage.read({
|
|
559
556
|
method: "repository.get",
|
|
560
557
|
repositoryId: b.config.repositoryId,
|
|
@@ -767,14 +764,12 @@ class WorkspaceAuthority {
|
|
|
767
764
|
branch: b.config.branch
|
|
768
765
|
});
|
|
769
766
|
if (existing.head) throw new WorkspaceError("conflict", "Canonical branch already has a head; hydrate instead");
|
|
770
|
-
await fs.writeFile(path.join(b.config.cwd, ".gitignore"), DEFAULT_WORKSPACE_IGNORE, { mode: 384, flag: "wx" });
|
|
771
767
|
await fs.writeFile(path.join(b.config.cwd, "README.md"), readme, { mode: 384, flag: "wx" });
|
|
772
768
|
b.state.initialized = true;
|
|
773
769
|
b.state.blocked = null;
|
|
774
770
|
await this.save(b);
|
|
775
771
|
const result = await this.publishIdle(b, identity);
|
|
776
772
|
if (this.linkedWorkbench(b)) {
|
|
777
|
-
await fs.unlink(path.join(b.config.cwd, ".gitignore"));
|
|
778
773
|
await fs.unlink(path.join(b.config.cwd, "README.md"));
|
|
779
774
|
}
|
|
780
775
|
await this.installGitPolicy(b, result.head);
|
|
@@ -866,15 +861,18 @@ class WorkspaceAuthority {
|
|
|
866
861
|
return { state: "completed", result: receipt.result };
|
|
867
862
|
});
|
|
868
863
|
}
|
|
869
|
-
async fileWrite(identity, raw) {
|
|
864
|
+
async fileWrite(identity, raw, allowBlockedConflict = false) {
|
|
870
865
|
const input = WorkspaceFileWrite.parse(raw), bytes = Buffer.from(input.base64, "base64");
|
|
871
866
|
if (bytes.toString("base64") !== input.base64 || bytes.length > 768 * 1024)
|
|
872
867
|
throw new WorkspaceError("invalid_input", "Expected canonical base64 for at most 768 KiB", true);
|
|
873
868
|
const b = await this.bench(identity), target = input.hostPath ? this.resolveHostPath(input.path) : path.join(b.config.cwd, input.path);
|
|
874
869
|
const fingerprint = { ...input, userId: identity.userId, sessionId: identity.sessionId };
|
|
875
870
|
let mode = 420;
|
|
871
|
+
let retainedBlock = null;
|
|
876
872
|
const check = async () => {
|
|
877
|
-
if (b.state.blocked
|
|
873
|
+
if (b.state.blocked && !(allowBlockedConflict && (b.state.blocked.code === "conflict" || b.state.blocked.code === "workbench_conflict")))
|
|
874
|
+
throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
|
|
875
|
+
retainedBlock = b.state.blocked;
|
|
878
876
|
if (!b.state.initialized) throw new WorkspaceError("uninitialized", "Hydrate this workspace before writing files");
|
|
879
877
|
let current = input.hostPath ? path.parse(target).root : b.config.cwd;
|
|
880
878
|
const parentParts = (input.hostPath ? target.slice(current.length) : input.path).split(path.sep).filter(Boolean).slice(0, -1);
|
|
@@ -936,7 +934,7 @@ class WorkspaceAuthority {
|
|
|
936
934
|
};
|
|
937
935
|
}, check);
|
|
938
936
|
if (b.state.blocked?.code === "file_write_unknown" && b.state.blocked.operationId === input.id) {
|
|
939
|
-
b.state.blocked =
|
|
937
|
+
b.state.blocked = retainedBlock;
|
|
940
938
|
await this.save(b);
|
|
941
939
|
}
|
|
942
940
|
return result;
|
|
@@ -1034,21 +1032,6 @@ class WorkspaceAuthority {
|
|
|
1034
1032
|
const staged = path.join(b.directory, `import-${randomUUID()}`);
|
|
1035
1033
|
await fs.mkdir(staged, { mode: 448 });
|
|
1036
1034
|
await materializeTree(b.repo, staged, entries, STORAGE_LIMITS.blobBytes, "Imported tree exceeds limit");
|
|
1037
|
-
const ignore = path.join(staged, ".gitignore");
|
|
1038
|
-
try {
|
|
1039
|
-
await verifyIgnore(b.repo, staged);
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
if (!(error instanceof WorkspaceError) && error.code !== "ENOENT") throw error;
|
|
1042
|
-
const oldIgnore = await fs.readFile(ignore, "utf8").catch((error2) => {
|
|
1043
|
-
if (error2.code === "ENOENT") return "";
|
|
1044
|
-
throw error2;
|
|
1045
|
-
});
|
|
1046
|
-
await fs.writeFile(ignore, `${oldIgnore}
|
|
1047
|
-
${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
1048
|
-
await verifyIgnore(b.repo, staged);
|
|
1049
|
-
}
|
|
1050
|
-
const tree = await snapshotTree(b.repo, staged, head);
|
|
1051
|
-
if (tree !== (await git(b.repo, ["rev-parse", `${head}^{tree}`])).toString().trim()) head = (await git(b.repo, ["commit-tree", tree, "-p", head], "Configure workspace exclusions\n")).toString().trim();
|
|
1052
1035
|
const storage = await this.storage(b, identity), operationId = `product-${sha256(id).slice(0, 48)}`;
|
|
1053
1036
|
let existing;
|
|
1054
1037
|
try {
|
|
@@ -1167,15 +1150,16 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1167
1150
|
return { ...result, reset: true, retained: true };
|
|
1168
1151
|
}));
|
|
1169
1152
|
}
|
|
1170
|
-
async publish(identity) {
|
|
1153
|
+
async publish(identity, options = {}) {
|
|
1171
1154
|
const b = await this.bench(identity);
|
|
1172
1155
|
if (b.config.rootProfile === "account") return { head: "", unchanged: true };
|
|
1173
1156
|
return this.serial(b, async () => {
|
|
1174
|
-
|
|
1175
|
-
|
|
1157
|
+
const conflictBlocked = b.state.blocked?.code === "conflict" || b.state.blocked?.code === "workbench_conflict";
|
|
1158
|
+
await this.assertAvailable(b, options.allowBlockedConflict === true && conflictBlocked);
|
|
1159
|
+
return this.publishIdle(b, identity, "Destination workspace snapshot", options.allowLargeDiff === true);
|
|
1176
1160
|
});
|
|
1177
1161
|
}
|
|
1178
|
-
async publishIdle(b, identity, message = "Destination workspace snapshot") {
|
|
1162
|
+
async publishIdle(b, identity, message = "Destination workspace snapshot", allowLargeDiff = false) {
|
|
1179
1163
|
if (!b.state.initialized) throw new WorkspaceError("not_initialized", "Hydrate or explicitly seed first");
|
|
1180
1164
|
const storage = await this.storage(b, identity);
|
|
1181
1165
|
const latest = await storage.read({
|
|
@@ -1194,6 +1178,19 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1194
1178
|
const tree = await snapshotTree(b.repo, b.config.cwd, b.state.head);
|
|
1195
1179
|
if (b.state.head && tree === (await git(b.repo, ["rev-parse", `${b.state.head}^{tree}`])).toString().trim())
|
|
1196
1180
|
return { head: b.state.head, unchanged: true };
|
|
1181
|
+
if (!allowLargeDiff) {
|
|
1182
|
+
const base = b.state.head ?? (await git(b.repo, ["hash-object", "-t", "tree", "--stdin"], "")).toString().trim();
|
|
1183
|
+
let diff;
|
|
1184
|
+
try {
|
|
1185
|
+
diff = await git(b.repo, ["diff", "--binary", "--no-ext-diff", "--no-textconv", base, tree]);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
if (error instanceof WorkspaceError && error.code === "git_failed")
|
|
1188
|
+
throw new WorkspaceError("too_large", "Source diff exceeds the 5242880-byte automatic publication limit");
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1191
|
+
if (diff.length > 5 * 1024 * 1024)
|
|
1192
|
+
throw new WorkspaceError("too_large", "Source diff exceeds the 5242880-byte automatic publication limit");
|
|
1193
|
+
}
|
|
1197
1194
|
const commit = (await git(b.repo, ["commit-tree", tree, ...b.state.head ? ["-p", b.state.head] : []], `${message}
|
|
1198
1195
|
`)).toString().trim();
|
|
1199
1196
|
await git(b.repo, ["update-ref", this.canonicalRef(b), commit]);
|
|
@@ -1312,11 +1309,12 @@ ${DEFAULT_WORKSPACE_IGNORE}`, { mode: 384 });
|
|
|
1312
1309
|
return this.serial(b, () => new SessionArtifactStore(this.config.workspaceRoot ?? this.config.root).materialize(identity, input));
|
|
1313
1310
|
}
|
|
1314
1311
|
/** Default arbitrary shell is mutating. Exact trusted config approval is the sole exemption. */
|
|
1315
|
-
async start(identity, input) {
|
|
1312
|
+
async start(identity, input, allowBlockedConflict = false) {
|
|
1316
1313
|
const b = await this.bench(identity);
|
|
1317
1314
|
const operation = OperationEnvelope.parse(JSON.parse(canonicalJson(input)));
|
|
1318
1315
|
return this.serial(b, async () => {
|
|
1319
|
-
|
|
1316
|
+
const conflictBlocked = b.state.blocked?.code === "conflict" || b.state.blocked?.code === "workbench_conflict";
|
|
1317
|
+
if (!b.state.initialized || b.state.blocked && !(allowBlockedConflict && conflictBlocked))
|
|
1320
1318
|
throw new WorkspaceError("workbench_blocked", b.state.blocked?.message ?? "Initialize workbench first");
|
|
1321
1319
|
if (operation.installationId !== this.config.installationId || operation.userId !== identity.userId || operation.sessionId !== identity.sessionId || operation.kind !== "host.shell")
|
|
1322
1320
|
throw new WorkspaceError("forbidden", "Shell identity does not match approved workbench");
|
|
@@ -36,29 +36,8 @@ class WorkspaceError extends Error {
|
|
|
36
36
|
code;
|
|
37
37
|
rejectedBeforeAdmission;
|
|
38
38
|
}
|
|
39
|
-
const DEFAULT_WORKSPACE_IGNORE = `# Required destination workbench exclusions; keep these rules effective.
|
|
40
|
-
.env
|
|
41
|
-
.env.*
|
|
42
|
-
!.env.example
|
|
43
|
-
node_modules/
|
|
44
|
-
.r5d/
|
|
45
|
-
.r5d-next/
|
|
46
|
-
.ssh/
|
|
47
|
-
.kube/
|
|
48
|
-
secrets/
|
|
49
|
-
credentials/
|
|
50
|
-
.next/
|
|
51
|
-
dist/
|
|
52
|
-
build/
|
|
53
|
-
coverage/
|
|
54
|
-
.cache/
|
|
55
|
-
.bun/
|
|
56
|
-
*.pem
|
|
57
|
-
*.key
|
|
58
|
-
`;
|
|
59
39
|
export {
|
|
60
40
|
ApprovedWorkbench,
|
|
61
|
-
DEFAULT_WORKSPACE_IGNORE,
|
|
62
41
|
WorkspaceConfig,
|
|
63
42
|
WorkspaceError
|
|
64
43
|
};
|
|
@@ -253,30 +253,7 @@ async function materializeTree(repo, destination, entries, limit, message) {
|
|
|
253
253
|
await fs.writeFile(target, bytes, { mode: entry.mode === "100755" ? 448 : 384, flag: "wx" });
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
|
-
async function
|
|
257
|
-
await readRegular(path.join(cwd, ".gitignore"), 64 * 1024);
|
|
258
|
-
const probes = [
|
|
259
|
-
".env",
|
|
260
|
-
".env.local",
|
|
261
|
-
"node_modules/probe",
|
|
262
|
-
".r5d/probe",
|
|
263
|
-
".r5d-next/probe",
|
|
264
|
-
".ssh/probe",
|
|
265
|
-
".kube/probe",
|
|
266
|
-
"secrets/probe",
|
|
267
|
-
"credentials/probe",
|
|
268
|
-
".next/probe",
|
|
269
|
-
"dist/probe",
|
|
270
|
-
"coverage/probe",
|
|
271
|
-
".cache/probe",
|
|
272
|
-
"probe.pem",
|
|
273
|
-
"probe.key"
|
|
274
|
-
];
|
|
275
|
-
const ignored = (await git(repo, [`--work-tree=${cwd}`, "check-ignore", "--no-index", "-z", "--stdin"], probes.join("\0") + "\0")).toString().split("\0").filter(Boolean);
|
|
276
|
-
if (ignored.length !== probes.length || probes.some((p) => !ignored.includes(p)))
|
|
277
|
-
throw new WorkspaceError("ignore_policy", "Restore effective .gitignore rules for secrets, dependency, cache and runtime roots");
|
|
278
|
-
}
|
|
279
|
-
async function snapshotTree(repo, cwd, canonicalHead) {
|
|
256
|
+
async function snapshotTree(repo, cwd, canonicalHead, maxBytes = 128 * 1024 * 1024) {
|
|
280
257
|
const metadata = path.join(cwd, ".git");
|
|
281
258
|
let workbenchIndex = null;
|
|
282
259
|
if (await fs.lstat(metadata).then(
|
|
@@ -328,15 +305,16 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
328
305
|
}
|
|
329
306
|
const canonical = canonicalHead ? await selectedTree(repo, canonicalHead) : [];
|
|
330
307
|
const indexedGitlinks = /* @__PURE__ */ new Map();
|
|
308
|
+
const indexedFiles = /* @__PURE__ */ new Set();
|
|
331
309
|
if (workbenchIndex) {
|
|
332
310
|
const indexed = await git(repo, ["ls-files", "--stage", "-z"], void 0, workbenchIndex);
|
|
333
311
|
if (!Buffer.from(indexed.toString()).equals(indexed)) throw new WorkspaceError("unsafe_path", "Non-UTF8 index names unsupported");
|
|
334
312
|
for (const record of indexed.toString().split("\0").filter(Boolean)) {
|
|
335
|
-
const match = /^160000 ([0-9a-f]{40}) 0\t(.+)$/.exec(record);
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
313
|
+
const match = /^(100644|100755|160000) ([0-9a-f]{40}) 0\t(.+)$/.exec(record);
|
|
314
|
+
if (!match) throw new WorkspaceError("unsafe_tree", "Malformed workbench index entry");
|
|
315
|
+
sourcePath(match[3]);
|
|
316
|
+
if (match[1] === "160000") indexedGitlinks.set(match[3], match[2]);
|
|
317
|
+
else indexedFiles.add(match[3]);
|
|
340
318
|
}
|
|
341
319
|
}
|
|
342
320
|
const canonicalGitlinks = new Map(canonical.filter((entry) => entry.mode === "160000").map((entry) => [entry.file, entry.oid]));
|
|
@@ -363,7 +341,6 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
363
341
|
if (nested) gitlinks.delete(file);
|
|
364
342
|
else gitlinkRoots.push(file);
|
|
365
343
|
}
|
|
366
|
-
await verifyIgnore(repo, cwd);
|
|
367
344
|
await git(repo, ["read-tree", "--empty"]);
|
|
368
345
|
const listing = await git(repo, [
|
|
369
346
|
`--work-tree=${cwd}`,
|
|
@@ -385,7 +362,7 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
385
362
|
current = parent;
|
|
386
363
|
}
|
|
387
364
|
};
|
|
388
|
-
const tracked = canonical.filter((entry) => entry.mode !== "160000").map((entry) => entry.file);
|
|
365
|
+
const tracked = [.../* @__PURE__ */ new Set([...canonical.filter((entry) => entry.mode !== "160000").map((entry) => entry.file), ...indexedFiles])];
|
|
389
366
|
const existingTracked = [];
|
|
390
367
|
for (const file of tracked) {
|
|
391
368
|
const exists = await fs.lstat(path.join(cwd, file)).then(() => true, (error) => {
|
|
@@ -395,6 +372,8 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
395
372
|
if (exists) existingTracked.push(file);
|
|
396
373
|
}
|
|
397
374
|
const files = [.../* @__PURE__ */ new Set([...listing.toString().split("\0").filter(Boolean), ...existingTracked])].filter((file) => !beneathGitlink(file)).sort();
|
|
375
|
+
if (files.some((file) => file.split("/").some((part) => part.toLowerCase() === "node_modules")))
|
|
376
|
+
throw new WorkspaceError("too_large", "An unignored dependency tree exceeds the automatic publication limit");
|
|
398
377
|
if (files.length + gitlinks.size > STORAGE_LIMITS.treeEntries) throw new WorkspaceError("too_large", "Too many source files");
|
|
399
378
|
let size = 0;
|
|
400
379
|
const records = [...gitlinks].map(([file, oid]) => ({
|
|
@@ -405,12 +384,12 @@ async function snapshotTree(repo, cwd, canonicalHead) {
|
|
|
405
384
|
sourcePath(file);
|
|
406
385
|
const bytes = await readRegular(path.join(cwd, file));
|
|
407
386
|
sourceBytes(bytes, file);
|
|
408
|
-
if ((size += bytes.length) >
|
|
387
|
+
if ((size += bytes.length) > maxBytes)
|
|
388
|
+
throw new WorkspaceError("too_large", `Source snapshot exceeds the ${maxBytes}-byte automatic publication limit`);
|
|
409
389
|
const st = await fs.lstat(path.join(cwd, file));
|
|
410
390
|
const oid = (await git(repo, ["hash-object", "-w", "--stdin", "--no-filters"], bytes)).toString().trim();
|
|
411
391
|
records.push({ file, value: `${st.mode & 73 ? "100755" : "100644"} ${oid} ${file}\0` });
|
|
412
392
|
}
|
|
413
|
-
if (!files.includes(".gitignore")) throw new WorkspaceError("ignore_policy", "The .gitignore policy itself must be published");
|
|
414
393
|
await git(repo, ["update-index", "-z", "--index-info"], records.sort((a, b) => a.file.localeCompare(b.file)).map((record) => record.value).join(""));
|
|
415
394
|
return (await git(repo, ["write-tree"])).toString().trim();
|
|
416
395
|
}
|
|
@@ -427,6 +406,5 @@ export {
|
|
|
427
406
|
sha256,
|
|
428
407
|
snapshotTree,
|
|
429
408
|
sourceBytes,
|
|
430
|
-
sourcePath
|
|
431
|
-
verifyIgnore
|
|
409
|
+
sourcePath
|
|
432
410
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { WorkspacePublicationReport } from "@ricsam/r5d-api/runtime-protocol";
|
|
2
3
|
import { WorkspaceAuthority } from "../runtime/workspace/authority";
|
|
3
4
|
import { type StorageTransport } from "../runtime/workspace/storage-client";
|
|
4
5
|
export declare const PersonalWorkerGrant: z.ZodObject<{
|
|
@@ -96,6 +97,7 @@ export declare const PersonalWorkspaceRequest: z.ZodObject<{
|
|
|
96
97
|
env: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
97
98
|
files: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
98
99
|
}, z.core.$strict>>>;
|
|
100
|
+
remediation: z.ZodOptional<z.ZodLiteral<true>>;
|
|
99
101
|
}, z.core.$strict>;
|
|
100
102
|
export type PersonalWorkerRuntime = Awaited<ReturnType<typeof openPersonalWorkerRuntime>>;
|
|
101
103
|
/** The host owns this keeper independently of every replaceable remote web/gateway generation. */
|
|
@@ -106,6 +108,9 @@ export declare function openPersonalWorkerRuntime(options: {
|
|
|
106
108
|
cliEntrypoint?: string;
|
|
107
109
|
/** Internal test/embedding override. Personal workers publish every minute. */
|
|
108
110
|
publicationIntervalMs?: number;
|
|
111
|
+
/** Authenticated transport for autonomous publication evidence. Failures are
|
|
112
|
+
* retried with the same attempt identity on the next publication cycle. */
|
|
113
|
+
publicationReport?: (report: z.infer<typeof WorkspacePublicationReport>) => Promise<void>;
|
|
109
114
|
}): Promise<{
|
|
110
115
|
grant: {
|
|
111
116
|
[x: string]: unknown;
|
|
@@ -256,7 +256,7 @@ export declare class WorkspaceAuthority {
|
|
|
256
256
|
state: "completed";
|
|
257
257
|
result: any;
|
|
258
258
|
}>;
|
|
259
|
-
fileWrite(identity: WorkspaceIdentity, raw: WorkspaceFileWrite): Promise<{
|
|
259
|
+
fileWrite(identity: WorkspaceIdentity, raw: WorkspaceFileWrite, allowBlockedConflict?: boolean): Promise<{
|
|
260
260
|
intentHash?: string | undefined;
|
|
261
261
|
method: string;
|
|
262
262
|
id: string;
|
|
@@ -323,7 +323,10 @@ export declare class WorkspaceAuthority {
|
|
|
323
323
|
retained: boolean;
|
|
324
324
|
head: string;
|
|
325
325
|
}>;
|
|
326
|
-
publish(identity: WorkspaceIdentity
|
|
326
|
+
publish(identity: WorkspaceIdentity, options?: {
|
|
327
|
+
allowLargeDiff?: boolean;
|
|
328
|
+
allowBlockedConflict?: boolean;
|
|
329
|
+
}): Promise<{
|
|
327
330
|
head: string;
|
|
328
331
|
unchanged?: boolean;
|
|
329
332
|
}>;
|
|
@@ -347,7 +350,7 @@ export declare class WorkspaceAuthority {
|
|
|
347
350
|
complete: boolean;
|
|
348
351
|
}>;
|
|
349
352
|
/** Default arbitrary shell is mutating. Exact trusted config approval is the sole exemption. */
|
|
350
|
-
start(identity: WorkspaceIdentity, input: OperationEnvelope): Promise<import("..").RunReceipt>;
|
|
353
|
+
start(identity: WorkspaceIdentity, input: OperationEnvelope, allowBlockedConflict?: boolean): Promise<import("..").RunReceipt>;
|
|
351
354
|
private runRoute;
|
|
352
355
|
private action;
|
|
353
356
|
poll(identity: WorkspaceIdentity, run: RunIdentity, stdoutOffset?: number, stderrOffset?: number, maxBytes?: number): Promise<PollResult>;
|
|
@@ -87,4 +87,3 @@ export type WorkspaceState = {
|
|
|
87
87
|
completedAt?: string;
|
|
88
88
|
}>;
|
|
89
89
|
};
|
|
90
|
-
export declare const DEFAULT_WORKSPACE_IGNORE = "# Required destination workbench exclusions; keep these rules effective.\n.env\n.env.*\n!.env.example\nnode_modules/\n.r5d/\n.r5d-next/\n.ssh/\n.kube/\nsecrets/\ncredentials/\n.next/\ndist/\nbuild/\ncoverage/\n.cache/\n.bun/\n*.pem\n*.key\n";
|
|
@@ -25,5 +25,4 @@ export type TreeEntry = {
|
|
|
25
25
|
};
|
|
26
26
|
export declare function selectedTree(repo: string, commit: string): Promise<TreeEntry[]>;
|
|
27
27
|
export declare function materializeTree(repo: string, destination: string, entries: TreeEntry[], limit: number, message: string): Promise<void>;
|
|
28
|
-
export declare function
|
|
29
|
-
export declare function snapshotTree(repo: string, cwd: string, canonicalHead?: string | null): Promise<string>;
|
|
28
|
+
export declare function snapshotTree(repo: string, cwd: string, canonicalHead?: string | null, maxBytes?: number): 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.159",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/mjs/main.mjs",
|
|
6
6
|
"module": "./dist/mjs/main.mjs",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"r5d-worker": "dist/mjs/main.mjs"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@ricsam/r5d-api": "^0.0.
|
|
24
|
+
"@ricsam/r5d-api": "^0.0.159",
|
|
25
25
|
"node-pty": "1.1.0",
|
|
26
26
|
"zod": "^4.1.13",
|
|
27
27
|
"picomatch": "^4.0.3"
|