Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Signed files: their exact bytes are what the Seals cover.
tests/fixtures/seal_view/** -text
packages/seal-view/test/fixtures/** -text
61 changes: 61 additions & 0 deletions .github/workflows/packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@
# typecheck, the ESM build (which refuses any runtime import beyond React) and
# a vendored copy's checksum verification.
#
# @openadapt/seal-view (packages/seal-view), one run's Seal drawn and checked
# from its files, runs its unit tests, rebuilds the offline viewer and fails
# when the launcher's copy (openadapt/seal_view/viewer.html) differs or is over
# its size budget, then opens the quickstart fixture's Seal pages from file://
# in Chromium (tests/test_seal_view.py): valid, tampered, and no requests.
#
# OpenAdapt Desktop (packages/desktop) runs two reusable workflows:
# desktop-test.yml (the Python engine on Linux for a PR and in the queue, plus
# macOS and Windows on main, and the bundled-Flow qualification contract) and
Expand Down Expand Up @@ -130,6 +136,7 @@ jobs:
desktop: ${{ steps.classify.outputs.desktop }}
tray: ${{ steps.classify.outputs.tray }}
program_graph: ${{ steps.classify.outputs.program_graph }}
seal_view: ${{ steps.classify.outputs.seal_view }}
weight: ${{ steps.classify.outputs.weight }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -185,6 +192,8 @@ jobs:
tray="$(hit "^packages/tray/|$wf")"
# The program map checks its schema against Flow's copy.
program_graph="$(hit "^packages/program-graph/|^packages/flow/schemas/program-graph-v2\.json$|^\.github/workflows/release-program-graph\.yml$|$wf")"
# The Seal viewer inlines the program map's source.
seal_view="$(hit "^packages/(seal-view|program-graph)/|^openadapt/seal_view/|^tests/test_seal_view\.py$|^tests/fixtures/seal_view/|$wf")"
# Any package's metadata, or the launcher's, can change what an
# install pulls in.
weight="$(hit "^packages/|^pyproject\.toml$|^openadapt/|^hatch_build\.py$|^scripts/install_weight/|$wf")"
Expand All @@ -197,6 +206,7 @@ jobs:
echo "desktop=$desktop"
echo "tray=$tray"
echo "program_graph=$program_graph"
echo "seal_view=$seal_view"
echo "weight=$weight"
} | tee -a "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -1014,6 +1024,56 @@ jobs:
node scripts/vendor.mjs --out "$RUNNER_TEMP/program-graph"
node "$RUNNER_TEMP/program-graph/check.mjs"

seal-view:
name: seal-view (test, viewer build, browser)
needs: scope
if: needs.scope.outputs.seal_view == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: "22"
cache: npm
cache-dependency-path: packages/seal-view/package-lock.json
- name: Install locked dependencies
working-directory: packages/seal-view
run: npm ci --ignore-scripts
- name: Unit tests
working-directory: packages/seal-view
run: npm test
- name: Rebuild the viewer; it must match the launcher's copy and its budget
working-directory: packages/seal-view
run: node scripts/build.mjs --check
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: pyproject.toml
- name: Install the launcher
run: pip install -e ".[dev]"
- name: Resolve Playwright version
id: pw
run: |
python -c "import importlib.metadata as m; print('version=' + m.version('playwright'))" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.pw.outputs.version }}
- name: Install and launch Playwright browser
timeout-minutes: 12
run: >-
python packages/flow/scripts/install_playwright_browser.py
--attempts 2 --attempt-timeout-seconds 270
- name: The Seal pages in a browser, from file://
env:
OPENADAPT_SEAL_VIEW_BROWSER: "1"
run: pytest -v tests/test_seal_view.py

install-weight:
# Each package is published on its own and must stay cheap to install:
# build every in-tree wheel, install each one alone, and fail on a
Expand Down Expand Up @@ -1116,6 +1176,7 @@ jobs:
- tray-contract
- tray-desktop-ipc
- program-graph
- seal-view
- install-weight
- capture-core-record
- desktop-release-intent
Expand Down
1 change: 1 addition & 0 deletions CAPABILITIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ improvising or retrying when something is off.
| Signed receipts (Seals), countersigned and timestamped, offline by default | Released | Unit tests, real TSA tokens | `openadapt-flow seal`, `countersign`, `local-seal` |
| Offline Seals signed with a key the customer holds: a key file, the OS keychain, or a PKCS#11 token or HSM; pinned keys with rotation and revocation; no fallback to a generated key | On main ([#1392](https://github.com/OpenAdaptAI/OpenAdapt/pull/1392)) | Unit tests with synthetic data (keychain and PKCS#11 through stand-ins) | `openadapt-flow local-seal --key`, `local-seal key create`, `pin`, `revoke`; `openadapt seal verify FOLDER` checks every sealed run in a folder; [OFFLINE_SEALS.md](packages/flow/docs/OFFLINE_SEALS.md#customer-held-keys) |
| Anchor an offline Seal in the public Sigstore Rekor transparency log (opt-in; sends one digest and the public key), and verify the anchor offline from the stored inclusion proof | On main ([#1394](https://github.com/OpenAdaptAI/OpenAdapt/pull/1394)) | Unit tests against a simulated Rekor log; real public Rekor entries and Sigstore TSA tokens for the landing demo's referral runs, verified offline in tests; Ed25519ph matches the RFC 8032 test vector | `openadapt-flow local-seal anchor`, `local-seal verify --require-anchor`; [OFFLINE_SEALS.md](packages/flow/docs/OFFLINE_SEALS.md#optional-anchor-a-seal-in-a-public-transparency-log) |
| See a run's Seal offline: one HTML page that checks the Ed25519 signature over the run's exact report in the browser, draws the Seal from it, replays the run over its program map, and lets the reader tamper with a copy; no network requests; the quickstart ends on the person's own | On main ([#1437](https://github.com/OpenAdaptAI/OpenAdapt/pull/1437)) | A real quickstart run's Seal opened from `file://` in Chromium (valid, tampered, no requests); RFC 8032 test vectors; a published OpenEMR Seal's Rekor entry and timestamp | `openadapt seal view RUN_DIR`; `openadapt quickstart`; [`@openadapt/seal-view`](packages/seal-view/README.md) |
| Run authorizations signed with the authorizer's GitHub SSH key (`ssh-keygen -Y sign`, after a summary of what it allows), naming the signing key and bound to the bundle digest, items, profile and Seal keys, and referenced by each Seal; the identity checked against github.com (`--live`) or pinned allowed signers; a pseudonymous operator profile counted from anchored Seals only | On main ([#1401](https://github.com/OpenAdaptAI/OpenAdapt/pull/1401)) | Unit tests with synthetic data; signatures checked against OpenSSH's `ssh-keygen` both ways | `openadapt-flow local-seal authorize`, `local-seal run --authorization`, `local-seal profile`; [OFFLINE_SEALS.md](packages/flow/docs/OFFLINE_SEALS.md#run-authorizations-who-authorized-the-runs) |
| Run reports redacted before they are sealed: session and CSRF tokens, OAuth grants and credential headers become `<redacted>` and are listed in `redactions`; a Seal refuses a report that still holds one; per-app policy | On main ([#1407](https://github.com/OpenAdaptAI/OpenAdapt/pull/1407)) | Unit tests with generated tokens; 7,773 local reports scanned, only OpenEMR `token_main` matched | `openadapt-flow redact-report`, `local-seal run --allow-unredacted`; [REPORT_REDACTION.md](packages/flow/docs/REPORT_REDACTION.md) |
| Strict no-outbound mode and an unattended on-prem service | Released | Synthetic | `openadapt-flow no-outbound`, `service`; [ON_PREM.md](packages/flow/docs/ON_PREM.md) |
Expand Down
1 change: 1 addition & 0 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@ improvising or retrying when something is off.
| Signed receipts (Seals), countersigned and timestamped, offline by default | Released | Unit tests, real TSA tokens | `openadapt-flow seal`, `countersign`, `local-seal` |
| Offline Seals signed with a key the customer holds: a key file, the OS keychain, or a PKCS#11 token or HSM; pinned keys with rotation and revocation; no fallback to a generated key | On main ([#1392](https://github.com/OpenAdaptAI/OpenAdapt/pull/1392)) | Unit tests with synthetic data (keychain and PKCS#11 through stand-ins) | `openadapt-flow local-seal --key`, `local-seal key create`, `pin`, `revoke`; `openadapt seal verify FOLDER` checks every sealed run in a folder; [OFFLINE_SEALS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/OFFLINE_SEALS.md#customer-held-keys) |
| Anchor an offline Seal in the public Sigstore Rekor transparency log (opt-in; sends one digest and the public key), and verify the anchor offline from the stored inclusion proof | On main ([#1394](https://github.com/OpenAdaptAI/OpenAdapt/pull/1394)) | Unit tests against a simulated Rekor log; real public Rekor entries and Sigstore TSA tokens for the landing demo's referral runs, verified offline in tests; Ed25519ph matches the RFC 8032 test vector | `openadapt-flow local-seal anchor`, `local-seal verify --require-anchor`; [OFFLINE_SEALS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/OFFLINE_SEALS.md#optional-anchor-a-seal-in-a-public-transparency-log) |
| See a run's Seal offline: one HTML page that checks the Ed25519 signature over the run's exact report in the browser, draws the Seal from it, replays the run over its program map, and lets the reader tamper with a copy; no network requests; the quickstart ends on the person's own | On main ([#1437](https://github.com/OpenAdaptAI/OpenAdapt/pull/1437)) | A real quickstart run's Seal opened from `file://` in Chromium (valid, tampered, no requests); RFC 8032 test vectors; a published OpenEMR Seal's Rekor entry and timestamp | `openadapt seal view RUN_DIR`; `openadapt quickstart`; [`@openadapt/seal-view`](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/seal-view/README.md) |
| Run authorizations signed with the authorizer's GitHub SSH key (`ssh-keygen -Y sign`, after a summary of what it allows), naming the signing key and bound to the bundle digest, items, profile and Seal keys, and referenced by each Seal; the identity checked against github.com (`--live`) or pinned allowed signers; a pseudonymous operator profile counted from anchored Seals only | On main ([#1401](https://github.com/OpenAdaptAI/OpenAdapt/pull/1401)) | Unit tests with synthetic data; signatures checked against OpenSSH's `ssh-keygen` both ways | `openadapt-flow local-seal authorize`, `local-seal run --authorization`, `local-seal profile`; [OFFLINE_SEALS.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/OFFLINE_SEALS.md#run-authorizations-who-authorized-the-runs) |
| Run reports redacted before they are sealed: session and CSRF tokens, OAuth grants and credential headers become `<redacted>` and are listed in `redactions`; a Seal refuses a report that still holds one; per-app policy | On main ([#1407](https://github.com/OpenAdaptAI/OpenAdapt/pull/1407)) | Unit tests with generated tokens; 7,773 local reports scanned, only OpenEMR `token_main` matched | `openadapt-flow redact-report`, `local-seal run --allow-unredacted`; [REPORT_REDACTION.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/REPORT_REDACTION.md) |
| Strict no-outbound mode and an unattended on-prem service | Released | Synthetic | `openadapt-flow no-outbound`, `service`; [ON_PREM.md](https://github.com/OpenAdaptAI/OpenAdapt/blob/main/packages/flow/docs/ON_PREM.md) |
Expand Down
85 changes: 82 additions & 3 deletions openadapt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
openadapt doctor
"""

import json
import os
import platform
import re
Expand Down Expand Up @@ -1263,12 +1264,15 @@ def seal(command_ctx: click.Context) -> None:

A folder is a run folder (or a folder of them, such as the quickstart's)
signed on this computer; each run's report is checked against its
`seal.json`. Also `openadapt seal keys pin` to pin Cloud's Seal keys after
a person confirms them. Every other argument passes to `openadapt-agent
seal` unchanged.
`seal.json`. `openadapt seal view RUN_DIR` writes the run's Seal as one
offline page (`RUN_DIR/seal.html`) and opens it. Also `openadapt seal keys
pin` to pin Cloud's Seal keys after a person confirms them. Every other
argument passes to `openadapt-agent seal` unchanged.
"""
_require_supported_python()
args = list(command_ctx.args)
if args[:1] == ["view"]:
sys.exit(_view_sealed_runs(args[1:]))
if len(args) >= 2 and args[0] == "verify" and Path(args[1]).is_dir():
sys.exit(_verify_sealed_runs(Path(args[1]), args[2:]))
try:
Expand All @@ -1289,6 +1293,81 @@ def seal(command_ctx: click.Context) -> None:
sys.exit(int(agent_main(["seal", *command_ctx.args]) or 0))


def _view_sealed_runs(argv: list[str]) -> int:
"""`openadapt seal view RUN_DIR`: write each sealed run's offline Seal page."""
import argparse

parser = argparse.ArgumentParser(
prog="openadapt seal view",
description=(
"Write a sealed run's Seal as one offline page, RUN_DIR/seal.html, and "
"open it. The page checks the Ed25519 signature over the run's report "
"in the browser, replays the run over its program map, and makes no "
"network requests. A folder of runs (such as the quickstart's) gets "
"one page per sealed run."
),
)
parser.add_argument(
"run_dir", type=Path, help="a sealed run folder, or a folder of them"
)
parser.add_argument(
"--keys",
type=Path,
default=None,
help="the Seal key registry to trust (default: this computer's Seal key)",
)
parser.add_argument(
"--bundle",
type=Path,
default=None,
help="the run's compiled bundle, for the program map (default: found next to the run)",
)
parser.add_argument("--no-open", action="store_true", help="write the page only")
options = parser.parse_args(argv)
folder = options.run_dir
runs = _sealed_run_dirs(folder) if folder.is_dir() else []
if not runs:
click.echo(f"no sealed run in {folder} (no seal.json)", err=True)
return 1
try:
from openadapt.seal_view import SealViewError, open_page, write_seal_page
except ImportError: # pragma: no cover - the viewer ships with the launcher
click.echo("This install has no Seal viewer.", err=True)
return 1
keys = None
if options.keys is not None:
try:
keys = json.loads(options.keys.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
click.echo(f"can't read the key registry {options.keys}: {exc}", err=True)
return 1
pages = []
for run in runs:
try:
page = write_seal_page(
run,
keys=keys,
bundle_dir=options.bundle,
commands=_hint(f"seal verify {_shell_quote(run)}"),
)
except (SealViewError, ImportError) as exc:
click.echo(f" {run.name} no page: {exc}", err=True)
continue
except Exception as exc: # noqa: BLE001 - e.g. no Seal key on this computer
click.echo(f" {run.name} no page: {type(exc).__name__}: {exc}", err=True)
continue
pages.append(page)
click.echo(f"Seal: {page}")
if not pages:
return 1
if not options.no_open:
from openadapt.quickstart.engine import display_available

if display_available():
open_page(pages[0])
return 0 if len(pages) == len(runs) else 1


def _sealed_run_dirs(folder: Path) -> list[Path]:
"""The run folder itself when it is sealed, else its sealed run folders."""
if (folder / "seal.json").is_file():
Expand Down
3 changes: 3 additions & 0 deletions openadapt/quickstart/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def build(outcome: Outcome) -> NextSteps:
)
return NextSteps(
verify=verify,
verify_run=lambda run_dir: hint(
f"seal verify {quote(display_path(run_dir))}"
),
record=hint(
f"flow record --backend web --url {YOUR_APP_URL} --out my-task"
),
Expand Down
Loading
Loading