@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.
- package/README.md +99 -146
- package/dist/chunk-56TDSWFU.js +255 -0
- package/dist/chunk-56TDSWFU.js.map +1 -0
- package/dist/chunk-ACD46ZM4.js +136 -0
- package/dist/chunk-ACD46ZM4.js.map +1 -0
- package/dist/{chunk-2GT3OPO5.js → chunk-FDYOTN55.js} +196 -6
- package/dist/chunk-FDYOTN55.js.map +1 -0
- package/dist/chunk-SOJTAJTF.js +78 -0
- package/dist/chunk-SOJTAJTF.js.map +1 -0
- package/dist/chunk-WAXEMZUV.js +877 -0
- package/dist/chunk-WAXEMZUV.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +58 -118
- package/dist/index.js.map +1 -1
- package/dist/main.js +1831 -143
- package/dist/main.js.map +1 -1
- package/dist/sandboxes/docker.js +3 -3
- package/dist/sandboxes/no-sandbox.js +2 -2
- package/dist/sandboxes/vercel.js +15 -35
- package/dist/sandboxes/vercel.js.map +1 -1
- package/dist/templates/parallel-planner/main.mts +11 -8
- package/dist/templates/parallel-planner-with-review/main.mts +23 -13
- package/package.json +3 -2
- package/dist/chunk-2GT3OPO5.js.map +0 -1
- package/dist/chunk-OCJKONZ7.js +0 -475
- package/dist/chunk-OCJKONZ7.js.map +0 -1
- package/dist/chunk-VDHZO5PP.js +0 -113
- package/dist/chunk-VDHZO5PP.js.map +0 -1
- package/dist/chunk-VW4MI5GJ.js +0 -126
- package/dist/chunk-VW4MI5GJ.js.map +0 -1
- package/dist/chunk-WA7QPAXO.js +0 -50
- package/dist/chunk-WA7QPAXO.js.map +0 -1
|
@@ -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(
|
|
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-
|
|
2620
|
-
//# sourceMappingURL=chunk-
|
|
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
|