a
This commit is contained in:
@@ -25,6 +25,7 @@ test("loads defaults and freezes config", () => {
|
||||
"session.failed",
|
||||
]);
|
||||
assert.equal(config.provider.openAiAuthMode, "auto");
|
||||
assert.equal(config.provider.claudeMaxTurns, 2);
|
||||
assert.equal(config.provider.claudeObservability.mode, "off");
|
||||
assert.equal(config.provider.claudeObservability.verbosity, "summary");
|
||||
assert.equal(config.provider.claudeObservability.logPath, ".ai_ops/events/claude-trace.ndjson");
|
||||
@@ -55,6 +56,11 @@ test("validates security violation mode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("loads dangerous_warn_only security violation mode", () => {
|
||||
const config = loadConfig({ AGENT_SECURITY_VIOLATION_MODE: "dangerous_warn_only" });
|
||||
assert.equal(config.security.violationHandling, "dangerous_warn_only");
|
||||
});
|
||||
|
||||
test("validates runtime discord severity mode", () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ AGENT_RUNTIME_DISCORD_MIN_SEVERITY: "verbose" }),
|
||||
@@ -69,6 +75,13 @@ test("validates claude observability mode", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("validates CLAUDE_MAX_TURNS bounds", () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ CLAUDE_MAX_TURNS: "0" }),
|
||||
/CLAUDE_MAX_TURNS must be an integer >= 1/,
|
||||
);
|
||||
});
|
||||
|
||||
test("validates claude observability verbosity", () => {
|
||||
assert.throws(
|
||||
() => loadConfig({ CLAUDE_OBSERVABILITY_VERBOSITY: "verbose" }),
|
||||
|
||||
@@ -380,6 +380,7 @@ test("injects resolved mcp/helpers and enforces Claude tool gate in actor execut
|
||||
);
|
||||
assert.deepEqual(allow, {
|
||||
behavior: "allow",
|
||||
updatedInput: {},
|
||||
toolUseID: "allow-1",
|
||||
});
|
||||
|
||||
@@ -997,6 +998,7 @@ test("createClaudeCanUseTool accepts tool casing differences from providers", as
|
||||
});
|
||||
assert.deepEqual(allow, {
|
||||
behavior: "allow",
|
||||
updatedInput: {},
|
||||
toolUseID: "allow-bash",
|
||||
});
|
||||
|
||||
@@ -1020,6 +1022,88 @@ test("createClaudeCanUseTool accepts tool casing differences from providers", as
|
||||
assert.equal(result.status, "success");
|
||||
});
|
||||
|
||||
test("dangerous_warn_only allows tool use outside persona allowlist", async () => {
|
||||
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
||||
const projectContextPath = resolve(stateRoot, "project-context.json");
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: "1",
|
||||
topologies: ["sequential"],
|
||||
personas: [
|
||||
{
|
||||
id: "reader",
|
||||
displayName: "Reader",
|
||||
systemPromptTemplate: "Reader",
|
||||
toolClearance: {
|
||||
allowlist: ["read_file"],
|
||||
banlist: [],
|
||||
},
|
||||
},
|
||||
],
|
||||
relationships: [],
|
||||
topologyConstraints: {
|
||||
maxDepth: 2,
|
||||
maxRetries: 0,
|
||||
},
|
||||
pipeline: {
|
||||
entryNodeId: "warn-node",
|
||||
nodes: [
|
||||
{
|
||||
id: "warn-node",
|
||||
actorId: "warn_actor",
|
||||
personaId: "reader",
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
},
|
||||
} as const;
|
||||
|
||||
const engine = new SchemaDrivenExecutionEngine({
|
||||
manifest,
|
||||
settings: {
|
||||
workspaceRoot,
|
||||
stateRoot,
|
||||
projectContextPath,
|
||||
maxChildren: 1,
|
||||
maxDepth: 2,
|
||||
maxRetries: 0,
|
||||
securityViolationHandling: "dangerous_warn_only",
|
||||
runtimeContext: {},
|
||||
},
|
||||
actorExecutors: {
|
||||
warn_actor: async (input) => {
|
||||
const canUseTool = input.mcp.createClaudeCanUseTool();
|
||||
const allow = await canUseTool("Bash", {}, {
|
||||
signal: new AbortController().signal,
|
||||
toolUseID: "allow-bash-warn",
|
||||
});
|
||||
assert.deepEqual(allow, {
|
||||
behavior: "allow",
|
||||
updatedInput: {},
|
||||
toolUseID: "allow-bash-warn",
|
||||
});
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
payload: {
|
||||
ok: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await engine.runSession({
|
||||
sessionId: "session-dangerous-warn-only",
|
||||
initialPayload: {
|
||||
task: "verify warn-only bypass",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "success");
|
||||
});
|
||||
|
||||
test("hard-aborts pipeline on security violations by default", async () => {
|
||||
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-workspace-"));
|
||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-session-state-"));
|
||||
|
||||
@@ -160,6 +160,7 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
||||
ANTHROPIC_API_KEY: "legacy-api-key",
|
||||
CLAUDE_MODEL: "claude-sonnet-4-6",
|
||||
CLAUDE_CODE_PATH: "/usr/local/bin/claude",
|
||||
CLAUDE_MAX_TURNS: "5",
|
||||
});
|
||||
|
||||
let closed = false;
|
||||
@@ -229,6 +230,7 @@ test("runClaudePrompt wires auth env, stream parsing, and output", async () => {
|
||||
assert.equal(queryInput?.prompt, "augmented prompt");
|
||||
assert.equal(queryInput?.options?.model, "claude-sonnet-4-6");
|
||||
assert.equal(queryInput?.options?.pathToClaudeCodeExecutable, "/usr/local/bin/claude");
|
||||
assert.equal(queryInput?.options?.maxTurns, 5);
|
||||
assert.equal(queryInput?.options?.cwd, "/tmp/claude-worktree");
|
||||
assert.equal(queryInput?.options?.authToken, "oauth-token");
|
||||
assert.deepEqual(queryInput?.options?.mcpServers, sessionContext.mcp.claudeMcpServers);
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseActorExecutionResultFromModelOutput } from "../src/ui/provider-executor.js";
|
||||
import { mkdtemp } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { resolve } from "node:path";
|
||||
import { loadConfig } from "../src/config.js";
|
||||
import type { ActorExecutionInput } from "../src/agents/pipeline.js";
|
||||
import {
|
||||
buildProviderRuntimeEnv,
|
||||
createProviderRunRuntime,
|
||||
parseActorExecutionResultFromModelOutput,
|
||||
resolveProviderWorkingDirectory,
|
||||
type ProviderRunRuntime,
|
||||
} from "../src/ui/provider-executor.js";
|
||||
|
||||
test("parseActorExecutionResultFromModelOutput parses strict JSON payload", () => {
|
||||
const parsed = parseActorExecutionResultFromModelOutput({
|
||||
@@ -64,3 +75,71 @@ test("parseActorExecutionResultFromModelOutput falls back when response is not J
|
||||
assert.equal(parsed.status, "success");
|
||||
assert.equal(parsed.payload?.assistantResponse, "Implemented update successfully.");
|
||||
});
|
||||
|
||||
test("resolveProviderWorkingDirectory reads cwd from actor execution context", () => {
|
||||
const actorInput = {
|
||||
executionContext: {
|
||||
security: {
|
||||
worktreePath: "/tmp/session/tasks/product-intake",
|
||||
},
|
||||
},
|
||||
} as unknown as ActorExecutionInput;
|
||||
|
||||
assert.equal(
|
||||
resolveProviderWorkingDirectory(actorInput),
|
||||
"/tmp/session/tasks/product-intake",
|
||||
);
|
||||
});
|
||||
|
||||
test("buildProviderRuntimeEnv scopes AGENT_WORKTREE_PATH to actor worktree and filters undefined auth", () => {
|
||||
const config = loadConfig({
|
||||
CLAUDE_CODE_OAUTH_TOKEN: "oauth-token",
|
||||
});
|
||||
const runtime = {
|
||||
provider: "claude",
|
||||
config,
|
||||
sharedEnv: {
|
||||
PATH: "/usr/bin",
|
||||
KEEP_ME: "1",
|
||||
},
|
||||
claudeObservability: {} as ProviderRunRuntime["claudeObservability"],
|
||||
close: async () => {},
|
||||
} satisfies ProviderRunRuntime;
|
||||
const actorInput = {
|
||||
executionContext: {
|
||||
security: {
|
||||
worktreePath: "/tmp/session/tasks/product-intake",
|
||||
},
|
||||
},
|
||||
} as unknown as ActorExecutionInput;
|
||||
|
||||
const env = buildProviderRuntimeEnv({
|
||||
runtime,
|
||||
actorInput,
|
||||
includeClaudeAuth: true,
|
||||
});
|
||||
|
||||
assert.equal(env.AGENT_WORKTREE_PATH, "/tmp/session/tasks/product-intake");
|
||||
assert.equal(env.CLAUDE_CODE_OAUTH_TOKEN, "oauth-token");
|
||||
assert.equal("ANTHROPIC_API_KEY" in env, false);
|
||||
assert.equal(env.KEEP_ME, "1");
|
||||
});
|
||||
|
||||
test("createProviderRunRuntime does not require session context provisioning", async () => {
|
||||
const observabilityRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-provider-runtime-"));
|
||||
const runtime = await createProviderRunRuntime({
|
||||
provider: "claude",
|
||||
config: loadConfig({}),
|
||||
observabilityRootPath: observabilityRoot,
|
||||
baseEnv: {
|
||||
PATH: "/usr/bin",
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(runtime.provider, "claude");
|
||||
assert.equal(runtime.sharedEnv.PATH, "/usr/bin");
|
||||
} finally {
|
||||
await runtime.close();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -111,6 +111,42 @@ test("rules engine enforces binary allowlist, tool policy, and path boundaries",
|
||||
);
|
||||
});
|
||||
|
||||
test("rules engine dangerous_warn_only logs but does not block violating shell commands", async () => {
|
||||
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-warn-worktree-"));
|
||||
const stateRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-warn-state-"));
|
||||
const projectContextPath = resolve(stateRoot, "project-context.json");
|
||||
|
||||
const rules = new SecurityRulesEngine(
|
||||
{
|
||||
allowedBinaries: ["git"],
|
||||
worktreeRoot,
|
||||
protectedPaths: [stateRoot, projectContextPath],
|
||||
requireCwdWithinWorktree: true,
|
||||
rejectRelativePathTraversal: true,
|
||||
enforcePathBoundaryOnArguments: true,
|
||||
allowedEnvAssignments: [],
|
||||
blockedEnvAssignments: [],
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
violationHandling: "dangerous_warn_only",
|
||||
},
|
||||
);
|
||||
|
||||
const validated = await rules.validateShellCommand({
|
||||
command: "unauthorized_bin --version",
|
||||
cwd: worktreeRoot,
|
||||
toolClearance: {
|
||||
allowlist: ["git"],
|
||||
banlist: [],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(validated.cwd, worktreeRoot);
|
||||
assert.equal(validated.parsed.commandCount, 0);
|
||||
assert.deepEqual(validated.parsed.commands, []);
|
||||
});
|
||||
|
||||
test("secure executor runs with explicit env policy", async () => {
|
||||
const worktreeRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-security-exec-"));
|
||||
|
||||
@@ -193,3 +229,47 @@ test("rules engine carries session context in tool audit events", () => {
|
||||
assert.equal(allowedEvent.nodeId, "node-ctx");
|
||||
assert.equal(allowedEvent.attempt, 2);
|
||||
});
|
||||
|
||||
test("rules engine applies tool clearance matching case-insensitively", () => {
|
||||
const rules = new SecurityRulesEngine({
|
||||
allowedBinaries: ["git"],
|
||||
worktreeRoot: "/tmp",
|
||||
protectedPaths: [],
|
||||
requireCwdWithinWorktree: true,
|
||||
rejectRelativePathTraversal: true,
|
||||
enforcePathBoundaryOnArguments: true,
|
||||
allowedEnvAssignments: [],
|
||||
blockedEnvAssignments: [],
|
||||
});
|
||||
|
||||
assert.doesNotThrow(() =>
|
||||
rules.assertToolInvocationAllowed({
|
||||
tool: "Bash",
|
||||
toolClearance: {
|
||||
allowlist: ["bash", "glob"],
|
||||
banlist: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
assert.throws(
|
||||
() =>
|
||||
rules.assertToolInvocationAllowed({
|
||||
tool: "Glob",
|
||||
toolClearance: {
|
||||
allowlist: ["bash", "glob"],
|
||||
banlist: ["GLOB"],
|
||||
},
|
||||
}),
|
||||
(error: unknown) =>
|
||||
error instanceof SecurityViolationError && error.code === "TOOL_BANNED",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
rules.filterAllowedTools(["Bash", "Glob", "Read"], {
|
||||
allowlist: ["bash", "glob"],
|
||||
banlist: ["gLoB"],
|
||||
}),
|
||||
["Bash"],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -228,3 +228,60 @@ test("session worktree manager recreates a task worktree after stale metadata pr
|
||||
const stats = await stat(recreatedTaskWorktreePath);
|
||||
assert.equal(stats.isDirectory(), true);
|
||||
});
|
||||
|
||||
test("session worktree manager applies target path sparse checkout and task working directory", async () => {
|
||||
const root = await mkdtemp(resolve(tmpdir(), "ai-ops-session-worktree-target-"));
|
||||
const projectPath = resolve(root, "project");
|
||||
const worktreeRoot = resolve(root, "worktrees");
|
||||
|
||||
await mkdir(resolve(projectPath, "app", "src"), { recursive: true });
|
||||
await mkdir(resolve(projectPath, "infra"), { recursive: true });
|
||||
await git(["init", projectPath]);
|
||||
await git(["-C", projectPath, "config", "user.name", "AI Ops"]);
|
||||
await git(["-C", projectPath, "config", "user.email", "ai-ops@example.local"]);
|
||||
await writeFile(resolve(projectPath, "app", "src", "index.ts"), "export const app = true;\n", "utf8");
|
||||
await writeFile(resolve(projectPath, "infra", "notes.txt"), "infra\n", "utf8");
|
||||
await git(["-C", projectPath, "add", "."]);
|
||||
await git(["-C", projectPath, "commit", "-m", "initial commit"]);
|
||||
|
||||
const manager = new SessionWorktreeManager({
|
||||
worktreeRoot,
|
||||
baseRef: "HEAD",
|
||||
targetPath: "app",
|
||||
});
|
||||
|
||||
const sessionId = "session-target-1";
|
||||
const baseWorkspacePath = manager.resolveBaseWorkspacePath(sessionId);
|
||||
await manager.initializeSessionBaseWorkspace({
|
||||
sessionId,
|
||||
projectPath,
|
||||
baseWorkspacePath,
|
||||
});
|
||||
|
||||
const baseWorkingDirectory = manager.resolveWorkingDirectoryForWorktree(baseWorkspacePath);
|
||||
assert.equal(baseWorkingDirectory, resolve(baseWorkspacePath, "app"));
|
||||
const baseWorkingStats = await stat(baseWorkingDirectory);
|
||||
assert.equal(baseWorkingStats.isDirectory(), true);
|
||||
await assert.rejects(() => stat(resolve(baseWorkspacePath, "infra")), {
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
const ensured = await manager.ensureTaskWorktree({
|
||||
sessionId,
|
||||
taskId: "task-target-1",
|
||||
baseWorkspacePath,
|
||||
});
|
||||
assert.equal(ensured.taskWorkingDirectory, resolve(ensured.taskWorktreePath, "app"));
|
||||
|
||||
await writeFile(resolve(ensured.taskWorkingDirectory, "src", "feature.ts"), "export const feature = true;\n", "utf8");
|
||||
|
||||
const mergeOutcome = await manager.mergeTaskIntoBase({
|
||||
taskId: "task-target-1",
|
||||
baseWorkspacePath,
|
||||
taskWorktreePath: ensured.taskWorktreePath,
|
||||
});
|
||||
assert.equal(mergeOutcome.kind, "success");
|
||||
|
||||
const merged = await readFile(resolve(baseWorkingDirectory, "src", "feature.ts"), "utf8");
|
||||
assert.equal(merged, "export const feature = true;\n");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user