Tinker is an ontology-driven builder for internal tools. Define an object once and it becomes a real Postgres table behind row-level security, a governed API, and an MCP front door for agents. Personal data is sealed by type. Every read gets a mask and automations get a key. A person sees the value only when a second person approves.
Email and phone fields are personal data by type, so they are always sealed. Text fields opt in. The core database never holds the value. It holds a vault reference and a keyed blind index, and each audience gets exactly the face it needs.
The typed query compiler writes the mask into the SQL itself, so every consumer that builds on it inherits the mask and none of them can forget it.
eq, ne and in go through the HMAC-SHA256 blind index.contains, ranges, sorting and row policies on sealed fields.// MCP · query { "object": "customer", "intent": { "select": ["name", "email", "phone", "plan"], "filters": [{ "field": "email", "op": "eq", "value": "Ada@Analytical.Engine " }] } } // → rows, matched via email__bidx [{ "name": "Ada Lovelace", "email": "••••••", "phone": "••••••", "plan": "Enterprise" }] // "order": [{ "field": "email" }] error: field 'email' is sensitive and cannot be sorted on
pk_ followed by 40 hex characters of HMAC over the blind index, in a separate domain. The same email always gives the same key, so you can join, dedupe and route on it. The key won't query the index and can't be reversed.
// MCP · query, selecting keys { "object": "customer", "intent": { "select": ["name", "email:key"] } } // → rows [{ "name": "Ada Lovelace", "email:key": "pk_3f9a0c71e2…b84d" }, { "name": "A. Lovelace", "email:key": "pk_3f9a0c71e2…b84d" }] // same person, found without seeing the address
The reveal tool returns one field of one record. It needs an owner or admin with an explicit scope, a stated purpose, and an approval decided by someone else. That approval works only once.
// MCP · reveal { "object": "customer", "record_id": "01J9…7f2c", "field": "email", "approval_request_id": "01J9…a40e" } // → value (audited as pii.reveal) "ada@analytical.engine" // same approval, second time error: reveal needs an approved pii.reveal request for this field, filed by you and approved by someone else
The builder, the HTTP API, the CLI, live grids, search, dashboards and agents all go through one compiler. Tenancy is enforced in Postgres, not in application code you have to trust.
Machine keys (tk_…), passkeys (WebAuthn ES256/EdDSA, UV for MFA) or OIDC code flow with PKCE, state and nonce. Each resolves to one actor, one org and one role.
SET LOCAL app.organization_id inside a transaction on a role that can't bypass RLS. The tenant comes from the credential, never from the caller.
Role projection, row policies, masking and the blind index are compiled into one SQL statement. Unauthorized rows come back not_found, so there's no existence oracle.
Validation, lifecycles (draft → review → publish), approvals bound to the action, and an automation event written in the same transaction.
One table per object. Sealed columns hold a vault ref plus __bidx.
Per-subject DEKs wrapped by the KEK, with every access audited.
Objects, fields, relations and lifecycles are metadata that materializes into real Postgres DDL, not an entity-attribute-value store. Schema changes ship as versioned evolutions with canary and promote.
A missing extension, an unset vault, a dead IdP or a half-configured key gives a typed error at startup or call time. Nothing silently falls back to plaintext.
DBOS-style workflows, queues and timers on Postgres itself. Outboxes for email and automations mean no event is lost and none is invented.
Conditions compare keys and actions resolve values only at the moment they run. Typed literals are turned into keys when the automation is saved. The saved definition never contains the plaintext you typed into it.
// MCP · automation { "action": "save", "object": "customer", "name": "VIP escalation", "trigger": { "on": "record_updated", "fields": ["email"] }, "conditions": [{ "field": "email", "op": "eq", "value": "ceo@northwind.example" }], "actions": [ { "type": "send_email", "to_field": "email", "subject": "Your account team", "body": "Hi {{ name }}, …" }, { "type": "webhook", "url": "https://hooks.example/vip", "fields": ["name", "email"] } ] }
-- automations.conditions (jsonb) [{ "field": "email", "op": "eq", "keyed": true, "value": "ceo@northwind.example""pk_8d07e5b2a9…41c2" }] -- run_as_role: admin (re-checked every run) -- send_email.to_field → a vault ref in the outbox, -- resolved in-process at send, audited, -- never written back -- webhook body → ids, "fields": {"name": …}, -- "keys": {"email": "pk_3f9a…b84d"} -- Idempotency-Key: automation:event:action
| event | record | email:key | outcome | detail |
|---|---|---|---|---|
| updated | 01J9…7f2c | pk_3f9a…b84d | succeeded | email queued · webhook 200 |
| updated | 01J9…c0d1 | pk_77e0…2a19 | skipped | condition false |
| updated | 01J9…7f2c | pk_3f9a…b84d | loop_blocked | depth 3 |
An event exists exactly when the write commits. Workers claim with SKIP LOCKED, and each (automation, event) pair runs once.
Writes made by an action carry the chain depth forward. At depth 3 the run records loop_blocked and stops.
An equality literal is a yes-or-no question about a value, so each actor can save at most 20 sealed literals per hour. Saves are audited with counts only.
Runs as the author's role, under that role's projection and row policy. If the author loses the role, the next run fails closed.
This uses the same four-eyes approvals that guard publishing, bound to the exact (object, record, field). The approval is consumed atomically with the reveal.
It files a pii.reveal request for one field. The purpose is stored and audited.
A different owner or admin lists the pending requests and approves or denies them. The requester can never decide their own request.
It must come from the same requester, for the same field, before the approval expires. The vault decrypts in-process and logs the access.
Self-approval. The requester is recorded on the request and refused at decision time, whether they approve or deny.
Replay. A second reveal with the same approval fails. So does a reveal of a different field or record.
Blanket scopes. mcp:tools doesn't cover reveal. The key needs mcp:tool:reveal by name.
Every record has its own data-encryption key. Erasure tombstones the refs, nulls the columns and destroys the key, so any copy of that ciphertext, in a dump or a replica, becomes noise.
rec_…7f2cThe MCP erase tool, gated like reveal and audited as pii.erase, destroys every value for the record (row, drafts, versions, superseded copies). It then shreds the subject's DEKs. One record's erasure never touches anyone else's key.
wrapped_deks still holds the key until it ages out, so back up the key table on a shorter retention than the values. Values sealed before per-subject keys sit under a legacy org key; erasure destroys them row by row but can't shred that shared key.Tinker isn't in any model's training data, so the MCP server is designed to be self-describing. An agent with nothing but a tk_ key discovers the schema, learns the governed write paths and does correct work. Agents only ever see the mask.
Objects, fields, lifecycles and allowed operations for this role.
Typed, governed reads with filters, keys and blind-index matches.
One record, projected and masked for the caller.
Validated, sealed at entry, with an outbox event.
Governed update; changed fields feed triggers.
Lifecycle moves: draft, review, publish.
Dashboards through the same compiler and mask.
Save, list, enable, disable and inspect runs.
File a purpose-bound pii.reveal request.
List, approve or deny, never your own request.
One field, once, with a second person's approval.
Destroy a record's PII and shred its keys.
mcp:toolsneeds mcp:tool:<name> by name, and owner or adminstdio and HTTP/SSE · tinker://ontology/{slug} resources“If the front door doesn't win clearly, it failed.”
That was the bar Tinker set itself: median time to the first correct publish for a fresh agent, measured against raw HTTP plus human docs. Across three timed, independently verified experiments, adding skill docs on top of the MCP never beat the MCP alone; every verdict was a loss or a tie. The MCP documents itself.
A tk_ key is bound to one organization and one role at issue time. Sessions take their tenant from the credential, never from the request. Unknown arguments are rejected with a teaching error, not ignored, so an agent learns the real shape instead of guessing.
It's one Cargo workspace, layered so the data plane knows nothing about the surfaces built on it. Each milestone has its own test crate and had to pass its gate before the next one began.
The status ledger ties each capability to a passing test, and the important ones were checked by breaking the code on purpose to see the test fail. Here are some that guard what this page claims.
sensitive_values_are_sealed_masked_findable_and_revealableNo plaintext in the row or the audit log, exact match through the index, and plaintext only through reveal.
reveal_needs_a_fresh_second_person_approval_for_that_fieldSelf-approval, replay and field substitution are all refused.
erasure_crypto_shreds_copies_of_the_ciphertextThe subject's key is gone, and a restored copy of the ciphertext is refused.
sealed_conditions_route_and_actions_resolve_only_at_sendKeyed conditions match, and the recipient is resolved only at send.
runs_fail_closed_when_the_author_loses_the_role_and_stop_when_disabledAn automation never outlives its author's authority.
webauthn_register_then_login_binds_origin_and_assurancePhishing origins, counter replay and UV assurance are covered.
oidc_token_abuse_fails_closedPKCE, single-use state and nonce binding. A bare id_token is refused.
ingest_seals_sensitive_fields_and_scrubs_landingImported PII is sealed at promotion and scrubbed from the landing tables.
Full suite on PostgreSQL 18 with a real browser test, clean under fmt and clippy -D warnings. The one test not run is a timing benchmark, ignored by design. pii verify runs as a gate.
A soak test of 4.1M operations over both MCP transports, plus backup and point-in-time-recovery drills with measured RPO/RTO, and an internal adversarial review that found and fixed a real DoS.
deploy/ kit is validated locally.# private PG18 cluster on :5440 (macOS or Linux), dev secrets, migrations $ bin/dev-db.sh $ eval "$(bin/dev-db.sh env)" # full suite (lean build), browser test, then `pii verify` $ bin/test.sh test: passed 662 failed 0 ignored 1 test: pii verify ok # the agent front door over stdio (`serve` for HTTP/SSE) $ TINKER_API_KEY=tk_… cargo run -p tinker-mcp
postgresql@18 or apt.postgresql.org)Then read sealed fields, automations and the threat model.
An internal-tool builder where governance is the kernel, not a feature you remember to turn on.