AI & agent access
Agents are not special actors.
An agent authenticates, resolves to a service-account identity with a role, and then travels the same permission, approval, and audit path as any human admin. There is deliberately no separate AI-governance bolt-on — the key and the role are the control.
One key, one boundary — the authz trace
Agent key → service-account identity → role → the same /api/graphql authz check a human dashboard click crosses; owlie_whoami returns the identity id.
An MCP server is not a backdoor
Trace one call.
The security question isn't what an agent might do — it's what path the call takes. In Owlie it takes the same one a person does. Creating the key and granting the role is the act of delegation; there is no second, agent-shaped control stacked on top of it.
- 01
An agent authenticates.
An API key over the MCP server, or the signed-in human's session cookie in chat. Nothing is minted specially for AI.
- 02
The credential resolves to an identity.
A service account or the human themselves — an ordinary identity carrying an ordinary role. The key plus the role is the delegation.
- 03
Every call crosses one authz boundary.
The same per-actor check at /api/graphql that governs a dashboard click governs the agent's call. There is no second seam that agents pass through.
- 04
The actor is never hidden.
owlie_whoami returns the exact identity id the agent is running as. What it can touch is what that identity's role already permits — nothing more.
No agent inbox. No per-key approval modes. No second control plane. The decision is on record: agent access adds no agent-specific authorization machinery — the key plus the role is the control.
Two ways in, one core
A third-party MCP client and Owlie's own assistant dispatch through the same registry.
An external agent connecting over MCP and the chat at/admin/assistantroute through one 33-tool registry, over the one typed SDK underneath. The assistant runs as a durable agent — one per signed-in user — so a paused mutation resumes cleanly instead of starting over. Composable by construction, not a chat demo grafted onto a real API.
Two ways in, one core
An external MCP client and the /admin/assistant chat both dispatch through the identical 33-tool registry, over the single typed SDK (25 namespaces) an external developer imports directly.
The capability core
Four building blocks, one shared client.
The access layer is a small set of composable pieces sitting on the typed SDK. Each one is bounded the same way — by the caller's identity and role — because none of them adds a capability the SDK doesn't already carry.
- 01
The MCP server
33 discrete tools from one in-process registry.
What it does
A stateless JSON-RPC endpoint over HTTP exposes owlie_whoami, owlie_describe, owlie_execute, plus 15 single-object get/create/update tools and 15 single-target action tools. There is no list or search tool by design — anything that spans a collection or a multi-step workflow belongs in code-mode. External MCP clients and Owlie's own chat dispatch through this identical registry.
How it's bounded
Every tool call resolves to the caller's identity and role. A tool exists only where the underlying SDK operation does; the registry adds no capability of its own.
- 02
Code-mode — owlie_execute
Model-authored TypeScript, run in a network-isolated sandbox.
What it does
The model writes a short TypeScript program that runs inside a V8 sandbox with outbound networking switched off — fetch and connect throw. The only way out is a typed owlie.* connector spanning 230+ methods across every SDK namespace: reads, aggregate counts, core mutations, and a raw-query escape hatch. One program does a whole multi-step job instead of round-tripping a tool call per step.
How it's bounded
The API key never enters the sandbox — it lives host-side only. A per-run data budget caps a single run at 50 tool calls and 10,000 rows; a breach throws a steering error that tells the model to narrow, count, or sample rather than silently drain partial data and treat it as complete.
- 03
The typed SDK
@owlieapp/sdk — 25 domain namespaces, one shared client.
What it does
A typed TypeScript client over 25 namespaces — identities, groups, policies, sync, reviews, provisioning, and the rest — mirroring the service layer, with rawQuery as the sole raw-GraphQL escape hatch. This is the same artifact both agent surfaces run on and the same one an external developer constructs directly with an API key. Not a chat demo bolted onto a separate real API — one capability core, three ways to reach it.
How it's bounded
The SDK carries no elevated path. It issues the same GraphQL a human session issues and is bounded by the same per-actor authz.
- 04
owlie_describe
A discovery tool, so the model plans real code.
What it does
owlie_describe returns the live method catalog — each method's name, one-line description, canonical input schema, and whether it requires approval — alongside 16 cross-cutting domain guides (grants, reviews, campaigns, policies, integrations, sync, provisioning, and more). The model reads the real shapes and writes against them instead of guessing.
How it's bounded
Discovery reflects only what the caller can already do; describe surfaces the catalog, it does not widen it.
One tag, three behaviors
One sensitivity tag. Three surface behaviors.
Every mutating operation is tagged once on the SDK. That single tag — not a per-surface rulebook — decides how each surface pauses. Change the tag in one place; every surface follows.
Chat
An approval card.
A single well-known mutation pauses as an inline card the human answers in one click. The card reads the sensitivity tag straight off the SDK to render a Required or Recommended badge — the same source of truth, not separately-authored UI copy.
Code-mode
A durable pause-and-replay.
An open-ended run pauses the whole durable execution and resumes by replaying it. Steps already applied are never re-run, so a mid-run approval never double-applies work.
MCP
A service-account posture.
The same tag informs the external MCP surface, where mutations run under the service account's role. One flag on the SDK, read by every surface — no per-surface rulebook to drift.
One tag, three behaviors
A single sensitivity flag on the SDK fanning out to a chat approval card (Required/Recommended badge), a durable code-mode pause-and-replay, and an MCP service-account posture.
The agent shows you the queue
You make the call.
For reviews, tickets, and policy exceptions, the assistant renders the queue — but it never decides. The decision cards submit directly to Owlie under your own session, and the assistant is handed only the outcome. There is no decision-mutation on the model-callable surface at all: not a prompt instruction it's asked to obey, a capability that isn't there.
“These decisions are yours — they submit directly to Owlie; the assistant only sees the outcomes.”
A ledger, not a promise
Coverage you can count, guarded by CI.
Every product write has a row in a coverage map, and a test fails the build the moment a new mutation ships without one. Today roughly 95% of product GraphQL writes are reachable from the agent surface, with a short, named, permanent exclusion list — not an open-ended gap nobody tracks. We won't claim an agent can do everything an admin can; we'll show you exactly what it can reach.
The coverage ledger
The parity map + CI guard: product GraphQL writes marked covered vs. a short named-exclusion list (credential ceremonies, file pickers, tenant deletion, self-state, the whole RPC lane); the build fails on any untracked mutation.
What it deliberately won't do.
Some of these are gaps we'll close and some are lines we drew on purpose. Here's which is which, stated plainly.
No unattended sessions.
Every action still requires the interactive human whose authority is being spent to be present. This is a self-confirmation model, not remote oversight — there is no run-this-agent-overnight mode, by choice.
MCP mutations run under the role alone, today.
On the external MCP surface, mutations execute under the service account's role without a per-call human prompt — only chat has approval cards. Per-operation approvals are deferred to a separate, actor-agnostic operation-approvals program, and that is not shipped yet.
A short, named, permanent exclusion list.
Eight GraphQL operations sit out by design — credential ceremonies, browser-inherent file pickers, tenant deletion, UI-local self-state, and a deprecated alias — along with the entire internal RPC lane the SDK's transport does not even reach. These are permanent gaps, not not-yet-built.
Decisions never move to the model.
The agent can render a review, ticket, or policy-exception queue, but it never commits the outcome. That exclusion is categorical — independent of how much of the write surface is reachable.