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
6 changes: 3 additions & 3 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
"preview": "bunx opennextjs-cloudflare build && bunx wrangler dev"
},
"dependencies": {
"@wasmagent/react": "^1.0.0",
"@wasmagent/ui-cards": "^1.0.0",
"@wasmagent/ui-cards-react": "^1.0.0",
"@wasmagent/react": "^1.4.1",
"@wasmagent/ui-cards": "^1.0.3",
"@wasmagent/ui-cards-react": "^1.0.3",
"@monaco-editor/react": "^4.6.0",
"@opennextjs/cloudflare": "^1.19.11",
"@terrastruct/d2": "^0.1.33",
Expand Down
28 changes: 14 additions & 14 deletions apps/worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@
"@hono/node-server": "^2.0.4",
"@jitl/quickjs-wasmfile-release-sync": "0.32.0",
"@modelcontextprotocol/sdk": ">=1.7.0",
"@wasmagent/aep": "^1.2.0",
"@wasmagent/agent-prompts": "^1.0.0",
"@wasmagent/core": "^1.0.1",
"@wasmagent/kernel-pyodide": "^1.0.0",
"@wasmagent/kernel-quickjs": "^1.0.0",
"@wasmagent/kernel-remote": "^1.0.0",
"@wasmagent/mcp-server": "^1.1.0",
"@wasmagent/model-anthropic": "^1.0.0",
"@wasmagent/model-deepseek": "^1.0.0",
"@wasmagent/model-doubao": "^1.0.0",
"@wasmagent/model-openai": "^1.0.0",
"@wasmagent/model-zhipu": "^1.0.0",
"@wasmagent/tools-browser": "^1.0.0",
"@wasmagent/tools-rag": "^1.0.0",
"@wasmagent/aep": "^2.3.0",
"@wasmagent/agent-prompts": "^1.0.3",
"@wasmagent/core": "^3.3.0",
"@wasmagent/kernel-pyodide": "^1.0.11",
"@wasmagent/kernel-quickjs": "^1.2.8",
"@wasmagent/kernel-remote": "^1.3.12",
"@wasmagent/mcp-server": "^1.1.9",
"@wasmagent/model-anthropic": "^2.0.4",
"@wasmagent/model-deepseek": "^2.0.4",
"@wasmagent/model-doubao": "^2.0.4",
"@wasmagent/model-openai": "^2.0.4",
"@wasmagent/model-zhipu": "^2.0.4",
"@wasmagent/tools-browser": "^1.0.11",
"@wasmagent/tools-rag": "^1.0.11",
"diff": "^9.0.0",
"hono": "^4.6.0",
"pyodide": "0.29.4",
Expand Down
6 changes: 3 additions & 3 deletions apps/worker/scripts/test-aep-roundtrip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ async function main() {
});

