Skip to content
Closed
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
118 changes: 118 additions & 0 deletions schemas/v0.1/canonical-event.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://wasmagent.dev/schemas/v0.1/canonical-event.schema.json",
"title": "CanonicalEvent",
"description": "Canonical event schema — the foundational, extensible event structure for the Agent Evidence Protocol. Maps onto the AEP record model and provides a single-event envelope that adapters can produce from heterogeneous upstream event formats.",
"type": "object",
"required": ["schema_version", "event_id", "event_type", "timestamp_ms", "source"],
"properties": {
"schema_version": {
"type": "string",
"enum": ["canonical-event/v0.1"],
"description": "Schema version discriminator. Consumers MUST reject unknown values."
},
"event_id": {
"type": "string",
"description": "Unique identifier for this event within its trace (UUIDv7 or equivalent)."
},
"event_type": {
"type": "string",
"description": "Qualified event type name, e.g. 'tool.invocation', 'capability.decision', 'verifier.result'. Dot-separated namespace convention."
},
"timestamp_ms": {
"type": "number",
"description": "Epoch-millisecond timestamp when the event occurred."
},
"source": {
"type": "object",
"required": ["run_id"],
"description": "Provenance: identifies the agent run that produced this event.",
"properties": {
"run_id": {
"type": "string",
"description": "Unique identifier for the agent run."
},
"trace_id": {
"type": "string",
"description": "Trace identifier for distributed correlation."
},
"parent_event_id": {
"type": ["string", "null"],
"description": "Parent event in a causal chain, if any."
},
"agent_id": {
"type": "string",
"description": "Logical agent identifier."
},
"runtime_version": {
"type": "string",
"description": "Runtime version string, e.g. 'wasmagent-js@1.20.0'."
},
"model_provider": {
"type": "string",
"description": "LLM provider, e.g. 'anthropic', 'openai'."
},
"model_id": {
"type": "string",
"description": "Model identifier, e.g. 'claude-sonnet-5-20250514'."
}
}
},
"payload": {
"type": "object",
"description": "Event-type-specific body. Schema is determined by event_type; consumers should dispatch on that field. This field is extensible and intentionally untyped at the protocol level."
},
"correlation": {
"type": "object",
"description": "Optional cross-event correlation metadata.",
"properties": {
"causal_chain_id": {
"type": "string",
"description": "Groups events sharing a causal relationship."
},
"scope_lease_id": {
"type": "string",
"description": "Capability scope lease, if applicable."
}
}
},
"security": {
"type": "object",
"description": "Security and attestation metadata attached to this event.",
"properties": {
"taint_labels": {
"type": "array",
"items": { "type": "string" },
"description": "Data-flow taint labels propagated from inputs."
},
"capability_decision": {
"type": "object",
"required": ["capability", "decision"],
"description": "Capability decision governing this event, if applicable.",
"properties": {
"capability": { "type": "string" },
"subject": { "type": "string" },
"resource": { "type": "string" },
"decision": {
"type": "string",
"enum": ["allow", "deny", "ask_user", "dry_run"]
},
"reason_code": { "type": "string" }
}
}
}
},
"signature": {
"type": "object",
"required": ["alg", "key_id", "sig"],
"description": "Cryptographic signature over this event.",
"properties": {
"alg": { "type": "string" },
"key_id": { "type": "string" },
"sig": { "type": "string" },
"bundle": { "type": "object" },
"transparency_log_ref": { "type": "string" }
}
}
}
}
4 changes: 4 additions & 0 deletions tests/fixtures/invalid/canonical-event/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"schema_version": "canonical-event/v9.9",
"timestamp_ms": 1737600000100
}
33 changes: 33 additions & 0 deletions tests/fixtures/valid/canonical-event/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"schema_version": "canonical-event/v0.1",
"event_id": "evt-01jxq5k2r-abc123",
"event_type": "tool.invocation",
"timestamp_ms": 1737600000100,
"source": {
"run_id": "run-abc123",
"trace_id": "trace-001",
"agent_id": "agent-1",
"runtime_version": "wasmagent-js@1.20.0",
"model_provider": "anthropic",
"model_id": "claude-sonnet-5"
},
"payload": {
"tool_name": "write_file",
"state_changing": true,
"args": { "path": "/tmp/out.txt" }
},
"security": {
"taint_labels": ["user-input"],
"capability_decision": {
"capability": "fs.write",
"subject": "agent-1",
"resource": "/tmp/out.txt",
"decision": "allow"
}
},
"signature": {
"alg": "ed25519",
"key_id": "k1",
"sig": "base64sig=="
}
}
55 changes: 55 additions & 0 deletions tests/loader.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { test } from 'node:test';
import { getSchema, index, schemas } from '../index.js';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');

