Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ jobs:

- name: Install uv for immutable lock check
if: needs.scope.outputs.code_changed == 'true'
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
version: "0.12.5"
enable-cache: false
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -466,15 +466,14 @@ runtime uses. It returns a signed `RewardEvidenceReceiptV1`: the terminal
effect landed, or it didn't, or the store couldn't be read and the episode is
unscored. Unscored is never 0.

The worker is not in a published release yet, and the release carrying it has
no date. These two commands are what will work once it lands:
The reward worker ships in version 1.35.1. You can install that release:

```bash
pip install 'openadapt-flow[reward]'
pip install 'openadapt-flow[reward]==1.35.1'
openadapt-flow serve-reward --seed-mockmed --port 8788
```

To run it today, install from the repository head:
For development, install from the repository head:

```bash
git clone https://github.com/OpenAdaptAI/openadapt-flow
Expand Down
8 changes: 3 additions & 5 deletions docs/REWARD_WORKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,14 @@ check.

## The MockMed run

The reward worker is not in a published release yet, and the release
carrying it has no date. These two commands are what will work once it
lands:
The reward worker ships in version 1.35.1. You can install that release:

```bash
pip install 'openadapt-flow[reward]'
pip install 'openadapt-flow[reward]==1.35.1'
openadapt-flow serve-reward --seed-mockmed --port 8788
```

Until then, run it from a checkout of the repository head:
For development, install from the repository head:

```bash
git clone https://github.com/OpenAdaptAI/openadapt-flow
Expand Down
18 changes: 15 additions & 3 deletions openadapt_flow/reward/calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import math
import random
from dataclasses import dataclass
from decimal import Decimal
from typing import Any, Callable, Iterable, Mapping, Optional, Sequence

from openadapt_types.process_capability import _digest_payload
Expand Down Expand Up @@ -55,6 +56,19 @@ def binomial_cdf(k: int, n: int, p: float) -> float:
return min(1.0, total)


def confidence_delta(confidence: float) -> float:
"""Complement the declared decimal confidence without subtraction drift.

JSON records 0.95 as 0.95. Binary subtraction would instead produce
0.050000000000000044, which exceeds a strict certificate policy of 0.05.
The bound and its certificate must use the same declared probability.
"""

if not 0.0 < confidence < 1.0:
raise ValueError("confidence must lie in (0, 1)")
return float(Decimal("1") - Decimal(str(confidence)))


