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.