Skip to content
Merged
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: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ Browser artifacts are written to `test-results/` and `playwright-report/`
- MandateBound’s prove step uses `simulate --scenario operator` as the dispute-oriented demo path. Full AP2 pack assemble/verify remains in MandateBound’s own CLI and docs.
- This repo does not read or write any private GitHub repositories.

For deeper coverage of how the orchestrator sequences the three libraries and
how `stack-lock.json` is governed, see [docs/architecture.md](docs/architecture.md)
and [docs/stack-lock.md](docs/stack-lock.md).

## License

Apache-2.0
Expand Down
114 changes: 114 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Orchestrator architecture

Agent Action Stack is a thin orchestrator. It does not re-implement policy
evaluation, recourse rails, or dispute evidence simulation. It sequences three
public sibling libraries in a fixed order and persists one isolated run
bundle per invocation.

This document describes the architecture the code in this repository actually
implements. It does not describe behavior owned by the sibling libraries;
their documentation is the source of truth for those.

## Bounded responsibility

- Orchestration only. Policy, recourse, and proof logic live in the three
sibling repositories.
- Synthetic connectors and scenarios only. No real account, merchant,
payment, or external provider integration.
- One run, one bundle. Each invocation writes an atomic bundle under
`.out/runs/<run-id>/` and updates `.out/latest.json` only on a complete
bundle; a failed or skipped stage cannot leave an older artifact looking
current.
- Public dependencies only. `stack-lock.json` pins the three sibling repos
to exact public commits. Nothing private is cloned or modified.

## Stage flow: decide then act then prove

The `runDemo` function in `bin/aas.mjs` is the orchestrator. It runs three
stages in order. Each stage returns a child result captured from a spawned
process, and the orchestrator records the stage status into the report.

```
decide (Constitutional Agent Testbench, Python)
|
| passed?
|--- no ---> stop (policy_failed)
v yes
act (Consequence Rail, Node)
|
| ok?
|--- no ---> stop (act_failed)
v ok
| outcome != "settled" or --dispute?
|--- no (and not --dispute) ---> skip (no_dispute)
v yes
prove (MandateBound, Node): dispute scenario or rail review
```

The `flow` field in the report records the actual path taken:

- `decide -> stop (policy failed)` when decide returns non-ok
- `decide -> act -> stop (act failed)` when act returns non-ok
- `decide -> act` when act settles cleanly and `--dispute` was not passed
- `decide -> act -> prove` when the rail outcome needs review or `--dispute`
forces the prove path
- `decide -> error` or `decide -> act error` or `decide -> act -> prove
error` when a stage throws

The prove stage has two modes, selected by `--prove`:

- `simulate` (default): runs MandateBound's canned dispute scenario. For the
demo the scenario is `operator`.
- `rail`: re-opens the act-stage rail bundle, verifies it with the rail's
own verifier, and binds it into a MandateBound review record for the same
action id and digests. The review records the rail's verdict without
re-verifying rail signatures; source truth stays unknown and legal effect
stays not determined.

## Child execution and timeouts

Each decide, act, and prove child is spawned with a bounded timeout. Default
is 30000 ms (`AAS_CHILD_TIMEOUT_MS`); empty values keep the default and
invalid integers are rejected at parse time. A hung child fails the stage
with code `AAS_CHILD_TIMEOUT` instead of blocking the run.

Child stdout is capped (`CHILD_JSON_LIMIT`) so a runaway tool cannot inflate
the run bundle. Captured stderr is clipped (`STDERR_LIMIT`) before it is
persisted with the stage record.

The decide stage runs on the first Python 3.11+ interpreter found, because
the locked Constitutional Agent Testbench declares `requires-python >= 3.11`.
Set `AAS_PYTHON` to use a specific interpreter; a missing interpreter or one
below 3.11 fails with an actionable message.

## Run bundle layout

Each invocation writes one atomic bundle under `.out/runs/<run-id>/`:

- `manifest.json`: stage status and component provenance
- `report.json`: user-facing run report
- `stages/<stage>.json`: captured output from each stage that ran

`.out/latest.json` is an atomic pointer to the most recent complete bundle.

## Provenance

The orchestrator resolves component provenance from `stack-lock.json` and the
checked-out `deps/` directories. Provenance is recorded on every run:
repository URL, commit, detached checkout flag, clean checkout flag, and
expected entrypoints present. The orchestrator never re-verifies the
components; the sibling CLIs and the rail's own verifier do that.

