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
75 changes: 74 additions & 1 deletion .github/workflows/packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ jobs:
flow: ${{ steps.classify.outputs.flow }}
types: ${{ steps.classify.outputs.types }}
agent: ${{ steps.classify.outputs.agent }}
verify: ${{ steps.classify.outputs.verify }}
capture: ${{ steps.classify.outputs.capture }}
privacy: ${{ steps.classify.outputs.privacy }}
desktop: ${{ steps.classify.outputs.desktop }}
Expand All @@ -137,7 +138,7 @@ jobs:
run: |
set -euo pipefail
all() {
for pkg in flow types agent capture privacy desktop tray program_graph weight; do echo "$pkg=true" >> "$GITHUB_OUTPUT"; done
for pkg in flow types agent verify capture privacy desktop tray program_graph weight; do echo "$pkg=true" >> "$GITHUB_OUTPUT"; done
exit 0
}
case "$EVENT_NAME" in
Expand Down Expand Up @@ -170,6 +171,7 @@ jobs:
types="$(hit "^packages/types/|$wf")"
# The agent shim is tested against the in-tree core.
agent="$(hit "^packages/(agent|flow|types)/|$wf")"
verify="$(hit "^packages/(verify|flow|types)/|$wf")"
capture="$(hit "^packages/capture/|$wf")"
privacy="$(hit "^packages/privacy/|$wf")"
# Desktop bundles the program map from packages/program-graph.
Expand All @@ -186,6 +188,7 @@ jobs:
echo "flow=$flow"
echo "types=$types"
echo "agent=$agent"
echo "verify=$verify"
echo "capture=$capture"
echo "privacy=$privacy"
echo "desktop=$desktop"
Expand Down Expand Up @@ -325,6 +328,74 @@ jobs:
- name: Test
run: pytest -q

verify:
name: verify facade (py${{ matrix.python-version }})
needs: scope
if: github.event.pull_request.draft != true && (needs.scope.outputs.verify == 'true')
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJSON((github.event_name == 'pull_request' || github.event_name == 'merge_group') && '["3.12"]' || '["3.10", "3.12"]') }}
defaults:
run:
working-directory: packages/verify
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
with:
python-version: ${{ matrix.python-version }}
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
with:
enable-cache: true
cache-dependency-glob: |
packages/verify/uv.lock
packages/flow/uv.lock
cache-suffix: verify-py${{ matrix.python-version }}
- name: Install the facade against the in-tree core and SDK callbacks
# The broader core/dev lock wins shared dependency versions.
run: >-
python ../../scripts/ci/locked_install.py
--lock 'packages/verify[dev,openai,claude,mcp]'
--lock 'packages/flow[dev,agent,openai-agents,claude-agent-sdk]'
--editable packages/types --editable packages/flow --editable packages/verify
- name: Lint, format and types
run: |
python -m ruff check src tests scripts
python -m ruff format --check src tests scripts
python -m mypy src/openadapt_verify
- name: Independent-store and scripted SDK/MCP controls
run: |
python -m pytest -q --junitxml=tests.xml
python - <<'PY'
import xml.etree.ElementTree as ET
cases = ET.parse("tests.xml").getroot().findall(".//testcase")
assert len(cases) == 38, "all independent-store controls must run"
assert not any(c.find("skipped") is not None for c in cases), "SDK/MCP controls must not skip"
PY
- name: MCP transport against the facade's own lock
run: |
uv venv .mcp-lock
.mcp-lock/bin/python ../../scripts/ci/locked_install.py \
--lock 'packages/verify[dev,mcp]' \
--editable packages/types --editable packages/flow --editable packages/verify
.mcp-lock/bin/python -c 'import importlib.metadata as m; print("mcp==" + m.version("mcp"))'
.mcp-lock/bin/python -m pytest -q tests/test_cli_mcp.py::test_mcp_actual_transport_with_independent_store --junitxml=mcp-lock.xml
.mcp-lock/bin/python - <<'PY'
import xml.etree.ElementTree as ET
cases = ET.parse("mcp-lock.xml").getroot().findall(".//testcase")
assert len(cases) == 1 and not any(c.find("skipped") is not None for c in cases)
PY
- name: Build and verify the public distribution
if: matrix.python-version == '3.12'
run: |
uv build --out-dir dist
python scripts/check_distribution.py --dist dist
python ../../scripts/check_source_boundary.py --repo OpenAdapt --root ../.. --dist dist

