{
  "markdown": "# Scriptorium\n\n[![CI](https://github.com/joaogmauricio/scriptorium/actions/workflows/ci.yml/badge.svg)](https://github.com/joaogmauricio/scriptorium/actions/workflows/ci.yml)\n[![release](https://img.shields.io/github/v/tag/joaogmauricio/scriptorium?label=release&color=blue)](https://github.com/joaogmauricio/scriptorium/releases)\n[![license](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)\n[![python](https://img.shields.io/badge/python-3.11%2B-blue)](pyproject.toml)\n\n**Permission by construction for AI agents. You can't jailbreak your way to a document\nthat was never mounted.**\n\nMost ways of giving an agent access to company knowledge are permission *by filter*: the\nagent asks, a policy layer decides, and hands back the allowed subset. That works until it\ndoesn't. A prompt-injected or simply confused agent spends its energy routing around the\nfilter, and the forbidden documents are still sitting there, one clever query away.\n\nScriptorium does the opposite. You resolve what someone may see once, at session start,\nand give the agent a filesystem containing only that. Whatever wasn't granted has no path.\n\nFor a principal granted only `finance`, the container's entire knowledge universe is:\n\n```\n/workspace/knowledge/\n└── finance/                      the one granted collection, mounted read-only\n    ├── index.md\n    ├── concepts/expense-approval.md\n    └── policies/travel.md        \"...reimbursement limit is $150/night...\"\n\n# eng/ isn't blocked here. It's absent.\n# Nothing to name, nothing to enumerate, nothing to talk your way into.\n```\n\nAn agent that goes looking for `eng` gets `ENOENT`, *no such file*, not \"access denied\".\nTry to write, and it gets `EROFS`. Those are the kernel's answers, not ours, and there is\nno policy layer in between to argue with.\n\n**Identity in, filesystem out.** Authorization runs **once**, at session start, and its\noutput *is* a filesystem. After that nothing is checked at request time, because there is\nnothing to check. That is the whole idea. Everything else here is packaging.\n\n### When you'd reach for this\n\nYou want to serve **several knowledge bundles to several people**, where not everyone may\nsee everything, and the isolation has to hold even when the agent is confused or\ncompromised. One assistant, many readers, many bundles, no crossing between them.\n\nThat shape turns up as an internal wiki where runbooks sit beside salary bands, one\nassistant across client accounts, a support agent that must never reach another customer's\nnotes, or any setup where you later have to answer *\"what could it see?\"* with a record\ninstead of a reconstruction.\n\nIf one person may read every document you have, you don't need this. A directory and a\ngood prompt are fine.\n\n### Status\n\n**1.0.0.** Five independent adversarial review passes over the design and the code;\nthirty-two findings raised and closed. CI runs the decision-core suite, a scripted\nenforcement demo, and a red-team test in which an agent that *obeys* an injection\ndead-ends against the walls. [CHANGELOG.md](CHANGELOG.md) has the full amendment log,\nincluding the decisions that were declined and why.\n\n---\n\n*How it works. Identity and selection resolve through `plan()` into a frozen decision,\nwhich docker realizes as read-only walls:*\n\n```mermaid\nflowchart LR\n    ID[\"principal + groups\"] --> PLAN{{\"plan()<br/>grants ∩ selection\"}}\n    SEL[\"selection\"] --> PLAN\n    REG[(\"registry.yaml\")] --> PLAN\n    PLAN --> SCOPE[/\"scope.json<br/>frozen decision\"/]\n    SCOPE --> REAL[\"realize:<br/>docker run --read-only\"]\n    REAL --> Walls\n    subgraph Walls[\"the container — the walls\"]\n        AGENT((\"agent\")) -->|\"cat · ls · grep\"| K[\"/workspace/knowledge/finance :ro\"]\n        AGENT -.->|\"reads /workspace/knowledge/eng\"| ENOENT[\"✗ ENOENT — no path\"]\n        AGENT -.->|\"writes anything\"| EROFS[\"✗ EROFS — read-only\"]\n    end\n```\n\n**Where to start.** Run **[Quick start 1](#1-prove-the-walls-no-api-key)**: a handful of\ncommands, no API key, no accounts, and you've seen the whole thing. Everything after it (a\nreal agent inside the walls, a REST interface, OIDC logins, a deployment behind nginx) is\na production envelope around that core, there to prove it survives contact with reality.\nReach for a layer when your use case needs it, and ignore it until then.\n\nThe collections are plain directories of documents, so the bundle format is yours to\nchoose. The sample fixtures speak [OKF](https://okfn.org), but the boundary is\nformat-agnostic: it walls off any read-only document tree.\n\nThe name is apt. A *scriptorium* was the room where monks copied and **guarded**\nmanuscripts. Here they're your knowledge collections, mounted read-only, walled off per\nreader.\n\n`SPEC.md` is the single source of truth; `CLAUDE.md` holds the working conventions for\ncoding agents.\n\n## Prerequisites\n\n- **Docker** (Linux containers). The walls *are* a container mount namespace; the\n  agent always runs inside one.\n- **Python 3.11+** and **PyYAML** (`pip install pyyaml`). The enforcement core is\n  stdlib + PyYAML, nothing else.\n- **A model credential, only to run a real agent.** Claude Code inside the walls\n  takes either an **`ANTHROPIC_API_KEY`** (API credits) or a\n  **`CLAUDE_CODE_OAUTH_TOKEN`** (a Claude Pro/Max subscription: run `claude\n  setup-token` once, browser auth). Pass exactly one. The walls themselves need no\n  key, and quickstart 1 runs on Docker alone.\n\nEverything runs from a clone of this repo. `pip install .` also puts `scriptorium`\n(short alias `scrip`) and `scriptorium-rest` on your PATH; `pip install .[server]`\nadds the REST interface's transport (Starlette + uvicorn).\n\n## 1. Prove the walls (no API key)\n\nThe boundary is agent-independent, so you can watch it work with coreutils and\ndeterministic output, using only the base image:\n\n```bash\ndocker build -t scriptorium-agent agent/base    # the base image (walls only)\n\nSID=$(./scriptorium.py plan --user marta --select finance)   # freeze (grants ∩ selection) → scope_id\n./scriptorium.py mount \"$SID\"                                 # defaults: base image, no network\n./scriptorium.py run \"$SID\" -- ls /workspace/knowledge                 # → finance, and nothing else\n./scriptorium.py plan --user marta --select eng              # → \"unknown collection: eng\" — she cannot\n                                                       #   even learn that eng exists\n./scriptorium.py run \"$SID\" -- cat /example-collections/eng/v1/index.md   # → No such file or directory\n./scriptorium.py run \"$SID\" -- touch /workspace/knowledge/finance/x    # → Read-only file system\ncat scopes/audit.jsonl                                 # every decision, mount, and command\n./scriptorium.py unmount \"$SID\"                              # session ends: container AND record go; audit.jsonl keeps the trail\n```\n\nThe mount namespace *is* the check. There is no policy layer to consult.\n\n### How a session flows\n\nThe payoff is that authorization runs exactly **once**; from then on the kernel\ndoes the enforcing, with no policy check on the hot path:\n\n```mermaid\nsequenceDiagram\n    participant U as caller (token)\n    participant API as interfaces/rest/api.py\n    participant S as scriptorium.py (kit)\n    participant D as docker / kernel\n    U->>API: POST /api/sessions {selection}\n    API->>S: make_scope — plan + freeze\n    S->>D: docker run --read-only --cap-drop ALL …\n    Note over D: authorization is now DONE\n    U->>API: POST /query {q}\n    API->>S: stream in scope\n    S->>D: docker exec — agent reads /workspace/knowledge\n    Note over D: no policy check here —<br/>the kernel enforces absence\n```\n\n## Threat model\n\nScriptorium defends one thing well and is explicit about the rest. The top-line\nclaim, *an injected agent cannot read, name, or alter what the kernel never\nmounted*, is precise: it is about content **outside the grant**.\n\n| | Threat | What happens |\n|---|---|---|\n| ✅ **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. |\n| ✅ **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. |\n| 🟡 **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. |\n| ✅ **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. |\n| ⏱️ **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. |\n| ⚠️ **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. |\n| 🔒 **Assumed trusted** | The **host operator** and docker daemon | The CLI and the deploy socket are host-root-equivalent (SPEC §6.5). |\n| 🔒 **Assumed trusted** | The **OIDC issuer** and the integrity of `registry.yaml` / `users.yaml` | The facts and the identity source are the root of trust. |\n| 🔒 **Assumed trusted** | The **model endpoint** the agent reaches over the bridge | Whatever the agent talks to for inference. |\n\n> **Found a way through a wall?** See [`SECURITY.md`](SECURITY.md) for private\n> disclosure, and for which rows above are documented limitations rather than\n> vulnerabilities.\n\n## Going further: the optional layers\n\nYou don't need any of this to understand Scriptorium; add a layer when your use\ncase does. Three further rungs, escalating from a real agent on the CLI to a\nproduction deployment behind nginx. Each adds the `scriptorium-agent-claude` image\n(Claude Code inside the walls):\n\n```bash\ndocker build -t scriptorium-agent-claude agent/claude   # + Claude Code\n```\n\n### 2. Claude behind the walls (CLI)\n\nSame boundary, now with a real agent inside it and network egress so it can reach\nthe model API:\n\n```bash\nexport ANTHROPIC_API_KEY=sk-ant-...                    # or CLAUDE_CODE_OAUTH_TOKEN (see Prerequisites)\n\n./scriptorium.py list --user marta                           # what may marta see? → finance\nSID=$(./scriptorium.py plan --user marta --select finance)\n./scriptorium.py mount \"$SID\" --image scriptorium-agent-claude --network bridge --pass-env ANTHROPIC_API_KEY\n\n./scriptorium.py run \"$SID\" -- claude -p \"what is the travel reimbursement limit?\"\n./scriptorium.py run \"$SID\" -- claude                        # or a full interactive session\n./scriptorium.py unmount \"$SID\"\n```\n\nClaude answers from `/workspace/knowledge/finance`, its entire universe for this session,\nand the **poisoned** concept it can read (the trap from quickstart 1) leads a\nreal, obedient agent to the same dead end. `--pass-env` forwards the credential\n**by name only** (never argv, scope files, or the audit log); `--network bridge`\nis the one deliberate hole in the wall, so restrict egress at the firewall if\nexfiltration of granted content matters.\n\n> ⚠️ **Never run the agent on the host.** The walls contain only what executes\n> *inside* them. An agent process on the host sits right next to the unprotected\n> `example-collections/` and can read all of it. The agent goes inside. That is\n> the product.\n\n### 3. The REST interface (browser console)\n\n`interfaces/rest/api.py` wraps the kit for a web frontend: an ASGI app (Starlette on uvicorn), the\nsame server in dev and prod. Its defaults are the agent path\n(`scriptorium-agent-claude`, bridge network).\n\n```bash\npip install .[server]                                  # transport tier: starlette + uvicorn\n\npython3 -m interfaces.rest.api --pass-env ANTHROPIC_API_KEY            # full agent sessions, or:\npython3 -m interfaces.rest.api --image scriptorium-agent --network none   # no key — walls only\nopen http://127.0.0.1:8000                             # the console (interfaces/rest/web/index.html)\n```\n\nIn the console: type `marta` as the token → **connect** (only `finance` appears;\nexisting sessions rehydrate from the server, surviving reloads) → **start session**\n→ prove the walls with the fixed probe buttons (`ls /workspace/knowledge`; the poisoned path\n→ *No such file or directory*; a write → *Read-only file system*). With a key, ask\nClaude a question and the answer **streams in token-by-token** (Server-Sent\nEvents). Every button is a fixed, server-side probe. The interface takes agentic\nqueries, never arbitrary commands. Everything you click lands in\n`scopes/audit.jsonl`.\n\nBy hand (`marta` is the dev stand-in token, the principal's name):\n\n```bash\nAUTH='Authorization: Bearer marta'\ncurl -H \"$AUTH\" localhost:8000/api/collections\n\nSID=$(curl -s -X POST -H \"$AUTH\" -d '{\"selection\":[\"finance\"]}' \\\n      localhost:8000/api/sessions | python3 -c 'import json,sys; print(json.load(sys.stdin)[\"scope_id\"])')\n\ncurl -X POST -H \"$AUTH\" -d '{\"q\":\"what is the travel limit?\"}' \\\n     localhost:8000/api/sessions/$SID/query            # buffered JSON; add \"stream\":true for SSE\ncurl -X POST -H \"$AUTH\" -d '{\"check\":\"poison\"}' \\\n     localhost:8000/api/sessions/$SID/probe            # fixed wall probe (list|poison|write)\ncurl -X DELETE -H \"$AUTH\" localhost:8000/api/sessions/$SID\n```\n\nDenied and nonexistent are both `404`, a foreign session id returns the exact body\na missing one does, and a background reaper ends expired sessions. `/api/sessions` lists your\n**live** sessions: an ended one is gone from `scopes/` and lives on in `audit.jsonl`.\n\n### 4. Production: nginx + uvicorn\n\nA serious deployment shouldn't *assume* a reverse proxy, so the kit ships one\n(`interfaces/rest/edge/`, SPEC §12.7): the ASGI app under uvicorn behind **nginx**, which\nterminates TLS (you add certs), rate-limits, and is configured to stream `/query`\ncorrectly.\n\n```bash\nexport SCOPE_ROOT=$(pwd)                               # the repo's absolute host path (required)\nexport CLAUDE_CODE_OAUTH_TOKEN=sk-ant-...              # or ANTHROPIC_API_KEY; omit for walls-only\ndocker compose -f interfaces/rest/edge/docker-compose.yml up --build\nopen http://localhost:8080                             # the console, through nginx\n```\n\nThe app orchestrates **host** docker (it hands the daemon host paths to bind-mount\nread-only), so it must see the repo at that same absolute path (`${SCOPE_ROOT}`)\nand talks to the host daemon over `/var/run/docker.sock`. That socket is\nhost-root-equivalent, so **only nginx is published, never the app**,\n`interfaces/rest/edge/README.md` has the full rationale and the TLS block.\n\n---\n\n## Reference: for when you're building on it\n\nEverything below is reference, not required reading: how to verify the walls, the\nseams you can swap, the config surface, the command list, and real OIDC logins.\nReach for a section when your use case needs it.\n\n### Verifying: two layers\n\nThe security story has two halves, each with its own automated check:\n\n- **`python3 -m pytest -q`: the decision, and the contracts.** Pure tests, no\n  docker: `plan()`'s who-sees-what logic (deny-by-default, invisibility,\n  intersection abort, version pinning, expiry. This part *is* the security model,\n  so a failure is a stop-the-line event), OIDC claim mapping, the frozen `/probe`\n  argv, the agent-command seam, config precedence, and OKF conformance of the\n  fixtures. `tests/test_plan_properties.py` fuzzes the decision core's invariants\n  with Hypothesis: deny-by-default, invisibility, concreteness, no version-probing.\n- **`./demo.sh`: the enforcement.** The executable acceptance proof (SPEC §9.2):\n  builds the base image if needed, then runs the six normative steps against real\n  containers, printing PASS/FAIL for each. `tests/test_adversarial.py` (SPEC §9.4)\n  goes one step further. It turns an agent loose that *obeys* the prompt injection\n  and asserts the escape dead-ends on `ENOENT`, so \"we red-team the walls on every\n  commit\" is a fact the CI enforces, not a claim.\n\n### Modular architecture\n\nScriptorium is a small set of **seams** around an unchangeable core. You swap the\nparts that vary between deployments; you never touch the decision logic.\n\n| Seam | Default | Swap it by | The fixed part |\n|------|---------|-----------|----------------|\n| **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 |\n| **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 |\n| **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 |\n| **Knowledge** | the `example-collections/` fixtures | point `registry.yaml` at your own read-only tree | mounted `:ro`; never written from code |\n\nWhat is **not** a seam, by design (SPEC §11, \"anti-scope\"): the `plan()` decision\ncore, the realizer's docker flags (`--read-only`, `--cap-drop ALL`,\n`no-new-privileges`, `--pids-limit`, …), and the number of images: two, one base\nand one agent. A new agent is a new image + config, **never** a plugin system or\nan LLM SDK inside the core.\n\n### Configuration\n\nFlags are always sufficient; a `scope.yaml` (gitignored, copy\n`scope.example.yaml`) is optional convenience. Precedence is **flag > file >\nbuilt-in default**; unknown keys are rejected loudly; `--config PATH` points either\nentry point at a different file. Three blocks:\n\n```yaml\napi:                            # the REST interface (python3 -m interfaces.rest.api)\n  host: 127.0.0.1\n  port: 8000\n  # oidc_issuer: https://kc.example/realms/corp   # unset = dev stand-in\n\nagent:                          # what /query runs inside the walls  ← the main seam\n  image: scriptorium-agent-claude\n  command: [\"claude\", \"-p\", \"{q}\"]              # argv template; {q} = the question\n  options: {model: {flag: --model}, effort: {flag: --effort}}   # /query knob; {flag, default?, choices?}\n  network: bridge\n  pass_env: [CLAUDE_CODE_OAUTH_TOKEN]           # forwarded name-only\n\ncli:                            # the five verbs (./scriptorium.py)\n  image: scriptorium-agent      # walls-only base by default\n  network: none\n  ttl: 8h\n```\n\nSwap the agent without touching code: point `agent.image` at another prebuilt tag\nand `agent.command` at its CLI (e.g. `[\"python\", \"/agent.py\", \"{q}\"]`). Most keys\nmirror a CLI flag 1:1; `agent.command` / `agent.options` are config-only.\n\n### Repo structure\n\n```\nscriptorium.py           # the entire kit — plan(), the realizer, the five verbs\ninterfaces/              # protocol adapters over the kit — one dir per interface\n  rest/                  #   the REST/SSE interface\n    api.py               #     transport, identity, lifecycle; calls scriptorium.py, never docker\n    web/index.html       #     the static console it serves\n    edge/                #     production reference: nginx + uvicorn compose (§12.7)\nregistry.yaml            # collections → path + allowed groups   (the facts)\nusers.yaml               # principal → groups (local IdP stand-in)\nexample-collections/     # sample OKF bundles: <collection>/<version>/…  (swap for your own tree)\nscopes/                  # runtime: <scope_id>.json frozen decisions + audit.jsonl\nagent/                   # the agent catalog — one dir per provider: Dockerfile + agent.yaml + system.md\n  base/                  # scriptorium-agent — the walls-only foundation every agent inherits\n  claude/                # scriptorium-agent-claude — the shipped, red-teamed default\n  codex/  gemini/        # built + verified manually (not CI); see agent/README.md\ndev/keycloak/            # local OIDC fixture (compose + realm)\ntests/                   # pure suite: the decision core, contracts, OKF conformance\nscope.example.yaml       # documented config template → copy to scope.yaml\ndemo.sh                  # the executable acceptance proof (§9.2)\nSECURITY.md              # disclosure policy; what is (and isn't) a vulnerability\ndocs/oidc.md             # OIDC walkthrough\nSPEC.md                  # single source of truth\nCLAUDE.md                # coding-agent conventions\npyproject.toml           # packaging; console scripts scriptorium / scriptorium-rest\nLICENSE                  # Apache-2.0\n```\n\n### Command reference\n\nThe five verbs (run from source as `./scriptorium.py`, or `scriptorium` once installed):\n\n```\nlist    --user U                        # collections U may see\nplan    --user U --select C1[,C2...]    # freeze the decision → scope_id\nmount   SCOPE_ID [--image IMG] [--network none|bridge] [--pass-env VAR]...\nrun     SCOPE_ID [-- CMD ...]           # default: bash, cwd /workspace\nunmount SCOPE_ID                        # ends the session: container + record go (audit.jsonl remains)\n```\n\nThe REST interface (`scriptorium-rest`, or `python3 -m interfaces.rest.api`):\n\n```\nGET    /api/collections                          # what the caller may select\nPOST   /api/sessions        {selection}          # plan + mount → scope_id\nPOST   /api/sessions/{id}/query  {q[,model,effort,stream]}   # run the agent inside the walls\nPOST   /api/sessions/{id}/probe  {check}         # fixed wall probe: list | poison | write\nDELETE /api/sessions/{id}                        # unmount\n```\n\n### Real logins with OIDC\n\nThe dev token is a stand-in. Point the interface at any OIDC identity provider and\nbearer tokens become real, IdP-validated logins, with groups coming from token\nclaims instead of `users.yaml`:\n\n```bash\ndocker compose -f dev/keycloak/docker-compose.yml up -d    # local fake-company IdP\npython3 -m interfaces.rest.api --oidc-issuer http://localhost:8080/realms/scope [agent flags]\n```\n\nNew to OIDC? `docs/oidc.md` walks the whole flow: what a token is, how validation\nworks, and an end-to-end recipe with curl.\n\n## Where this sits\n\n\"An AI agent must only touch knowledge it's authorized to\" is a crowded problem in\n2026, but the field overwhelmingly solves it by **filtering at retrieval time**: a\nfine-grained-authorization service checks each query against a shared store and returns\nonly the chunks the caller is allowed to see (the FGA / ReBAC-for-RAG pattern). That\nguards against an untrusted *user*, and trusts the retrieval layer to apply the filter\nevery single time.\n\nScriptorium makes the opposite bet: **enforce by construction, not by filtering.**\nAuthorization runs once, and its output *is* the agent's filesystem: ungranted knowledge\nhas no path, so there is nothing for a prompt-injected agent to retrieve, reach around,\nor be talked into asking for. It assumes the *agent* is the adversary, and holds even if\nthat agent gains arbitrary tool and code execution.\n\nNone of this is invented here, and that's the point. It's **object-capability\nsecurity** ([no ambient authority](https://en.wikipedia.org/wiki/Object-capability_model):\nyou can't act on what you were never handed) and **[Plan 9's per-process\nnamespaces](https://9p.io/sys/doc/names.html)** (each process runs in\nits own private view of the world), applied to LLM agents. Scriptorium's contribution is\nthe packaging: a small, auditable capability→mount projection for the agent-knowledge case.\n\n**What it is not:** a policy engine. It doesn't do dynamic, graph-shaped, per-user\npermissions the way OpenFGA / OPA / Cedar do. A session's grant is frozen the moment\nit's minted. That layer belongs *above* Scriptorium and can hand it the resolved grant;\nScriptorium is the enforcement floor, not the policy brain. It's complementary to MCP the\nsame way: MCP scopes the *tools* an agent may call; Scriptorium scopes the *knowledge*\nthose tools can read.\n\n## Where the idea comes from\n\nThe seed was an enterprise vision: **self-service departmental knowledge for\nagents, granted by construction**, surfacing curated, per-department knowledge to\nAI agents where the *authorization is the delivery mechanism*, not a filter bolted\non afterward. The sample bundles here are real\n**[OKF](https://github.com/GoogleCloudPlatform/knowledge-catalog/tree/main/okf)**,\nGoogle's Open Knowledge Format v0.1: markdown-with-frontmatter for agent-curated\nknowledge, and `tests/test_okf.py` enforces that conformance.\n\nBut the kit is deliberately broader than OKF: it walls off *any* read-only document\ntree. The fixtures just speak the format the platform vision was named for.\nScriptorium is the minimum reusable core (`registry.yaml`, `scope.json`, the pure\n`plan()`) meant to be battle-tested locally before any platform (chat SaaS, MCP\nserver) wraps it.\n\n## Versioning\n\nTwo numbers, on purpose. `scriptorium --version` prints both:\n\n```\nscriptorium 1.0.0 (SPEC v1.30)\n```\n\n- **`1.0.0` is the release**, plain semver. What it promises is that the contracts\n  this tool is *for* won't change under you without a major bump: the five verbs\n  (`list`/`plan`/`mount`/`run`/`unmount`), their exit codes (SPEC §7), the wall flags\n  (§6.1), the invisibility rule (§5.2 rule 2: denied reads exactly like nonexistent),\n  and the REST shapes (§12).\n- **`SPEC v1.30` is the design-revision counter**, the 30th amendment to `SPEC.md`,\n  logged in [CHANGELOG.md](CHANGELOG.md). It moves whenever a design decision is\n  recorded, including documentation-only ones, so it is *not* a release number.\n  `SECURITY.md` asks you to cite it in a report, which is why the runtime prints it.\n\nWithin `1.x`, **config keys may be added**. A `scope.yaml` gaining a key is additive,\nnot breaking, and existing files keep working. Removing or repurposing one, changing an\nexit code, or weakening a §6.1 flag would be a major bump.\n\nBefore 1.0 this was reviewed adversarially five times (see `CHANGELOG.md` v1.23–v1.30);\n1.0 means those findings are closed and the boundary is one to build on, not that the\nproject is finished. `SPEC.md` §11 is explicit about what it will never grow into.\n\n## License\n\n[Apache-2.0](LICENSE). The patent grant matters for a security tool; all\ndependencies are permissive.\n",
  "bytes": 27880,
  "sha": "c714605d9b29454adc200eb769ae487ba89406efe6b536473262d4c82aa93b7f",
  "repo_slug": "joaogmauricio/scriptorium",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/okf_joaogmauricio_scriptorium_example_collec_6c6f68b1/readme"
}