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.