@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.
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.157",
3
+ "version": "0.0.159",
4
4
  "type": "commonjs"
5
5
  }
@@ -20605,7 +20605,7 @@ function resolveEntrypointPath(entrypoint) {
20605
20605
  }
20606
20606
  }
20607
20607
  function getR5dctlVersion() {
20608
- if (true) return "0.0.157";
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.157" : "development"}`);
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.157" : "development"
18
+ true ? "0.0.159" : "development"
19
19
  );
20
20
  console.log(`Worker connected: ${runtime.resourceId}`);
21
21
  let closing = false;
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.157",
3
+ "version": "0.0.159",
4
4
  "type": "module"
5
5
  }
@@ -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) throw new WorkspaceError(b.state.blocked.code, b.state.blocked.message);
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 = null;
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
- await this.assertAvailable(b);
1175
- return this.publishIdle(b, identity);
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
- if (!b.state.initialized || b.state.blocked)
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 verifyIgnore(repo, cwd) {
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 (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]);
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) > 128 * 1024 * 1024) throw new WorkspaceError("too_large", "Source snapshot exceeds 128 MiB");
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): Promise<{
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 verifyIgnore(repo: string, cwd: string): Promise<void>;
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.157",
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.157",
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"