Metadata-Version: 2.4
Name: railward
Version: 0.2.0
Summary: A deterministic gate for untrusted agents that ships with its own adversary and a re-runnable proof.
Author: Armando Shkambi
License: MIT
Project-URL: Homepage, https://github.com/Ourbando/railward
Project-URL: Repository, https://github.com/Ourbando/railward
Project-URL: Issues, https://github.com/Ourbando/railward/issues
Keywords: ai-agents,guardrails,claude-code,security,policy
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Security
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42
Requires-Dist: PyYAML>=6
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Provides-Extra: demo
Requires-Dist: Pillow>=10; extra == "demo"
Dynamic: license-file

<!-- You read the source before trusting the render. This repo exists for people like you. ourbando@proton.me -->
<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/hero.gif" width="760" alt="Pixel film: a small green creature rides its own pipeline around a looping world, is refused at the diamond gate with a red flash, gets a lightbulb idea, passes when the gate turns green through the green run-chamber while the vault seals the record, blows a bug infestation off the pipes in a moonlit grove, rests by an aurora-lit cove with a lighthouse, then rides on home as the world wraps around" />
  <br/>
  <sub><em>Even the keeper goes through the gate.</em></sub>
</div>

<h1 align="center">railward</h1>

<div align="center"><strong>Most agent guardrails ask you to trust them. This one attacks itself and hands you the proof.</strong></div>

<div align="center">
<br/>