// ── Step 2: Validate structure ────────────────────────────────────────────────
if (record.schema_version !== "aep/v0.2") {
console.error("[test-aep-roundtrip] FAIL: schema_version is not aep/v0.2");
if (record.schema_version !== "aep/v0.3") {
console.error("[test-aep-roundtrip] FAIL: schema_version is not aep/v0.3");
process.exit(1);
}
if (!record.signature || record.signature.sig === "UNSIGNED_PLACEHOLDER") {
Expand Down Expand Up @@ -91,7 +91,7 @@ async function main() {
const fixtureLine = (fixtureContent as { default: string }).default.trim().split("\n")[0];
const fixtureRecord = JSON.parse(fixtureLine);
if (
fixtureRecord.schema_version !== "aep/v0.2" ||
fixtureRecord.schema_version !== "aep/v0.3" ||
!fixtureRecord.signature ||
fixtureRecord.signature.sig === "UNSIGNED_PLACEHOLDER"
) {
Expand Down
2 changes: 1 addition & 1 deletion apps/worker/src/__fixtures__/aep-shared.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"schema_version":"aep/v0.2","run_id":"fixture-run-aep-shared-001","model_provider":"test-provider","model_id":"test-model-v1","input_refs":[{"uri":"task://fixture-task-001","taint_labels":[]}],"output_refs":[{"uri":"file://dist/main.js","digest":"sha256:deadbeef"}],"capability_decisions":[{"capability":"bash","subject":"bscode-agent","resource":"bscode-workspace","decision":"allow","reason_code":"auto_derived"}],"actions":[{"action_id":"action-0","tool_name":"read_file","state_changing":false,"evidence_refs":[],"timestamp_ms":1700000001000},{"action_id":"action-1","tool_name":"bash","state_changing":true,"evidence_refs":[],"timestamp_ms":1700000002000}],"verifier_results":[{"verifier_id":"bscode-build-verifier","passed":true,"score":1,"claim_ids":[]}],"budget_ledger":{"tool_budget":{"spent":2}},"created_at_ms":1700000000000,"signature":{"alg":"ed25519","key_id":"bscode-aep-key-v1","sig":"grK3DFEKzWKn4eckEI5j9HPIn1C7bADPaU9BRRHgWQvKIfxDiaaeHG7LsoRTe7JqLevjwnftWaxGhhqWLieOAg=="}}
{"schema_version":"aep/v0.3","run_id":"fixture-run-aep-shared-001","model_provider":"test-provider","model_id":"test-model-v1","input_refs":[{"uri":"task://fixture-task-001","taint_labels":[]}],"output_refs":[{"uri":"file://dist/main.js","digest":"sha256:deadbeef"}],"capability_decisions":[{"capability":"bash","subject":"bscode-agent","resource":"bscode-workspace","decision":"allow","reason_code":"auto_derived","approval_mode":"policy-allow-with-receipt"}],"actions":[{"action_id":"action-0","tool_name":"read_file","state_changing":false,"evidence_refs":[],"timestamp_ms":1700000001000,"recording_mode":"validation","side_effect_class":"read"},{"action_id":"action-1","tool_name":"bash","state_changing":true,"evidence_refs":[],"timestamp_ms":1700000002000,"recording_mode":"full","side_effect_class":"mutate-external"}],"verifier_results":[{"verifier_id":"bscode-build-verifier","passed":true,"score":1,"claim_ids":[]}],"budget_ledger":{"tool_budget":{"spent":2}},"created_at_ms":1700000000000,"prev_record_hash":null,"run_side_effect_class_max":"mutate-external","signature":{"alg":"ed25519","key_id":"bscode-aep-key-v1","sig":"xSyJ1UNqpDRLaeQxnSaQxfntzt4b4Jb77XPvwL4zEL5WKn+yqxoY68YWyZXh4W43cli0SKpuwXEYB8sAZ8opBw=="}}
48 changes: 45 additions & 3 deletions apps/worker/src/trajectoryExport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,15 +331,15 @@ describe("buildAEPEvidence", () => {
{ event: "tool_result" as const, data: { exit_code: 0 }, timestamp_ms: 1003 },
];

it("returns a signed AEPRecord with schema_version aep/v0.2", async () => {
it("returns a signed AEPRecord with schema_version aep/v0.3", async () => {
const record = await buildAEPEvidence({
run_id: "run-aep-test-01",
model_id: "test-model",
tool_calls: toolCalls,
objective_passed: true,
created_at_ms: 1_700_000_000_000,
});
expect(record.schema_version).toBe("aep/v0.2");
expect(record.schema_version).toBe("aep/v0.3");
expect(record.run_id).toBe("run-aep-test-01");
expect(record.model_id).toBe("test-model");
});
Expand Down Expand Up @@ -424,6 +424,42 @@ describe("buildAEPEvidence", () => {
expect(record.capability_decisions[0].capability).toBe("bash");
expect(record.capability_decisions[0].decision).toBe("allow");
expect(record.capability_decisions[0].reason_code).toBe("auto_derived");
// AEP v0.3: an executed tool is an allow backed by a policy receipt.
expect(record.capability_decisions[0].approval_mode).toBe("policy-allow-with-receipt");
});

it("classifies side_effect_class and recording_mode per AEP v0.3", async () => {
const record = await buildAEPEvidence({
run_id: "run-side-effect-test",
model_id: "test-model",
tool_calls: toolCalls,
objective_passed: null,
created_at_ms: 1_700_000_000_000,
});
// Fixture has one read_file (read-only) and one bash (shells out) call.
const readAction = record.actions.find((a) => a.tool_name === "read_file");
const bashAction = record.actions.find((a) => a.tool_name === "bash");
expect(readAction?.side_effect_class).toBe("read");
expect(readAction?.recording_mode).toBe("validation");
expect(bashAction?.side_effect_class).toBe("mutate-external");
expect(bashAction?.recording_mode).toBe("full");
// Run-level max severity is computed by the emitter across all actions.
expect(record.run_side_effect_class_max).toBe("mutate-external");
});

it("emits deny decisions with policy-deny-with-evidence approval_mode", async () => {
const record = await buildAEPEvidence({
run_id: "run-deny-test",
model_id: "test-model",
tool_calls: toolCalls,
objective_passed: null,
denied_tools: ["curl_exfiltrate"],
created_at_ms: 1_700_000_000_000,
});
const deny = record.capability_decisions.find((d) => d.decision === "deny");
expect(deny?.capability).toBe("curl_exfiltrate");
expect(deny?.approval_mode).toBe("policy-deny-with-evidence");
expect(deny?.deny_reason_class).toBe("policy-rule");
});

it("records budget_ledger with auto-derived tool_budget", async () => {
Expand All @@ -445,7 +481,13 @@ describe("buildAEPEvidence", () => {
tool_calls: toolCalls,
objective_passed: null,
capability_decisions: [
{ capability: "write_file", subject: "agent", resource: "main.ts", decision: "allow" },
{
capability: "write_file",
subject: "agent",
resource: "main.ts",
decision: "allow",
approval_mode: "policy-allow-with-receipt",
},
],
verifier_results: [
{ verifier_id: "custom-verifier", passed: true, score: 0.9, claim_ids: ["c1"] },
Expand Down
63 changes: 62 additions & 1 deletion apps/worker/src/trajectoryExport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,49 @@ const STATE_CHANGING_TOOLS = new Set([
"npm_publish",
]);

// AEP v0.3 side-effect classification (README §"Side-Effect Classification").
// Tools that cross the process/network boundary — their effects escape the
// sandbox and are therefore the highest-severity, evidence-worthy actions.
const NETWORK_EGRESS_TOOLS = new Set(["git_push", "npm_publish"]);
// Tools that shell out — they can mutate anything reachable from the sandbox,
// so we conservatively class them as external mutation.
const MUTATE_EXTERNAL_TOOLS = new Set(["bash", "run_bash", "execute_bash"]);
// Tools that mutate only local workspace state (file edits, local commit).
const MUTATE_LOCAL_TOOLS = new Set(["write_file", "create_file", "delete_file", "git_commit"]);

type SideEffectClass = "read" | "mutate-local" | "mutate-external" | "network-egress" | "unknown";

/**
* Classify a tool's side-effect severity for AEP v0.3 `side_effect_class`.
* Unknown tool names fall back to "read" only when they are not in the
* state-changing set; anything state-changing but unclassified is "unknown"
* so the emitter's `run_side_effect_class_max` never under-reports risk.
*/
function classifySideEffect(toolName: string | undefined): SideEffectClass {
if (!toolName) return "unknown";
if (NETWORK_EGRESS_TOOLS.has(toolName)) return "network-egress";
if (MUTATE_EXTERNAL_TOOLS.has(toolName)) return "mutate-external";
if (MUTATE_LOCAL_TOOLS.has(toolName)) return "mutate-local";
if (STATE_CHANGING_TOOLS.has(toolName)) return "unknown";
return "read";
}

/**
* Choose an AEP v0.3 `recording_mode` for an action (README §"Recording
* Modes"): capture full content for high-risk actions (external mutation,
* network egress, or tainted input), a delta for local mutations, and
* digests-only ("validation") for read-only actions.
*/
function recordingModeFor(
sideEffect: SideEffectClass,
isUntrusted: boolean
): "validation" | "delta" | "full" {
if (isUntrusted) return "full";
if (sideEffect === "mutate-external" || sideEffect === "network-egress") return "full";
if (sideEffect === "mutate-local") return "delta";
return "validation";
}

/** Default AEP seed used in development / CI. Override with BSCODE_AEP_SEED env var. */
const DEV_AEP_SEED = "0".repeat(64);

Expand Down Expand Up @@ -464,13 +507,24 @@ export async function buildAEPEvidence(opts: {
const toolName = (ev.data as Record<string, unknown>).name as string | undefined;
const isStateChanging = Boolean(toolName && STATE_CHANGING_TOOLS.has(toolName));
const isUntrusted = Boolean((ev.data as Record<string, unknown>).isUntrusted);
// AEP v0.3: classify the action's blast radius and pick a recording mode
// proportional to its risk (README §"Side-Effect Classification" /
// §"Recording Modes"). Tainted inputs force full capture.
const sideEffect = classifySideEffect(toolName);
emitter.addAction({
action_id: `action-${i}`,
tool_name: toolName ?? "unknown",
state_changing: isStateChanging,
side_effect_class: sideEffect,
recording_mode: recordingModeFor(sideEffect, isUntrusted),
evidence_refs: [],
timestamp_ms: ev.timestamp_ms ?? Date.now() + i,
...(isUntrusted ? { output_taint_labels: ["external", "user-supplied"] } : {}),
...(isUntrusted
? {
input_taint_labels: ["external", "user-supplied"],
output_taint_labels: ["external", "user-supplied"],
}
: {}),
});
}
}
Expand Down Expand Up @@ -510,6 +564,9 @@ export async function buildAEPEvidence(opts: {
subject: "bscode-agent",
resource: "bscode-workspace",
decision: "allow",
// AEP v0.3: the tool actually executed, so the allow is backed by a
// policy receipt rather than an interactive one-shot approval.
approval_mode: "policy-allow-with-receipt",
reason_code: "auto_derived",
});
}
Expand All @@ -521,6 +578,10 @@ export async function buildAEPEvidence(opts: {
subject: "bscode-agent",
resource: "bscode-workspace",
decision: "deny",
// AEP v0.3: the firewall denied on a policy rule and the record
// itself is the evidence of that denial.
approval_mode: "policy-deny-with-evidence",
deny_reason_class: "policy-rule",
reason_code: "policy_denied",
});
}
Expand Down
36 changes: 18 additions & 18 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading