Merge branch 'main' into claude/simplify-remediation-steps-AA2ao

This commit is contained in:
SafeKey Lab 2026-01-26 23:06:11 -05:00 committed by GitHub
commit 4f3edef3f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 51 additions and 7 deletions

View File

@ -67,7 +67,8 @@ Example:
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too.
Clawdbot prepends `env.PATH` after profile sourcing via an internal env var (no shell interpolation);
`tools.exec.pathPrepend` applies here too.
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.

View File

@ -60,11 +60,18 @@ export function buildDockerExecArgs(params: {
for (const [key, value] of Object.entries(params.env)) {
args.push("-e", `${key}=${value}`);
}
const hasCustomPath = typeof params.env.PATH === "string" && params.env.PATH.length > 0;
if (hasCustomPath) {
// Avoid interpolating PATH into the shell command; pass it via env instead.
args.push("-e", `CLAWDBOT_PREPEND_PATH=${params.env.PATH}`);
}
// Login shell (-l) sources /etc/profile which resets PATH to a minimal set,
// overriding both Docker ENV and -e PATH=... environment variables.
// Prepend custom PATH after profile sourcing to ensure custom tools are accessible
// while preserving system paths that /etc/profile may have added.
const pathExport = params.env.PATH ? `export PATH="${params.env.PATH}:$PATH"; ` : "";
const pathExport = hasCustomPath
? 'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; '
: "";
args.push(params.containerName, "sh", "-lc", `${pathExport}${params.command}`);
return args;
}

View File

@ -318,9 +318,30 @@ describe("buildDockerExecArgs", () => {
});
const commandArg = args[args.length - 1];
expect(commandArg).toContain('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"');
expect(args).toContain("CLAWDBOT_PREPEND_PATH=/custom/bin:/usr/local/bin:/usr/bin");
expect(commandArg).toContain('export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"');
expect(commandArg).toContain("echo hello");
expect(commandArg).toBe('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"; echo hello');
expect(commandArg).toBe(
'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; echo hello',
);
});
it("does not interpolate PATH into the shell command", () => {
const injectedPath = "$(touch /tmp/clawdbot-path-injection)";
const args = buildDockerExecArgs({
containerName: "test-container",
command: "echo hello",
env: {
PATH: injectedPath,
HOME: "/home/user",
},
tty: false,
});
const commandArg = args[args.length - 1];
expect(args).toContain(`CLAWDBOT_PREPEND_PATH=${injectedPath}`);
expect(commandArg).not.toContain(injectedPath);
expect(commandArg).toContain("CLAWDBOT_PREPEND_PATH");
});
it("does not add PATH export when PATH is not in env", () => {

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { generateUUID } from "./uuid";
@ -26,7 +26,13 @@ describe("generateUUID", () => {
});
it("still returns a v4 UUID when crypto is missing", () => {
const id = generateUUID(null);
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
const id = generateUUID(null);
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
expect(warnSpy).toHaveBeenCalled();
} finally {
warnSpy.mockRestore();
}
});
});

View File

@ -3,6 +3,8 @@ export type CryptoLike = {
getRandomValues?: ((array: Uint8Array) => Uint8Array) | undefined;
};
let warnedWeakCrypto = false;
function uuidFromBytes(bytes: Uint8Array): string {
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
@ -29,6 +31,12 @@ function weakRandomBytes(): Uint8Array {
return bytes;
}
function warnWeakCryptoOnce() {
if (warnedWeakCrypto) return;
warnedWeakCrypto = true;
console.warn("[uuid] crypto API missing; falling back to weak randomness");
}
export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string {
if (cryptoLike && typeof cryptoLike.randomUUID === "function") return cryptoLike.randomUUID();
@ -38,5 +46,6 @@ export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto):
return uuidFromBytes(bytes);
}
warnWeakCryptoOnce();
return uuidFromBytes(weakRandomBytes());
}