@snappedly-tools/shipyard 0.4.4 → 0.5.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.
@@ -0,0 +1,136 @@
1
+ import { createInterface } from 'readline';
2
+
3
+ // src/boundedTail.ts
4
+ var MAX_TAIL_CHARS = 64 * 1024;
5
+ var OutputByteCounter = class {
6
+ constructor(maxBytes) {
7
+ this.maxBytes = maxBytes;
8
+ }
9
+ totalBytes = 0;
10
+ limitExceeded = false;
11
+ add(chunk) {
12
+ this.totalBytes += typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength;
13
+ if (this.totalBytes > this.maxBytes) this.limitExceeded = true;
14
+ }
15
+ get exceeded() {
16
+ return this.limitExceeded;
17
+ }
18
+ };
19
+ var BoundedTail = class {
20
+ items = [];
21
+ totalChars = 0;
22
+ maxChars;
23
+ separator;
24
+ /**
25
+ * @param maxChars Maximum length of the joined tail. Defaults to {@link MAX_TAIL_CHARS}.
26
+ * @param separator String placed between items by {@link toString}. Must match
27
+ * how the caller would otherwise have joined the accumulated chunks (e.g.
28
+ * `"\n"` for line streams, `""` for raw chunk streams).
29
+ */
30
+ constructor(maxChars = MAX_TAIL_CHARS, separator = "") {
31
+ this.maxChars = maxChars;
32
+ this.separator = separator;
33
+ }
34
+ /** Append one item to the tail, evicting oldest items to stay within budget. */
35
+ push(item) {
36
+ const bounded = item.length > this.maxChars ? item.slice(item.length - this.maxChars) : item;
37
+ this.totalChars += bounded.length + (this.items.length > 0 ? this.separator.length : 0);
38
+ this.items.push(bounded);
39
+ while (this.totalChars > this.maxChars && this.items.length > 1) {
40
+ const dropped = this.items.shift();
41
+ this.totalChars -= dropped.length + this.separator.length;
42
+ }
43
+ }
44
+ /** Join the retained tail into a single string (length ≤ `maxChars`). */
45
+ toString() {
46
+ return this.items.join(this.separator);
47
+ }
48
+ };
49
+ var StreamedProcessOutput = class {
50
+ maxOutputBytes;
51
+ byteCounter;
52
+ stdoutTail;
53
+ stderrTail;
54
+ onLine;
55
+ outputLimitError;
56
+ constructor(options) {
57
+ this.maxOutputBytes = options.maxOutputBytes;
58
+ this.byteCounter = options.maxOutputBytes === void 0 ? void 0 : new OutputByteCounter(options.maxOutputBytes);
59
+ const tailChars = options.maxOutputBytes ?? options.maxOutputTailChars ?? MAX_TAIL_CHARS;
60
+ this.stdoutTail = new BoundedTail(tailChars, "\n");
61
+ this.stderrTail = new BoundedTail(tailChars, "");
62
+ this.onLine = options.onLine ?? (() => {
63
+ });
64
+ }
65
+ checkLimit(chunk) {
66
+ if (this.byteCounter === void 0 || this.outputLimitError !== void 0) {
67
+ return this.outputLimitError;
68
+ }
69
+ this.byteCounter.add(chunk);
70
+ if (this.byteCounter.exceeded) {
71
+ this.outputLimitError = new Error(
72
+ `Sandbox command output exceeded ${this.maxOutputBytes} bytes`
73
+ );
74
+ }
75
+ return this.outputLimitError;
76
+ }
77
+ addStdoutLine(line) {
78
+ this.stdoutTail.push(line);
79
+ this.onLine(line);
80
+ }
81
+ addStderr(chunk) {
82
+ this.stderrTail.push(chunk);
83
+ }
84
+ get error() {
85
+ return this.outputLimitError;
86
+ }
87
+ result(exitCode) {
88
+ return {
89
+ stdout: this.stdoutTail.toString(),
90
+ stderr: this.stderrTail.toString(),
91
+ exitCode
92
+ };
93
+ }
94
+ };
95
+ var collectProcessOutput = (process, options, resolve, reject) => {
96
+ if (options.onLine || options.maxOutputBytes !== void 0) {
97
+ const output = new StreamedProcessOutput(options);
98
+ const checkLimit = (chunk) => {
99
+ if (output.checkLimit(chunk) !== void 0) process.kill();
100
+ };
101
+ process.stdout.on("data", checkLimit);
102
+ process.stderr.on("data", checkLimit);
103
+ const lines = createInterface({ input: process.stdout });
104
+ lines.on("line", (line) => output.addStdoutLine(line));
105
+ process.stderr.on("data", (chunk) => {
106
+ output.addStderr(chunk.toString());
107
+ });
108
+ process.onClose((code) => {
109
+ if (output.error !== void 0) {
110
+ reject(output.error);
111
+ return;
112
+ }
113
+ resolve(output.result(code ?? 0));
114
+ });
115
+ return;
116
+ }
117
+ const stdoutChunks = [];
118
+ const stderrChunks = [];
119
+ process.stdout.on("data", (chunk) => {
120
+ stdoutChunks.push(chunk.toString());
121
+ });
122
+ process.stderr.on("data", (chunk) => {
123
+ stderrChunks.push(chunk.toString());
124
+ });
125
+ process.onClose((code) => {
126
+ resolve({
127
+ stdout: stdoutChunks.join(""),
128
+ stderr: stderrChunks.join(""),
129
+ exitCode: code ?? 0
130
+ });
131
+ });
132
+ };
133
+
134
+ export { MAX_TAIL_CHARS, StreamedProcessOutput, collectProcessOutput };
135
+ //# sourceMappingURL=chunk-ACD46ZM4.js.map
136
+ //# sourceMappingURL=chunk-ACD46ZM4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/boundedTail.ts","../src/processOutput.ts"],"names":[],"mappings":"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","file":"chunk-ACD46ZM4.js","sourcesContent":["/**\n * A bounded, rolling tail of streamed output — a pure, provider-agnostic\n * utility shared by every sandbox provider that streams `exec` output.\n *\n * When a provider streams output line-by-line, it accumulates the stream only\n * to build the returned `ExecResult.stdout`/`stderr`. Consumers read just the\n * tail of that value (e.g. the last lines of an error, or a fallback for the\n * agent's final result), so retaining the whole stream is unnecessary — and,\n * once the accumulated string passes V8's ~512MB max string length, fatal: a\n * naive `chunks.join()` throws `RangeError: Invalid string length`, which on a\n * long agent run crashes the whole orchestration.\n */\n\n/**\n * Default maximum number of characters retained in a bounded output tail.\n *\n * 64KiB sits comfortably above any agent completion signal or structured-output\n * payload while staying far below V8's max string length.\n */\nexport const MAX_TAIL_CHARS = 64 * 1024;\n\n/**\n * Counts streamed output without retaining it. Providers use this when a\n * caller needs a hard byte ceiling for a one-shot command (for example prompt\n * expansion), so an untrusted command cannot exhaust memory before its output\n * is inspected.\n */\nexport class OutputByteCounter {\n private totalBytes = 0;\n private limitExceeded = false;\n\n constructor(private readonly maxBytes: number) {}\n\n add(chunk: string | Uint8Array): void {\n this.totalBytes +=\n typeof chunk === \"string\" ? Buffer.byteLength(chunk) : chunk.byteLength;\n if (this.totalBytes > this.maxBytes) this.limitExceeded = true;\n }\n\n get exceeded(): boolean {\n return this.limitExceeded;\n }\n}\n\n/**\n * A fixed-size rolling tail of strings, bounded by total character length.\n *\n * `push` appends to the tail; once the joined length would exceed `maxChars`,\n * the oldest items are dropped from the front. A single item longer than\n * `maxChars` is truncated to its own tail, so a newline-free blob can't\n * overflow on one push. `toString` joins the retained items, and its length is\n * always at most `maxChars`.\n *\n * The running length counter is encapsulated so callers can't desync it.\n */\nexport class BoundedTail {\n private readonly items: string[] = [];\n private totalChars = 0;\n private readonly maxChars: number;\n private readonly separator: string;\n\n /**\n * @param maxChars Maximum length of the joined tail. Defaults to {@link MAX_TAIL_CHARS}.\n * @param separator String placed between items by {@link toString}. Must match\n * how the caller would otherwise have joined the accumulated chunks (e.g.\n * `\"\\n\"` for line streams, `\"\"` for raw chunk streams).\n */\n constructor(maxChars: number = MAX_TAIL_CHARS, separator = \"\") {\n this.maxChars = maxChars;\n this.separator = separator;\n }\n\n /** Append one item to the tail, evicting oldest items to stay within budget. */\n push(item: string): void {\n const bounded =\n item.length > this.maxChars\n ? item.slice(item.length - this.maxChars)\n : item;\n this.totalChars +=\n bounded.length + (this.items.length > 0 ? this.separator.length : 0);\n this.items.push(bounded);\n while (this.totalChars > this.maxChars && this.items.length > 1) {\n const dropped = this.items.shift()!;\n this.totalChars -= dropped.length + this.separator.length;\n }\n }\n\n /** Join the retained tail into a single string (length ≤ `maxChars`). */\n toString(): string {\n return this.items.join(this.separator);\n }\n}\n","import type { Readable } from \"node:stream\";\nimport { createInterface } from \"node:readline\";\nimport {\n BoundedTail,\n MAX_TAIL_CHARS,\n OutputByteCounter,\n} from \"./boundedTail.js\";\n\nexport interface ProcessOutputResult {\n readonly stdout: string;\n readonly stderr: string;\n readonly exitCode: number;\n}\n\nexport interface ProcessOutputOptions {\n readonly onLine?: (line: string) => void;\n readonly maxOutputBytes?: number;\n readonly maxOutputTailChars?: number;\n}\n\nexport class StreamedProcessOutput {\n private readonly maxOutputBytes: number | undefined;\n private readonly byteCounter: OutputByteCounter | undefined;\n private readonly stdoutTail: BoundedTail;\n private readonly stderrTail: BoundedTail;\n private readonly onLine: (line: string) => void;\n private outputLimitError: Error | undefined;\n\n constructor(options: ProcessOutputOptions) {\n this.maxOutputBytes = options.maxOutputBytes;\n this.byteCounter =\n options.maxOutputBytes === undefined\n ? undefined\n : new OutputByteCounter(options.maxOutputBytes);\n const tailChars =\n options.maxOutputBytes ?? options.maxOutputTailChars ?? MAX_TAIL_CHARS;\n this.stdoutTail = new BoundedTail(tailChars, \"\\n\");\n this.stderrTail = new BoundedTail(tailChars, \"\");\n this.onLine = options.onLine ?? (() => {});\n }\n\n checkLimit(chunk: string | Uint8Array): Error | undefined {\n if (this.byteCounter === undefined || this.outputLimitError !== undefined) {\n return this.outputLimitError;\n }\n this.byteCounter.add(chunk);\n if (this.byteCounter.exceeded) {\n this.outputLimitError = new Error(\n `Sandbox command output exceeded ${this.maxOutputBytes} bytes`,\n );\n }\n return this.outputLimitError;\n }\n\n addStdoutLine(line: string): void {\n this.stdoutTail.push(line);\n this.onLine(line);\n }\n\n addStderr(chunk: string): void {\n this.stderrTail.push(chunk);\n }\n\n get error(): Error | undefined {\n return this.outputLimitError;\n }\n\n result(exitCode: number): ProcessOutputResult {\n return {\n stdout: this.stdoutTail.toString(),\n stderr: this.stderrTail.toString(),\n exitCode,\n };\n }\n}\n\nexport const collectProcessOutput = (\n process: {\n readonly stdout: Readable;\n readonly stderr: Readable;\n readonly kill: () => void;\n readonly onClose: (listener: (code: number | null) => void) => void;\n },\n options: ProcessOutputOptions,\n resolve: (result: ProcessOutputResult) => void,\n reject: (error: Error) => void,\n): void => {\n if (options.onLine || options.maxOutputBytes !== undefined) {\n const output = new StreamedProcessOutput(options);\n const checkLimit = (chunk: Buffer): void => {\n if (output.checkLimit(chunk) !== undefined) process.kill();\n };\n process.stdout.on(\"data\", checkLimit);\n process.stderr.on(\"data\", checkLimit);\n const lines = createInterface({ input: process.stdout });\n lines.on(\"line\", (line) => output.addStdoutLine(line));\n process.stderr.on(\"data\", (chunk: Buffer) => {\n output.addStderr(chunk.toString());\n });\n process.onClose((code) => {\n if (output.error !== undefined) {\n reject(output.error);\n return;\n }\n resolve(output.result(code ?? 0));\n });\n return;\n }\n\n const stdoutChunks: string[] = [];\n const stderrChunks: string[] = [];\n process.stdout.on(\"data\", (chunk: Buffer) => {\n stdoutChunks.push(chunk.toString());\n });\n process.stderr.on(\"data\", (chunk: Buffer) => {\n stderrChunks.push(chunk.toString());\n });\n process.onClose((code) => {\n resolve({\n stdout: stdoutChunks.join(\"\"),\n stderr: stderrChunks.join(\"\"),\n exitCode: code ?? 0,\n });\n });\n};\n"]}
@@ -1,5 +1,5 @@
1
1
  import { toSessionTransferHandle, shellQuote } from './chunk-KMGNFXKN.js';
