@jigging/jig 0.1.0-alpha.11 → 0.1.0-alpha.13

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.
@@ -2,7 +2,7 @@
2
2
  // src/internal/linux-rootless-supervisor.ts
3
3
  import { spawn } from "child_process";
4
4
  import { randomBytes } from "crypto";
5
- import { closeSync, readFileSync, statSync, writeSync } from "fs";
5
+ import { closeSync, readFileSync, readSync, statSync, writeSync } from "fs";
6
6
  import {
7
7
  link,
8
8
  mkdir,
@@ -56,12 +56,17 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
56
56
  let childExit;
57
57
  let stopReason;
58
58
  let admitted = false;
59
+ let continued = false;
60
+ let outputDirectory;
59
61
  let resolveAdmission;
60
62
  let rejectAdmission;
61
63
  const admission = new Promise((resolve, reject) => {
62
64
  resolveAdmission = resolve;
63
65
  rejectAdmission = reject;
64
66
  });
67
+ admission.catch(() => {
68
+ return;
69
+ });
65
70
  const killRun = async () => {
66
71
  if (!cgroupCreated)
67
72
  return;
@@ -85,6 +90,12 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
85
90
  if (type === "admit" && !admitted) {
86
91
  admitted = true;
87
92
  resolveAdmission();
93
+ } else if (type === "continue" && configuration.output && outputDirectory !== undefined && !continued) {
94
+ continued = true;
95
+ const gate = child?.stdio[4];
96
+ if (gate === undefined)
97
+ return stop("setup_failed");
98
+ gate.end(Buffer.from([1]));
88
99
  } else if (type === "cancel") {
89
100
  stop("cancelled");
90
101
  } else {
@@ -113,7 +124,11 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
113
124
  `);
114
125
  if (stopReason !== undefined)
115
126
  throw new Error(`rootless launch stopped during setup: ${stopReason}`);
116
- safeSend(control, { type: "prepared", runCgroup, supervisorPid: process.pid });
127
+ safeSend(control, {
128
+ type: "prepared",
129
+ runCgroup,
130
+ supervisorPid: process.pid
131
+ });
117
132
  await admission;
118
133
  if (stopReason !== undefined)
119
134
  throw new Error(`rootless launch stopped before spawn: ${stopReason}`);
@@ -132,16 +147,30 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
132
147
  env: {
133
148
  LD_LIBRARY_PATH: configuration.bunHostLibraryPath
134
149
  },
135
- stdio: ["inherit", "inherit", "inherit", "pipe"]
150
+ stdio: [
151
+ "inherit",
152
+ "inherit",
153
+ "inherit",
154
+ "pipe",
155
+ configuration.output ? "pipe" : "ignore",
156
+ configuration.output ? "pipe" : "ignore",
157
+ ...configuration.capturedInputs.map((file) => file.fd)
158
+ ]
136
159
  });
137
160
  childExit = childClose(launched);
138
161
  child = launched;
139
162
  const ready = await readReady(launched);
163
+ if (configuration.output) {
164
+ const sandboxPid = await readSandboxPid(launched);
165
+ await outputReady(launched);
166
+ outputDirectory = await open(`/proc/${sandboxPid}/root/jig-output`, 65536);
167
+ }
140
168
  safeSend(control, {
141
169
  type: "ready",
142
170
  runCgroup,
143
171
  payloadPid: ready,
144
- supervisorPid: process.pid
172
+ supervisorPid: process.pid,
173
+ ...outputDirectory === undefined ? {} : { outputFd: outputDirectory.fd }
145
174
  });
146
175
  const exit = await childExit;
147
176
  exitCode = exit.code;
@@ -154,6 +183,7 @@ async function superviseConnected(control, startupDeadlineUnixMs) {
154
183
  `);
155
184
  } finally {
156
185
  clearTimeout(deadline);
186
+ await outputDirectory?.close();
157
187
  let cleanupError;
158
188
  try {
159
189
  child?.kill("SIGKILL");
@@ -216,13 +246,35 @@ async function enterMain(arguments_) {
216
246
  writeSync(3, `${process.pid}
217
247
  `);
218
248
  closeSync(3);
219
- const child = spawn(bubblewrap, bubblewrapArguments_, { cwd: "/", env: {}, stdio: "inherit" });
249
+ const output = bubblewrapArguments_.includes("--sync-fd");
250
+ const inputDescriptors = bubblewrapArguments_.flatMap((arg, index) => arg === "--ro-bind-data" ? [Number(bubblewrapArguments_[index + 1])] : []);
251
+ if (inputDescriptors.some((fd, index) => fd !== 6 + index))
252
+ throw new Error("invalid captured input descriptor order");
253
+ const child = spawn(bubblewrap, bubblewrapArguments_, {
254
+ cwd: "/",
255
+ env: {},
256
+ stdio: [
257
+ "inherit",
258
+ "inherit",
259
+ "inherit",
260
+ "ignore",
261
+ output ? 4 : "ignore",
262
+ output ? 5 : "ignore",
263
+ ...inputDescriptors
264
+ ]
265
+ });
266
+ for (const fd of inputDescriptors)
267
+ closeSync(fd);
268
+ if (output) {
269
+ closeSync(4);
270
+ closeSync(5);
271
+ }
220
272
  const exit = await childClose(child);
221
273
  if (exit.signal !== null)
222
274
  process.kill(process.pid, exit.signal);
223
275
  process.exitCode = exit.code ?? 1;
224
276
  }
225
- async function innerMain(command) {
277
+ async function innerMain(command, output = false) {
226
278
  if (command.length === 0 || !absolute(command[0]))
227
279
  throw new Error("invalid rootless inner command");
228
280
  const nullDevice = statSync("/dev/null");
@@ -230,7 +282,19 @@ async function innerMain(command) {
230
282
  if (!nullDevice.isCharacterDevice() || !entropyDevice.isCharacterDevice() || (nullDevice.mode & 511) !== 438 || (entropyDevice.mode & 511) !== 438) {
231
283
  throw new Error("rootless private devices do not satisfy the required projection");
232
284
  }
233
- const child = spawn(command[0], command.slice(1), { cwd: "/work", env: process.env, stdio: "inherit" });
285
+ if (output) {
286
+ writeSync(4, Buffer.from([1]));
287
+ const gate = Buffer.alloc(1);
288
+ if (readSync(4, gate, 0, 1, null) !== 1 || gate[0] !== 1) {
289
+ throw new Error("output ownership was not acquired");
290
+ }
291
+ closeSync(4);
292
+ }
293
+ const child = spawn(command[0], command.slice(1), {
294
+ cwd: "/work",
295
+ env: process.env,
296
+ stdio: "inherit"
297
+ });
234
298
  const exit = await childClose(child);
235
299
  if (exit.signal !== null)
236
300
  process.kill(process.pid, exit.signal);
@@ -272,11 +336,21 @@ function bubblewrapArguments(configuration) {
272
336
  ];
273
337
  for (const mount of configuration.readOnlyMounts)
274
338
  result.push("--ro-bind", mount.source, mount.destination);
339
+ if (configuration.inputDirectories.length > 0)
340
+ result.push("--size", "1048576", "--tmpfs", "/jig-input");
341
+ for (const path of configuration.inputDirectories)
342
+ result.push("--dir", path);
343
+ for (const file of configuration.capturedInputs)
344
+ result.push("--ro-bind-data", String(file.fd), file.destination);
345
+ if (configuration.inputDirectories.length > 0)
346
+ result.push("--remount-ro", "/jig-input");
347
+ if (configuration.output)
348
+ result.push("--size", String(16 * 1024 * 1024), "--tmpfs", "/jig-output", "--sync-fd", "4", "--json-status-fd", "5");
275
349
  result.push("--ro-bind", configuration.supervisorPath, MODULE_DESTINATION);
276
350
  for (const [name, value] of Object.entries(configuration.environment).sort(([a], [b]) => a.localeCompare(b))) {
277
351
  result.push("--setenv", name, value);
278
352
  }
279
- result.push("--setenv", "LD_LIBRARY_PATH", SANDBOX_LIBRARY_PATH, "--uid", String(configuration.payloadUid), "--gid", String(configuration.payloadGid), "--cap-drop", "ALL", "--", SANDBOX_BUN, ...POLICY, MODULE_DESTINATION, "--inner", "--", ...configuration.command);
353
+ result.push("--setenv", "LD_LIBRARY_PATH", SANDBOX_LIBRARY_PATH, "--uid", String(configuration.payloadUid), "--gid", String(configuration.payloadGid), "--cap-drop", "ALL", "--", SANDBOX_BUN, ...POLICY, MODULE_DESTINATION, configuration.output ? "--inner-output" : "--inner", "--", ...configuration.command);
280
354
  return result;
281
355
  }
282
356
  async function claimActive(configuration) {
@@ -361,12 +435,15 @@ function requireStart(value) {
361
435
  "bunHostLibraryPath",
362
436
  "bunPath",
363
437
  "command",
438
+ "capturedInputs",
364
439
  "delegatedCgroup",
365
440
  "environment",
366
441
  "limits",
442
+ "inputDirectories",
367
443
  "mechanismDigest",
368
444
  "nestedUserNamespaces",
369
445
  "network",
446
+ "output",
370
447
  "ownerDigest",
371
448
  "ownerStateAllocationDigest",
372
449
  "ownerStateDirectory",
@@ -378,7 +455,7 @@ function requireStart(value) {
378
455
  "sealedPlanDigest",
379
456
  "supervisorPath"
380
457
  ];
381
- if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration) || Object.keys(configuration).sort().join("\x00") !== keys.sort().join("\x00") || !absolute(configuration.delegatedCgroup) || !absolute(configuration.bunHostLibraryPath) || !absolute(configuration.runCgroup) || posix.dirname(configuration.runCgroup) !== configuration.delegatedCgroup || !/^jig-run-[a-z0-9][a-z0-9-]{0,47}-[0-9a-f]{24}$/.test(posix.basename(configuration.runCgroup)) || !configuration.runCgroup.startsWith("/sys/fs/cgroup/") || !absolute(configuration.ownerStateDirectory) || !absolute(configuration.bunPath) || !absolute(configuration.bubblewrapPath) || !absolute(configuration.supervisorPath) || typeof configuration.mechanismDigest !== "string" || !DIGEST.test(configuration.mechanismDigest) || typeof configuration.ownerDigest !== "string" || !DIGEST.test(configuration.ownerDigest) || typeof configuration.sealedPlanDigest !== "string" || !DIGEST.test(configuration.sealedPlanDigest) || typeof configuration.ownerStateAllocationDigest !== "string" || !DIGEST.test(configuration.ownerStateAllocationDigest) || typeof configuration.ownerToken !== "string" || !OWNER_TOKEN.test(configuration.ownerToken) || configuration.network !== "isolated" && configuration.network !== "inherited" || typeof configuration.nestedUserNamespaces !== "boolean" || !positiveInteger(configuration.payloadUid) || !positiveInteger(configuration.payloadGid) || !validLimits(configuration.limits) || !validMounts(configuration.readOnlyMounts) || !validEnvironment(configuration.environment) || !Array.isArray(configuration.command) || configuration.command.length === 0 || !absolute(configuration.command[0]) || configuration.command.some((part) => typeof part !== "string" || part.includes("\x00"))) {
458
+ if (configuration === null || typeof configuration !== "object" || Array.isArray(configuration) || Object.keys(configuration).sort().join("\x00") !== keys.sort().join("\x00") || !absolute(configuration.delegatedCgroup) || !absolute(configuration.bunHostLibraryPath) || !absolute(configuration.runCgroup) || posix.dirname(configuration.runCgroup) !== configuration.delegatedCgroup || !/^jig-run-[a-z0-9][a-z0-9-]{0,47}-[0-9a-f]{24}$/.test(posix.basename(configuration.runCgroup)) || !configuration.runCgroup.startsWith("/sys/fs/cgroup/") || !absolute(configuration.ownerStateDirectory) || !absolute(configuration.bunPath) || !absolute(configuration.bubblewrapPath) || !absolute(configuration.supervisorPath) || typeof configuration.mechanismDigest !== "string" || !DIGEST.test(configuration.mechanismDigest) || typeof configuration.ownerDigest !== "string" || !DIGEST.test(configuration.ownerDigest) || typeof configuration.sealedPlanDigest !== "string" || !DIGEST.test(configuration.sealedPlanDigest) || typeof configuration.ownerStateAllocationDigest !== "string" || !DIGEST.test(configuration.ownerStateAllocationDigest) || typeof configuration.ownerToken !== "string" || !OWNER_TOKEN.test(configuration.ownerToken) || configuration.network !== "isolated" && configuration.network !== "inherited" || typeof configuration.nestedUserNamespaces !== "boolean" || typeof configuration.output !== "boolean" || !Array.isArray(configuration.capturedInputs) || configuration.capturedInputs.length > 64 || !Array.isArray(configuration.inputDirectories) || configuration.inputDirectories.length > 8 || configuration.inputDirectories.some((path) => typeof path !== "string" || path.length > 75 || !/^\/jig-input\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(path)) || configuration.capturedInputs.some((file, index) => file === null || typeof file !== "object" || file.fd !== 6 + index || !absolute(file.destination) || !configuration.inputDirectories.some((path) => file.destination.startsWith(`${path}/`))) || !positiveInteger(configuration.payloadUid) || !positiveInteger(configuration.payloadGid) || !validLimits(configuration.limits) || !validMounts(configuration.readOnlyMounts) || !validEnvironment(configuration.environment) || !Array.isArray(configuration.command) || configuration.command.length === 0 || !absolute(configuration.command[0]) || configuration.command.some((part) => typeof part !== "string" || part.includes("\x00"))) {
382
459
  throw new Error("invalid rootless supervisor configuration");
383
460
  }
384
461
  return configuration;
@@ -461,6 +538,39 @@ async function readReady(child) {
461
538
  throw new Error("invalid rootless entry readiness receipt");
462
539
  return pid;
463
540
  }
541
+ async function readSandboxPid(child) {
542
+ const stream = child.stdio[5];
543
+ if (stream === undefined)
544
+ throw new Error("missing Bubblewrap status pipe");
545
+ let text = "";
546
+ for await (const chunk of stream.iterator({ destroyOnReturn: false })) {
547
+ text += String(chunk);
548
+ if (text.length > 4096)
549
+ throw new Error("oversized Bubblewrap status");
550
+ const end = text.indexOf("}");
551
+ if (end < 0)
552
+ continue;
553
+ const record = JSON.parse(text.slice(0, end + 1));
554
+ if (!positiveInteger(record["child-pid"]))
555
+ throw new Error("invalid Bubblewrap child identity");
556
+ stream.resume();
557
+ return record["child-pid"];
558
+ }
559
+ throw new Error("Bubblewrap exited before output handoff");
560
+ }
561
+ function outputReady(child) {
562
+ const stream = child.stdio[4];
563
+ return new Promise((resolve, reject) => {
564
+ stream.once("error", reject);
565
+ stream.once("end", () => reject(new Error("output launcher ended before handoff")));
566
+ stream.once("data", (bytes) => {
567
+ if (bytes.length !== 1 || bytes[0] !== 1)
568
+ reject(new Error("invalid output readiness"));
569
+ else
570
+ resolve();
571
+ });
572
+ });
573
+ }
464
574
  async function waitUntilEmpty(cgroup, timeoutMs) {
465
575
  const deadline = Date.now() + timeoutMs;
466
576
  while (Date.now() <= deadline) {
@@ -505,7 +615,11 @@ async function readCounters(path) {
505
615
  return result;
506
616
  }
507
617
  function emptyEvidence() {
508
- return Object.freeze({ cpuStat: Object.freeze({}), memoryEvents: Object.freeze({}), pidsEvents: Object.freeze({}) });
618
+ return Object.freeze({
619
+ cpuStat: Object.freeze({}),
620
+ memoryEvents: Object.freeze({}),
621
+ pidsEvents: Object.freeze({})
622
+ });
509
623
  }
510
624
  async function exists(path) {
511
625
  try {
@@ -606,10 +720,10 @@ async function main() {
606
720
  }
607
721
  if (mode === "--enter")
608
722
  return await enterMain(arguments_);
609
- if (mode === "--inner") {
723
+ if (mode === "--inner" || mode === "--inner-output") {
610
724
  if (arguments_[0] !== "--")
611
725
  throw new Error("invalid rootless inner separator");
612
- return await innerMain(arguments_.slice(1));
726
+ return await innerMain(arguments_.slice(1), mode === "--inner-output");
613
727
  }
614
728
  throw new Error("invalid rootless supervisor mode");
615
729
  }
@@ -1,15 +1,7 @@
1
1
  // @bun
2
2
  // src/internal/bun-native-preparation-worker.ts
3
3
  import { spawn } from "child_process";
4
- import {
5
- copyFile,
6
- lstat,
7
- mkdir,
8
- opendir,
9
- readFile,
10
- rm,
11
- writeFile
12
- } from "fs/promises";
4
+ import { copyFile, lstat, mkdir, opendir, readFile, rm, writeFile } from "fs/promises";
13
5
  import { dirname, join } from "path";
14
6
 
15
7
  // src/internal/bun-native-lock-policy.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jigging/jig",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.13",
4
4
  "description": "A local, secure host for admitted FLOW packages",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
@@ -66,16 +66,6 @@
66
66
  "LICENSE",
67
67
  "THIRD_PARTY_NOTICES"
68
68
  ],
69
- "scripts": {
70
- "verify:build-bun": "bun -e \"if (Bun.version !== '1.3.3' || Bun.revision !== '274e01c737e85f8142070a9745b43a2ba09fce4c') throw new Error('Jig must be built with exact Bun 1.3.3 revision 274e01c737e85f8142070a9745b43a2ba09fce4c')\"",
71
- "build": "bun run verify:build-bun && bun run clean && mkdir -p bin libexec/agent libexec/evaluator libexec/preparation && cp scripts/installed-jig.sh bin/jig && chmod 755 bin/jig && bun node_modules/typescript/bin/tsc -p tsconfig.json && bun build src/index.ts --target=bun --format=esm --outfile=dist/index.js && bun build src/installed-cli.ts --target=bun --format=esm --outfile=libexec/installed-cli.js && bun build src/internal/linux-rootless-supervisor.ts --target=bun --format=esm --outfile=libexec/linux-rootless-supervisor.js && bun build src/internal/project-evaluator-worker.ts --target=bun --format=esm --outfile=libexec/evaluator/project-evaluator-worker.js && bun build src/internal/openai-agent-worker.ts --target=bun --format=esm --outfile=libexec/agent/openai-agent-worker.js && bun build src/internal/codex-agent-launcher.ts --target=bun --format=esm --outfile=libexec/agent/codex-agent-launcher.js && cp node_modules/@agentclientprotocol/codex-acp/dist/index.js libexec/agent/codex-acp.js && cp node_modules/@agentclientprotocol/codex-acp/LICENSE libexec/agent/codex-acp.LICENSE && cp support/codex-requirements.toml libexec/agent/codex-requirements.toml && bun build src/internal/claude-agent-launcher.ts --target=bun --format=esm --outfile=libexec/agent/claude-agent-launcher.js && bun build node_modules/@agentclientprotocol/claude-agent-acp/dist/index.js --target=bun --format=esm --outfile=libexec/agent/claude-agent-acp.js && cp node_modules/@agentclientprotocol/claude-agent-acp/LICENSE libexec/agent/claude-agent-acp.LICENSE && cp node_modules/@anthropic-ai/claude-agent-sdk/LICENSE.md libexec/agent/claude-agent-sdk.LICENSE && bun build src/internal/pi-agent-launcher.ts --target=bun --format=esm --outfile=libexec/agent/pi-agent-launcher.js && bun build node_modules/pi-acp/dist/index.js --target=bun --format=esm --outfile=libexec/agent/pi-acp.js && cp node_modules/pi-acp/LICENSE libexec/agent/pi-acp.LICENSE && bun build src/internal/bun-native-preparation-worker.ts --target=bun --format=esm --outfile=libexec/preparation/bun-native-preparation-worker.js && bun build src/project/evaluator-sdk-entry.ts --target=bun --format=esm --outfile=libexec/evaluator/project-evaluator-sdk.bundle.js && cp ../../docs/jig/spec/machine/project-authoring-1.schema.json libexec/evaluator/project-authoring-1.schema.json && cp node_modules/openai/LICENSE libexec/agent/openai.LICENSE",
72
- "clean": "bun -e \"import { rm } from 'node:fs/promises'; for (const path of ['dist', 'bin', 'libexec']) await rm(path, { recursive: true, force: true })\"",
73
- "prepack": "bun run build",
74
- "test": "bun test",
75
- "test:package": "bun run build && bun test/package-smoke.ts",
76
- "test:linux-rootless:proof-host": "bun run build && JIG_LINUX_ROOTLESS_HOSTILE=1 bun test test/linux-rootless-delegation-hostile.test.ts --timeout 30000 && JIG_LINUX_ROOTLESS_HOSTILE=1 bun test test/linux-rootless-run.test.ts --timeout 30000 && JIG_LINUX_ROOTLESS_HOSTILE=1 bun test test/bun-native-preparation.test.ts --timeout 120000 && JIG_LINUX_ROOTLESS_HOSTILE=1 bun test test/private-foreground.test.ts --timeout 30000 && JIG_LINUX_ROOTLESS_HOSTILE=1 bun test test/root-agent-run-lifecycle.test.ts --timeout 180000",
77
- "check": "bun run build && bun test"
78
- },
79
69
  "dependencies": {
80
70
  "@oven/bun-linux-x64-baseline": "1.3.3"
81
71
  },
@@ -83,6 +73,7 @@
83
73
  "@agentclientprotocol/claude-agent-acp": "0.73.0",
84
74
  "@agentclientprotocol/codex-acp": "1.8.0",
85
75
  "@agentclientprotocol/sdk": "1.4.0",
76
+ "@anthropic-ai/claude-agent-sdk": "0.3.257",
86
77
  "@types/node": "24.13.3",
87
78
  "openai": "7.9.0",
88
79
  "pi-acp": "0.0.33",
@@ -90,4 +81,4 @@
90
81
  "yaml": "2.9.0",
91
82
  "zod": "4.3.6"
92
83
  }
93
- }
84
+ }