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.
git clone https://github.com/TAIPANBOX/stack-up
cd stack-up
./up.shThen 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).
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 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.
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 nothing5.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.
./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 aboveMeasured 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.
./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.
./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.)
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/v1What 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-trainingThis 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.jsonlThe 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.
--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.
--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-signalIt 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).
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.
- 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.
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, sendingx-fuse-keywith 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 isGET :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 ungatedPOST /v1/ingest. stack-up points the gateway at cloud, so your live traffic through:4100shows 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.
--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-fleetIt 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).
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 --allor 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.packtrailryx-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.
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.
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.shThe 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.
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 nothingWith 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_readorread_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.
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 alltokenfuse 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 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.
--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
Run it in the foreground and Ctrl-C stops everything. If you background it
(./up.sh &), stop it later with:
./down.shdown.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.
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 viewEach 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.
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 directoryEvery 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).
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.
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.
-
Every service binds to loopback only. Nothing is exposed off your machine.
-
cloudruns 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, exportSTACK_UP_CLOUD_KEY(16 to 128 characters fromA-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
devkeyno longer opens the cloud. It used to, throughTOKENFUSE_CLOUD_ALLOW_DEVKEY=1and 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-inmockryx-drillsends 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 servehas 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.
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.
Apache-2.0. See LICENSE.