From b697a56ae016cf87eeee7c872275339c23083aa3 Mon Sep 17 00:00:00 2001 From: claude-bot-go Date: Tue, 28 Jul 2026 07:59:23 +0800 Subject: [PATCH] Fix #131: Create @wasmagent/protocol package with aep-record type definition --- README.md | 31 +++++ index.d.ts | 155 ++++++++++++++++++++++++ src/wasmagent_protocol/__init__.py | 43 ++++++- src/wasmagent_protocol/types.py | 185 +++++++++++++++++++++++++++++ 4 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 src/wasmagent_protocol/types.py diff --git a/README.md b/README.md index 826208a..52f521b 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,25 @@ npm install @wasmagent/protocol ```ts import { schemas, getSchema } from "@wasmagent/protocol"; +import type { AEPRecord, AEPAction, AEPSideEffectClass } from "@wasmagent/protocol"; const aep = getSchema("aep-record"); // parsed JSON Schema object + +// Compile-time type checking for AEP records: +const record: AEPRecord = { + schema_version: "aep/v0.3", + run_id: "run-abc", + created_at_ms: Date.now(), +}; ``` +The package exports TypeScript interfaces for all `aep-record` sub-structures: +`AEPRecord`, `AEPAction`, `AEPInputRef`, `AEPOutputRef`, +`AEPCapabilityDecision`, `AEPVerifierResult`, `AEPBudgetLedger`, +`AEPBudgetEntry`, `AEPRunContext`, `AEPArgumentDrift`, `AEPSignature`, and +the union types `AEPRecordSchemaVersion`, `AEPCapabilityDecisionValue`, +`AEPSideEffectClass`, and `AEPRecordingMode`. + ### Python ```bash @@ -73,11 +88,27 @@ pip install wasmagent-protocol ```python from wasmagent_protocol import get_schema, schema_path +from wasmagent_protocol.types import AEPRecord, AEPAction aep = get_schema("aep-record") # parsed dict path = schema_path("aep-record") # pathlib.Path to the .json file + +# Static type checking with TypedDict: +record: AEPRecord = { + "schema_version": "aep/v0.3", + "run_id": "run-abc", + "created_at_ms": 1737600000000, +} ``` +The ``wasmagent_protocol.types`` module exports TypedDict classes for all +`aep-record` sub-structures: `AEPRecord`, `AEPAction`, `AEPInputRef`, +`AEPOutputRef`, `AEPCapabilityDecision`, `AEPVerifierResult`, +`AEPBudgetEntry`, `AEPBudgetLedger`, `AEPRunContext`, `AEPArgumentDrift`, +`AEPSignature`, and the literal types `AEPRecordSchemaVersion`, +`SideEffectClass`, and `RecordingMode`. All are also re-exported from the +top-level package for convenience. + ## Preventing cross-repo drift Downstream repos must not keep local copies of these schemas — but "must not" diff --git a/index.d.ts b/index.d.ts index 465117e..3629114 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,5 +1,160 @@ // Type declarations for @wasmagent/protocol. +// --------------------------------------------------------------------------- +// AEP Record types — mirrors schemas/aep/aep-record.schema.json. +// --------------------------------------------------------------------------- + +/** Schema version strings accepted by the canonical aep-record. */ +export type AEPRecordSchemaVersion = 'aep/v0.1' | 'aep/v0.2' | 'aep/v0.3'; + +export interface AEPInputRef { + uri: string; + digest?: string; + taint_labels?: string[]; +} + +export interface AEPOutputRef { + uri: string; + digest?: string; + redaction_profile?: string; +} + +/** Capability decision values. */ +export type AEPCapabilityDecisionValue = 'allow' | 'deny' | 'ask_user' | 'dry_run'; + +export interface AEPCapabilityDecision { + capability: string; + subject: string; + resource: string; + decision: AEPCapabilityDecisionValue; + reason_code?: string; +} + +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; +} + +export interface AEPVerifierResult { + verifier_id: string; + passed: boolean; + score?: number; + claim_ids?: string[]; +} + +export interface AEPBudgetEntry { + limit?: number; + spent: number; +} + +export interface AEPBudgetLedger { + token_budget?: AEPBudgetEntry & { spent: number }; + latency_budget?: { limit_ms?: number; actual_ms: number }; + tool_budget?: AEPBudgetEntry & { spent: number }; + risk_budget?: AEPBudgetEntry & { spent: number }; + retry_budget?: AEPBudgetEntry & { spent: number }; + human_approval_budget?: AEPBudgetEntry & { spent: number }; +} + +export interface AEPRunContext { + agent_id?: string; + agent_version?: string; + subagent_id?: string; + delegation_chain?: string[]; + environment_digest?: string; + dependency_lock_digest?: string; +} + +/** Per-record and per-run side-effect classification. */ +export type AEPSideEffectClass = + | 'read' + | 'mutate-local' + | 'mutate-external' + | 'network-egress' + | 'unknown'; + +/** How evidence for a run was captured. */ +export type AEPRecordingMode = 'full' | 'delta' | 'validation'; + +/** + * Detected drift between an action's declared arguments and the arguments + * used at runtime. The JSON Schema allows additional properties. + */ +export interface AEPArgumentDrift { + tool_name?: string; + declared_digest?: string; + actual_digest?: string; + diff_summary?: string; + drifted_args?: string[]; + /** Additional properties beyond the known fields. */ + [key: string]: string | string[] | undefined; +} + +export interface AEPSignature { + alg: string; + key_id: string; + sig: string; + bundle?: Record; + transparency_log_ref?: string; +} + +/** + * Agent Evidence Protocol record — runtime action evidence and run provenance. + * + * Mirrors `schemas/aep/aep-record.schema.json`. Use `getSchema("aep-record")` + * for runtime JSON Schema validation; use this interface for compile-time type + * checking in TypeScript. + */ +export interface AEPRecord { + schema_version: AEPRecordSchemaVersion; + 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; + input_refs?: AEPInputRef[]; + output_refs?: AEPOutputRef[]; + capability_decisions?: AEPCapabilityDecision[]; + actions?: AEPAction[]; + verifier_results?: AEPVerifierResult[]; + budget_ledger?: AEPBudgetLedger; + run_context?: AEPRunContext; + user_id?: string; + subject_id?: string; + side_effect_class?: AEPSideEffectClass; + run_side_effect_class_max?: AEPSideEffectClass; + recording_mode?: AEPRecordingMode; + argument_drift?: AEPArgumentDrift; + signature?: AEPSignature; +} + +// --------------------------------------------------------------------------- +// Schema index and loader types. +// --------------------------------------------------------------------------- + export interface SchemaIndexEntry { id: string; title: string; diff --git a/src/wasmagent_protocol/__init__.py b/src/wasmagent_protocol/__init__.py index 72fd3df..517c422 100644 --- a/src/wasmagent_protocol/__init__.py +++ b/src/wasmagent_protocol/__init__.py @@ -17,7 +17,27 @@ from pathlib import Path from typing import Any -__all__ = ["INDEX", "get_schema", "schema_path", "schema_ids"] +__all__ = [ + "INDEX", + "get_schema", + "schema_path", + "schema_ids", + # Re-export TypedDict types from types.py for convenience. + "AEPRecord", + "AEPRecordSchemaVersion", + "AEPInputRef", + "AEPOutputRef", + "AEPCapabilityDecision", + "AEPAction", + "AEPVerifierResult", + "AEPBudgetEntry", + "AEPBudgetLedger", + "AEPRunContext", + "SideEffectClass", + "RecordingMode", + "AEPArgumentDrift", + "AEPSignature", +] _SCHEMAS_PKG = "wasmagent_protocol.schemas" # Editable / source-checkout fallback: the canonical schemas live at the repo @@ -105,3 +125,24 @@ def schema_path(schema_id: str) -> Path: return node with resources.as_file(node) as p: return Path(p) + + +# --------------------------------------------------------------------------- +# Re-export TypedDict types for convenience. +# --------------------------------------------------------------------------- +from wasmagent_protocol.types import ( # noqa: E402, F811 — lazy re-export + AEPAction, + AEPArgumentDrift, + AEPBudgetEntry, + AEPBudgetLedger, + AEPCapabilityDecision, + AEPInputRef, + AEPRecord, + AEPRecordSchemaVersion, + AEPRunContext, + AEPSignature, + AEPOutputRef, + AEPVerifierResult, + RecordingMode, + SideEffectClass, +) diff --git a/src/wasmagent_protocol/types.py b/src/wasmagent_protocol/types.py new file mode 100644 index 0000000..5067d4d --- /dev/null +++ b/src/wasmagent_protocol/types.py @@ -0,0 +1,185 @@ +"""TypedDict definitions for the canonical AEP record schema. + +These types mirror ``schemas/aep/aep-record.schema.json`` and provide static +type-checking for consumers. Use :func:`get_schema` for runtime JSON Schema +validation; use these TypedDicts for IDE/mypy support. + +.. code-block:: python + + from wasmagent_protocol.types import AEPRecord + + record: AEPRecord = { + "schema_version": "aep/v0.3", + "run_id": "run-abc", + "created_at_ms": 1737600000000, + } +""" + +from __future__ import annotations + +from typing import Literal, NotRequired, TypedDict + + +# --------------------------------------------------------------------------- +# Sub-structure types +# --------------------------------------------------------------------------- + + +class AEPInputRef(TypedDict): + uri: str + digest: NotRequired[str] + taint_labels: NotRequired[list[str]] + + +class AEPOutputRef(TypedDict): + uri: str + digest: NotRequired[str] + redaction_profile: NotRequired[str] + + +class AEPCapabilityDecision(TypedDict): + capability: str + subject: str + resource: str + decision: Literal["allow", "deny", "ask_user", "dry_run"] + reason_code: NotRequired[str] + + +class AEPAction(TypedDict): + action_id: str + tool_name: str + state_changing: bool + timestamp_ms: int | float + precondition_digest: NotRequired[str] + result_digest: NotRequired[str] + evidence_refs: NotRequired[list[str]] + parent_action_id: NotRequired[str] + causal_chain_id: NotRequired[str] + tool_descriptor_digest: NotRequired[str] + server_card_digest: NotRequired[str] + scope_lease_id: NotRequired[str] + approval_context_hash: NotRequired[str] + input_taint_labels: NotRequired[list[str]] + output_taint_labels: NotRequired[list[str]] + memory_read_refs: NotRequired[list[str]] + memory_write_refs: NotRequired[list[str]] + pre_state_digest: NotRequired[str] + post_state_digest: NotRequired[str] + + +class AEPVerifierResult(TypedDict): + verifier_id: str + passed: bool + score: NotRequired[int | float] + claim_ids: NotRequired[list[str]] + + +class AEPBudgetEntry(TypedDict): + limit: NotRequired[int | float] + spent: int | float + + +class AEPBudgetLedger(TypedDict): + token_budget: NotRequired[AEPBudgetEntry] + latency_budget: NotRequired[TypedDict("LatencyBudget", {"limit_ms": NotRequired[int | float], "actual_ms": int | float})] + tool_budget: NotRequired[AEPBudgetEntry] + risk_budget: NotRequired[AEPBudgetEntry] + retry_budget: NotRequired[AEPBudgetEntry] + human_approval_budget: NotRequired[AEPBudgetEntry] + + +class AEPRunContext(TypedDict): + agent_id: NotRequired[str] + agent_version: NotRequired[str] + subagent_id: NotRequired[str] + delegation_chain: NotRequired[list[str]] + environment_digest: NotRequired[str] + dependency_lock_digest: NotRequired[str] + + +SideEffectClass = Literal[ + "read", "mutate-local", "mutate-external", "network-egress", "unknown" +] + +RecordingMode = Literal["full", "delta", "validation"] + + +class AEPArgumentDrift(TypedDict, total=False): + """Detected drift between declared and actual runtime arguments. + + The JSON Schema allows ``additionalProperties``, so any extra keys are + accepted at runtime. + """ + tool_name: str + declared_digest: str + actual_digest: str + diff_summary: str + drifted_args: list[str] + + +class AEPSignature(TypedDict): + alg: str + key_id: str + sig: str + bundle: NotRequired[dict] + transparency_log_ref: NotRequired[str] + + +# --------------------------------------------------------------------------- +# Top-level record +# --------------------------------------------------------------------------- + +AEPRecordSchemaVersion = Literal["aep/v0.1", "aep/v0.2", "aep/v0.3"] + + +class AEPRecord(TypedDict): + """Agent Evidence Protocol record. + + Mirrors ``schemas/aep/aep-record.schema.json``. Use + :func:`~wasmagent_protocol.get_schema` for runtime JSON Schema validation; + use this TypedDict for static type-checking. + """ + schema_version: AEPRecordSchemaVersion + run_id: str + created_at_ms: int | float + trace_id: NotRequired[str] + parent_trace_id: NotRequired[str | None] + repo_commit: NotRequired[str] + runtime_version: NotRequired[str] + model_provider: NotRequired[str] + model_id: NotRequired[str] + policy_bundle_digest: NotRequired[str] + tool_manifest_digest: NotRequired[str] + mcp_server_card_digest: NotRequired[str | None] + input_refs: NotRequired[list[AEPInputRef]] + output_refs: NotRequired[list[AEPOutputRef]] + capability_decisions: NotRequired[list[AEPCapabilityDecision]] + actions: NotRequired[list[AEPAction]] + verifier_results: NotRequired[list[AEPVerifierResult]] + budget_ledger: NotRequired[AEPBudgetLedger] + run_context: NotRequired[AEPRunContext] + user_id: NotRequired[str] + subject_id: NotRequired[str] + side_effect_class: NotRequired[SideEffectClass] + run_side_effect_class_max: NotRequired[SideEffectClass] + recording_mode: NotRequired[RecordingMode] + argument_drift: NotRequired[AEPArgumentDrift] + signature: NotRequired[AEPSignature] + + +__all__ = [ + "AEPRecord", + "AEPRecordSchemaVersion", + "AEPInputRef", + "AEPOutputRef", + "AEPCapabilityDecision", + "AEPAction", + "AEPVerifierResult", + "AEPBudgetEntry", + "AEPBudgetLedger", + "AEPRunContext", + "SideEffectClass", + "RecordingMode", + "AEPArgumentDrift", + "AEPSignature", +]