Skip to content

Repository files navigation

Agent Action Stack

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.

Reference workflow from policy evaluation through recourse-gated action and outcome verification, with an optional dispute evidence simulation. Policy failure stops execution.

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.

Libraries used

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

Requirements

  • 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 into deps/)

Private repositories are never cloned or modified.

Quick start

npm run bootstrap
npm run demo

Expected 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:fail

Force the dispute path via a compensated rail outcome:

npm run demo:dispute

Expected flow line:

flow: decide -> act -> prove

Review the same case instead of simulating one:

node ./bin/aas.mjs demo --fault duplicate --prove rail

The 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 --json

Export 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.json

Replay 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.

Reproducibility and run bundles

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 provenance
  • report.json: user-facing run report
  • stages/*.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.

Guided local GUI

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.

Tests

npm test
npm run check

npm 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:browser

They 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

  • fixtures/policy.json: refund gate: accept, low/moderate risk, recourse required, not blocked
  • fixtures/response.pass.json: passes the gate
  • fixtures/response.fail.json: fails the gate; act and prove are skipped

Design bounds

  • Orchestration only. Behavior lives in the three libraries.
  • Synthetic connectors and scenarios only.
  • 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.

License

Apache-2.0

Synthetic action domains

--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 rail

Both 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.

Guided case workbench

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.

About

Decide with Constitutional Agent Testbench, act with Consequence Rail, prove disputes with MandateBound.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages