One reference path across three public libraries: decide, act, prove.
Agent Action Stack is a thin orchestrator. It does not re-implement the libraries. It runs them in a fixed order so a visitor can see how they compose.
On policy failure the stack stops. On a clean settled outcome, MandateBound is skipped unless you pass --dispute.
Experimental reference demo. Not legal advice, not a hosted service, not a safety certification.
| Stage | Public repo | Role in this demo |
|---|---|---|
| Decide | constitutional-agent-testbench | Evaluate refund-authorization JSON against a declared policy |
| Act | consequence-rail | Reserve recourse, execute a synthetic refund, settle or compensate |
| Prove | mandatebound | Run a dispute-oriented evidence simulation when the rail outcome needs review |
- Node.js 22.12+ (the full-stack workflow shares the pinned MandateBound floor; Node.js 20 is not supported for any workflow, standalone or full-stack)
- Python 3.11+ (stdlib only; no pip install required for the testbench)
- git
- network access once, for
npm run bootstrap(clones the three public repos intodeps/)
Private repositories are never cloned or modified.
npm run bootstrap
npm run demoExpected human output (pass path, no fault):
stack: agent-action-stack
response: pass
decide: passed
decide_passed: true
act: passed
act_outcome: settled
act_state: CLOSED
act_fault: none
prove: skipped
prove_scenario: none
prove_triggered_by: none
prove_mode: none
flow: decide -> act
bundle: .out/runs/<run-id>
Fail closed at decide:
npm run demo:failForce the dispute path via a compensated rail outcome:
npm run demo:disputeExpected flow line:
flow: decide -> act -> prove
Review the same case instead of simulating one:
node ./bin/aas.mjs demo --fault duplicate --prove railThe rail-review path persists 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: a recorded review proves the handoff, not the rail's claims.
JSON report:
node ./bin/aas.mjs demo --fault duplicate --jsonExport a run and replay its verification offline, without rerunning the action:
node ./bin/aas.mjs demo --fault duplicate --prove rail
node ./bin/aas.mjs export "$(ls -t .out/runs | head -1)" --out case.json
node ./bin/aas.mjs replay case.jsonReplay recomputes the evidence digest, re-runs the rail's own bundle verification over the exported bytes, and re-executes the MandateBound review, requiring a byte-identical review digest. It reports unavailable evidence, conflicts, and unsupported verification explicitly, and exits nonzero unless every check passes. Trust basis: the rail's synthetic demo keys via its own verifier; nothing embedded in the bundle is trusted for its own integrity. An exported case can also be imported in the GUI ("Replay an imported case"), which runs the same verification with no action execution or remediation; imported identity is untrusted text and the result proves no provenance or link to a local run.
stack-lock.json records the reviewed public repository URLs, exact commits, and
expected entrypoints. Bootstrap uses detached checkouts, rejects substituted or
dirty pre-existing directories, runs npm ci --ignore-scripts for MandateBound,
then runs its explicit build command.
Each decide, act, and prove child is bounded by AAS_CHILD_TIMEOUT_MS
(default 30000). A hung child fails the stage instead of blocking the run.
Empty AAS_CHILD_TIMEOUT_MS and AAS_GUI_PORT values keep those defaults;
invalid integers are rejected.
The decide stage runs on the first Python 3.11+ interpreter found, because the
locked 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 instead of an unreadable traceback.
Each invocation writes one atomic bundle under .out/runs/<run-id>/:
manifest.json: stage status and component provenancereport.json: user-facing run reportstages/*.json: output from stages that ran
.out/latest.json is an atomic pointer to the most recent complete bundle. A
failed or skipped stage cannot leave an older stage artifact looking current.
List runs newest-first with aas runs, inspect bounded case summaries with
aas cases (outcome, policy reference, review verdict, evidence digest, and
component revisions — never raw evidence), and compare two cases with
aas compare <run-id> <run-id>, which classifies the pair as identical,
different, or not comparable and lists the fields that differ. Comparison
states that differences do not establish causation and that matching
metadata does not prove matching evidence; it never mutates a case.
Remove oldest runs beyond a window with aas prune --keep <n> (--dry-run
previews). Pruning never deletes the run the latest pointer identifies, and
nothing is deleted without an explicit --keep. The GUI exposes the same
history and comparison through Load history and Compare selected cases.
Run npm run gui and open the printed loopback URL. The GUI calls the same
orchestrator, shows a readable decide/act/prove summary with skip reasons, a
bindings panel (action identity, recomputed evidence digest, provenance, and
the review verdict with its limits), and downloads a JSON export of the
selected run bundle. The prove selector offers the canned simulation or the
same-case rail review; every result and export stays tied to its run id.
npm run gui:smoke checks the server without
starting a long-running process. The server binds only to 127.0.0.1 on port
8787 by default (AAS_GUI_PORT selects another loopback port), requires the
exact loopback Host and same-origin boundary, and uses POST for a run.
npm test
npm run checknpm test is the unit suite (orchestrator and GUI models). npm run integration proves the pinned components from a clean checkout, and
npm run example:review-handoff runs the integrator example.
Real browser workflow tests drive the GUI through actual clicks, file selection, and asynchronous responses with Playwright (Chromium only, to keep downloads bounded):
npm install
npx playwright install chromium
npm run bootstrap
npm run test:browserThey cover run → inspect → export → import → replay, refusal, repeated
runs, stale-result clearing, and malformed/unavailable/tampered imports.
Browser artifacts are written to test-results/ and playwright-report/
(both ignored). Browsers cache under ~/.cache/ms-playwright.
fixtures/policy.json: refund gate: accept, low/moderate risk, recourse required, not blockedfixtures/response.pass.json: passes the gatefixtures/response.fail.json: fails the gate; act and prove are skipped
- Orchestration only. Behavior lives in the three libraries.
- Synthetic connectors and scenarios only.
- MandateBound’s prove step uses
simulate --scenario operatoras 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.
Apache-2.0
--domain selects the synthetic action domain; both use the same rail,
recourse, and review machinery:
refund(default): the documented refund scenario.inventory: a bounded synthetic inventory allocation that reserves a declared quantity of one synthetic SKU for one synthetic order; its pre-reserved remedy reverses only the allocation bound to the action.
node ./bin/aas.mjs demo --domain inventory --fault duplicate --prove railBoth domains keep their own policy fixture and their own remedy scope field
(max_amount_minor for refunds, max_quantity for allocations), so neither
is disguised as the other. The GUI exposes the same choice with a Domain
selector. Everything remains synthetic: no warehouse,
merchant, payment, or external provider integration is involved, and a
recorded review proves the handoff rather than any real-world reversibility.
The local GUI includes four explicit synthetic scenario presets: clean settlement, policy refusal, duplicate compensation with same-case review, and settled-action review. Apply a preset, select refund or inventory, then press Run stack. Applying a preset never starts work. Domain identity is persisted in new reports; older bundles display an unknown domain instead of guessing.
Load recent case history to search run identity, policy, domain, or review metadata and filter settled or compensated outcomes. Filters affect the displayed list; comparison selectors retain all loaded cases. History is bounded and may omit older or unreadable cases. Inspect left case loads a saved report and its bindings in a separate panel and enables an identity-checked download. Inspection does not verify source truth. Use imported replay to re-verify synthetic evidence.
GUI runs, Python discovery, and replay verification execute in a worker thread, so synchronous component commands leave HTTP health checks and admission responsive. Existing child timeouts and output caps remain enforced. The server retains admission until the worker exits, including after a client disconnect. Only one run or replay upload is admitted at a time per GUI server. Busy callers receive HTTP 503 with Retry-After; retry after the current operation finishes. Ambiguous duplicate run or comparison options are rejected. File changes clear stale replay results, and oversized imports are rejected before browser file reads as well as at the server boundary. No preset, inspection, or history workflow performs real account operations.
Saved cases now retain the exact requested response, fault, domain, proof mode, and dispute flag, including refused runs. After inspection, Use saved settings prepares those controls without starting work. Older cases without complete valid settings cannot be restored.
Saved exports fail closed when the directory, manifest, or report identities disagree, the schema is unsupported, or persisted JSON exceeds the one-megabyte import budget. Linked case directories/files are rejected. These structural checks do not replace receipt verification.
History is now cursor-paged: aas cases --limit 25 --before <run-id> --json and GET /api/history?limit=25&before=<run-id> return next_cursor, scanned count, and unavailable case identities. A damaged entry does not prevent reaching older cases. At most 50 candidate cases are opened per page; GUI history work runs outside the HTTP event loop.
In the workbench, Load older cases appends the next page while keeping existing search and comparison selections. Each page reports unreadable entries and whether more remain. Up to 250 summaries can be loaded at once; refresh starts again from the newest page. Only one history worker runs per server at a time.
Inspect by ID reaches a saved case outside loaded history. A successful inspection provides a local fragment bookmark. Opening that bookmark pre-fills the ID only; it does not execute a run or verification. Missing and structurally invalid cases produce distinct HTTP 404 and 422 responses.
Verify saved case reopens the selected persisted case on the server and re-verifies its evidence in a worker. It uses the same origin and admission checks as other verification requests, never invokes action execution, and displays the saved-case result separately from imported evidence.
Saved inspection includes expandable decide, act, and prove artifacts with persisted status, reason, diagnostic code, and captured stderr where available. Skipped or absent artifacts are labeled explicitly. JSON and diagnostics remain escaped text, and artifact inspection is separate from verification.
Download case review creates a Markdown handoff with case identity, stage records, component revisions, recorded and recomputed evidence digests, and explicit verification limits. It summarizes persisted data and does not certify receipts, source truth, legal effect, or real-world execution.
For offline handoffs, aas inspect <run-id> --root <output-directory> --markdown prints the same readable case review; --json emits its versioned machine-readable model. The default root is this checkout’s .out and the default format is Markdown. This command only reads saved files, needs no component bootstrap, and returns exit 2 for invalid arguments or exit 1 for unreadable cases.
After comparing two cases, Download comparison review exports the selected metadata differences and unavailable-evidence notices as Markdown. Selection changes invalidate the download link. The report explicitly states that matching metadata is not evidence equivalence and differences do not establish causation.
The export budget counts the formatted JSON bytes actually downloaded, including indentation and the trailing newline, so a permitted export fits the replay upload limit.
Saved-file reads use nonblocking descriptors and validate regular-file type and size before reading, so a named pipe cannot hold the reader open. Invalid UTF-8 is rejected rather than silently replacing evidence bytes.
History summaries and run listings use the same bounded saved-file reader, including when a malformed entry is skipped as unavailable.
Replay checks actual UTF-8 bytes for both files and pipes, stops oversized streams as soon as the budget is crossed, and rejects nonregular files, invalid encoding, and malformed JSON without echoing document contents.
Exporting with --out creates a new handoff atomically and refuses to replace an existing file. Pass --overwrite with --out to explicitly replace it. --json supports machine-readable export success and error output.
All saved-case commands (runs, cases, compare, inspect, export, and prune) accept --root output-directory. Use a copied case store without moving it into the checkout or bootstrapping components; paths with spaces are supported when quoted. Prune still requires an explicit --keep and supports --dry-run.
Human-readable cases output includes scanned count, each unavailable case ID, and next_cursor. An entirely damaged page still gives its continuation cursor, so older readable cases remain reachable. Continue with --before and the same root/page options.
Use aas compare left-id right-id --root output-directory --markdown to print the GUI comparison handoff from the terminal. All formats return exit 1 when cases cannot be compared; JSON also sets ok to false. Comparable differences remain exit 0 and never establish causation.
Use aas verify run-id --root output-directory --json to verify a saved case directly. It performs the same receipt and review checks as imported replay, with the same pinned component requirements, and never executes an action or modifies the case store. Refused or simulation-only cases without same-case evidence return unavailable and exit 1.
Use aas latest --root output-directory to print the ID named by the latest complete-bundle pointer, or add --json for machine output. It validates the referenced saved bundle and fails closed for missing, inconsistent, or unreadable pointers/cases. It never guesses by sorting directory names.
Case-review Markdown now carries all five requested settings and a policy-failure summary with rule identity, path, kind, and reason code. It includes at most 50 failed rules with bounded text fields and an explicit omitted count; raw response values remain in the original artifact only. Missing rule records never imply a policy pass.
Case reviews classify verification readiness as unavailable, conflicting, or ready and explain the next read-only step. Ready means that recorded action identity and recomputed digest agree; it does not mean receipts are verified. Missing rail evidence, missing reviews, and conflicting bindings receive separate recovery guidance.
Terminal case history supports --domain refund|inventory|unknown, --outcome settled|compensated|none, and --search text (case-insensitive, 1 to 200 characters). Filters combine and inspect summary metadata only. They apply within each bounded page: an empty filtered page can still have next_cursor, and callers must keep the same filters when continuing.