All documents · Slip spec (do not ship as gateway)

Slip — implementation spec (1–2 day MVP)

Product: Slip
Date: 18 September 2026
Decision: DECISION.md
Theme: ../design/virtus-theme.md
Bind: 127.0.0.1:3100 (PM2). Public via nginx 80/443 later — do not edit nginx in this build.
Database: Postgres on localhost (127.0.0.1:5432).
Auth: email + password (bcrypt). Magic-link is a day-3 add if SMTP exists.
Not in scope: customer MCP reverse proxy, LLM key paste, token wallet, GitHub App, quantum, traces, SSO, LiveKit.

The MVP must be a working permit engine, not a brochure: interactive playground + signup + dashboard + at least one enforceable permit API (POST /v1/decide).


1. Product name and positioning

Copy
Name Slip
H1 Slip. See what the agent can do. Stop it. Prove it.
Sub Permission slips for coding agents. English rules in, signed receipt out. We never take your model keys.
One-sentence test A public blast-radius card and a signed decision receipt for your coding agent, from English rules, without becoming your MCP gateway, your LLM vendor, your IdP, or your auditor.
Kicker Not a gateway · Not an auditor · Not your LLM bill

Do not say: CryptoBook, MCP gateway, observability, control plane, AgentOps, AI Act compliant, SOC 2, quantum, 100% secure, “proves the work was done,” “makes you compliant.”

Do say: We enforce your rules on your tools. Enforcement of real GitHub/Slack/filesystem servers runs next to your agent. This site stores hashes and decisions, not chats, not keys. Receipts are logs, not legal opinions. US only, 18+.

Wordmark is text “Slip” (700–900, -0.02em) plus the Virtus live mark (design / Virtus logo.webp). Do not redraw the mark in pink.


2. What “real working core” means (acceptance)

A reviewer with no account can, in under 60 seconds:

  1. Open /.
  2. Click Example: GitHub MCP.
  3. See a blast card: red tools (delete_repo, secret-delete, …), counts, one-line blast radius.
  4. Leave the prefilled English: never delete; ask before git_push or merge; at most 20 tool calls this session.
  5. Click Run demo agent — “archive old repos”.
  6. Watch a live tape of real POST /v1/decide results: list_repos ALLOW, delete_repo DENY, git_push ASK, call counter 3 / 20.
  7. Open a receipt page /r/{id} that verifies a signature in the browser.

A signed-in user can:

  1. Register (email + password, 18+ checkbox, US-only notice).
  2. Save the policy, mint a project token, call POST /v1/decide with curl and get deny for delete_file.
  3. See that deny in the dashboard inbox.
  4. Copy an unwatermarked receipt URL on Indie (watermarked on Free).

If step 6 is a hardcoded CSS animation that never hits the permit engine, the build has failed.


3. Architecture (non-negotiable)

Browser ──► nginx (later) ──► 127.0.0.1:3100  Node (or Python)
                                  │
                                  ├─ Playground UI (static + small JS)
                                  ├─ POST /v1/compile     English → YAML (deterministic)
                                  ├─ POST /v1/decide      PDP  {allow|deny|ask} + receipt event
                                  ├─ GET  /mcp/demo       Toy MCP (tools we own, no customer auth)
                                  └─ Postgres             policies, users, hashed tokens, receipts
Piece Where Allowed to see
Policy compiler This VPS English + YAML. No keys, no prompts.
PDP /v1/decide This VPS tool, args_hash, call_count, policy_id, project token.
Toy MCP /mcp/demo This VPS Fake in-memory repo tools. No customer secrets.
PEP sidecar User machine, later Real tools, real keys. Document the curl contract; do not build a full IDE plugin in 2 days.
Receipt ledger This VPS Tool name, decision, args hash, policy hash, signature. Never prompt bodies or raw args.

Forbidden routes:

Spend: cap tool-call counts and named denies. A UI “USD estimate” is allowed only as usd_estimate the client sent, labeled “reported by your sidecar — not your provider invoice.” Default playground: no dollar ticker.


