From c0ce81e3fdb361ce0b896bc99eeb124a68cbdfde Mon Sep 17 00:00:00 2001 From: abrichr Date: Sat, 29 Aug 2026 15:01:39 -0400 Subject: [PATCH] feat: add a Tier-2 oracle adapter and refuse visual VERIFIED A partner implements channel plus read (API, DB, file, ack, or a second session). Production Execute receipts require oracle tier 2 or 3. Visual and OCR (tier 0) cannot mint VERIFIED. --- README.md | 8 +- docs/CONTRACTS.md | 9 ++ docs/ORACLE.md | 58 +++++++++ examples/oracle/README.md | 13 ++ examples/oracle/file_oracle.py | 24 ++++ openadapt_types/__init__.py | 27 ++++- openadapt_types/execute.py | 24 ++-- openadapt_types/oracle.py | 152 +++++++++++++++++++++++ tests/test_oracle.py | 213 +++++++++++++++++++++++++++++++++ 9 files changed, 512 insertions(+), 16 deletions(-) create mode 100644 docs/ORACLE.md create mode 100644 examples/oracle/README.md create mode 100644 examples/oracle/file_oracle.py create mode 100644 openadapt_types/oracle.py create mode 100644 tests/test_oracle.py diff --git a/README.md b/README.md index 43230d7..14b799e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ out the pixels. Coordinates are the thing that breaks when a window moves. | `ActionResult` | The outcome, with an error taxonomy and a state delta | | `Episode` / `Step` | A whole trajectory: observation, action, result | | `FailureRecord` | A classified failure, for dataset pipelines | +| `OracleObservation` | One independent effect read. Production `VERIFIED` needs tier 2 or 3 | Plus the versioned wire contracts: `ControlOverlayFrameV1`/`V2` and `ControlOverlayTimelineV1`/`V2` for PHI-safe execution overlays, @@ -76,7 +77,8 @@ Plus the versioned wire contracts: `ControlOverlayFrameV1`/`V2` and asynchronous qualified execution, `EffectStrengthV1`, and the `BusinessDecision*V1` family for signed, finite human choices. What those contracts may and may not carry is in -[docs/CONTRACTS.md](docs/CONTRACTS.md). +[docs/CONTRACTS.md](docs/CONTRACTS.md). Oracle tiers and the ten-line +adapter are in [docs/ORACLE.md](docs/ORACLE.md). ## JSON Schema for everything else @@ -111,7 +113,9 @@ state = from_benchmark_observation(obs.__dict__) A partner sends an authorized request, gets an execution ID back, and then either reads a terminal receipt or waits for a signed webhook. The contract exposes no runner, no customer data, no evidence bytes, and no control-plane -internals. +internals. A `verified` receipt needs an oracle at tier 2 or 3 (API, DB, +file, ack, or a counterparty artifact). Visual and OCR reads are tier 0 +and cannot mint it. ```python from openadapt_types import ExecuteClient diff --git a/docs/CONTRACTS.md b/docs/CONTRACTS.md index 7e71a7f..047201f 100644 --- a/docs/CONTRACTS.md +++ b/docs/CONTRACTS.md @@ -69,3 +69,12 @@ client = ExecuteClient( The client does not poll forever. A workflow can wait for a human decision or reconciliation. Use the terminal receipt or signed webhook as the completion signal. + +## Oracle tiers + +`verified` and `rolled_back_verified` require an observed effect strength +that maps to Seal oracle tier 2 or 3. Tier 0 is visual / OCR. Tier 1 is a +second-session UI read. Neither can mint production `VERIFIED`. + +The partner adapter is `channel` plus `read`. Tiers, the gate, and a +ten-line file oracle are in [ORACLE.md](ORACLE.md). diff --git a/docs/ORACLE.md b/docs/ORACLE.md new file mode 100644 index 0000000..d203b0e --- /dev/null +++ b/docs/ORACLE.md @@ -0,0 +1,58 @@ +# Oracle adapters + +A production `VERIFIED` receipt needs an independent read of the system of +record. The click is the commodity. The read is what a Seal is for. + +## Tiers + +| Tier | What you read | Production `VERIFIED` | +| --- | --- | --- | +| 0 | Pixels, OCR, same-surface banner | No | +| 1 | Second session / independent UI | No | +| 2 | API, DB, file, ack | Yes, if the other contracts pass | +| 3 | Counterparty artifact (payer status, legal export) | Yes, if the other contracts pass | + +Tier 0 is for local development. `issue_production_verified` raises +`ProductionSealRefused` for tier 0 and tier 1. The Execute receipt does +the same: `verified` and `rolled_back_verified` require observed effect +strength that maps to tier 2 or 3. + +This numbering is the Seal ladder. `EffectVerificationTier` counts the +other way (1 is strongest there). Use `OracleTier` when you talk about a +Seal. + +## Adapter + +Implement `channel` and `read`. That's it. + +```python +from pathlib import Path +import json +from openadapt_types import OracleChannel, OracleObservation + +class FileStatusOracle: + channel = OracleChannel.FILE + + def __init__(self, path: Path) -> None: + self.path = path + + def read(self, identity): + rec = json.loads(self.path.read_text(encoding="utf-8"))[identity["record_id"]] + return OracleObservation( + channel=self.channel, + identity={"record_id": identity["record_id"]}, + value={"status": rec["status"]}, + ) +``` + +API, DB, ack, and second-session adapters keep that shape. The channel +sets the tier. A visual adapter that stuffs a JSON body into `value` +is still tier 0. + +Worked file: [`examples/oracle/file_oracle.py`](../examples/oracle/file_oracle.py). + +Don't put a per-vendor recipe in this repository. The interface is +public. Productionized connector recipes stay private. + +Flow will call this contract on the Execute path. This package holds the +adapter and the Seal gate. It does not run the GUI. diff --git a/examples/oracle/README.md b/examples/oracle/README.md new file mode 100644 index 0000000..686dcdf --- /dev/null +++ b/examples/oracle/README.md @@ -0,0 +1,13 @@ +# Oracle adapter + +A production `VERIFIED` receipt needs a read that did not come from the +acting screen. The adapter is `channel` plus `read`. That's the whole +interface. + +Tiers are in [`docs/ORACLE.md`](../../docs/ORACLE.md). Tier 0 (pixels, OCR) +cannot mint production `VERIFIED`. Tier 2 is an API, DB, file, or ack +read. Tier 1 is a second session; same adapter, no production Seal. + +`file_oracle.py` is the file-channel shape. Swap the body of `read` for an +HTTP GET, a SQL SELECT, or a second-session UI query. Do not copy a +vendor recipe into this package. diff --git a/examples/oracle/file_oracle.py b/examples/oracle/file_oracle.py new file mode 100644 index 0000000..ca7ed6f --- /dev/null +++ b/examples/oracle/file_oracle.py @@ -0,0 +1,24 @@ +"""A ten-line Tier-2 oracle. The file is the system of record, not the screen.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Mapping + +from openadapt_types import OracleChannel, OracleObservation + + +class FileStatusOracle: + channel = OracleChannel.FILE + + def __init__(self, path: Path) -> None: + self.path = path + + def read(self, identity: Mapping[str, str]) -> OracleObservation: + rec = json.loads(self.path.read_text(encoding="utf-8"))[identity["record_id"]] + return OracleObservation( + channel=self.channel, + identity={"record_id": identity["record_id"]}, + value={"status": rec["status"]}, + ) diff --git a/openadapt_types/__init__.py b/openadapt_types/__init__.py index 01e8182..7235c2b 100644 --- a/openadapt_types/__init__.py +++ b/openadapt_types/__init__.py @@ -121,7 +121,6 @@ EXECUTE_WEBHOOK_SCHEMA, EffectStrengthV1, OracleTierV1, - oracle_tier_from_effect_strength, ExecuteAcceptedV1, ExecuteAuthorizationContextV1, ExecuteDecisionRequiredWebhookV1, @@ -140,6 +139,19 @@ ) from openadapt_types.execute_client import ExecuteApiError, ExecuteClient from openadapt_types.execute_openapi import execute_openapi_document +from openadapt_types.oracle import ( + OracleAdapter, + OracleChannel, + OracleObservation, + OracleTier, + ProductionSealRefused, + Tier2Oracle, + issue_production_verified, + oracle_tier_from_effect_strength, + production_seal_allowed, + refuse_production_verified, + tier_of, +) from openadapt_types.execution_requirements import ( CAPABILITY_MATCH_SCHEMA, EXECUTION_REQUIREMENTS_SCHEMA, @@ -301,7 +313,6 @@ "EXECUTE_WEBHOOK_SCHEMA", "EffectStrengthV1", "OracleTierV1", - "oracle_tier_from_effect_strength", "ExecuteAcceptedV1", "ExecuteAuthorizationContextV1", "ExecuteDecisionRequiredWebhookV1", @@ -320,6 +331,18 @@ "execute_openapi_document", "ExecuteApiError", "ExecuteClient", + # oracle adapter + "OracleAdapter", + "OracleChannel", + "OracleObservation", + "OracleTier", + "ProductionSealRefused", + "Tier2Oracle", + "issue_production_verified", + "oracle_tier_from_effect_strength", + "production_seal_allowed", + "refuse_production_verified", + "tier_of", # runner capability and execution requirements "CAPABILITY_MATCH_SCHEMA", "EXECUTION_REQUIREMENTS_SCHEMA", diff --git a/openadapt_types/execute.py b/openadapt_types/execute.py index 8a8ec36..26aaba9 100644 --- a/openadapt_types/execute.py +++ b/openadapt_types/execute.py @@ -31,6 +31,10 @@ from openadapt_types.business_decision import BusinessDecisionTaskV1 from openadapt_types.human_decision import HumanDecisionTaskV1 +from openadapt_types.oracle import ( + oracle_tier_from_effect_strength, + production_seal_allowed, +) EXECUTE_REQUEST_SCHEMA = "openadapt.execute-request/v1" EXECUTE_ACCEPTED_SCHEMA = "openadapt.execute-accepted/v1" @@ -72,21 +76,11 @@ class EffectStrengthV1(str, Enum): # Seal oracle ladder. 0 visual, 1 second-session UI, 2 SoR, 3 counterparty. # Production Seals require 2 or 3. Tier 3 has no EffectStrengthV1 member yet. +# Mapping and production_seal_allowed live in oracle.py; this alias is the +# receipt field so OpenAPI stays an integer enum. OracleTierV1: TypeAlias = Literal[0, 1, 2, 3] -def oracle_tier_from_effect_strength( - strength: EffectStrengthV1 | None, -) -> OracleTierV1: - """Map Execute effect strength onto the Seal oracle ladder.""" - - if strength is EffectStrengthV1.INDEPENDENT_SYSTEM_OF_RECORD: - return 2 - if strength is EffectStrengthV1.INDEPENDENT_SESSION: - return 1 - return 0 - - class ExecuteLifecycleStateV1(str, Enum): QUEUED = "queued" RUNNING = "running" @@ -217,6 +211,12 @@ def _validate_proof(self) -> "ExecuteEvidenceReceiptV1": < _EFFECT_STRENGTH_RANK[self.contracts.minimum_effect_strength] ): raise ValueError("observed effect strength is below the required strength") + if not production_seal_allowed( + oracle_tier_from_effect_strength( + self.contracts.observed_effect_strength + ) + ): + raise ValueError("a verified outcome requires oracle tier 2 or 3") if self.outcome is ExecuteTerminalOutcomeV1.ROLLED_BACK_VERIFIED: if not self.compensation_effect_verified: raise ValueError("rolled_back_verified requires independently verified compensation") diff --git a/openadapt_types/oracle.py b/openadapt_types/oracle.py new file mode 100644 index 0000000..ef90cdf --- /dev/null +++ b/openadapt_types/oracle.py @@ -0,0 +1,152 @@ +"""Partner oracle adapter: one read, a tier, a production Seal gate. + +This module is the public interface. It does not contain per-system-of-record +recipes, thresholds, or connector credentials. A partner implements +``channel`` and ``read``. Flow consumes the same contract later. + +Seal ladder (higher is stronger): + +* 0 visual / OCR. Local development. Never a production ``VERIFIED``. +* 1 second session / independent UI read. Not a production Seal. +* 2 system-of-record read (API, DB, file, ack). +* 3 counterparty artifact (payer status, legal export). + +This numbering is the Seal ladder. It is not +``EffectVerificationTier``, which counts the other way. +""" + +from __future__ import annotations + +from enum import Enum, IntEnum +from typing import Mapping, Protocol, runtime_checkable + +from pydantic import ( + BaseModel, + ConfigDict, + Field, + JsonValue, + StrictStr, + model_validator, +) + +PRODUCTION_SEAL_MINIMUM_TIER = 2 + + +class OracleTier(IntEnum): + """Seal oracle ladder. Higher is stronger. Production Seals require 2 or 3.""" + + VISUAL = 0 + INDEPENDENT_SESSION = 1 + SYSTEM_OF_RECORD = 2 + COUNTERPARTY = 3 + + +class OracleChannel(str, Enum): + """How the oracle read the effect. The channel, not the payload, sets the tier.""" + + VISUAL = "visual" + OCR = "ocr" + SECOND_SESSION = "second_session" + API = "api" + DB = "db" + FILE = "file" + ACK = "ack" + COUNTERPARTY = "counterparty" + + +_CHANNEL_TIER: Mapping[OracleChannel, OracleTier] = { + OracleChannel.VISUAL: OracleTier.VISUAL, + OracleChannel.OCR: OracleTier.VISUAL, + OracleChannel.SECOND_SESSION: OracleTier.INDEPENDENT_SESSION, + OracleChannel.API: OracleTier.SYSTEM_OF_RECORD, + OracleChannel.DB: OracleTier.SYSTEM_OF_RECORD, + OracleChannel.FILE: OracleTier.SYSTEM_OF_RECORD, + OracleChannel.ACK: OracleTier.SYSTEM_OF_RECORD, + OracleChannel.COUNTERPARTY: OracleTier.COUNTERPARTY, +} + +_EFFECT_STRENGTH_TO_TIER: Mapping[str, OracleTier] = { + "independent_system_of_record": OracleTier.SYSTEM_OF_RECORD, + "independent_session": OracleTier.INDEPENDENT_SESSION, +} + + +class ProductionSealRefused(ValueError): + """Raised when a production ``VERIFIED`` stamp is requested below tier 2.""" + + +class OracleObservation(BaseModel): + """One read-only observation. The channel decides the tier.""" + + model_config = ConfigDict(extra="forbid", frozen=True) + + channel: OracleChannel + identity: dict[str, StrictStr] = Field(min_length=1) + value: dict[str, JsonValue] + + @model_validator(mode="after") + def _identity_keys_are_present(self) -> "OracleObservation": + if any(not key for key in self.identity): + raise ValueError("oracle identity keys must be non-empty") + return self + + @property + def tier(self) -> OracleTier: + return _CHANNEL_TIER[self.channel] + + +@runtime_checkable +class OracleAdapter(Protocol): + """Read-only effect check. Implement ``channel`` and ``read``. + + Tier-2 channels are ``api``, ``db``, ``file``, and ``ack``. + ``second_session`` uses the same adapter and classifies as tier 1. + """ + + channel: OracleChannel + + def read(self, identity: Mapping[str, str]) -> OracleObservation: ... + + +Tier2Oracle = OracleAdapter + + +def tier_of(channel: OracleChannel) -> OracleTier: + """Return the Seal tier for one channel.""" + + return _CHANNEL_TIER[channel] + + +def oracle_tier_from_effect_strength(strength: object | None) -> OracleTier: + """Map an Execute ``EffectStrengthV1`` value onto the Seal ladder.""" + + name = getattr(strength, "value", strength) + if not isinstance(name, str): + return OracleTier.VISUAL + return _EFFECT_STRENGTH_TO_TIER.get(name, OracleTier.VISUAL) + + +def production_seal_allowed(tier: OracleTier | int) -> bool: + """True when ``tier`` may stamp a production ``VERIFIED`` Seal.""" + + return int(tier) >= PRODUCTION_SEAL_MINIMUM_TIER + + +def refuse_production_verified(tier: OracleTier | int) -> None: + """Raise if ``tier`` cannot mint production ``VERIFIED``.""" + + if not production_seal_allowed(tier): + raise ProductionSealRefused( + "a verified outcome requires oracle tier 2 or 3" + ) + + +def issue_production_verified(observation: OracleObservation) -> OracleTier: + """Return ``observation.tier`` if it may stamp production ``VERIFIED``. + + Visual / OCR (tier 0) and second-session UI (tier 1) raise + ``ProductionSealRefused``. The payload cannot upgrade the channel. + """ + + refuse_production_verified(observation.tier) + return observation.tier diff --git a/tests/test_oracle.py b/tests/test_oracle.py new file mode 100644 index 0000000..91ddb57 --- /dev/null +++ b/tests/test_oracle.py @@ -0,0 +1,213 @@ +"""A visual-only oracle cannot mint production VERIFIED.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path +from typing import Mapping + +import pytest +from pydantic import ValidationError + +from openadapt_types import ( + EffectStrengthV1, + ExecuteEvidenceContractV1, + ExecuteEvidenceReceiptV1, + ExecuteTerminalOutcomeV1, + OracleAdapter, + OracleChannel, + OracleObservation, + OracleTier, + ProductionSealRefused, + issue_production_verified, + oracle_tier_from_effect_strength, + production_seal_allowed, + refuse_production_verified, + tier_of, +) + + +class BannerOracle: + """Tier-0: the acting screen said saved.""" + + channel = OracleChannel.OCR + + def read(self, identity: Mapping[str, str]) -> OracleObservation: + return OracleObservation( + channel=self.channel, + identity={"record_id": identity["record_id"]}, + value={"banner": "Saved"}, + ) + + +class FileStatusOracle: + """Tier-2: a JSON file is the system of record.""" + + channel = OracleChannel.FILE + + def __init__(self, path: Path) -> None: + self._path = path + + def read(self, identity: Mapping[str, str]) -> OracleObservation: + records = json.loads(self._path.read_text(encoding="utf-8")) + record_id = identity["record_id"] + return OracleObservation( + channel=self.channel, + identity={"record_id": record_id}, + value={"status": records[record_id]["status"]}, + ) + + +class SecondSessionOracle: + channel = OracleChannel.SECOND_SESSION + + def read(self, identity: Mapping[str, str]) -> OracleObservation: + return OracleObservation( + channel=self.channel, + identity={"record_id": identity["record_id"]}, + value={"status": "posted"}, + ) + + +def _contract(**updates: object) -> ExecuteEvidenceContractV1: + fields: dict[str, object] = { + "authorization_passed": True, + "identity_passed": True, + "postcondition_passed": True, + "effect_passed": True, + "minimum_effect_strength": EffectStrengthV1.IMMEDIATE_SCREEN_CONFIRMATION, + "observed_effect_strength": EffectStrengthV1.IMMEDIATE_SCREEN_CONFIRMATION, + "model_used": False, + "external_network_used": False, + } + fields.update(updates) + return ExecuteEvidenceContractV1.model_validate(fields) + + +def _receipt(**updates: object) -> ExecuteEvidenceReceiptV1: + fields: dict[str, object] = { + "receipt_id": "receipt_12345678", + "execution_id": "execution_12345678", + "workflow_digest": "sha256:" + "a" * 64, + "workflow_version": "workflow_20260729", + "qualification_id": "qualification_12345678", + "environment_id": "environment_12345678", + "runner_id": "runner:hosted", + "nonce": "nonce:execution_12345678", + "oracle_tier": 2, + "outcome": ExecuteTerminalOutcomeV1.VERIFIED, + "contracts": _contract( + minimum_effect_strength=EffectStrengthV1.INDEPENDENT_SYSTEM_OF_RECORD, + observed_effect_strength=EffectStrengthV1.INDEPENDENT_SYSTEM_OF_RECORD, + ), + "delivery_uncertain": False, + "evidence_digest": "sha256:" + "b" * 64, + "issued_at": "2026-07-29T12:00:00Z", + } + fields.update(updates) + return ExecuteEvidenceReceiptV1.model_validate(fields) + + +def test_channel_sets_the_tier_not_the_payload() -> None: + visual = OracleObservation( + channel=OracleChannel.VISUAL, + identity={"record_id": "demo-1"}, + value={"status": "posted", "so_r_looking": True}, + ) + assert visual.tier is OracleTier.VISUAL + assert tier_of(OracleChannel.FILE) is OracleTier.SYSTEM_OF_RECORD + assert tier_of(OracleChannel.SECOND_SESSION) is OracleTier.INDEPENDENT_SESSION + assert not production_seal_allowed(visual.tier) + + +def test_visual_only_oracle_cannot_mint_production_verified() -> None: + obs = BannerOracle().read({"record_id": "demo-1"}) + assert isinstance(BannerOracle(), OracleAdapter) + assert obs.tier is OracleTier.VISUAL + with pytest.raises(ProductionSealRefused, match="oracle tier 2 or 3"): + issue_production_verified(obs) + with pytest.raises(ProductionSealRefused, match="oracle tier 2 or 3"): + refuse_production_verified(OracleTier.VISUAL) + + with pytest.raises(ValidationError, match="oracle tier 2 or 3"): + _receipt( + contracts=_contract( + minimum_effect_strength=EffectStrengthV1.IMMEDIATE_SCREEN_CONFIRMATION, + observed_effect_strength=EffectStrengthV1.IMMEDIATE_SCREEN_CONFIRMATION, + ), + oracle_tier=0, + ) + + +def test_ocr_and_persisted_screen_readback_are_tier_zero() -> None: + assert oracle_tier_from_effect_strength( + EffectStrengthV1.IMMEDIATE_SCREEN_CONFIRMATION + ) is OracleTier.VISUAL + assert oracle_tier_from_effect_strength( + EffectStrengthV1.PERSISTED_STATE_REACQUISITION + ) is OracleTier.VISUAL + with pytest.raises(ValidationError, match="oracle tier 2 or 3"): + _receipt( + contracts=_contract( + minimum_effect_strength=EffectStrengthV1.PERSISTED_STATE_REACQUISITION, + observed_effect_strength=EffectStrengthV1.PERSISTED_STATE_REACQUISITION, + ), + oracle_tier=0, + ) + + +def test_second_session_adapter_is_valid_and_cannot_seal() -> None: + obs = SecondSessionOracle().read({"record_id": "demo-1"}) + assert obs.tier is OracleTier.INDEPENDENT_SESSION + with pytest.raises(ProductionSealRefused, match="oracle tier 2 or 3"): + issue_production_verified(obs) + with pytest.raises(ValidationError, match="oracle tier 2 or 3"): + _receipt( + contracts=_contract( + minimum_effect_strength=EffectStrengthV1.INDEPENDENT_SESSION, + observed_effect_strength=EffectStrengthV1.INDEPENDENT_SESSION, + ), + oracle_tier=1, + ) + + +def test_file_oracle_can_issue_production_verified(tmp_path: Path) -> None: + store = tmp_path / "claims.json" + store.write_text( + json.dumps({"demo-1": {"status": "posted"}}), + encoding="utf-8", + ) + oracle = FileStatusOracle(store) + obs = oracle.read({"record_id": "demo-1"}) + assert isinstance(oracle, OracleAdapter) + assert obs.tier is OracleTier.SYSTEM_OF_RECORD + assert obs.value == {"status": "posted"} + assert issue_production_verified(obs) is OracleTier.SYSTEM_OF_RECORD + receipt = _receipt() + assert receipt.outcome is ExecuteTerminalOutcomeV1.VERIFIED + + +def test_visual_observation_may_reconcile_but_not_verify() -> None: + receipt = _receipt( + outcome=ExecuteTerminalOutcomeV1.RECONCILIATION_REQUIRED, + delivery_uncertain=True, + contracts=_contract(effect_passed=False), + oracle_tier=0, + ) + assert receipt.outcome is ExecuteTerminalOutcomeV1.RECONCILIATION_REQUIRED + + +def test_shipped_file_oracle_example_is_tier_two(tmp_path: Path) -> None: + store = tmp_path / "claims.json" + store.write_text( + json.dumps({"demo-1": {"status": "posted"}}), + encoding="utf-8", + ) + path = Path(__file__).resolve().parents[1] / "examples" / "oracle" / "file_oracle.py" + spec = importlib.util.spec_from_file_location("file_oracle_example", path) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + obs = module.FileStatusOracle(store).read({"record_id": "demo-1"}) + assert issue_production_verified(obs) is OracleTier.SYSTEM_OF_RECORD