The security model
Every agent connection is minted deliberately by the owner (or, for delegated workers, by a connection the owner already trusted), carries a profile and an entity scope enforced at the query layer, and reaches a fixed set of elevated writes through one approval gate no profile can skip.
Verified 2026-07-09 @ dfd671a3
What this is
The security model is how the daemon decides what an agent connection can see and do: nothing is granted by dropping a token in a config file. Every connection is minted through a deliberate act — the owner in the owner UI, or, for a delegated worker, a connection the owner already authorized to spawn workers — and from that moment on carries a profile, an entity scope, and (for delegated children) a lineage that the daemon checks on every request, not once at setup.
Why it exists
An MCP connection can read the graph and, depending on profile, write to it — so the question “which connection is this, and what did the owner actually authorize it to touch” has to be answered the same way every time, not inferred from context. The Trust Covenant states the resulting guarantees (scoped access, an elevated-write gate no profile bypasses); this page is the mechanism underneath them — minting, profiles, scope, delegation, and the two failure modes that would matter if any of it were soft.
How it works
Minting is a request the owner (or an authorized delegator) makes, not a
file the daemon reads at boot. A human mint goes through POST /mcp/connections, whose ownerSession flag is computed server-side from
the authenticated request context — “never read from the request
body/headers/query” — and never from anything an agent could forge
(proxy/src/mcp/index.ts:602-613, auth.ts:1255-1260).
Three profiles gate what a connection can do without a fresh human
gesture. observer is read-only; assistant and full can write.
Regardless of profile, a fixed set of elevated writes — remember_fact,
create_entity, begin_import_session, revert_mutation — always needs
an explicit owner-approval round trip; only a full, pre_approved
connection skips approval for everything else (ELEVATED_WRITE_TOOLS and
needsApproval(), proxy/src/mcp/approval.ts:402-437).
Entity scope is enforced in the query itself. scopeClause /
factScopeClause / wikiScopeClause (proxy/src/mcp/scope.ts) turn a
connection’s entity_scope into a SQL fragment spliced into every scoped
read and write: 'all' adds no filter, an explicit list becomes AND e.id IN (?), and an empty list becomes AND 1=0 — fail-closed, not
fail-open, when a scope somehow parses to nothing (scope.ts:23-35).
Delegated minting lets an authorized connection spawn its own
workers, always more restricted than itself. sophia.mint_worker_key
requires the calling connection’s own can_delegate flag — set only at
that connection’s own mint time by a human — and the child it creates is
capped on every axis: profile at or below the parent’s, entity_scope an
explicit subset of the parent’s, TTL defaulting to 60 minutes and capped
at 240, and can_delegate forced to 0 so a worker can never delegate
again (proxy/src/mcp/mintPolicy.ts:11-14, auth.ts:1262-1269,
tools/delegationTools.ts:83-92). A parent may hold at most 10 live
children and mint at most 20 per hour, both counted from the audit log
rather than an in-memory counter so the caps survive a restart
(mintPolicy.ts:75-78). sophia.complete_worker and
sophia.revoke_worker_key are parent-only — a worker cannot end its own
lifecycle, and revoking a parent cascades to every live child.
A bearer is a secret the daemon never has to leak, because it never
keeps it. Only bearer_token_hash, a SHA-256 digest, is stored; the raw
token is returned once, at mint time, and re-derivable by no one who
didn’t see that response (auth.ts:431, 656). A lighter, opt-in audit
trail covers both reads and instrumented writes on the same table: a
fixed registry of tools — reads like fetch_document and
get_entity_profile, writes like remember_fact — logs one row per
touched entity to subscriber_agent_entity_access_log, rotated to 30
days, plus a permanent per-entity counter, all queryable back through
sophia.entity_access_history and sophia.agent_trail
(proxy/src/mcp/tools/accessLog.ts, agentAccessLog.ts). Tools outside
that registry stay unlogged by design, not by oversight: “silent
over-logging is hard to undo.”
Fail-closed posture at the transport layer — the daemon refusing to bind to anything but loopback, the provider registry throwing instead of guessing — is Trust Covenant’s own pillar and isn’t re-derived here.
What your agent does with it
// Live response from this daemon, captured 2026-07-09 (read-only):
const perms = await sophia.get_permissions({});
// → { permission_profile: 'full', entity_scope: 'all',
// write_approval_mode: 'pre_approved', approved_tools: [...166],
// request_count: 3422 }
// Schema-derived from mintWorkerKey's live Zod schema — a mint is a write,
// so it was NOT called in this read-only research session:
await sophia.mintWorkerKey({
worker_name: 'DocWorker-1',
entity_scope: ['e-1'], // must be a subset of the caller's own scope
ttl_minutes: 60, // capped at policy.maxDelegatedTtlMinutes (240)
task_label: 'summarize inbox documents',
});
// → { connection_id, bearer_token, expires_at, spawn_id, worker_call_recipe }
// bearer_token appears in this response exactly once. get_permissions is the identity check every agent can run on itself
before assuming a capability. mint_worker_key’s shape comes straight
from its live tool schema — the call itself would create a real,
attributable connection, so it stays undialed here by design.
Boundaries
The elevated-write gate’s only code-level bypass, conn.is_portal, exists
and is exercised by one test, but every live connection-creation path
hardcodes it to 0 — the same dormant carve-out Trust
Covenant documents in full; this page doesn’t
restate it.
Beta hardening is ongoing, and two gaps are known, deliberately-accepted
local-trust tradeoffs rather than oversights: the owner’s own browser
session can mint an elevated or all-scope connection without the
grant-token gate every other mint path requires (auth.ts:1255-1260) —
that’s the mechanism by which the owner UI can mint its own first
connection at all — and the daemon’s own REST layer, the owner UI’s own
plane, is scope-blind by design: its session-key auth branch hardcodes
scope = 'all' for every request it authenticates
(proxy/src/middleware/auth.ts:131-182), so entity scopes are enforced
on the MCP surface but not there. That’s a deliberate local-trust posture
during beta — the REST plane binds to loopback and answers only to your
own session — and narrowing it is tracked work. Both gaps are candidates
for tightening before this ships to anyone running multi-user or
network-exposed. The full connection lifecycle and tool catalog they
operate over is MCP Surface; wiring an agent
into a connection in the first place is Connect.