toolcraft 0.0.24 → 0.0.26
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/dist/cli.js +11 -9
- package/dist/error-report.js +14 -11
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.js +70 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +33 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +2 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +36 -9
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/browser.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +11 -1
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +64 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +9 -11
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +18 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +11 -18
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +2 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +32 -22
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +5 -9
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.js +81 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +2 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +3 -3
- package/node_modules/@poe-code/process-runner/dist/docker/args.d.ts +1 -0
- package/node_modules/@poe-code/process-runner/dist/docker/args.js +11 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +170 -36
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +66 -9
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.d.ts +6 -0
- package/node_modules/@poe-code/process-runner/dist/docker/env-file.js +49 -0
- package/node_modules/@poe-code/process-runner/dist/host/host-runner.js +5 -4
- package/node_modules/@poe-code/process-runner/dist/types.d.ts +3 -0
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.d.ts +5 -1
- package/node_modules/@poe-code/process-runner/dist/workspace-transfer.js +29 -10
- package/node_modules/auth-store/dist/encrypted-file-store.d.ts +1 -0
- package/node_modules/auth-store/dist/encrypted-file-store.js +36 -2
- package/node_modules/auth-store/dist/keychain-store.js +20 -1
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +6 -3
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +3 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +39 -14
- package/node_modules/tiny-mcp-client/src/internal.ts +45 -17
- package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +32 -0
- package/node_modules/tiny-mcp-client/src/transports.test.ts +68 -0
- package/package.json +2 -2
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import { buildDockerRunArgs } from "./args.js";
|
|
6
|
+
import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
|
|
7
7
|
import { buildContextArgs, detectContext } from "./context.js";
|
|
8
8
|
import { detectEngine } from "./engine.js";
|
|
9
|
+
import { createDockerEnvFile } from "./env-file.js";
|
|
9
10
|
import { createHostRunner } from "../host/host-runner.js";
|
|
10
11
|
import { downloadWorkspace as downloadTransferredWorkspace, uploadWorkspace as uploadTransferredWorkspace } from "../workspace-transfer.js";
|
|
11
12
|
const containerCommand = ["sh", "-c", "while :; do sleep 3600; done"];
|
|
@@ -103,25 +104,33 @@ function createDockerEnv(input) {
|
|
|
103
104
|
return downloadTransferredWorkspace(workspaceTransferEnv, opts);
|
|
104
105
|
},
|
|
105
106
|
exec(spec) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
107
|
+
const envFile = createDockerEnvFile(spec.env);
|
|
108
|
+
try {
|
|
109
|
+
return cleanUpEnvFileAfterRun(input.runner.exec({
|
|
110
|
+
command: input.engine,
|
|
111
|
+
args: [
|
|
112
|
+
...buildContextArgs(input.engine, input.context),
|
|
113
|
+
"exec",
|
|
114
|
+
...(spec.stdin === "pipe" || spec.stdin === "inherit" ? ["-i"] : []),
|
|
115
|
+
...(spec.tty === true ? ["-t"] : []),
|
|
116
|
+
...(spec.cwd !== undefined ? ["-w", spec.cwd] : []),
|
|
117
|
+
...buildDockerEnvArgs({ env: spec.env, envFilePath: envFile?.path }),
|
|
118
|
+
containerRef,
|
|
119
|
+
spec.command,
|
|
120
|
+
...(spec.args ?? [])
|
|
121
|
+
],
|
|
122
|
+
stdin: spec.stdin,
|
|
123
|
+
stdout: spec.stdout,
|
|
124
|
+
stderr: spec.stderr,
|
|
125
|
+
tty: spec.tty,
|
|
126
|
+
signal: spec.signal,
|
|
127
|
+
killProcessGroup: spec.killProcessGroup
|
|
128
|
+
}), envFile?.cleanup);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
envFile?.cleanup();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
125
134
|
},
|
|
126
135
|
async detach() {
|
|
127
136
|
return createContainerJob(containerRef, input.runner, input.engine, input.context, detachedJobContext);
|
|
@@ -136,7 +145,8 @@ function createDockerEnv(input) {
|
|
|
136
145
|
stdin: "inherit",
|
|
137
146
|
stdout: "inherit",
|
|
138
147
|
stderr: "inherit",
|
|
139
|
-
tty: true
|
|
148
|
+
tty: true,
|
|
149
|
+
signal: shellSpec?.signal
|
|
140
150
|
});
|
|
141
151
|
},
|
|
142
152
|
async close() {
|
|
@@ -173,8 +183,7 @@ export async function buildDockerRuntimeTemplate(input) {
|
|
|
173
183
|
const runner = input.runner ?? createHostRunner();
|
|
174
184
|
const engine = input.runtime.engine ?? detectEngine();
|
|
175
185
|
const context = detectContext();
|
|
176
|
-
const dockerfilePath =
|
|
177
|
-
const buildContext = path.resolve(input.cwd, input.runtime.build_context ?? ".");
|
|
186
|
+
const { dockerfilePath, buildContext } = await resolveRuntimeBuildPaths(input.cwd, input.runtime);
|
|
178
187
|
const dockerfileBytes = await readFile(dockerfilePath);
|
|
179
188
|
const buildContextFiles = await readBuildContextFiles(buildContext);
|
|
180
189
|
const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
|
|
@@ -211,6 +220,28 @@ export async function buildDockerRuntimeTemplate(input) {
|
|
|
211
220
|
cached: false
|
|
212
221
|
};
|
|
213
222
|
}
|
|
223
|
+
async function resolveRuntimeBuildPaths(cwd, runtime) {
|
|
224
|
+
const dockerfilePath = path.resolve(cwd, runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
|
|
225
|
+
const buildContext = path.resolve(cwd, runtime.build_context ?? ".");
|
|
226
|
+
const canonicalCwd = await realpath(cwd);
|
|
227
|
+
const canonicalDockerfilePath = await realpath(dockerfilePath);
|
|
228
|
+
const canonicalBuildContext = await realpath(buildContext);
|
|
229
|
+
assertRuntimePathInsideCwd(canonicalCwd, canonicalDockerfilePath, "runtime.dockerfile");
|
|
230
|
+
assertRuntimePathInsideCwd(canonicalCwd, canonicalBuildContext, "runtime.build_context");
|
|
231
|
+
return {
|
|
232
|
+
dockerfilePath: canonicalDockerfilePath,
|
|
233
|
+
buildContext: canonicalBuildContext
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function assertRuntimePathInsideCwd(cwd, targetPath, fieldName) {
|
|
237
|
+
if (!isPathInsideOrEqual(cwd, targetPath)) {
|
|
238
|
+
throw new Error(`${fieldName} must remain inside runtime cwd ${cwd}.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function isPathInsideOrEqual(rootPath, targetPath) {
|
|
242
|
+
const relativePath = path.relative(rootPath, targetPath);
|
|
243
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
244
|
+
}
|
|
214
245
|
function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engine) {
|
|
215
246
|
const hash = createHash("sha256");
|
|
216
247
|
hash.update(dockerfileBytes);
|
|
@@ -306,9 +337,49 @@ async function runAndRead(runner, spec) {
|
|
|
306
337
|
}
|
|
307
338
|
return output;
|
|
308
339
|
}
|
|
340
|
+
async function runAndReadBytes(runner, spec) {
|
|
341
|
+
const handle = runner.exec(spec);
|
|
342
|
+
const stdout = readStreamBytes(handle.stdout);
|
|
343
|
+
const stderr = readStream(handle.stderr);
|
|
344
|
+
const result = await handle.result;
|
|
345
|
+
const output = await stdout;
|
|
346
|
+
if (result.exitCode !== 0) {
|
|
347
|
+
const errorOutput = await stderr;
|
|
348
|
+
throw new Error(`Command failed with exit code ${result.exitCode}: ${spec.command} ${(spec.args ?? []).join(" ")}${errorOutput ? `\n${errorOutput}` : ""}`);
|
|
349
|
+
}
|
|
350
|
+
return output;
|
|
351
|
+
}
|
|
309
352
|
async function runOrThrow(runner, spec) {
|
|
310
353
|
await runAndRead(runner, spec);
|
|
311
354
|
}
|
|
355
|
+
function cleanUpEnvFileAfterRun(handle, cleanup) {
|
|
356
|
+
if (cleanup === undefined) {
|
|
357
|
+
return handle;
|
|
358
|
+
}
|
|
359
|
+
let cleanedUp = false;
|
|
360
|
+
const cleanupOnce = () => {
|
|
361
|
+
if (cleanedUp) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
cleanedUp = true;
|
|
365
|
+
try {
|
|
366
|
+
cleanup();
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
// Cleanup is best effort; preserve the command result.
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
pid: handle.pid,
|
|
374
|
+
stdin: handle.stdin,
|
|
375
|
+
stdout: handle.stdout,
|
|
376
|
+
stderr: handle.stderr,
|
|
377
|
+
result: handle.result.finally(cleanupOnce),
|
|
378
|
+
kill(signal) {
|
|
379
|
+
handle.kill(signal);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
}
|
|
312
383
|
async function readStream(stream) {
|
|
313
384
|
if (stream === null) {
|
|
314
385
|
return "";
|
|
@@ -320,15 +391,19 @@ async function readStream(stream) {
|
|
|
320
391
|
}
|
|
321
392
|
return chunks.join("");
|
|
322
393
|
}
|
|
394
|
+
async function readStreamBytes(stream) {
|
|
395
|
+
if (stream === null) {
|
|
396
|
+
return Buffer.alloc(0);
|
|
397
|
+
}
|
|
398
|
+
const chunks = [];
|
|
399
|
+
for await (const chunk of stream) {
|
|
400
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk, "utf8") : Buffer.from(chunk));
|
|
401
|
+
}
|
|
402
|
+
return Buffer.concat(chunks);
|
|
403
|
+
}
|
|
323
404
|
function sortedBuildArgs(buildArgs) {
|
|
324
405
|
return Object.entries(buildArgs).sort(([left], [right]) => left.localeCompare(right));
|
|
325
406
|
}
|
|
326
|
-
function buildEnvArgs(env) {
|
|
327
|
-
if (env === undefined) {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
return Object.entries(env).flatMap(([key, value]) => ["-e", `${key}=${value}`]);
|
|
331
|
-
}
|
|
332
407
|
function createContainerName() {
|
|
333
408
|
return `poe-env-${randomBytes(6).toString("hex")}`;
|
|
334
409
|
}
|
|
@@ -383,10 +458,25 @@ function createContainerWorkspaceFileSystem(input) {
|
|
|
383
458
|
await execShell(`mkdir -p ${shellQuote(targetPath)}`);
|
|
384
459
|
},
|
|
385
460
|
async readdir(targetPath) {
|
|
386
|
-
const
|
|
461
|
+
const quotedTargetPath = shellQuote(targetPath);
|
|
462
|
+
const output = await execShell([
|
|
463
|
+
`for item in ${quotedTargetPath}/* ${quotedTargetPath}/.[!.]* ${quotedTargetPath}/..?*; do`,
|
|
464
|
+
`[ -e "$item" ] || [ -L "$item" ] || continue;`,
|
|
465
|
+
`if [ -L "$item" ]; then kind=l; size=0;`,
|
|
466
|
+
`elif [ -d "$item" ]; then kind=d; size=0;`,
|
|
467
|
+
`elif [ -f "$item" ]; then kind=f; size=$(wc -c < "$item");`,
|
|
468
|
+
`else continue; fi;`,
|
|
469
|
+
`printf '%s\\t%s\\t%s\\n' "\${item##*/}" "$kind" "$size";`,
|
|
470
|
+
`done`
|
|
471
|
+
].join(" "));
|
|
387
472
|
return output.split("\n").filter(Boolean).map((line) => {
|
|
388
473
|
const [name = "", kind = "f"] = line.split("\t");
|
|
389
|
-
return {
|
|
474
|
+
return {
|
|
475
|
+
name,
|
|
476
|
+
isFile: () => kind === "f",
|
|
477
|
+
isDirectory: () => kind === "d",
|
|
478
|
+
isSymbolicLink: () => kind === "l"
|
|
479
|
+
};
|
|
390
480
|
});
|
|
391
481
|
},
|
|
392
482
|
readFile: readFileFromContainer,
|
|
@@ -459,8 +549,10 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
459
549
|
? ""
|
|
460
550
|
: ` && test $(stat -c %Y ${logFile} 2>/dev/null || stat -f %m ${logFile}) -ge ${Math.ceil(opts.since.getTime() / 1000)}`;
|
|
461
551
|
let byteOffset = opts?.sinceByte ?? 0;
|
|
552
|
+
let pendingBytes = Buffer.alloc(0);
|
|
553
|
+
let pendingByteOffset = byteOffset;
|
|
462
554
|
while (true) {
|
|
463
|
-
const stdout = await
|
|
555
|
+
const stdout = await runAndReadBytes(runner, {
|
|
464
556
|
command: engine,
|
|
465
557
|
args: [
|
|
466
558
|
...buildContextArgs(engine, context),
|
|
@@ -473,9 +565,18 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
473
565
|
stdout: "pipe",
|
|
474
566
|
stderr: "pipe"
|
|
475
567
|
});
|
|
476
|
-
if (stdout.
|
|
477
|
-
|
|
478
|
-
|
|
568
|
+
if (stdout.byteLength > 0) {
|
|
569
|
+
const combined = pendingBytes.byteLength === 0
|
|
570
|
+
? stdout
|
|
571
|
+
: Buffer.concat([pendingBytes, stdout]);
|
|
572
|
+
const completeLength = completeUtf8PrefixLength(combined);
|
|
573
|
+
byteOffset += stdout.byteLength;
|
|
574
|
+
pendingBytes = combined.subarray(completeLength);
|
|
575
|
+
const data = combined.subarray(0, completeLength).toString("utf8");
|
|
576
|
+
if (data.length > 0) {
|
|
577
|
+
yield { byteOffset: pendingByteOffset, data };
|
|
578
|
+
pendingByteOffset += completeLength;
|
|
579
|
+
}
|
|
479
580
|
}
|
|
480
581
|
if (opts?.follow !== true || (await this.status()) !== "running") {
|
|
481
582
|
return;
|
|
@@ -517,6 +618,39 @@ function createContainerJob(containerId, runner, engine, context, detachedJobCon
|
|
|
517
618
|
}
|
|
518
619
|
};
|
|
519
620
|
}
|
|
621
|
+
function completeUtf8PrefixLength(contents) {
|
|
622
|
+
if (contents.length === 0) {
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
let leadIndex = contents.length - 1;
|
|
626
|
+
while (leadIndex >= 0 && isUtf8ContinuationByte(contents[leadIndex])) {
|
|
627
|
+
leadIndex -= 1;
|
|
628
|
+
}
|
|
629
|
+
if (leadIndex < 0) {
|
|
630
|
+
return contents.length;
|
|
631
|
+
}
|
|
632
|
+
const expectedLength = utf8SequenceLength(contents[leadIndex]);
|
|
633
|
+
if (expectedLength === 0) {
|
|
634
|
+
return contents.length;
|
|
635
|
+
}
|
|
636
|
+
const availableLength = contents.length - leadIndex;
|
|
637
|
+
return availableLength < expectedLength ? leadIndex : contents.length;
|
|
638
|
+
}
|
|
639
|
+
function isUtf8ContinuationByte(byte) {
|
|
640
|
+
return byte >= 0x80 && byte <= 0xbf;
|
|
641
|
+
}
|
|
642
|
+
function utf8SequenceLength(byte) {
|
|
643
|
+
if (byte >= 0xc2 && byte <= 0xdf) {
|
|
644
|
+
return 2;
|
|
645
|
+
}
|
|
646
|
+
if (byte >= 0xe0 && byte <= 0xef) {
|
|
647
|
+
return 3;
|
|
648
|
+
}
|
|
649
|
+
if (byte >= 0xf0 && byte <= 0xf4) {
|
|
650
|
+
return 4;
|
|
651
|
+
}
|
|
652
|
+
return 0;
|
|
653
|
+
}
|
|
520
654
|
async function readDetachedExitCode(containerId, jobId, runner, engine, context) {
|
|
521
655
|
const exitFile = shellQuote(`/tmp/poe-jobs/${jobId}.exit`);
|
|
522
656
|
const handle = runner.exec({
|
|
@@ -3,6 +3,9 @@ import { randomBytes } from "node:crypto";
|
|
|
3
3
|
import { buildDockerRunArgs } from "./args.js";
|
|
4
4
|
import { buildContextArgs, detectContext } from "./context.js";
|
|
5
5
|
import { detectEngine } from "./engine.js";
|
|
6
|
+
import { createDockerEnvFile } from "./env-file.js";
|
|
7
|
+
const DOCKER_ABORT_GRACE_MS = 10_000;
|
|
8
|
+
const DOCKER_ABORT_FORCE_GRACE_MS = 5_000;
|
|
6
9
|
export function createDockerRunner(options) {
|
|
7
10
|
const engine = options.engine ?? detectEngine();
|
|
8
11
|
const context = options.context ?? detectContext();
|
|
@@ -27,6 +30,7 @@ export function createDockerRunner(options) {
|
|
|
27
30
|
stderrMode === "inherit" &&
|
|
28
31
|
spec.tty === true;
|
|
29
32
|
const containerName = buildContainerName(options.containerName ?? spec.command);
|
|
33
|
+
const envFile = createDockerEnvFile(spec.env);
|
|
30
34
|
const runArgs = buildDockerRunArgs({
|
|
31
35
|
engine,
|
|
32
36
|
context,
|
|
@@ -35,6 +39,7 @@ export function createDockerRunner(options) {
|
|
|
35
39
|
args: spec.args ?? [],
|
|
36
40
|
cwd: spec.cwd,
|
|
37
41
|
env: spec.env,
|
|
42
|
+
envFilePath: envFile?.path,
|
|
38
43
|
mounts: options.mounts ?? [],
|
|
39
44
|
ports: options.ports ?? [],
|
|
40
45
|
network: options.network,
|
|
@@ -46,25 +51,55 @@ export function createDockerRunner(options) {
|
|
|
46
51
|
extraArgs: options.extraArgs ?? []
|
|
47
52
|
});
|
|
48
53
|
const [command, ...args] = runArgs;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
let child;
|
|
55
|
+
try {
|
|
56
|
+
child = childProcess.spawn(command, args, {
|
|
57
|
+
stdio: interactiveMode ? "inherit" : [stdinMode, stdoutMode, stderrMode]
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
envFile?.cleanup();
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
52
64
|
let isResultSettled = false;
|
|
65
|
+
let exitCodeOverride = null;
|
|
53
66
|
let resolveResult = null;
|
|
67
|
+
let abortEscalationTimers = [];
|
|
54
68
|
const result = new Promise((resolve) => {
|
|
55
69
|
resolveResult = resolve;
|
|
56
70
|
});
|
|
71
|
+
const clearAbortEscalation = () => {
|
|
72
|
+
for (const timer of abortEscalationTimers) {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
}
|
|
75
|
+
abortEscalationTimers = [];
|
|
76
|
+
};
|
|
57
77
|
const settleResult = (exitCode) => {
|
|
58
78
|
if (isResultSettled) {
|
|
59
79
|
return;
|
|
60
80
|
}
|
|
61
81
|
isResultSettled = true;
|
|
62
82
|
cleanupAbort();
|
|
63
|
-
|
|
83
|
+
clearAbortEscalation();
|
|
84
|
+
envFile?.cleanup();
|
|
85
|
+
resolveResult?.({ exitCode: exitCodeOverride ?? exitCode });
|
|
86
|
+
};
|
|
87
|
+
const scheduleAbortEscalation = () => {
|
|
88
|
+
const terminateTimer = setTimeout(() => {
|
|
89
|
+
killHostDockerChild(child, "SIGTERM");
|
|
90
|
+
const forceTimer = setTimeout(() => {
|
|
91
|
+
killHostDockerChild(child, "SIGKILL");
|
|
92
|
+
}, DOCKER_ABORT_FORCE_GRACE_MS);
|
|
93
|
+
unrefTimer(forceTimer);
|
|
94
|
+
abortEscalationTimers.push(forceTimer);
|
|
95
|
+
}, DOCKER_ABORT_GRACE_MS);
|
|
96
|
+
unrefTimer(terminateTimer);
|
|
97
|
+
abortEscalationTimers.push(terminateTimer);
|
|
64
98
|
};
|
|
65
99
|
const cleanupAbort = bindAbortSignal(spec.signal, () => {
|
|
66
|
-
|
|
100
|
+
exitCodeOverride = 1;
|
|
67
101
|
spawnControlCommand(engine, context, ["stop", containerName]);
|
|
102
|
+
scheduleAbortEscalation();
|
|
68
103
|
});
|
|
69
104
|
child.once("error", () => {
|
|
70
105
|
settleResult(1);
|
|
@@ -93,6 +128,14 @@ export function createDockerRunner(options) {
|
|
|
93
128
|
}
|
|
94
129
|
};
|
|
95
130
|
}
|
|
131
|
+
function killHostDockerChild(child, signal) {
|
|
132
|
+
try {
|
|
133
|
+
child.kill(signal);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
96
139
|
function buildContainerName(name) {
|
|
97
140
|
const suffix = randomBytes(3).toString("hex").slice(0, 6);
|
|
98
141
|
const sanitizedName = sanitizeContainerName(name);
|
|
@@ -123,10 +166,16 @@ function isContainerNameCharacter(char) {
|
|
|
123
166
|
return char === "." || char === "_" || char === "-";
|
|
124
167
|
}
|
|
125
168
|
function spawnControlCommand(engine, context, args) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
169
|
+
try {
|
|
170
|
+
const child = childProcess.spawn(engine, [...buildContextArgs(engine, context), ...args], {
|
|
171
|
+
stdio: "ignore"
|
|
172
|
+
});
|
|
173
|
+
child.once("error", () => undefined);
|
|
174
|
+
child.unref();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
130
179
|
}
|
|
131
180
|
function bindAbortSignal(signal, onAbort) {
|
|
132
181
|
if (signal === undefined) {
|
|
@@ -141,3 +190,11 @@ function bindAbortSignal(signal, onAbort) {
|
|
|
141
190
|
signal.removeEventListener("abort", onAbort);
|
|
142
191
|
};
|
|
143
192
|
}
|
|
193
|
+
function unrefTimer(timer) {
|
|
194
|
+
if (typeof timer === "object" &&
|
|
195
|
+
timer !== null &&
|
|
196
|
+
"unref" in timer &&
|
|
197
|
+
typeof timer.unref === "function") {
|
|
198
|
+
timer.unref();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface DockerEnvFile {
|
|
2
|
+
path: string;
|
|
3
|
+
cleanup(): void;
|
|
4
|
+
}
|
|
5
|
+
export declare function createDockerEnvFile(env: Record<string, string> | undefined): DockerEnvFile | null;
|
|
6
|
+
export declare function serializeDockerEnvFile(entries: Array<[string, string]>): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function createDockerEnvFile(env) {
|
|
5
|
+
const entries = Object.entries(env ?? {});
|
|
6
|
+
if (entries.length === 0) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const directory = mkdtempSync(path.join(tmpdir(), "poe-docker-env-"));
|
|
10
|
+
const filePath = path.join(directory, "env");
|
|
11
|
+
let active = true;
|
|
12
|
+
try {
|
|
13
|
+
writeFileSync(filePath, serializeDockerEnvFile(entries), {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
flag: "wx",
|
|
16
|
+
mode: 0o600
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
rmSync(directory, { recursive: true, force: true });
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
path: filePath,
|
|
25
|
+
cleanup() {
|
|
26
|
+
if (!active) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
active = false;
|
|
30
|
+
rmSync(directory, { recursive: true, force: true });
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function serializeDockerEnvFile(entries) {
|
|
35
|
+
return (entries.map(([key, value]) => `${formatDockerEnvKey(key)}=${formatDockerEnvValue(value)}`).join("\n") +
|
|
36
|
+
"\n");
|
|
37
|
+
}
|
|
38
|
+
function formatDockerEnvKey(key) {
|
|
39
|
+
if (key.length === 0 || key.includes("=") || key.includes("\n") || key.includes("\r")) {
|
|
40
|
+
throw new Error(`Invalid Docker environment variable name: ${JSON.stringify(key)}`);
|
|
41
|
+
}
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
function formatDockerEnvValue(value) {
|
|
45
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
46
|
+
throw new Error("Docker env-file values cannot contain newline characters.");
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
2
|
export function createHostRunner(options = {}) {
|
|
3
|
-
const
|
|
3
|
+
const detachedByDefault = options.detached === true;
|
|
4
4
|
return {
|
|
5
5
|
name: "host",
|
|
6
6
|
exec(spec) {
|
|
@@ -17,6 +17,7 @@ export function createHostRunner(options = {}) {
|
|
|
17
17
|
const stdinMode = spec.stdin ?? "ignore";
|
|
18
18
|
const stdoutMode = spec.stdout ?? "pipe";
|
|
19
19
|
const stderrMode = spec.stderr ?? "pipe";
|
|
20
|
+
const killProcessGroup = detachedByDefault || spec.killProcessGroup === true;
|
|
20
21
|
const stdio = stdinMode === "inherit" && stdoutMode === "inherit" && stderrMode === "inherit"
|
|
21
22
|
? "inherit"
|
|
22
23
|
: [stdinMode, stdoutMode, stderrMode];
|
|
@@ -24,13 +25,13 @@ export function createHostRunner(options = {}) {
|
|
|
24
25
|
cwd: spec.cwd,
|
|
25
26
|
env: spec.env,
|
|
26
27
|
stdio,
|
|
27
|
-
...(
|
|
28
|
+
...(killProcessGroup ? { detached: true } : {})
|
|
28
29
|
});
|
|
29
|
-
if (
|
|
30
|
+
if (killProcessGroup) {
|
|
30
31
|
child.unref();
|
|
31
32
|
}
|
|
32
33
|
const kill = (signal) => {
|
|
33
|
-
if (
|
|
34
|
+
if (killProcessGroup && process.platform !== "win32" && child.pid !== undefined) {
|
|
34
35
|
process.kill(-child.pid, signal);
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
@@ -20,6 +20,8 @@ export interface RunSpec {
|
|
|
20
20
|
stderr?: "pipe" | "inherit";
|
|
21
21
|
tty?: boolean;
|
|
22
22
|
signal?: AbortSignal;
|
|
23
|
+
/** Start in a separate process group so kill() can signal the full group where supported. */
|
|
24
|
+
killProcessGroup?: boolean;
|
|
23
25
|
}
|
|
24
26
|
export interface Runner {
|
|
25
27
|
exec(spec: RunSpec): RunHandle;
|
|
@@ -163,6 +165,7 @@ export interface DockerRunArgs {
|
|
|
163
165
|
args: string[];
|
|
164
166
|
cwd?: string;
|
|
165
167
|
env?: Record<string, string>;
|
|
168
|
+
envFilePath?: string;
|
|
166
169
|
mounts: DockerMount[];
|
|
167
170
|
ports: DockerPortMapping[];
|
|
168
171
|
network?: string;
|
|
@@ -4,6 +4,7 @@ export interface WorkspaceTransferDirent {
|
|
|
4
4
|
name: string;
|
|
5
5
|
isFile(): boolean;
|
|
6
6
|
isDirectory(): boolean;
|
|
7
|
+
isSymbolicLink?(): boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface WorkspaceTransferStats {
|
|
9
10
|
isFile(): boolean;
|
|
@@ -20,7 +21,10 @@ export interface WorkspaceTransferFileSystem {
|
|
|
20
21
|
}): Promise<WorkspaceTransferDirent[]>;
|
|
21
22
|
readFile(path: string): Promise<Buffer>;
|
|
22
23
|
readFile(path: string, encoding: BufferEncoding): Promise<string>;
|
|
23
|
-
writeFile(path: string, data: string | Buffer
|
|
24
|
+
writeFile(path: string, data: string | Buffer, options?: {
|
|
25
|
+
flag?: string;
|
|
26
|
+
mode?: number;
|
|
27
|
+
}): Promise<void>;
|
|
24
28
|
stat(path: string): Promise<WorkspaceTransferStats>;
|
|
25
29
|
lstat?(path: string): Promise<WorkspaceTransferStats>;
|
|
26
30
|
rename?(oldPath: string, newPath: string): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { promises as nodeFs } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
const uploadState = new WeakMap();
|
|
@@ -82,7 +82,7 @@ export async function downloadWorkspace(env, opts) {
|
|
|
82
82
|
const remoteFs = env.remoteFs ?? localFs;
|
|
83
83
|
const workspaceDir = env.workspaceDir ?? "/workspace";
|
|
84
84
|
const state = uploadState.get(env) ?? new Map();
|
|
85
|
-
const remoteFiles = await listFilesIfExists(remoteFs, workspaceDir);
|
|
85
|
+
const remoteFiles = await listFilesIfExists(remoteFs, workspaceDir, { rejectSymlinks: true });
|
|
86
86
|
const remotePaths = new Set(remoteFiles.map((file) => file.path));
|
|
87
87
|
const conflicts = [];
|
|
88
88
|
let files = 0;
|
|
@@ -96,7 +96,7 @@ export async function downloadWorkspace(env, opts) {
|
|
|
96
96
|
conflicts.push({ path: remoteFile.path, reason: "local_modified" });
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
|
-
await writeFileAtomically(localFs, localPath, remoteContent, ".download-tmp");
|
|
99
|
+
await writeFileAtomically(localFs, env.cwd, localPath, remoteContent, ".download-tmp");
|
|
100
100
|
state.set(remoteFile.path, {
|
|
101
101
|
hash: hashBuffer(remoteContent),
|
|
102
102
|
uploaded: true
|
|
@@ -122,9 +122,9 @@ export async function downloadWorkspace(env, opts) {
|
|
|
122
122
|
}
|
|
123
123
|
return { files, bytes, conflicts };
|
|
124
124
|
}
|
|
125
|
-
async function listFilesIfExists(fs, root) {
|
|
125
|
+
async function listFilesIfExists(fs, root, options = {}) {
|
|
126
126
|
try {
|
|
127
|
-
return await listFiles(fs, root);
|
|
127
|
+
return await listFiles(fs, root, options);
|
|
128
128
|
}
|
|
129
129
|
catch (error) {
|
|
130
130
|
if (isNotFoundError(error)) {
|
|
@@ -133,12 +133,15 @@ async function listFilesIfExists(fs, root) {
|
|
|
133
133
|
throw error;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
async function listFiles(fs, root) {
|
|
136
|
+
async function listFiles(fs, root, options = {}) {
|
|
137
137
|
const result = [];
|
|
138
138
|
async function visit(dir) {
|
|
139
139
|
const dirents = await fs.readdir(dir, { withFileTypes: true });
|
|
140
140
|
for (const dirent of dirents.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
141
141
|
const absolutePath = path.join(dir, dirent.name);
|
|
142
|
+
if (options.rejectSymlinks === true && dirent.isSymbolicLink?.() === true) {
|
|
143
|
+
throw new Error("Workspace download must not follow symbolic links.");
|
|
144
|
+
}
|
|
142
145
|
if (dirent.isDirectory()) {
|
|
143
146
|
await visit(absolutePath);
|
|
144
147
|
continue;
|
|
@@ -347,15 +350,28 @@ async function renamePath(fs, sourcePath, destinationPath) {
|
|
|
347
350
|
}
|
|
348
351
|
await fs.rename(sourcePath, destinationPath);
|
|
349
352
|
}
|
|
350
|
-
async function writeFileAtomically(fs, destinationPath, data, temporarySuffix) {
|
|
351
|
-
const temporaryPath = `${destinationPath}${temporarySuffix}`;
|
|
353
|
+
async function writeFileAtomically(fs, workspacePath, destinationPath, data, temporarySuffix) {
|
|
354
|
+
const temporaryPath = `${destinationPath}.${randomUUID()}${temporarySuffix}`;
|
|
355
|
+
let temporaryCreated = false;
|
|
352
356
|
await fs.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
353
357
|
try {
|
|
354
|
-
await fs
|
|
358
|
+
await assertSafeLocalDownloadPath(fs, workspacePath, temporaryPath);
|
|
359
|
+
try {
|
|
360
|
+
await fs.writeFile(temporaryPath, data, { flag: "wx", mode: 0o600 });
|
|
361
|
+
temporaryCreated = true;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (!isAlreadyExistsError(error)) {
|
|
365
|
+
await removeFile(fs, temporaryPath).catch(() => undefined);
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
355
369
|
await renamePath(fs, temporaryPath, destinationPath);
|
|
356
370
|
}
|
|
357
371
|
catch (error) {
|
|
358
|
-
|
|
372
|
+
if (temporaryCreated) {
|
|
373
|
+
await removeFile(fs, temporaryPath).catch(() => undefined);
|
|
374
|
+
}
|
|
359
375
|
throw error;
|
|
360
376
|
}
|
|
361
377
|
}
|
|
@@ -482,3 +498,6 @@ function stripSlashes(value) {
|
|
|
482
498
|
function isNotFoundError(error) {
|
|
483
499
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
484
500
|
}
|
|
501
|
+
function isAlreadyExistsError(error) {
|
|
502
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
503
|
+
}
|
|
@@ -33,6 +33,7 @@ export interface EncryptedFileStoreInput {
|
|
|
33
33
|
export declare class EncryptedFileStore implements SecretStore {
|
|
34
34
|
private readonly fs;
|
|
35
35
|
private readonly filePath;
|
|
36
|
+
private readonly symbolicLinkCheckStartPath;
|
|
36
37
|
private readonly salt;
|
|
37
38
|
private readonly getMachineIdentity;
|
|
38
39
|
private readonly getRandomBytes;
|