def clopper_pearson_upper(
failures: int, trials: int, *, confidence: float = 0.95
) -> float:
Expand All @@ -67,9 +81,7 @@ def clopper_pearson_upper(
raise ValueError("trials must be positive")
if not 0 <= failures <= trials:
raise ValueError("failures must lie in [0, trials]")
if not 0.0 < confidence < 1.0:
raise ValueError("confidence must lie in (0, 1)")
alpha = 1.0 - confidence
alpha = confidence_delta(confidence)
if failures == trials:
return 1.0
if failures == 0:
Expand Down
3 changes: 2 additions & 1 deletion openadapt_flow/reward/seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@

from openadapt_flow.reward.calibration import (
CalibrationResult,
confidence_delta,
corpus_digest_for,
corpus_from_effects,
extradup_trials,
Expand Down Expand Up @@ -305,7 +306,7 @@ def write_bundle(
issued_at_policy_update=0,
expiry_policy_updates=CERTIFICATE_EXPIRY_UPDATES,
epsilon=calibration.epsilon,
delta=1.0 - calibration.confidence,
delta=confidence_delta(calibration.confidence),
)
_write(directory / CERTIFICATE_FILE, certificate.model_dump(mode="json"))
_write(directory / CALIBRATION_FILE, calibration.as_metadata())
Expand Down
2 changes: 1 addition & 1 deletion openadapt_flow/reward/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,7 @@ def _issue(
tier,
self.certificate,
episode.policy_update,
scoring=self.contract.scoring,
contract=self.contract,
)
state = certificate_state(self.certificate, episode.policy_update)
receipt_id = _new_id("reward_receipt")
Expand Down
2 changes: 1 addition & 1 deletion public-artifacts.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
},
{
"path": ".github/workflows/ci.yml",
"sha256": "46976c2a8daa9a936a4db9f64b72719f4618ac3e29ae02d3cc8035dd61c4b356"
"sha256": "50810ca5284b7b108049b18c7b6752d2e7c544dc35f65b3e246ad7fe2f24199a"
},
{
"path": ".github/workflows/citrix-workspace-standin.yml",
Expand Down
19 changes: 10 additions & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ dependencies = [
"cryptography>=42.0",
# Portable ProcessContract v1 capability, artifact, authentication, and
# process-receipt contracts. The healthy runtime remains model-neutral.
"openadapt-types>=0.17.0,<0.18.0",
"openadapt-types>=0.18.0,<0.19.0",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -77,10 +77,10 @@ dev = [
# scope and issuer), and reward evidence receipt the reward worker signs.
# A peer must still negotiate each schema; the dependency alone never
# upgrades an existing decision surface.
"openadapt-types>=0.17.0,<0.18.0",
"openadapt-types>=0.18.0,<0.19.0",
# Engineering-hygiene gates (lint+format, type-check, coverage). Pinned to
# majors so CI and local dev run the same checkers.
"ruff==0.16.5",
"ruff==0.16.6",
"mypy>=1.11",
"pytest-cov>=5.0",
"jsonschema>=4.21",
Expand All @@ -100,15 +100,15 @@ grounding = ["openadapt-grounding>=0.1.0"]
console = [
"fastapi>=0.110",
"uvicorn>=0.29",
"openadapt-types>=0.17.0,<0.18.0",
"openadapt-types>=0.18.0,<0.19.0",
]
# Reference Execute server: `openadapt-flow serve-execute`. Same public
# request schema as Cloud Execute, hosted in this process with a local
# self-signed receipt. Not the hosted control plane.
execute = [
"fastapi>=0.110",
"uvicorn>=0.29",
"openadapt-types>=0.17.0,<0.18.0",
"openadapt-types>=0.18.0,<0.19.0",
]
# WindowsBackend: HTTP client for the WAA (Windows Agent Arena) server.
windows = ["requests>=2.31", "pywin32>=312; platform_system == 'Windows'"]
Expand All @@ -118,7 +118,7 @@ windows = ["requests>=2.31", "pywin32>=312; platform_system == 'Windows'"]
reward = [
"fastapi>=0.110",
"uvicorn>=0.29",
"openadapt-types>=0.17.0,<0.18.0",
"openadapt-types>=0.18.0,<0.19.0",
]
# Native macOS window capture/input. Imported lazily; other platforms never
# install or import these framework bindings.
Expand Down Expand Up @@ -162,8 +162,9 @@ service = ["fastapi>=0.110", "uvicorn[standard]>=0.29"]
service-mlx = [
"fastapi>=0.110",
"uvicorn[standard]>=0.29",
"mlx-vlm>=0.6.4,<0.7",
"transformers>=5.5,<5.15",
"mlx-vlm>=0.6.17,<0.7",
"transformers>=5.14,<5.17",
"jinja2>=3.1,<4",
]
# Cross-platform desktop recording on-ramp: convert an openadapt-capture
# session into a flow recording (openadapt_flow.adapters.capture). Optional and
Expand All @@ -181,7 +182,7 @@ capture = ["openadapt-capture>=1.2.0"]
# field-exact against the released schemas. The `interop-types` CI job
# type-checks and tests the boundaries against the real package; each schema is
# consumed only by an explicitly negotiated peer.
interop = ["openadapt-types>=0.17.0,<0.18.0"]
interop = ["openadapt-types>=0.18.0,<0.19.0"]

[project.scripts]
openadapt-flow = "openadapt_flow.__main__:main"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_ci_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_lint_job_rejects_a_stale_dependency_lock() -> None:
lint_job = workflow[lint_start:compatibility_start]

assert (
"uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9" in lint_job
"uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d" in lint_job
)
assert 'version: "0.12.5"' in lint_job
assert "run: uv lock --locked" in lint_job
Expand Down
7 changes: 4 additions & 3 deletions tests/test_dependency_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ def _locked_version(name: str) -> tuple[int, ...]:
def test_mlx_research_extra_keeps_transformers_in_patched_range() -> None:
"""Do not reintroduce the three model-loading/Trainer RCE advisories."""

assert (5, 5) <= _locked_version("transformers") < (5, 13)
assert (0, 6, 4) <= _locked_version("mlx-vlm") < (0, 7)
assert (5, 14) <= _locked_version("transformers") < (5, 17)
assert (0, 6, 17) <= _locked_version("mlx-vlm") < (0, 7)
assert (3, 1) <= _locked_version("jinja2") < (4,)


def test_transformers_is_confined_to_the_mlx_research_extra() -> None:
Expand All @@ -48,7 +49,7 @@ def test_transformers_is_confined_to_the_mlx_research_extra() -> None:
for dependency in root["optional-dependencies"]["service-mlx"]
}
assert "transformers" not in core_names
assert {"mlx-vlm", "transformers"} <= mlx_names
assert {"mlx-vlm", "transformers", "jinja2"} <= mlx_names


def test_core_runtime_excludes_imagehash_scipy_closure() -> None:
Expand Down
72 changes: 71 additions & 1 deletion tests/test_reward_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from __future__ import annotations

import base64
import json
import math
from pathlib import Path
from typing import Any

Expand All @@ -24,6 +26,7 @@
from openadapt_flow.reward.calibration import ( # noqa: E402
CorpusRecipe,
clopper_pearson_upper,
confidence_delta,
corpus_from_effects,
extradup_trials,
)
Expand Down Expand Up @@ -153,7 +156,7 @@ def test_verified_tier2_is_certified(seeded: dict[str, Any]) -> None:
assert receipt.reward_components == {"terminal_effect": 1.0}
assert receipt.certificate_state.value == "current"
assert receipt.calibration_scope is RewardCalibrationScopeV1.SYNTHETIC
assert receipt.production_certified is False
assert receipt.certification_refusals(worker.contract, worker.certificate) == ()
assert receipt.reward_contract_digest == worker.contract.digest
assert envelope["unscored"] is False
assert envelope["execute_seal"] is False
Expand Down Expand Up @@ -182,6 +185,52 @@ def test_verified_tier2_expired_certificate_is_not_certified(
assert receipt.scalar_reward == 1.0


@pytest.mark.parametrize(
("field", "value", "reason"),
[
("epsilon", math.nextafter(0.05, 1.0), "epsilon"),
("delta", math.nextafter(0.05, 1.0), "delta"),
("threshold", 0.6, "threshold"),
("expiry_policy_updates", 1001, "expiry"),
],
)
def test_current_certificate_must_satisfy_full_policy(
seeded: dict[str, Any], field: str, value: float, reason: str
) -> None:
from openadapt_types.process_capability import canonical_json_bytes

from openadapt_flow.execute.keys import load_or_create_private_key

path = seeded["tier2"] / CERTIFICATE_FILE
payload = json.loads(path.read_text())
if field == "expiry_policy_updates":
value = payload[field] + 1
payload[field] = value
unsigned = {
k: v
for k, v in payload.items()
if k not in {"signature", "signature_algorithm"}
}
key = load_or_create_private_key(seeded["data_dir"])
payload["signature"] = base64.b64encode(
key.sign(canonical_json_bytes(unsigned))
).decode("ascii")
path.write_text(json.dumps(payload))
worker = _worker(seeded)
receipt = _receipt(_run(worker, seeded, MOCKMED_HONEST_PATIENT, "strict_policy"))
assert receipt.reward_outcome is RewardOutcomeV1.VERIFIED
assert receipt.scalar_reward == 1.0
assert receipt.certificate_state.value == "current"
assert receipt.certified is False
assert any(
reason in refusal
for refusal in receipt.certification_refusals(
worker.contract, worker.certificate
)
)
assert worker.verify_receipt(receipt)


def test_tier0_is_development_only_never_certified(seeded: dict[str, Any]) -> None:
worker = _worker(seeded, "tier0")
assert worker.certificate is None
Expand Down Expand Up @@ -530,6 +579,9 @@ def test_certificate_bound_is_recomputable(seeded: dict[str, Any]) -> None:
calibration["calibration_trials"],
confidence=calibration["calibration_confidence"],
)
bundle = RewardBundle.load(bundle_dir)
assert certificate.delta == bundle.contract.certificate_policy.delta == 0.05
assert certificate.unmet(bundle.contract.certificate_policy) == ()
assert certificate.epsilon == recomputed
assert certificate.epsilon == pytest.approx(
1.0 - 0.05 ** (1.0 / CALIBRATION_TRIALS)
Expand Down Expand Up @@ -558,6 +610,24 @@ def _mockmed_corpus() -> CorpusRecipe:
)


@pytest.mark.parametrize(
("confidence", "delta"), [(0.95, 0.05), (0.99, 0.01), (0.9, 0.1)]
)
def test_confidence_delta_preserves_declared_decimal(
confidence: float, delta: float
) -> None:
assert confidence_delta(confidence) == delta
assert clopper_pearson_upper(0, 20, confidence=confidence) == 1 - delta ** (1 / 20)


@pytest.mark.parametrize("confidence", [0.0, 1.0, -0.1, math.nan, math.inf])
def test_confidence_delta_refuses_invalid_probability(confidence: float) -> None:
with pytest.raises(ValueError, match="confidence must lie"):
confidence_delta(confidence)
with pytest.raises(ValueError, match="confidence must lie"):
clopper_pearson_upper(0, 20, confidence=confidence)


def test_clopper_pearson_upper_matches_known_values() -> None:
# 0 of 15 is the bound the openadapt-evals proof run reports.
assert clopper_pearson_upper(0, 15) == pytest.approx(0.181036, abs=1e-6)
Expand Down
Loading