4. Stack

Layer Choice Why
Runtime Node 20+ (already on box) One process, PM2, matches other Virtus apps. Python FastAPI is acceptable if the builder is faster in it. One language.
HTTP Express or Fastify (Node) / FastAPI (Python) Host + JSON API + static.
DB Postgres 127.0.0.1:5432, database slip Already running. SQLite only if Postgres role creation is blocked — prefer Postgres.
Auth email + password, bcrypt cost 12, session cookie HttpOnly; Secure; SameSite=Lax 1–2 day reliability.
Signing Ed25519 (server key in env SLIP_SIGNING_SEED). Public key on /r/ pages Receipt verify in-browser with noble-ed25519 or WebCrypto if possible; else server /v1/receipts/:id/verify.
Hash SHA-256 hex args_hash = sha256(canonical_json(args)).
Email none in v1 Password reset = “email us” stub.
Payments stub Plans in DB (free / indie / studio). Stripe Checkout is day-3. Pricing page shows prices; “Start free” → signup. Do not fake a charged card.
Process PM2 name slip, HOST=127.0.0.1, PORT=3100 Cluster not required for MVP.
Files /root/cryptobook-next/app/ Do not modify /root/cryptobook (old social).

Env file /root/cryptobook-next/app/.env (never commit secrets):

HOST=127.0.0.1
PORT=3100
DATABASE_URL=postgres://slip:SLIP_PASSWORD@127.0.0.1:5432/slip
SESSION_SECRET=...
SLIP_SIGNING_SEED=...   # 32-byte hex
PUBLIC_BASE=https://cryptobook.space

Create Postgres role/db slip locally. App listens only on 127.0.0.1.


5. Pages

Virtus chrome on every page: design/virtus-theme.md tokens, paste the :root block, data-theme boot script (cryptobook:themeMode, auto 07:00–18:59 light else dark), 64px nav, #050505 footer. Dark default canvas #000. Primary CTA = 135° #e91e8a → #ff6ec7, white label. Cards: glass + 1px hairline + radius 14–18px. Font: Inter, system-ui. No cyan UI, no TMC greens.

5.1 Marketing nav

Left: Virtus mark 40–50px + wordmark Slip.
Links: Product (/#playground), Pricing (/pricing), Docs (/docs), Legal (/legal).
Right: theme cycle, Sign in, primary Open playground.

Footer: Offices/Contact not required. Columns: Product, Legal (Terms, Privacy, Legal), “Slip is not CryptoBook social. Not legal advice. Not a compliance certification.” © 2026. No fake SOC2 badges, no fake logos, no fake testimonials.

5.2 / Homepage

  1. Hero — centered. Kicker pill with pink dot: PERMISSION SLIPS FOR AGENTS. H1 as in §1, one word (Stop or Slip) in #e91e8a. Two CTAs: primary Try the playground, ghost See a receipt. Trust line: We never take your model keys. US only · 18+.
  2. Playground (#playground) — token .card + --shadow-glow. This is the product. Spec in §7.
  3. How it works — 3 glass cards: (1) Paste or use the example catalog. (2) English → YAML you can edit. (3) Decide API + receipt you can forward.
  4. What we are not — short list: not Permit.io, not Langfuse, not your LLM vendor, not an auditor.
  5. Pricing strip — $0 / $19 / $49 with link to /pricing.
  6. No logo marquee of customers we do not have.

5.3 /c/:publicId Blast card (public, no login)

Shareable card. Tool rows classified destructive | exfil | write | read | other. Raw tool names always visible; color labels are an opinion. Schema hash per tool. Buttons: Copy link, Use these tools in the playground. Watermark on free anonymous cards: Slip free.

5.4 /r/:publicId Receipt (public, no login)

Human page + JSON toggle.

Tamper demo: a “mutate JSON” control that flips verify to red. This is the 60s proof.

5.5 /pricing

Three glass cards. No $29. No annual 50% off hopium.

Plan Price Includes
Free $0 1 saved policy, 500 decide-calls/day, 7-day inbox, watermarked receipts/cards, playground forever
Indie $19/mo Unlimited personal policies, 90-day inbox, unwatermarked receipts, webhook URL (stub ok)
Studio $49/mo 5 seats (enforce 1 seat in v1, show “coming”), ask inbox, CSV export

CTA on all: Start free/signup. Copy: “Stripe in a later cut. Plans are recorded on the account; nothing is charged today.” Honest.

5.6 /signup and /login

Email, password, confirm. Required checkboxes: I am 18 or older, I am not using this from the EU/EEA/UK (honor-system in v1; geo-block is nginx later). Link to /legal. No OAuth. No social.

On success: session cookie, redirect /app.

5.7 /app Dashboard (auth)

Sidebar (glass, active = pink wash): Overview, Policies, Inbox, Receipts, Token, Docs.

Overview: counts (policies, decide-calls today, denials 7d), last 5 inbox rows, copy-paste curl for /v1/decide.

Empty state: “Run the homepage playground, then Save policy.”

5.8 /app/policies

List + create. Editor: English textarea, Compile, YAML panel (editable, source of truth), Save. Show public_id. Button Run on toy agent → playground with this policy.

5.9 /app/inbox

Table of recent decide events for this user: time, policy, tool, decision. Filter deny/ask. No raw args.

5.10 /app/token

Project token, shown once on create (slip_live_…). Store only SHA-256 hash. Prefix slip_live_ + 8 chars displayed. Rotate invalidates old hash. Scopes: decide + fetch_policy.

5.11 /docs

One page:

See §11. Linked from footer.

5.13 /healthz

{"ok": true, "service": "slip"} for PM2/nginx later.


6. API

All JSON. CORS: same-origin plus POST /v1/decide and GET /v1/receipts/:id open (public verify). Rate-limit /v1/decide 60/min/IP anonymous, 600/min/token.

6.1 POST /v1/compile

Auth optional.

{ "english": "never delete; ask before git_push; at most 20 tool calls this session" }

Response:

{
  "yaml": "version: 1\ncaps:\n  max_calls: 20\nrules:\n  - id: r1\n    action: deny\n    tools: [delete_file, delete_repo, rm]\n  - id: r2\n    action: ask\n    tools: [git_push, merge_pr, gh_pr_merge]\n  - id: r3\n    action: allow\n    tools: [\"*\"]\n",
  "policy": { "version": 1, "caps": { "max_calls": 20 }, "rules": [] }
}

Compiler is deterministic, no LLM. See §8. Do not call OpenAI.

6.2 POST /v1/decidethe enforceable permit API

This is the MVP. It must actually allow/deny.

Headers: Authorization: Bearer slip_live_… or playground session header X-Slip-Playground: <ephemeral id> (homepage demo).

{
  "policy_id": "pol_…",
  "tool": "delete_repo",
  "args_hash": "sha256-hex-or-empty",
  "call_index": 4,
  "usd_estimate": null
}

Response:

{
  "decision": "deny",
  "rule_id": "r1",
  "reason": "matched deny tools",
  "receipt_id": "rct_…",
  "receipt_url": "/r/hp_7f3a…",
  "calls": { "used": 4, "cap": 20 },
  "sig": "base64"
}

decisionallow | deny | ask.

If call_index or server-side counter ≥ caps.max_callsdeny with rule_id: cap.

Anonymous playground: create an ephemeral policy + receipt chain in memory or a playground_sessions table TTL 2h. After 2h, receipts remain if we persisted hashes; otherwise only signed-in receipts last.

429 when Free plan exceeds 500 calls/day.

Never echo args. If a client sends args by mistake, hash and drop.

6.3 POST /v1/playground/run

Auth optional. Server-side scripted agent for the demo so the tape cannot desync.

{ "english": "…", "scenario": "archive_old_repos" }

Runs the canned tool sequence in §7 against /v1/decide internally. Returns { card, events[], receipt_id }.

6.4 GET /v1/receipts/:publicId

Public. Metadata + events (tool, decision, args_hash, ts) + sig + pubkey.

6.5 GET /v1/cards/:publicId

Public blast card JSON.

6.6 Auth

6.7 Policies (auth)

6.8 Token (auth)

6.9 Inbox (auth)

6.10 Toy MCP POST /mcp/demo (and GET SSE if easy)

Streamable HTTP or simple JSON-RPC MCP subset:

This is how the playground is a real MCP, not a slide. It is not a generic upstream proxy.


7. Playground behavior

Layout, one card, two columns on desktop, stacked on mobile.

Left

Right

After run

Example catalog (pin in code)

GitHub-like (names only; we do not call GitHub):

list_repos, get_repo, list_files, read_file, create_pr, git_push, merge_pr, delete_repo, delete_file, secret-delete, add_collaborator.

Filesystem-like: ls, read_file, write_file, delete_file, rm.

Classification heuristic (deterministic):

Blast one-liner template: can {top destructive tools} ({n} destructive of {total} tools).

Scenario archive_old_repos (scripted)

  1. list_repos → allow
  2. get_repo → allow
  3. delete_repo → deny (rule)
  4. git_push → ask
  5. list_files → allow

Stop. Show “demo agent halted on deny/ask. This is a simulation on toy tools, not your account.”

Filesystem scenario clean_up_this_repo: ls allow, delete_file deny, write_file allow, rm deny.


8. Policy engine

YAML schema (agent-permit.yaml)

version: 1
caps:
  max_calls: 20          # per receipt/session
rules:                   # first match wins
  - id: r1
    action: deny         # deny | ask | allow
    tools: [delete_file, delete_repo, rm]
  - id: r2
    action: ask
    tools: [git_push, merge_pr]
  - id: r3
    action: allow
    tools: ["*"]

No CEL in v1. Tool match: exact name or glob delete_* or *.

Default if no match: deny (fail closed).

English → YAML (no LLM)

Ordered regex extracts; remaining sentence ignored.

Pattern Effect
at most N (tool )?calls / max N calls caps.max_calls = N
never delete / don't delete / do not delete deny delete_*, rm, secret-delete
ask before (X or Y or …) ask those tool aliases
allow only A, B allow those; implicit deny * at end
deny X deny X

Aliases: pushgit_push, mergemerge_pr, deletedelete_file + delete_repo.

Always append allow: ["*"] last unless allow only was used.

Show YAML beside English. User edit of YAML is what the engine runs. Label: YAML is the source of truth. English is a compiler.


9. Data model (Postgres)

CREATE TABLE users (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email         citext UNIQUE NOT NULL,
  password_hash text NOT NULL,
  plan          text NOT NULL DEFAULT 'free',  -- free | indie | studio
  age18         boolean NOT NULL,
  not_eu        boolean NOT NULL,
  created_at    timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE policies (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id    uuid REFERENCES users(id),
  public_id  text UNIQUE NOT NULL,
  name       text NOT NULL,
  english    text NOT NULL,
  yaml       text NOT NULL,
  policy_hash text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE project_tokens (
  id         uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id    uuid NOT NULL REFERENCES users(id),
  prefix     text NOT NULL,
  token_hash text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  revoked_at timestamptz
);

CREATE TABLE receipts (
  id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  public_id    text UNIQUE NOT NULL,
  user_id      uuid REFERENCES users(id),
  policy_id    uuid REFERENCES policies(id),
  policy_hash  text NOT NULL,
  event_count  int NOT NULL DEFAULT 0,
  deny_count   int NOT NULL DEFAULT 0,
  call_cap     int,
  watermarked  boolean NOT NULL DEFAULT true,
  pubkey       text NOT NULL,
  sig          text NOT NULL,
  created_at   timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE receipt_events (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  receipt_id  uuid NOT NULL REFERENCES receipts(id) ON DELETE CASCADE,
  seq         int NOT NULL,
  tool        text NOT NULL,
  args_hash   text,
  decision    text NOT NULL,  -- allow | deny | ask
  rule_id     text,
  created_at  timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE blast_cards (
  id           uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  public_id    text UNIQUE NOT NULL,
  source       text NOT NULL,  -- example_github | example_fs | paste
  tools_json   jsonb NOT NULL, -- names + class + schema_hash only
  created_at   timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE playground_sessions (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  yaml        text NOT NULL,
  call_count  int NOT NULL DEFAULT 0,
  expires_at  timestamptz NOT NULL,
  created_at  timestamptz NOT NULL DEFAULT now()
);

Indexes: receipts.public_id, blast_cards.public_id, receipt_events(receipt_id, seq), project_tokens.token_hash.

Do not add columns for api_key, prompt, args_json, raw_body.

Logging: structured JSON. Redact authorization, cookie, password, token, sk-. Do not log request bodies on /v1/decide.


10. Receipt signature

Canonical event: seq|tool|decision|args_hash|policy_hash.

Merkle: sha256 of \n-joined canonical events (fine for <100 events; no need for a real tree in v1).

Sign merkle_root with Ed25519. Store sig + server pubkey on the receipt.

Public page: recompute root from visible events, verify. If the user edits the JSON in the textarea, verify fails.

Obsigna-compatible export is not required in 2 days. JSON on /r/{id}.json is enough.


Not legal advice. Counsel before public marketing. Pages can be one /legal with anchors if faster.

/legal/terms

/legal/privacy

On-page disclaimers (required chrome)

Homepage footer + every /r/ + /c/:

Not legal advice. Not a compliance certification. Not an auditor. This is a log of rules you wrote. US only · 18+.

Playground: This demo uses toy tools we host. It does not access your GitHub, disk, or LLM account.


12. Pricing (product, not Stripe)

Plan Price Enforcement in v1
Free $0 1 policy, 500 /v1/decide / day / user-or-IP, 7-day inbox, watermark
Indie $19/mo Unmetered decide (still rate-limit 600/min), 90-day inbox, no watermark. Set users.plan='indie' via a /app debug toggle or SQL for demo until Stripe exists. Do not hide a fake “Pay” that charges nothing without saying so.
Studio $49/mo Same as Indie for v1 plus CSV export. Seats = 1 with “5 seats — next cut” note.

Paywall is unwatermarked receipt + 90-day inbox, not “more events” as the headline. Free already feels the deny.

No $199 Pro. No SOC2. No SSO.


13. Virtus theme — builder checklist

Copy from design/virtus-theme.md. Do not invent a palette.

Playground sits in a token .card with --shadow-glow. No 3D AI orb.


14. nginx / PM2 (for ops later — do not apply now)

# pm2
HOST=127.0.0.1 PORT=3100 pm2 start server.js --name slip

Intended nginx (ops, not this task):

server {
  listen 443 ssl http2;
  server_name cryptobook.space www.cryptobook.space cryptobook.com www.cryptobook.com;
  # existing cert paths for .space; .com cert after DNS cutover
  location /.well-known/acme-challenge/ { root /var/www/letsencrypt; }
  location / {
    proxy_pass http://127.0.0.1:3100;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }
}

Do not add a location ~ /\. that 404s ACME. Do not open port 3100 on the WAN. Do not stop the old :3000 cluster until nginx no longer points at it.

Until nginx is cut, verify locally:

curl -sS http://127.0.0.1:3100/healthz
curl -sS http://127.0.0.1:3100/v1/decide -H 'content-type: application/json' -d '{"tool":"delete_repo","policy_id":"playground"}'

15. Day-1 / Day-2 split

Day 1

Day 2

Explicitly not day 1–2: Stripe, magic link, GitHub App, customer MCP wrap, sidecar binary, Telegram ASK, CSV of loops, light-mode pixel-perfect beyond tokens, EU geo-block (document it).


16. Definition of done

If those boxes are ticked, the pivot has a real core. Everything else is a later cut.