2
- import { existsSync, constants, statSync } from 'fs';
2
+ import { existsSync, constants, lstatSync, realpathSync, statSync } from 'fs';
3
3
  import { mkdtemp, writeFile, chmod, mkdir, stat, copyFile, rm, readdir, lstat, readFile, open } from 'fs/promises';
4
4
  import * as clack from '@clack/prompts';
5
5
  import { FileSystem } from '@effect/platform';
@@ -29,6 +29,15 @@ var SilentDisplay = {
29
29
  ]),
30
30
  () => effect
31
31
  ),
32
+ progress: (title, effect) => Effect.gen(function* () {
33
+ const updates = [];
34
+ const result = yield* effect((update) => updates.push(update));
35
+ yield* Ref.update(ref, (entries) => [
36
+ ...entries,
37
+ { _tag: "progress", title, updates: [...updates] }
38
+ ]);
39
+ return result;
40
+ }),
32
41
  summary: (title, rows) => Ref.update(ref, (entries) => [
33
42
  ...entries,
34
43
  { _tag: "summary", title, rows }
@@ -127,6 +136,20 @@ var FileDisplay = {
127
136
  yield* appendToLog(`${message} done (${elapsed}s)`);
128
137
  return result;
129
138
  }),
139
+ progress: (title, effect) => Effect.gen(function* () {
140
+ yield* appendToLog(`${title}...`);
141
+ const start = Date.now();
142
+ const updates = [];
143
+ const result = yield* effect((update) => updates.push(update));
144
+ for (const update of updates) {
145
+ yield* appendToLog(
146
+ ` ${update.message} (${update.current}/${update.total})`
147
+ );
148
+ }
149
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
150
+ yield* appendToLog(`${title} done (${elapsed}s)`);
151
+ return result;
152
+ }),
130
153
  summary: (title, rows) => {
131
154
  const lines = Object.entries(rows).map(([key, value]) => ` ${key}: ${value}`).join("\n");
132
155
  return appendToLog(`${title}
@@ -186,6 +209,34 @@ var ClackDisplay = {
186
209
  }
187
210
  })
188
211
  ),
212
+ progress: (title, effect) => Effect.acquireUseRelease(
213
+ Effect.sync(() => {
214
+ const progress2 = clack.progress({ max: 100 });
215
+ let percent = 0;
216
+ progress2.start(title);
217
+ const report = (update) => {
218
+ const total = Math.max(1, update.total);
219
+ const current = Math.min(total, Math.max(0, update.current));
220
+ const nextPercent = Math.round(current / total * 100);
221
+ const message = `${update.message} (${current}/${total})`;
222
+ if (nextPercent > percent) {
223
+ progress2.advance(nextPercent - percent, message);
224
+ } else {
225
+ progress2.message(message);
226
+ }
227
+ percent = nextPercent;
228
+ };
229
+ return { progress: progress2, report };
230
+ }),
231
+ ({ report }) => effect(report),
232
+ ({ progress: progress2 }, exit) => Effect.sync(() => {
233
+ if (exit._tag === "Success") {
234
+ progress2.stop(`${title} complete`);
235
+ } else {
236
+ progress2.error(`${title} failed`);
237
+ }
238
+ })
239
+ ),
189
240
  summary: (title, rows) => Effect.sync(() => {
190
241
  const lines = Object.entries(rows).map(([key, value]) => terminalStyle.summaryRow(key, value)).join("\n");
191
242
  clack.note(lines, terminalStyle.summaryTitle(title));
@@ -306,11 +357,14 @@ var SessionCaptureError = class extends Data.TaggedError(
306
357
  var PRODUCT_NAME = "Shipyard";
307
358
  var CLI_NAME = "shipyard";
308
359
  var RUNTIME_NAMESPACE = "shipyard";
360
+ var ACTIVATION_LABEL = "shipyard";
309
361
  var CONFIG_DIR = ".shipyard";
310
362
  var LOGS_DIR = "logs";
311
363
  var WORKTREES_DIR = "worktrees";
312
364
  var PATCHES_DIR = "patches";
313
365
  var LOCKS_DIR = "locks";
366
+ var RUNNER_DIR = "runner";
367
+ var RUNNER_SANDBOX_MASK_DIR = "runner-sandbox-mask";
314
368
  var PARENT_GIT_SANDBOX_DIR = "/.shipyard-parent-git";
315
369
  var SYNC_BASE_REF = "refs/shipyard/sync-base";
316
370
  var CONTAINER_NAME_PREFIX = "shipyard-";
@@ -815,6 +869,79 @@ var pruneStale = (repoDir) => Effect.gen(function* () {
815
869
  })
816
870
  )
817
871
  );
872
+ var PROTECTED_RUNNER_PATH = `${CONFIG_DIR}/${RUNNER_DIR}`;
873
+ var pathKey = (value) => value.normalize("NFC").toLocaleLowerCase("en-US");
874
+ var SAFE_RUNNER_ENV_KEYS = /* @__PURE__ */ new Set([
875
+ "HOME",
876
+ "LANG",
877
+ "LC_ALL",
878
+ "LOGNAME",
879
+ "PATH",
880
+ "SHELL",
881
+ "TEMP",
882
+ "TERM",
883
+ "TMP",
884
+ "TMPDIR",
885
+ "USER"
886
+ ]);
887
+ var repositoryRunnerEnvironment = (source) => Object.fromEntries(
888
+ Object.entries(source).filter(
889
+ ([key, value]) => SAFE_RUNNER_ENV_KEYS.has(key) && value !== void 0
890
+ )
891
+ );
892
+ var assertExcludesRepositoryRunner = (relativePath) => {
893
+ const normalized = pathKey(
894
+ posix.normalize(relativePath.replace(/\\/g, "/")).replace(/^\.\//, "").replace(/\/$/, "")
895
+ );
896
+ const protectedPath = pathKey(PROTECTED_RUNNER_PATH);
897
+ if (normalized === "." || normalized === protectedPath || normalized.startsWith(`${protectedPath}/`) || protectedPath.startsWith(`${normalized}/`)) {
898
+ throw new Error(
899
+ `Copy path ${relativePath} would expose the protected repository runner to an agent sandbox.`
900
+ );
901
+ }
902
+ };
903
+ var assertSafeRepositoryRunnerDirectories = (repoDir) => {
904
+ const runnerDir = join(repoDir, CONFIG_DIR, RUNNER_DIR);
905
+ const maskDir = join(repoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR);
906
+ for (const [name, path] of [
907
+ ["repository runner", runnerDir],
908
+ ["repository runner sandbox mask", maskDir]
909
+ ]) {
910
+ const stat2 = lstatSync(path);
911
+ if (stat2.isSymbolicLink() || !stat2.isDirectory()) {
912
+ throw new Error(
913
+ `The protected ${name} path must be a real directory: ${path}`
914
+ );
915
+ }
916
+ }
917
+ if (pathKey(realpathSync(runnerDir)) === pathKey(realpathSync(maskDir))) {
918
+ throw new Error(
919
+ "The protected repository runner and sandbox mask paths must be distinct directories."
920
+ );
921
+ }
922
+ };
923
+ var assertProtectedDirectoryIdentities = async (runnerDir, maskDir, inspect) => {
924
+ if (!inspect) return;
925
+ const runner = await inspect(runnerDir);
926
+ const mask = await inspect(maskDir);
927
+ if (!runner.directory || runner.symbolicLink) {
928
+ throw new Error(
929
+ `The protected repository runner path must be a real directory: ${runnerDir}`
930
+ );
931
+ }
932
+ if (!mask.directory || mask.symbolicLink) {
933
+ throw new Error(
934
+ `The protected repository runner sandbox mask path must be a real directory: ${maskDir}`
935
+ );
936
+ }
937
+ if (pathKey(runner.realPath) === pathKey(mask.realPath)) {
938
+ throw new Error(
939
+ "The protected repository runner and sandbox mask paths must be distinct directories."
940
+ );
941
+ }
942
+ };
943
+
944
+ // src/CopyToWorktree.ts
818
945
  var COPY_TO_WORKTREE_TIMEOUT_MS = 6e4;
819
946
  var getCopyOnWriteFlags = (platform) => platform === "darwin" ? ["-cR"] : ["-R", "--reflink=auto"];
820
947
  var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
@@ -825,6 +952,7 @@ var copyToWorktree = (paths, hostRepoDir, worktreePath, timeoutMs) => {
825
952
  let src;
826
953
  let dest;
827
954
  try {
955
+ assertExcludesRepositoryRunner(relativePath);
828
956
  src = resolveSafeRelativePath(hostRepoDir, relativePath, "copy path");
829
957
  dest = resolveSafeRelativePath(worktreePath, relativePath, "copy path");
830
958
  } catch (error) {
@@ -1109,7 +1237,35 @@ var syncOut = (hostRepoDir, handle) => Effect.gen(function* () {
1109
1237
  const hasUntracked = lsFilesResult.stdout.length > 0;
1110
1238
  const untrackedFiles = hasUntracked ? lsFilesResult.stdout.split("\0").filter((f) => f.length > 0) : [];
1111
1239
  yield* Effect.try({
1112
- try: () => untrackedFiles.forEach(assertSafeGitWorktreePath),
1240
+ try: () => untrackedFiles.forEach((path) => {
1241
+ assertSafeGitWorktreePath(path);
1242
+ assertExcludesRepositoryRunner(path);
1243
+ }),
1244
+ catch: (error) => new SyncError({ message: String(error) })
1245
+ });
1246
+ const changedPaths = [];
1247
+ if (hasCommits) {
1248
+ const committedNames = yield* execOk(
1249
+ handle,
1250
+ `git diff --name-only -z ${shellQuote(`${base}..HEAD`)}`,
1251
+ { cwd: worktreePath }
1252
+ );
1253
+ changedPaths.push(
1254
+ ...committedNames.stdout.split("\0").filter((path) => path.length > 0)
1255
+ );
1256
+ }
1257
+ if (hasDiff) {
1258
+ const uncommittedNames = yield* execOk(
1259
+ handle,
1260
+ "git diff --name-only -z HEAD",
1261
+ { cwd: worktreePath }
1262
+ );
1263
+ changedPaths.push(
1264
+ ...uncommittedNames.stdout.split("\0").filter((path) => path.length > 0)
1265
+ );
1266
+ }
1267
+ yield* Effect.try({
1268
+ try: () => changedPaths.forEach(assertExcludesRepositoryRunner),
1113
1269
  catch: (error) => new SyncError({ message: String(error) })
1114
1270
  });
1115
1271
  if (!hasCommits && !hasDiff && !hasUntracked) {
@@ -1727,6 +1883,18 @@ ${result.stderr}`
1727
1883
  )
1728
1884
  );
1729
1885
  var syncIn = (hostRepoDir, handle) => Effect.gen(function* () {
1886
+ const protectedRunnerPath = `${CONFIG_DIR}/${RUNNER_DIR}`;
1887
+ const runnerObjects = yield* execHostGit3(
1888
+ ["rev-list", "--objects", "--all", "--", protectedRunnerPath],
1889
+ hostRepoDir
1890
+ );
1891
+ if (runnerObjects.trim().length > 0) {
1892
+ return yield* Effect.fail(
1893
+ new SyncError({
1894
+ message: `Refusing to copy repository history into the sandbox because it contains protected repository runner files under ${protectedRunnerPath}. Remove those files from all refs before retrying.`
1895
+ })
1896
+ );
1897
+ }
1730
1898
  const branch = (yield* execHostGit3(
1731
1899
  ["rev-parse", "--abbrev-ref", "HEAD"],
1732
1900
  hostRepoDir
@@ -1823,12 +1991,33 @@ var startNoSandbox = (options) => Effect.tryPromise({
1823
1991
  );
1824
1992
  var startBindMountSandbox = (options) => Effect.tryPromise({
1825
1993
  try: () => {
1994
+ const usesRunnerMask = options.worktreeOrRepoPath === options.hostRepoDir && existsSync(join(options.hostRepoDir, CONFIG_DIR, RUNNER_DIR)) && existsSync(
1995
+ join(options.hostRepoDir, CONFIG_DIR, RUNNER_SANDBOX_MASK_DIR)
1996
+ );
1997
+ if (usesRunnerMask) {
1998
+ assertSafeRepositoryRunnerDirectories(options.hostRepoDir);
1999
+ }
1826
2000
  const rawMounts = [
1827
2001
  {
1828
2002
  hostPath: options.worktreeOrRepoPath,
1829
2003
  sandboxPath: options.repoDir
1830
2004
  },
1831
- ...options.gitMounts
2005
+ ...options.gitMounts,
2006
+ ...usesRunnerMask ? [
2007
+ {
2008
+ hostPath: join(
2009
+ options.hostRepoDir,
2010
+ CONFIG_DIR,
2011
+ RUNNER_SANDBOX_MASK_DIR
2012
+ ),
2013
+ sandboxPath: posix.join(
2014
+ options.repoDir,
2015
+ CONFIG_DIR,
2016
+ RUNNER_DIR
2017
+ ),
2018
+ readonly: true
2019
+ }
2020
+ ] : []
1832
2021
  ];
1833
2022
  const mounts = normalizeMounts(
1834
2023
  rawMounts,
@@ -1895,6 +2084,7 @@ var startIsolatedSandbox = (options) => Effect.gen(function* () {
1895
2084
  let hostPath;
1896
2085
  let sandboxPath;
1897
2086
  try {
2087
+ assertExcludesRepositoryRunner(relativePath);
1898
2088
  hostPath = resolveSafeRelativePath(
1899
2089
  options.sourceRepoDir ?? options.hostRepoDir,
1900
2090
  relativePath,
@@ -2615,6 +2805,6 @@ var registerShutdown = (teardown) => {
2615
2805
  };
2616
2806
  };
2617
2807
 
2618
- export { AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertIsolatedWorkspaceMounts, assertNoSymlinkComponents, copyToWorktree, create, defaultImageName, formatVolumeMount, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, patchGitMountsForWindows, processFileMountParents, pruneStale, registerShutdown, remove, resolveCwd, resolveGitMounts, resolveUserMounts, runHostHooks, startSandbox, syncOut, withSandboxLifecycle };
2619
- //# sourceMappingURL=chunk-2GT3OPO5.js.map
2620
- //# sourceMappingURL=chunk-2GT3OPO5.js.map
2808
+ export { ACTIVATION_LABEL, AgentError, AgentIdleTimeoutError, CLI_NAME, CONFIG_DIR, ClackDisplay, ConfigDirError, CwdError, Display, DockerError, ExecHostError, FileDisplay, InitError, LOCKS_DIR, LOGS_DIR, PRODUCT_NAME, PromptError, PromptExpansionTimeoutError, RUNNER_DIR, RUNNER_SANDBOX_MASK_DIR, SANDBOX_REPO_DIR, SandboxConfig, SandboxFactory, SessionCaptureError, SilentDisplay, WorktreeDockerSandboxFactory, assertIsolatedWorkspaceMounts, assertNoSymlinkComponents, assertProtectedDirectoryIdentities, copyToWorktree, create, defaultImageName, formatVolumeMount, generateTempBranchName, getCurrentBranch, hasUncommittedChanges, makeSandboxFromHandle, patchGitMountsForWindows, processFileMountParents, pruneStale, registerShutdown, remove, repositoryRunnerEnvironment, resolveCwd, resolveGitMounts, resolveUserMounts, runHostHooks, startSandbox, syncOut, withSandboxLifecycle };
2809
+ //# sourceMappingURL=chunk-FDYOTN55.js.map
2810
+ //# sourceMappingURL=chunk-FDYOTN55.js.map