flow-lint:
name: flow core lint (ruff, mypy)
needs: scope
Expand Down Expand Up @@ -1411,6 +1482,7 @@ jobs:
- scope
- types
- agent
- verify
- flow-lint
- flow-unit
- flow-critical
Expand Down Expand Up @@ -1464,6 +1536,7 @@ jobs:
selects = {
"types": "types",
"agent": "agent",
"verify": "verify",
"flow-lint": "flow",
"flow-unit": "flow",
"flow-critical": "flow",
Expand Down
16 changes: 16 additions & 0 deletions docs/monorepo/INSTALL_WEIGHT.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,19 @@ time: Playwright, OpenCV, onnxruntime and RapidOCR are the largest wheels.
containers don't.
- Flow's `uv.lock` predates the fold and still pins openadapt-types 0.21. CI
doesn't enforce it.

### Standalone verifier facade

`openadapt-verify` depends on `openadapt-flow[receipts]`; its small wheel does
not imply a small dependency tree. A fresh Python 3.12 x86_64 WSL installation
of the in-tree Types 0.24.0, Flow 2.6.0 and Verify 0.1.0 wheels on 2026-11-05
installed 33 distributions and 413.2 MB of site-packages before import. The facade
wheel was 10,370 bytes. OpenAI/Claude SDKs, MCP, browser-use and Stagehand were
absent from that core installation. This measures installation size, not live
provider behavior or grounding latency.

The new `verify` budget allows 500 MB and 42 distributions, following the existing
size/count margin rules. It permits Flow's core closure plus the existing
portable receipt dependencies (`cbor2`, `cwt`, `pyhpke`) and the facade itself.
The optional SDK/MCP stacks stay outside the core budget. Existing package
budgets remain unchanged.
7 changes: 7 additions & 0 deletions packages/verify/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.ruff_cache/
dist/
21 changes: 21 additions & 0 deletions packages/verify/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 MLDSAI Inc., Richard Abrich, and contributors.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
149 changes: 149 additions & 0 deletions packages/verify/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# openadapt-verify

Check an agent's write through an independent system-of-record read path.
This package exposes OpenAdapt's existing verifier, signed receipts and agent
tool wrappers. The operator supplies the read path and verification contract;
the writer's response is never evidence that its write persisted.

```sh
pip install openadapt-verify
```

This is a small facade over `openadapt-flow[receipts]`. Its installation includes
Flow's dependencies, including its vision stack.

## Wrap a write

Use an existing [`openadapt.verify/v1` contract](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/VERIFIER_API.md)
that reads your store through SQL, HTTP or another supported independent path.
Pin record and tenant identifiers that the agent must not choose.

```python
from openadapt_verify import WriteGuard

guard = WriteGuard("verify.yaml", pinned={"account": "account-17"})
reply = guard.run(values, lambda: existing_agent_write(values))
print(reply["outcome"], reply["seal"], reply["seal_path"])
```

The canonical guard reads before the write, refuses an unreadable baseline,
and reads again after the write, including an error or cancellation. It keeps
the existing uncertain-write ledger and retry protections. `arun` accepts an
async write. Only calls through the guard are checked; other tools and direct
calls remain outside it.

`CONFIRMED`, `REFUTED` and `UNCERTAIN` describe the store result. A confirmed
write can still be provisional when its contract requires a later recheck.
The contract's scope states what was read and what was not checked. It does
not establish that the agent chose an authorized or appropriate write.

## Two signed formats

`WriteGuard` and the SDK/MCP adapters keep their existing result format:
a confirmed effect receipt can have a **local Seal** at `seal_path`.
`seal: "sealed"` reports issuance; a store result can remain confirmed when
signing is off or fails. `verify_local_seal` checks that format against an
independently pinned key registry and the receipt file's SHA256.

For a **portable COSE/SCITT write receipt**, use the canonical verifier API:

```python
from openadapt_verify import Actor, Contract, begin_write, verify_write

pending = begin_write(Contract.load("verify.yaml"), values,
actor=Actor("agent", "existing-agent"), workdir="./state")
try:
existing_agent_write(values)
finally:
result = verify_write(pending, write_receipt=True)
print(result.outcome.value, result.write_receipt, result.write_receipt_path)
```

Check `write_receipt == "issued"` before using its path. `verify_receipt`
checks portable receipts with pinned issuer and log public keys and returns
`PortableReceiptVerification`; `verify_local_seal` returns
`LocalSealVerification`. A valid signature reports integrity and the pinned
issuer, separately from the payload's verdict. Never trust a key merely because
the receipt carries it. No receipt format or cryptography is defined here.

## Existing agents

The existing `WriteGuard` compatibility adapter emits Flow's
`DeprecationWarning` when first requested. Plain `import openadapt_verify`
does not load that adapter.

OpenAI and Claude use the existing tool adapters:

```sh
pip install 'openadapt-verify[openai]' # or [claude]
```

Import `verified` from `openadapt_verify.adapters.openai_agents` or
`openadapt_verify.adapters.claude_sdk`, and pass an existing tool and
`contract="verify.yaml"`. The Claude module also exports the existing `only`
helper for restricting the agent's built-in tools.

For browser-use, wrap a typed async action **before** registering it with
`Tools.action`:

```python
from openadapt_verify.adapters.browser_use import verified

checked_save = verified(save_action, contract="verify.yaml")
tools.action("Save the item")(checked_save)
```

Parameter names and annotations, including `browser_session`, are preserved.
The action returns the verifier reply as tool text. Other browser actions are
unchanged. See the [official custom-action API](https://docs.browser-use.com/open-source/customize/tools/add).

For a caller-owned Stagehand Python session:

```python
from openadapt_verify.adapters.stagehand import verified

checked = verified(session, contract="verify.yaml")
reply = await checked.act(values=values, input=observed_action)
```

This checks that single `act` call. It does not replace the original session
or wrap its other methods. See the [official Python session API](https://docs.stagehand.dev/v3/sdk/python).
Tests use scripted SDK callbacks and local stores; they do not establish live
model/provider behavior.

## Commands and MCP

```sh
openadapt-verify run --contract verify.yaml --params values.json --state ./state -- python write.py
openadapt-verify verify write.receipt.cbor --issuer-key issuer.pub.pem --log-key log.pub.pem
openadapt-verify recheck --contract verify.yaml --params values.json --recheck-state ./state/WRITE_ID/recheck.json
pip install 'openadapt-verify[mcp]'
openadapt-verify serve --contracts ./contracts --state ./state
```

`run` executes an explicit argv without a shell. It reads back and requests a
portable receipt even when the command fails. Exit zero requires a final
confirmed result, issued portable receipt and command exit zero. A provisional
result exits 5 and reports `recheck_state_path`. Command diagnostics go to stderr.

`verify` checks the portable signature and log inclusion against the pinned
keys. Its JSON `valid` field reports those integrity checks; the exit code
also requires a final `CONFIRMED` payload. Both commands use 0 for final
`CONFIRMED`, 3 for `UNCERTAIN`, 4 for `REFUTED` and 5 for provisional confirmation.
Code 1 reports a signature, signing or command failure; 2 reports a usage,
configuration or read error. Inspect the JSON outcome and signing fields
separately before deciding what happened.

At or after `recheck_due_at`, `recheck` reads the store using the canonical
verifier and the private state saved by `run`. Supply the original contract,
params and `--agent-id`, and retain the same verifier authority and ledger
directories. Before the due time it exits 5 without ending the window.
A confirmed recheck exits 0, an uncertain one 3 and a refuted one 4. It reports
the current result and read window as unsigned JSON; the earlier signed
receipt remains provisional. A write after the recheck read is outside its
window. A refusal requires a person to check the record before any retry.

`serve` exposes the existing `verify_contracts`, `verify_begin`, `verify_check`
and `verify_recheck` MCP tools. Their contracts remain operator-owned, and a
refusal never authorizes retrying the write. The `openadapt_verify.shadow`
module also exposes the canonical shadow verifier without changing its scope.
62 changes: 62 additions & 0 deletions packages/verify/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "openadapt-verify"
version = "0.1.0"
description = "Verify an agent's writes through an independent system-of-record read path"
readme = "README.md"
requires-python = ">=3.10,<3.13"
license = "MIT"
dependencies = [
"openadapt-flow[receipts]>=2.6.0,<3",
"cryptography>=50.0.0",
]

[project.optional-dependencies]
openai = ["openadapt-flow[openai-agents]>=2.6.0,<3"]
claude = ["openadapt-flow[claude-agent-sdk]>=2.6.0,<3"]
mcp = ["openadapt-flow[agent]>=2.6.0,<3"]
dev = ["pytest>=8", "ruff>=0.4", "mypy>=1.10", "build>=1.2"]

[project.scripts]
openadapt-verify = "openadapt_verify.cli:main"

[project.urls]
Repository = "https://github.com/OpenAdaptAI/OpenAdapt/tree/main/packages/verify"

[tool.hatch.build]
dev-mode-dirs = ["src"]

[tool.hatch.build.targets.wheel]
ignore-vcs = true
exclude = ["**/__pycache__/**", "**/*.pyc"]
packages = ["src/openadapt_verify"]
core-metadata-version = "2.4"

[tool.hatch.build.targets.sdist]
ignore-vcs = true
include = ["/src/openadapt_verify/**/*.py", "/tests/*.py", "/scripts/*.py", "/README.md", "/LICENSE", "/pyproject.toml", "/uv.lock"]
core-metadata-version = "2.4"

[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.ruff]
target-version = "py310"
line-length = 88

[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I"]

[tool.mypy]
files = ["src/openadapt_verify"]
ignore_missing_imports = true

[tool.uv]
required-environments = [
"python_version == '3.10' and sys_platform == 'linux'",
"python_version == '3.10' and sys_platform == 'darwin'",
"python_version == '3.10' and sys_platform == 'win32'",
]
Loading
Loading