LA.001Docs · Security
LA.001Docs · Security

Row-level security is the entire authorisation story.

Hub has no bespoke permission table and no per-surface access list. Every user, staff, agent or client, is a real database auth user with one profile row, and the database's own row-level policies decide what each one can see and change. The floor: an anonymous caller resolves every predicate to false and sees nothing.

LA.002Authorisation
LA.002Authorisation

Five predicates, one profile row, and the floor is false.

Revocation is one column. Set a profile inactive and every predicate drops that user on the next request. No key rotation, no token blocklist.

PredicateTrue forGates
is_team_member()Active team members, humans and agentsThe normal workspace
is_human_team()Active team, and not an agentWorkspace settings, space admin, client-content writes, the access panel, approval decisions
is_admin()Active team human with the admin flagPrivilege management
has_crm_access()Admin or CRM-flagged humanThe sensitive tier: spend, intel, suggestions. Agents never hold it
client_id_of_user()An active client loginThe portal. Each client sees only its own organisation

Fourteen policies are restrictive, and every one is an agent fence. A restrictive policy is AND-ed with the whole permissive set: it can only ever take access away, and no permissive policy can out-vote it. That's what stops an agent deleting a task, proven in a rolled-back transaction: a human gets one row updated, an agent token gets zero.

LA.003Custody
LA.003Custody

Your keys: a bank teller, not a key ring.

Client keys live in a vault and one authorised runtime home. An agent never enters the vault. When a task is routed it gets a signed withdrawal slip: one client, one purpose, expiring within hours. The teller (the broker) checks the slip and does the transaction itself, or hands over a single-purpose pass that expires on its own. Everything, including knocked-back slips, lands on the statement. A stolen slip expires. A stolen key would be forever, which is why there are no keys in agent hands, ever.

01 · CUSTODYwhere a key actually lives02 · THE SLIPwhat this agent may do, right now03 · THE DOORone way through, and it is typed04 · WHAT COMES OUTreached, and written downBY HAND, ONCEVAULThumansThe vaultVault of record · noagent reads itKEYruntimeRuntime custodyOne authorised homefor this keyGRANTexpiringA withdrawal slipOne client, one actionclass, hoursBROKERone doorThe tellerDoes the deal, orlends a passEXECfail-closedTyped executorTyped, allowlisted,and nothing elseAUDITrefusals tooThe statementEvery attempt,refusals tooTRIPdailyTripwire scanGrants past theirlife, or with no taskPROVexternalThe platformReached, and neverhanded a keyTHE TEN CHECKS · IN FRONT OF EVERY ACTIONFIXED ORDER · FAIL-CLOSED · ONE REASON OUT01Round lock02Masterflag03Killswitch04Allowlist05Handler06Approval07Spend cap08Egress09Credential10GrantANY ONE OF THE TEN CAN REFUSE · THE REASON RECORDED IS THE FIRST THAT APPLIED · 06 IS THE ONE A PERSON MAKESTHE GATE IS DRAWN ONCE ON THIS PAGE, AND THIS IS IT
01 · CUSTODY · where a key actually lives
  • The vaultVault of record · no agent reads it
  • Runtime custodyOne authorised home for this key
02 · THE SLIP · what this agent may do, right now
  • A withdrawal slipOne client, one action class, hours
03 · THE DOOR · one way through, and it is typed
  • The tellerDoes the deal, or lends a pass
  • Typed executorTyped, allowlisted, and nothing else
04 · WHAT COMES OUT · reached, and written down
  • The statementEvery attempt, refusals too
  • Tripwire scanGrants past their life, or with no task
  • The platformReached, and never handed a key
The ten checks · fixed order, fail-closed, one reason out
  • 01 · Round locksource-level, now open
  • 02 · Master flagoff by default
  • 03 · Kill switchper client, or all
  • 04 · Allowlisttyped actions only
  • 05 · Handlerbound at build time
  • 06 · Approvalon an exact payload
  • 07 · Spend caprefused whole
  • 08 · Egressno open world
  • 09 · Credentialresolved, never held
  • 10 · Grantlive, scoped, unexpired
  • Then, and only then, it actsAny refusal is recorded with the first reason that applied.
How a credential reaches a platform, and the ten checks in front of it. The gate is drawn once on this page, and this is it.
Credential classWhere it livesWhy
Money and email (accounting, mail send)Cloud custody behind mandatory human approval gates. The accounting connector stays sole token authority: its refresh tokens are single-use, so a second holder kills the first.A stolen key here spends money or sends mail as you
OAuth tokens (Google, Meta)Nango minds the tokens. The broker makes the calls.Refresh lifecycle is their whole job
Read-write action keys (CMS, ads)Vault, fetched by the broker at call time under a grant and an egress allowlist.Can change client sites. Needs the tightest leash
Read-only analytics keys (GA4, Search Console, SEO tools)With Airbyte, where they're used.Low stakes, revocable in minutes
Platform infrastructure secretsA separate compartment, unreachable from any client-facing path.A client-path breach must never reach these
The record itselfReferences and audit rows only, never secrets.The control-plane pattern

Custody is a plug, not a place. The broker, grants and audit core are fixed. The custody backend is per-tenant config: a cloud KMS-backed vault as the product default, a local-custody backend for a team that wants its own machine, or bring-your-own KMS for an enterprise tenant. High-stakes classes for cloud-only tenants are protected by mandatory human gates in place of physical locality.

LA.004The gate chain
LA.004The gate chain

Ten checks in front of every action. Fixed order. Fail-closed. One reason out.

Any one of the ten can refuse. The reason recorded is the first that applied. Step 06 is the one a person makes.

01

Round lock

Source-level. Open by design.

02

Master flag

Off by default.

03

Kill switch

Per client, or all.

04

Allowlist

