DESIGN.md@docs · git:20261015.b168f96 · 2026-10-15 · sha256 5bddf4160013cfdf

DESIGN.md@docs git:20261015.b168f96A

Immutable. This exact content never changes and is served at /api/v1/blob/5bddf4160013cfdf.

# OpenAdapt Agent design

An exact Agent release gets its product state from the signed Production
admission record. A missing, expired, revoked, mismatched, or unverifiable
admission means **not actively admitted**. The validator doesn't restore an
older admission or assign a fallback lifecycle label.

The live target ledger is
https://openadapt.ai/production-lifecycle.json. It currently has seven
target admissions. Evidence class is `remote-safe-synthetic`. The live
workflow ledger is
https://openadapt.ai/production-workflow-admissions.json. It currently
lists seven synthetic admissions (`0.0.0-synthetic`). That isn't a
customer job. Standard and Regulated need an active workflow admission
for the exact bundle version. Demo and the synthetic sandbox may run
without one.

Your AI agent decides what to enter. OpenAdapt enters it in the app and checks that it saved.

`openadapt-agent` is the default runtime interface for a calling agent. It
is not a second workflow engine, and it is not "OpenAdapt the agent." Flow
owns compilation, policy, identity, verification, durable execution, repair,
and audit. The CLI remains. Headless does not mean the UI is gone.

## Roles

| Role | Who | What they do |
|---|---|---|
| Calling agent | Your AI agent (Claude Code, an MCP client, an orchestrator) | Chooses a workflow, sends inputs and its own `request_id`, acts on the outcome |
| Operator | A person at the computer that runs workflows | Starts the server, answers paused runs, checks records the result says to check |
| Authority | Named person | Approves the compiled workflow, certifies policy, resolves identity, record-check, and judgment stops |
| Auditor | Compliance | Reviews receipts, revokes admission |

"Operator" always means a person. Computer-use agents are the user of
OpenAdapt. They are not the executor inside OpenAdapt. A local agent may
click during the demonstration while the person watches. The calling agent
must not be the sole source of a production demonstration of a novel GUI
path. Identity, record-check, expired-policy, novel-UI, and admission stops
still require a person. A calling agent never resolves those classes; it
reads `needs_review` or `not_sure_if_saved` and waits.

`openadapt-agent` exposes two complementary interfaces:

1. MCP tools over local stdio.
2. Portable Agent Skills.

## Architecture

```text
MCP client / Agent Skill
          │
          │ local stdio + exact JSON schemas (inputSchema / outputSchema)
          ▼
openadapt_agent.mcp          (local stdio only; HTTP shim forbidden)
          │
          ├── openadapt_agent.bridge         tool specs + dispatch
          │     ├── list_workflows / run_workflow / get_run   (the contract)
          │     │     ├── cards     workflow cards: name, purpose, done_means, typed inputs
          │     │     ├── service   validate → request gate → worker thread → wait
          │     │     ├── runs      durable records + Flow IdempotencyLedger
          │     │     └── contract  transaction_outcome → four outcomes, closed reasons
          │     │           │
          │     │           ├── operator bundles ─► openadapt-flow run subprocess
          │     │           └── sandbox ─────────► Flow run gate + Replayer on MockMed
          │     ├── older tools: get_workflow, get_run_report, run_<id> (deprecated)
          │     ├── PHI-safe Needs Attention projection
          │     └── action-specific attended decisions ─► openadapt-flow durable API
          │
          ├── openadapt_agent.authoring   (--authoring first demo, stdio)
          │     observe / start_record / click / halt
          │     local type (agent-driven Recorder.type_text)
          │     pause Continue → record_observed (never type_text)
          │           │
          │           ▼
          │     openadapt_flow.authoring.AuthoringSession
          │
          └── openadapt_agent.mailbox     (authoring connect, outbound HTTPS)
                parse openadapt://runner / pack URL
                POST claim oab_ → poll wait=0 → Allow-per-sub
                Continue → record_observed (never type_text)
                overlay chrome stays Desktop-only
```

