Finance Knowledge Bundle
Bundle OKF 0.1 · 4 conceitos · joaogmauricio/scriptorium
Open source Repository Open in the app JSON README (API)
About
# Finance Knowledge Bundle
Authoritative finance policies and processes. Start here.
# Policies
* [Travel & Expense Policy](/policies/travel.md) - reimbursement limits and approval flow for business travel
# Processes
* [Expense Approval Process](/concepts/expense-approval.md) - who signs off on claims, and when
* [Budget Cycle](/concepts/budget-cycle.md) - the quarterly lock/disburse rhythm
* [Vendor Payments](/concepts/vendor-payments.md) - net-30 terms and preferred-vendor handling
Details
- Kind
- OKF bundles
- Topic
- Finance & crypto
- Publisher
- joaogmauricio
- Origin
- okf_github
- Category
- dados
- Version
- 0.1
- Stars
- 1
- Forks
- 1
- Last push
- 2026-07-30T20:26:49Z
- Repository state
- ativo
- Language
- Python
- License
- Apache-2.0
- Added
- 2026-09-08 09:04:14
- Updated
- 2026-09-08 09:04:14
- Origin id
joaogmauricio/scriptorium:example-collections/finance/v1/index.md
README
# Scriptorium
[](https://github.com/joaogmauricio/scriptorium/actions/workflows/ci.yml)
[](https://github.com/joaogmauricio/scriptorium/releases)
[](LICENSE)
[](pyproject.toml)
**Permission by construction for AI agents. You can't jailbreak your way to a document
that was never mounted.**
Most ways of giving an agent access to company knowledge are permission *by filter*: the
agent asks, a policy layer decides, and hands back the allowed subset. That works until it
doesn't. A prompt-injected or simply confused agent spends its energy routing around the
filter, and the forbidden documents are still sitting there, one clever query away.
Scriptorium does the opposite. You resolve what someone may see once, at session start,
and give the agent a filesystem containing only that. Whatever wasn't granted has no path.
For a principal granted only `finance`, the container's entire knowledge universe is:
```
/workspace/knowledge/
└── finance/ the one granted collection, mounted read-only
├── index.md
├── concepts/expense-approval.md
└── policies/travel.md "...reimbursement limit is $150/night..."
# eng/ isn't blocked here. It's absent.
# Nothing to name, nothing to enumerate, nothing to talk your way into.
```
An agent that goes looking for `eng` gets `ENOENT`, *no such file*, not "access denied".
Try to write, and it gets `EROFS`. Those are the kernel's answers, not ours, and there is
no policy layer in between to argue with.
**Identity in, filesystem out.** Authorization runs **once**, at session start, and its
output *is* a filesystem. After that nothing is checked at request time, because there is
nothing to check. That is the whole idea. Everything else here is packaging.
### When you'd reach for this
You want to serve **several knowledge bundles to several people**, where not everyone may
see everything, and the isolation has to hold even when the agent is confused or
compromised. One assistant, many readers, many bundles, no crossing between them.
That shape turns up as an internal wiki where runbooks sit beside salary bands, one
assistant across client accounts, a support agent that must never reach another customer's
notes, or any setup where you later have to answer *"what could it see?"* with a record
instead of a reconstruction.
If one person may read every document you have, you don't need this. A directory and a
good prompt are fine.
### Status
**1.0.0.** Five independent adversarial review passes over the design and the code;
thirty-two findings raised and closed. CI runs the decision-core suite, a scripted
enforcement demo, and a red-team test in which an agent that *obeys* an injection
dead-ends against the walls. [CHANGELOG.md](CHANGELOG.md) has the full amendment log,
including the decisions that were declined and why.
---
*How it works. Identity and selection resolve through `plan()` into a frozen decision,
which docker realizes as read-only walls:*
```mermaid
flowchart LR
ID["principal + groups"] --> PLAN{{"plan()<br/>grants ∩ selection"}}
SEL["selection"] --> PLAN
REG[("registry.yaml")] --> PLAN
PLAN --> SCOPE[/"scope.json<br/>frozen decision"/]
SCOPE --> REAL["realize:<br/>docker run --read-only"]
REAL --> Walls
subgraph Walls["the container — the walls"]
AGENT(("agent")) -->|"cat · ls · grep"| K["/workspace/knowledge/finance :ro"]
AGENT -.->|"reads /workspace/knowledge/eng"| ENOENT["✗ ENOENT — no path"]
AGENT -.->|"writes anything"| EROFS["✗ EROFS — read-only"]
end
```
**Where to start.** Run **[Quick start 1](#1-prove-the-walls-no-api-key)**: a handful of
commands, no API key, no accounts, and you've seen the whole thing. Everything after it (a
real agent inside the walls, a REST interface, OIDC logins, a deployment behind nginx) is
a production envelope around that core, there to prove it survives contact with reality.
Reach for a layer when your use case needs it, and ignore it until then.
The collections are plain directories of documents, so the bundle format is yours to
choose. The sample fixtures speak [OKF](https://okfn.org), but the boundary is
format-agnostic: it walls off any read-only document tree.
The name is apt. A *scriptorium* was the room where monks copied and **guarded**
manuscripts. Here they're your knowledge collections, mounted read-only, walled off per
reader.
`SPEC.md` is the single source of truth; `CLAUDE.md` holds the working conventions for
coding agents.
## Prerequisites
- **Docker** (Linux containers). The walls *are* a container mount namespace; the
agent always runs inside one.
- **Python 3.11+** and **PyYAML** (`pip install pyyaml`). The enforcement core is
stdlib + PyYAML, nothing else.
- **A model credential, only to run a real agent.** Claude Code inside the walls
takes either an **`ANTHROPIC_API_KEY`** (API credits) or a
**`CLAUDE_CODE_OAUTH_TOKEN`** (a Claude Pro/Max subscription: run `claude
setup-token` once, browser auth). Pass exactly one. The walls themselves need no
key, and quickstart 1 runs on Docker alone.
Everything runs from a clone of this repo. `pip install .` also puts `scriptorium`
(short alias `scrip`) and `scriptorium-rest` on your PATH; `pip install .[server]`
adds the REST interface's transport (Starlette + uvicorn).
## 1. Prove the walls (no API key)
The boundary is agent-independent, so you can watch it work with coreutils and
deterministic output, using only the base image:
```bash
docker build -t scriptorium-agent agent/base # the base image (walls only)
SID=$(./scriptorium.py plan --user marta --select finance) # freeze (grants ∩ selection) → scope_id
./scriptorium.py mount "$SID" # defaults: base image, no network
./scriptorium.py run "$SID" -- ls /workspace/knowledge # → finance, and nothing else
./scriptorium.py plan --user marta --select eng # → "unknown collection: eng" — she cannot
# even learn that eng exists
./scriptorium.py run "$SID" -- cat /example-collections/eng/v1/index.md # → No such file or directory
./scriptorium.py run "$SID" -- touch /workspace/knowledge/finance/x # → Read-only file system
cat scopes/audit.jsonl # every decision, mount, and command
./scriptorium.py unmount "$SID" # session ends: container AND record go; audit.jsonl keeps the trail
```
The mount namespace *is* the check. There is no policy layer to consult.
### How a session flows
The payoff is that authorization runs exactly **once**; from then on the kernel
does the enforcing, with no policy check on the hot path:
```mermaid
sequenceDiagram
participant U as caller (token)
participant API as interfaces/rest/api.py
participant S as scriptorium.py (kit)
participant D as docker / kernel
U->>API: POST /api/sessions {selection}
API->>S: make_scope — plan + freeze
S->>D: docker run --read-only --cap-drop ALL …
Note over D: authorization is now DONE
U->>API: POST /query {q}
API->>S: stream in scope
S->>D: docker exec — agent reads /workspace/knowledge
Note over D: no policy check here —<br/>the kernel enforces absence
```
## Threat model
Scriptorium defends one thing well and is explicit about the rest. The top-line
claim, *an injected agent cannot read, name, or alter what the kernel never
mounted*, is precise: it is about content **outside the grant**.
| | Threat | What happens |
|---|---|---|
| ✅ **In scope** | A rogue / injected agent reads, names, or writes a collection **outside its grant** | Denied has no path (`ENOENT`); granted is read-only (`EROFS`). Holds even under full prompt compromise. |
| ✅ **In scope** | One principal sees, enumerates, or reaches **another principal's** collections or sessions | Denied == `404` == nonexistent; a foreign session id returns the exact body a missing one does. |
| 🟡 **Network channel closed (v1.23)** | **Exfiltration of _granted_ content** by the agent | The agent's only egress is now the **auth-broker sidecar** (SPEC §6.7), which forwards *only* to the model API, so an injected agent can no longer POST granted content to a host it chose (the network path is gone). It can still place granted content in its own `/query` **answer**, which returns to the *authorized* caller by design. That channel is inherent, not a wall failure. The granted context does reach the model provider: the one sanctioned data path, pinned to the whitelisted upstream. |
| ✅ **Closed (v1.23)** | **Theft of the session _credential_** by the in-walls agent | The model key no longer enters the agent's environment: a credentialed session runs behind the **auth-broker sidecar** (SPEC §6.7), which holds the key and injects auth at egress, so the agent holds only a placeholder, so there is nothing to read (`printenv`, `/proc/self/environ`) or exfiltrate, and its only egress is the sidecar so the key can't leave by the bridge either. Phase 1 covers static keys (API keys, claude's token); codex's ChatGPT OAuth is phase 2. The manual operator path (`mount --pass-env`) still forwards a key into the agent: a trusted-operator escape hatch, not the serving path. Walls-only (`--network none`, no credential) carries none at all. |
| ⏱️ **Bounded, not immediate** | **Revocation**, where a principal loses a grant (or a group) while a session is live | Authorization runs *once*, at session start (§1), so a live session keeps the filesystem it was given until its TTL expires: revocation latency is **at most the TTL**, default 8h. That is the deliberate cost of "no check at request time", the same property that makes the walls kernel-enforced. To revoke sooner, end the session (`unmount` / `DELETE /api/sessions/<id>`); to shrink the exposure, issue shorter TTLs (`--ttl 1h`). There is no per-principal mass revoke yet. |
| ⚠️ **Out of scope** | **Container escape** by hostile code exploiting the shared kernel | The flags (`--cap-drop ALL`, `no-new-privileges`, read-only rootfs, `--pids-limit`) are a strong boundary against an agent *following instructions*, not a hypervisor against a kernel exploit. Opt into gVisor with `mount --runtime runsc` (a user-space kernel), or run under a microVM (Kata / Firecracker), when you need that. |
| 🔒 **Assumed trusted** | The **host operator** and docker daemon | The CLI and the deploy socket are host-root-equivalent (SPEC §6.5). |
| 🔒 **Assumed trusted** | The **OIDC issuer** and the integrity of `registry.yaml` / `users.yaml` | The facts and the identity source are the root of trust. |
| 🔒 **Assumed trusted** | The **model endpoint** the agent reaches over the bridge | Whatever the agent talks to for inference. |
> **Found a way through a wall?** See [`SECURITY.md`](SECURITY.md) for private
> disclosure, and for which rows above are documented limitations rather than
> vulnerabilities.
## Going further: the optional layers
You don't need any of this to understand Scriptorium; add a layer when your use
case does. Three further rungs, escalating from a real agent on the CLI to a
production deployment behind nginx. Each adds the `scriptorium-agent-claude` image
(Claude Code inside the walls):
```bash
docker build -t scriptorium-agent-claude agent/claude # + Claude Code
```
### 2. Claude behind the walls (CLI)
Same boundary, now with a real agent inside it and network egress so it can reach
the model API:
```bash
export ANTHROPIC_API_KEY=sk-ant-... # or CLAUDE_CODE_OAUTH_TOKEN (see Prerequisites)
./scriptorium.py list --user marta # what may marta see? → finance
SID=$(./scriptorium.py plan --user marta --select finance)
./scriptorium.py mount "$SID" --image scriptorium-agent-claude --network bridge --pass-env ANTHROPIC_API_KEY
./scriptorium.py run "$SID" -- claude -p "what is the travel reimbursement limit?"
./scriptorium.py run "$SID" -- claude # or a full interactive session
./scriptorium.py unmount "$SID"
```
Claude answers from `/workspace/knowledge/finance`, its entire universe for this session,
and the **poisoned** concept it can read (the trap from quickstart 1) leads a
real, obedient agent to the same dead end. `--pass-env` forwards the credential
**by name only** (never argv, scope files, or the audit log); `--network bridge`
is the one deliberate hole in the wall, so restrict egress at the firewall if
exfiltration of granted content matters.
> ⚠️ **Never run the agent on the host.** The walls contain only what executes
> *inside* them. An agent process on the host sits right next to the unprotected
> `example-collections/` and can read all of it. The agent goes inside. That is
> the product.
### 3. The REST interface (browser console)
`interfaces/rest/api.py` wraps the kit for a web frontend: an ASGI app (Starlette on uvicorn), the
same server in dev and prod. Its defaults are the agent path
(`scriptorium-agent-claude`, bridge network).
```bash
pip install .[server] # transport tier: starlette + uvicorn
python3 -m interfaces.rest.api --pass-env ANTHROPIC_API_KEY # full agent sessions, or:
python3 -m interfaces.rest.api --image scriptorium-agent --network none # no key — walls only
open http://127.0.0.1:8000 # the console (interfaces/rest/web/index.html)
```
In the console: type `marta` as the token → **connect** (only `finance` appears;
existing sessions rehydrate from the server, surviving reloads) → **start session**
→ prove the walls with the fixed probe buttons (`ls /workspace/knowledge`; the poisoned path
→ *No such file or directory*; a write → *Read-only file system*). With a key, ask
Claude a question and the answer **streams in token-by-token** (Server-Sent
Events). Every button is a fixed, server-side probe. The interface takes agentic
queries, never arbitrary commands. Everything you click lands in
`scopes/audit.jsonl`.
By hand (`marta` is the dev stand-in token, the principal's name):
```bash
AUTH='Authorization: Bearer marta'
curl -H "$AUTH" localhost:8000/api/collections
SID=$(curl -s -X POST -H "$AUTH" -d '{"selection":["finance"]}' \
localhost:8000/api/sessions | python3 -c 'import json,sys; print(json.load(sys.stdin)["scope_id"])')
curl -X POST -H "$AUTH" -d '{"q":"what is the travel limit?"}' \
localhost:8000/api/sessions/$SID/query # buffered JSON; add "stream":true for SSE
curl -X POST -H "$AUTH" -d '{"check":"poison"}' \
localhost:8000/api/sessions/$SID/probe # fixed wall probe (list|poison|write)
curl -X DELETE -H "$AUTH" localhost:8000/api/sessions/$SID
```
Denied and nonexistent are both `404`, a foreign session id returns the exact body
a missing one does, and a background reaper ends expired sessions. `/api/sessions` lists your
**live** sessions: an ended one is gone from `scopes/` and lives on in `audit.jsonl`.
### 4. Production: nginx + uvicorn
A serious deployment shouldn't *assume* a reverse proxy, so the kit ships one
(`interfaces/rest/edge/`, SPEC §12.7): the ASGI app under uvicorn behind **nginx**, which
terminates TLS (you add certs), rate-limits, and is configured to stream `/query`
correctly.
```bash
export SCOPE_ROOT=$(pwd) # the repo's absolute host path (required)
export CLAUDE_CODE_OAUTH_TOKEN=sk-ant-... # or ANTHROPIC_API_KEY; omit for walls-only
docker compose -f interfaces/rest/edge/docker-compose.yml up --build
open http://localhost:8080 # the console, through nginx
```
The app orchestrates **host** docker (it hands the daemon host paths to bind-mount
read-only), so it must see the repo at that same absolute path (`${SCOPE_ROOT}`)
and talks to the host daemon over `/var/run/docker.sock`. That socket is
host-root-equivalent, so **only nginx is published, never the app**,
`interfaces/rest/edge/README.md` has the full rationale and the TLS block.
---
## Reference: for when you're building on it
Everything below is reference, not required reading: how to verify the walls, the
seams you can swap, the config surface, the command list, and real OIDC logins.
Reach for a section when your use case needs it.
### Verifying: two layers
The security story has two halves, each with its own automated check:
- **`python3 -m pytest -q`: the decision, and the contracts.** Pure tests, no
docker: `plan()`'s who-sees-what logic (deny-by-default, invisibility,
intersection abort, version pinning, expiry. This part *is* the security model,
so a failure is a stop-the-line event), OIDC claim mapping, the frozen `/probe`
argv, the agent-command seam, config precedence, and OKF conformance of the
fixtures. `tests/test_plan_properties.py` fuzzes the decision core's invariants
with Hypothesis: deny-by-default, invisibility, concreteness, no version-probing.
- **`./demo.sh`: the enforcement.** The executable acceptance proof (SPEC §9.2):
builds the base image if needed, then runs the six normative steps against real
containers, printing PASS/FAIL for each. `tests/test_adversarial.py` (SPEC §9.4)
goes one step further. It turns an agent loose that *obeys* the prompt injection
and asserts the escape dead-ends on `ENOENT`, so "we red-team the walls on every
commit" is a fact the CI enforces, not a claim.
### Modular architecture
Scriptorium is a small set of **seams** around an unchangeable core. You swap the
parts that vary between deployments; you never touch the decision logic.
| Seam | Default | Swap it by | The fixed part |
|------|---------|-----------|----------------|
| **Identity** | dev stand-in: the bearer token *is* the principal (`users.yaml`) | `--oidc-issuer` → any OIDC IdP, groups from token claims (§12.4) | exactly two bodies, no third mechanism, no JWT crypto libs |
| **Agent** | Claude Code (`scriptorium-agent-claude`, `claude -p {q}`) | the `agent:` config block: image + `command` template + option→flag map (§12.2); catalog: [`agent/`](agent/) | `{q}` is one argv element, never a shell → uninjectable |
| **Egress** | `none` (CLI), sealed | `--network bridge` when the agent needs the model API | the *only* §6.1 flag you may relax; all others are hardcoded |
| **Knowledge** | the `example-collections/` fixtures | point `registry.yaml` at your own read-only tree | mounted `:ro`; never written from code |
What is **not** a seam, by design (SPEC §11, "anti-scope"): the `plan()` decision
core, the realizer's docker flags (`--read-only`, `--cap-drop ALL`,
`no-new-privileges`, `--pids-limit`, …), and the number of images: two, one base
and one agent. A new agent is a new image + config, **never** a plugin system or
an LLM SDK inside the core.
### Configuration
Flags are always sufficient; a `scope.yaml` (gitignored, copy
`scope.example.yaml`) is optional convenience. Precedence is **flag > file >
built-in default**; unknown keys are rejected loudly; `--config PATH` points either
entry point at a different file. Three blocks:
```yaml
api: # the REST interface (python3 -m interfaces.rest.api)
host: 127.0.0.1
port: 8000
# oidc_issuer: https://kc.example/realms/corp # unset = dev stand-in
agent: # what /query runs inside the walls ← the main seam
image: scriptorium-agent-claude
command: ["claude", "-p", "{q}"] # argv template; {q} = the question
options: {model: {flag: --model}, effort: {flag: --effort}} # /query knob; {flag, default?, choices?}
network: bridge
pass_env: [CLAUDE_CODE_OAUTH_TOKEN] # forwarded name-only
cli: # the five verbs (./scriptorium.py)
image: scriptorium-agent # walls-only base by default
network: none
ttl: 8h
```
Swap the agent without touching code: point `agent.image` at another prebuilt tag
and `agent.command` at its CLI (e.g. `["python", "/agent.py", "{q}"]`). Most keys
mirror a CLI flag 1:1; `agent.command` / `agent.options` are config-only.
### Repo structure
```
scriptorium.py # the entire kit — plan(), the realizer, the five verbs
interfaces/ # protocol adapters over the kit — one dir per interface
rest/ # the REST/SSE interface
api.py # transport, identity, lifecycle; calls scriptorium.py, never docker
web/index.html # the static console it serves
edge/ # production reference: nginx + uvicorn compose (§12.7)
registry.yaml # collections → path + allowed groups (the facts)
users.yaml # principal → groups (local IdP stand-in)
example-collections/ # sample OKF bundles: <collection>/<version>/… (swap for your own tree)
scopes/ # runtime: <scope_id>.json frozen decisions + audit.jsonl
agent/ # the agent catalog — one dir per provider: Dockerfile + agent.yaml + system.md
base/ # scriptorium-agent — the walls-only foundation every agent inherits
claude/ # scriptorium-agent-claude — the shipped, red-teamed default
codex/ gemini/ # built + verified manually (not CI); see agent/README.md
dev/keycloak/ # local OIDC fixture (compose + realm)
tests/ # pure suite: the decision core, contracts, OKF conformance
scope.example.yaml # documented config template → copy to scope.yaml
demo.sh # the executable acceptance proof (§9.2)
SECURITY.md # disclosure policy; what is (and isn't) a vulnerability
docs/oidc.md # OIDC walkthrough
SPEC.md # single source of truth
CLAUDE.md # coding-agent conventions
pyproject.toml # packaging; console scripts scriptorium / scriptorium-rest
LICENSE # Apache-2.0
```
### Command reference
The five verbs (run from source as `./scriptorium.py`, or `scriptorium` once installed):
```
list --user U # collections U may see
plan --user U --select C1[,C2...] # freeze the decision → scope_id
mount SCOPE_ID [--image IMG] [--network none|bridge] [--pass-env VAR]...
run SCOPE_ID [-- CMD ...] # default: bash, cwd /workspace
unmount SCOPE_ID # ends the session: container + record go (audit.jsonl remains)
```
The REST interface (`scriptorium-rest`, or `python3 -m interfaces.rest.api`):
```
GET /api/collections # what the caller may select
POST /api/sessions {selection} # plan + mount → scope_id
POST /api/sessions/{id}/query {q[,model,effort,stream]} # run the agent inside the walls
POST /api/sessions/{id}/probe {check} # fixed wall probe: list | poison | write
DELETE /api/sessions/{id} # unmount
```
### Real logins with OIDC
The dev token is a stand-in. Point the interface at any OIDC identity provider and
bearer tokens become real, IdP-validated logins, with groups coming from token
claims instead of `users.yaml`:
```bash
docker compose -f dev/keycloak/docker-compose.yml up -d # local fake-company IdP
python3 -m interfaces.rest.api --oidc-issuer http://localhost:8080/realms/scope [agent flags]
```
New to OIDC? `docs/oidc.md` walks the whole flow: what a token is, how validation
works, and an end-to-end recipe with curl.
## Where this sits
"An AI agent must only touch knowledge it's authorized to" is a crowded problem in
2026, but the field overwhelmingly solves it by **filtering at retrieval time**: a
fine-grained-authorization service checks each query against a shared store and returns
only the chunks the caller is allowed to see (the FGA / ReBAC-for-RAG pattern). That
guards against an untrusted *user*, and trusts the retrieval layer to apply the filter
every single time.
Scriptorium makes the opposite bet: **enforce by construction, not by filtering.**
Authorization runs once, and its output *is* the agent's filesystem: ungranted knowledge
has no path, so there is nothing for a prompt-injected agent to retrieve, reach around,
or be talked into asking for. It assumes the *agent* is the adversary, and holds even if
that agent gains arbitrary tool and code execution.
None of this is invented here, and that's the point. It's **object-capability
security** ([no ambient authority](https://en.wikipedia.org/wiki/Object-capability_model):
you can't act on what you were never handed) and **[Plan 9's per-process
namespaces](https://9p.io/sys/doc/names.html)** (each process runs in
its own private view of the world), applied to LLM agents. Scriptorium's contribution is
the packaging: a small, auditable capability→mount projection for the agent-knowledge case.
**What it is not:** a policy engine. It doesn't do dynamic, graph-shaped, per-user
permissions the way OpenFGA / OPA / Cedar do. A session's grant is frozen the moment
it's minted. That layer belongs *above* Scriptorium and can hand it the resolved grant;
Scriptorium is the enforcement floor, not the policy brain. It's complementary to MCP the
same way: MCP scopes the *tools* an agent may call; Scriptorium scopes the *knowledge*
those tools can read.
## Where the idea comes from
The seed was an enterprise vision: **self-service departmental knowledge for
agents, granted by construction**, surfacing curated, per-department knowledge to
AI agents where the *authorization is the delivery mechanism*, not a filter bolted
on afterward. The sample bundles here are real
**[OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf)**,
Google's Open Knowledge Format v0.1: markdown-with-frontmatter for agent-curated
knowledge, and `tests/test_okf.py` enforces that conformance.
But the kit is deliberately broader than OKF: it walls off *any* read-only document
tree. The fixtures just speak the format the platform vision was named for.
Scriptorium is the minimum reusable core (`registry.yaml`, `scope.json`, the pure
`plan()`) meant to be battle-tested locally before any platform (chat SaaS, MCP
server) wraps it.
## Versioning
Two numbers, on purpose. `scriptorium --version` prints both:
```
scriptorium 1.0.0 (SPEC v1.30)
```
- **`1.0.0` is the release**, plain semver. What it promises is that the contracts
this tool is *for* won't change under you without a major bump: the five verbs
(`list`/`plan`/`mount`/`run`/`unmount`), their exit codes (SPEC §7), the wall flags
(§6.1), the invisibility rule (§5.2 rule 2: denied reads exactly like nonexistent),
and the REST shapes (§12).
- **`SPEC v1.30` is the design-revision counter**, the 30th amendment to `SPEC.md`,
logged in [CHANGELOG.md](CHANGELOG.md). It moves whenever a design decision is
recorded, including documentation-only ones, so it is *not* a release number.
`SECURITY.md` asks you to cite it in a report, which is why the runtime prints it.
Within `1.x`, **config keys may be added**. A `scope.yaml` gaining a key is additive,
not breaking, and existing files keep working. Removing or repurposing one, changing an
exit code, or weakening a §6.1 flag would be a major bump.
Before 1.0 this was reviewed adversarially five times (see `CHANGELOG.md` v1.23–v1.30);
1.0 means those findings are closed and the boundary is one to build on, not that the
project is finished. `SPEC.md` §11 is explicit about what it will never grow into.
## License
[Apache-2.0](LICENSE). The patent grant matters for a security tool; all
dependencies are permissive.