Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/milestone-1-2-impl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@wasmagent/agentbom-core": minor
"@wasmagent/agentbom-autogen": patch
---

feat: complete Milestone 1 & 2 — workflow_layer validation, compliance controls, ISO 27001 & EU AI Act tests

- validateAgentBOM: full workflow_layer (action_pathway) field validation with schema descriptor entries
- checkCompliance: adds ControlResult type for per-control pass/fail structured result
- compliance.test.ts: SOC2, ISO 27001, EU AI Act Annex IV end-to-end profile coverage
- agentbom-autogen: AutoGen 0.4+ adapter now maps workflows to workflow_layer
117 changes: 117 additions & 0 deletions packages/agentbom-autogen/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,4 +162,121 @@ describe("autogen-agentbom generateAgentBOM", () => {
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});

it("maps workflow definitions to workflow_layer", () => {
const bom = generateAgentBOM({
agent_id: "ag-agent-006",
agent_name: "Workflow Agent",
workflows: [
{
workflow_id: "wf-001",
workflow_name: "Search and Summarize",
description: "Search the web and summarize results",
version: "1.0.0",
steps: [
{
step_id: "step-search",
action: "web_search",
description: "Execute web search",
allowed_tools: ["web-search"],
},
{
step_id: "step-summarize",
action: "summarize",
description: "Summarize results",
depends_on: ["step-search"],
},
],
},
],
});

expect(bom.workflow_layer).toBeDefined();
expect(bom.workflow_layer).toHaveLength(1);
expect(bom.workflow_layer?.[0].workflow_id).toBe("wf-001");
expect(bom.workflow_layer?.[0].workflow_name).toBe("Search and Summarize");
expect(bom.workflow_layer?.[0].description).toBe(
"Search the web and summarize results",
);
expect(bom.workflow_layer?.[0].version).toBe("1.0.0");
expect(bom.workflow_layer?.[0].steps).toHaveLength(2);
expect(bom.workflow_layer?.[0].steps[0].step_id).toBe("step-search");
expect(bom.workflow_layer?.[0].steps[1].depends_on).toEqual([
"step-search",
]);

const result = validateAgentBOM(bom);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});

it("omits workflow_layer when no workflows provided", () => {
const bom = generateAgentBOM({
agent_id: "ag-agent-007",
agent_name: "No Workflow Agent",
});
expect(bom.workflow_layer).toBeUndefined();

const result = validateAgentBOM(bom);
expect(result.valid).toBe(true);
});

it("maps multiple workflows for multi-agent AutoGen usage", () => {
const bom = generateAgentBOM({
agent_id: "ag-multiagent-001",
agent_name: "AutoGen Group Chat Orchestrator",
agent_version: "0.4.0",
deployment_context: "production",
tools: [
{
name: "code_executor",
permissions: ["process:exec", "fs:write"],
risk_signals: ["code_execution"],
},
{
name: "web_browser",
permissions: ["network:outbound"],
},
],
workflows: [
{
workflow_id: "wf-code-review",
workflow_name: "Code Review Pipeline",
steps: [
{ step_id: "write", action: "code_executor" },
{ step_id: "test", action: "code_executor", depends_on: ["write"] },
{
step_id: "review",
action: "decision",
depends_on: ["test"],
allowed_tools: [],
},
],
},
{
workflow_id: "wf-research",
workflow_name: "Research Pipeline",
steps: [
{
step_id: "search",
action: "web_browser",
allowed_tools: ["web-browser"],
},
{
step_id: "summarize",
action: "decision",
depends_on: ["search"],
},
],
},
],
});

expect(bom.workflow_layer).toHaveLength(2);
expect(bom.tool_layer).toHaveLength(2);

const result = validateAgentBOM(bom);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
});
60 changes: 60 additions & 0 deletions packages/agentbom-autogen/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ export interface AutoGenAgentConfig {
granted_scopes?: string[];
data_access?: string[];
credential_references?: string[];
/** Workflow / action pathway definitions. */
workflows?: AutoGenWorkflowConfig[];
}

