Skip to content
Draft
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
4 changes: 3 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ jobs:

- name: Verify the public distribution boundary
if: steps.check_skip.outputs.skip != 'true' && steps.release.outputs.released == 'true'
run: python scripts/check_source_boundary.py --require-dist
run: |
python scripts/verify_release_artifacts.py
python scripts/check_source_boundary.py --require-dist

# v1.14.0 bundles twine 6.1.0 and packaging 25.0, which reject the
# Metadata-Version 2.5 that current hatchling emits. That pin failed the
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ jobs:
- name: Build and verify the public distribution boundary
run: |
uv build
python scripts/verify_release_artifacts.py
python scripts/check_source_boundary.py --require-dist
33 changes: 16 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# openadapt-types

> [!IMPORTANT]
> **Status: Experimental. Interoperability schemas, not the product.** This
> package publishes shared schemas for computer-use agents as an optional
> component, with no production support promise.
> **Lifecycle: Support.** `openadapt-types` is the current public schema
> dependency for OpenAdapt components and partner integrations. Support
> identifies its role in the stack. It does not create an additional OpenAdapt
> product target or a separate Production claim.
>
> The OpenAdapt product is the demonstration compiler,
> [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow), installed
Expand All @@ -12,9 +13,9 @@
> on Windows `cmd.exe` use `pip install "openadapt[browser]"`): it compiles a
> demonstrated GUI workflow into a
> deterministic, locally executable program. Healthy runs make no model calls,
> and it halts instead of guessing when verification fails. Lifecycle labels for
> every repository are in the
> [repository lifecycle registry](https://github.com/OpenAdaptAI/.github/blob/main/REPOSITORY_LIFECYCLE.md).
> and it halts instead of guessing when verification fails. The live admission
> result for the seven OpenAdapt product targets is available from
> [`openadapt.ai/status.json`](https://openadapt.ai/status.json).

Canonical Pydantic schemas for computer-use agents.

Expand All @@ -34,24 +35,22 @@ OpenAdapt is a governed demonstration compiler: record a workflow once, compile
the recording into a deterministic program, and replay that program with zero
model calls on the healthy path. When the live screen does not match what was
demonstrated it halts instead of guessing, using identity gates and independent
effect verification. Every substrate is first-class.
effect verification.

Substrate maturity, stated the same way across the OpenAdapt repositories:

| Substrate | Maturity |
| --- | --- |
| Browser (web) | Beta; available in production today through the managed browser product |
| Native desktop (Windows, macOS, Linux) | Available for customer-controlled execution; qualification evidence is task- and environment-specific |
| Remote display (RDP) | Available for customer-controlled execution; qualification evidence is task- and environment-specific |
| Citrix / VDI | Available for customer-controlled execution; real-environment ICA/HDX qualification is deployment-specific |
Schemas describe a stable interface. They do not assign a static maturity state
to Browser, native desktop, RDP, or Citrix/VDI. Product status is derived from
signed, expiring, and revocable release admissions for the exact seven product
targets. A current product release can execute only an exact workflow version
that has its own active admission for the bound application, environment,
contracts, evidence authority, validity, and revocation state.

The packages in the stack:

| Package | Role |
| --- | --- |
| [`openadapt`](https://github.com/OpenAdaptAI/OpenAdapt) | Launcher and installer (`pip install openadapt`) |
| [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow) | Records, compiles, verifies, and replays workflows |
| [`openadapt-capture`](https://github.com/OpenAdaptAI/openadapt-capture) | Cross-platform local desktop recording |
| [`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow) | Normalizes demonstrations, then compiles, verifies, and replays workflows |
| [`openadapt-capture`](https://github.com/OpenAdaptAI/openadapt-capture) | Canonical native screen, mouse, keyboard, timing, window, and media capture |
| **`openadapt-types`** | Canonical action and UI-state schema (this package) |
| [`openadapt-grounding`](https://github.com/OpenAdaptAI/openadapt-grounding) | Local OCR text-anchoring plus optional model grounding |
| [`openadapt-privacy`](https://github.com/OpenAdaptAI/openadapt-privacy) | PHI/PII detection and redaction |
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ authors = [
]
keywords = ["computer-use", "gui-automation", "schemas", "pydantic", "agents"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
Expand All @@ -38,6 +37,7 @@ Issues = "https://github.com/OpenAdaptAI/openadapt-types/issues"
dev = [
"pytest>=7.0",
"pytest-cov",
"tomli>=2.0.0; python_version < '3.11'",
]


Expand All @@ -57,7 +57,7 @@ allow_zero_version = true
# existing editable lock entry without resolving dependencies, stage it in the
# generated release commit, and build exactly those reviewed inputs. The PSR
# action is a Docker action, so uv must be installed inside its container.
build_command = "python -m pip install uv==0.11.29 && python scripts/verify_release_lock.py --write && git add uv.lock && uv build"
build_command = "python -m pip install uv==0.11.29 && python scripts/verify_release_lock.py --write && git add uv.lock && uv build && python scripts/verify_release_artifacts.py"

[tool.semantic_release.branches.main]
match = "main"
Expand Down
103 changes: 103 additions & 0 deletions scripts/verify_release_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Verify the exact wheel and source distribution before publication."""

from __future__ import annotations

import argparse
import email
import re
import tarfile
import zipfile
from email.message import Message
from pathlib import Path

try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - exercised by supported Python 3.10
import tomli as tomllib

ROOT = Path(__file__).resolve().parents[1]


class ArtifactError(RuntimeError):
"""A release artifact does not match the reviewed project metadata."""


def _canonical_name(value: str) -> str:
return re.sub(r"[-_.]+", "-", value).lower()


def _project_identity(root: Path) -> tuple[str, str]:
project = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))[
"project"
]
name = project.get("name")
version = project.get("version")
if not isinstance(name, str) or not name or not isinstance(version, str) or not version:
raise ArtifactError("pyproject.toml must declare a project name and version")
classifiers = project.get("classifiers", [])
if any(str(item).startswith("Development Status ::") for item in classifiers):
raise ArtifactError("project metadata must not publish a static maturity classifier")
return name, version


def _wheel_metadata(path: Path) -> bytes:
with zipfile.ZipFile(path) as archive:
names = [name for name in archive.namelist() if name.endswith(".dist-info/METADATA")]
if len(names) != 1:
raise ArtifactError(f"{path.name} must contain exactly one METADATA file")
return archive.read(names[0])


def _sdist_metadata(path: Path) -> bytes:
with tarfile.open(path, "r:gz") as archive:
members = [
member
for member in archive.getmembers()
if member.isfile() and member.name.count("/") == 1 and member.name.endswith("/PKG-INFO")
]
if len(members) != 1:
raise ArtifactError(f"{path.name} must contain exactly one root PKG-INFO file")
stream = archive.extractfile(members[0])
if stream is None:
raise ArtifactError(f"{path.name} PKG-INFO cannot be read")
return stream.read()


def _verify_metadata(path: Path, payload: bytes, name: str, version: str) -> None:
metadata: Message = email.message_from_bytes(payload)
if _canonical_name(metadata.get("Name", "")) != _canonical_name(name):
raise ArtifactError(f"{path.name} has the wrong package name")
if metadata.get("Version") != version:
raise ArtifactError(f"{path.name} has the wrong package version")
classifiers = metadata.get_all("Classifier", [])
if any(value.startswith("Development Status ::") for value in classifiers):
raise ArtifactError(f"{path.name} publishes a static maturity classifier")


def verify_distributions(root: Path = ROOT) -> tuple[Path, Path]:
"""Verify one wheel and one source archive against ``pyproject.toml``."""
name, version = _project_identity(root)
dist = root / "dist"
wheels = sorted(dist.glob("*.whl"))
sdists = sorted(dist.glob("*.tar.gz"))
if len(wheels) != 1 or len(sdists) != 1:
raise ArtifactError("dist must contain exactly one wheel and one source distribution")
_verify_metadata(wheels[0], _wheel_metadata(wheels[0]), name, version)
_verify_metadata(sdists[0], _sdist_metadata(sdists[0]), name, version)
return wheels[0], sdists[0]


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=ROOT)
args = parser.parse_args()
try:
wheel, sdist = verify_distributions(args.root.resolve())
except (ArtifactError, OSError, KeyError, tarfile.TarError, zipfile.BadZipFile) as exc:
parser.exit(1, f"release artifact verification failed: {exc}\n")
print(f"verified {wheel.name} and {sdist.name}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
63 changes: 63 additions & 0 deletions tests/test_release_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import importlib.util
import io
import tarfile
import zipfile
from pathlib import Path

import pytest

ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "verify_release_artifacts.py"
SPEC = importlib.util.spec_from_file_location("verify_release_artifacts", SCRIPT)
assert SPEC and SPEC.loader
artifacts = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(artifacts)


def _metadata(*, classifier: str | None = None) -> bytes:
lines = [
"Metadata-Version: 2.4",
"Name: example-package",
"Version: 1.2.3",
]
if classifier is not None:
lines.append(f"Classifier: {classifier}")
return ("\n".join(lines) + "\n\n").encode()


def _write_release(root: Path, *, artifact_classifier: str | None = None) -> None:
(root / "pyproject.toml").write_text(
'[project]\nname = "example-package"\nversion = "1.2.3"\nclassifiers = []\n',
encoding="utf-8",
)
dist = root / "dist"
dist.mkdir()
payload = _metadata(classifier=artifact_classifier)
with zipfile.ZipFile(dist / "example_package-1.2.3-py3-none-any.whl", "w") as archive:
archive.writestr("example_package-1.2.3.dist-info/METADATA", payload)
with tarfile.open(dist / "example_package-1.2.3.tar.gz", "w:gz") as archive:
member = tarfile.TarInfo("example_package-1.2.3/PKG-INFO")
member.size = len(payload)
archive.addfile(member, io.BytesIO(payload))


def test_matching_wheel_and_source_distribution_pass(tmp_path: Path) -> None:
_write_release(tmp_path)
wheel, sdist = artifacts.verify_distributions(tmp_path)
assert wheel.suffix == ".whl"
assert sdist.name.endswith(".tar.gz")


def test_static_maturity_classifier_in_archive_fails(tmp_path: Path) -> None:
_write_release(tmp_path, artifact_classifier="Development Status :: 3 - Alpha")
with pytest.raises(artifacts.ArtifactError, match="static maturity classifier"):
artifacts.verify_distributions(tmp_path)


def test_extra_release_archive_fails(tmp_path: Path) -> None:
_write_release(tmp_path)
(tmp_path / "dist" / "unexpected.whl").touch()
with pytest.raises(artifacts.ArtifactError, match="exactly one wheel"):
artifacts.verify_distributions(tmp_path)
14 changes: 12 additions & 2 deletions tests/test_release_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,14 @@ def test_release_configuration_is_fail_closed() -> None:
assert (
"python -m pip install uv==0.11.29 && "
"python scripts/verify_release_lock.py --write && "
"git add uv.lock && uv build"
"git add uv.lock && uv build && "
"python scripts/verify_release_artifacts.py"
) in metadata
assert metadata.index("python -m pip install uv==0.11.29") < metadata.index(
"python scripts/verify_release_lock.py --write"
) < metadata.index("git add uv.lock") < metadata.index("uv build")
) < metadata.index("git add uv.lock") < metadata.index("uv build") < metadata.index(
"python scripts/verify_release_artifacts.py"
)
assert "run: uv build" not in workflow
assert "astral-sh/setup-uv" not in workflow
assert "actions/setup-python" not in workflow
Expand All @@ -80,6 +83,13 @@ def test_release_configuration_is_fail_closed() -> None:
assert 'version: "0.11.29"' in test_workflow
assert 'python-version: "3.12"' in test_workflow
assert "uv sync --locked --extra dev" in test_workflow
assert "python scripts/verify_release_artifacts.py" in test_workflow
assert "python scripts/verify_release_artifacts.py" in workflow


def test_project_has_no_static_maturity_classifier() -> None:
metadata = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
assert "Development Status ::" not in metadata


def test_all_third_party_actions_are_commit_pinned() -> None:
Expand Down
2 changes: 2 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading