August 23, 2026 · 7 min read
Two Trust Boundaries between a Public DMG and MCP Writes
How intent authorization and content-host policy protect user records and private networks even when the implementation is fully inspectable.
Anyone can download and inspect a public DMG. They can read binaries, frameworks, resources, entitlements, strings, sidecar tables, and MCP tool schemas. Security therefore cannot depend on keeping the implementation secret. Knowing the protocol must not grant authority to modify user records, and supplying a URL must not make a victim's Mac contact a private network.
This English edition is grounded in the fresh security record, AgentIntentTrustTests, the ContentFetchHostPolicy source, MCP security guidance, and the notarized-build re-audit. The trust-boundary diagram is localized from the same semantic spec rather than redrawn as a generic English box row.
Begin with a public-implementation threat model
The audit assumes an attacker knows the sidecar schema, staging convention, URL consumers, and tool inputs. Code signing does not make those details secret. It proves who signed the code and whether bytes changed. Protection comes from account ownership, append permission, canonical paths, host policy, encryption, and server authorization.
The boundary in the center is not decoration. Untrusted input on the left must pass a policy component before it can reach user records or a public network destination on the right. MCP intents and content URLs are different inputs, but both can trigger a privileged component to act on behalf of an untrusted source. This is the confused-deputy shape described in MCP security guidance: possession of a protocol or input format is mistaken for possession of the authority held by the component that processes it.
Treat the sidecar as a write interface
The intent sidecar is a local SQLite database, but it is not a harmless cache. The app executor reads rows and calls the real item repository operations for create, note, status, and retract. A forged row can therefore reach the normal delete path and propagate a real deletion through sync. Validation at the helper boundary is not enough. An attacker who writes directly to the sidecar can bypass helper argument checks. Authorization must be repeated where the app promotes the intent into a user-library operation.
A staged path carries read and delete authority
Applying attachmentStagedPath means reading bytes into an attachment and deleting the staged file afterward. The executor must canonicalize the path, require it to remain under the staging root, and resolve symlinks before either operation.
assertOutsideStagingIsRejected()
assertSymlinkEscapeIsRejected()
assertNoItemWasCreated()
assertOutsideFileStillExists()
The test deliberately uses bytes with a valid PNG signature. A file-type check can succeed while authorization must still fail. This separates content validation from path authority.
Ownership and operation are checked at apply time
A row that contains a task key and actor label does not become owned by that actor. The executor verifies that the anchor belongs to an agent-created record, that the account scope matches, that the intent kind is allowed by append permission, and that the idempotency key is not an unauthorized replay. The sidecar can preserve an intent while an account is unresolved, but preservation is not execution. The trust decision occurs when the library write is about to happen.
Treat content URLs as network authority
Shared records, pasted HTML, and public content may contain URLs the local user did not choose. Link HTML, hero images, favicons, JavaScript rendering, and Markdown images can cause the victim's Mac to make requests. A loopback or private address can have a state-changing effect even if the attacker never reads the response.
ContentFetchHostPolicy applies to addresses chosen by content. It does not apply the same rule to a backend URL the user explicitly configured for self-hosting, because local-network access is a product feature in that path.
| Consumer | Policy placement |
|---|---|
| Link HTML | Immediately before fetch |
| Hero image | Before image download |
| Favicon | After candidate URL construction |
| JavaScript renderer | Before page load |
| Markdown image | Before provider selection |
Every consumer must use the same decision. A forgotten fetch path is a bypass even when the central policy is otherwise correct.
Prefer the most dangerous valid address interpretation
0177.0.0.1 is interpreted differently by two accepted parsers. Network.IPv4Address can read it like public 177.0.0.1, while inet_aton reads it as loopback 127.0.0.1. If validation and connection use different parsers, a public decision can lead to a loopback connection.
input Network inet_aton decision
0177.0.0.1 177.0.0.1 127.0.0.1 BLOCK
127.0.0.1 127.0.0.1 127.0.0.1 BLOCK
8.8.8.8 8.8.8.8 8.8.8.8 ALLOWThe policy collects every interpretation the connection stack may accept. If any one is loopback, link-local, private, CGNAT, unspecified, or reserved, the request is rejected. The rule is intentionally conservative. It would be unsafe to choose the most permissive interpretation and hope the networking stack chooses the same one.
DNS rebinding remains a separate boundary
A hostname can resolve to a public address during validation and a private address when the socket connects. A URL-string policy cannot fully solve that case without observing the actual peer address in the transport layer. We record the current scope as literal and directly resolved private-address protection, not complete rebinding defense. Stating the limit matters because “SSRF fixed” can otherwise be read as a claim about every resolution and redirect behavior. The residual risk remains attached to the architecture.
Apply the same authority principle to both gates
The Intent Trust Gate asks whether this caller may mutate this record. The Content Host Policy asks whether this content may choose this destination. Both questions are about authority, not merely whether the input is well formed. MCP security guidance reaches the same conclusion through confused-deputy and token-handling examples. A component must not lend its broader privilege to a caller that only knows how to invoke it. The shared principle lets us review new features consistently. A new tool, sidecar field, redirect handler, or media provider adds a trust edge. That edge must terminate at an existing policy gate or introduce a new, testable one.
Rebuild the artifact after fixing the source
A source branch with green tests does not change the DMG already available to users. We built a new signed and notarized artifact containing the Intent Trust and Host Policy changes, then repeated the same threat-model audit against the new digest. The verification sequence was: reproduce the failing input, apply the policy, pass the focused tests, package a new DMG, verify Gatekeeper and helper runtime, and repeat the audit. We did not attach new results to the old binary.
| Layer | Failing input | Passing observation |
|---|---|---|
| Intent trust | Forged owner or staged path | No item, no external read or delete |
| Host policy | Loopback, private, octal literal | Request rejected before consumer |
| Artifact | Old DMG digest | New signed, notarized digest |
| Runtime | Source-only assumption | Installed helper and app checks |
Audit new inputs as architecture changes
A new MCP write tool, sidecar column, or URL consumer creates another trust edge. Before shipping, we identify which component receives the input, what authority it has, and which gate constrains that authority. The dominant question is component and boundary placement, so architecture is the accurate diagram type. The 0.4.4 renderer now also routes non-adjacent component edges around intervening nodes, and the audit checks endpoint attachment and edge-node intersections rather than trusting labels alone. The English diagram was rendered with justsend-blog 0.4.4, which records node bounds and edge route points, uses distinct branch attach points, and fails the audit when an edge crosses a non-endpoint or travels through an endpoint node.