The MCP adapter is intentionally thin. Tool descriptions and dispatch
live in a transport-independent bridge so they can be tested without
starting stdio.

## Why the package keeps both MCP and Agent Skills

`openadapt-agent emit-skill` writes a skill from the workflow card: name,
purpose, what done means, typed inputs, how to call `run_workflow`, and how
to read the four outcomes. A skill is loaded into model context, which for
most clients means a hosted model, so it never carries recorded example
values, step intents, observed text, typed secrets, local paths, or a
replay recipe that would fall back to recorded values. Each description
names its own workflow and inputs. `--include-bundle` copies the compiled
bundle next to `SKILL.md`; that copy is protected workflow data.

Flow's per-bundle MCP emitter and `openadapt-flow emit-skill` have a
different purpose: self-contained replay demos for one workflow.
`openadapt-agent serve` is the governed multi-bundle local surface. It
uses Flow's `run` admission path and returns durable evidence rather than
invoking permissive replay.

The repository therefore remains `openadapt-agent`, not
`openadapt-mcp`: MCP is one transport; Agent Skills are an equally
supported surface.

## The result contract

Every agent-facing result comes from `openadapt_agent.contract`. It has
four terminal outcomes, each mapped to one decision the calling agent
makes:

| `outcome` | Meaning | `safe_to_retry` |
|---|---|---|
| `done` | Saved and checked by reading the record back | false |
| `needs_review` | Stopped before saving; nothing written; a person decides | false |
| `not_sure_if_saved` | A save may have gone through; a person checks the record | false |
| `did_not_run` | This call wrote nothing. After `request_id_conflict`, an earlier request with the same `request_id` may have saved | true when a retry may succeed |

`running` is the one non-terminal value. The plain result is a pure
function of Flow's `transaction_outcome`, the process facts (did Flow
start, did it exit cleanly, did it time out), the state of any durable
pause, and a consistency check of the persisted report:

