Add AST-based security middleware and enforcement wiring
This commit is contained in:
@@ -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"],
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user