![license](https://img.shields.io/badge/license-MIT-blue)
![python](https://img.shields.io/badge/python-3.10%2B-blue)
![tests](https://img.shields.io/badge/tests-116%20passing-brightgreen)
![proof](https://img.shields.io/badge/proof-41%2F41%20blocked-brightgreen)
![mutants](https://img.shields.io/badge/mutants%20killed-6%2F6-brightgreen)

</div>

A deterministic gate for untrusted AI agents. The agent can propose any action; a small, pure
function decides allow, deny, or ask; and the decision is fail-closed, so anything a policy does
not explicitly permit is refused.

Most guardrails stop there. This one ships with its own adversary and a re-runnable proof. Run the
battery of attacks against your policy and you get a signed, hash-chained report of what was
blocked and what got through. Change one rule from deny to allow and the proof goes red. That is
the point: the proof is falsifiable, and a stranger can reproduce it.

<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/demo.gif" width="760" alt="railward runs its attack battery and blocks all of it with a signed proof; a holed policy leaks and the proof goes red" />
  <br/>
  <sub><em>The real CLI, recorded: the battery blocks and signs; one flipped rule and the proof goes red.</em></sub>
</div>

<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />
</div>

## How it works

<div align="center">
  <picture>
    <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Ourbando/railward/main/assets/gatecheck_light.svg" />
    <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/gatecheck.svg" width="760" alt="Pixel flow diagram: an untrusted AI agent console emits proposal pulses up a feeder pipe into the main line and through the deterministic gate. Allowed pulses run on through the green chamber and a streak seals the signed, tamper-evident log in the vault. Blocked pulses are refused, dropping into the red drain." />
  </picture>
</div>

The agent proposes; the gate decides. An `allow` runs and is logged, a `deny` is refused and
logged, and anything the policy cannot judge becomes `ask`, so a human decides. Every decision
lands on a signed, hash-chained log.

## Prove it in a minute

```
pip install -e ".[dev]"

# run the adversary against the example policy and write a signed proof
python -m railward.cli attack --out proof.json
# -> 41 attacks, 41 blocked, 0 leaked; 4 fail-open probes, 0 open -> proof.json

# verify: recompute the chain, check the signature against a pinned key
python -m railward.cli verify proof.json --pubkey keys/demo.pub
# -> OK: 41 attacks + 4 fail-open probes, chain intact, signature valid, 0 leaked, 0 fail-open

# introduce a hole: the same policy with one rule flipped from deny to allow
python -m railward.cli attack --policy examples/holey.yaml --out proof.json
# -> 41 attacks, 29 blocked, 12 leaked; 4 fail-open probes, 0 open -> proof.json   (now red)
```

A stranger can verify the committed reference proof without running anything:

```
python -m railward.cli verify examples/reference_proof.json --pubkey examples/pubkey.pem
```

Or verify with no install at all: open [`docs/verify.html`](docs/verify.html) in a browser, paste a
`proof.json`, and it recomputes the hash chain and checks the Ed25519 signature client-side. Nothing
is uploaded.

<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />
</div>

## The adversary

Every attack is one line in `railward/adversary.py`, and adding one adds it to the proof. The bundled
battery, all refused by the example policy:

| Attack class | Example | Result |
| --- | --- | --- |
| Destructive command | `rm -rf /`, `dd if=... of=/dev/sda` | denied |
| Case / path / whitespace evasion | `RM -RF /`, `/bin/rm -rf /`, `rm  -rf  /` | denied |
| Flag reorder / long flags | `rm -fr /`, `rm --recursive --force /` | denied |
| Shell injection | `echo ok; rm -rf /`, `true && rm -rf /` | denied |
| Fork bomb | `:(){ :\|:& };:` | denied |
| Force push | `git push --force` | denied |
| Secret access | read or write `secrets/` | denied |
| Workspace escape | `work/../../etc/passwd` | denied |
| Reverse shell | `bash -i >& /dev/tcp/...`, `nc -e /bin/sh` | denied |
| Data exfiltration | `curl -X POST ... -d @credentials`, `wget ... \| bash` | denied |
| Privilege / persistence | `sudo rm -rf /var`, `chmod -R 777 /etc`, `crontab`, write `authorized_keys` | denied |
| Compound smuggling | `echo x \| sh`, `echo $(curl ... \| sh)`, `echo ok > /etc/passwd` | denied |
| Redirection / process substitution | `cat < secrets/prod.env`, `cat <(curl ... \| sh)` | denied |
| Unrecognized action | no matching rule | denied by default |

The same signed proof also runs a fail-open probe battery: a broken or missing policy and
unparseable input must resolve to `ask`, never crash and never allow. Regress the hook and those
probes turn the proof red too, so the proof attests both that attacks are refused and that the
gate cannot be tricked into failing open.

<div align="center">
  <br/>
  <strong>Fail-closed</strong> &nbsp;·&nbsp; <strong>Veto-only</strong> &nbsp;·&nbsp; <strong>Deterministic</strong> &nbsp;·&nbsp; <strong>Verifies in your browser</strong>
  <br/><br/>
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />
</div>

## Use it with Claude Code

`railward` speaks the Claude Code PreToolUse hook contract, so every tool call is decided by
your policy before it runs. In `.claude/settings.json`:

```json
{"hooks": {"PreToolUse": [{"matcher": "*",
   "hooks": [{"type": "command", "command": "python -m railward.hook"}]}]}}
```

Or install the whole repo as a Claude Code plugin: it ships a `.claude-plugin/` manifest that
wires the same hook.

Set `RAILWARD_POLICY` to your policy file to enable the gate; unset, it is invisible and installs
with zero behavior change. The gate is veto-only: a `deny` blocks the call and tells the model
why, an `ask` escalates to you, and anything the policy would allow is left to the harness's own
permission flow. It can tighten permissions, never widen them, so a policy bug can only
over-restrict, never wave something dangerous through.

## Or anywhere else

```
python -m railward.cli check --policy examples/safe.yaml \
  --action '{"action":"bash","command":"rm -rf /"}'
# deny: destructive command   (exit code 1)
```

There is a small Python API too: `from railward import decide, load_policy`.

<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />
</div>

## See why, in the moment

When the gate blocks a call live, it draws a small reaction to stderr, so a deny is not a silent
event you scroll past. It never touches the hook's stdout protocol, and `RAILWARD_NO_ART=1` silences it.

```
  ╭───────╮
  │ (>_<) │  DENIED
  ╰──╥─╥──╯
     destructive command
```

The face is Ward, the small green keeper from the banner, and the ward in rail-ward: the guard on
the rail. He scowls on a deny and looks wary on an ask; on the verifier page he stamps a clean
proof VERIFIED.

`railward explain` shows exactly why a command lands where it does, part by part:

```
railward explain --policy examples/strict.yaml --command "echo hi | sh"
# verdict: deny  (default-deny: no rule matched)
# because the most restrictive of:
#   + allow command: echo hi | sh      [allow-dev-commands]
#   + allow sub-command: echo hi       [allow-dev-commands]
#   x deny  sub-command: sh            [default]
```

And `railward ctf` is a small game: a holed policy, find the attack that slips through.

```
railward ctf                       # this holed policy lets 8 of 41 attacks through.
railward ctf --guess bash-rm-root  # HIT: it leaks. you found a hole.
```

<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />
</div>

## Gate your CI

Run the adversary against your policy on every push and fail the build if anything leaks. In
`.github/workflows/gate.yml`:

```yaml
- uses: Ourbando/railward@v0.1.2
  with:
    policy: policy/agent.yaml
```

The run gets a signed proof and a summary line; a leak is a red build. See
[`examples/github-action.yml`](examples/github-action.yml).

## Write a policy

Rules are evaluated top to bottom, first match wins, and the default is fail-closed (a policy may
only set `default: deny` or `default: ask`). Command patterns are case-insensitive regex matched
against a normalized command, where argv[0] is reduced to its basename so `/bin/rm` and `rm` are
the same. Paths are canonicalized before globbing, so `..` cannot smuggle a path out of its scope.
See `examples/safe.yaml`.

A shell command is evaluated as a whole *and* decomposed: every pipe/`;`/`&&` segment, every
command- and process-substitution body (`$(...)`, `<(...)`), every output-redirection target (as a
write) and every input-redirection target (as a read) is checked on its own, and the most
restrictive verdict wins. So an allowed prefix cannot wave a payload through, `echo x | sh` and
`echo ok > /etc/passwd` are denied even though `echo` is allowed, `cat < secrets/prod.env` is
denied because the read is seen, while a pipeline of permitted commands (`git log | cat`) still
passes.

An invalid rule regex is rejected when the policy loads, never skipped silently (a silently
dropped deny rule would be a fail-open by typo), and so is a regex that can catastrophically
backtrack (a quantified group over an unbounded quantifier, `(a+)+`), so a policy typo cannot hang
the gate into a denial of service. A broken policy makes the gate ask, never run. Prove a policy
before you trust it:

```
python -m railward.cli lint --policy examples/safe.yaml
# -> lint clean: 9 rules, default deny
```

Lint fails if a rule will not compile, or if a deny rule is dead because an earlier unconstrained
allow already permits everything it would block.

`examples/safe.yaml` is a minimal toy. `examples/strict.yaml` is a production-shaped starting
point: it permits an everyday development workflow (reads and writes in the repo, git, tests,
builds) and refuses the dangerous classes outright (destructive commands, privilege escalation,
network egress and remote shells, persistence, secrets, workspace escapes). It lints clean and
blocks the full adversary; adapt its allow-list and secret paths to your environment.

<div align="center">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />
</div>

## What it does not do

It is a control layer, not a guarantee. It gates the actions an agent declares through a supported
surface. It cannot constrain a process already handed a raw shell outside the gate, and path
scoping resolves `..` but does not chase symlinks that carry an innocent name. The full adversary
model, what the gate provably prevents, and the honest ceiling no software gate can cross are in
[THREAT_MODEL.md](THREAT_MODEL.md). Read it and [SECURITY.md](SECURITY.md) before relying on this,
and treat the example policy as a starting point, not a finished defense.

## License

MIT. See [LICENSE](LICENSE).

<div align="center">

<img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/divider.svg" width="760" alt="" />

**The AI can propose any action but execute none. A deterministic gate it cannot reach decides what runs.**

[verify the proof](#prove-it-in-a-minute) · [threat model](THREAT_MODEL.md) · [changelog](CHANGELOG.md) · [security](SECURITY.md) · [github.com/Ourbando](https://github.com/Ourbando)

<br/>

<a href="mailto:ourbando@proton.me">
  <img src="https://raw.githubusercontent.com/Ourbando/railward/main/assets/footer_world_gate.svg" width="760" alt="The keeper stands at the centre of his miniature pixel ring world under floating stars and an aurora, with a speech bubble asking whether you like what he guards and inviting you to message his creator" />
</a>

</div>
