# Pricklypear agent instructions The image is the API. Eval Lisp on the running habitat; do not invent REST endpoints for app features (todos, notes, bible, …). ## Origin Base URL (this host): https://pricklypear.rocks Machine contract: GET https://pricklypear.rocks/.well-known/agent.json ## Auth Scheme: HTTP Basic against the image credentials table (same username/password as https://pricklypear.rocks/login). How to get credentials: ask the human once. Store them in a local secrets file (mode 600). This document never contains passwords. Create users at /admin/users after bootstrap login. ### Onboarding ceremony (first login) A human creates your account at /admin/users and hands you the username + password ONCE. Your first moves: 1. Verify: POST /api/eval {"expr":"(whoami)"} 2. Mint a token: {"expr":"(issue-token \"agent-label\")"} The raw token is returned ONCE. Store it mode 600. 3. Switch auth: Authorization: Bearer (eval and every other authed endpoint accept Bearer). 4. Discard the password — you never need it again. 5. Claim your branch: {"expr":"(checkout \"USER/personal\")"} All tokens are per-user, hashed at rest, and revocable ((list-tokens), (revoke-token id), or the /tokens page). There are NO global tokens — if you are handed one alert your human. Identity patterns, simplest first: - SAME USER + labeled token: you act AS your human (attribution = their username). - DELEGATED token: the owner runs (mint-delegated-token "agent-username" "label") and hands you the result — data-only (no define/route/deftype/checkout, never admin), (current-actor) keeps your provenance. - SEPARATE AGENT USER: the admin creates e.g. 'marcus-agent' — a full identity of your own (own branch, own tokens). ### Running glochid against this habitat The local glochid harness (the coding-agent binary) authenticates with env: PP_URL + PP_TOKEN (or AGENT_HABITAT_URL & TOKEN). The token wins over user/pass when set — your laptop holds a revocable token, never a password. ~/.config/pricklypear/env (mode 600) is read as a shell file as a fallback. Notes: - Use the image password, not nginx/edge htpasswd. - No OAuth/JWT in v1. - Browser clients may use cookie pp_session after POST /login; agents use Basic for first login, then a Bearer token. ### Delegation: act AS your human (on-behalf-of tokens) Preferred for a designated agent that works inside the owner's data space (logging workouts, todos, notes as them): the OWNER mints a delegated token once and hands it to the agent: (mint-delegated-token "" "label") Returns the raw token ONCE. The agent then authenticates with Authorization: Bearer instead of Basic. Effects (auth. delegation-tokens): - (whoami) returns the OWNER; rows are created as the owner (author=owner, owner's datum space) — (current-actor) returns the agent, for field-level provenance. - DATA ONLY: the session may not persist code. define/route, load-library of non-seeded packages, reseed-package, deftype, and checkout/fork/merge are denied; no admin capability even when the owner is admin. - Revocation: (list-delegated-tokens) on the owner's session, then (revoke-delegated-token ""); takes effect on the holder's next request. Owners: prefer this over handing out your own api_key/password — it is scoped to data and carries provenance. ## Eval API POST https://pricklypear.rocks/api/eval Content-Type: application/json Accept: application/json Authorization: Basic Request body: {"expr":"(+ 1 2)"} Response: {"ok": true, "value": "3"} {"ok": false, "error": "unbound: foo"} Unauthenticated calls are rejected (401). Errors are plain strings (arity mismatch, unbound, no active request). ## First calls - `(whoami)` - `(current-branch)` - `(list-packages)` - `(notes/doc/get "agents")` - `(describe "todo")` - `(apropos-docs "todo/item")` - `(function-doc "todo/item/add")` - `(count (todo/list))` ## Two kinds of functions 1. DATA / builders — take explicit arguments; safe from /api/eval: (bible/chapter "John" 3) (bible/chapter-html "John" 3 ()) (todo/list) 2. HTTP HANDLERS — read (request-method)/(request-param ...); from /api/eval they return: no active request Examples: bible/chapter-reader, todo/page, notes/page, */page. That message is expected. Call data functions, or hit the route in a browser / with a real HTTP request. ## Selective discovery (REPL) Do not dump (list-bindings) into the model context (hundreds of names). Expand one package or name at a time: (list-packages) (describe "todo") ; package summary + shallow exports (package-exports "todo") ; shallow pkg/local names (package-bindings "todo") ; ALL names under todo/ (any depth) (apropos-docs "todo/item") ; ((name doc) …) (function-doc "todo/item/add") (function-source "todo/item/add") (function-ast "todo/item/add") (read-string "(lambda (x) x)") ; parse LLM Lisp text to data (package-exports "std") ; frozen kernel/std surface Human browser: https://pricklypear.rocks/ref (packages) or https://pricklypear.rocks/ref?q=todo ## How to build a REPL client The image is the API. Your client only needs: 1. Base URL (this host) 2. HTTP Basic username/password (from a human; never in this file) 3. POST /api/eval with JSON body {"expr":"(whoami)"} 4. Parse JSON {ok, value|error} curl: ``` curl -sS -u USER:PASS -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"expr":"(whoami)"}' https://pricklypear.rocks/api/eval ``` Python: ``` import base64, json, urllib.request base = "https://pricklypear.rocks" user, password = "USER", "PASS" expr = "(whoami)" req = urllib.request.Request( base.rstrip("/") + "/api/eval", data=json.dumps({"expr": expr}).encode(), method="POST", headers={ "Content-Type": "application/json", "Accept": "application/json", "Authorization": "Basic " + base64.b64encode( f"{user}:{password}".encode()).decode("ascii"), }, ) print(urllib.request.urlopen(req).read().decode()) ``` Local laptop: OCaml pp-eval CLI (see repo skill pricklypear-eval) or scripts/pp-eval.sh after sourcing ~/.config/pricklypear/env. ## Symbols vs strings (code-as-data) Quoted bare names are symbols, not strings: (symbol? 'lambda) => #t (string? "lambda") => #t (symbol=? (car (read-string "(lambda (x) x)")) 'lambda) => #t Walk trees with symbol=?, not string-eq on car. (string-eq (symbol->string (car form)) "lambda") is OK if needed. CRUD still accepts 'type-name (symbols coerced at the DB boundary). ## Application libraries - std, ui, layout, welcome — shell / HTML helpers - todo, notes, outline, plan, contacts, bookmarks, chat - calendar, flashcards, bible, dabar, weather, signup, ai Names bind lazily from your branch chain — no load step. (load-library "pkg") is the deploy/repair tool: it re-evals the RELEASE file onto the CURRENT branch and warns on non-main (it pins bodies that differ). Never in repair scripts or handlers. ## Live agents note (habitat AGENTS.md) After (load-library "notes"), read: (notes/doc/get "agents") Browser: https://pricklypear.rocks/notes?slug=agents This note is the living agent playbook for this habitat. Agents SHOULD update it (notes/doc/upsert) when they learn durable conventions, new libraries, or wiki workflow changes — same as editing AGENTS.md in a git repo, but durable in Postgres on the current branch. Also see (notes/doc/get "wiki-schema") for the wiki LLM preamble. ## Notes / wiki (image is the API) (load-library "notes") (notes/doc/list) (notes/doc/get "pricklypear") (roam/backlinks "pricklypear") (wiki/ingest-from-source "…raw source text…") (wiki/query "…") (wiki/lint) Outline nodes support op=eval (Lisp with self/doc/slug bound). Wiki links in bodies: [[slug]] or [[slug|label]]. ## Media upload (images / PDF / files) Do NOT push multi-KiB binaries through POST /api/eval — the eval body cap is 64 KiB. Use the dedicated media endpoints instead. ### Write (auth required — HTTP Basic or session) POST https://pricklypear.rocks/api/media Max body: 5 MiB (images/pdf), 64 MiB (audio/* and application/octet-stream — the files-flavor carrier for whole-file sync via scripts/pp_files.py). Allowed Content-Types: image/png image/jpeg image/gif image/webp image/svg+xml application/pdf audio/mpeg audio/ogg audio/flac audio/wav audio/mp4 audio/aac audio/webm application/octet-stream Body modes: 1. Raw binary — set Content-Type to the media type; optional header X-Filename: original-name.png 2. JSON — Content-Type: application/json {"content_type":"image/png","data_b64":"…", "filename":"shot.png"} // filename optional 3. Multipart — Content-Type: multipart/form-data field name=file (or name=media) with filename + part Content-Type Success (201): {"ok":true,"id":"","url":"/media/", "content_type":"image/png","byte_size":N,"sha256":"…"} ### Read / list / delete GET https://pricklypear.rocks/media/ → raw bytes (auth; Range/206 for audio seek) GET https://pricklypear.rocks/api/media → JSON meta list (no bytes) GET https://pricklypear.rocks/api/media/by-sha256/<64-hex> → meta if you already hold those bytes (files-lens dedup probe) DELETE https://pricklypear.rocks/api/media/ → author or admin GET https://pricklypear.rocks/media → browser gallery + upload form POST https://pricklypear.rocks/media/upload → browser multipart form ### Client helper # same env as pp_eval: PP_URL PP_USER PP_PASS python3 scripts/pp_media.py put ./shot.png python3 scripts/pp_media.py get -o out.png python3 scripts/pp_media.py list python3 scripts/pp_media.py delete ### Whole-directory file sync (files-lens) python3 scripts/pp_files.py init ~/ppfiles # once python3 scripts/pp_files.py sync ~/ppfiles # pull+push, conflict policy in borge/storage.borg ### curl (raw PNG) curl -sS -u USER:PASS -X POST https://pricklypear.rocks/api/media \ -H 'Content-Type: image/png' -H 'X-Filename: shot.png' \ --data-binary @shot.png ### curl (multipart) curl -sS -u USER:PASS -F 'file=@shot.png;type=image/png' \ https://pricklypear.rocks/api/media ### Lisp (meta only — not bulk upload) (media-meta "") ; JSON meta or () (media-list) ; your recent meta (media-url "") ; "/media/" (media-delete "") (media-put "image/png" b64 [filename]) ; ≤256 KiB only Embed in a note node body (wiki syntax): [[media:]] [[media:|alt text]] Renders when the note is viewed (auth required to fetch the image). Ownership is the uploading author. Reference media from notes/HTML with the returned /media/ URL (auth required to fetch). ## Rules 1. One Lisp expression per POST /api/eval unless you own multi-step state yourself. 2. Mint your own labeled per-user Bearer token on first login — (issue-token "agent-label") — store it mode 600, switch to Bearer, and stop carrying the password. All tokens are per-user and revocable; there are no global tokens. 3. Package functions bind LAZILY from your branch chain — call them directly, no (load-library) needed. load-library is a DEPLOY tool: it re-evals the release file onto your current branch (warns + pins on non-main). Never use it in repair scripts or route handlers. 4. Prefer a personal branch before defining things: (checkout "USER/personal"). 5. Two function kinds: DATA builders take explicit args and work from /api/eval (e.g. bible/chapter, bible/chapter-html with book/chapter); HTTP HANDLERS (*/page, */reader) use request-method/request-param and return "no active request" outside a browser hit — that is expected, not a crash. 6. Do not invent app-specific REST APIs — call Lisp instead. 7. Never put passwords in code you write back to the image. 8. Unauthenticated /api/eval is rejected; /agent (HTML portal) is private. 9. Dotted rest params (a . b) are NOT supported; use fixed-arity params. 10. File product issues as notes in the inbox when you cannot fix OCaml; do not invent REST for filing. 11. Read the live agents note: (notes/doc/get "agents"). It is the habitat AGENTS.md — edit it with notes/doc/upsert when agent guidance should change. 12. Discover selectively — do not dump all bindings into context: (list-packages), (describe "pkg"), (package-bindings "pkg"), (package-exports "pkg"), (apropos-docs "pat"), (function-doc name). 13. Build a REPL client from this document: HTTP Basic + POST /api/eval JSON {"expr":"…"}. Prefer that over inventing REST. curl and Python examples are below; local hosts may use the pp-eval CLI. 14. Knowledge wiki (Karpathy loop): (wiki/ingest-from-source …), (wiki/query …), (wiki/lint); wiki links are [[slug]] on outline bodies. 15. Binary media (images/PDF/audio): do NOT push multi-KiB bytes through /api/eval (64 KiB cap). Use POST /api/media (Basic auth; raw body, JSON data_b64, or multipart field name=file; 5 MiB images/pdf, 64 MiB audio/*) or scripts/pp_media.py. Browser UI: GET /media + POST /media/upload. Serve via GET /media/ (supports HTTP Range for audio seek). Meta via (media-meta id) / (media-list); tiny fixtures only via (media-put …). ## curl example curl -sS -u USER:PASS -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"expr":"(whoami)"}' https://pricklypear.rocks/api/eval BusyBox wget often lacks --user/--password; prefer curl or python. ## python example ```python import base64, json, urllib.request base = "https://pricklypear.rocks" user, password = "USER", "PASS" expr = "(whoami)" req = urllib.request.Request( base.rstrip("/") + "/api/eval", data=json.dumps({"expr": expr}).encode(), method="POST", headers={ "Content-Type": "application/json", "Accept": "application/json", "Authorization": "Basic " + base64.b64encode( f"{user}:{password}".encode()).decode("ascii"), }, ) print(urllib.request.urlopen(req).read().decode()) ``` ## Filing issues Prefer notes inbox (notes/doc/ensure-inbox + outline nodes) over inventing bug-tracker REST. Keep OCaml-kernel bugs precise with repro expressions. Durable agent guidance belongs in the live agents note (slug agents), not only in git AGENTS.md. ## Related public surfaces - Welcome: https://pricklypear.rocks/welcome - Source browser: https://pricklypear.rocks/code - Book PDF: https://pricklypear.rocks/book.pdf - Health: https://pricklypear.rocks/health - Interactive REPL UI (auth required): https://pricklypear.rocks/repl - Private agent portal (auth required): https://pricklypear.rocks/agent - Media gallery / upload (auth required): https://pricklypear.rocks/media ## What not to do - Do not open or rely on the raw TCP REPL port from the internet. - Do not invent per-app REST endpoints. - Do not put secrets into (define …) forms or public routes. - Do not treat "no active request" as a host crash — call a data fn. - Do not base64 multi-MiB images into /api/eval — use POST /api/media.