diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5ba2929..7766cf8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -74,5 +74,7 @@ jobs:
# committed. Nothing is installed, so it cannot fail on a lock refresh.
- name: Check the vendored tokens against openadapt-web@main
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # github.token cannot read private OpenAdaptAI/openadapt-web.
+ # ADMIN_TOKEN can; fall back so local/fork runs still try github.token.
+ GITHUB_TOKEN: ${{ secrets.ADMIN_TOKEN || github.token }}
run: python scripts/vendor_design_tokens.py --check
diff --git a/docs/get-started/index.md b/docs/get-started/index.md
index 5d8f588..d0089d8 100644
--- a/docs/get-started/index.md
+++ b/docs/get-started/index.md
@@ -1,104 +1,76 @@
---
description: >-
- Install OpenAdapt, run the MockMed tutorial, watch --break-it halt a fake
- success, then record one read-only workflow.
+ Install OpenAdapt on this computer with Desktop or pip. Show the task
+ once; it becomes a program your agent can run. Tutorial is optional.
---
# Get started
-The default reader is the calling agent. A named human authors the program
-and resolves identity, effect, and judgment halts. You need no account, target
-application, API key, or operating-system automation permission. Python 3.10
-through 3.12.
+You want an agent to do a job on this computer. Keep that job here.
-Point Claude Code or Cursor at the local server:
+You show the clicks once. OpenAdapt turns that into a program on this
+computer, and later an agent runs it and checks the work landed. Healthy
+repeats don't send your data to a model.
-```bash
-claude mcp add openadapt -- \
- uvx --from 'openadapt-agent[tutorial]' openadapt-agent \
- serve --allow-run
-```
+OpenAdapt is a compiler. The chat is the IDE, and credentials stay on this
+computer. The core is free forever under MIT.
+
+## Install
-`--allow-run` is an explicit opt-in. The server generates the public synthetic
-tutorial at serve time. Halt, refused, timeout, and error come back as those
-outcomes. Never summarize halt as success.
+Python 3.10 through 3.12 for pip. Desktop and the CLI compile the same
+program.
-!!! info "What the calling agent may do / must not do"
- **May:** bind declared parameters, invoke the compiled program, read
- typed outcomes, supply a missing declared parameter, retry a retryable
- transport failure, escalate to a human.
+### Desktop
- **Must not:** summarize halt as success, resolve identity or effect
- contradictions, or be the sole source of a production demonstration.
+Get the installer for this machine from
+[openadapt.ai/download](https://openadapt.ai/download). You can also start
+from [openadapt.ai/start](https://openadapt.ai/start). Open the app. Grant
+Screen Recording and Accessibility if asked.
- Full contract: [agents.txt](../agents.txt).
+First-run permissions and checksums are in
+[Install Desktop](../desktop/install.md).
-Same loop from the CLI:
+### CLI
```bash
python -m pip install --upgrade openadapt
-openadapt quickstart
-openadapt quickstart --break-it
```
-Add `--headed` if you want to watch the browser.
-
-The bundled workflow is a tutorial. Qualifying a real one means declaring its
-application boundary, its action risks, its identities, its effect verifiers,
-its fault cases, and its deployment policy.
-
-`openadapt quickstart` records a task in MockMed, a synthetic
-practice-management fixture, compiles the observed
-[effect contract](../reference/glossary.md#effect-contract), certifies it with
-the shipped clinical-write [policy](../reference/glossary.md#policy), and runs
-it under the Standard [profile](../reference/glossary.md#profile). A separate
-read-only API confirms the saved record outside the screen that performed the
-write. The healthy run returns [`VERIFIED`](../reference/run-outcomes.md) with
-no model or Cloud call.
-
-`--break-it` is the aha. Same certified bundle. The backend rejects the write
-after the app has already painted its success banner, so every on-screen check
-passes and the run halts anyway, because the independent read disagrees. The
-store is unchanged.
-
-OpenAdapt refuses to overwrite `openadapt-quickstart/`. Artifacts land there.
-
-**Isolated CLI alternative.** The public installer creates and maintains an
-isolated environment with [uv](https://docs.astral.sh/uv/):
+If you don't want to touch the Python you already have, use the isolated
+installer:
```bash
curl -fsSL https://openadapt.ai/install.sh | sh
```
-Both paths install the same `openadapt` command. You need no package extra for
-the browser tutorial.
+Either command leaves `openadapt` on PATH.
+
+## After install
-## The receipt you just got
+If you came from a job page (`/j/{id}`), go back to that tab. It's already
+there. If a local coding agent is on this machine, it can call `openadapt`
+on PATH.
-Tutorial `VERIFIED` is a local receipt on synthetic MockMed. It is not a
-production Seal. `--break-it` is the aha: the banner can lie, and the
-independent read stops the run. When you qualify a real job, that same
-independent check is what a Seal attests. Public synthetic verify lives at
-[openadapt.ai/seals](https://openadapt.ai/seals). The contract is
-[The Seal](../commercial/seal.md).
+Sign in on this computer, in the real app. Not in the chat.
-A production Seal needs a qualified program and an oracle at tier 2 or 3.
-Oracle tiers 0 (visual) and 1 (second-session UI) never mint one. Local
-unsigned replay stays free.
+### Optional tutorial
-## What the healthy run wrote
+`openadapt quickstart` records a bundled tutorial and compiles it, then
+runs it locally. To see a halt when the on-screen banner disagrees with
+the record, run `openadapt quickstart --break-it` against the same
+compiled tutorial; the run stops because the independent read failed, and
+the store stays as it was. Add `--headed` if you want to watch the
+browser.
-You now have:
+Skip it if you already have a real task.
-- `openadapt-quickstart/recording/`: the demonstration and retained target
- evidence
-- `openadapt-quickstart/bundle/`: the inspectable compiled workflow
-- `openadapt-quickstart/run/REPORT.md`: the ordered actions, evidence, outcome,
- and any halt reason
-- `openadapt-quickstart/run/receipt.json`: a local, privacy-safe summary of the
- synthetic verified run
+OpenAdapt refuses to overwrite `openadapt-quickstart/`. Artifacts land
+there:
-Open the report, then inspect the program and its deployment gaps:
+- `openadapt-quickstart/recording/`
+- `openadapt-quickstart/bundle/`
+- `openadapt-quickstart/run/REPORT.md`
+- `openadapt-quickstart/run/receipt.json`
```bash
less openadapt-quickstart/run/REPORT.md
@@ -106,190 +78,27 @@ openadapt flow visualize openadapt-quickstart/bundle --out graph.html
openadapt flow lint openadapt-quickstart/bundle
```
-Open `graph.html` in a browser. That page is the compiled program: the steps
-it can take, the evidence each one needs, and the paths that stop the run.
-See [Read a compiled program](../concepts/program-visualizer.md).
-
-!!! important "A tutorial result is not production certification"
- The bundled fixture proves that the local product path and its Standard
- verification gates work. It certifies only this bundled synthetic task,
- application, and local system of record. A customer workflow must bind its
- own application, execution surface, action risks, identity checks,
- independent effect verifier, fault cases, and deployment policy.
-
-## What qualifying a real job adds
-
-Qualification tests the workflow against real failures in its environment
-before it runs. You declare:
-
-- the application boundary
-- the action risks
-- the identities
-- the effect verifiers
-- the fault cases
-- the deployment policy
-
-Start with one real, read-only task. Don't start with a write.
-
-| Goal | Next guide |
-|---|---|
-| Author one real, read-only browser workflow | [Author a workflow](first-workflow.md) |
-| See what the compiled program looks like | [Read a compiled program](../concepts/program-visualizer.md) |
-| Bind identity, effects, faults, and policy | [Qualify a workflow](../guides/qualify-a-workflow.md) |
-| Use the Desktop application | [Install Desktop](../desktop/install.md) |
-| Use native desktop, RDP, or Citrix | [Install a different execution surface](#install-a-different-execution-surface) |
-| Prepare a qualified production run | [Move from demo to deployment](#move-from-demo-to-deployment) |
-
-
- { width="900" }
- The same loop against openIMIS 25.10 on synthetic data. One
- recorded eligibility check, then the compiled program replaying that check
- twice. A read-only SQL query verifies the first replay and contradicts the
- second, so the second one halts.
-
-
-Want to watch before you record your own app?
-
-- **[Hosted demo](https://app.openadapt.ai/demo)**: recorded demonstrations,
- verified replays, and fail-safe halts on real footage.
-- **[Template gallery](https://openadapt.ai/templates)**: ready-to-adapt
- workflow templates.
-- **[Blog](https://blog.openadapt.ai)**: guides, updates, and automation
- recipes.
-
-## First real (read-only) workflow
-
-[Author a workflow](first-workflow.md) records one small real task that
-doesn't change business data. A read-only lookup against test data works.
-Open a known test record, then stop when a field shows the expected value.
-
-Don't start with a task that saves, submits, creates, or deletes data. A
-write waits until qualification binds its risks, identities, and effect
-verifiers.
-
-## See a fail-safe halt from UI drift
-
-`--break-it` already showed a painted success that failed the independent
-read. Theme drift is a different halt. Use the compiled tutorial bundle in an
-ordinary Demo-profile replay. This path has no independent verifier, so
-OpenAdapt must not reuse the Standard `VERIFIED` result:
-
-```bash
-openadapt flow replay openadapt-quickstart/bundle \
- --drift modal \
- --run-dir openadapt-quickstart-halt
-```
-
-!!! note "Why this command exits 1"
- The command expects a [halt](../reference/glossary.md#halt), so it exits
- `1`. The compiled program has no approved branch for the changed screen
- state and refuses to act. If you see `Replay HALTED`, open
- `openadapt-quickstart-halt/REPORT.md` to see the retained evidence. Do not
- retry a possibly dispatched write; reconcile it against an independent
- system of record first. Every outcome is defined in
- [Run outcomes and halt reasons](../reference/run-outcomes.md).
+## Local coding agents
-## Install a different execution surface
-
-The base package includes the browser driver. Its matching Chromium build
-downloads only when a browser action starts. Native desktop, RDP, and Citrix
-workflows do not start or download Chromium.
-
-For native or remote-only work, install the selected driver:
-
-```bash
-pip install openadapt
-pip install 'openadapt[capture,windows]' # example: native Windows
-pip install 'openadapt[capture,rdp]' # example: network RDP
-```
-
-The selected native or remote extras do not download Chromium. The public
-command remains `openadapt flow ` for every surface. The standalone
-[`openadapt-flow`](https://github.com/OpenAdaptAI/openadapt-flow) package is for
-engine contributors. It is not a second end-user path.
-
-## When a real run halts
-
-The bundled theme drift is a deterministic re-resolution demonstration, not a
-general teaching demo. When a real, durable run halts on an unhandled state,
-record only the corrective actions and feed the halted run to `teach`:
-
-```bash
-openadapt flow teach runs/ \
- --fix recordings/ \
- --bundle bundle \
- --out bundle-v2
-```
-
-`teach` writes `bundle-v2` only if the correction is promoted. The shipped
-deterministic reference inducer covers the optional-dialog correction class; it
-does not generalize arbitrary UI changes. An underdetermined or safety-weakening
-correction is refused and the original bundle remains halting.
-
-## Move from demo to deployment
-
-Follow [Run a deployment](../guides/run-a-deployment.md) to seal the exact
-bundle, certify it, run a dry check, and start the governed run. A failed
-certification exits `2` and names each violated requirement. Close those gaps
-before another attempt. Do not promote a bundle because the sample application
-passed; complete the [security and deployment review](../guides/security-review.md)
-for the real environment.
-
-## Where to go next
-
-To compile several recordings of the same task, read
-[Induce a program](../guides/induce-a-program.md). A task that starts in one
-application and finishes in another is two recordings. Don't record them as
-one. Sequence the compiled bundles with `compose`, or after each child is
-admitted, with a process parent:
+Claude Code and Cursor can talk to OpenAdapt over MCP after `openadapt` is
+on this machine. ChatGPT.com can't. Don't send a hosted chat your
+passwords.
```bash
-openadapt flow compose \
- --child intake=./intake-bundle \
- --child posting=./posting-bundle \
- --handoff intake.patient_id=posting.patient_id \
- --out composed
-```
-
-`visualize composed` draws those two children and the `patient_id` handoff.
-Each child stays its own compiled program:
-
-```mermaid
-flowchart TD
- n0(["intake web"])
- n1(["posting linux"])
- n2{{"End of declared steps"}}
- n0 --> n1
- n1 --> n2
- n0 -->|patient_id| n1
+claude mcp add openadapt -- \
+ uvx --from 'openadapt-agent[tutorial]' openadapt-agent \
+ serve --allow-run
```
-See [Sequence work across two applications](../guides/compose-multi-application.md)
-and [Read a compiled program](../concepts/program-visualizer.md).
-[Durable runs](../concepts/durable-runtime.md) explains how an operator can
-resume from the last verified checkpoint after a halt.
-
-
-
-- [__Author a workflow__](first-workflow.md)
-
- Record a read-only task with test data, review it, supervise its first
- replay, and inspect the report.
-
-- [__What you get__](what-you-get.md)
-
- The bundle, the run report, and what each artifact is for.
-
-- [__Read a compiled program__](../concepts/program-visualizer.md)
-
- The program map, a composed parent, and a process parent.
-
-- [__Qualification evidence__](what-works-today.md)
+`--allow-run` is an explicit opt-in. Halt, refused, timeout, and error come
+back as those outcomes. Never summarize halt as success.
- Accepted substrate results, exact environments, and deployment boundaries.
+The machine contract is in [agents.txt](../agents.txt).
-- [__Core concepts__](../concepts/index.md)
+## Next
- Understand the compiler model before you deploy it for real work.
+Author one real read-only task on your own app in
+[Author a workflow](first-workflow.md). Start with a lookup that doesn't
+change business data.
-
+More Desktop setup is in [Install Desktop](../desktop/install.md).
diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md
index 87566a4..a67198c 100644
--- a/docs/getting-started/index.md
+++ b/docs/getting-started/index.md
@@ -1,6 +1,6 @@
---
title: Getting started moved
-description: Install OpenAdapt and run the current record, compile, replay tutorial.
+description: Install OpenAdapt on this computer, then continue at Get started.
canonical: https://docs.openadapt.ai/get-started/
redirect_to: /get-started/
hide:
@@ -10,14 +10,15 @@ hide:
# Getting started moved
-This URL is from an older docs tree. Start at [Get started](/get-started/).
+This URL is from an older docs tree. Install first, then continue at
+[Get started](/get-started/).
```bash
-pip install openadapt
-openadapt quickstart
-openadapt quickstart --break-it
+python -m pip install --upgrade openadapt
```
-`openadapt quickstart` records a demonstration, compiles it, and replays the program. `--break-it` is the halt demo. For your own app, use `openadapt flow record`, `compile`, and `replay`.
+The bundled tutorial (`openadapt quickstart`, then optionally
+`openadapt quickstart --break-it`) is optional. For your own app, see
+[Author a workflow](/get-started/first-workflow/).
[Continue to Get started](/get-started/){ .md-button .md-button--primary }
diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md
index 6848863..3a8bc14 100644
--- a/docs/getting-started/installation.md
+++ b/docs/getting-started/installation.md
@@ -1,6 +1,6 @@
---
title: Installation moved
-description: Install OpenAdapt with pip, then run the current tutorial.
+description: Install OpenAdapt with Desktop or pip, then continue at Get started.
canonical: https://docs.openadapt.ai/get-started/
redirect_to: /get-started/
hide:
@@ -10,13 +10,13 @@ hide:
# Installation moved
-Install the current package, then run the tutorial:
+Install the current package, then continue at
+[Get started](/get-started/). Tutorial is optional.
```bash
-pip install openadapt
-openadapt quickstart
+python -m pip install --upgrade openadapt
```
-The same `openadapt` command drives `openadapt flow record`, `compile`, and `replay`. Details are on [Get started](/get-started/).
+Desktop works too: [openadapt.ai/download](https://openadapt.ai/download).
[Continue to Get started](/get-started/){ .md-button .md-button--primary }
diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md
index 484cd83..ba6a2e2 100644
--- a/docs/getting-started/quickstart.md
+++ b/docs/getting-started/quickstart.md
@@ -1,6 +1,6 @@
---
title: Quick Start moved
-description: Record a GUI workflow, compile it, and replay the program with openadapt flow.
+description: Install OpenAdapt on this computer, then continue at Get started.
canonical: https://docs.openadapt.ai/get-started/
redirect_to: /get-started/
hide:
@@ -10,21 +10,13 @@ hide:
# Quick Start moved
-The current first run is at [Get started](/get-started/).
+The current first run is install, then [Get started](/get-started/).
```bash
-pip install openadapt
-openadapt quickstart
+python -m pip install --upgrade openadapt
```
-That records a bundled demonstration, compiles a program, and replays it locally. A healthy run makes no model API call.
-
-To record your own application:
-
-```bash
-openadapt flow record --backend web --url https://your.app --out rec
-openadapt flow compile rec --out bundle --name my-task
-openadapt flow replay bundle --url https://your.app --headed
-```
+Desktop works too: [openadapt.ai/start](https://openadapt.ai/start).
+Tutorial is optional.
[Continue to Get started](/get-started/){ .md-button .md-button--primary }
diff --git a/docs/index.md b/docs/index.md
index 950c8ca..443bec4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -15,7 +15,7 @@ no model calls. Uncertainty escalates. Humans audit. Computer-use agents are
the user of OpenAdapt. They are not the executor inside it.
-[Call it from an agent](get-started/index.md){ .md-button .md-button--primary }
+[Install OpenAdapt](get-started/index.md){ .md-button .md-button--primary }
[See how the compiler works](concepts/demonstration-compiler.md){ .md-button }
[Author a workflow](get-started/first-workflow.md){ .md-button }
@@ -158,10 +158,9 @@ report determine its outcome.
-- [__Call it from an agent__](get-started/index.md)
+- [__Install OpenAdapt__](get-started/index.md)
- `claude mcp add openadapt`, then `openadapt quickstart --break-it`.
- Never summarize halt as success.
+ Desktop or pip on this computer. Tutorial is optional.
- [__Author a workflow__](get-started/first-workflow.md)
diff --git a/docs/llms.txt b/docs/llms.txt
index a1329bb..0bbeaab 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -4,7 +4,7 @@
## Get started
- [agents.txt](https://docs.openadapt.ai/agents.txt): MCP, skills, outcome vocabulary (`VERIFIED` / `HALTED` / `RECONCILIATION_REQUIRED`), who may resolve a halt
-- [Get started](https://docs.openadapt.ai/get-started/): `claude mcp add openadapt`, then `openadapt quickstart`, then `openadapt quickstart --break-it`. The bundled workflow is a tutorial. The current CLI is `openadapt flow record`, `compile`, `replay`. Older `/getting-started/` URLs and the `/start/` alias redirect here.
+- [Get started](https://docs.openadapt.ai/get-started/): Install with Desktop or `python -m pip install --upgrade openadapt`. Tutorial (`openadapt quickstart`, optionally `openadapt quickstart --break-it`) is optional. Local coding agents can `claude mcp add openadapt` later. Never summarize halt as success. Older `/getting-started/` URLs and the `/start/` alias redirect here. The current CLI after install is `openadapt flow record`, `compile`, `replay`.
- [Qualification evidence](https://docs.openadapt.ai/get-started/what-works-today/): Accepted substrate results, exact environments, and deployment boundaries
- [Author a workflow](https://docs.openadapt.ai/get-started/first-workflow/): A named human records, compiles, lints, replays, and reads the report on your own web app. Authority, not daily operator.
- [What you get](https://docs.openadapt.ai/get-started/what-you-get/): The workflow bundle, the run report, and what each artifact is for
diff --git a/docs/start/index.md b/docs/start/index.md
index 0a8580e..12046ff 100644
--- a/docs/start/index.md
+++ b/docs/start/index.md
@@ -1,6 +1,6 @@
---
title: Start moved
-description: Install OpenAdapt and run the current record, compile, replay tutorial.
+description: Install OpenAdapt on this computer, then continue at Get started.
canonical: https://docs.openadapt.ai/get-started/
redirect_to: /get-started/
search:
@@ -12,12 +12,15 @@ hide:
# Start moved
-This URL is an alias. The walkthrough is [Get started](/get-started/).
+This URL is an alias. Install first. The walkthrough is
+[Get started](/get-started/).
```bash
-pip install openadapt
-openadapt quickstart
-openadapt quickstart --break-it
+python -m pip install --upgrade openadapt
```
-The bundled workflow is a tutorial. [Continue to Get started](/get-started/){ .md-button .md-button--primary }
+Desktop works too: [openadapt.ai/start](https://openadapt.ai/start). The
+bundled tutorial (`openadapt quickstart`, then optionally
+`openadapt quickstart --break-it`) is optional.
+
+[Continue to Get started](/get-started/){ .md-button .md-button--primary }
diff --git a/scripts/vendor_design_tokens.py b/scripts/vendor_design_tokens.py
index d046cea..27cb5ec 100644
--- a/scripts/vendor_design_tokens.py
+++ b/scripts/vendor_design_tokens.py
@@ -21,6 +21,8 @@
import json
import os
import sys
+import urllib.error
+import urllib.parse
import urllib.request
from pathlib import Path
@@ -29,24 +31,64 @@
PROVENANCE_PATH = VENDOR_DIR / "provenance.json"
TIMEOUT_SECONDS = 30
+USER_AGENT = "openadapt-ops-vendor-design-tokens"
+API_ROOT = "https://api.github.com"
+
+
+class FetchError(RuntimeError):
+ """A canonical-file fetch failed. --check must not treat this as a match."""
def sha256(payload: bytes) -> str:
return hashlib.sha256(payload).hexdigest()
+def github_token() -> str | None:
+ return os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
+
+
+def contents_url(repository: str, path: str, ref: str) -> str:
+ return (
+ f"{API_ROOT}/repos/{repository}/contents/{path}"
+ f"?ref={urllib.parse.quote(ref, safe='')}"
+ )
+
+
def fetch(url: str, accept: str) -> bytes:
- request = urllib.request.Request(url, headers={"Accept": accept})
- token = os.environ.get("GITHUB_TOKEN")
- if token and url.startswith("https://api.github.com/"):
+ headers = {"Accept": accept, "User-Agent": USER_AGENT}
+ request = urllib.request.Request(url, headers=headers)
+ token = github_token()
+ if token and url.startswith(f"{API_ROOT}/"):
request.add_header("Authorization", f"Bearer {token}")
- with urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) as response:
- return response.read()
+ try:
+ with urllib.request.urlopen(request, timeout=TIMEOUT_SECONDS) as response:
+ return response.read()
+ except urllib.error.HTTPError as exc:
+ raise FetchError(f"GET {url} -> HTTP {exc.code}") from exc
+ except urllib.error.URLError as exc:
+ raise FetchError(f"GET {url} -> {exc.reason}") from exc
+ except TimeoutError as exc:
+ raise FetchError(f"GET {url} -> timed out") from exc
+
+
+def fetch_canonical(provenance: dict, entry: dict) -> bytes:
+ # openadapt-web is private, so raw.githubusercontent.com 404s in Actions.
+ # A repo-scoped GITHUB_TOKEN cannot authenticate that host. Use the
+ # Contents API the same way openadapt-desktop does (PR 154).
+ token = github_token()
+ if token:
+ url = contents_url(
+ provenance["canonical_repository"],
+ entry["canonical_path"],
+ provenance["canonical_branch"],
+ )
+ return fetch(url, "application/vnd.github.raw")
+ return fetch(entry["raw_url"], "text/plain")
def canonical_commit(provenance: dict) -> str:
url = (
- f"https://api.github.com/repos/{provenance['canonical_repository']}"
+ f"{API_ROOT}/repos/{provenance['canonical_repository']}"
f"/commits/{provenance['canonical_branch']}"
)
return json.loads(fetch(url, "application/vnd.github+json"))["sha"]
@@ -76,7 +118,11 @@ def main() -> int:
f"{entry['canonical_path']} instead."
)
- canonical = fetch(entry["raw_url"], "text/plain")
+ try:
+ canonical = fetch_canonical(provenance, entry)
+ except FetchError as exc:
+ print(f"{name}: {exc}", file=sys.stderr)
+ return 1
canonical_digest = sha256(canonical)
if arguments.write:
diff --git a/tests/test_vendor_design_tokens.py b/tests/test_vendor_design_tokens.py
new file mode 100644
index 0000000..f2f15b6
--- /dev/null
+++ b/tests/test_vendor_design_tokens.py
@@ -0,0 +1,110 @@
+"""Fail-closed fetch for the online half of the design-token guard.
+
+``tests/test_design_tokens.py`` is the offline half. This file covers
+``scripts/vendor_design_tokens.py`` without hitting the network: a mocked
+HTTP 404 must exit non-zero, and a token must send the request to the
+GitHub Contents API rather than raw.githubusercontent.com.
+"""
+
+from __future__ import annotations
+
+import io
+import sys
+import urllib.error
+from email.message import Message
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parent.parent
+sys.path.insert(0, str(ROOT / "scripts"))
+
+import vendor_design_tokens as vendor # noqa: E402
+
+
+CI_WORKFLOW = (ROOT / ".github" / "workflows" / "ci.yml").read_text(encoding="utf-8")
+VENDOR_DIR = ROOT / "docs" / "stylesheets" / "vendor" / "openadapt-web"
+
+
+class FakeResponse:
+ def __init__(self, body: bytes) -> None:
+ self._body = body
+
+ def read(self) -> bytes:
+ return self._body
+
+ def __enter__(self) -> FakeResponse:
+ return self
+
+ def __exit__(self, *args: object) -> None:
+ return None
+
+
+def _http_error(url: str, code: int) -> urllib.error.HTTPError:
+ return urllib.error.HTTPError(
+ url, code, "Not Found", Message(), io.BytesIO(b"")
+ )
+
+
+def test_fetch_raises_on_http_404(mocker) -> None:
+ url = (
+ "https://api.github.com/repos/OpenAdaptAI/openadapt-web"
+ "/contents/styles/tokens.json?ref=main"
+ )
+ mocker.patch.object(
+ vendor.urllib.request,
+ "urlopen",
+ side_effect=_http_error(url, 404),
+ )
+ with pytest.raises(vendor.FetchError, match="HTTP 404"):
+ vendor.fetch(url, "application/vnd.github.raw")
+
+
+def test_check_returns_nonzero_when_canonical_fetch_404s(
+ monkeypatch, mocker, capsys
+) -> None:
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
+ monkeypatch.setattr(sys, "argv", ["vendor_design_tokens.py", "--check"])
+
+ def fake_urlopen(request, timeout=None):
+ raise _http_error(request.full_url, 404)
+
+ mocker.patch.object(vendor.urllib.request, "urlopen", side_effect=fake_urlopen)
+
+ assert vendor.main() == 1
+ captured = capsys.readouterr()
+ assert "HTTP 404" in captured.err
+ assert "Vendored design tokens match" not in captured.out
+ assert "/contents/styles/tokens.json" in captured.err
+
+
+def test_check_uses_contents_api_when_token_is_set(monkeypatch, mocker) -> None:
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
+ monkeypatch.setattr(sys, "argv", ["vendor_design_tokens.py", "--check"])
+ requested: list[str] = []
+ authed: list[bool] = []
+
+ def fake_urlopen(request, timeout=None):
+ url = request.full_url
+ requested.append(url)
+ authed.append(request.has_header("Authorization"))
+ if "raw.githubusercontent.com" in url:
+ raise _http_error(url, 404)
+ if "/contents/styles/tokens.json" in url:
+ return FakeResponse((VENDOR_DIR / "tokens.json").read_bytes())
+ if "/contents/styles/tokens.css" in url:
+ return FakeResponse((VENDOR_DIR / "tokens.css").read_bytes())
+ raise _http_error(url, 404)
+
+ mocker.patch.object(vendor.urllib.request, "urlopen", side_effect=fake_urlopen)
+
+ assert vendor.main() == 0
+ assert any("/contents/styles/tokens.json" in url for url in requested)
+ assert any("/contents/styles/tokens.css" in url for url in requested)
+ assert not any("raw.githubusercontent.com" in url for url in requested)
+ assert all(authed)
+
+
+def test_ci_job_still_runs_check_and_reads_private_web_with_admin_token() -> None:
+ assert "python scripts/vendor_design_tokens.py --check" in CI_WORKFLOW
+ assert "secrets.ADMIN_TOKEN || github.token" in CI_WORKFLOW