Files
openclaw/src/infra/exec-authorization-render.test.ts
Jesse Merhi c9707ab635 fix(exec): rebuild command authorization on the Tree-sitter command planner
Replace the exec approval parser/planner path with Tree-sitter-backed authorization planning, carrying planner decisions through node and gateway execution.

This keeps unpersistable shell shapes one-shot, adds typed `unavailableDecisions` for approval prompts, and refreshes coverage for allowlist matching, command rendering, durable allow-always persistence, and host approval paths.

Verification:
- GitHub PR checks for ce2381192d: CLEAN, 142 success, 32 skipped, 0 failed, 0 pending.
- /Users/jmerhi/.nvm/versions/node/v24.12.0/bin/node scripts/plugin-sdk-surface-report.mjs --check
- /Users/jmerhi/.nvm/versions/node/v24.12.0/bin/node scripts/run-vitest.mjs test/scripts/plugin-sdk-surface-report.test.ts --reporter=verbose
- Focused exec approval suite: 13 files, 467 tests.
2026-06-18 10:12:48 +10:00

272 lines
7.8 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { makeExecutable, makePathEnv, makeTempDir } from "./exec-approvals-test-helpers.js";
import { planShellAuthorization } from "./exec-authorization-plan.js";
import { buildAuthorizedShellCommandFromPlan } from "./exec-authorization-render.js";
const POSIX_ENV = { PATH: "/usr/bin:/bin" };
function renderOk(result: ReturnType<typeof buildAuthorizedShellCommandFromPlan>): string {
expect(result).toEqual(expect.objectContaining({ ok: true }));
if (!result.ok) {
throw new Error(result.reason);
}
return result.command;
}
describe("exec authorization renderer", () => {
it("exposes ordered top-level executable spans for pipeline candidates", async () => {
const plan = await planShellAuthorization({ command: "git diff | head", env: POSIX_ENV });
expect(plan.ok).toBe(true);
if (!plan.ok) {
return;
}
expect(
plan.groups.flatMap((group) =>
group.candidates.map((candidate) => ({
argv: candidate.sourceSegment.argv,
span: candidate.sourceStep.executableSpan,
})),
),
).toEqual([
{ argv: ["git", "diff"], span: expect.objectContaining({ startIndex: 0, endIndex: 3 }) },
{ argv: ["head"], span: expect.objectContaining({ startIndex: 11, endIndex: 15 }) },
]);
});
it("exposes wrapper payload candidates while retaining wrapper transport", async () => {
const plan = await planShellAuthorization({
command: "sh -c 'git status && head -c 16'",
env: POSIX_ENV,
});
expect(plan.ok).toBe(true);
if (!plan.ok) {
return;
}
expect(
plan.groups.flatMap((group) =>
group.candidates.map((candidate) => ({
argv: candidate.sourceSegment.argv,
executableSpan: candidate.sourceStep.executableSpan,
transport: candidate.transport,
})),
),
).toEqual([
{
argv: ["git", "status"],
executableSpan: expect.objectContaining({ startIndex: 7, endIndex: 10 }),
transport: expect.objectContaining({
kind: "shell-wrapper",
wrapperArgv: ["sh", "-c", "git status && head -c 16"],
}),
},
{
argv: ["head", "-c", "16"],
executableSpan: expect.objectContaining({ startIndex: 21, endIndex: 25 }),
transport: expect.objectContaining({
kind: "shell-wrapper",
wrapperArgv: ["sh", "-c", "git status && head -c 16"],
}),
},
]);
});
it("fails closed when POSIX safe-bin arguments contain shell expansion source", async () => {
const plan = await planShellAuthorization({
command: "rg foo src/*.ts | head -n {5,/etc/passwd} && echo ok",
env: POSIX_ENV,
});
expect(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: [null, "safeBins", null],
}),
).toEqual({ ok: false, reason: "shell expansion in safe-bin arguments" });
});
it("renders dispatch-wrapper safe-bin commands without quote-all argv rendering", async () => {
const plan = await planShellAuthorization({
command: "env rg -n needle",
env: POSIX_ENV,
});
const command = renderOk(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: ["safeBins"],
}),
);
expect(command).toBe("rg -n needle");
});
it("renders shell-wrapper payloads by preserving wrapper transport", async () => {
const plan = await planShellAuthorization({
command: "sh -c 'tr a b && head -c 16'",
env: POSIX_ENV,
});
const command = renderOk(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: ["safeBins", "safeBins"],
}),
);
expect(command).toMatch(/^sh -c '\/.+\/tr a b && \/.+\/head -c 16'$/);
});
it("preserves non-rewritten wrapper payload commands", async () => {
const plan = await planShellAuthorization({
command: "sh -c 'git status && head -c 16'",
env: POSIX_ENV,
});
const command = renderOk(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: [null, "safeBins"],
}),
);
expect(command).toMatch(/^sh -c 'git status && \/.+\/head -c 16'$/);
});
it("source-preserves arguments for enforced POSIX commands", async () => {
const plan = await planShellAuthorization({
command: "head -c 16",
env: POSIX_ENV,
});
const command = renderOk(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "enforced",
segmentSatisfiedBy: ["safeBins"],
}),
);
expect(command).toMatch(/^\/.+\/head -c 16$/);
});
it("rewrites quoted POSIX executable source spans", async () => {
const plan = await planShellAuthorization({
command: '"head" -c 16',
env: POSIX_ENV,
});
const command = renderOk(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: ["safeBins"],
}),
);
expect(command).toMatch(/^\/.+\/head -c 16$/);
});
it("fails closed for enforced POSIX commands with shell glob arguments", async () => {
const plan = await planShellAuthorization({
command: "ls *.ts",
env: POSIX_ENV,
});
expect(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "enforced",
segmentSatisfiedBy: ["allowlist"],
}),
).toEqual({ ok: false, reason: "shell expansion in enforced arguments" });
});
it("fails closed for enforced POSIX commands with tilde-expanded arguments", async () => {
const plan = await planShellAuthorization({
command: "cat ~/secret",
env: POSIX_ENV,
});
expect(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "enforced",
segmentSatisfiedBy: ["allowlist"],
}),
).toEqual({ ok: false, reason: "shell expansion in enforced arguments" });
});
it("preserves env assignment prefixes for enforced POSIX commands", async () => {
const plan = await planShellAuthorization({
command: "LIMIT=1 head -n 5",
env: POSIX_ENV,
});
const command = renderOk(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "enforced",
segmentSatisfiedBy: ["allowlist"],
}),
);
expect(command).toMatch(/^LIMIT=1 \/.+\/head -n 5$/);
});
it("fails closed for enforced shell-wrapper payload rewrites", async () => {
const plan = await planShellAuthorization({
command: "sh -c 'head -n 5'",
env: POSIX_ENV,
});
expect(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "enforced",
segmentSatisfiedBy: ["allowlist"],
}),
).toEqual({ ok: false, reason: "shell quoting required in wrapper payload" });
});
it("fails closed when shell-wrapper safe-bin rewrites would need outer quote escaping", async () => {
const dir = path.join(makeTempDir(), "safe bin dir");
fs.mkdirSync(dir);
makeExecutable(dir, "head");
const plan = await planShellAuthorization({
command: "sh -c 'head -n 5'",
env: makePathEnv(dir),
});
expect(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: ["safeBins"],
}),
).toEqual({ ok: false, reason: "shell quoting required in wrapper payload" });
});
it("fails closed when candidate metadata does not match the plan", async () => {
const plan = await planShellAuthorization({
command: "git diff | head",
env: POSIX_ENV,
});
expect(
buildAuthorizedShellCommandFromPlan({
plan,
mode: "safeBins",
segmentSatisfiedBy: ["safeBins"],
}),
).toEqual({ ok: false, reason: "segment metadata mismatch" });
});
});