## Verify-only CI boundary

GitHub Actions in `.github/workflows/ci.yml` runs three jobs and nothing
else: `test` (unit suite plus syntax check plus GUI smoke), `integration`
(clean checkout bootstrap plus the integrator examples), and `browser`
(Playwright real-browser workflow tests). CI has no publish, deploy, push,
or release step. It does not write to any registry, package index, or
hosted target. `contents: read` is the only permission requested.

The same boundary holds locally: `npm test`, `npm run integration`,
`npm run example:review-handoff`, and `npm run test:browser` are read-only
with respect to anything outside `.out/`. The orchestrator writes only to
`.out/` for run bundles and to `.out/latest.json` for the latest pointer.
79 changes: 79 additions & 0 deletions docs/stack-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# stack-lock.json policy

`stack-lock.json` is the single source of truth for which versions of the
three sibling libraries the orchestrator is allowed to compose. Schema:
`agent-action-stack.lock/v1`. It pins each sibling's public URL, exact
reviewed commit, expected entrypoints, and where applicable the build hooks.

## What it pins

One record per sibling. The current lock has three:

- `constitutional-agent-testbench` (decide). Public URL, one reviewed
commit. Expected entrypoints `pyproject.toml` and
`src/constitutional_agent_testbench/cli.py`. No install or build step.
- `consequence-rail` (act). Public URL, one reviewed commit. Expected
entrypoints `package.json` and `cmd/crctl.js`. No install or build step.
- `mandatebound` (prove). Public URL, one reviewed commit. Expected
entrypoints `package.json`, `package-lock.json`, `src/cli.ts`. Post-build
entrypoint `dist/cli.js`; `install` hook `npm-ci`; `build` hook
`npm-run-build`. Bootstrap runs `npm ci --ignore-scripts` then the build.
The lock also pins, by construction, the public-only origin: every record
points at the public EauDoon GitHub repo. Substituting a private URL or
editing `deps/` directly is out of scope.

## How the lock is enforced

`scripts/bootstrap.mjs` loads the lock, asserts the full-stack Node version,
and prepares each dependency under `deps/`:

1. If `deps/<component>` is present, `inspectDependencyDirectory` verifies it
is a regular directory, detached (`HEAD` matches the pinned commit
exactly), clean, and contains every expected entrypoint. Substituted or
dirty pre-existing directories are rejected.
2. If absent, bootstrap clones the public URL at the pinned commit, checks
out detached, and verifies the entrypoints.
3. For components with an `install` hook, bootstrap runs the declared
command (`npm ci --ignore-scripts` for MandateBound).
4. For components with a `build` hook, bootstrap runs the declared command
(`npm-run-build` for MandateBound), then verifies post-build entrypoints.
The orchestrator records each component's provenance on every run bundle. A
run resolving components whose checkout disagrees with the lock surfaces the
disagreement in its manifest.

## When to update

Changing a pinned component is a coordinated cross-repo change. Do not bump
the lock on its own.

1. Land the upstream change in the sibling repo first; it must pass that
sibling's own CI and review.
2. Bump the matching `commit` field to the reviewed merge SHA.
3. Update `expected_entrypoints`, `post_build_entrypoints`, `install`, or
`build` only if the sibling's published contract actually changed.
4. Run `npm run integration` locally on Ubuntu and Windows. The CI
integration job runs the same proof on every push and pull request.
Security fixes follow the same path. The schema version (`schema_version`)
is bumped only when the shape changes in a way that requires loader
changes; existing tools keep reading older versions until the bump lands
across all consumers.

## Who can update

The lock is owned by the Agent Action Stack maintainers. Updates land via
pull request on `imp/<short-topic>-<date>` branches. PRs that change
`stack-lock.json` must cite the sibling repo, PR, and reviewed merge SHA;
show a passing `integration` job on both Ubuntu and Windows; and show a
passing `test` job (lock-load and dependency helpers live in the unit
suite).
Do not bypass the lockfile. Editing `deps/` directly, swapping a remote
URL, or relaxing the dirty-checkout rejection are out of scope. A PR that
needs any of those should propose a permanent fix in `scripts/bootstrap.mjs`
or in this policy document.

## What is out of scope

Real connectors, real secrets, real payment or merchant integrations;
changes that would read or write private repositories; policy or rail
rules that should live in their owning library. The lock pins reviewable
public artifacts. Anything else belongs in the sibling that owns it.
Loading