Shipped: scoped retrieval, the permission model, and durable ingestion
A few days ago we wrote about the platform direction FirstHQ was forcing. Here's the first wave, shipped: client/team-scoped retrieval with document ACLs, a discovery-vs-mutation permission model with one review queue, and durable, resumable, incremental ingestion.
We recently wrote that building FirstHQ on Vocion is a forcing function —
that a real product tells you the truth about what the platform needs next. Here's the first wave of
that work, shipped across v1.26–v1.29.
Scoped retrieval + document ACLs (v1.26)
Retrieval was org-scoped, which is the right floor and the wrong ceiling: an agency has many clients
inside one org, and one client's documents must never surface in another's work. So
knowledge_document and knowledge_chunk now carry client_id / team_id scope, and
RetrievalService.search takes clientId / teamId / allClients with a safe-by-default ACL:
- a client-scoped search returns shared docs plus that client only — never another client's
- an unscoped search returns shared docs only (client docs never leak)
allClientsis an explicit admin escape hatch
Enforced on both the vector and keyword arms, with cross-client isolation tests.
The permission model: discovery vs. mutation (v1.27–v1.28)
Reading is not writing. The keystone is one decision point — principal × scope × resource × mode × gate:
- discover (read/search) is governed by scope + ACLs (above)
- mutate (send/change/publish) is governed by an action grant plus the autonomy gate — does it need human approval?
The mission autonomy ladder now delegates its gate to this single rule, and there's one review
queue: pending skill runs, paused workflow runs, and missions awaiting review unify into a single
list, so a gated action is reviewed the same way no matter which plane produced it — the MCP tool
plane or the app's API. authz.enforce() is the call site a mutation passes through.
Durable, incremental ingestion (v1.29)
"Connect Google Drive" is easy until the crawl is 100k files and the request times out. Ingestion now
keeps a source_sync_checkpoint per source: a since watermark so connectors fetch only what
changed, a resume cursor, and run state that's preserved on failure — a retry picks up from
the last good point instead of re-crawling from zero. Full syncs prune deletes; incremental runs don't.
What's next
Wrapping the sync in a Temporal workflow (so it survives restarts and runs on a schedule), the write API + MCP-over-HTTP so apps and agents drive the runtime through one shared authorization layer, and unifying authored + ingested context. The pattern holds: build it in the platform, surface it in the product.