export interface AutoGenWorkflowStep {
/** Unique step identifier within the workflow. */
step_id: string;
/** Action to perform (tool_id, prompt name, sub_workflow, or decision). */
action: string;
/** Human-readable step description. */
description?: string;
/** Step IDs this step depends on. */
depends_on?: string[];
/** Tool IDs allowed at this step. */
allowed_tools?: string[];
}

export interface AutoGenWorkflowConfig {
/** Unique workflow identifier. */
workflow_id: string;
/** Human-readable workflow name. */
workflow_name: string;
/** Workflow description. */
description?: string;
/** Workflow version (semver). */
version?: string;
/** Ordered steps. */
steps: AutoGenWorkflowStep[];
}

/** The shape of a generated AgentBOM document. */
Expand Down Expand Up @@ -92,6 +120,19 @@ export interface AgentBOMRecord {
data_access?: string[];
credential_references?: string[];
};
workflow_layer?: Array<{
workflow_id: string;
workflow_name: string;
description?: string;
version?: string;
steps: Array<{
step_id: string;
action: string;
description?: string;
depends_on?: string[];
allowed_tools?: string[];
}>;
}>;
attestation: {
generator: string;
generator_version: string;
Expand Down Expand Up @@ -185,6 +226,25 @@ export function generateAgentBOM(config: AutoGenAgentConfig): AgentBOMRecord {
},
}
: {}),
...(config.workflows?.length
? {
workflow_layer: config.workflows.map((wf) => ({
workflow_id: wf.workflow_id,
workflow_name: wf.workflow_name,
...(wf.description ? { description: wf.description } : {}),
...(wf.version ? { version: wf.version } : {}),
steps: wf.steps.map((s) => ({
step_id: s.step_id,
action: s.action,
...(s.description ? { description: s.description } : {}),
...(s.depends_on?.length ? { depends_on: s.depends_on } : {}),
...(s.allowed_tools?.length
? { allowed_tools: s.allowed_tools }
: {}),
})),
})),
}
: {}),
attestation: {
generator: GENERATOR,
generator_version: GENERATOR_VERSION,
Expand Down
17 changes: 10 additions & 7 deletions packages/agentbom-cli/src/agentbom-diff.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import {
diffAgentBOM,
formatAgentBOMDiff,
validateAgentBOM,
} from '@wasmagent/agentbom-core';
} from "@wasmagent/agentbom-core";

