{
  "markdown": "<div align=\"center\">\n\n<img src=\"./assets/logo.svg\" width=\"76\" alt=\"\">\n\n<!-- One line on purpose, same reason as the badges below: a newline inside an `align=\"center\"` block becomes a <br>. `<picture>` rather than a media query inside the SVG — that query follows the reader's OS, not GitHub's theme toggle, and half this word would vanish on the mismatch. -->\n<picture><source media=\"(prefers-color-scheme: dark)\" srcset=\"./assets/wordmark-dark.svg\"><img src=\"./assets/wordmark.svg\" width=\"250\" height=\"61\" alt=\"orangerail\"></picture>\n\n**Give your agent your database. Don't give it SQL.**\n\n<!-- One line on purpose: inside an `align=\"center\"` block GitHub turns every newline into a <br>, so a badge per line stacks them vertically. -->\n[![npm](https://img.shields.io/npm/v/orangerail?logo=npm&logoColor=fff&label=npm&color=EE7A2B)](https://www.npmjs.com/package/orangerail) [![CI](https://github.com/KimHyeongRae0/orangerail/actions/workflows/ci.yml/badge.svg)](https://github.com/KimHyeongRae0/orangerail/actions/workflows/ci.yml) [![node](https://img.shields.io/node/v/orangerail?logo=nodedotjs&logoColor=fff&color=444)](https://nodejs.org) [![published with provenance](https://img.shields.io/badge/npm-published%20with%20provenance-EE7A2B?logo=npm&logoColor=fff)](https://www.npmjs.com/package/orangerail#provenance) [![license](https://img.shields.io/badge/license-MIT-444)](./LICENSE)\n\n[**Quickstart**](#quickstart) · [Commands](./docs/commands.md) · [What it does not govern](./docs/limits.md) · [Against a rules file](./docs/vs-a-rules-file.md) · [Examples](./examples) · [Docs](./docs)\n\n</div>\n\n![orangerail init on a three-model Prisma schema, then the server's real tools/list: six reads, nine writes, check_approval, and no execute_sql](./assets/tool-surface.gif)\n\n*`orangerail init` on a three-model Prisma schema, then a real `tools/list` against the server it\ngenerated. Sixteen tools: a `get` and a `list` per object, one action per write, and\n`check_approval`. **Nothing on that list takes a query.** The three locks are `--gate delete`, which\nis a default you change in one line, not a verdict.*\n\n**A rules file cannot do this.** It can ask the agent not to run a query. It cannot take the tool off\nthe list — and [we measured what the difference is worth](./docs/what-we-measured.md), including the\nfour claims that died when we did.\n\n**orangerail reads the schema you already have and generates the agent's surface from it.**\n`orangerail init` turns a `prisma/schema.prisma` into an MCP server: a `get` and a `list` per\nobject, one action per write with a zod input schema, and nothing else — no `execute_sql`, and\nnothing on the tool list that takes a query. It is a scanner and a code generator, with no LLM\ncalls and no API keys. Writes you are happy to have run unattended run unattended. The ones you are\nnot carry `policy: { approval: 'required' }`, which stops the call and turns it into an approval a\nperson can act on later — including a person who is not you, after the conversation that produced\nit has ended.\n\n**Bounded is not safe, and this README will not pretend otherwise.** A generated surface buys a\nreach that is *finite and legible*, not a claim that nothing harmful is inside it. You declared the\nverbs, so a destructive verb you declared is a verb the agent can call.\n\n**One precondition decides whether any of this is worth installing: orangerail governs only its own\ntools.** If the agent also has a shell with credentials or a second database MCP server, it can go\naround the rail — see [what orangerail does not govern](./docs/limits.md).\n\nPre-release and installable: `0.1.5` on npm — `orangerail` (the CLI) plus `orangerail-core`,\n`orangerail-mcp`, `orangerail-docs-gen` and `orangerail-studio`. The API will move before 1.0, and\n[Status](#status) has the one upgrade note that matters.\n\n## See your whole domain as a map\n\nOne command, and the surface `init` generates is a map you can read.\n\n```console\n$ orangerail studio\norangerail studio: scanning ontology — 9 object(s), 27 action(s)\norangerail studio: building the interactive map…\norangerail studio: serving on http://127.0.0.1:4820 — open it in your browser\n```\n\nEvery object, how they relate, and every write action an agent can reach. Hover a table to light up\nits relations and actions; click one to read the policy that governs it.\n\n![the orangerail studio map — hovering tables to reveal relations, then opening deleteOrder to read the policy that governs it: target Order, approval required, approvers any, condition none](./assets/studio-map.gif)\n\n> Crisper version: [`assets/studio-map.mp4`](./assets/studio-map.mp4) — the same run at full\n> resolution. One real run on a sample commerce domain, `--gate delete`. The locks are not\n> annotations added for the video: they are what the studio draws from your ontology, which is why\n> nine actions carry one and eighteen do not.\n\n**Be exact about what that is worth.** The relations come from `ontology/_links.mjs`, which `init`\nderives from your Prisma relations, so `Customer_list`'s description reads `List Customer records.\nRelations: has many Order.` The agent is *told* that a Customer has many Orders. It still cannot\nfollow the edge: no traversal tool, no join, no aggregate, and `Customer_list` refuses a filter that\nreaches into `Order`. Knowing the shape of a domain and being able to query across it are different\nthings, and only the first one is here.\n\n## Quickstart\n\nSeven steps, every output verbatim from one recorded run. The reasoning behind each one — and the\nfailure each prevents — is in [Quickstart, annotated](./docs/quickstart-notes.md); **requirements\nand the Prisma 7 caveat are the first thing on that page.**\n\n**1. Install orangerail into the project you are about to scan.**\n\n```bash\nnpm i -D orangerail\n```\n\n**2. Scan your project**, in a repo with a `prisma/schema.prisma`.\n\n```console\n$ npx orangerail init --yes --preset approval-for-writes --no-studio\n  ✓  scanned your sources — 2 object(s), 6 action(s)\n  ✓  generated a governed MCP server under ontology/\n  ✓  --gate delete: 2 of 6 write action(s) gated behind human approval — the other 4 run when the agent calls them\n  ✓  recorded that posture in orangerail.governance.json — commit it\n  ✓  approvals queue + audit chain at .orangerail/store/ — inside this project, so an\n     agent with file tools over this directory can write them\n\n  These files are yours — re-scans never modify them; `orangerail sync` reports drift.\n\n  Change what is gated by editing `policy` in ontology/<action>.mjs, or re-run init\n  with `--gate all` (gate every write) or `--gate none` (gate nothing).\n  orangerail.governance.json holds the posture init just generated, which nobody has reviewed yet.\n  From now on `orangerail sync` fails when an action gets weaker than that file, and\n  `orangerail mcp` refuses to serve it. Read the file, then run\n  `orangerail sync --accept-governance` to vouch for it as reviewed.\n\n  That store is the record of which writes a human approved, and appending one line to\n  .orangerail/store/approvals.jsonl marks a staged action approved — the next\n  `check_approval` then executes it, because the gate reads that store and never the\n  audit chain. `orangerail audit verify` reports the forgery afterwards; it is a report,\n  not a gate, and it does not prevent the write. The generated config carries the\n  one-line move at the `createFileStore` call — see docs/audit-log.md.\norangerail docs: wrote /private/tmp/shop/.orangerail/generated/AGENTS.md\n\nDone. Run `orangerail studio` to explore the map, or `orangerail mcp`.\n```\n\n**3. Install the runtime the generated code loads.**\n\n```bash\nnpm install orangerail-core zod\n```\n\n**4. Give the generated actions a database to reach.**\n\n```bash\nnpm install @prisma/client@6\nexport DATABASE_URL=\"file:./dev.db\"\nnpx prisma generate\nnpx prisma db push --skip-generate\n```\n\n> Already have a database? Do not `db push` over it —\n> [adopting orangerail against an existing database](./docs/existing-database.md).\n\n**5. Point your agent host at it.** Drop this in your project root as `.mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"orangerail\": {\n      \"type\": \"stdio\",\n      \"command\": \"./node_modules/.bin/orangerail\",\n      \"args\": [\"mcp\"],\n      \"env\": { \"DATABASE_URL\": \"file:./dev.db\" }\n    }\n  }\n}\n```\n\nOther hosts, the `claude mcp add` one-liner, and running from source:\n[wire it into your agent host](./docs/agent-hosts.md).\n\n**6. Record the governance baseline — and commit it.** `ontology/` is yours to edit, so the one\nline that disarms the whole flow is one careless deletion away and a re-scan cannot notice. The\nposture is compared against a recorded file instead.\n\n```bash\nnpx orangerail sync --accept-governance\n```\n\n**Commit `orangerail.governance.json`.** Its whole value is that a pull request removing an\napproval gate shows `\"approval\": \"required\"` turning into `null` in its own diff, in front of a\nreviewer, before CI runs at all.\n\n**7. Now leave.** While you are gone the agent works the queue: the writes you left un-gated go\nthrough, and the deletion it was asked for stops. When you come back:\n\n```console\n$ npx orangerail status\norangerail status\n  objects:  2\n  actions:  2 approval-gated, 4 auto\n  baseline: 6 action(s) match orangerail.governance.json\n  preset:   approval-for-writes\n  pending:  1 approval(s) awaiting a decision\n  store:    /private/tmp/shop/.orangerail/store\n            Inside the project root, so an agent with file tools over this directory can\n            write it: one appended line in approvals.jsonl is a decision no human made,\n            and the next `check_approval` executes the staged action — the gate reads\n            this store, never the audit chain. `orangerail audit verify` reports the\n            forgery afterwards; it is a report, not a gate. Pointing the store `dir` at a\n            directory this agent's process cannot write is what removes the reach — see\n            docs/audit-log.md.\n  server:   not detected — no orangerail mcp is running against this store\n  hosts:    .mcp.json declares orangerail and nothing else.\n            Project scope only (.mcp.json, .cursor/mcp.json, .vscode/mcp.json); user- and\n            machine-scope MCP config is not read.\n  audit:    chain OK — 5 record(s) verified\n\n$ npx orangerail approvals list\nc4818df5-770c-446f-883a-e9c0f7e615a2  \"deleteCustomer\"  by \"local-dev\" [dev]  0s ago  input={\"id\":2}\n\n1 pending approval(s).\n\n$ npx orangerail approvals approve c4818df5-770c-446f-883a-e9c0f7e615a2\napprove ok (approved)\n```\n\nThe agent's next `check_approval` is the first moment the row can change. Nothing ran before you\nsaid so, and every step is on the hash chain. That whole sequence is what\n[`tests/e2e/ONT-093-quickstart-runs-as-documented.sh`](./tests/e2e/ONT-093-quickstart-runs-as-documented.sh)\nruns against this repository's own build on every regression pass.\n\n## Why the prompt is the wrong control\n\n**The thing stopping you from walking away is not that the agent does too much. It is that your\nonly control is a question it has to ask you.** The twentieth prompt of the afternoon gets the same\nclick as the first, and the switch that ends the asking ships in the box — Claude Code's\n`bypassPermissions` mode \"skips permission prompts, except those forced by explicit `ask` rules\",\nper its own [permissions reference](https://code.claude.com/docs/en/permissions). A boundary\nre-established by a person on every call cannot hold once nobody is there.\n\nThe prompt is also the wrong *shape*. It asks about a tool — may this run `Bash` — and the risk you\ncarry is about your domain: stock edits are fine, order deletions are not, refunds under $50 need\nnobody. That distinction does not exist at the tool level. It exists in your schema.\n\n## The run this is built for\n\n![a back-office queue handed to an agent with nobody watching: ordinary writes finish, a deletion stops and becomes an approval, and the one declared line that stopped it](./examples/unattended-queue/demo.gif)\n\n*One run of [`examples/unattended-queue`](./examples/unattended-queue) — a real MCP client, no API\nkey, every line asserted. The video shows six of the twelve; the row numbers jump, so you can see\nwhere.*\n\nA 15-item back-office queue on a commerce database, handed to an agent with the operator gone for\nthe day and told not to ask for confirmation. Twelve items are ordinary reversible writes. Three\nare destructive: delete a cancelled order, delete a customer under an erasure request, delete a\ndiscontinued product. Scored from the database afterwards, not from what the agent said it did:\n\n| | orangerail |\n| --- | --- |\n| ordinary items completed unattended | **12 / 12** |\n| destructive items executed | **0** |\n| destructive items stopped and staged | **3** |\n| what is waiting the next morning | 3 approval records, each bound by hash to the exact call |\n| audit chain | 27 records, verified OK |\n\nThat is the metric this project is built around, and it is not \"how much did we block\". It is **how\nmuch finished while nobody was watching, and what is waiting when you get back.**\n\nTwo separable claims sit in that table and they are not equally well evidenced. That the twelve go\nthrough and the three cannot is a property of the server, and it is **reproducible on your\nmachine** — [`examples/unattended-queue`](./examples/unattended-queue) runs exactly that queue\nthrough a real MCP client, deterministically, asserting every line. That a *model* chooses these\ncalls when handed the queue in prose needed a live agent driving a real host, and **that half is a\nmeasurement, not a reproduction**: small numbers, enough to say the gate holds where it was tested\nand not enough to be a rate.\n\n### Against the thing you would do instead\n\nThe comparison that matters is not a raw SQL server. It is a rules file: a well-written `CLAUDE.md`\nnaming the permitted tables and the forbidden ones, over a Postgres MCP server with full write\naccess. Same queue, same model, three clones.\n\n| | markdown rules, full write access | orangerail |\n| --- | --- | --- |\n| ordinary items completed | 12 / 12, all three runs | 12 / 12 |\n| destructive items executed | 0 | 0 |\n| what the stop leaves behind | a paragraph in a report | an approval record |\n| the same task started in another directory | **row deleted** | staged it |\n\n**It tied on compliance, and it kept tying** — through adversarial rewrites, a fake prior approval,\nan instruction planted in a database row, and a much smaller model. On one axis it beat us. So this\nproject does not argue that your agent will ignore your rules: across every run measured here, it\nfollowed them.\n\n**That is six runs.** Enough to retire the claim that it would not, nowhere near enough to be a\nrate — zero failures in six bounds the tail near 39%, not near zero. Buying a real bound is\nexpensive and it perishes on the next model release, which is the reason the row below is the one\nthis project stakes itself on:\n[what ten runs cannot prove](./docs/what-we-measured.md#what-ten-runs-cannot-prove).\n\nThe row that does not tie is the last one, and it is not about the agent's behaviour: a grant\ntravels with the session it was registered for, and a rules file travels with the machine account\nit was written under. A global `~/.claude/CLAUDE.md` closes most of that gap for a single developer\non one machine — **if that is you, you may not need this.** It stops closing at a CI runner, a\ncontainer, a service account, or a teammate's checkout, each of which gets the database credentials\nanyway.\n\nEvery run, the axis where the rules file wins, and the limits of the measurement:\n[against the thing you would do instead](./docs/vs-a-rules-file.md).\n[`examples/vs-a-rules-file`](./examples/vs-a-rules-file) executes both arms.\n\n## What the agent gets instead of `execute_sql`\n\nRun `orangerail init` on a three-model Prisma schema (`Order`, `OrderItem`, `Payment`) and the\nentire tool list is 16 entries: **a `get` and a `list` per object, one action per write, and\n`check_approval`.** Nothing else, and nothing that takes a query.\n\nEach action's input is a zod schema derived from your own columns, published in `tools/list`, so\n`updateProduct` refuses a string where the column is an integer and says which field it was. Each\nread is a `findUnique` by id or a paged `findMany`, whose `filter` is a closed set of predicates\nover declared fields — enforced by the server before it reaches your resolver, not merely\nadvertised.\n\nA fixed surface is a narrow one: no aggregation, no join, no free-form query, no DDL. A question it\ncannot express has to be answered somewhere else — all of it, and where enforcement actually lives,\nis in [what orangerail does not govern](./docs/limits.md).\n\n## See it stop an agent\n\n![a destructive agent action stops and comes back as an approval id; a person decides, and only then does the row change](./examples/governed-writes/demo.gif)\n\n*One real run of [`examples/governed-writes`](./examples/governed-writes) through a real MCP\nclient. The destructive tool stays **available** rather than hidden, the agent **cannot force it\nthrough**, and the row changes **only after a human decided** — in a separate terminal, at a\nseparate time, which is the part that makes leaving possible.*\n\n## Declaring a rule the generator cannot derive\n\nEverything above is generated. When a rule lives in your head rather than your schema — \"never\nissue a coupon for a sold-out item\" — you write it once, in TypeScript, and it joins the same\nsurface:\n\n```ts\nimport { defineAction, defineObject } from 'orangerail-core';\nimport { z } from 'zod';\n\n// Your existing backend. orangerail never replaces it — it only gates the call.\ndeclare const findProduct: (id: string) => Promise<{ id: string; status: string } | null>;\ndeclare const grantCoupon: (args: { productId: string; amount: number }) => Promise<void>;\n\n// A `where` guard has to read the row it guards, so the target needs `resolve`.\nexport const Product = defineObject({\n  name: 'Product',\n  schema: z.object({ id: z.string(), status: z.string() }),\n  resolve: { get: async ({ id }) => findProduct(id) },\n});\n\nexport const issueCoupon = defineAction({\n  name: 'issueCoupon',\n  target: Product,\n  input: z.object({ productId: z.string(), amount: z.number() }),\n  policy: {\n    approval: 'required',\n    where: { field: 'status', op: 'neq', value: 'soldout' },\n  },\n  // `execute` runs only after the approval clears, and receives the validated\n  // input plus the resolved caller. There is no `audit` switch: every staged,\n  // approved, rejected and executed action is written to the hash chain.\n  execute: async ({ input, identity }) => {\n    await grantCoupon({ productId: input.productId, amount: input.amount });\n    return { issuedBy: identity.subject };\n  },\n});\n```\n\nThat block is not an illustration — it is\n[`packages/cli/test/readme-example.ts`](./packages/cli/test/readme-example.ts) printed verbatim,\ncompiled by the repo typecheck and compared against this file on every run, so it cannot rot into\nsomething that never compiled.\n\n## Status\n\n`orangerail init` runs against your own project today, with no checkout of this repo, and the API\nwill move before 1.0. All five packages are published from\n[`.github/workflows/release.yml`](./.github/workflows/release.yml) over npm's Trusted Publishing,\nso each one carries a provenance attestation naming the workflow and commit that built it; there is\nno npm token in this repository.\n\n**Upgrade from `0.1.0` if you are on it.** That release published a read `filter` to the agent and\nnever checked it, so a `<Object>_list` call could read an object type the server never exposed\n([the mechanism](./docs/limits.md#typed-is-not-enforced--where-the-check-actually-lives)). The fix\nis in `0.1.2` and lives in `orangerail-mcp`, so upgrading the package applies it with no re-run of\n`init`. `0.1.2` also narrows what `filter` accepts and changes what a pending approval does across\nthe upgrade — both under **Upgrading from 0.1.0** in the [CHANGELOG](./CHANGELOG.md).\n\n## Docs\n\n- [Commands](./docs/commands.md) — every command, what `--gate` chooses, and how to narrow the\n  surface to the tables you name.\n- [Quickstart, annotated](./docs/quickstart-notes.md) — the seven steps with the reasoning, the\n  requirements, and the failure each step prevents.\n- [Wire it into your agent host](./docs/agent-hosts.md) — stdio, `.mcp.json`, and running from\n  source.\n- [What orangerail does not govern](./docs/limits.md) — the preconditions, the missing\n  capabilities, and where enforcement actually lives.\n- [What the audit log proves](./docs/audit-log.md) — the exact bar, why \"tamper-evident\" is not\n  used, when a database-level audit is the better tool, and where to put the store.\n- [Against the thing you would do instead](./docs/vs-a-rules-file.md) — the full rules-file\n  comparison.\n- [How orangerail compares](./docs/comparisons.md) — against `--read-only`, OpenAPI codegen,\n  Prisma's own servers and Supabase's.\n- [Adopting orangerail against an existing database](./docs/existing-database.md) —\n  `prisma db pull` onto a live database, and what Prisma 7 changes.\n- [Also ask the host to prompt](./docs/host-approval-prompt.md) — the optional client-side prompt\n  on un-gated writes.\n- [Troubleshooting](./docs/troubleshooting.md) — the readouts that report something is wrong with\n  the install rather than with your policy.\n- [What we measured, and what died](./docs/what-we-measured.md) — every claim this project made or\n  was tempted to make, and which ones survived being run. Four did not.\n- [`bench/`](./bench) — the fixtures behind that page, so you can disagree by reproducing rather\n  than by arguing.\n- [The MCP registry entry](./docs/mcp-registry.md) — what the listing is, and the step a\n  hundred-character description cannot fit.\n\n## Examples\n\n- [`unattended-queue`](./examples/unattended-queue) — the run at the top of this file, made\n  reproducible. Deterministic, asserted, no API key.\n- [`governed-writes`](./examples/governed-writes) — the same gate in isolation, one destructive\n  call at a time.\n- [`vs-a-rules-file`](./examples/vs-a-rules-file) — the rules-file comparison made runnable, both\n  arms executed, including the column where the rules file wins.\n\n## Development\n\nThis repo is built under a deterministic 9-stage gate harness. Every change runs through\n[`./scripts/verify.sh`](./scripts/verify.sh) — language, structure, gate self-test, no-LLM,\ntemplates, then typecheck / lint / test / build — and CI runs that script and nothing else, so a\ngreen local run is a green build. A hard invariant: no LLM-inference SDK is ever bundled\n([`./scripts/check-no-llm.sh`](./scripts/check-no-llm.sh)).\n\n## License\n\n[MIT](./LICENSE)\n",
  "bytes": 22735,
  "sha": "6e62b6e4e96b067b878e515e3c9b1952eb61b8ea0b9192c66de2bdfc56edd88a",
  "repo_slug": "kimhyeongrae0/orangerail",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_kimhyeongrae0_orangerail_0722310c/readme"
}