Files
fusion/packages/engine/src/cli-agent/__tests__/autonomy.test.ts
2026-06-05 00:30:27 -07:00

183 lines
6.2 KiB
TypeScript

/**
* Autonomy elevation detection + approval gate (U15).
*
* Central invariant under test: the posture chip / gate derive from the FULLY
* RESOLVED argv + env, so elevation smuggled through ANY channel (autonomy
* field, extra args, env additions, command override) trips the gate — not just
* the autonomy field.
*/
import { describe, expect, it } from "vitest";
import {
resolveEffectivePosture,
assertAutonomyApproved,
CliAutonomyNotApprovedError,
type CliAgentResolveSettings,
} from "../autonomy.js";
import { claudeCodeAdapter } from "../adapters/claude-code.js";
import { codexAdapter } from "../adapters/codex.js";
import { genericCliAdapter } from "../adapters/generic.js";
describe("resolveEffectivePosture", () => {
it("returns default (no elevation) for a baseline launch", () => {
const posture = resolveEffectivePosture({ adapter: claudeCodeAdapter });
expect(posture.elevated).toBe(false);
expect(posture.mode).toBe("default");
expect(posture.flags).toEqual([]);
expect(posture.adapterId).toBe("claude-code");
});
it("detects elevation from the autonomy field (autoApprove)", () => {
const posture = resolveEffectivePosture({
adapter: claudeCodeAdapter,
nodeConfig: { cliAutonomy: { autoApprove: true } },
});
expect(posture.elevated).toBe(true);
// Claude's buildLaunch emits --dangerously-skip-permissions for autoApprove,
// so it surfaces as an argv-channel marker (not merely the autonomy field).
expect(posture.flags.some((f) => f.channel === "args")).toBe(true);
expect(
posture.flags.some((f) => f.marker === "--dangerously-skip-permissions"),
).toBe(true);
});
it("BYPASS CLOSURE: --dangerously-skip-permissions via extraArgs (not the field) trips elevation", () => {
const settings: CliAgentResolveSettings = {
extraArgs: ["--dangerously-skip-permissions"],
};
const posture = resolveEffectivePosture({ adapter: claudeCodeAdapter, settings });
expect(posture.elevated).toBe(true);
expect(
posture.flags.some(
(f) => f.channel === "args" && f.marker === "--dangerously-skip-permissions",
),
).toBe(true);
});
it("detects codex -c approval_policy override smuggled via extraArgs", () => {
const posture = resolveEffectivePosture({
adapter: codexAdapter,
settings: { extraArgs: ["-c", "approval_policy=never"] },
});
expect(posture.elevated).toBe(true);
expect(posture.flags.some((f) => f.marker.includes("approval_policy"))).toBe(true);
});
it("detects autonomy-toggling env var via envAdditions", () => {
const posture = resolveEffectivePosture({
adapter: claudeCodeAdapter,
settings: { envAdditions: ["SOME_TOOL_SKIP_PERMISSIONS"] },
});
expect(posture.elevated).toBe(true);
expect(
posture.flags.some(
(f) => f.channel === "env" && f.marker === "SOME_TOOL_SKIP_PERMISSIONS",
),
).toBe(true);
});
it("does NOT flag a benign env addition", () => {
const posture = resolveEffectivePosture({
adapter: claudeCodeAdapter,
settings: { envAdditions: ["HTTP_PROXY", "NO_COLOR"] },
});
expect(posture.elevated).toBe(false);
});
it("treats a non-default command override as privileged", () => {
const posture = resolveEffectivePosture({
adapter: claudeCodeAdapter,
settings: { commandOverride: "/tmp/evil-claude" },
});
expect(posture.elevated).toBe(true);
expect(
posture.flags.some((f) => f.channel === "command" && f.marker === "/tmp/evil-claude"),
).toBe(true);
});
it("does NOT flag a command override equal to the adapter default", () => {
const posture = resolveEffectivePosture({
adapter: claudeCodeAdapter,
settings: { commandOverride: claudeCodeAdapter.defaultCommand },
});
expect(posture.elevated).toBe(false);
});
it("maps autonomyMode:elevated onto the posture even with no field", () => {
const posture = resolveEffectivePosture({
adapter: claudeCodeAdapter,
settings: { autonomyMode: "elevated" },
});
expect(posture.elevated).toBe(true);
});
it("flags generic-tier bypass args (heuristic patterns)", () => {
const posture = resolveEffectivePosture({
adapter: genericCliAdapter,
settings: { commandOverride: "mytool", extraArgs: ["--auto-approve-everything"] },
});
expect(posture.elevated).toBe(true);
});
});
describe("assertAutonomyApproved (gate)", () => {
const elevated: CliAgentResolveSettings = {
extraArgs: ["--dangerously-skip-permissions"],
};
it("permits a non-elevated launch without consulting approval", async () => {
let consulted = false;
const posture = await assertAutonomyApproved({
adapter: claudeCodeAdapter,
projectId: "p1",
isApproved: () => {
consulted = true;
return false;
},
});
expect(posture.elevated).toBe(false);
expect(consulted).toBe(false);
});
it("throws a typed error when elevated and unapproved", async () => {
await expect(
assertAutonomyApproved({
adapter: claudeCodeAdapter,
settings: elevated,
projectId: "p1",
isApproved: () => false,
}),
).rejects.toBeInstanceOf(CliAutonomyNotApprovedError);
});
it("includes the offending flags + scope on the error", async () => {
let err: unknown;
try {
await assertAutonomyApproved({
adapter: claudeCodeAdapter,
settings: elevated,
projectId: "proj-x",
isApproved: () => false,
});
} catch (e) {
err = e;
}
expect(err).toBeInstanceOf(CliAutonomyNotApprovedError);
const typed = err as CliAutonomyNotApprovedError;
expect(typed.code).toBe("CLI_AUTONOMY_NOT_APPROVED");
expect(typed.projectId).toBe("proj-x");
expect(typed.adapterId).toBe("claude-code");
expect(typed.flags.length).toBeGreaterThan(0);
});
it("permits an elevated launch once the project has approved", async () => {
const posture = await assertAutonomyApproved({
adapter: claudeCodeAdapter,
settings: elevated,
projectId: "p1",
isApproved: async ({ projectId, adapterId }) =>
projectId === "p1" && adapterId === "claude-code",
});
expect(posture.elevated).toBe(true);
});
});