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.
Every claim on this page checked against the product source at a pinned revision — 2026-07-16 @ f34b15ff.
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:641,707-729, auth.ts:1462-1466).
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 hard-capped at observer or assistant no
matter what the parent holds — a full parent cannot mint a full child,
the ceiling is structural, not relative — 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:1244-1263,
tools/delegationTools.ts:82-94). 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:457, 706). 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.
Recent hardening fails closed at boundaries that used to be easy to get wrong. A supplied blank or invalid bearer is a rejected credential, not an anonymous fallback, and persistence does not proceed when authentication is invalid. The test bootstrap also pins a test-only data directory, so a test run cannot quietly attach to a developer’s live local database. These are deliberately unexciting controls: they make a mistaken credential, path, or test environment stop rather than become an implicit authorization.
Skills are the sharpest case. A skill is instructions another agent loads and follows, so publishing one injects instructions into every sibling agent’s context. Agents may only draft; no agent profile can publish. That boundary is Skills and instruction trust.
What your agent does with it
// Live response from this daemon, captured 2026-07-17 (read-only):
const perms = await sophia.get_permissions({});
// → { permission_profile: 'full', entity_scope: 'all',
// write_approval_mode: 'pre_approved',
// approved_tools: [/* the full catalog — 179 tools at this capture */],
// request_count: 336 }
// 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.
One daemon, one owner — enforced by the kernel
Connection trust assumes one more thing this page used to leave implicit: that there is exactly one daemon answering. There wasn’t, once. Under memory pressure an accidental second copy of the daemon kept holding the encrypted state database’s lock after it had stopped serving, ignored every request to shut down, and drove the managed service into eleven busy-database restarts. Nothing was malicious — two well-behaved daemons both believed they were in charge, and the system had no way to insist that only one may be.
Now it can. Before the daemon opens either database or binds its port, it
must acquire a kernel-enforced lock (flock) on a file co-located with the
state database, inside a data directory whose ownership and permissions are
verified first — a legacy too-permissive directory is quietly tightened
rather than bricked. A process that cannot acquire ownership fails fast
with a named reason — another live owner, an untrusted directory, a
lockfile that isn’t what it should be — and never touches the database at
all: no contention, no busy-storm, exit. The refusal codes are a frozen
contract, each with its own exit code, so a supervisor can tell “someone
already owns this” from “something is wrong with this install.” The hostile
cases are proven by test rather than promised: a symlinked or wrong-owner
lockfile is refused, a file swapped between check and open aborts the boot,
a child process cannot inherit ownership — each guard demonstrated by
removing it and watching the attack succeed before restoring it. Every
database-opening path, including the one that mints credentials, routes
through the single owner. The guarantee is scoped, and the scope is written
down: it holds under an explicit, owner-ratified threat model — the
accidental same-user duplicate that caused the incident — not against every
adversary imaginable, and where that boundary sits is recorded rather than
implied away.
On that guarantee sits workspace identity. A local broker — refusing to run at all unless the single-owner capability is proven held — issues each agent workspace a durable lease over an authenticated local socket, the caller’s identity checked at the operating-system level rather than taken on its word. Leases survive daemon restarts, an agent’s coordination work is bound to its lease, and resuming one is governed by the same authority as a fresh issue — identity that evaporates on restart is not identity, and identity issued by a daemon that might not be the real one is not identity either.
Work claims carry receipts, not reports
The newest layer governs not what an agent may do but what it may claim.
Saying a change was verified is no longer something a report can assert — the
change carries a receipt bound to the exact commit: its hash, its tree, its
precise list of changed paths. An independent checker re-derives every one of
those facts from the repository and refuses the receipt if any drifted, so a
verification cannot be moved onto a commit it was not run against. Claims
from a dirty working copy are rejected outright — commands run against
uncommitted bytes did not test the commit being claimed. Deleting a file
cannot shrink coverage, because removals count as changes; and discovering
nothing to check fails rather than passing quietly. The separate attestor
re-executes the claimed commands itself instead of taking the runner’s word —
evidence and beneficiary are never the same party. The honest limit is
enforced in the schema rather than footnoted: the receipt’s field for
external enforcement only accepts the value "unverified", because no CI
gate yet forces any of this to run, and the system is not permitted to claim
an authority it does not have.
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:1819-1823) —
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.