From 564b8ff1ebf454d24732ad132140bab31c353478 Mon Sep 17 00:00:00 2001 From: telleroutlook Date: Tue, 28 Jul 2026 10:25:42 +0800 Subject: [PATCH 1/2] Fix #140: add artifact-attestation fixture scaffolding Create tests/fixtures/valid/artifact-attestation/basic.json and tests/fixtures/invalid/artifact-attestation/missing-required-field.json so verify-first can proceed to PROCEED when schema + index entry land. --- .../artifact-attestation/missing-required-field.json | 3 +++ tests/fixtures/valid/artifact-attestation/basic.json | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 tests/fixtures/invalid/artifact-attestation/missing-required-field.json create mode 100644 tests/fixtures/valid/artifact-attestation/basic.json diff --git a/tests/fixtures/invalid/artifact-attestation/missing-required-field.json b/tests/fixtures/invalid/artifact-attestation/missing-required-field.json new file mode 100644 index 0000000..149e014 --- /dev/null +++ b/tests/fixtures/invalid/artifact-attestation/missing-required-field.json @@ -0,0 +1,3 @@ +{ + "artifact_uri": "pkg:npm/example@1.0.0" +} diff --git a/tests/fixtures/valid/artifact-attestation/basic.json b/tests/fixtures/valid/artifact-attestation/basic.json new file mode 100644 index 0000000..a318b3e --- /dev/null +++ b/tests/fixtures/valid/artifact-attestation/basic.json @@ -0,0 +1,7 @@ +{ + "artifact_uri": "pkg:npm/example@1.0.0", + "digest": "sha256:abc123", + "produced_by_run_id": "run-123", + "tool_name": "builder", + "signature": "sig123" +} From ca655eccec8c7ca8f7e9d777f3014f83770cee03 Mon Sep 17 00:00:00 2001 From: claude-bot-go Date: Tue, 28 Jul 2026 10:31:36 +0800 Subject: [PATCH 2/2] Fix #138: Create `schemas/v0.1/canonical-event.schema.json` and `aep-record` type definition in @wasmagent/protocol --- index.d.ts | 94 ++++++++++++ schemas/index.json | 17 +++ schemas/v0.1/canonical-event.schema.json | 142 ++++++++++++++++++ src/wasmagent_protocol/__init__.py | 65 +++++++- tests/aep-record.test.js | 47 ++++++ .../invalid/canonical-event/example.json | 10 ++ .../valid/canonical-event/example.json | 25 +++ tests/test_aep_record.py | 88 +++++++++++ 8 files changed, 486 insertions(+), 2 deletions(-) create mode 100644 schemas/v0.1/canonical-event.schema.json create mode 100644 tests/aep-record.test.js create mode 100644 tests/fixtures/invalid/canonical-event/example.json create mode 100644 tests/fixtures/valid/canonical-event/example.json create mode 100644 tests/test_aep_record.py diff --git a/index.d.ts b/index.d.ts index 465117e..fea2cd3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -31,6 +31,100 @@ export const schemas: Record; */ export function getSchema(id: string): unknown; +// --------------------------------------------------------------------------- +// AEP record type (mirrors schemas/aep/aep-record.schema.json). +// +// The schema is additive/evolving: it does not set additionalProperties: false, +// so validated records may carry fields not listed here. The interfaces below +// model the documented fields; retrieve the authoritative shape with +// `getSchema('aep-record')`. +// --------------------------------------------------------------------------- + +/** Allowed `schema_version` values for an AEP record. */ +export type AepSchemaVersion = 'aep/v0.1' | 'aep/v0.2' | 'aep/v0.3'; + +/** A recorded capability decision (allow / deny / ask_user / dry_run). */ +export interface AepCapabilityDecision { + capability: string; + subject: string; + resource: string; + decision: 'allow' | 'deny' | 'ask_user' | 'dry_run'; + reason_code?: string; +} + +/** A single runtime action captured as evidence. */ +export interface AepAction { + action_id: string; + tool_name: string; + state_changing: boolean; + timestamp_ms: number; + precondition_digest?: string; + result_digest?: string; + evidence_refs?: string[]; + parent_action_id?: string; + causal_chain_id?: string; + tool_descriptor_digest?: string; + server_card_digest?: string; + scope_lease_id?: string; + approval_context_hash?: string; + input_taint_labels?: string[]; + output_taint_labels?: string[]; + memory_read_refs?: string[]; + memory_write_refs?: string[]; + pre_state_digest?: string; + post_state_digest?: string; +} + +/** Result of a verifier run over the record. */ +export interface AepVerifierResult { + verifier_id: string; + passed: boolean; + score?: number; + claim_ids?: string[]; +} + +/** Optional tamper-evident signature over the record. */ +export interface AepSignature { + alg: string; + key_id: string; + sig: string; + bundle?: unknown; + transparency_log_ref?: string; +} + +/** + * Agent Evidence Protocol record — runtime action evidence and run provenance. + * Mirrors `schemas/aep/aep-record.schema.json` (`aep/v0.3`). Only + * `schema_version`, `run_id`, and `created_at_ms` are required; every other + * field is optional and additive. + */ +export interface AepRecord { + schema_version: AepSchemaVersion; + run_id: string; + created_at_ms: number; + trace_id?: string; + parent_trace_id?: string | null; + repo_commit?: string; + runtime_version?: string; + model_provider?: string; + model_id?: string; + policy_bundle_digest?: string; + tool_manifest_digest?: string; + mcp_server_card_digest?: string | null; + capability_decisions?: AepCapabilityDecision[]; + actions?: AepAction[]; + verifier_results?: AepVerifierResult[]; + budget_ledger?: Record; + run_context?: Record; + user_id?: string; + subject_id?: string; + side_effect_class?: 'read' | 'mutate-local' | 'mutate-external' | 'network-egress' | 'unknown'; + run_side_effect_class_max?: 'read' | 'mutate-local' | 'mutate-external' | 'network-egress' | 'unknown'; + recording_mode?: 'full' | 'delta' | 'validation'; + argument_drift?: Record; + signature?: AepSignature; +} + // --------------------------------------------------------------------------- // Cross-repo schema drift detection. // --------------------------------------------------------------------------- diff --git a/schemas/index.json b/schemas/index.json index 2bc43b1..0601365 100644 --- a/schemas/index.json +++ b/schemas/index.json @@ -184,6 +184,23 @@ "open-agent-audit" ], "summary": "Trust Passport \u2014 signed, expiring, verifiable trust-state artifact for AI agents." + }, + { + "id": "canonical-event", + "title": "CanonicalEvent", + "path": "schemas/v0.1/canonical-event.schema.json", + "canonical_id": "https://wasmagent.dev/schemas/v0.1/canonical-event.schema.json", + "version": "canonical-event/v0.1", + "stability": "evolving", + "owners": [ + "open-agent-audit" + ], + "consumers": [ + "open-agent-audit", + "wasmagent-js", + "trace-pipeline" + ], + "summary": "A single observable audit event in an agent's runtime trace \u2014 the canonical event shape that producers (open-agent-audit) emit and that maps onto aep-record." } ] } diff --git a/schemas/v0.1/canonical-event.schema.json b/schemas/v0.1/canonical-event.schema.json new file mode 100644 index 0000000..7d719c0 --- /dev/null +++ b/schemas/v0.1/canonical-event.schema.json @@ -0,0 +1,142 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://wasmagent.dev/schemas/v0.1/canonical-event.schema.json", + "title": "CanonicalEvent", + "description": "A single observable event in an AI agent's runtime trace — the canonical audit-event shape emitted by open-agent-audit and other producers. Cross-repository contract: downstream repos consume this via @wasmagent/protocol and map it onto aep-record (see open-agent-audit adapter). The schema_version field identifies the event format and is independent of aep-record's schema_version.", + "type": "object", + "required": ["schema_version", "run_id", "event_id", "timestamp", "type", "actor", "agent_id", "model_id"], + "additionalProperties": false, + "properties": { + "schema_version": { + "const": "open-agent-audit/v0.1" + }, + "run_id": { "type": "string", "minLength": 1 }, + "session_id": { "type": "string" }, + "agent_id": { "type": "string", "minLength": 1 }, + "model_id": { "type": "string", "minLength": 1 }, + "event_id": { "type": "string", "minLength": 1 }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "RFC 3339 timestamp with timezone offset" + }, + "type": { + "type": "string", + "enum": [ + "tool_call", + "policy_decision", + "human_approval", + "observation", + "model_output", + "final_answer", + "error" + ] + }, + "actor": { + "type": "string", + "enum": ["agent", "user", "system", "tool", "human_reviewer"] + }, + "tool": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { "type": "string", "minLength": 1 }, + "capability": { "type": "string" }, + "args_hash": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "result_hash": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "risk_tags": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true + } + } + }, + "policy": { + "type": "object", + "additionalProperties": false, + "required": ["decision", "reason"], + "properties": { + "decision": { + "type": "string", + "enum": ["allow", "deny", "ask_user"] + }, + "reason": { "type": "string" }, + "rule_id": { "type": "string" } + } + }, + "human": { + "type": "object", + "additionalProperties": false, + "required": ["reviewer_id", "decision"], + "properties": { + "reviewer_id": { "type": "string" }, + "decision": { + "type": "string", + "enum": ["approve", "deny", "escalate"] + }, + "justification": { "type": "string" } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "message"], + "properties": { + "kind": { "type": "string" }, + "message": { "type": "string" } + } + }, + "model_output": { + "type": "object", + "additionalProperties": false, + "properties": { + "content_hash": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "token_count": { "type": "integer", "minimum": 0 }, + "finish_reason": { "type": "string" } + } + }, + "observation": { + "type": "object", + "additionalProperties": false, + "properties": { + "source": { "type": "string" }, + "content_hash": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "byte_size": { "type": "integer", "minimum": 0 } + } + }, + "evidence": { + "type": "object", + "additionalProperties": false, + "properties": { + "evidence_id": { "type": "string" }, + "hash": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "prev_hash": { "type": "string", "pattern": "^[A-Fa-f0-9]{64}$" }, + "signature": { "type": "string" }, + "signature_algorithm": { + "type": "string", + "enum": ["ed25519", "ecdsa-p256"] + }, + "signer_key_id": { "type": "string" } + } + } + }, + "allOf": [ + { + "if": { "properties": { "type": { "const": "tool_call" } }, "required": ["type"] }, + "then": { "required": ["tool"] } + }, + { + "if": { "properties": { "type": { "const": "policy_decision" } }, "required": ["type"] }, + "then": { "required": ["policy"] } + }, + { + "if": { "properties": { "type": { "const": "human_approval" } }, "required": ["type"] }, + "then": { "required": ["human"] } + }, + { + "if": { "properties": { "type": { "const": "error" } }, "required": ["type"] }, + "then": { "required": ["error"] } + } + ] +} diff --git a/src/wasmagent_protocol/__init__.py b/src/wasmagent_protocol/__init__.py index 72fd3df..12c4378 100644 --- a/src/wasmagent_protocol/__init__.py +++ b/src/wasmagent_protocol/__init__.py @@ -15,9 +15,70 @@ from functools import lru_cache from importlib import resources from pathlib import Path -from typing import Any +from typing import Any, Literal, TypedDict -__all__ = ["INDEX", "get_schema", "schema_path", "schema_ids"] +__all__ = [ + "AepRecord", + "INDEX", + "get_schema", + "schema_path", + "schema_ids", +] + + +# --------------------------------------------------------------------------- +# AEP record type (mirrors schemas/aep/aep-record.schema.json). +# +# The schema is additive/evolving and does not forbid additional properties, +# so validated records may carry fields not modelled here. These TypedDicts +# describe the documented top-level shape; retrieve the authoritative schema +# with ``get_schema("aep-record")``. Required keys live on the base class and +# the remainder are optional via ``total=False`` (Python 3.9-compatible). +# --------------------------------------------------------------------------- + +_AepSideEffectClass = Literal[ + "read", "mutate-local", "mutate-external", "network-egress", "unknown" +] + + +class _AepRecordRequired(TypedDict): + schema_version: Literal["aep/v0.1", "aep/v0.2", "aep/v0.3"] + run_id: str + created_at_ms: float + + +class AepRecord(_AepRecordRequired, total=False): + """Agent Evidence Protocol record — runtime action evidence and run provenance. + + Mirrors ``schemas/aep/aep-record.schema.json`` (``aep/v0.3``). Nested arrays + and objects (``actions``, ``capability_decisions``, ``budget_ledger`` …) are + typed as ``list[dict[str, Any]]`` / ``dict[str, Any]``; consult the schema + for their inner structure. + """ + + trace_id: str + parent_trace_id: "str | None" + repo_commit: str + runtime_version: str + model_provider: str + model_id: str + policy_bundle_digest: str + tool_manifest_digest: str + mcp_server_card_digest: "str | None" + input_refs: "list[dict[str, Any]]" + output_refs: "list[dict[str, Any]]" + capability_decisions: "list[dict[str, Any]]" + actions: "list[dict[str, Any]]" + verifier_results: "list[dict[str, Any]]" + budget_ledger: "dict[str, Any]" + run_context: "dict[str, Any]" + user_id: str + subject_id: str + side_effect_class: _AepSideEffectClass + run_side_effect_class_max: _AepSideEffectClass + recording_mode: Literal["full", "delta", "validation"] + argument_drift: "dict[str, Any]" + signature: "dict[str, Any]" _SCHEMAS_PKG = "wasmagent_protocol.schemas" # Editable / source-checkout fallback: the canonical schemas live at the repo diff --git a/tests/aep-record.test.js b/tests/aep-record.test.js new file mode 100644 index 0000000..f5c97f2 --- /dev/null +++ b/tests/aep-record.test.js @@ -0,0 +1,47 @@ +// Sample AEP record instantiation + conformance (wasmagent-protocol #138). +// +// The `AepRecord` TypeScript interface in index.d.ts documents the record +// shape; this runtime test validates a conforming sample against the canonical +// `aep-record` schema loaded through the package. Full JSON-Schema validation +// lives in the Python suite + tests/conformance.py; here we assert the sample +// carries every schema-required field and uses an accepted schema_version. +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { getSchema } from '../index.js'; + +/** Sample record typed against the exported AepRecord interface. */ +const sampleAepRecord = /** @type {import('../index.d.ts').AepRecord} */ ({ + schema_version: 'aep/v0.3', + run_id: 'run-abc123', + trace_id: 'trace-001', + runtime_version: 'wasmagent-js@1.20.0', + created_at_ms: 1737600000000, + capability_decisions: [ + { capability: 'fs.write', subject: 'agent-1', resource: '/tmp/out.txt', decision: 'allow' }, + ], + actions: [ + { action_id: 'a1', tool_name: 'write_file', state_changing: true, timestamp_ms: 1737600000100 }, + ], +}); + +test('sample AEP record satisfies every schema-required field', () => { + const schema = getSchema('aep-record'); + for (const key of schema.required) { + assert.ok(key in sampleAepRecord, `sample missing required field: ${key}`); + } +}); + +test('sample schema_version is accepted by the aep-record schema', () => { + const schema = getSchema('aep-record'); + assert.deepEqual( + schema.properties.schema_version.enum, + ['aep/v0.1', 'aep/v0.2', 'aep/v0.3'], + ); + assert.ok(schema.properties.schema_version.enum.includes(sampleAepRecord.schema_version)); +}); + +test('aep-record schema is the canonical wasmagent.dev record', () => { + const schema = getSchema('aep-record'); + assert.equal(schema.$id, 'https://wasmagent.dev/schemas/aep/aep-record.schema.json'); + assert.equal(schema.title, 'AEPRecord'); +}); diff --git a/tests/fixtures/invalid/canonical-event/example.json b/tests/fixtures/invalid/canonical-event/example.json new file mode 100644 index 0000000..ef29a3a --- /dev/null +++ b/tests/fixtures/invalid/canonical-event/example.json @@ -0,0 +1,10 @@ +{ + "schema_version": "open-agent-audit/v0.1", + "run_id": "run-abc123", + "agent_id": "agent-researcher-1", + "model_id": "claude-opus-4-8", + "event_id": "evt-0002", + "timestamp": "2026-07-28T14:03:23Z", + "type": "tool_call", + "actor": "agent" +} diff --git a/tests/fixtures/valid/canonical-event/example.json b/tests/fixtures/valid/canonical-event/example.json new file mode 100644 index 0000000..adba518 --- /dev/null +++ b/tests/fixtures/valid/canonical-event/example.json @@ -0,0 +1,25 @@ +{ + "schema_version": "open-agent-audit/v0.1", + "run_id": "run-abc123", + "session_id": "session-001", + "agent_id": "agent-researcher-1", + "model_id": "claude-opus-4-8", + "event_id": "evt-0001", + "timestamp": "2026-07-28T14:03:22Z", + "type": "tool_call", + "actor": "agent", + "tool": { + "name": "write_file", + "capability": "filesystem.write", + "args_hash": "9e6ce95e6a1e4d0f4b8b0e8b0a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d", + "result_hash": "f0e1d2c3b4a5968778695a4b3c2d1e0f1234567890abcdef1234567890abcdef", + "risk_tags": ["shell", "filesystem"] + }, + "evidence": { + "evidence_id": "ev-0001", + "hash": "1f2e3d4c5b6a79887766554433221100ffeeddccbbaa99887766554433221100", + "signature": "base64sig==", + "signature_algorithm": "ed25519", + "signer_key_id": "k1" + } +} diff --git a/tests/test_aep_record.py b/tests/test_aep_record.py new file mode 100644 index 0000000..3269104 --- /dev/null +++ b/tests/test_aep_record.py @@ -0,0 +1,88 @@ +"""AEP record type + sample-instantiation tests (wasmagent-protocol #138). + +Covers the acceptance criterion that a sample AEP record can be instantiated +using the exported protocol types and that it validates against the canonical +``aep-record`` JSON Schema. +""" + +from __future__ import annotations + +import pytest + +from wasmagent_protocol import AepRecord, get_schema + +# Full schema validation needs jsonschema (the [dev] extra). Skip the module if +# it is unavailable rather than failing collection. +jsonschema = pytest.importorskip("jsonschema") +from jsonschema import Draft202012Validator # noqa: E402 + + +def _sample_aep_record() -> AepRecord: + """A minimal-but-realistic AEP record constructed via the protocol type.""" + return { + "schema_version": "aep/v0.3", + "run_id": "run-abc123", + "trace_id": "trace-001", + "runtime_version": "wasmagent-js@1.20.0", + "created_at_ms": 1737600000000, + "capability_decisions": [ + { + "capability": "fs.write", + "subject": "agent-1", + "resource": "/tmp/out.txt", + "decision": "allow", + } + ], + "actions": [ + { + "action_id": "a1", + "tool_name": "write_file", + "state_changing": True, + "timestamp_ms": 1737600000100, + } + ], + } + + +def test_aep_record_type_is_exported(): + import wasmagent_protocol as w + + assert hasattr(w, "AepRecord") + assert "AepRecord" in w.__all__ + # The three schema-required keys are modelled on the type. + for key in ("schema_version", "run_id", "created_at_ms"): + assert key in AepRecord.__annotations__ + + +def test_sample_aep_record_is_instantiable_via_protocol_type(): + record: AepRecord = _sample_aep_record() + assert record["run_id"] == "run-abc123" + assert record["schema_version"] == "aep/v0.3" + + +def test_sample_aep_record_validates_against_schema(): + record = _sample_aep_record() + validator = Draft202012Validator(get_schema("aep-record")) + errors = sorted(validator.iter_errors(record), key=lambda e: list(e.path)) + assert not errors, "sample AepRecord should validate against the schema: " + "; ".join( + e.message for e in errors + ) + + +def test_canonical_event_schema_registered_and_validates(): + # Sanity check for the canonical-event schema added by the same change. + schema = get_schema("canonical-event") + assert schema["$id"] == "https://wasmagent.dev/schemas/v0.1/canonical-event.schema.json" + event = { + "schema_version": "open-agent-audit/v0.1", + "run_id": "run-abc123", + "agent_id": "agent-1", + "model_id": "claude-opus-4-8", + "event_id": "evt-0001", + "timestamp": "2026-07-28T14:03:22Z", + "type": "tool_call", + "actor": "agent", + "tool": {"name": "write_file"}, + } + validator = Draft202012Validator(schema) + assert not list(validator.iter_errors(event))