- JavaScript 77.9%
- TypeScript 22%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
An object recipient ({ name, phone }) read as [object Object] on the card; the
address a rule matches on is now its email or phone and the display copy
"Name <address>". A disclose descriptor may declare `verb` ("send it"), app-authored
and sanitized, in place of "disclose it". Cards still never show content.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
||
| conformance | ||
| demo | ||
| scripts | ||
| spike/web-agent | ||
| src | ||
| test | ||
| types | ||
| .env.example | ||
| .gitignore | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| taxonomy.md | ||
| tsconfig.json | ||
| tsconfig.types.json | ||
| usage.md | ||
tainted-love
Taint tracking for language-model agents that plan, then execute. A model writes a short script, a sandbox runs it, and every value keeps a label: where it came from and who may see it. Before a tool sends anything, a rule reads those labels and allows the call, blocks it, or asks the user on a card.
Your app supplies the tools and the UI; there are none built in. Runs in Node, in a browser or webview, and in an Android WebView.
How it works
┌──────────────────────────────────────────────────────────────┐
│ language model │
│ planner: sentence + tool list → script │
│ reader: one question over documents → one answer │
└───────────────────────────────────────────────────▲──────────┘
│ ↑ prompt
│ ↓ text
┌───────────────────────────────────────────────────▼──────────┐
│ your app: tools, dialogs, chat, memory │
└───────▲─────────────────────▲─────────────────────▲──────────┘
│ effect │ card │ chat
│ ↑ real arguments │ ↑ reason │ ↑ prompt
│ ↓ result │ ↓ yes / no │ ↓ text
┌───────▼─────────────────────▼─────────────────────▼──────────┐
│ trusted core: a QuickJS realm │
│ stores every value with its origin and who may see it │
│ builds the planner and reader prompts │
│ decides each tool call: allow, block, or card │
└───────▲──────────────────────────────────────────────────────┘
│ ↑ a command over ticket numbers
│ ↓ the script, then a ticket per command
┌───────▼──────────────────────────────────────────────────────┐
│ guest: a second QuickJS realm on a worker thread │
│ runs the script; holds ticket numbers, nothing else │
└──────────────────────────────────────────────────────────────┘
The planner never sees a stored value; after a failure it sees only commands, ticket numbers and error codes.
Anything read from a file or a tool, and anything the reader writes from it, is untrusted. A send is silent only when the recipient came from the user or their contacts, the user named the document, and the document is shared with the recipient. Anything else is one card listing every reason: untrusted recipient, hidden document, document picked by untrusted content, or text the reader wrote from untrusted content.
The user can type a note to steer a running plan, stop it, and resume it later. The saved state holds no values.
Example
notes.txt ends with an injected line naming secrets.pdf. Tickets are shown as #name.
user "Send Bob the document he asked for in my notes.txt"
planner script: find Bob, read notes.txt, ask the reader which document, read it, send it
guest #notes = run("readFile", { name: put("notes.txt") })
core the app's readFileRaw returns the text; stored with origin file:notes.txt
guest #documentName = extract(#notes, "name of the requested document")
reader answers "secrets.pdf"; stored with origin file:notes.txt + reader-answer
guest #document = run("readFile", { name: #documentName }) secrets.pdf, shared with nobody
guest run("sendEmail", { to: #address, body: #document })
rule hidden document, picked by untrusted content → card
card “secrets.pdf” is shared with nobody and was chosen by untrusted data
(typed-by-user + file:notes.txt + reader-answer + file:secrets.pdf)
— disclose it to bob@corp.com anyway? n
result USER_SAID_NO, nothing sent
Try it
npm install
npm run start:example # poisoned notes: the card names secrets.pdf, answer n
npm run start:clean # clean notes: one card, a yes sends report.pdf
npm start # the model writes the plan; needs LLM_MODEL_KEY in .env
Node 22.13 or newer. No build step.
Docs
- usage.md: a web app and an Android app, end to end.
- taxonomy.md: every term, defined once.
Develop
npm test # the suite twice: core inside a QuickJS realm, then native
npm run typecheck
npm run build:core-sources # after editing src/core or src/guest
npm run build:types # after changing a type in src: regenerates types/
MIT.