Typed actions only.

05

Handler

Bound at build time.

06 · a person

Approval

On an exact payload.

07

Spend cap

Refused whole, never partial.

08

Egress

No open world.

09

Credential

Resolved, never held.

10

Grant

Live, scoped, unexpired.

Every step returns a refusal decision rather than throwing, so nothing can silently swallow it, and both allowed and refused attempts write an append-only audit row. Thirty-six client-platform operations are declared: 17 read-only, 4 stage-only, and 15 marked never. Publishing a tag container is one of the fifteen, permanently.

LA.005Whose key
LA.005Whose key

Whose account answers, and who pays.

Who pays for this AI request resolves from the verified identity of the person asking, never from a value the caller supplies. An operator's own build work rides a personal subscription, never stored in Hub. A teammate's default chat is free on the local model; premium quality means their own key, billed to them. Client-carrying production work runs on the organisation's own metered key.

WHOSE REQUEST IS THIS?resolved from a verified identity, never guessed and never supplied by the callerRESOLVERin the product · in useOne question, asked before anything is spentA missing key is a named refusal and an instruction to add one · never a quiet fallback to a shared key01 · THE OPERATOR'S SUBSCRIPTIONtoday, and every hour of the build02 · A KEY THEY BRING THEMSELVESa teammate, or a client who wants one03 · THE ORGANISATION'S KEYmetered, at launch, and one deliberate decisionOWNtodayYou — the operatorYour own sessions, never pipedthrough the HubBUILDnowThe build-out phasePlanning, testing and the firstruns ride thisCAPSzeroCaps ship at zeroInternal, test and pilotorganisations onlyMATEtodayA teammateThe free local model — no key, nocostBYOKbuilt · offTheir own keyFrom a developer console. Never aloginBILLbuilt · offBilled to themTheir console, their spend, theirlimitsFLEETplannedThe production fleetUnattended work, for payingclientsLAUNCHplannedThe launch phasePer organisation, not a migrationPROVnot yetNot provisionedTurning it on is a decision with acostNO ONE’S CREDENTIAL EVER ANSWERS SOMEONE ELSE’S REQUESTA SUBSCRIPTION-AUTHENTICATED WORKER REFUSES A JOB THAT CARRIES A CLIENT
Whose request is this? · in the product, in use
  • One question, asked before anything is spentAnswered from the verified identity of the person asking, never from a value the caller supplies. A missing key is a named refusal and an instruction to add one, never a quiet fallback to a shared key.
01 · THE OPERATOR'S SUBSCRIPTION · today, and every hour of the build
  • You — the operatorYour own sessions, never piped through the Hub
  • The build-out phasePlanning, testing and the first runs ride this
  • Caps ship at zeroInternal, test and pilot organisations only
02 · A KEY THEY BRING THEMSELVES · a teammate, or a client who wants one
  • A teammateThe free local model — no key, no cost
  • Their own key · built, not switched onFrom a developer console. Never a login
  • Billed to them · built, not switched onTheir console, their spend, their limits
03 · THE ORGANISATION'S KEY · metered, at launch, and one deliberate decision
  • The production fleet · plannedUnattended work, for paying clients
  • The launch phase · plannedPer organisation, not a migration
  • Not provisioned · plannedTurning it on is a decision with a cost
Whose credential answers a request, and which phase pays. Resolved from the verified identity of the person asking.
  • Refusal 01Cannot store anything shaped like a subscription login.
  • Refusal 02Cannot serve one person's request on another's credential.
  • Refusal 03Cannot quietly fall back to a shared key when a personal one is missing.
  • Refusal 04A subscription-authenticated worker refuses a job that carries a client.
LA.006Evidence
LA.006Evidence

Which of these have actually been made to fire.

A refusal nobody has ever triggered is a comment in the source code. These were provoked on a real client's data, on a test copy of the platform, and the measurements kept.

ClaimWhat was measured
Approving a recommendation records a decision and executes nothingAcross 23 approvals in two runs, the executor's counters were read before and after: zero executions, zero outcomes, master switch unset.
Both refusal guards fire when provokedA rejection with a blank reason, and a protected decision pushed through the general-purpose door. Both refused.
Nothing on the action path makes a network call36 operations declared, 59 broker and 186 executor checks passed with the network replaced by a trap that was never touched.
A client login returns only that client's rowsA 998-row, 24-table tenant rebuilt twice with zero drift, read through the portal's own path.
Client data cannot be written out of the machine it lives onTwo deliberate attempts to write client data into a tracked directory refused, one via a symlink.
Credential-shaped text is stopped mechanicallyLive on every commit. Fired twice on 18 August on placeholders. Neither gate was weakened.
No figure is published that wasn't re-checked against sourceEvery figure in 23 generated recommendations re-queried and matched exactly. An impossible control returned nothing.
Approval routing cannot bypass a human decision4,836 database policies and functions scanned: none reads a routing identifier when deciding whether an action may proceed.
One run's cost and duration measuredAbout $0.24 and just under three minutes, on a harness measurement, not a metered charge.

Weaker than it looks: the weekly and monthly briefs render from real data, but only 6 of 24 weekly regions carry real numbers. A visible zero there is an empty table, not a result. And the unsolved case, named: a hijacked agent holding a legitimate grant. Egress allowlists on grants are the answer being built.

LA.007Proof
LA.007Proof

Three layers of proof, because each catches a different lie.

10,021 passing

Unit tests

Pure logic and component behaviour. 454 files, zero failures, measured 31 August.

147, one contract each

Policy probes

Assert policy behaviour against a running database from both an allowed and a denied identity. A client-facing policy is tested as a client, over REST, never as a superuser.

Real Chrome over CDP

Browser smokes

A green build doesn't prove the app loads. Anything touching focus, drag or rects gets a real browser.