export function diffAgentBOMCommand(oldFilePath: string, newFilePath: string): number {
export function diffAgentBOMCommand(
oldFilePath: string,
newFilePath: string,
): number {
const oldPath = resolve(oldFilePath);
const newPath = resolve(newFilePath);

let oldRaw: string;
try {
oldRaw = readFileSync(oldPath, 'utf-8');
oldRaw = readFileSync(oldPath, "utf-8");
} catch {
console.error(`Error: cannot read file "${oldPath}"`);
return 1;
}

let newRaw: string;
try {
newRaw = readFileSync(newPath, 'utf-8');
newRaw = readFileSync(newPath, "utf-8");
} catch {
console.error(`Error: cannot read file "${newPath}"`);
return 1;
Expand Down Expand Up @@ -64,7 +67,7 @@ export function diffAgentBOMCommand(oldFilePath: string, newFilePath: string): n
const newBom = newData as Record<string, unknown>;
const diff = diffAgentBOM(oldBom, newBom);

console.log('Comparing AgentBOMs:');
console.log("Comparing AgentBOMs:");
console.log(` old: ${oldPath}`);
console.log(` new: ${newPath}`);
console.log();
Expand Down
8 changes: 4 additions & 4 deletions packages/agentbom-cli/src/agentbom-inspect.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { inspectAgentBOM, validateAgentBOM } from '@wasmagent/agentbom-core';
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { inspectAgentBOM, validateAgentBOM } from "@wasmagent/agentbom-core";

export function inspectAgentBOMCommand(filePath: string): number {
const resolvedPath = resolve(filePath);

let raw: string;
try {
raw = readFileSync(resolvedPath, 'utf-8');
raw = readFileSync(resolvedPath, "utf-8");
} catch {
console.error(`Error: cannot read file "${resolvedPath}"`);
return 1;
Expand Down
39 changes: 25 additions & 14 deletions packages/agentbom-cli/src/agentbom-pipeline.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { resolve } from 'node:path';
import { type PipelineConfig, runPipeline } from '@wasmagent/agentbom-core/pipeline';
import { resolve } from "node:path";
import {
type PipelineConfig,
runPipeline,
} from "@wasmagent/agentbom-core/pipeline";

/**
* `agentbom pipeline <path> [--partitions N] [--no-incremental]`
Expand All @@ -10,8 +13,10 @@ import { type PipelineConfig, runPipeline } from '@wasmagent/agentbom-core/pipel
*/
export async function agentbomPipelineCommand(args: string[]): Promise<number> {
if (args.length < 1) {
console.error('Error: agentbom pipeline requires a <path> argument');
console.error('Usage: agentbom pipeline <path> [--partitions N] [--no-incremental]');
console.error("Error: agentbom pipeline requires a <path> argument");
console.error(
"Usage: agentbom pipeline <path> [--partitions N] [--no-incremental]",
);
return 1;
}

Expand All @@ -22,15 +27,17 @@ export async function agentbomPipelineCommand(args: string[]): Promise<number> {
let i = 1;
while (i < args.length) {
const flag = args[i];
if (flag === '--partitions' && args[i + 1] !== undefined) {
if (flag === "--partitions" && args[i + 1] !== undefined) {
const n = Number.parseInt(args[i + 1], 10);
if (Number.isNaN(n) || n < 1) {
console.error(`Error: --partitions must be a positive integer, got "${args[i + 1]}"`);
console.error(
`Error: --partitions must be a positive integer, got "${args[i + 1]}"`,
);
return 1;
}
config.partitionCount = n;
i += 2;
} else if (flag === '--no-incremental') {
} else if (flag === "--no-incremental") {
config.emitIncremental = false;
i += 1;
} else {
Expand All @@ -40,8 +47,8 @@ export async function agentbomPipelineCommand(args: string[]): Promise<number> {
}

const partLabel = config.partitionCount
? ` (${config.partitionCount} partition${config.partitionCount > 1 ? 's' : ''})`
: '';
? ` (${config.partitionCount} partition${config.partitionCount > 1 ? "s" : ""})`
: "";
console.log(`Processing BOM artifacts from: ${filePath}${partLabel}`);

const wallStart = Date.now();
Expand All @@ -50,7 +57,7 @@ export async function agentbomPipelineCommand(args: string[]): Promise<number> {

// Per-artifact results
for (const result of results) {
const status = result.valid ? '✓' : '✗';
const status = result.valid ? "✓" : "✗";
console.log(
` ${status} ${result.artifactId} — ${result.durationMs.toFixed(1)}ms — ${result.sizeBytes} bytes`,
);
Expand All @@ -59,22 +66,26 @@ export async function agentbomPipelineCommand(args: string[]): Promise<number> {
console.log(` ${err}`);
}
if (result.validation.errors.length > 5) {
console.log(` ... and ${result.validation.errors.length - 5} more errors`);
console.log(
` ... and ${result.validation.errors.length - 5} more errors`,
);
}
}
}

// Summary
console.log();
console.log('Pipeline summary:');
console.log("Pipeline summary:");
console.log(` Artifacts: ${metrics.totalProcessed}`);
console.log(` Errors: ${metrics.totalErrors}`);
console.log(` Bytes: ${metrics.totalBytesProcessed}`);
console.log(` Duration: ${metrics.durationMs.toFixed(1)}ms (wall: ${wallMs}ms)`);
console.log(
` Duration: ${metrics.durationMs.toFixed(1)}ms (wall: ${wallMs}ms)`,
);
console.log(` Peak heap: ${metrics.peakMemoryBytes} bytes`);

if (config.partitionCount && config.partitionCount > 1) {
console.log(' Partition counts:');
console.log(" Partition counts:");
for (const [p, count] of metrics.partitionCounts) {
console.log(` Partition ${p}: ${count}`);
}
Expand Down
Loading
Loading