Skip to content
TAIPANBOXPublic

About

Run the open TAIPANBOX agent-governance stack locally, natively, no Docker, and open its money-plane dashboard in your browser.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

stack-up

Run the open TAIPANBOX agent-governance stack on your own machine, natively, with no Docker, and look at it in a browser. The stack's home on the web is it-rat.com.

One command builds (or reuses) the service binaries from source, starts them on a fixed loopback port map, waits for each to report healthy, and names a one-click link to the money-plane dashboard. Press Ctrl-C and it stops cleanly, with no orphaned processes.

Seven processes on a fixed loopback port map, every one bound to 127.0.0.1 only, with heraldyx pinned to file mode so it writes what it would have mailed and opens no socket. Nothing leaves the machine, which is what makes this a sandbox rather than a deployment
git clone https://github.com/TAIPANBOX/stack-up
cd stack-up
./up.sh

Then open the dashboard link the summary prints (http://127.0.0.1:3000/?base=http://127.0.0.1:8080). The key is not in that link. The summary names the file that holds the key-bearing link, ~/.stack-up/dashboard-link, and the one line that opens it. The key is minted fresh on every run and held in ~/.stack-up/cloud-key (mode 0600). It is never printed, so it stays out of your terminal history and out of the dashboard's server log (the key rides in the link's #key= fragment, which a browser never sends to a server).

What it starts

Everything binds to 127.0.0.1 only.

Service Port What it is
tokenfuse-gateway 4100 Budget-enforcement proxy for the Anthropic Messages API (/v1/messages): point an agent's base URL here and every call is metered, and an over-budget one gets a hard 402.
tokenfuse-cloud 8080 The money-plane control API (runs, budgets, savings, incidents). Started with one key minted for this run (see below).
dashboard 3000 The money-plane dashboard, a static page served locally. This is the thing you actually look at.
wardryx 8090 Policy decision point, seeded with a tiny demo policy scoped to fire-drill identities only. Its approvals are signed by keyryx (below).
keyryx (custody) 4340 Holds the key wardryx signs approvals with, and publishes the keys a verifier needs. Started with the policy plane or the record plane; see "Keyryx" below.
keyryx (witness) 4341 Reads the event streams from its own process and records what it saw. It writes nothing to the streams; its findings go to their own file, which heraldyx reads.
idryx 8081 Identity/access graph, built from the event stream. Its own default :8080 collides with cloud, so stack-up runs it on :8081.
heraldyx none The notifier. Reads the same event stream and decides which events are worth writing to a human about. Here it is pinned to file mode: it writes what it WOULD mail to ~/.stack-up/mail.txt and opens no socket. --no-notify skips it.
scopyx 4300 Governed web egress. Agents fetch through it, and wardryx decides every destination before anything leaves. ./up.sh makes one call through it, to the cloud metadata address, which is refused on its address before a packet leaves the machine: read the refusal in ~/.stack-up/events/scopyx.ndjson and the alert it raised in ~/.stack-up/mail.txt. --no-egress skips it.
vouchryx 4310 The delegation-token service, only with --with-delegation. Issues RFC 8693 tokens bound to a key the caller proved it holds, and the revocation list the gateway polls. Without the flag the gateway's delegation door stays shut, and a chain reaching the policy plane is one the CALLER asserted.
costcrew 8321 The FinOps console, only with --with-finops. Cloud and AI spend, an agent crew that triages it, and a person who reviews what they wrote. A guest producer: it writes the shared bus and calls nobody, and it enforces nothing.
typryx 4320 Typed answers with a probability, only with --with-typed. A choice, a score, or a yes/no, each with a probability, instead of a sentence a policy cannot threshold. Runs with the free, deterministic stub backend unless you choose a data mode (--typed-mode, see "Typed answers: choose where your data goes"); without the flag the stack behaves exactly as before.
tokenfuse-mcp-broker 4200 tokenfuse's own MCP credential broker, also with --with-typed, fronting typryx: an agent authenticates to this one door instead of typryx's own. tokenfuse's code is unchanged; this is configuration only.
typryx wardryx-proxy 4330 typryx's reverse proxy in front of wardryx, only with --typed-risk-signal (and only the broker is pointed at it): it adds typryx's risk answer for a brokered tool call to wardryx's decision request as a signal. See "The typed risk signal".

The money plane (gateway + cloud + dashboard) is mandatory; the rest degrade gracefully. If a toolchain or a port is missing, stack-up says so and brings up what it can, rather than failing the whole run. wardryx is the exception once the gateway is wired to it: the gateway refuses every decision it cannot get from wardryx (TOKENFUSE_WARDRYX_FAILMODE=closed), so a wardryx that does not come up stops the run with exit status 1, and the processes it had started are stopped.

The gateway's declassify key

The gateway's POST /v1/fuse/declassify lifts a run's taint label (the release valve for its agent firewall: a person reviews a run and the label comes off). It is not behind the money plane's key. It has a key of its own, x-fuse-declassify-key, and the gateway treats it as optional, so with none set anything that reaches the gateway port could clear a run. up.sh therefore mints one fresh on every run and hands it to the gateway through its process environment only, never as an argument and never in a file; the closing summary prints it once, beside the other per-run keys, and clearing a run needs it. A restart mints a new one. Nothing in the stack calls the endpoint.

The gateway's run-budget ceiling

A run's budget comes from the header the agent sends (x-fuse-budget-usd), from a policy default, or from tokenfuse's built-in USD 5, and the next call of an open run can widen it. With nothing to stop that, the per-run limit of a gateway with no client keys, no identity map and no unit caps is whatever the caller says. tokenfuse v1.5.0 adds the operator's ceiling, and up.sh sets it on every gateway start: 5.00 USD per run by default, --run-budget-ceiling <usd> to change it (a positive number with at most six decimals; anything else exits with status 2 while the arguments are still being read, before anything is built).

./up.sh                                  # ceiling 5.00
./up.sh --run-budget-ceiling 1.50
./up.sh --plan --run-budget-ceiling 1.50 # prints the figure and exits, starting nothing

5.00 is tokenfuse's own default run budget, so an ordinary run is unchanged and only a caller-declared larger budget is clamped. A clamped call's answer carries x-fuse-budget-clamped: <ceiling>. Measured 2026-10-04 on this launcher (pull request #41 records the run), a stack brought up with the real up.sh over a tokenfuse main build: a call declaring x-fuse-budget-usd: 50 came back with x-fuse-budget-clamped: 5.00, a call declaring 1.00 carried no such header, and widening the same open run to 500 was clamped to 5.00 again.

What it does not do: it is one figure per run, not per agent (an agent that opens a new run id gets a new ceiling's worth), it does not clamp a budget that comes from the Cloud (that is the operator's own word), and it is not applied to the MCP broker, which holds no run budget. A gateway built before v1.5.0 ignores the setting without saying so, so up.sh checks that the gateway binary names it: refused when you asked for a figure, a warning when you did not.

Delegation, and what leaving it off actually means

./up.sh --with-delegation also starts vouchryx, mints a demo issuer into ~/.stack-up/delegation, and opens the gateway's delegation door against it. The gateway then says so at startup:

delegation door: ON, chains reaching the PDP are verified here
revocation list: polling url=http://127.0.0.1:4310/v1/revocations
                 interval_ms=1000 fail_mode=Closed

Since vouchryx's own on-disk revocation store landed (2026-09-23), a revocation also survives THIS launcher's own restart, which it did not before. up.sh mints a bearer key once, at ~/.stack-up/delegation/revoke.key (0600, umask 077, reused on every later run, never printed to a log), and points vouchryx at its own revocation file, ~/.stack-up/delegation/revocations.ndjson. Neither file sits under a path ./down.sh touches, so ./down.sh then ./up.sh --with-delegation again keeps an earlier revocation in force rather than forgetting it:

D=~/.stack-up/delegation
curl -X POST http://127.0.0.1:4310/v1/revoke \
  -H "Authorization: Bearer $(cat "$D/revoke.key")" \
  -d '{"subject":"agent://stack-up.local/demo","actor":"you","reason":"testing"}'
./down.sh && ./up.sh --with-delegation
curl http://127.0.0.1:4310/v1/revocations   # still lists the subject above

Measured 2026-09-24 on this launcher (pull request #31 records the run): the subject above stayed listed after a full ./down.sh / ./up.sh cycle, revoke.key's sha256 and its 0600 mode were unchanged before and after, and running ./up.sh again while the stack was still up (no down.sh first) refused cleanly on the busy gateway port without touching the key.

The console can revoke now too, not only curl. genaryx's delegation_revoke command (admin-only, passkey-gated) posts the same POST /v1/revoke this section's curl example does, if genaryx-web is started with GENARYX_VOUCHRYX_URL and GENARYX_VOUCHRYX_REVOKE_KEY_FILE pointing at this run's vouchryx and revoke key. up.sh --with-delegation prints the exact pair to export, in its closing summary, alongside the curl line above; it never starts genaryx-web itself.

The whole loop below, measured on 2026-08-27 against this launcher (pull request #18 records the run), using the client vouchryx ships:

D=~/.stack-up/delegation; B=~/.taipan/bin
TOK=$("$B/vouchryx-demo" exchange -url http://127.0.0.1:4310 \
  -idp-key "$D/idp.pem" -kid stack-up-idp \
  -iss https://idp.stack-up.local -aud http://127.0.0.1:4310 \
  -subject user://acme/ada -actor agent://acme/triage -holder-key "$D/holder.pem")
step result
a call carrying that token and a fresh proof HTTP 200
POST :4310/v1/revoke for user://acme/ada, bearer $(cat ~/.stack-up/delegation/revoke.key) {"revoked":true}
the same token, after one poll HTTP 401, reason=BadToken in the gateway log
the same revoked token, on a launcher started WITHOUT the flag HTTP 200

That last row is what "optional" costs. With the door shut the token is never examined, so a credential somebody has explicitly ended is simply not looked at, and x-fuse-on-behalf-of is whatever the caller typed.

The demo issuer is a sandbox, not a way to run this. A real deployment's subject and actor tokens come from an identity provider somebody else operates; this mints one because a laptop does not have one, and its private key sits in ~/.stack-up/delegation by design.

Two things the loop needs that are easy to miss. The gateway wants x-fuse-agent-id even when a proven chain already names the agent, because the chain fills the RECORD's subject and not the identity that policy and billing run on. And a DPoP proof is bound to one method and one URL, so a fresh one is needed per request.

FinOps, and the one wiring that is easy to get wrong

./up.sh --with-finops also starts costcrew and points it at this launcher's bus. Two flags carry the whole integration and both are easy to get subtly wrong, so the launcher sets them rather than leaving them to a reader:

The file name is the integration. genaryx tails a directory and keys each source's read offset off the file stem, so the stream has to be called costcrew.ndjson and nothing else. Empty is the console's own default, which means a console started by hand looks perfectly healthy and puts nothing anywhere.

The trust domain has to be the one the record plane is given. The seal accepts an event only if its agent id begins agent://<domain>/, matched against exactly one value. A console minting under its own default (costcrew.local) would have every line it emits refused by the seal and counted as foreign, which reads as a calm night rather than as a misconfiguration. So it is given $STACK_UP_TRUST_DOMAIN, the same value the seal gets, from one place.

The console enforces nothing. It has no gateway, no interceptor and no refusal path, and every budget_threshold it emits carries enforced: false, stamped by the translation rather than by the caller. tools/enforce in that repository is what pushes a budget to TokenFuse, it is a separate binary, and this launcher does not start it.

Typed answers, and what stub means

./up.sh --with-typed also starts typryx: a small service that turns a yes/no or which-one question into a choice, score, or noul answer, each with a probability, instead of a sentence a policy cannot threshold. Absent the flag, the stack behaves exactly as it did before typryx existed.

On its own it runs with TYPRYX_BACKEND=stub: free, deterministic, and makes no outbound call, so this launcher never chooses a paid or external backend for anyone. To use a real backend, choose a data mode with --typed-mode; the next section says what each one sends where. (An operator who exports TYPRYX_BACKEND and its variables by hand before ./up.sh --with-typed still gets exactly that, as before; the explicit flags are the checked way.)

Typed answers: choose where your data goes

typryx needs something to ask. There are three choices, and this launcher never makes one for you: with no --typed-mode nothing starts, and --with-typed alone is the stub above.

Mode How What leaves the machine
jev ./up.sh --typed-mode jev --typed-key-file ~/keys/jev.key The named fields of each question, and only those (typryx's question template is the allowlist), go to TypeSafe AI's hosted typed-decision API. Nothing else does.
own-model ./up.sh --typed-mode own-model --typed-model-url http://127.0.0.1:11434/v1 --typed-model qwen2.5:7b Nothing leaves your hardware, if the server you name is yours. Questions go only to that one URL, which must be an OpenAI-compatible server (Ollama, vLLM, and similar) and end in /v1. --typed-key-file is optional here, for a server that wants a bearer key.
off ./up.sh --typed-mode off (or no flag at all) typryx is not started.

The key is a file, never a value: --typed-key-file takes a path, and there is no environment variable or argument that carries the key itself. This launcher checks that the file exists and is not blank, then hands typryx the path; typryx reads the file. The launcher never reads the key's bytes, and the only thing it ever prints is the path. A bad choice (a missing or blank key file, a model URL that does not end in /v1, --typed-mode off together with --with-typed, a key file with no mode) exits with status 2 while the arguments are still being read, before anything is built or started. Under a chosen mode, typryx's other backend variables are removed from its environment, so a stale TYPRYX_JEV_URL left in your shell cannot redirect the key.

To see the resolved choice without starting anything (it is also the quickest way to check what would leave the machine):

./up.sh --typed-mode own-model --typed-model-url http://127.0.0.1:11434/v1 \
        --typed-model qwen2.5:7b --typed-plan
# typed: mode: own-model
# typed: backend: openai-logprobs
# typed: leaves this machine: questions go only to the model server you named, ...
# typed: env: TYPRYX_OPENAI_URL=http://127.0.0.1:11434/v1

What the two real modes measured. One run each on 2026-09-30, on a frozen 434-question test. Accuracy is how often the top answer was right; ECE (expected calibration error, lower is better) is how far the stated probabilities sit from how often they turned out true; latency is the median per question.

Accuracy ECE Median latency
jev (hosted) 87.1% 0.042 229 ms
own-model, qwen2.5:7b, no tuning, on an 8-vCPU CPU-only VM 70.0% 0.273 2130 ms
no typed answer (a constant default answer) 25.1% not applicable not applicable

A model slower than typryx's own default of 2 seconds per question needs TYPRYX_TIMEOUT_MS exported before ./up.sh; the launcher passes it through unchanged. On 2026-09-30, typryx launched the way up.sh launches it answered a real typed question from a local Ollama serving qwen2.5:7b (pull request #38 records that run). The two timeout results are not kept in any pull request: with the default, one ask came back unanswered with reason timeout; with TYPRYX_TIMEOUT_MS=60000 the same ask was answered in 1.8 seconds.

Read the middle row as a starting point, not a ceiling: it is a general 7-billion-parameter model nobody has tuned, on a machine with no GPU. It already beats a constant answer by a wide margin, and its probabilities are the weak part (an ECE of 0.273 means its stated confidence sits far from how often it is right). These are one test set and one run each; your questions will score differently.

Your own model, on your own data. We do not fine-tune or ship models for customers. If you choose own-model, you can fine-tune and calibrate that model on your own data, and typryx gives you what you need to do it: the ledger this launcher keeps under ~/.stack-up/typryx/ledger (every answer, and the truth you later record against it), typryx calibration, which scores the ledger (Brier score and ECE) so a tuned model's probabilities can be checked against what happened, and an opt-in local training log.

The training log is off unless you switch it on, with --typed-training. It needs typryx v0.3.0 or newer (the launcher refuses an older one rather than start a typryx that would quietly keep no log) and needs typryx to run, so pair it with --with-typed or with --typed-mode jev or own-model:

./up.sh --typed-mode own-model --typed-model-url http://127.0.0.1:11434/v1 \
        --typed-model qwen2.5:7b --typed-training

This sets typryx's TYPRYX_TRAINING_DIR to ~/.stack-up/typryx/training, a directory the launcher creates private (mode 0700, tightened if it already existed looser) beside the ledger. --typed-plan prints both paths and the export command without creating anything. Each answered question that has a template adds one line holding the fields the template lets through to the model and nothing else: no backend answer, no probabilities. The log starts when you switch it on, and typryx never rotates or deletes it. It stays on this machine: nothing in this launcher reads it or sends it anywhere.

Then the loop is: ask, record the truth a person decided, export, fine-tune with your own tooling, serve the tuned model behind the same kind of server and compare it with typryx calibration.

# record the truth a person decided for one answer (the answer_id is in the
# ask's reply; the truth is a choice name, a score index, or true/false)
curl -s -X POST http://127.0.0.1:4320/v1/outcome -H "X-Typryx-Key: <key>" \
  -H "Content-Type: application/json" \
  -d '{"answer_id":"<id>","truth":true,"source":"human"}'

# pair each logged question with the truth a person posted for it
# (the binary is ~/.taipan/bin/typryx)
typryx export --training \
  --training-dir ~/.stack-up/typryx/training \
  --ledger ~/.stack-up/typryx/ledger \
  --out train.jsonl

The export carries human truths only: every row is template, template_version, type, state and label, where label is the truth a person posted, never a backend's answer. An answer with no truth is skipped and counted, however confident the model was. That matters for jev: a hosted provider's terms may forbid using its output to train another model, so a Jev answer never becomes a label here. typryx cannot see how a truth was produced, so post labels you decided yourself, not ones copied from a model. The log holds your questions, which can be sensitive: where that disk is backed up, and for how long, is yours to decide. @decided 2026-09-30: the three modes, the choice being the operator's, the log being opt-in and off by default, and training being the customer's own work on their own data are settled.

The door key is generated fresh on every run, held only in this process's environment, and never written to a file, the same posture as scopyx's own key just above it. The closing summary prints it once, together with a ready-to-run curl line, so it is only ever useful for that one run:

./up.sh --with-typed
# ... prints something like:
#   typryx:  http://127.0.0.1:4320  (key: <40 random chars>, stub backend, minted fresh this run)
#   curl -s -X POST http://127.0.0.1:4320/v1/ask -H "X-Typryx-Key: <key>" ...

typryx's journal now lives at ~/.stack-up/events/typryx.ndjson, on the shared bus trailryx-seal reads, and its ledger stays on its own path, ~/.stack-up/typryx/ledger. @decided 2026-09-26: agent-passport SPEC 6.2 (agent-passport#67) registers typryx's four event types (typed_answer, typed_unanswered, typed_refused, calibration_drift) in the estate's own event schema, so the earlier reason for keeping this journal off the bus (an unregistered source trailryx-seal's *.ndjson import would pick up blind) no longer holds; the same reasoning still keeps vouchryx's revocation store and costcrew's own data directory off paths other planes read as their own.

Registered is not the same as sealed. typryx emits schema agent-event/v1.0, which trailryx-agentevent does not accept yet (it reads v0.1 through v0.3), so trailryx-seal reads this file and refuses every line in it for unknown_schema, before the event type is ever inspected - measured 2026-09-26 against this launcher's own run (pull request #36). The practical outcome is the one roughly forty other unmapped or unrecognized event types already get on this bus (counted, not sealed; see "The record plane" below). Moving the journal onto the bus stops it being an unregistered source; it does not make trailryx seal it. heraldyx reads the whole events directory regardless of what trailryx maps, so a typed_unanswered, typed_refused, or calibration_drift line now reaches ~/.stack-up/mail.txt the same way any other medium-or-above event does. That is expected, not a new leak: the bus is exactly what heraldyx has always watched.

Connecting to typryx through tokenfuse's own MCP broker

--with-typed also starts tokenfuse's mcp-broker subcommand (tokenfuse-mcp-broker above), fronting typryx: an agent authenticates to the broker's one door instead of typryx's own, the same door the rest of the estate already uses for MCP tool calls. tokenfuse's code is not changed for this: it runs exactly as it does without typryx, and typryx joins it purely through the broker's own documented environment contract (TOKENFUSE_MCP_UPSTREAM(S), TOKENFUSE_MCP_KEYS, TOKENFUSE_MCP_SECRETS, TOKENFUSE_MCP_SECRET_SCOPES).

A brokered tools/call reaches typryx with only a content-type header: no credential, no agent identity of its own. So the credential travels as a resolved secret handle instead: this launcher hands the broker typryx's per-run key under typryx_key, scoped to typryx's three tools, and a caller carries only {{secret:typryx_key}} at params._meta["typryx/key"]. typryx reads a credential from that place because this launcher sets TYPRYX_ACCEPT_KEY_IN_META=1 (off in typryx by default); see typryx's own README, "Connect it", for the full contract.

The broker's own client key is minted fresh per run, the same posture as typryx's and scopyx's: a loopback bind does not require one, and this launcher mints one anyway so the printed example shows the real shape a real deployment would need.

./up.sh --with-typed
# ... prints something like:
#   broker:  http://127.0.0.1:4200/mcp  (key: <40 random chars>, minted fresh this run, fronts typryx as upstream "typryx")
#   curl -s -X POST http://127.0.0.1:4200/mcp -H "x-fuse-key: <key>" -H "X-Fuse-Mcp-Upstream: typryx" -H "x-fuse-agent-id: agent://demo.local/mcp-broker-demo" ...

The broker keeps its own event file, ~/.stack-up/events/tokenfuse-mcp.ndjson, never the gateway's: two independent process invocations writing one file would interleave two writers' lines with nothing in the record to tell them apart. A call needs an x-fuse-agent-id header (or an XAA/client-cert identity, neither of which this sandbox has) to actually be recorded there: agent-passport SPEC 6.1 forbids a fabricated agent id, so a brokered call that names nobody is served exactly the same and its tool_call record is silently skipped (tokenfuse crates/gateway/src/mcpbroker.rs, emit_tool_call); measured 2026-09-26 (pull request #36), the same call without that header answered identically and left the file empty. The closing summary's own example carries the header for this reason.

Recorded is still not the same as sealed. tool_call is a type trailryx recognizes, unlike typryx's four, but the broker's own event carries no run_id (it has no run/budget/step state to put one in, the same reason its Wardryx context above sends cost, steps, and model empty), and trailryx's mapper refuses any line with none. Measured 2026-09-26 (pull request #36): a tool_call line here is refused for no_run_id, not unknown_schema or unknown_type - counted, not sealed, the same outcome typryx's journal gets for a different reason.

The typed risk signal

--typed-risk-signal lets typryx's answer to "how risky is this tool call" reach wardryx, so a policy can hold a call for a person. It is off by default, and it is refused (status 2, before anything is built) unless typryx is going to run (--with-typed, or --typed-mode jev or own-model) and wardryx is (not --only money):

./up.sh --with-typed --typed-risk-signal --plan   # what would start and what would leave
./up.sh --with-typed --typed-risk-signal

It starts typryx wardryx-proxy (typryx v0.4.0) on 127.0.0.1:4330 in front of wardryx and points only the MCP broker at it. For a brokered tools/call the broker (tokenfuse v1.5.0) now sends wardryx the call's tool name, arguments and target; the proxy asks typryx's action.risk_class template about exactly those three things and adds the answer to the request as a signal; wardryx v1.2.0 reads it through a hold_if_signal rule, which can turn an allow into a hold and nothing else. The LLM gateway keeps talking to wardryx directly: on the model path there is no pending tool call to classify, and a hosted backend's latency would push most decisions past the gateway's own timeout. Because the broker's policy gate is off unless both its mode and its URL are set, up.sh gives the broker TOKENFUSE_WARDRYX_MODE=enforce (the gateway's own mode here) along with the proxy's URL.

What leaves the machine is the same backend you chose, for every brokered call. With the stub, nothing (and its probabilities carry no information). Under --typed-mode jev, the tool name, arguments and target of every brokered tool call go to TypeSafe AI, one ask per call. Under own-model, they go only to the server you named. --typed-plan prints this before anything starts. Each call costs one ask, capped by the proxy's own TYPRYX_MAX_CALLS_PER_HOUR (typryx's default is 1000, kept separately from the service's).

The launcher seeds one hold. With --typed-risk-signal, wardryx gets the rule stack-up-typed-risk-hold: a brokered call whose risk class is destructive, external_send or financial at 0.8 or more is held for a person, and so is a brokered call the signal never reaches (when_absent: hold, invariant 28 in CLAUDE.md). An approval lifts one held call; single-use stays on. Without the flag nothing is seeded.

To write a rule of your own, this is wardryx's example (its README, "Typed risk signals"), pointed at the broker's demo agent:

curl -X PUT http://127.0.0.1:8090/v1/policies/risk-hold \
  -H "Authorization: Bearer $(cat ~/.stack-up/wardryx-admin.key)" -H "Content-Type: application/json" \
  -d '{"target":"agent://demo.local/*","hold_if_signal":{"name":"action.risk_class","values":["destructive","external_send","financial"],"min_probability":0.8}}'

Policies written this way live in wardryx's memory for the run (the seeded demo policy file is rewritten on every ./up.sh). Measured 2026-10-04 on this launcher (pull request #41 records the run) with the stub backend: a brokered call with no policy was allowed, and the decision event carried the call's name and target and an action.risk_class signal from typryx; after a test policy holding the stub's own answer (read_only at 0.3 and above, not the example above, because the stub's answers mean nothing) the same call was refused with "requires approval" and a wardryx approval id, and a call through the gateway still answered 200.

Two deadlines matter. The proxy asks typryx's backend before it forwards the decision, and typryx's own default of 150 ms for that ask drops almost every real answer (a hosted Jev answer takes about 229 ms at the median, a qwen2.5:7b own-model about 2130 ms on 8 vCPU), after which the call goes to wardryx with no signal and a hold_if_signal rule never fires, silently. So up.sh gives the proxy TYPRYX_PROXY_ASK_TIMEOUT_MS=3000 and the broker TOKENFUSE_MCP_WARDRYX_TIMEOUT_MS=7000 (the variable tokenfuse reads for a decision that carries a tool call), the same figures stack-single uses. The LLM gateway's own wardryx timeout is unchanged. A local model slower than typryx's own 2-second backend deadline still needs TYPRYX_TIMEOUT_MS exported before ./up.sh.

The proxy runs with no journal, no ledger, no keys and no training log, deliberately. The typryx service's journal is one hash chain with one writer, and a second process appending to it would break the chain the on-box verifier checks; the training log would copy every call's arguments. The answer's id travels in the signal, so wardryx's own decision record names it. "Only the broker can reach it" is as strong as a loopback bind is on one machine: any local process can, and it carries no key because the broker cannot add an X-Typryx-Key header (typryx refuses a wide bind without one, so widening the bind is refused too).

What it installs but does not start

Five of the stack's tools are not servers. qryx scans a path and exits. mockryx fires crafted requests at a gateway you name and exits. engram is a library, a CLI, and a stdio-only MCP server over a local file. verdryx is a CLI over a local file. agent-conform is the on-box chain verifier: run by the chain-verify routine below, it checks the hash chain of every stream in the events directory. There is no port to connect to and nothing to keep running, so for these "up" means something different:

Tool Installed as Store
qryx ~/.taipan/bin/qryx none, scans on demand
mockryx ~/.taipan/bin/mockryx none, runs on demand
engram ~/.taipan/bin/engram-mcp ~/.taipan/engram.engram
verdryx ~/.taipan/bin/verdryx ~/.taipan/verdryx.db
agent-conform ~/.taipan/bin/agent-conform none; its state is ~/.stack-up/routines/agent-conform.state.json, its output ~/.stack-up/events/agent-conform.ndjson

~/.taipan rather than ~/.stack-up because that is where the rest of the stack looks. It is a fixed path, not a search: qryx in particular is looked up at exactly ~/.taipan/bin/qryx, with no environment variable and no PATH fallback, so a binary installed anywhere else is a binary nothing can find.

The two stores are created empty and are never seeded. They are files on disk that other tools read as real data, so demo rows in them would be indistinguishable from your own, forever. The money plane's demo dataset is different and stays on by default: it lives in a process that keeps it in memory and forgets it on exit.

stack-up is not the only writer of ~/.taipan/bin - the taipan deploy CLI installs the same binaries there. It records a checksum of everything it installs, so on a later run it refreshes its own files and leaves anyone else's alone, saying so. --force-install overrides that.

Skip this whole section with --no-tools.

Requirements

  • git and curl.
  • Rust (stable, via rustup) - tokenfuse is built from source, and so is trailryx (the record plane, below). If that build fails for any reason, stack-up skips just that plane and everything else still comes up.
  • Node and npm - only for the dashboard (a one-time static build).
  • python3 - to serve the dashboard, and to install engram and verdryx into their own virtualenvs (3.11+ for those two).
  • Go - for wardryx, idryx, heraldyx, scopyx, mockryx, qryx and agent-conform, plus the opt-in vouchryx (--with-delegation), costcrew (--with-finops) and typryx (--with-typed). Skip them with --only money. qryx pins a newer Go toolchain than the others and downloads it on the first build; that is automatic, and slow exactly once.

The first run builds tokenfuse in release mode and can take several minutes. After that, builds are cached and startup is seconds. Everything after the initial build and clone works offline.

Every component is built from its pinned release tag, at the commit that tag names. The pins are one table at the top of up.sh (pin_of), and each clone is refused unless its commit is the one the table names. Clones go into ~/.stack-up/repos/. A sibling checkout next to stack-up is not used by default, because a branch there can hold anything. --from-main builds sibling or main checkouts instead, as this launcher used to, and the run says at the top that its build is not a release.

What you are looking at

The money plane has two independent faces, and stack-up runs both:

  • The gateway (:4100) is the live enforcement proxy. Point an Anthropic Messages API client at it (POST /v1/messages, sending x-fuse-key with the value in ~/.stack-up/gateway-client.key) and every call is metered against a per-run budget, with a hard 402 when it is spent. Its own view is GET :4100/v1/runs, which needs this run's admin key (see "This is a sandbox, not a deployment").
  • The cloud (:8080) is the aggregate control plane, and the dashboard reads from it. Cloud is populated by anything posting call records to its ungated POST /v1/ingest. stack-up points the gateway at cloud, so your live traffic through :4100 shows up on the dashboard on its own; any other reporter feeds the same endpoint.

So the dashboard shows whatever has been ingested into cloud, your own gateway traffic included. So a first look is not empty before you have sent any, stack-up also seeds a short, clearly-labeled demo dataset (two runs under agent://demo.local/*); pass --no-demo to skip it. Cloud runs in-memory here, so that seed is fresh every run and nothing is written to disk.

Demo fleet

--with demo-fleet replaces that short seed with a richer one, for showing the console to someone without wiring up any of their own agents:

./up.sh --with demo-fleet

It seeds a believable single-org fleet: 16 agents across finance, sre, support and data, each with a few runs at realistic per-model cost; semantic- cache hits and model-router downgrades so "Governed savings" shows real cache and router numbers; and two budget breaches, including one runaway agent whose spend climbs run over run until it is caught, so an incident and prevented spend show too.

It is the exact same mechanism as the short seed above, just more of it: clearly-labeled synthetic data (agent://demo.local/*) posted to cloud's /v1/ingest with this run's key. Cloud keeps it in memory, so it is fresh on every run and gone the moment you stop the stack - never written to disk, never presented as real, and off by default. --no-demo skips it exactly like it skips the short seed.

To push your own data, POST call records to cloud with the key the summary names, held in ~/.stack-up/cloud-key (mode 0600):

CLOUD_KEY=$(cat ~/.stack-up/cloud-key)
curl -X POST http://127.0.0.1:8080/v1/ingest \
  -H "authorization: Bearer $CLOUD_KEY" \
  -H 'content-type: application/json' \
  -d '{"records":[{"run_id":"my-run","model":"gpt-4o-mini","cost_microusd":4200,"input_tokens":900,"output_tokens":300,"agent_id":"agent://acme.local/support/bot"}]}'

Refresh the dashboard and your run is there. To exercise the gateway's live enforcement instead, send Anthropic Messages API traffic to :4100 with an x-fuse-key (the value in ~/.stack-up/gateway-client.key), an x-fuse-run-id and an x-fuse-budget-usd, and watch it 402 when the budget runs out (see the tokenfuse README for wiring a real upstream).

The record plane

Every plane above produces events. trailryx is the one that reads them and seals them: a chained, offline-verifiable ledger of what the stack did, built so that nobody can quietly edit a record or drop one from the middle without every seal after it stopping to match. Each sealed root is signed through keyryx by default, and --without-keyryx turns that off (see "Keyryx" below).

After the demo has run, ./up.sh imports every *.ndjson file under ~/.stack-up/events (tokenfuse's, wardryx's, scopyx's - the same directory heraldyx already reads as a whole) into ~/.stack-up/records, mapping what it can and counting by name what it cannot: an event type this reading of the estate's shared envelope does not map yet is unknown_type, and it is counted rather than dropped silently, the same way an unmapped destination is refused rather than let through elsewhere in this launcher. routines.sh's trailryx-seal (see below) runs the identical command on a timer, so the record stays current after the demo: it remembers where it stopped in each file, so a run that finds nothing new costs one file read, not a duplicate.

What it does not do here, because this is a sandbox. No signing key is configured, so a pack has no signature over its root: the offline verifier says so by name (root-signature: no signature, so this pack proves it is self-consistent and not who published it) rather than silently passing. No witness and no timestamp authority are wired either, so nothing independent attests to when the root existed. Neither weakness is hidden: trailryx-verify prints both, plainly, every time. Payload text is never stored at all here - trailryx-node has no key custodian, so it declines payload parts and counts them rather than sealing them under a key it generated and forgets on exit. None of this is a claim of compliance; it is a demonstration of what a real deployment would need to add (a signing key, an independent witness) to close those two gaps, which is exactly what the verifier's own output tells you.

See it in one command, once the stack has come up:

~/.taipan/bin/trailryx-node read --data ~/.stack-up/records --all

or seal a portable, offline-checkable snapshot of it:

~/.taipan/bin/trailryx-node read --data ~/.stack-up/records --all --pack ~/.stack-up/records/trailryx.pack
~/.taipan/bin/trailryx-verify ~/.stack-up/records/trailryx.pack

trailryx-verify reads no network, no configuration and no state: it is the one binary here meant to be run by somebody who does not trust the rest of the stack, and it exits non-zero the moment anything in the pack fails to verify.

--only money skips it, the same way it skips wardryx, idryx, heraldyx and scopyx. If the sibling trailryx checkout or a working Rust toolchain is missing, or the build fails, ./up.sh says so and skips just this plane - the rest of the stack is unaffected, the same way a missing go skips scopyx.

Which file names the readers accept

heraldyx v0.3.0 and idryx v1.1.0 refuse an event whose source is not allowed for the file it was read from: <source>.ndjson carries <source>, and tokenfuse-cloud.ndjson and tokenfuse-mcp.ndjson carry tokenfuse. Every file this launcher configures is one of those (scripts/bus-files-match-sources.sh holds the list):

File under ~/.stack-up/events Written by Source its lines claim
tokenfuse.ndjson the gateway tokenfuse
tokenfuse-mcp.ndjson the MCP broker, --with-typed tokenfuse
wardryx.ndjson wardryx wardryx
scopyx.ndjson scopyx scopyx
vouchryx.ndjson vouchryx, --with-delegation vouchryx
costcrew.ndjson costcrew, --with-finops costcrew
typryx.ndjson typryx, --with-typed typryx
verdryx.ndjson the verdryx-drift routine verdryx
agent-conform.ndjson the chain-verify routine agent-conform

idryx is loaded with --load tokenfuse:tokenfuse.ndjson, in up.sh and in the idryx-detect routine. Measured 2026-10-04 on a real run of up.sh --with-typed --typed-risk-signal --with-delegation --with-finops (pull request #41 records it): seven files were read and six of them had a line. Each of those held only lines claiming the source in its row (vouchryx.ndjson was still empty), and heraldyx v0.3.0 and idryx v1.1.0, run over that directory, printed no refusal and no unknown-stream notice. The verdryx.ndjson row is read from the routine's code and agent-conform.ndjson was run through the chain-verify routine, not by up.sh.

Which trust domain it seals

The record plane accepts an event only if its agent id begins agent://<domain>/, matched against one value. Give it a domain nothing on your box carries and every line is refused, counted, and the run exits 0.

So the domain comes from the deployment. ./up.sh takes it from STACK_UP_TRUST_DOMAIN (default demo.local) and writes it to ~/.stack-up/trust-domain, which is what the scheduled seal reads. A file rather than an exported variable, because neither the systemd unit nor the launchd plist this repo generates passes environment through, so an export reaches a run by hand and never reaches the timer.

STACK_UP_TRUST_DOMAIN=acme.example ./up.sh

The seal refuses before it imports, not after. The plane commits its reading position even for a run that wrote nothing, so one pass under the wrong domain marks every line read and no later correction gets them back: the next pass answers nothing new. The cursor is at byte N of N (40 line(s), 0 record(s) so far). Measured on 2026-08-27 (pull request #19 records the run), which is also the day this launcher was found to have sealed nothing but its own synthetic demo fleet while four real planes wrote to the same directory.

Partial refusal is normal and is not an error. This launcher mints under several domains on purpose: scopyx identifies its demo agent as agent://local.invalid/demo-agent, because RFC 2606 reserves .invalid and a sandbox identity must not be mistakable for a real one in a trail somebody keeps. Only a total refusal stops the run.

The agent firewall

tokenfuse's agent firewall watches what a run reads and then what it asks to do. up.sh runs it in enforce on every gateway start. --firewall enforce|shadow|off changes that, and any other word exits with status 2 while the arguments are still being read, before anything is built.

./up.sh --plan                 # prints the firewall's mode, starting nothing
./up.sh --firewall shadow      # record what enforce would refuse, and refuse nothing

With tokenfuse's starter policy, enforce refuses these calls:

  • exec, write and network egress (run_shell, exec, bash; write_file, db_write, deploy; send_email, http_post, send_message) once the run has read untrusted input: web pages, email, files, anything unclassified, or a tool result that reads like an injected instruction;
  • network egress once the run holds a secret, read through vault_read or read_secret;
  • a tool a request declares whose description reads like an injected instruction counts as untrusted input from that call on.

Unclassified means any tool the starter policy's source map does not name, so an agent that calls any tool of its own and later, in the same run, one of the nine names above is refused. An agent that chains those names on purpose runs under --firewall shadow, or the gateway gets a policy file that classifies its tools. A tool the starter policy does not map to one of those capabilities is not refused by these rules. --firewall shadow writes the same refusals to the event stream and lets the calls through, so you can see what enforce would have done before you choose it. --firewall off turns the firewall off: no taint tracking and no refusals.

The PII masks

tokenfuse can scan what an agent sends for personal data: email addresses, card numbers that pass a Luhn check, and international phone numbers written with a +. up.sh runs the scanner in shadow on every gateway start and on the MCP broker. A hit is noted as a count and the kinds found, never the text: the gateway adds an x-fuse-dlp header to its answer, the broker logs a warning and marks _tokenfuse.dlp_pii on tool responses. Neither writes an event or a trace row, and the call goes through unchanged, so the personal data still reaches the provider. --pii off|shadow|mask|block changes that. Any other word exits with status 2 while the arguments are still being read, before anything is built.

./up.sh --plan          # prints the PII mode, starting nothing
./up.sh --pii mask      # replace each hit with [REDACTED:kind] before the call is forwarded
./up.sh --pii block     # refuse a call that carries a hit (HTTP 403, error type dlp_blocked)
./up.sh --pii off       # no PII scanning at all

tokenfuse itself keeps this scanner off until it is asked for, because its matches are heuristics over ordinary text and their false positives are prose. The launcher runs it in shadow so that a stand keeps a record of what passed through it, and it changes nothing else about a call.

Keyryx: custody of the approval key and of the record's root, and a witness of the record

keyryx runs as two processes, and it is part of every install. Custody holds the keys that sign. It holds the key that signs an approval when a person grants one: wardryx v2.2.0 asks custody for that signature, so an approval can be checked against a published key rather than against a secret wardryx holds. It also holds trailryx-root, the key that signs each sealed root of the record. The witness reads the event streams and the record from its own process, keeps its own log, and writes nothing to the streams. Its findings go to a file of their own, ~/.stack-up/keyryx/alerts/keyryx.ndjson, which is neither the shared bus nor the witness's data directory. heraldyx (v0.8.0, which renders keyryx's findings) reads that file beside the bus and writes what it would mail to ~/.stack-up/mail.txt. This launcher starts no console; the closing summary prints the GENARYX_EVENTS_EXTRA line a Genaryx console you start yourself needs to show the findings as incidents. On any run after the first, the witness reports tokenfuse.ndjson as cut short or rewritten, because up.sh empties that file each time it starts and the witness remembers what it saw; that finding is the launcher's own doing, not a tamper.

keyryx starts whenever a signature has work: with the policy plane (approvals) or with the record plane (sealed records). --only money has neither, so it starts none.

The record is signed through keyryx by default. Each sealed root is signed under trailryx-root, an ES384 key, and the witness reads the records. To turn this off, run with --without-keyryx. The record pack is then unsigned, and nothing witnesses the record. The closing output and --plan say so, because an unsigned pack looks like a signed one to a reader who does not check the signature.

The launcher gives custody and the witness separate data directories and separate root keys, all under ~/.stack-up/keyryx/, and it writes each key to a file that only your user can read. The root keys and the record's credential are minted once and kept, so a root signed in one run still verifies in the next. The wardryx bearer key is minted for each run. No key value is printed, logged, or put on a command line; the summary names no key.

keyryx is built from the commit that the tag v0.1.2 names and nothing else. Its releases are container images, not binaries, and this launcher runs no containers, so the clone is refused unless it is that commit. The pin is one line of the pin table (pin_of) in up.sh; moving it is one change.

The record seal's key is pinned. trailryx v1.5.0 signs a sealed root only under the key its TRAILRYX_KEYRYX_KID names, and it refuses to start without one. Once custody is up, the launcher reads custody's trailryx-root key set and its state, and takes the pin from custody's active key: at install the set must hold exactly one P-384 key, and it must be the active one. The pin is kept in ~/.stack-up/keyryx/trailryx.kid (private, 0600) and kept on every later run while it is still the active key. The kid is the key's RFC 7638 thumbprint, which is public. If a refusal happens, the run says why and its record is not signed.

Rotating the record key, on purpose. Rotate trailryx-root in custody as usual. Custody publishes the new key and makes it active after its publish-ahead time. From then on, a run without the flag refuses the pin and says so, because a root signed under the new key would not match the pin. Run ./up.sh --repin-record-key once to move the pin to the active key. The flag needs keyryx to run in that run; with --without-keyryx it is refused. A key set that still holds the old key beside the new one is accepted by the repin, because the pin takes the active key only.

Upgrading. Nothing to migrate. Earlier versions wiped keyryx's directory on every run and minted new root keys each time. The first run of this version keeps the root keys and the directory the last run wrote, mints the record's credential, and pins the active trailryx-root key. Records sealed before this version stay unsigned; they are not re-signed. A run with --without-keyryx leaves the record unsigned, as before. The trailryx and keyryx pins moved to trailryx v1.5.0 and keyryx v0.1.2 with this change. A keyryx v0.1.2 witness writes its attestations as mode 0640 in 0750 directories; this launcher reads them as the same operator, so nothing changes for it.

What this does not prove: that custody refuses a consumer it does not know, that the witness finds a real rewrite of a stream, and that the processes keep their keys from your own user. Those are keyryx's own tests. Custody and the witness run as one OS user here, so they are separated by process and directory, not by user ID. Not run here: a live custody signing a root, and a live rotation followed by a repin.

Options

--only money       just the money plane (gateway + cloud + dashboard)
--no-dashboard     skip building and serving the dashboard
--no-demo          do not seed the short demo dataset into cloud
--with demo-fleet  seed a richer fleet into cloud (see "Demo fleet" above)
--no-tools         skip the five installed-not-started tools
--no-notify        skip heraldyx, the notifier (it is pinned to file mode and sends no mail either way)
--no-egress        skip scopyx, the web-egress enforcement point
--with-delegation  also start vouchryx, the delegation-token service, and open the gateway's delegation door against it
--with-finops      also start costcrew, the FinOps console, wired to this launcher's bus
--repin-record-key move the record seal's pin to custody's active trailryx-root key, after a rotation (needs keyryx); see "Keyryx"
--without-keyryx   turn keyryx off: the sealed record is then UNSIGNED (the weaker setting); see "Keyryx"
--force-install    replace binaries another tool installed
--workspace <dir>  look here for sibling checkouts (with --from-main only)
--from-main        build sibling or main checkouts instead of the pinned releases (a developer switch; not a release)
--with-typed       also start typryx (typed answers; stub backend unless a mode is chosen)
--typed-mode <m>   jev | own-model | off: where typed-answer data goes (see "Typed answers: choose where your data goes")
--typed-key-file <path>     a file holding the key (required for jev, optional for own-model)
--typed-model-url <url>     own-model: the server's base URL, ending in /v1
--typed-model <name>        own-model: the model name the server knows
--typed-training   keep typryx's opt-in local training log (off by default; needs typryx v0.3.0+, see "Your own model, on your own data")
--typed-risk-signal  also run typryx wardryx-proxy and point the MCP broker at it (off by default; see "The typed risk signal")
--run-budget-ceiling <usd>  the most one run may be granted (default 5.00; see "The gateway's run-budget ceiling")
--firewall <mode>  the agent firewall: enforce (default), shadow or off (see "The agent firewall")
--pii <mode>       personal-data masks: shadow (default), mask, block or off (see "The PII masks")
--plan             print every resolved choice and exit, starting nothing (--typed-plan is the same flag)
-h, --help         show help

Stopping

Run it in the foreground and Ctrl-C stops everything. If you background it (./up.sh &), stop it later with:

./down.sh

down.sh only signals the PIDs up.sh recorded when it launched each service.

If one service exits on its own, up.sh notices within two seconds, names it, points at its log, and stops the rest, exiting with status 1 (Ctrl-C or SIGTERM is a deliberate stop and exits 0; a refusal such as a gateway that will not start exits with 1 too, so a script driving up.sh can tell). This is a single-host launcher, not a supervisor: nothing is restarted, and a half-alive stand would mislead more than a stopped one. To watch how the planes behave when one of them is gone (the gateway refuses every decision it cannot get from wardryx, and scopyx fails closed too), start those planes by hand with the same environment up.sh gives them, outside the launcher.

Scheduled governance runs

The stack produces governance signal on its own, but nobody looks at it unless something runs it on a schedule. routines.sh is that schedule: it installs OS-native timers (systemd on Linux, launchd on macOS) for seven routines, and it is also the thing those timers invoke.

Routine Runs (local time) What it does
focus-export daily 06:07 tokenfuse-gateway focus-export -> a FOCUS-format FinOps CSV built from the gateway's own trace.
qryx-trend daily 06:17 qryx scan (saving evidence) then qryx trend -> crypto-inventory compliance-score history.
verdryx-drift daily 06:27 verdryx drift against a baseline you set -> a quality-regression check.
idryx-detect daily 06:37 idryx detect over the tokenfuse event stream -> an identity/access anomaly sweep.
mockryx-drill weekly, Monday 06:47 a live fire drill against your own gateway. Opt-in only - see the warning below.
chain-verify daily 06:52 agent-conform watch-dir over the shared event directory -> every stream's hash chain checked from its first line. A new break becomes one chain_broken (high) event in agent-conform.ndjson, which heraldyx reads; a break already announced is not announced again, and the routine keeps saying findings until it is gone.
trailryx-seal daily 06:57 trailryx-node events over every file in the shared event directory -> a sealed, offline-verifiable record of what the other routines, and the rest of the stack, did. Runs last so it can seal the same day's verdryx-drift finding too.
./routines.sh list                  # one line per routine: installed? last run?
./routines.sh run <name>            # run one now (this is what the scheduler calls)
./routines.sh install               # install timers for the six safe routines
./routines.sh install --with-drill  # also install the weekly drill (see the warning below)
./routines.sh uninstall             # remove exactly and only what install created
./routines.sh status                # last record per routine, plus the scheduler's own view

Each routine checks its own preconditions first - the binary it needs, the store or file it reads, any required config - before doing anything. A precondition that is not met is recorded as skipped with the exact reason and is not a failure: that is the expected state right after a fresh install, before you have pointed it at real binaries and data. Once those exist, it runs for real.

Config

An optional shell fragment at ~/.stack-up/routines/config, sourced if present:

ROUTINE_QRYX_SCAN_PATH=/path/to/scan        # qryx-trend's scan target (default: repos/ under STACK_UP_HOME)
ROUTINE_VERDRYX_BASELINE=some-baseline-id   # required for verdryx-drift; unset means it stays skipped
ROUTINE_VERDRYX_WINDOW=5                    # verdryx-drift's --window (default: 5)
ROUTINE_DRILL_SCENARIOS=/path/to/scenarios  # mockryx-drill's scenarios directory

The record

Every run is recorded twice: appended as one line to ~/.stack-up/routines/history.ndjson (the full history), and written atomically to ~/.stack-up/routines/status/<name>.json (just the latest). Both use the same shape:

{"schema":"stackup.routine-run/v1","routine":"<name>","started_at":"<RFC3339 UTC>","finished_at":"<RFC3339 UTC>","exit_code":N,"status":"ok|findings|skipped|error","reason":"<only for skipped/error>","artifact":"<path under out/, when produced>","summary":"<one line>"}

This is a stable contract: the Genaryx console reads it, so its shape does not change casually. status is one of ok (ran, nothing wrong - this includes a routine that found something, since finding it is the point of running it), findings (mockryx-drill: it found a gap, which is what a drill is for; chain-verify: a hash chain is broken, and it stays findings on every later run while the break is still there, because the verifier announces a break once and exits 0 afterwards), skipped (a precondition was not met - see reason), or error (a real usage/tool failure - see reason).

The mockryx-drill warning

mockryx-drill is never installed by default. install --with-drill prints this, first, before it installs anything:

Its weekly timer sends real traffic through your local gateway to whatever LLM provider is configured there. That traffic can spend that provider's money, and the drill is deliberately built to trip your policies - that is what a fire drill is for.

A note on clocks

The timers fire in whatever time zone the OS scheduler uses: local time, on the machine's own clock. Every timestamp inside a recorded run (started_at, finished_at) is UTC. If the machine's local time zone is not UTC, "06:07 local" and the UTC timestamp on that run's record will not line up numerically - that is expected, not a bug.

This is a sandbox, not a deployment

  • Every service binds to loopback only. Nothing is exposed off your machine.

  • cloud runs with one key minted fresh for each run (TOKENFUSE_CLOUD_KEYS=<key>:default:admin). The gateway, the demo seed and the dashboard link use the same key. It is held in ~/.stack-up/cloud-key (mode 0600) and never printed; the summary names that file. To use a key of your own, export STACK_UP_CLOUD_KEY (16 to 128 characters from A-Z a-z 0-9 . _ ~ -; anything else is refused before anything is built). It is an admin key on a local sandbox and nothing else. Do not run this on a host anything else can reach.

  • The literal bearer devkey no longer opens the cloud. It used to, through TOKENFUSE_CLOUD_ALLOW_DEVKEY=1 and an empty key set; tokenfuse removed that fallback (tokenfuse#380), and a cloud started with that variable now refuses to start. wardryx no longer runs in its dev-key mode either: this launcher gives it one key per run for the enforcement points (the gateway, the MCP broker and scopyx), in the pep role, and one admin key for the operator, in ~/.stack-up/wardryx-admin.key (mode 0600). The devkey literal is not used.

  • The gateway admits only a caller that presents this run's client key as x-fuse-key. The demo's own calls present it, and the key is in ~/.stack-up/gateway-client.key (mode 0600), named in the summary and never printed. A call without it is refused with 401. The opt-in mockryx-drill sends no client key (its tool does not take one yet), so the keyed gateway refuses it until it does.

  • The MCP broker (--with-typed) blocks a secret carried in a tool's arguments and a tool whose description reads as poisoned, and refuses a call when wardryx cannot answer. These are fixed settings, not switches: the broker's own tests show them working, and this launcher has not yet run a call through typryx's tools with them on.

  • The gateway's admin routes (GET /v1/runs, POST /v1/runs/{id}/kill, GET /v1/keys, GET /v1/policy-plane, GET /v1/agent-ids) need this run's admin key. The key is written to ~/.stack-up/gateway-admin.key (mode 0600), a new one each run, and the summary names that file without printing the value. The kill switch is one of these routes:

    curl -H "Authorization: Bearer $(cat ~/.stack-up/gateway-admin.key)" http://127.0.0.1:4100/v1/runs
  • idryx serve has no authentication of its own by design (loopback only).

  • The gateway clamps one run's caller-declared budget to the run-budget ceiling (5.00 USD by default, --run-budget-ceiling; see "The gateway's run-budget ceiling" for what that does and does not bound).

  • The gateway's semantic response cache is off (TOKENFUSE_CACHE=off): left on, its shadow-mode default serialises every call on one lock instead of serving anything (tokenfuse#319).

  • There is no telemetry. The only network access is cloning the repos and fetching build dependencies.

This exists so you can try the whole open stack in a couple of minutes and see how the pieces fit. For a real, governed, self-hosted deployment - keys, remote reachability, policy you actually wrote - follow each service's own README.

Layout

Two directories, with a clear split: one is the stack's, one is this script's.

Installed artifacts go to the stack's own home, ~/.taipan/, because that is where every other part of the stack looks for them. taipan writes here too.

~/.taipan/
  bin/              the built binaries, and the tool entry points
  venv/             the virtualenvs engram-mcp and verdryx run out of
  engram.engram     the memory store (created empty, never seeded)
  verdryx.db        the quality store (created empty, never seeded)

Everything that is only stack-up's business stays under ~/.stack-up/ (override with STACK_UP_HOME):

~/.stack-up/
  build/       where a build lands before it is installed
  markers/     staleness stamps + a checksum of each file stack-up installed
  repos/       repos stack-up cloned itself (absent if you use sibling checkouts)
  events/      the NDJSON event stream the services write and read
  records/     trailryx's sealed ledger, built from events/ (absent if that
               plane was skipped)
  logs/        one log file per service
  pids/        recorded PIDs, used by down.sh
  gateway-admin.key  the gateway's admin key for this run (mode 0600)
  keyryx/      custody's and the witness's data and root keys, rebuilt each run
               (key files mode 0600; see "Keyryx" above)
  typryx/      ledger only, only with --with-typed (its journal is now in
               events/typryx.ndjson above; see "Typed answers" above)
  routines/    routines.sh's own state: history, the latest record per routine,
               and the chain verifier's state file (agent-conform.state.json)

Earlier versions kept the binaries in ~/.stack-up/bin. They are moved on the next run rather than rebuilt, and the old directory is left empty.

down.sh stops processes; it is not an uninstall. Nothing under ~/.taipan/ is removed by it, deliberately: those binaries and stores may be in use by something stack-up did not start.

License

Apache-2.0. See LICENSE.

About

Run the open TAIPANBOX agent-governance stack locally, natively, no Docker, and open its money-plane dashboard in your browser.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages