@uchibeke/gallivant 0.0.0-stage → 0.2.0

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.
Files changed (168) hide show
  1. package/CONTRIBUTING.md +26 -0
  2. package/LICENSE +204 -0
  3. package/README.md +201 -2
  4. package/SECURITY.md +19 -0
  5. package/bin/gallivant.mjs +257 -0
  6. package/bin/package-plugin.mjs +16 -0
  7. package/bin/release-live.mjs +29 -0
  8. package/bin/release-package.mjs +23 -0
  9. package/bin/session-runtime.mjs +13 -0
  10. package/bin/task-delivery-state.mjs +12 -0
  11. package/docs/README.md +70 -0
  12. package/docs/agent-setup.md +214 -0
  13. package/docs/app.md +111 -0
  14. package/docs/architecture.md +265 -0
  15. package/docs/chains.md +102 -0
  16. package/docs/codex-cloud.md +284 -0
  17. package/docs/commands.md +620 -0
  18. package/docs/config.md +430 -0
  19. package/docs/execution.md +796 -0
  20. package/docs/guardrails.md +311 -0
  21. package/docs/install.md +338 -0
  22. package/docs/launch.md +42 -0
  23. package/docs/learning.md +369 -0
  24. package/docs/network-recovery.md +101 -0
  25. package/docs/openai-agents.md +457 -0
  26. package/docs/prd.md +246 -0
  27. package/docs/preparation.md +110 -0
  28. package/docs/release-qualification.md +196 -0
  29. package/docs/releasing.md +140 -0
  30. package/docs/runtime-environment.md +113 -0
  31. package/docs/task-delivery.md +476 -0
  32. package/docs/tasks.md +739 -0
  33. package/lib/aport.mjs +95 -0
  34. package/lib/attribution.mjs +95 -0
  35. package/lib/block.mjs +89 -0
  36. package/lib/capabilities.mjs +3 -0
  37. package/lib/chains.mjs +222 -0
  38. package/lib/codex_cloud.mjs +160 -0
  39. package/lib/codex_cloud_config.mjs +20 -0
  40. package/lib/codex_cloud_input.mjs +49 -0
  41. package/lib/codex_cloud_result.mjs +74 -0
  42. package/lib/commands/doctor.mjs +457 -0
  43. package/lib/commands/init.mjs +34 -0
  44. package/lib/commands/learn.mjs +54 -0
  45. package/lib/commands/new.mjs +47 -0
  46. package/lib/commands/ready.mjs +45 -0
  47. package/lib/commands/register.mjs +195 -0
  48. package/lib/commands/resume.mjs +224 -0
  49. package/lib/commands/serve.mjs +349 -0
  50. package/lib/commands/start.mjs +247 -0
  51. package/lib/commands/start_services.mjs +22 -0
  52. package/lib/commands/status.mjs +150 -0
  53. package/lib/commands/task.mjs +73 -0
  54. package/lib/commands/up.mjs +228 -0
  55. package/lib/commands/why.mjs +143 -0
  56. package/lib/config.mjs +461 -0
  57. package/lib/context.mjs +100 -0
  58. package/lib/control.mjs +202 -0
  59. package/lib/control_contract.mjs +16 -0
  60. package/lib/coordinator.mjs +19 -0
  61. package/lib/credential.mjs +161 -0
  62. package/lib/daemon.mjs +156 -0
  63. package/lib/deliverable.mjs +357 -0
  64. package/lib/events.mjs +261 -0
  65. package/lib/executable.mjs +31 -0
  66. package/lib/execution.mjs +631 -0
  67. package/lib/execution_contract.mjs +15 -0
  68. package/lib/execution_input.mjs +4 -0
  69. package/lib/execution_io.mjs +94 -0
  70. package/lib/execution_journal.mjs +69 -0
  71. package/lib/execution_preparation.mjs +29 -0
  72. package/lib/execution_result.mjs +266 -0
  73. package/lib/fs.mjs +133 -0
  74. package/lib/gh.mjs +289 -0
  75. package/lib/git.mjs +434 -0
  76. package/lib/harness.mjs +521 -0
  77. package/lib/harness_log.mjs +432 -0
  78. package/lib/hash.mjs +16 -0
  79. package/lib/identity.mjs +98 -0
  80. package/lib/learning.mjs +654 -0
  81. package/lib/learning_contract.mjs +2 -0
  82. package/lib/learning_data.mjs +135 -0
  83. package/lib/learning_input.mjs +111 -0
  84. package/lib/learning_qualification.mjs +67 -0
  85. package/lib/learning_signer.mjs +151 -0
  86. package/lib/learning_store.mjs +88 -0
  87. package/lib/log.mjs +53 -0
  88. package/lib/network_diagnostics.mjs +64 -0
  89. package/lib/network_recovery.mjs +42 -0
  90. package/lib/notify.mjs +58 -0
  91. package/lib/openai_agents.mjs +373 -0
  92. package/lib/openai_agents_config.mjs +14 -0
  93. package/lib/openai_agents_http.mjs +87 -0
  94. package/lib/openai_agents_input.mjs +15 -0
  95. package/lib/openai_agents_sources.mjs +112 -0
  96. package/lib/owner_delivery.mjs +443 -0
  97. package/lib/plugin_package.mjs +340 -0
  98. package/lib/prd.mjs +635 -0
  99. package/lib/preparation_agents.mjs +33 -0
  100. package/lib/preparation_codex.mjs +212 -0
  101. package/lib/probe.mjs +126 -0
  102. package/lib/prompt.mjs +238 -0
  103. package/lib/provider_health.mjs +55 -0
  104. package/lib/public-format.mjs +31 -0
  105. package/lib/public_output.mjs +49 -0
  106. package/lib/release_package.mjs +154 -0
  107. package/lib/repository_files.mjs +86 -0
  108. package/lib/run.mjs +385 -0
  109. package/lib/run_log.mjs +94 -0
  110. package/lib/runtime-environment.mjs +87 -0
  111. package/lib/runtime_dependencies.mjs +15 -0
  112. package/lib/runtime_node.mjs +107 -0
  113. package/lib/runtime_state.mjs +174 -0
  114. package/lib/sources/index.mjs +27 -0
  115. package/lib/sources/prd.mjs +50 -0
  116. package/lib/start_contract.mjs +37 -0
  117. package/lib/state.mjs +246 -0
  118. package/lib/task_allowance.mjs +31 -0
  119. package/lib/task_completion.mjs +61 -0
  120. package/lib/task_contract.mjs +450 -0
  121. package/lib/task_delivery.mjs +219 -0
  122. package/lib/task_delivery_cancel.mjs +34 -0
  123. package/lib/task_delivery_contract.mjs +61 -0
  124. package/lib/task_delivery_mcp.mjs +40 -0
  125. package/lib/task_delivery_store.mjs +43 -0
  126. package/lib/task_delivery_view.mjs +88 -0
  127. package/lib/task_environment.mjs +281 -0
  128. package/lib/task_prepare.mjs +224 -0
  129. package/lib/task_store.mjs +38 -0
  130. package/lib/tasks.mjs +513 -0
  131. package/lib/tick.mjs +1635 -0
  132. package/lib/time.mjs +36 -0
  133. package/lib/tracker/assets.mjs +43 -0
  134. package/lib/tracker/pages.mjs +320 -0
  135. package/lib/tracker/runs.mjs +102 -0
  136. package/lib/tracker/stream.mjs +74 -0
  137. package/lib/tracker_process.mjs +107 -0
  138. package/lib/verification_sandbox.mjs +181 -0
  139. package/node_modules/smol-toml/LICENSE +24 -0
  140. package/node_modules/smol-toml/README.md +605 -0
  141. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  142. package/node_modules/smol-toml/dist/date.js +156 -0
  143. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  144. package/node_modules/smol-toml/dist/error.js +67 -0
  145. package/node_modules/smol-toml/dist/extract.js +289 -0
  146. package/node_modules/smol-toml/dist/index.cjs +770 -0
  147. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  148. package/node_modules/smol-toml/dist/index.js +34 -0
  149. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  150. package/node_modules/smol-toml/dist/parse.js +164 -0
  151. package/node_modules/smol-toml/dist/primitive.js +151 -0
  152. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  153. package/node_modules/smol-toml/dist/stringify.js +219 -0
  154. package/node_modules/smol-toml/dist/struct.js +156 -0
  155. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  156. package/node_modules/smol-toml/dist/util.js +57 -0
  157. package/node_modules/smol-toml/package.json +68 -0
  158. package/npm-shrinkwrap.json +1823 -0
  159. package/package.json +105 -4
  160. package/templates/claude-settings.json +18 -0
  161. package/templates/passport.gallivant.json +83 -0
  162. package/templates/plugin/bundled-runtime.json +26 -0
  163. package/templates/plugin/schemas/mcp.schema.json +120 -0
  164. package/templates/plugin/schemas/plugin.schema.json +65 -0
  165. package/templates/plugin/task-delivery.SKILL.md +60 -0
  166. package/templates/prd.md +32 -0
  167. package/templates/tracker/tracker.css +301 -0
  168. package/templates/tracker/tracker.js +721 -0
@@ -0,0 +1,107 @@
1
+ // Standalone tracker ownership. Embedded trackers keep their host's lifecycle.
2
+ import { spawn, execFile } from "node:child_process";
3
+ import { openSync, closeSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { acquireProcessLock, assertUnpinnedHome, matchesProcess, pidExists } from "./daemon.mjs";
7
+ import { codeStamp, readJson, writeJsonAtomic } from "./fs.mjs";
8
+ import { serve, DEFAULT_PORT } from "./commands/serve.mjs";
9
+
10
+ const recordPath = home => join(home, "tracker.json");
11
+ const lockPath = home => join(home, "tracker.lock");
12
+ export const trackerUrl = port => `http://127.0.0.1:${port}`;
13
+ export function trackerPort(value = DEFAULT_PORT) {
14
+ const port = Number(value);
15
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("--port must be an integer from 1 to 65535");
16
+ return port;
17
+ }
18
+
19
+ export function readTracker(home) {
20
+ const lock = readJson(lockPath(home));
21
+ if (!lock) return null;
22
+ const health = readJson(recordPath(home));
23
+ const record = { ...(health?.instance === lock.instance ? health : {}), ...lock };
24
+ return { ...record, alive: matchesProcess(record), exists: pidExists(record.pid) };
25
+ }
26
+
27
+ function usable(record, port) {
28
+ if (!record) return;
29
+ if (!record.alive && record.exists) throw new Error("Tracker process identity cannot be verified; inspect tracker.json before retrying");
30
+ if (!record.alive || !record.port) return;
31
+ if (record.port !== port) throw new Error(`Tracker already uses port ${record.port}; use --port ${record.port}, or gallivant down before changing it`);
32
+ if (record.code !== codeStamp()) throw new Error("Tracker is running different code; run gallivant down, then gallivant start");
33
+ }
34
+
35
+ export async function trackerForeground(ctx, { port = DEFAULT_PORT } = {}) {
36
+ port = trackerPort(port);
37
+ assertUnpinnedHome(ctx.home);
38
+ const lock = acquireProcessLock(lockPath(ctx.home), "tracker");
39
+ let server;
40
+ const cleanup = () => {
41
+ lock.release();
42
+ process.off("SIGINT", stop); process.off("SIGTERM", stop); process.off("exit", cleanup);
43
+ };
44
+ const stop = () => { server?.closeAllConnections(); server?.close(); cleanup(); };
45
+ try {
46
+ server = await serve(ctx, { port, instance: lock.owner.instance });
47
+ writeJsonAtomic(recordPath(ctx.home), { ...lock.owner, port, code: codeStamp() });
48
+ server.once("close", cleanup);
49
+ process.on("SIGINT", stop); process.on("SIGTERM", stop); process.on("exit", cleanup);
50
+ return server;
51
+ } catch (error) { server?.close(); cleanup(); throw error; }
52
+ }
53
+
54
+ // Never accept an arbitrary listener on this port as our tracker. Check both the
55
+ // process fingerprint and the instance published only after this home bound it.
56
+ async function ready(record, port) {
57
+ if (!record?.alive || record.port !== port) return false;
58
+ try {
59
+ const response = await fetch(`${trackerUrl(port)}/api/health`, { signal: AbortSignal.timeout(500), redirect: "error" });
60
+ if (!response.ok) return false;
61
+ return (await response.json()).instance === record.instance;
62
+ } catch { return false; }
63
+ }
64
+
65
+ export async function trackerInBackground(ctx, { port = DEFAULT_PORT, waitMs = 10_000, spawnFn = spawn } = {}) {
66
+ port = trackerPort(port);
67
+ assertUnpinnedHome(ctx.home);
68
+ const prior = readTracker(ctx.home);
69
+ usable(prior, port);
70
+ if (await ready(prior, port)) return prior;
71
+ let child, spawnError;
72
+ // A concurrent start may already own the lock but still be binding its port.
73
+ if (!prior?.alive) {
74
+ const out = openSync(join(ctx.home, "tracker.log"), "a", 0o600);
75
+ try {
76
+ child = spawnFn(process.execPath, [fileURLToPath(new URL("../bin/gallivant.mjs", import.meta.url)), "serve", "--foreground", "--no-daemon", "--no-open", "--port", String(port)], {
77
+ detached: true, stdio: ["ignore", out, out], env: { ...process.env, GALLIVANT_HOME: ctx.home },
78
+ });
79
+ child.on("error", error => { spawnError = error; }); child.unref();
80
+ } finally { closeSync(out); }
81
+ }
82
+ const deadline = Date.now() + waitMs;
83
+ while (Date.now() < deadline) {
84
+ assertUnpinnedHome(ctx.home);
85
+ const record = readTracker(ctx.home);
86
+ usable(record, port);
87
+ if (await ready(record, port)) return record;
88
+ if (spawnError || child?.exitCode != null && !record?.alive) throw new Error(`Tracker failed to start; see ${join(ctx.home, "tracker.log")}`);
89
+ await new Promise(resolve => setTimeout(resolve, 100));
90
+ }
91
+ child?.kill("SIGTERM");
92
+ throw new Error(`Tracker did not become ready; see ${join(ctx.home, "tracker.log")}`);
93
+ }
94
+
95
+ export function stopTracker(ctx) {
96
+ const record = readTracker(ctx.home);
97
+ if (!record?.alive) return { stopped: false, unverified: !!record?.exists, why: record?.exists ? "Tracker identity is unverified; no signal sent" : "No standalone tracker is running" };
98
+ process.kill(record.pid, "SIGTERM");
99
+ return { stopped: true, pid: record.pid };
100
+ }
101
+
102
+ export async function openTracker(port, { platform = process.platform, env = process.env, exec = execFile } = {}) {
103
+ if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return false;
104
+ const command = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null;
105
+ if (!command) return false;
106
+ return new Promise(resolve => exec(command, [trackerUrl(trackerPort(port))], { timeout: 5000, env }, error => resolve(!error)));
107
+ }
@@ -0,0 +1,181 @@
1
+ // Provider-controlled Verify commands run behind an OS boundary, including their
2
+ // descendants. An empty HOME is useful isolation, but is not that boundary.
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { existsSync, realpathSync, statSync, mkdtempSync, writeFileSync, rmSync, mkdirSync, readlinkSync } from "node:fs";
6
+ import { createHash } from "node:crypto";
7
+ import { deflateSync } from "node:zlib";
8
+ import { execFileAsync, MAX_DIFF_BYTES } from "./gh.mjs";
9
+ import { executionError } from "./execution_io.mjs";
10
+
11
+ const unavailable = () => executionError("EXECUTION_VERIFICATION_SANDBOX", "OS verification sandbox unavailable or enforcement probe failed; use macOS sandbox-exec with its installed dyld-support.sb and Command Line Tools, or Linux /usr/bin/bwrap with working user/PID/network namespaces; /usr/bin/git and a dedicated Node installation with npm are required. Preserve saved results and repair host isolation before explicitly resuming; verification never falls back to an unsandboxed shell");
12
+ export { unavailable as verificationSandboxUnavailable };
13
+ const inside = (path, root) => path === root || path.startsWith(root + "/");
14
+
15
+ export function validVerificationSandbox(value) {
16
+ return value?.version === 1 && ["sandbox-exec", "bubblewrap"].includes(value.driver) && value.filesystem === "checkout-and-runtime" && value.network === "disabled" && value.probe === "passed";
17
+ }
18
+
19
+ // Bind the CLI module and create its normal entry point without exposing the
20
+ // entire Node bin directory. Binding a symlink as a file would lose npm's module
21
+ // location and break its relative imports. System bin mounts already own theirs.
22
+ export function verificationRuntimeMounts({ runtime, extra, npmLauncher, npmCli }) {
23
+ const roots = [...new Set([...runtime, ...extra])], args = [];
24
+ for (const path of roots) args.push("--ro-bind", path, path);
25
+ if (npmCli && !roots.some(root => inside(npmLauncher, root))) args.push("--symlink", npmCli, npmLauncher);
26
+ return args;
27
+ }
28
+
29
+ // Runtime grants are fixed system tool/library directories, the running Node
30
+ // binary and its npm modules, never the operator's PATH, HOME or config roots.
31
+ export function verificationSandboxSpec({ worktree, home, env, gitView = null, platform = process.platform, node = process.execPath }) {
32
+ worktree = realpathSync(worktree); home = realpathSync(home); node = realpathSync(node);
33
+ if (inside(home, worktree) || inside(worktree, home)) throw unavailable();
34
+ const nodeBin = dirname(node), npm = join(dirname(nodeBin), "lib/node_modules/npm");
35
+ const npmCli = existsSync(npm) ? realpathSync(join(npm, "bin/npm-cli.js")) : null;
36
+ env.PATH = [...new Set([nodeBin, "/usr/bin", "/bin"])].join(":");
37
+ const runtime = ["/bin", "/usr/bin", "/usr/lib", ...(platform === "darwin"
38
+ ? ["/System/Library", "/Library/Apple/usr/lib", "/Library/Developer/CommandLineTools", "/usr/share/zoneinfo", "/usr/share/nls"]
39
+ : ["/lib", "/lib64", "/usr/share/zoneinfo"])].filter(existsSync);
40
+ // A Node installed directly in /bin needs no extra installation-wide grant.
41
+ const extra = [node, ...(existsSync(npm) ? [realpathSync(npm)] : [])];
42
+ if ([...runtime, ...extra].some(root => inside(worktree, root) || inside(home, root) || inside(realpathSync(homedir()), root))) throw unavailable();
43
+ const git = join(worktree, ".git");
44
+ if (platform === "darwin") {
45
+ const loader = "/System/Library/Sandbox/Profiles/dyld-support.sb";
46
+ if (!existsSync("/usr/bin/sandbox-exec") || !existsSync(loader)) throw unavailable();
47
+ const quote = value => JSON.stringify(value);
48
+ const read = [...runtime.map(path => `(subpath ${quote(realpathSync(path))})`), ...extra.map(path => `(${statSync(path).isDirectory() ? "subpath" : "literal"} ${quote(path)})`)];
49
+ const profile = `(version 1)
50
+ (deny default)
51
+ (import ${quote(loader)})
52
+ (allow syscall* system-fcntl mach-bootstrap)
53
+ (allow process-exec process-fork)
54
+ (allow signal (target self))
55
+ (allow file-read-metadata file-test-existence)
56
+ ; libc page-size queries also use the compatibility name. Blocking it can
57
+ ; make Node's allocator receive -1 and abort before the verification probe.
58
+ (allow sysctl-read (sysctl-name "hw.ncpu" "hw.logicalcpu" "hw.logicalcpu_max" "hw.physicalcpu" "hw.physicalcpu_max" "hw.memsize" "hw.pagesize" "hw.pagesize_compat" "hw.cachelinesize" "hw.machine" "hw.optional.arm64" "kern.ostype" "kern.hostname" "kern.osrelease" "kern.osversion" "kern.osproductversion" "kern.version" "kern.argmax" "kern.maxfiles" "kern.maxfilesperproc"))
59
+ (allow file-read* file-map-executable ${read.join(" ")})
60
+ (allow file-read* file-write* file-map-executable (subpath ${quote(worktree)}) (subpath ${quote(home)}))
61
+ (deny file-read* file-write* file-map-executable (subpath ${quote(git)}))
62
+ ${gitView ? `(deny file-write* (subpath ${quote(gitView)}))` : ""}
63
+ (allow file-read* (literal "/dev/random") (literal "/dev/urandom"))
64
+ (allow file-read* file-write-data (literal "/dev/null") (literal "/dev/zero") (subpath "/dev/fd"))
65
+ `;
66
+ // Compile immutable coordinator-owned bytes for each launch. A policy file
67
+ // in the writable profile could be replaced by an earlier Verify command.
68
+ return { cmd: "/usr/bin/sandbox-exec", args: ["-p", profile], driver: "sandbox-exec", node, env };
69
+ }
70
+ if (platform === "linux") {
71
+ if (!existsSync("/usr/bin/bwrap")) throw unavailable();
72
+ // A privileged Linux host can create the required namespaces directly.
73
+ // Nesting a user namespace on Cloudflare prevents mounting its private proc.
74
+ // Rootless hosts still need the user namespace. Both paths retain private
75
+ // PID/network/IPC/UTS and mount namespaces, drop every capability, and must
76
+ // pass the exact filesystem/network/privilege probe before repository code.
77
+ const namespaces = process.getuid?.() === 0
78
+ ? ["--unshare-pid", "--unshare-net", "--unshare-ipc", "--unshare-uts"]
79
+ : ["--unshare-all"];
80
+ const args = [...namespaces, "--die-with-parent", "--new-session", "--cap-drop", "ALL", "--proc", "/proc", "--dev", "/dev"];
81
+ args.push(...verificationRuntimeMounts({ runtime, extra, npmLauncher: join(nodeBin, "npm"), npmCli }));
82
+ args.push("--bind", worktree, worktree, "--bind", home, home);
83
+ if (gitView) args.push("--ro-bind", gitView, gitView);
84
+ if (existsSync(git)) {
85
+ const empty = join(home, "hidden-git");
86
+ if (statSync(git).isDirectory()) { mkdirSync(empty, { recursive: true }); args.push("--ro-bind", empty, git); }
87
+ else args.push("--ro-bind", "/dev/null", git);
88
+ }
89
+ args.push("--chdir", worktree, "--");
90
+ return { cmd: "/usr/bin/bwrap", args, driver: "bubblewrap", node, env };
91
+ }
92
+ throw unavailable();
93
+ }
94
+
95
+ // A disposable read-only repository contains only the current commit and its
96
+ // tree/blob objects, plus an index reconstructed from that tree. Never copy the
97
+ // coordinator's config, hooks, remotes, reflogs, alternate stores or history.
98
+ async function verificationGitView({ worktree, home, env, signal }, exec) {
99
+ signal = AbortSignal.any([...(signal ? [signal] : []), AbortSignal.timeout(20_000)]);
100
+ const options = { cwd: worktree, env: { ...env, GIT_NO_REPLACE_OBJECTS: "1" }, signal, timeout: 20_000, maxBuffer: MAX_DIFF_BYTES };
101
+ const git = args => exec("/usr/bin/git", args, options);
102
+ // Readiness precedes cloning. Use a trusted empty commit so that the same
103
+ // native Git read/denied-write probe runs even before a real checkout exists.
104
+ const empty = new Map(), object = (type, data) => { const oid = createHash("sha1").update(Buffer.concat([Buffer.from(`${type} ${data.length}\0`), data])).digest("hex"); empty.set(oid, { type, data }); return oid; };
105
+ let head;
106
+ if (existsSync(join(worktree, ".git"))) head = (await git(["rev-parse", "HEAD"])).stdout.trim();
107
+ else { const tree = object("tree", Buffer.alloc(0)); head = object("commit", Buffer.from(`tree ${tree}\nauthor Readiness <readiness@localhost> 0 +0000\ncommitter Readiness <readiness@localhost> 0 +0000\n\nReadiness\n`)); }
108
+ if (!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(head)) throw unavailable();
109
+ const objects = empty.size ? [...empty.keys()] : (await git(["rev-list", "--objects", "--no-object-names", "--no-walk", head])).stdout.trim().split("\n");
110
+ if (objects.length > 10_000) throw unavailable();
111
+ const view = join(realpathSync(home), "verification-git");
112
+ mkdirSync(join(view, "objects"), { recursive: true }); mkdirSync(join(view, "refs"));
113
+ writeFileSync(join(view, "HEAD"), head + "\n"); writeFileSync(join(view, "shallow"), head + "\n");
114
+ writeFileSync(join(view, "config"), `[core]\nrepositoryformatversion = ${head.length === 64 ? 1 : 0}\nbare = false\nfilemode = true\n${head.length === 64 ? "[extensions]\nobjectformat = sha256\n" : ""}`);
115
+ let size = 0;
116
+ for (const oid of new Set(objects)) {
117
+ if (oid.length !== head.length || !/^[a-f0-9]+$/.test(oid)) throw unavailable();
118
+ const type = empty.get(oid)?.type ?? (await git(["cat-file", "-t", oid])).stdout.trim();
119
+ if (!["commit", "tree", "blob"].includes(type)) throw unavailable();
120
+ const stdout = empty.get(oid)?.data ?? (await exec("/usr/bin/git", ["cat-file", type, oid], { ...options, encoding: "buffer" })).stdout;
121
+ size += stdout.length; if (size > MAX_DIFF_BYTES) throw unavailable();
122
+ const raw = Buffer.concat([Buffer.from(`${type} ${stdout.length}\0`), stdout]);
123
+ if (createHash(head.length === 64 ? "sha256" : "sha1").update(raw).digest("hex") !== oid) throw unavailable();
124
+ mkdirSync(join(view, "objects", oid.slice(0, 2)), { recursive: true });
125
+ writeFileSync(join(view, "objects", oid.slice(0, 2), oid.slice(2)), deflateSync(raw));
126
+ }
127
+ const isolated = { ...env, GIT_DIR: view, GIT_WORK_TREE: realpathSync(worktree), GIT_OPTIONAL_LOCKS: "0", GIT_NO_REPLACE_OBJECTS: "1" };
128
+ await exec("/usr/bin/git", ["read-tree", "HEAD"], { ...options, env: isolated });
129
+ Object.assign(env, isolated);
130
+ return view;
131
+ }
132
+
133
+ // Probe the exact boundary before any repository code runs. The marker remains
134
+ // outside both grants. macOS must deny the TCP syscall; Linux must demonstrate a
135
+ // different network namespace as well as failure to reach the host loopback.
136
+ const PROBE = `const fs=require('node:fs'),net=require('node:net'),cp=require('node:child_process');
137
+ require('node:os').type();
138
+ const [outside,home,namespace]=process.argv.slice(1); const denied=e=>['EACCES','EPERM'].includes(e.code)||(namespace&&e.code==='ENOENT');
139
+ for(const action of [()=>fs.readFileSync(outside),()=>fs.writeFileSync(outside,'changed')]) {
140
+ try { action(); process.exit(41); } catch(e) { if(!denied(e)) process.exit(42); }
141
+ }
142
+ fs.writeFileSync(home+'/probe-ok','ok'); fs.unlinkSync(home+'/probe-ok');
143
+ cp.execFileSync('/usr/bin/git',['diff','--check']); cp.execFileSync('/usr/bin/git',['rev-parse','--verify','HEAD']);
144
+ const npm=require('node:path').join(require('node:path').dirname(process.execPath),'npm');
145
+ fs.accessSync(npm,fs.constants.R_OK|fs.constants.X_OK); fs.readFileSync(npm);
146
+ try { fs.writeFileSync(process.env.GIT_DIR+'/index','changed'); process.exit(47); } catch(e) { if(!denied(e)&&e.code!=='EROFS') process.exit(48); }
147
+ if(namespace) {
148
+ if(fs.readlinkSync('/proc/self/ns/net')===namespace) process.exit(46);
149
+ // CapBnd is a ceiling, not held authority. Require zero held/inheritable
150
+ // capabilities and no-new-privileges, including after a child exec.
151
+ const statuses=[fs.readFileSync('/proc/self/status','utf8'),cp.execFileSync(process.execPath,['-e',"process.stdout.write(require('node:fs').readFileSync('/proc/self/status','utf8'))"],{encoding:'utf8'})];
152
+ for(const status of statuses) {
153
+ for(const field of ['CapInh','CapPrm','CapEff','CapAmb']) {
154
+ const value=status.split(String.fromCharCode(10)).find(line=>line.startsWith(field+':'))?.slice(field.length+1).trim();
155
+ if(!value||!/^[0-9a-f]+$/.test(value)||BigInt('0x'+value)!==0n) process.exit(49);
156
+ }
157
+ if(!/^NoNewPrivs:\\s*1$/m.test(status)) process.exit(50);
158
+ }
159
+ }
160
+ const socket=net.connect({host:'127.0.0.1',port:9}); socket.on('connect',()=>process.exit(43));
161
+ socket.on('error',e=>{if(!denied(e)&&!(namespace&&['ENETUNREACH','EHOSTUNREACH','ECONNREFUSED'].includes(e.code))) process.exit(44); process.stdout.write('sandbox-enforced');});
162
+ setTimeout(()=>process.exit(45),2000).unref();`;
163
+
164
+ export async function prepareVerificationSandbox(options, exec = execFileAsync) {
165
+ let probeRoot;
166
+ try {
167
+ // Validate containment, fixed runtime grants and the OS driver before
168
+ // constructing any temporary repository metadata.
169
+ verificationSandboxSpec(options);
170
+ const gitView = await verificationGitView(options, exec);
171
+ const spec = verificationSandboxSpec({ ...options, gitView });
172
+ probeRoot = mkdtempSync(join(dirname(realpathSync(options.home)), "verification-probe-"));
173
+ const outside = join(probeRoot, "private-marker"); writeFileSync(outside, "private-sandbox-probe", { mode: 0o600 });
174
+ const namespace = spec.driver === "bubblewrap" ? readlinkSync("/proc/self/ns/net") : "";
175
+ const observed = await exec(spec.cmd, [...spec.args, spec.node, "-e", PROBE, outside, realpathSync(options.home), namespace],
176
+ { cwd: options.worktree, env: spec.env, signal: options.signal, timeout: 5000, maxBuffer: 4096 });
177
+ if (observed.stdout !== "sandbox-enforced") throw unavailable();
178
+ return { ...spec, evidence: { version: 1, driver: spec.driver, filesystem: "checkout-and-runtime", network: "disabled", probe: "passed" } };
179
+ } catch { throw unavailable(); }
180
+ finally { if (probeRoot) rmSync(probeRoot, { recursive: true, force: true }); }
181
+ }
@@ -0,0 +1,24 @@
1
+ Copyright (c) Squirrel Chat et al., All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this
7
+ list of conditions and the following disclaimer.
8
+ 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ 3. Neither the name of the copyright holder nor the names of its contributors
12
+ may be used to endorse or promote products derived from this software without
13
+ specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.