diff --git a/README.md b/README.md index 835009f..469ccdb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Agent Control Plane (ACP)** is an experimental control-plane kernel for coordinating, constraining, and observing AI-agent/workflow execution. -> **Epistemic status:** Experimental engineering. The repository contains an executable deterministic kernel with policy hooks, cooperative task budgets, and run-scoped provenance. It is not a complete autonomous control plane, security boundary, or production-ready orchestration platform. +> **Epistemic status:** Experimental engineering. The repository contains an executable deterministic kernel with policy hooks, cooperative task budgets, run-scoped provenance, and an additive versioned framework-neutral execution/trace contract. It is not a complete autonomous control plane, security boundary, proven cross-runtime portability layer, or production-ready orchestration platform. ## Current implementation @@ -20,9 +20,15 @@ The kernel currently provides: - run-scoped provenance events carrying both task ID and run ID; - terminal provenance containing reported resource usage; - a portable `agent-control-plane.provenance.v1` manifest for the current in-memory run; -- tests covering successful dispatch, handler failure, cancellation, policy decisions, adversarial/invariant cases, duplicate registration, unknown-capability evidence, provenance binding, exact-limit budget use, atomic overrun handling, and suppressed-exception fail-closure; +- additive `agent-control-plane.execution.v1` contract types for execution, trace/span, component/runtime/adapter, artifact, and event identity; +- strict fail-closed validation of required identities, schema version, trace parentage, SHA-256 references, monotonic values, and UTC timestamps; +- deterministic execution-event serialization with canonical UTC `Z` timestamps and validated round-trip reconstruction; +- an explicit mapper from legacy `ProvenanceEvent` records into the new execution contract when the caller supplies context absent from legacy provenance; +- tests covering successful dispatch, handler failure, cancellation, policy decisions, adversarial/invariant cases, duplicate registration, unknown-capability evidence, provenance binding, exact-limit budget use, atomic overrun handling, suppressed-exception fail-closure, execution-contract validation/serialization, round-trip reconstruction, and provenance mapping; - GitHub Actions CI for the Python suite. +The legacy provenance manifest and the execution contract are distinct representations. `agent-control-plane.provenance.v1` remains unchanged and process-local. Mapping a legacy event into `agent-control-plane.execution.v1` requires caller-supplied execution/trace/component/monotonic context; ACP does not fabricate missing historical trace or identity data. + The provenance manifest is **an in-memory/exportable execution record**, not durable storage, tamper-evident attestation, or an external audit log. Resource accounting is **cooperative**: handlers or runtime/tool adapters must report usage with `Task.consume(...)`; the kernel does not infer provider token counts, tool usage, elapsed time, or cost automatically. ## Example @@ -51,6 +57,38 @@ manifest = plane.provenance_manifest() assert manifest["run_id"] == "example-run" ``` +The versioned execution contract is available from the additive subpackage: + +```python +from agent_control_plane.contract import ( + ComponentIdentity, + ExecutionEvent, + ExecutionIdentity, + TraceContext, +) + +identity = ExecutionIdentity(execution_id="exec-1", run_id="run-1") +trace = TraceContext(trace_id="trace-1", span_id="span-1") +component = ComponentIdentity( + component_id="kernel", + component_type="kernel", + runtime_id="python", + adapter_id="native-acp", +) + +event = ExecutionEvent( + event_type="task.completed", + identity=identity, + trace=trace, + component=component, + task_id="task-1", + status="completed", + utc_timestamp="2026-09-14T23:00:00Z", + monotonic_ns=1, +) +assert ExecutionEvent.from_dict(event.to_dict()) == event +``` + Run verification with: ```bash @@ -60,7 +98,7 @@ python -m pytest ## Fail-closed boundaries -The current kernel deliberately rejects or records several ambiguous states: +The current kernel and contract deliberately reject or record several ambiguous states: - an empty capability cannot be registered; - an already registered capability cannot be silently replaced; @@ -68,17 +106,27 @@ The current kernel deliberately rejects or records several ambiguous states: - an unknown capability produces a provenance rejection event and raises `KeyError`; - a handler exception becomes a recorded `FAILED` task state; - a policy denial is recorded rather than treated as successful execution; -- an attempted cooperative resource-budget overrun becomes `BUDGET_EXHAUSTED` and cannot be converted into successful completion by catching the budget exception inside the handler. - -These properties are local software invariants. They do not establish distributed reliability or system security. +- an attempted cooperative resource-budget overrun becomes `BUDGET_EXHAUSTED` and cannot be converted into successful completion by catching the budget exception inside the handler; +- blank required execution/trace/component/artifact identity is rejected; +- unsupported execution-contract schema versions are rejected; +- trace span self-parenting is rejected; +- malformed optional SHA-256 references are rejected; +- negative or non-integer monotonic values are rejected; +- naive or non-UTC event timestamps are rejected; +- legacy provenance cannot be mapped across a mismatched run identity; +- absent legacy state maps to explicit `unspecified`, never inferred success. + +These properties are local software invariants under the tested conditions. They do not establish distributed reliability, system security, governance efficacy, or cross-runtime portability. ## Not yet implemented / established Unless added and independently verified later, ACP does **not** currently provide: +- materially different external runtime adapters conforming to `agent-control-plane.execution.v1`; +- two-runtime portability evidence without core-schema fork; - model/provider integrations; - automatic provider token/cost/tool-call metering; -- durable event or budget persistence; +- durable event, trace, or budget persistence; - cryptographic/tamper-evident provenance; - distributed execution; - authentication or authorization infrastructure; @@ -96,15 +144,17 @@ Claims in this repository should distinguish: `DEFINED → IMPLEMENTED → COMPUTED → VERIFIED → ATTESTED → HISTORICAL → HYPOTHESIS → METAPHOR → UNSUPPORTED → DEPRECATED` -A passing unit test establishes only the tested property under that test environment. An exported provenance manifest is not an attestation. Cooperative resource accounting is not proof of externally measured consumption. Cross-repository use does not transfer validation. +A passing unit test establishes only the tested property under that test environment. An exported provenance manifest is not an attestation. Cooperative resource accounting is not proof of externally measured consumption. ACP-native execution-contract conformance is not proof of cross-runtime portability. Cross-repository use does not transfer validation. ## Ecosystem relationship -ACP may provide reusable primitives to other `ndrorchestration` projects. `DGAF-Framework`, PDMAL, Orbit-Driftwatch, Sentinel, and other repositories maintain separate evidence and governance boundaries. Integration should be demonstrated through explicit interfaces and tests. +ACP is the preferred experimental implementation host for a framework-neutral execution contract. DGAF retains governance, authorization, provenance/evidence-discipline, and evidence-state authority. PDMAL is a governed empirical workload/research consumer rather than a prerequisite parent of ACP. Other `ndrorchestration` repositories maintain separate evidence boundaries. + +A framework/runtime adapter may populate the ACP execution contract, but adapter readiness, CI success, deployment health, or successful execution cannot self-promote DGAF authorization, PDMAL scientific validity, empirical efficacy, or general portability. ## Current status -**Experimental / development track — executable kernel with run-scoped provenance, fail-closed dispatch invariants, and cooperative task-budget accounting.** +**Experimental / development track — executable kernel with run-scoped provenance, fail-closed dispatch invariants, cooperative task-budget accounting, and an ACP-native versioned execution/trace contract with deterministic serialization and validation. Cross-runtime portability remains NOT ESTABLISHED.** ## Provenance diff --git a/docs/CONTROL_PLANE_KERNEL_SPEC.md b/docs/CONTROL_PLANE_KERNEL_SPEC.md index 52e4436..0dad4ce 100644 --- a/docs/CONTROL_PLANE_KERNEL_SPEC.md +++ b/docs/CONTROL_PLANE_KERNEL_SPEC.md @@ -53,32 +53,79 @@ This is local deterministic routing. It is **not** distributed scheduling, load Policy evaluation is represented as an explicit allow/deny decision with an optional reason. Policy is intentionally separated from execution so higher-level governance systems can supply policies without coupling them to the kernel. +The current kernel policy hook is a **pre-execution** allow/deny mechanism. The cross-runtime execution contract described below can carry an optional `policy_decision_ref`, but that field only associates an identified decision with an execution event. It does not add in-execution or post-execution policy engines and does not itself establish authorization. + ## Provenance -Each execution transition emits a structured `ProvenanceEvent` containing: +Each kernel execution transition emits a structured `ProvenanceEvent` containing: - event type; - task identifier; +- run identifier; - capability when applicable; - resulting state when applicable; - optional failure or usage detail; - UTC timestamp. -The event list is currently process-local and non-durable. +`ControlPlane.provenance_manifest()` continues to export the existing `agent-control-plane.provenance.v1` representation. That manifest remains process-local and non-durable. + +## Cross-runtime execution contract + +ACP additionally defines a versioned framework-neutral execution/trace contract with schema identity: + +`agent-control-plane.execution.v1` + +The contract is additive to the existing kernel provenance representation. It currently provides immutable typed records for: + +- `ExecutionIdentity` — explicit execution ID, run ID, and schema version; +- `TraceContext` — trace ID, span ID, and optional parent span ID; +- `ComponentIdentity` — component, component type, runtime, adapter, and optional source/version identity; +- `ArtifactRef` — artifact identity/type plus optional URI, version, and SHA-256 reference; +- `ExecutionEvent` — event type, execution/trace/component context, task/status, UTC timestamp, monotonic ordering value, optional capability/policy reference/artifacts/detail. + +Required identity fields fail closed when blank. Unsupported schema versions, span self-parenting, malformed SHA-256 values, negative/non-integer monotonic values, and naive or non-UTC event timestamps are rejected with `ContractValidationError`. + +Event timestamps serialize canonically in UTC with a trailing `Z`. Contract serialization is deterministic and `ExecutionEvent.from_dict()` reconstructs serialized events through the same validation paths, so malformed nested data does not bypass validation. + +### Legacy provenance mapping + +`map_provenance_event(...)` converts an existing `ProvenanceEvent` into an `ExecutionEvent` only when the caller supplies execution, trace, component, and monotonic context that the legacy event does not contain. + +The mapper: + +- preserves the legacy event type, task ID, capability, state, detail, and UTC timestamp; +- requires the legacy `run_id` to equal the supplied contract `run_id` and fails closed on mismatch; +- maps an absent legacy state to the explicit non-success placeholder `unspecified`; +- does not fabricate trace IDs, component IDs, artifacts, policy decisions, or successful status. + +This mapper does not change `agent-control-plane.provenance.v1` and does not imply that historical events contained trace/span data they did not record. + +### Current conformance boundary + +The present contract tests establish ACP-native schema construction, validation, deterministic serialization, round-trip reconstruction, and legacy-provenance mapping under the tested Python environments. + +They do **not** establish cross-runtime portability. No materially different external runtime adapter is implemented in this slice, and there is not yet a two-runtime conformance result using the same core schema without fork. ## Evidence boundary -The kernel and tests demonstrate local deterministic behavior only. The budget tests establish the cooperative count/cost accounting and fail-closed exhaustion properties exercised by those tests. They do not establish: +The kernel and tests demonstrate local deterministic behavior only. The budget tests establish the cooperative count/cost accounting and fail-closed exhaustion properties exercised by those tests. The execution-contract tests establish only the ACP-native contract properties exercised by those tests. + +They do not establish: +- general cross-runtime portability; - production reliability; - distributed correctness; - security authorization; +- DGAF authorization or governance effectiveness; +- PDMAL scientific validity or efficacy; - persistence guarantees; +- durable or tamper-evident provenance; +- cryptographic attestation or custody independence; - hard execution-time enforcement; - provider-accurate token/cost metering; - retry/checkpoint/delegation correctness; - model quality; - multi-agent coordination quality; -- governance effectiveness. +- superiority to existing orchestration standards or frameworks. -Those claims require separate implementation and empirical validation. +Those claims require separate implementation and evidence. diff --git a/docs/superpowers/plans/2026-09-14-cross-runtime-execution-contract-plan.md b/docs/superpowers/plans/2026-09-14-cross-runtime-execution-contract-plan.md new file mode 100644 index 0000000..d04f2f4 --- /dev/null +++ b/docs/superpowers/plans/2026-09-14-cross-runtime-execution-contract-plan.md @@ -0,0 +1,97 @@ +# Cross-Runtime Execution Contract v1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a versioned framework-neutral execution/trace contract to ACP without changing existing kernel behavior or the `agent-control-plane.provenance.v1` manifest. + +**Architecture:** Introduce focused immutable contract dataclasses plus strict validators in a new `agent_control_plane.contract` package. Keep the existing kernel/provenance model intact, then add an explicit mapper from legacy `ProvenanceEvent` into the new contract when the caller supplies trace/component context that legacy events do not contain. + +**Tech Stack:** Python >=3.10, stdlib dataclasses/datetime/re, pytest, existing GitHub Actions matrix Python 3.10–3.14. + +**Spec:** `docs/superpowers/specs/2026-09-14-cross-runtime-execution-contract-design.md` + +## Global Constraints + +- Contract schema version is exactly `agent-control-plane.execution.v1`. +- Canonical serialized UTC timestamps end in `Z`. +- Legacy provenance with absent state maps exactly to `unspecified`. +- Invalid required identity, unsupported schema version, malformed SHA-256, non-UTC timestamp, negative monotonic value, span self-parenting, and provenance/identity run mismatch fail closed with `ContractValidationError`. +- No new external dependencies. +- Existing `Task`, `ControlPlane`, budgets, public imports, and `agent-control-plane.provenance.v1` behavior remain compatible. +- This slice establishes ACP-native schema/serialization conformance only; portability remains NOT ESTABLISHED. + +--- + +### Task 1: Core identities and validation + +**Files:** +- Create: `tests/test_contract.py` +- Create: `src/agent_control_plane/contract/__init__.py` +- Create: `src/agent_control_plane/contract/model.py` + +**Interfaces:** +- Produces: `ContractValidationError`, `ExecutionIdentity`, `TraceContext`, `ComponentIdentity`, `ArtifactRef`, `SCHEMA_VERSION`. + +- [ ] **Step 1: Write failing tests** for valid deterministic serialization and each required rejection path: blank identity, unsupported schema version, blank trace/span, blank optional parent, self-parent, blank component fields, and malformed artifact SHA-256. +- [ ] **Step 2: Push tests only and verify RED in GitHub Actions.** Expected failures are import/definition failures for `agent_control_plane.contract` or missing required contract classes, not syntax/configuration failures. +- [ ] **Step 3: Implement minimal immutable dataclasses and validators** in `model.py`; export them through `contract/__init__.py`. +- [ ] **Step 4: Push implementation and verify GREEN** for the full Python 3.10–3.14 matrix. + +### Task 2: Execution event and canonical time serialization + +**Files:** +- Modify: `tests/test_contract.py` +- Modify: `src/agent_control_plane/contract/model.py` +- Modify: `src/agent_control_plane/contract/__init__.py` + +**Interfaces:** +- Produces: `ExecutionEvent` with `to_dict()` and strict UTC/monotonic validation. +- Consumes: identities and artifact refs from Task 1. + +- [ ] **Step 1: Write failing tests** for exact full-event dictionary serialization, canonical `Z` timestamp output, artifact order preservation, negative monotonic rejection, naive timestamp rejection, and non-UTC offset rejection. +- [ ] **Step 2: Push tests only and verify RED** for missing `ExecutionEvent`/event validation behavior. +- [ ] **Step 3: Implement the minimum event type and timestamp canonicalizer** needed to satisfy the tests; no generated timestamps during serialization. +- [ ] **Step 4: Push and verify GREEN** across the matrix. + +### Task 3: Legacy provenance mapper + +**Files:** +- Create: `src/agent_control_plane/contract/mapping.py` +- Modify: `src/agent_control_plane/contract/__init__.py` +- Modify: `tests/test_contract.py` + +**Interfaces:** +- Produces: `map_provenance_event(event, *, identity, trace, component, monotonic_ns, input_artifacts=(), output_artifacts=(), policy_decision_ref=None) -> ExecutionEvent`. +- Consumes: existing `agent_control_plane.provenance.ProvenanceEvent` plus Task 1/2 contract types. + +- [ ] **Step 1: Write failing tests** proving legacy event/task/run/capability/state/detail preservation, exact `unspecified` mapping for absent state, canonical UTC conversion, and fail-closed run mismatch. +- [ ] **Step 2: Push tests only and verify RED** for missing mapper behavior. +- [ ] **Step 3: Implement the mapper without fabricating trace/component/artifact/policy data.** Require supplied `monotonic_ns`; reject run mismatch with `ContractValidationError`. +- [ ] **Step 4: Push and verify GREEN** across the matrix. + +### Task 4: Contract round-trip and evidence-boundary documentation + +**Files:** +- Modify: `tests/test_contract.py` +- Modify: `src/agent_control_plane/contract/model.py` +- Modify: `docs/CONTROL_PLANE_KERNEL_SPEC.md` +- Modify: `README.md` + +**Interfaces:** +- Produces: `ExecutionEvent.from_dict()` (or equivalent explicit validator) that reconstructs the exact semantic contract from serialized data. + +- [ ] **Step 1: Write failing round-trip test** from a fully populated serialized event back to an equal contract object; reject unsupported schema/version data during reconstruction. +- [ ] **Step 2: Push tests only and verify RED** for missing round-trip reconstruction. +- [ ] **Step 3: Implement minimal reconstruction/validation** reusing the same constructors rather than duplicating validation. +- [ ] **Step 4: Update README/kernel spec** to distinguish legacy provenance from the new execution contract and explicitly state that runtime portability, DGAF authorization, PDMAL efficacy, durability, attestation, and production readiness remain unestablished. +- [ ] **Step 5: Push and verify GREEN** across the complete matrix. + +### Task 5: Final branch verification and review gate + +**Files:** no new production scope. + +- [ ] **Step 1: Compare branch against base `dbab7c1afafec524ce7c18157de2089cafe79c87`** and verify only planned contract/tests/docs changed. +- [ ] **Step 2: Verify exact-head GitHub Actions** across Python 3.10–3.14 with no failures. +- [ ] **Step 3: Re-read the design acceptance criteria** and classify each as VERIFIED, NOT VERIFIED, or NOT APPLICABLE from branch evidence. +- [ ] **Step 4: Open a PR only after exact-head verification is green.** PR text must preserve the evidence ceiling and state that portability remains NOT ESTABLISHED. +- [ ] **Step 5: Do not merge until PR-head checks are complete and no evidence-boundary regression is found.** diff --git a/docs/superpowers/specs/2026-09-14-cross-runtime-execution-contract-design.md b/docs/superpowers/specs/2026-09-14-cross-runtime-execution-contract-design.md new file mode 100644 index 0000000..4138c85 --- /dev/null +++ b/docs/superpowers/specs/2026-09-14-cross-runtime-execution-contract-design.md @@ -0,0 +1,287 @@ +# Cross-Runtime Execution Contract v1 — Design + +**Status:** Design candidate for implementation in `agent-control-plane`. This document does not establish contract acceptance, portability, production readiness, DGAF authorization, PDMAL empirical validity, or efficacy. + +## Purpose + +Add a versioned, framework-neutral execution/trace contract alongside the existing Agent Control Plane kernel so heterogeneous runtimes can emit and consume the same execution identity, trace, event, artifact, policy, and evidence metadata without making any runtime framework authoritative over the core ontology. + +The change is additive. Existing `Task`, `ControlPlane`, cooperative budget behavior, and `agent-control-plane.provenance.v1` remain compatible during this slice. + +## Architecture boundary + +- **DGAF** owns governance, authorization, evidence-state transitions, and fail-closed policy authority. +- **agent-control-plane (ACP)** is the preferred implementation host for the framework-neutral execution contract and future runtime adapters. +- **PDMAL** is a governed empirical workload/research consumer. It does not define the ACP contract and is not a prerequisite parent of ACP. +- **Metasymphony** may name a composed execution profile using the contract. It is not a separate framework or control plane by default. +- **Evaluators** such as Axiom Lens or provenance-regression tooling attach through explicit bounded interfaces. They do not mutate the core schema merely because they exist. + +## Design principles + +1. **Framework neutrality:** LangGraph, Microsoft Agent Framework, MCP, A2A, OpenAI runtimes, Hermes, PDMAL-specific runtimes, and future systems are adapters or consumers, not schema authorities. +2. **Additive compatibility:** do not rewrite the existing kernel to introduce the contract. Existing behavior remains valid while the new contract is introduced and mapped. +3. **Typed identity before adapter work:** establish execution, trace, span, actor/component, runtime/adapter, artifact, and schema identity before building external adapters. +4. **Deterministic serialization:** contract objects must serialize deterministically for fixtures, hashing, comparison, and evidence binding. +5. **Fail closed on malformed identity:** blank or structurally invalid required identifiers and unsupported schema versions are rejected rather than normalized silently. +6. **Evidence separation:** contract conformance is engineering evidence. It is not runtime portability, governance authorization, empirical efficacy, or scientific validation. +7. **No hidden ontology fork:** runtime adapters may populate contract fields but may not silently redefine required semantics. + +## Scope of this implementation slice + +### In scope + +- a versioned contract module/package; +- execution identity; +- trace/span context; +- actor/component and runtime/adapter identity; +- artifact references with optional version/hash metadata; +- versioned execution-event objects; +- UTC timestamp plus monotonic ordering/timing field semantics; +- execution status/event classification; +- optional policy-decision reference; +- deterministic `to_dict` / JSON-ready serialization; +- strict construction/validation for required fields; +- mapping from current ACP provenance events into the new contract without removing `agent-control-plane.provenance.v1`; +- deterministic contract/conformance fixtures and tests; +- documentation of evidence boundaries and unsupported claims. + +### Explicitly out of scope + +- LangGraph, Microsoft Agent Framework, MCP, A2A, OpenAI, Hermes, or PDMAL adapter implementation; +- two-runtime portability claims; +- durable persistence or distributed trace storage; +- cryptographic/tamper-evident provenance; +- provider-accurate metering; +- hard deadline/preemption; +- checkpoint/resume; +- delegated-budget conservation; +- authentication or authorization infrastructure; +- empirical DGAF/PDMAL runs; +- semantic/embedding evaluators; +- changes to DGAF governance state. + +## Contract structure + +The implementation should use a focused package under `src/agent_control_plane/contract/`, with public exports collected in `src/agent_control_plane/contract/__init__.py`. Internal decomposition should keep validation/model and provenance-mapping responsibilities separate. + +The package exports `ContractValidationError`, a subclass of `ValueError`, for contract construction and mapping failures. Callers may catch `ValueError` broadly without losing compatibility with ordinary Python validation conventions. + +### `ExecutionIdentity` + +Required fields: + +- `execution_id: str` — stable identifier for one logical execution; +- `run_id: str` — ACP/run-level identity; +- `schema_version: str` — exact contract version, initially `agent-control-plane.execution.v1`. + +Validation: + +- all required identifiers must be non-empty after trimming; +- unsupported schema versions fail closed with `ContractValidationError`; +- identifiers are preserved exactly after validation; they are not silently regenerated. + +`execution_id` is intentionally distinct from existing `task_id` and `run_id`: a task is a kernel object, a run scopes ACP provenance, and an execution is the cross-runtime unit represented by this contract. + +### `TraceContext` + +Required fields: + +- `trace_id: str`; +- `span_id: str`. + +Optional field: + +- `parent_span_id: str | None`. + +Validation: + +- `trace_id` and `span_id` must be non-empty; +- when present, `parent_span_id` must be non-empty; +- `span_id` must not equal `parent_span_id`; +- this slice does not prescribe UUID, W3C Trace Context, or OpenTelemetry formatting. Format standardization may be added later through an explicit compatibility decision. + +### `ComponentIdentity` + +Required fields: + +- `component_id: str` — stable actor/tool/agent/service/component identifier; +- `component_type: str` — bounded descriptive type such as `kernel`, `agent`, `tool`, `runtime`, or `evaluator`; +- `runtime_id: str` — runtime/execution environment identity; +- `adapter_id: str` — adapter identity, including `native-acp` for the current kernel mapping. + +Optional fields: + +- `version: str | None`; +- `source_ref: str | None` — commit/build/package/deployment identity where material. + +No named persona is required by the contract. Persona presentation or historical actor lineage belongs outside this primitive identity layer unless represented as ordinary metadata by a higher-level profile. + +### `ArtifactRef` + +Required fields: + +- `artifact_id: str`; +- `kind: str`. + +Optional fields: + +- `uri: str | None`; +- `version: str | None`; +- `sha256: str | None`. + +Validation: + +- required fields must be non-empty; +- if `sha256` is present it must be 64 lowercase hexadecimal characters; +- the contract does not claim that a hash proves custody, attestation, or independence. + +### `ExecutionEvent` + +Required fields: + +- `event_type: str`; +- `identity: ExecutionIdentity`; +- `trace: TraceContext`; +- `component: ComponentIdentity`; +- `task_id: str`; +- `status: str`; +- `utc_timestamp: str`; +- `monotonic_ns: int`. + +Optional fields: + +- `capability: str | None`; +- `policy_decision_ref: str | None`; +- `input_artifacts: tuple[ArtifactRef, ...]`; +- `output_artifacts: tuple[ArtifactRef, ...]`; +- `detail: str | None`. + +Validation: + +- required strings must be non-empty; +- `monotonic_ns` must be an integer >= 0; +- `utc_timestamp` must parse as timezone-aware ISO-8601 UTC; +- serialization normalizes UTC timestamps to the canonical `YYYY-MM-DDTHH:MM:SS[.ffffff]Z` form, using `Z` rather than `+00:00`; +- artifact arrays preserve caller order; +- serialization emits stable field names and deterministic nested ordering. + +## Event mapping from current ACP provenance + +The current `ProvenanceEvent` remains supported. The additive mapper converts an existing provenance event to `ExecutionEvent` when the caller supplies the cross-runtime context not present in v1 provenance: + +- `ExecutionIdentity`; +- `TraceContext`; +- `ComponentIdentity`; +- optional artifact references and policy-decision reference. + +Mapping rules: + +- existing `ProvenanceEvent.event` -> `ExecutionEvent.event_type`; +- existing `task_id` -> `task_id`; +- existing `run_id` must equal `ExecutionIdentity.run_id`; mismatch fails closed; +- existing `capability` -> `capability`; +- existing `state` -> `status`; when state is absent the mapper uses the exact literal `unspecified`, never inferred success; +- existing `detail` -> `detail`; +- existing UTC timestamp -> validated and canonically serialized `utc_timestamp`; +- `monotonic_ns` must be supplied by the mapping call or event-emission boundary; it is not reconstructed from wall-clock time. + +The mapper must not fabricate trace IDs, component IDs, source refs, artifacts, policy decisions, or successful status from missing historical data. + +## Policy lifecycle boundary + +This slice records a `policy_decision_ref` but does not implement new in-execution or post-execution policy engines. + +The current ACP policy hook remains a pre-execution allow/deny mechanism. Future policy phases must be separately designed and tested. A recorded policy reference means only that an identified policy decision is associated with an event; it does not prove authorization unless DGAF or another governing authority establishes that relationship. + +## Serialization and conformance + +Contract objects expose deterministic JSON-ready dictionaries. The implementation must not include process addresses, unordered set output, generated timestamps at serialization time, or other nondeterministic fields. + +A conformance fixture should serialize a fully populated event to an exact expected dictionary. A second round-trip validation test should construct an equivalent event from the serialized dictionary and confirm semantic equality without field loss or reinterpretation. + +This first suite establishes **schema/serialization conformance for ACP-native events only**. It is not a cross-runtime portability suite until a materially different adapter is implemented and executes the same fixtures. + +## Error handling + +Construction or conversion must raise `ContractValidationError` for: + +- blank required identity fields; +- unsupported schema version; +- blank parent span when provided; +- span self-parenting; +- malformed artifact SHA-256; +- negative monotonic value; +- non-UTC or naive event timestamps; +- `run_id` mismatch during provenance mapping. + +No invalid required value may be replaced automatically with a generated identifier. + +## Compatibility + +The existing public imports and current kernel behavior remain unchanged in this slice. + +`agent-control-plane.provenance.v1` remains exportable. The new contract is an additional representation and does not silently change the manifest schema. + +A later explicit migration may version the provenance manifest to include execution/trace data after conformance evidence exists. That migration is outside this design. + +## Test strategy + +Use TDD for each behavior. The minimum tests are: + +1. valid execution identity construction and deterministic serialization; +2. blank/unsupported identity rejection; +3. valid trace construction; +4. blank trace ID/span ID and self-parent rejection; +5. component identity serialization without persona dependence; +6. artifact hash validation; +7. deterministic full event serialization with canonical `Z` timestamp; +8. rejection of negative monotonic values; +9. rejection of naive/non-UTC timestamps; +10. provenance-to-contract mapping preserves event/task/run/capability/state/detail; +11. provenance mapping rejects run mismatch; +12. provenance mapping maps absent source state exactly to `unspecified`; +13. serialized-dictionary round-trip preserves semantic equality; +14. existing ACP test suite remains green without public-behavior changes. + +Tests must assert real behavior, not implementation-specific mocks. + +## Acceptance criteria for this slice + +This design slice may be described as implemented only when: + +- the contract types and validation rules exist in source; +- required TDD red/green evidence has been observed during implementation; +- deterministic serialization tests pass; +- current provenance can be mapped without modifying the existing manifest schema; +- the full existing test suite passes; +- CI passes on the exact implementation head; +- documentation states that portability and empirical efficacy remain not established. + +Even after those criteria pass, the ADR remains a candidate until its broader acceptance gate is met, including materially different adapters using the same core schema without fork. + +## Follow-on sequence + +After this slice is accepted: + +1. define a minimal adapter protocol that consumes/emits the stable contract; +2. implement one ACP-native reference adapter if a distinct adapter surface is still needed; +3. implement one materially different external/runtime adapter; +4. run the same conformance fixtures across both; +5. only then evaluate a portability claim; +6. separately design in-execution/post-execution policy phases, evaluator attachment, persistence, and advanced bounded-execution capabilities as evidence justifies them. + +## Evidence ceiling + +The strongest claim this slice can support is: + +> ACP implements and tests a versioned framework-neutral execution/trace contract for its native kernel representation, with deterministic serialization and fail-closed validation under the tested conditions. + +It cannot establish: + +- general runtime portability; +- DGAF authorization; +- PDMAL efficacy; +- secure or tamper-evident provenance; +- distributed reliability; +- production readiness; +- superiority to existing orchestration standards or frameworks. diff --git a/src/agent_control_plane/contract/__init__.py b/src/agent_control_plane/contract/__init__.py new file mode 100644 index 0000000..76187ce --- /dev/null +++ b/src/agent_control_plane/contract/__init__.py @@ -0,0 +1,23 @@ +"""Public execution-contract API.""" + +from .mapping import map_provenance_event +from .model import ( + SCHEMA_VERSION, + ArtifactRef, + ComponentIdentity, + ContractValidationError, + ExecutionEvent, + ExecutionIdentity, + TraceContext, +) + +__all__ = [ + "SCHEMA_VERSION", + "ArtifactRef", + "ComponentIdentity", + "ContractValidationError", + "ExecutionEvent", + "ExecutionIdentity", + "TraceContext", + "map_provenance_event", +] diff --git a/src/agent_control_plane/contract/mapping.py b/src/agent_control_plane/contract/mapping.py new file mode 100644 index 0000000..38febd7 --- /dev/null +++ b/src/agent_control_plane/contract/mapping.py @@ -0,0 +1,47 @@ +"""Explicit mapping from legacy ACP provenance into the execution contract.""" + +from typing import Iterable, Optional + +from ..provenance import ProvenanceEvent +from .model import ( + ArtifactRef, + ComponentIdentity, + ContractValidationError, + ExecutionEvent, + ExecutionIdentity, + TraceContext, +) + + +def map_provenance_event( + event: ProvenanceEvent, + *, + identity: ExecutionIdentity, + trace: TraceContext, + component: ComponentIdentity, + monotonic_ns: int, + input_artifacts: Iterable[ArtifactRef] = (), + output_artifacts: Iterable[ArtifactRef] = (), + policy_decision_ref: Optional[str] = None, +) -> ExecutionEvent: + """Convert one legacy provenance event without fabricating missing context.""" + if event.run_id != identity.run_id: + raise ContractValidationError( + f"provenance run_id {event.run_id!r} does not match contract run_id {identity.run_id!r}" + ) + + return ExecutionEvent( + event_type=event.event, + identity=identity, + trace=trace, + component=component, + task_id=event.task_id, + status=event.state if event.state is not None else "unspecified", + utc_timestamp=event.timestamp, + monotonic_ns=monotonic_ns, + capability=event.capability, + policy_decision_ref=policy_decision_ref, + input_artifacts=tuple(input_artifacts), + output_artifacts=tuple(output_artifacts), + detail=event.detail, + ) diff --git a/src/agent_control_plane/contract/model.py b/src/agent_control_plane/contract/model.py new file mode 100644 index 0000000..7d61d62 --- /dev/null +++ b/src/agent_control_plane/contract/model.py @@ -0,0 +1,205 @@ +"""Framework-neutral execution contract primitives.""" + +from dataclasses import asdict, dataclass +from datetime import datetime, timedelta, timezone +import re +from typing import Any, Dict, Mapping, Optional, Tuple + +SCHEMA_VERSION = "agent-control-plane.execution.v1" +_SHA256_RE = re.compile(r"^[0-9a-f]{64}$") + + +class ContractValidationError(ValueError): + """Raised when required execution-contract data is invalid.""" + + +def _required(value: str, field_name: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ContractValidationError(f"{field_name} must not be blank") + return value + + +def _canonical_utc_timestamp(value: str) -> str: + _required(value, "utc_timestamp") + candidate = value[:-1] + "+00:00" if value.endswith("Z") else value + try: + parsed = datetime.fromisoformat(candidate) + except ValueError as exc: + raise ContractValidationError("utc_timestamp must be valid ISO-8601") from exc + if parsed.tzinfo is None or parsed.utcoffset() is None: + raise ContractValidationError("utc_timestamp must be timezone-aware UTC") + if parsed.utcoffset() != timedelta(0): + raise ContractValidationError("utc_timestamp must use UTC") + return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +@dataclass(frozen=True) +class ExecutionIdentity: + execution_id: str + run_id: str + schema_version: str = SCHEMA_VERSION + + def __post_init__(self) -> None: + _required(self.execution_id, "execution_id") + _required(self.run_id, "run_id") + if self.schema_version != SCHEMA_VERSION: + raise ContractValidationError(f"unsupported schema_version: {self.schema_version}") + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class TraceContext: + trace_id: str + span_id: str + parent_span_id: Optional[str] = None + + def __post_init__(self) -> None: + _required(self.trace_id, "trace_id") + _required(self.span_id, "span_id") + if self.parent_span_id is not None: + _required(self.parent_span_id, "parent_span_id") + if self.parent_span_id == self.span_id: + raise ContractValidationError("span_id must not equal parent_span_id") + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class ComponentIdentity: + component_id: str + component_type: str + runtime_id: str + adapter_id: str + version: Optional[str] = None + source_ref: Optional[str] = None + + def __post_init__(self) -> None: + _required(self.component_id, "component_id") + _required(self.component_type, "component_type") + _required(self.runtime_id, "runtime_id") + _required(self.adapter_id, "adapter_id") + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class ArtifactRef: + artifact_id: str + kind: str + uri: Optional[str] = None + version: Optional[str] = None + sha256: Optional[str] = None + + def __post_init__(self) -> None: + _required(self.artifact_id, "artifact_id") + _required(self.kind, "kind") + if self.sha256 is not None: + if not isinstance(self.sha256, str) or _SHA256_RE.fullmatch(self.sha256) is None: + raise ContractValidationError("sha256 must be 64 lowercase hexadecimal characters") + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +@dataclass(frozen=True) +class ExecutionEvent: + event_type: str + identity: ExecutionIdentity + trace: TraceContext + component: ComponentIdentity + task_id: str + status: str + utc_timestamp: str + monotonic_ns: int + capability: Optional[str] = None + policy_decision_ref: Optional[str] = None + input_artifacts: Tuple[ArtifactRef, ...] = () + output_artifacts: Tuple[ArtifactRef, ...] = () + detail: Optional[str] = None + + def __post_init__(self) -> None: + _required(self.event_type, "event_type") + _required(self.task_id, "task_id") + _required(self.status, "status") + if not isinstance(self.identity, ExecutionIdentity): + raise ContractValidationError("identity must be ExecutionIdentity") + if not isinstance(self.trace, TraceContext): + raise ContractValidationError("trace must be TraceContext") + if not isinstance(self.component, ComponentIdentity): + raise ContractValidationError("component must be ComponentIdentity") + if not isinstance(self.input_artifacts, tuple) or not all( + isinstance(artifact, ArtifactRef) for artifact in self.input_artifacts + ): + raise ContractValidationError("input_artifacts must be a tuple of ArtifactRef") + if not isinstance(self.output_artifacts, tuple) or not all( + isinstance(artifact, ArtifactRef) for artifact in self.output_artifacts + ): + raise ContractValidationError("output_artifacts must be a tuple of ArtifactRef") + if isinstance(self.monotonic_ns, bool) or not isinstance(self.monotonic_ns, int): + raise ContractValidationError("monotonic_ns must be an integer >= 0") + if self.monotonic_ns < 0: + raise ContractValidationError("monotonic_ns must be an integer >= 0") + object.__setattr__(self, "utc_timestamp", _canonical_utc_timestamp(self.utc_timestamp)) + + def to_dict(self) -> Dict[str, Any]: + return { + "event_type": self.event_type, + "identity": self.identity.to_dict(), + "trace": self.trace.to_dict(), + "component": self.component.to_dict(), + "task_id": self.task_id, + "status": self.status, + "utc_timestamp": self.utc_timestamp, + "monotonic_ns": self.monotonic_ns, + "capability": self.capability, + "policy_decision_ref": self.policy_decision_ref, + "input_artifacts": [artifact.to_dict() for artifact in self.input_artifacts], + "output_artifacts": [artifact.to_dict() for artifact in self.output_artifacts], + "detail": self.detail, + } + + @classmethod + def from_dict(cls, data: Mapping[str, Any]) -> "ExecutionEvent": + """Reconstruct a serialized event through the same fail-closed validators.""" + if not isinstance(data, Mapping): + raise ContractValidationError("execution event payload must be a mapping") + try: + identity_data = data["identity"] + trace_data = data["trace"] + component_data = data["component"] + input_artifacts_data = data.get("input_artifacts", ()) + output_artifacts_data = data.get("output_artifacts", ()) + if not isinstance(identity_data, Mapping): + raise ContractValidationError("identity must be a mapping") + if not isinstance(trace_data, Mapping): + raise ContractValidationError("trace must be a mapping") + if not isinstance(component_data, Mapping): + raise ContractValidationError("component must be a mapping") + if not isinstance(input_artifacts_data, (list, tuple)): + raise ContractValidationError("input_artifacts must be a sequence") + if not isinstance(output_artifacts_data, (list, tuple)): + raise ContractValidationError("output_artifacts must be a sequence") + + return cls( + event_type=data["event_type"], + identity=ExecutionIdentity(**dict(identity_data)), + trace=TraceContext(**dict(trace_data)), + component=ComponentIdentity(**dict(component_data)), + task_id=data["task_id"], + status=data["status"], + utc_timestamp=data["utc_timestamp"], + monotonic_ns=data["monotonic_ns"], + capability=data.get("capability"), + policy_decision_ref=data.get("policy_decision_ref"), + input_artifacts=tuple(ArtifactRef(**dict(item)) for item in input_artifacts_data), + output_artifacts=tuple(ArtifactRef(**dict(item)) for item in output_artifacts_data), + detail=data.get("detail"), + ) + except ContractValidationError: + raise + except (KeyError, TypeError, ValueError) as exc: + raise ContractValidationError("malformed execution event payload") from exc diff --git a/tests/test_contract.py b/tests/test_contract.py new file mode 100644 index 0000000..bb50552 --- /dev/null +++ b/tests/test_contract.py @@ -0,0 +1,326 @@ +import pytest + +from agent_control_plane.contract import ( + SCHEMA_VERSION, + ArtifactRef, + ComponentIdentity, + ContractValidationError, + ExecutionEvent, + ExecutionIdentity, + TraceContext, + map_provenance_event, +) +from agent_control_plane.provenance import ProvenanceEvent + + +def test_execution_identity_serializes_deterministically(): + identity = ExecutionIdentity(execution_id="exec-1", run_id="run-1") + + assert identity.to_dict() == { + "execution_id": "exec-1", + "run_id": "run-1", + "schema_version": SCHEMA_VERSION, + } + + +@pytest.mark.parametrize("field", ["execution_id", "run_id"]) +def test_execution_identity_rejects_blank_required_fields(field): + values = {"execution_id": "exec-1", "run_id": "run-1"} + values[field] = " " + + with pytest.raises(ContractValidationError): + ExecutionIdentity(**values) + + +def test_execution_identity_rejects_unsupported_schema_version(): + with pytest.raises(ContractValidationError): + ExecutionIdentity( + execution_id="exec-1", + run_id="run-1", + schema_version="agent-control-plane.execution.v999", + ) + + +def test_trace_context_serializes_with_optional_parent(): + trace = TraceContext(trace_id="trace-1", span_id="span-1", parent_span_id="span-0") + + assert trace.to_dict() == { + "trace_id": "trace-1", + "span_id": "span-1", + "parent_span_id": "span-0", + } + + +@pytest.mark.parametrize( + "kwargs", + [ + {"trace_id": "", "span_id": "span-1"}, + {"trace_id": "trace-1", "span_id": " "}, + {"trace_id": "trace-1", "span_id": "span-1", "parent_span_id": " "}, + {"trace_id": "trace-1", "span_id": "span-1", "parent_span_id": "span-1"}, + ], +) +def test_trace_context_rejects_invalid_identity(kwargs): + with pytest.raises(ContractValidationError): + TraceContext(**kwargs) + + +def test_component_identity_serializes_without_persona_dependency(): + component = ComponentIdentity( + component_id="kernel", + component_type="kernel", + runtime_id="python", + adapter_id="native-acp", + version="0.1.0", + source_ref="dbab7c1", + ) + + assert component.to_dict() == { + "component_id": "kernel", + "component_type": "kernel", + "runtime_id": "python", + "adapter_id": "native-acp", + "version": "0.1.0", + "source_ref": "dbab7c1", + } + + +@pytest.mark.parametrize("field", ["component_id", "component_type", "runtime_id", "adapter_id"]) +def test_component_identity_rejects_blank_required_fields(field): + values = { + "component_id": "kernel", + "component_type": "kernel", + "runtime_id": "python", + "adapter_id": "native-acp", + } + values[field] = "" + + with pytest.raises(ContractValidationError): + ComponentIdentity(**values) + + +def test_artifact_ref_serializes_valid_sha256(): + artifact = ArtifactRef( + artifact_id="artifact-1", + kind="input", + uri="memory://artifact-1", + version="v1", + sha256="a" * 64, + ) + + assert artifact.to_dict() == { + "artifact_id": "artifact-1", + "kind": "input", + "uri": "memory://artifact-1", + "version": "v1", + "sha256": "a" * 64, + } + + +@pytest.mark.parametrize("sha256", ["A" * 64, "a" * 63, "g" * 64, "not-a-hash", 123]) +def test_artifact_ref_rejects_malformed_sha256(sha256): + with pytest.raises(ContractValidationError): + ArtifactRef(artifact_id="artifact-1", kind="input", sha256=sha256) + + +@pytest.mark.parametrize("field", ["artifact_id", "kind"]) +def test_artifact_ref_rejects_blank_required_fields(field): + values = {"artifact_id": "artifact-1", "kind": "input"} + values[field] = " " + + with pytest.raises(ContractValidationError): + ArtifactRef(**values) + + +def _event_kwargs(): + return { + "event_type": "task.completed", + "identity": ExecutionIdentity(execution_id="exec-1", run_id="run-1"), + "trace": TraceContext(trace_id="trace-1", span_id="span-1"), + "component": ComponentIdentity( + component_id="kernel", + component_type="kernel", + runtime_id="python", + adapter_id="native-acp", + ), + "task_id": "task-1", + "status": "completed", + "utc_timestamp": "2026-09-14T23:00:00+00:00", + "monotonic_ns": 42, + } + + +def test_execution_event_serializes_deterministically_with_canonical_utc(): + input_artifacts = ( + ArtifactRef(artifact_id="input-1", kind="input"), + ArtifactRef(artifact_id="input-2", kind="input"), + ) + output_artifacts = (ArtifactRef(artifact_id="output-1", kind="output"),) + event = ExecutionEvent( + **_event_kwargs(), + capability="echo", + policy_decision_ref="policy-1", + input_artifacts=input_artifacts, + output_artifacts=output_artifacts, + detail="steps=1", + ) + + assert event.to_dict() == { + "event_type": "task.completed", + "identity": { + "execution_id": "exec-1", + "run_id": "run-1", + "schema_version": SCHEMA_VERSION, + }, + "trace": { + "trace_id": "trace-1", + "span_id": "span-1", + "parent_span_id": None, + }, + "component": { + "component_id": "kernel", + "component_type": "kernel", + "runtime_id": "python", + "adapter_id": "native-acp", + "version": None, + "source_ref": None, + }, + "task_id": "task-1", + "status": "completed", + "utc_timestamp": "2026-09-14T23:00:00Z", + "monotonic_ns": 42, + "capability": "echo", + "policy_decision_ref": "policy-1", + "input_artifacts": [ + {"artifact_id": "input-1", "kind": "input", "uri": None, "version": None, "sha256": None}, + {"artifact_id": "input-2", "kind": "input", "uri": None, "version": None, "sha256": None}, + ], + "output_artifacts": [ + {"artifact_id": "output-1", "kind": "output", "uri": None, "version": None, "sha256": None} + ], + "detail": "steps=1", + } + + +def test_execution_event_preserves_artifact_order(): + event = ExecutionEvent( + **_event_kwargs(), + input_artifacts=( + ArtifactRef(artifact_id="first", kind="input"), + ArtifactRef(artifact_id="second", kind="input"), + ), + ) + + assert [item["artifact_id"] for item in event.to_dict()["input_artifacts"]] == ["first", "second"] + + +def test_execution_event_rejects_negative_monotonic_value(): + values = _event_kwargs() + values["monotonic_ns"] = -1 + + with pytest.raises(ContractValidationError): + ExecutionEvent(**values) + + +@pytest.mark.parametrize( + "timestamp", + [ + "2026-09-14T23:00:00", + "2026-09-15T00:00:00+01:00", + ], +) +def test_execution_event_rejects_naive_or_non_utc_timestamp(timestamp): + values = _event_kwargs() + values["utc_timestamp"] = timestamp + + with pytest.raises(ContractValidationError): + ExecutionEvent(**values) + + +@pytest.mark.parametrize( + ("field", "invalid_value"), + [ + ("identity", "not-an-identity"), + ("trace", "not-a-trace"), + ("component", "not-a-component"), + ("input_artifacts", ("not-an-artifact",)), + ("output_artifacts", ("not-an-artifact",)), + ], +) +def test_execution_event_rejects_invalid_nested_contract_types(field, invalid_value): + values = _event_kwargs() + values[field] = invalid_value + + with pytest.raises(ContractValidationError): + ExecutionEvent(**values) + + +def _mapping_context(run_id="run-1"): + return { + "identity": ExecutionIdentity(execution_id="exec-1", run_id=run_id), + "trace": TraceContext(trace_id="trace-1", span_id="span-1"), + "component": ComponentIdentity( + component_id="kernel", + component_type="kernel", + runtime_id="python", + adapter_id="native-acp", + ), + "monotonic_ns": 77, + } + + +def test_map_provenance_event_preserves_legacy_fields_and_context(): + source = ProvenanceEvent( + event="task.completed", + task_id="task-1", + run_id="run-1", + capability="echo", + state="completed", + detail="steps=1", + timestamp="2026-09-14T23:00:00+00:00", + ) + input_artifact = ArtifactRef(artifact_id="input-1", kind="input") + + mapped = map_provenance_event( + source, + **_mapping_context(), + input_artifacts=(input_artifact,), + policy_decision_ref="policy-1", + ) + + assert mapped.event_type == "task.completed" + assert mapped.task_id == "task-1" + assert mapped.identity.run_id == "run-1" + assert mapped.capability == "echo" + assert mapped.status == "completed" + assert mapped.detail == "steps=1" + assert mapped.utc_timestamp == "2026-09-14T23:00:00Z" + assert mapped.monotonic_ns == 77 + assert mapped.input_artifacts == (input_artifact,) + assert mapped.policy_decision_ref == "policy-1" + + +def test_map_provenance_event_maps_absent_state_to_unspecified(): + source = ProvenanceEvent( + event="task.rejected", + task_id="task-1", + run_id="run-1", + timestamp="2026-09-14T23:00:00Z", + ) + + mapped = map_provenance_event(source, **_mapping_context()) + + assert mapped.status == "unspecified" + + +def test_map_provenance_event_rejects_run_mismatch(): + source = ProvenanceEvent( + event="task.started", + task_id="task-1", + run_id="run-source", + state="running", + timestamp="2026-09-14T23:00:00Z", + ) + + with pytest.raises(ContractValidationError): + map_provenance_event(source, **_mapping_context(run_id="run-contract")) diff --git a/tests/test_contract_roundtrip.py b/tests/test_contract_roundtrip.py new file mode 100644 index 0000000..a96e22d --- /dev/null +++ b/tests/test_contract_roundtrip.py @@ -0,0 +1,69 @@ +import pytest + +from agent_control_plane.contract import ( + SCHEMA_VERSION, + ArtifactRef, + ComponentIdentity, + ContractValidationError, + ExecutionEvent, + ExecutionIdentity, + TraceContext, +) + + +def _full_event(): + return ExecutionEvent( + event_type="task.completed", + identity=ExecutionIdentity(execution_id="exec-1", run_id="run-1"), + trace=TraceContext(trace_id="trace-1", span_id="span-1", parent_span_id="span-0"), + component=ComponentIdentity( + component_id="kernel", + component_type="kernel", + runtime_id="python", + adapter_id="native-acp", + version="0.1.0", + source_ref="commit:dbab7c1", + ), + task_id="task-1", + status="completed", + utc_timestamp="2026-09-14T23:00:00Z", + monotonic_ns=123, + capability="echo", + policy_decision_ref="policy-1", + input_artifacts=( + ArtifactRef( + artifact_id="input-1", + kind="input", + uri="memory://input-1", + version="v1", + sha256="a" * 64, + ), + ), + output_artifacts=(ArtifactRef(artifact_id="output-1", kind="output"),), + detail="steps=1", + ) + + +def test_execution_event_round_trip_preserves_semantics(): + event = _full_event() + + reconstructed = ExecutionEvent.from_dict(event.to_dict()) + + assert reconstructed == event + assert reconstructed.to_dict() == event.to_dict() + + +def test_execution_event_from_dict_rejects_unsupported_schema_version(): + payload = _full_event().to_dict() + payload["identity"]["schema_version"] = "agent-control-plane.execution.v999" + + with pytest.raises(ContractValidationError): + ExecutionEvent.from_dict(payload) + + +def test_execution_event_from_dict_rejects_malformed_structure_fail_closed(): + payload = _full_event().to_dict() + del payload["trace"]["span_id"] + + with pytest.raises(ContractValidationError): + ExecutionEvent.from_dict(payload)