test('registry lists at least one schema', () => {
assert.ok(index.schemas.length >= 1);
});
Expand All @@ -23,3 +28,53 @@ test('schemas map is keyed by id', () => {
test('getSchema throws on unknown id', () => {
assert.throws(() => getSchema('does-not-exist'), /unknown schema id/);
});

// ---------------------------------------------------------------------------
// Consumer-surface guarantee for wasmagent-js (Milestone 1 — single source of
// truth). wasmagent-js must be able to delete its local schema copies —
// packages/compliance/schemas/* and
// packages/core/src/ranking/schemas/rollout-wire.schema.json — and import them
// from @wasmagent/protocol instead. Deleting those copies lives in the
// wasmagent-js repo (tracked via cross-repo note); the in-repo half of the
// contract — that every such schema is registered, declared as a wasmagent-js
// consumer, and reachable through the published package surface — is locked
// down here so the consumer-repo deletion can never silently regress.
const WASMAGENT_JS_SCHEMAS = [
'rollout-wire',
'constraint-ir',
'constraint-violation',
'repair-trace',
'task-spec',
'compliance-eval-record',
];

test('every schema wasmagent-js consumes is registered, declared, and published', () => {
const byId = new Map(index.schemas.map((s) => [s.id, s]));
for (const id of WASMAGENT_JS_SCHEMAS) {
const entry = byId.get(id);
assert.ok(
entry,
`${id} is missing from the registry — wasmagent-js cannot drop its local copy`,
);
assert.ok(
entry.consumers.includes('wasmagent-js'),
`${id} does not declare wasmagent-js as a consumer`,
);
// Programmatic import path: getSchema() must serve the live document.
assert.equal(getSchema(id).$id, entry.canonical_id, `${id} getSchema $id mismatch`);
// File import path: the schema file must ship so the ./schemas/* subpath
// export can serve it to consumers.
assert.ok(
existsSync(join(ROOT, entry.path)),
`${entry.path} is not on disk — the ./schemas/* subpath export cannot serve it`,
);
}
});

test("package.json exports the './schemas/*' subpath wasmagent-js imports from", () => {
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
assert.ok(
Object.keys(pkg.exports).includes('./schemas/*'),
"package.json exports must map './schemas/*' so consumers can import schema files directly",
);
});
24 changes: 24 additions & 0 deletions tests/test_conformance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Pytest bridge for the conformance harness.

The canonical conformance checks live in ``conformance.py`` and are invoked
directly (``python3 tests/conformance.py``, per ``CLAUDE.md`` and CI). This
module re-exposes them under ``pytest tests/`` so the package's standard test
command runs the full schema conformance suite instead of exiting with "no
tests ran" (pytest exit code 5).
"""
import subprocess
import sys
from pathlib import Path


def test_conformance_harness_passes():
harness = Path(__file__).parent / "conformance.py"
result = subprocess.run(
[sys.executable, str(harness)],
capture_output=True,
text=True,
)
assert result.returncode == 0, (
f"conformance.py exited {result.returncode}\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
Loading