Add AST-based security middleware and enforcement wiring

This commit is contained in:
2026-02-23 14:21:22 -05:00
parent 9b4216dda9
commit ef2a25b5fb
28 changed files with 1936 additions and 37 deletions

View File

@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { SchemaDrivenExecutionEngine } from "../src/agents/orchestration.js";
import type { ActorExecutionResult } from "../src/agents/pipeline.js";
import { SecurityViolationError } from "../src/security/index.js";
function createManifest(): unknown {
return {
@@ -191,6 +192,7 @@ test("runs DAG pipeline with state-dependent routing and retry behavior", async
coder: async (input): Promise<ActorExecutionResult> => {
assert.match(input.prompt, /AIOPS-123/);
assert.deepEqual(input.toolClearance.allowlist, ["read_file", "write_file"]);
assert.ok(input.security);
coderAttempts += 1;
if (coderAttempts === 1) {
return {
@@ -759,3 +761,158 @@ test("propagates abort signal into actor execution and stops the run", async ()
await assert.rejects(() => runPromise, /(AbortError|manual-abort|aborted)/i);
assert.equal(observedAbort, true);
});
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-"));
const projectContextPath = resolve(stateRoot, "project-context.json");
const manifest = {
schemaVersion: "1",
topologies: ["retry-unrolled", "sequential"],
personas: [
{
id: "coder",
displayName: "Coder",
systemPromptTemplate: "Coder",
toolClearance: {
allowlist: ["git"],
banlist: [],
},
},
],
relationships: [],
topologyConstraints: {
maxDepth: 3,
maxRetries: 2,
},
pipeline: {
entryNodeId: "secure-node",
nodes: [
{
id: "secure-node",
actorId: "secure_actor",
personaId: "coder",
},
],
edges: [],
},
} as const;
const engine = new SchemaDrivenExecutionEngine({
manifest,
settings: {
workspaceRoot,
stateRoot,
projectContextPath,
maxDepth: 3,
maxRetries: 2,
maxChildren: 2,
runtimeContext: {},
},
actorExecutors: {
secure_actor: async () => {
throw new SecurityViolationError("blocked by policy", {
code: "TOOL_NOT_ALLOWED",
});
},
},
});
await assert.rejects(
() =>
engine.runSession({
sessionId: "session-security-hard-abort",
initialPayload: {
task: "Security hard abort",
},
}),
/blocked by policy/,
);
});
test("can map security violations to validation_fail for retry-unrolled remediation", 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: ["retry-unrolled", "sequential"],
personas: [
{
id: "coder",
displayName: "Coder",
systemPromptTemplate: "Coder",
toolClearance: {
allowlist: ["git"],
banlist: [],
},
},
],
relationships: [],
topologyConstraints: {
maxDepth: 3,
maxRetries: 2,
},
pipeline: {
entryNodeId: "secure-node",
nodes: [
{
id: "secure-node",
actorId: "secure_actor",
personaId: "coder",
constraints: {
maxRetries: 1,
},
},
],
edges: [],
},
} as const;
let attempts = 0;
const engine = new SchemaDrivenExecutionEngine({
manifest,
settings: {
workspaceRoot,
stateRoot,
projectContextPath,
maxDepth: 3,
maxRetries: 2,
maxChildren: 2,
securityViolationHandling: "validation_fail",
runtimeContext: {},
},
actorExecutors: {
secure_actor: async () => {
attempts += 1;
if (attempts === 1) {
throw new SecurityViolationError("first attempt blocked", {
code: "PATH_TRAVERSAL_BLOCKED",
});
}
return {
status: "success",
payload: {
fixed: true,
},
};
},
},
});
const result = await engine.runSession({
sessionId: "session-security-validation-retry",
initialPayload: {
task: "Security retry path",
},
});
assert.equal(result.status, "success");
assert.deepEqual(
result.records.map((record) => `${record.nodeId}:${record.status}:${String(record.attempt)}`),
["secure-node:validation_fail:1", "secure-node:success:2"],
);
});