| Flow evidence | `reason` | `outcome` |
|---|---|---|
| consistent `VERIFIED`, exit 0 | `saved_and_checked` | `done` |
| `HALTED_BEFORE_EFFECT` | from the pause category, for example `record_not_confirmed` | `needs_review` |
| a proven-no-effect outcome whose report says the item stopped for a person's approval (`approval.decision: approval_required`) | `approval_needed` | `needs_review` |
| a run still going whose save waits for a person's approval (an unanswered `item-approval` request) | `waiting_for_approval`, `record_changed: "unknown"`, never stored as the result | `needs_review` |
| `REJECTED_POLICY`, `CANCELED`, `FAILED_PLATFORM` with an open durable pause | from the pause category | `needs_review` |
| the same with no open pause | `policy_refused`, `canceled`, `platform_error` | `did_not_run` |
| any proven-no-effect outcome whose pause a person rejected | `stopped_by_person` | `did_not_run` (no retry) |
| `RECONCILIATION_REQUIRED` | `save_not_confirmed` | `not_sure_if_saved` |
| `COMPLETED_UNVERIFIED` | `not_checked` | `not_sure_if_saved` |
| `ROLLED_BACK` | `change_reversed` | `not_sure_if_saved` |
| timeout after start (time a person spends approving doesn't count), inconsistent or missing report, coarse `HALTED` alone | `timed_out`, `result_unreadable` | `not_sure_if_saved` |
| a server that stopped mid-run | `interrupted` | `not_sure_if_saved` |
| exit 2 before execution | `not_ready_to_run` with closed `failed_checks` | `did_not_run` |
| Flow could not be launched | `platform_error` | `did_not_run` |
| the app isn't reachable, checked before anything is sent (sandbox) | `app_unreachable` | `did_not_run` |
| invalid inputs, unknown workflow, runs disabled | `invalid_input`, `unknown_workflow`, `runs_disabled` | `did_not_run` |
| a used `request_id` with a different workflow or inputs | `request_id_conflict`, with `record_changed: "unknown"` | `did_not_run` (no retry) |
| a `request_id` whose five attempts each proved nothing was written | `retry_limit` | `did_not_run` (no retry) |

Only `done` says the change was saved. `record_changed: "no"` and
`safe_to_retry: true` appear only when the evidence proves nothing was
written. `request_id_conflict` starts nothing, but an earlier attempt
under that `request_id` may have saved, so it reports
`record_changed: "unknown"`. A coarse `HALTED` without a transaction
outcome is uncertain, never "nothing changed". A failed Needs Attention
lookup counts as an open pause, so the run waits for a person. `proof`
(`local`, `sealed`, `simulated`, or `none`) is separate from the outcome:
a verified local run is `done` with `proof: "local"`, never an error. A
partner whose policy requires a signed receipt checks `proof`.

Every sentence in `what_happened` and `next_action` is fixed copy keyed by
a closed reason code. No observed screen text, recorded value, input value,
caller-supplied name, or local path reaches a result.

### Requests, retries, and records

`request_id` is required on `run_workflow`. Each attempt reserves
`<sha256(request_id)>:<attempt>` in openadapt-flow's `IdempotencyLedger`
(namespace `openadapt-agent/run-workflow/v1`, under
`<runs-dir>/openadapt-agent/`) before anything runs. A lost race reads back
the winner. The same `request_id` returns the latest attempt's result with
`replayed: true`. A new attempt starts only when the latest one ended in a
reason from `RETRYABLE_REASONS`, each of which proves nothing was written,
and at most five times. A `request_id` reused with different inputs is
refused as `request_id_conflict`, with `record_changed: "unknown"` because
the earlier attempt may have saved. If Flow's ledger can't be opened, local
exclusive-create claim files give the same at-most-once guarantee for that
runs directory.

Every call leaves a record at
`<runs-dir>/openadapt-agent/runs/<run_id>.json`, refusals included, so
`get_run` can always answer. Records hold the contract result only. Inputs
are never stored; a keyed fingerprint of `(workflow, inputs)` is.

`run_workflow` runs a new attempt on a worker thread and waits up to
`wait_seconds` (default 60, maximum 600). A run still going returns
`outcome: "running"` with its `run_id`. `get_run` waits the same way. A
record left `running` by a process that is gone reads as `interrupted`. A
`needs_review` result is re-read from Flow on each `get_run`, so a
person's Continue or Reject shows up. A re-read never turns a reviewed run
into a retryable result: Flow clears a continued pause just before it
replaces the pre-resume report, so the run keeps waiting until the new
report lands.

### Workflow cards

`list_workflows` returns one card per workflow. Types and choices come from
Flow's `param_specs`. Words come from an optional operator-written
`openadapt-card.json` next to `workflow.json`. Card text is
length-limited and refused when it looks like it carries an identifier (a
run of five or more digits, an email address, a phone number). A refused
card falls back to the default card with a local warning that never
repeats the text. Every declared input stays required unless
`--allow-synthetic-recorded-defaults` is set.

## Modes and tool registration

| Start | Mode | Tools |
|---|---|---|
| `serve` (no flags), `--mode sandbox`, `--tutorial`, `--allow-run` without `--bundles` | sandbox | `list_workflows`, `run_workflow`, `get_run` |
| `--bundles DIR` | production, read-only | `list_workflows`, `get_run`, `get_workflow`, `get_run_report`, `list_needs_attention`, `get_attention_item` |
| `--mode production --bundles DIR` (or `--allow-run`) | production | the above plus `run_workflow` and deprecated `run_<opaque-id>` |
| `--mode attended --bundles DIR` (or `--allow-run --allow-attended-actions`) | attended | the above plus Reject, Teach, Escalate; Continue and Skip with a qualified `--config` |
| `--bundles DIR --allow-attended-actions` (older flags, no runs) | attended, read-only | the read-only tools plus the same decisions on paused runs |

The read-only tools return PHI-safe projections: workflow cards, opaque
ids, availability, status, and count or boolean metrics. They do not
return workflow labels, recorded values, step intents, report bodies,
observed text, local paths, subprocess output, or exception text. The
attention tools use the same boundary plus typed categories, artifact
ids, and non-authorizing capability metadata.

The deprecated per-workflow `run_<opaque-id>` tools keep their schemas and
legacy `status` field and add the contract fields. They take no
`request_id`, so their descriptions say a retry can write twice. Missing
or unknown parameters are rejected before the subprocess starts. A
per-call URL is accepted only if the operator separately enabled
`--allow-url-override`. No run path passes
`--approve-unverified-writes`.

Two server-start options are intentionally separate from ordinary
operation:

- `--allow-protected-export` includes raw labels, values, intents, local
  paths, reports, stdout/stderr, and detailed local errors. It is for an
  explicitly trusted MCP client inside the same protected data boundary.
- `--allow-synthetic-recorded-defaults` permits omitted parameters to
  use demonstrated values. It requires run authority and is only for
  synthetic demos; it never places those values in a tool schema.

### Recording tools

`--authoring` registers first-demo tools over the same local stdio
server. Probe names match hosted MCP: `observe`, `start_record`,
`click`, `halt`. The rest are local stdio only. `AUTHORING_TOOLS` in
`openadapt_agent.authoring` is the source of truth, in this order:

| Tool | Group | What it does |
| --- | --- | --- |
| `observe` | probe | Returns a PHI-safe tree of the pinned window: no values, titles, URLs, screenshots, or pixels. |
| `start_record` | probe | Starts a Flow Recorder session. Refused for Windows native, Citrix, and RDP, which are coach-only. |
| `click` | probe | Clicks one observed node. Local stdio also accepts x,y pixels. |
| `halt` | probe | Ends the authoring session without compiling. It isn't a governed run halt. |
| `type` | local | Types text the agent supplies through `Recorder.type_text`. Not for secrets or for text a person typed. |
| `pause_for_input` | local | Pauses so a person can type in the app. A secret pause stores no text. |
| `continue_input` | local | Stands in for overlay Continue and saves the person's input with `Recorder.record_observed`. |
| `stop_record` | local | Finishes the Recorder session without compiling. |
| `compile` | local | Wraps Flow `compile_recording` and returns `needs_human_admit`. |
| `admit` | local | Asks the person to accept the draft in a form-elicitation prompt. Refused when the client can't show one. |
| `get_command_result` | local | Returns the last in-process result, for clients that expect the hosted `command_id` shape. |
| `set_coach` | local | Stores a PHI-safe coach hint of up to 80 characters in this process. |
| `get_coach` | local | Returns the last coach hint. |
| `bind_status` | local | Reports local bind status. A stdio server is already on the machine, so there's no pack mailbox. |

Hosted MCP remains pause-only: it has no `type`. Human
type during `pause_for_input` is persisted with `Recorder.record_observed`
on the pause-target node, never `type_text`. `compile` wraps Flow
`compile_recording` and returns `needs_human_admit`; an agent click never
paints `VERIFIED`. `admit` asks the person to accept the pre-filled
draft through MCP form elicitation; the human does not fill schema,
authority, effect, environment, or digest. The tool takes no arguments, so
the calling agent can't supply the acceptance, and a client without form
elicitation is refused. If the Flow session has no `admit`, the tool fails
closed and does not mint a Seal or write an unsigned ledger row.

`--authoring` does not imply `--allow-run`. `--bundles` is optional iff
`--authoring` (or the sandbox). The
published run recipe in `server.json` still requires `--bundles` and
stays `transport: stdio`. Authoring is a first demo; there is no bundle
yet.

Observe is a fail-closed PHI projection (`openadapt.authoring.observe/v1`):
no `value`, `text`, window `title`, screenshot, OCR, URL, or backend
pixels. Windows native, Citrix, and RDP are `COACH_ONLY` in v1.

The session object is Flow's public `openadapt_flow.authoring` module
(`AuthoringSession(backend, out_dir, backend_kind=…)` when F1 is
importable). Until that module is importable, `serve --authoring` fails
closed with an explicit dependency error. Windows native, Citrix, and
RDP construct a coach-only stand-in and never spawn `win_agent`. Observe
is fail-closed to the T1 wire (`additionalProperties: false`, node ids
`n_` + 8 hex, 200 nodes / 32 KiB). Capture's projector is used when
importable. If Desktop has advertised authoring IPC, overlay stays
Desktop-owned; stdio `--authoring` does not speak the D2 protocol.
`authoring connect` is the outbound mailbox client for hosted chat apps.
Tests cover the stdio tool surface with a fake session and an F1-shaped
session, and the mailbox client against a mocked wait=0 poll.

## Governed runs

Each operator-bundle run shells out to the `openadapt-flow` installed in
the same Python environment. The command uses Flow's fail-closed `run`
verb, so its certification, identity, effects, encryption, integrity, and
egress gates remain authoritative.

The bridge reports `done` (legacy `status: "success"`) only when all of
these hold:

1. Flow exits with code 0.
2. The persisted report has `transaction_outcome: VERIFIED` and
   `execution_outcome: VERIFIED`.
3. The persisted report has a consistent `success: true` value and a
   consistent outcome envelope.

A production-eligible `VERIFIED` run is `done` with `proof: "local"`.
Local MCP mints no Seal, and reporting a verified write as an error would
invite a retry and a duplicate write. A legacy report with only the
`success` flag is `not_sure_if_saved` ("finished, not checked"). Standard
and Regulated need an active workflow admission for the exact bundle
version. Demo and the synthetic sandbox may run without one.

Exit 2 is a governed refusal before execution, mapped to closed
`failed_checks` codes. A timeout is uncertain, never a rollback. Report
evidence always outranks a process exit code.

Parameters are passed in a mode-`0600` temporary JSON file and removed
after the run. Server-fixed target, policy, deployment configuration,
timeout, and extra Flow arguments cannot be changed by an MCP call.

The runner retains detailed reports and subprocess diagnostics locally,
but its default MCP projection contains only the contract fields, opaque
ids, fixed copy, and schema-limited metrics. Every outcome path shares
this projection, so an exception cannot turn into an egress channel.

## Sandbox

With no `--bundles`, the server serves one synthetic workflow,
`add_triage_note`, on MockMed, the synthetic clinic app that ships with
openadapt-flow. The `sandbox_case` input selects what the app does:
`normal` (done), `duplicate_record` (needs_review), `false_saved_banner`
and `timeout_after_save` (not_sure_if_saved), and `app_offline`
(did_not_run). Results always carry `mode: "sandbox"` and the engine name.

- The `flow` engine needs the `tutorial` extra. In a background thread at
  start, Flow records the workflow once (reused afterwards), compiles it
  with mined record checks, binds the mined `note` read-back to the `note`
  parameter (the operator review Flow leaves open after mining), and
  certifies it under `clinical-write`. Each run is admitted by Flow's run
  gate under the standard profile and executed by Flow's `Replayer` with
  the caller's note, a right-record check, and an independent read of
  MockMed's record store. This is the same path Flow's own tutorial uses.
- The `simulated` engine returns the same contract results without opening
  any app. Results open with "Simulated sandbox result" and `done` carries
  `proof: "simulated"`.

`--sandbox-engine auto` picks `flow` when Playwright and a compatible Flow
are installed, and falls back to `simulated` when the app can't start. The
sandbox keeps its records under `~/.openadapt/agent-sandbox` (or
`OPENADAPT_AGENT_SANDBOX_DIR`, or `--runs-dir`), so it never writes into
the project a client was launched from.

## Needs Attention

Flow creates a signed attended capability for a specific durable pause.
The capability binds:

- run and pause identity;
- workflow and bundle version;
- exact step or interpreter cursor;
- checkpoint lineage and expected next transition;
- verification and delivery state;
- allowed actions and expiration.

Only the capability digest and allowed-action summary cross the
agent-facing queue projection. The HMAC, protected evidence, and local
paths stay inside the Flow runtime boundary.

Each mutation is an action-specific MCP tool with
`additionalProperties: false`. Its payload contains only:

- opaque attention ID;
- exact capability digest;
- stable idempotency key;
- one explicit boolean operator confirmation.

There is no field for challenge answers, credentials, screenshots,
observed text, or arbitrary approval prose.

The payload's confirmation boolean is not treated as proof of human
presence. Before dispatch, the MCP server performs a second,
action-specific form elicitation and requires an explicit accept plus
confirmation from the local operator. Elicitation is a host-mediated
explicit-confirmation signal, not cryptographic proof that a particular
person clicked, nor identity proof. Flow separately records the effective
local OS account as the operator. A client that does not advertise
form elicitation cannot execute attended mutations through MCP; the
operator uses Flow's existing attended console/CLI instead, where all
five capabilities remain available. This is a transport authorization
choice, not a read-only conversion. Tool annotations also mark Continue
and Skip as destructive, idempotent, and open-world. Reject is destructive
and idempotent but not open-world because it dispatches no new application
action. These hints let the host apply
its own approval policy. Annotations and elicitation do not replace
Flow's signed capability, live revalidation, idempotency, or durable
audit.

### Continue

Continue means the human already completed the paused task in the live
application. Flow:

1. reloads and validates the exact signed pause under a filesystem
   lease;
2. verifies the human-completed postconditions and independent effects
   in the deployment-bound live session;
3. commits a human-completed checkpoint;
4. resumes from the next transition.

The completed action is never actuated again. If delivery may have
crossed the boundary without a terminal receipt, retries are refused
until reconciliation.

### Skip

Skip is not a generic bypass. It exists only when the signed capability
and compiled workflow declare a safe, non-consequential skip. Flow
rechecks that guard against current state. Consequential, stale,
ambiguous, or undeclared skips are refused.

### Reject

Reject terminates the current run and permanently prevents resume. The
rejection dispatches no new application action. Earlier steps in the run can
still have effects, so the operator must inspect the protected local report
and transaction outcome. Use Escalate instead when a qualified operator can
still inspect and continue the run.

### Teach

Teach records an audited request for a corrective demonstration. The
durable pause remains intact. The existing Flow teach/revision pipeline
owns capture, regression evaluation, evidence banking, and promotion;
the MCP client cannot directly rewrite a bundle.

### Escalate

Escalate records a durable, audited request for qualified assistance and
leaves the pause available for later resolution.

## Idempotency and thread ownership

Flow persists attended decisions before crossing a delivery boundary.
Repeating the same action with the same idempotency key returns its
terminal decision. Reusing a key for different content is refused.

Continue and Skip use a persistent deployment-bound backend. Some
backends, including Playwright, are thread-affine and their synchronous
APIs cannot run inside MCP's asyncio event loop. Flow's public
`AttendedActionService` therefore creates, uses, and closes the live
executor on one dedicated non-async owner thread. The bridge submits
exact signed requests through that service while the event loop remains
responsive. Run subprocesses and read-only projections use ordinary
worker threads.

Flow serializes live attended actions and applies per-pause filesystem
leases. A second process cannot silently duplicate an in-flight
decision.

## Identity and transport

The MCP server uses local stdio. The process inherits the OS user's
permissions, and the effective local OS account is recorded as the
attended operator. POSIX uses the effective UID account; Windows uses
the process/thread token-backed `GetUserNameW` API rather than the
caller-controlled `USERNAME` environment variable. A blank operator
identity fails closed.

This process must not be port-forwarded or exposed as an unauthenticated
network service. An HTTP / Streamable-HTTP **listener** in this MIT package
remains forbidden, including when `--authoring` is set. Hosted ChatGPT.com
/ Claude.ai cannot talk to localhost. Send those tabs to
https://openadapt.ai/start. They still can't click the user's GUI.
Pip users run `openadapt-agent
authoring connect` — an **outbound** mailbox client (claim `oab_`, poll
`wait_seconds: 0`, Allow-per-`sub`) copied from Desktop
`engine/authoring_runner.py` when that engine is not importable. Overlay
chrome, launchd, and the `openadapt://` URL handler stay Desktop-only.
See `docs/MAILBOX_CLI.md`. OpenAdapt Cloud owns remote authentication,
multi-tenancy, tenant-scoped authorization, fleet policy, and managed
execute. `--authoring` does not add those, and it does not imply
`--allow-run`.

## Dependency boundary

The attended bridge uses Flow's public durable action contract and public
`AttendedActionService`, constructed from a public `DeploymentConfig`.
The package pins the Flow minor release containing that contract so a
later refactor cannot silently change backend construction, thread
ownership, or cleanup semantics.

Agent applies only explicit server-start URL, visibility, and egress
overrides to that typed deployment config. Replayer, backend, policy,
verification, owner-thread, resume, and audit logic are never copied
into this repository.

## Test and release contract

Tests cover:

- exact tool registration and schemas;
- opaque workflow/run IDs, required parameters, and no recorded defaults
  in MCP schemas;
- adversarial PHI/secret/path/exception strings across workflow
  discovery, success, halt, refusal, timeout, error, and report lookup;
- explicit protected-export and synthetic-default modes;
- MCP form elicitation and action annotations;
- PHI-safe queue projections and path traversal refusal;
- stale capability, unknown field, and false-confirmation refusal;
- idempotent Continue without re-actuation;
- Reject, Teach, and Escalate without a live service;
- delegation to Flow's public service context;
- compatibility with Flow's public, thread-owned attended service;
- the four-outcome contract: every reason against the output schema, real
  Flow 1.35.1 outcome shapes, pause-aware mapping, and the rule that only
  proven no-effect results allow a retry;
- `request_id` replay, conflicts, retry limits, interrupted runs, and
  reviewed runs a person resolves;
- `outputSchema` and `structuredContent` on MCP SDK 1 and SDK 2;
- workflow cards: Flow types, operator text, identifier guard, closed input
  problems;
- every sandbox case with the simulated engine, and, in the opt-in
  `sandbox-e2e` job, with real Flow, a real browser, and MockMed;
- success/halt/refusal/timeout outcome mapping for the older tools;
- MCP serialization and thread ownership;
- Agent Skill emission without recorded values, step intents, or secrets,
  and one description per workflow;
- `--authoring` probe tools (`observe`, `start_record`, `click`, `halt`)
  and local `type`; observe projection drops values/titles/screenshots
  and extra keys, caps the wire at 32 KiB, and uses `n_` + 8 hex node
  ids; pause Continue uses `record_observed` rather than `type_text`;
  compile returns `needs_human_admit`; `admit` needs the person's answer
  in a form-elicitation prompt; `--authoring` does not enable
  run tools; `server.json` stays stdio with `--bundles` required;
  `authoring connect` parses `openadapt://runner` / pack URLs, claims
  `oab_`, polls `wait_seconds: 0`, prompts Allow-per-`sub`, and Continue
  uses `record_observed` (never `type_text`).

CI runs on Python 3.10, 3.11, and 3.12. It also builds the wheel and
sdist, verifies MIT metadata and license inclusion, and refuses package
artifacts containing repository-only copyleft benchmark material.