Harden drop-sudo regression coverage
This commit is contained in:
@@ -33,6 +33,9 @@ jobs:
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: Test privileged Linux drop-sudo
|
||||
run: pnpm run test:privileged
|
||||
|
||||
- name: Build bundles
|
||||
run: pnpm run build
|
||||
|
||||
|
||||
Vendored
+21
-12
File diff suppressed because one or more lines are too long
+1
-1
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "esbuild src/main.ts --sourcemap=inline --format=cjs --bundle --platform=node --target=node20 --outdir=dist",
|
||||
"check": "tsc --noEmit",
|
||||
"test": "pnpm run build && node --test test/*.test.mjs && node --test --test-concurrency=1 test/privileged/*.test.mjs",
|
||||
"test": "pnpm run build && node --test test/*.test.mjs",
|
||||
"test:privileged": "pnpm run build && node --test --test-concurrency=1 test/privileged/*.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -172,7 +172,7 @@ async function dropSudoWithPrivileges(options: DropSudoOptions): Promise<void> {
|
||||
);
|
||||
}
|
||||
|
||||
async function ensurePasswordlessSudo(): Promise<void> {
|
||||
export async function ensurePasswordlessSudo(): Promise<void> {
|
||||
try {
|
||||
await execCommand(SUDO_PATH, ["-n", "true"], { capture: true });
|
||||
} catch (error) {
|
||||
|
||||
+31
-16
@@ -4,7 +4,11 @@ import path from "path";
|
||||
import os from "os";
|
||||
import { setOutput } from "@actions/core";
|
||||
import { checkOutput } from "./checkOutput";
|
||||
import { dropSudo, verifySudoUnavailable } from "./dropSudo";
|
||||
import {
|
||||
dropSudo,
|
||||
ensurePasswordlessSudo,
|
||||
verifySudoUnavailable,
|
||||
} from "./dropSudo";
|
||||
import {
|
||||
buildGatedCodexCommand,
|
||||
endChildInput,
|
||||
@@ -93,6 +97,31 @@ export async function runCodexExec({
|
||||
break;
|
||||
}
|
||||
|
||||
const isLinuxDropSudo =
|
||||
safetyStrategy === "drop-sudo" && process.platform === "linux";
|
||||
if (isLinuxDropSudo) {
|
||||
if (
|
||||
typeof process.getuid !== "function" ||
|
||||
typeof process.getgid !== "function"
|
||||
) {
|
||||
throw new Error("Linux drop-sudo requires POSIX user and group APIs.");
|
||||
}
|
||||
|
||||
if (process.getuid() === 0) {
|
||||
throw new Error(
|
||||
"Linux drop-sudo cannot run Codex from a runner whose default user is root."
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await ensurePasswordlessSudo();
|
||||
} catch {
|
||||
throw new Error(
|
||||
"Linux drop-sudo requires passwordless sudo before Codex starts. If an earlier drop-sudo invocation already removed sudo, run Codex in that invocation or use a fresh job."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const runAsUser: string | null =
|
||||
safetyStrategy === "unprivileged-user" ? codexUser : null;
|
||||
|
||||
@@ -119,21 +148,7 @@ export async function runCodexExec({
|
||||
let linuxGate: GatedCodexCommand | null = null;
|
||||
|
||||
let pathToCodex = "codex";
|
||||
if (safetyStrategy === "drop-sudo" && process.platform === "linux") {
|
||||
if (
|
||||
typeof process.getuid !== "function" ||
|
||||
typeof process.getgid !== "function"
|
||||
) {
|
||||
throw new Error("Linux drop-sudo requires POSIX user and group APIs.");
|
||||
}
|
||||
|
||||
const uid = process.getuid();
|
||||
if (uid === 0) {
|
||||
throw new Error(
|
||||
"Linux drop-sudo cannot run Codex from a runner whose default user is root."
|
||||
);
|
||||
}
|
||||
|
||||
if (isLinuxDropSudo) {
|
||||
pathToCodex = (await checkOutput(["which", "codex"])).trim();
|
||||
if (!pathToCodex) {
|
||||
throw new Error("could not find 'codex' in PATH");
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const actionPath = fileURLToPath(new URL("../action.yml", import.meta.url));
|
||||
const action = readFileSync(actionPath, "utf8");
|
||||
|
||||
function stepCondition(name) {
|
||||
const marker = ` - name: ${name}\n`;
|
||||
const start = action.indexOf(marker);
|
||||
assert.notEqual(start, -1, `missing action step: ${name}`);
|
||||
const next = action.indexOf("\n - name: ", start + marker.length);
|
||||
const block = action.slice(start, next < 0 ? undefined : next);
|
||||
const condition = block
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith(" if: "));
|
||||
assert.ok(condition, `missing condition for action step: ${name}`);
|
||||
return condition.slice(" if: ".length);
|
||||
}
|
||||
|
||||
test("keeps Linux prompt execution out of the pre-drop steps", () => {
|
||||
const expected = `\${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' && (runner.os != 'Linux' || (inputs.prompt == '' && inputs['prompt-file'] == '')) }}`;
|
||||
|
||||
assert.equal(stepCondition("Drop sudo privilege, if appropriate"), expected);
|
||||
assert.equal(stepCondition("Verify sudo privilege removed"), expected);
|
||||
});
|
||||
@@ -272,6 +272,23 @@ test(
|
||||
run("/usr/bin/id", ["-nG", user]).stdout.split(/\s+/).includes("sudo"),
|
||||
false
|
||||
);
|
||||
|
||||
const repeatedResult = spawnCodexAs({
|
||||
uid,
|
||||
gid,
|
||||
user,
|
||||
home,
|
||||
binDir,
|
||||
actionPath,
|
||||
workspace,
|
||||
outputPath,
|
||||
prompt: "second invocation must remain gated",
|
||||
});
|
||||
assert.notEqual(repeatedResult.status, 0);
|
||||
assert.match(
|
||||
repeatedResult.stderr,
|
||||
/If an earlier drop-sudo invocation already removed sudo/
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user