Trulana is a local-first personal data broker. All data stays on-device in an encrypted database. Third-party agents access user context through two transport layers — REST (localhost HTTP) and MCP (stdio) — both of which are gated by the same shared services.
┌────────────────────────────────────────────────────────┐
│ Transport: REST (shelf, 127.0.0.1:8432) │
│ Transport: MCP (stdio, TRULANA_MCP=1) │
├────────────────────────────────────────────────────────┤
│ Shared Services (policy enforcement) │
│ ├── AuthService → token issuance + audit │
│ ├── ContextQueryService → vault + redaction + audit │
│ ├── TokenManager → in-memory, 15-min TTL │
│ └── AutoRedactEngine → 3-stage PII stripping │
├────────────────────────────────────────────────────────┤
│ Encrypted Storage │
│ ├── sqflite_sqlcipher → AES-256 encrypted SQLite │
│ └── flutter_secure_storage → Keychain (macOS) │
└────────────────────────────────────────────────────────┘
- Bound to
127.0.0.1only — kernel-level rejection of non-localhost traffic. - Defense-in-depth middleware double-checks remote address.
- CORS restricted to
http://localhost. - Agents must call
POST /api/v1/auth/requestto receive a scoped, time-limited token before any data access.
- Runs as a subprocess launched by the MCP client.
- First contact enrolls a raw Ed25519 public key as a pending agent. The Gatekeeper UI shows its SHA-256 fingerprint alongside requested scopes.
- An approved caller requests a random, 60-second in-memory challenge and signs the exact returned payload. The challenge is removed before verification, making every proof attempt single-use.
- Only a valid signature from the approved key reaches
AuthServicetoken issuance. The legacy direct MCP token tool fails closed. - Key-bound identities are also rejected by the REST app-ID route unless the internal MCP proof flag is present, preventing cross-transport app-ID impersonation.
- Signature failures attributable to a known challenge are recorded as blocked audit events; unknown/replayed challenge IDs are safe-logged without an agent attribution.
- Any local process can start the stdio server, but it cannot mint a token for a key-bound identity without proving possession of the approved Ed25519 private key.
- First-contact agents are persisted as
pendingwith requested scopes and an MCP key fingerprint. No challenge/token is issued until the user approves; the user may narrow scopes before approval. - Approved tokens carry only the scopes the user confirmed; re-auth cannot broaden the grant.
/api/v1/context/queryand MCPsovereign_query_contextrequire thecontext.readscope./api/v1/monetize/lease_offerrequires themonetize.writescope.- Every access attempt (successful, rejected, or pending) is written to the encrypted audit log.
- Tokens are in-memory only (RAM), 15-minute TTL, and do not survive process restart.
- Per-agent rate limiting on MCP: The REST path has sliding-window rate limiting. The MCP stdio transport currently does not enforce per-agent rate limits.
- Tamper-evidence on the audit log: The audit table lives in the encrypted database. An attacker who obtained the database key could modify rows without detection (no hash chain, no append-only journal, no external attestation).
- Host/process attestation: MCP verifies possession of an approved Ed25519 key. It does not attest the host OS or the integrity of the MCP client process, and it cannot protect a private key already compromised on that host.
- Cryptographic REST identity: REST-only approvals remain app-ID based. An identity enrolled with an MCP key cannot fall back to that path.
The Ed25519 MCP implementation is newer than the signed v1.2.1 binary and is not a shipped release property until a later signed build is published.
sqflite_sqlcipherprovides AES-256 full-database encryption.- The encryption key is a 256-bit value generated from
Random.secure()(maps to/dev/urandomon POSIX). - The key is stored via
flutter_secure_storage:- iOS: Keychain with
KeychainAccessibility.first_unlock_this_device(non-migratable between devices; items stored by iOS under hardware-protected storage on supported devices). The key itself is a symmetric AES key held as a generic Keychain item — it is not generated or operated on inside the Secure Enclave. The Secure Enclave is only engaged bySecKeyCreateRandomKeywithkSecAttrTokenIDSecureEnclave, which this code does not use. - Android:
EncryptedSharedPreferences. - macOS: default Keychain options (no explicit hardware-key binding).
- iOS: Keychain with
- The key never appears in logs, environment variables, or application memory beyond the initial retrieval.
- Generated once on first launch.
- Retrieved from Keychain on each subsequent session.
KeyManager.deleteKey()makes the database permanently irrecoverable (account reset only).
Every outbound response passes through three sequential stages:
- Regex Filter — Deterministic stripping of SSNs, emails, phones, credit cards, IP addresses.
- NER Processor — Named-entity recognition for person names, locations, organizations (mock keyword matching in MVP; abstract interface ready for on-device LLM).
- Privacy Filter — Level-dependent generalization:
- Standard: pass-through after stages 1+2.
- Strict: additionally generalizes times, currency amounts, ages.
- Paranoid: all of strict plus proper-noun catch-all and noise disclaimer.
SafeLogger enforces a strict output contract:
- Logged: route, HTTP method, status code, agent ID, action type, redaction count, privacy level, vault hit count, TTL.
- Never logged: token values, raw query text, response data, PII, database paths, encryption keys, error stack traces containing user data.
- MCP mode: all log output goes to stderr. stdout is reserved exclusively for JSON-RPC protocol messages.
- Test coverage:
test/security/log_hygiene_test.dartasserts that no SafeLogger method can emit tokens, PII, keys, or database paths.
Every agent interaction produces an AuditLogEntry in the encrypted database:
log_id— UUIDagent_id— which agent made the requestrequest_intent— structural description of the actionaction_taken— approved, blocked, redacted, or negotiatedtimestamp— ISO 8601
Both REST and MCP paths write audit entries through the same AuthService and ContextQueryService. There is no path that bypasses audit logging.
Release signing is performed manually on the built artifact, not inside the Xcode project. The local project uses CODE_SIGN_IDENTITY = "-" (ad-hoc) for developer iteration; release binaries are re-signed with a Developer ID and notarized as a separate step. Status below reflects the current public release, v1.2.1 (release notes):
- Code signing — signed with
Developer ID Application: Adam Gurley (7S7QY5ND9Q). Verified viacodesign -dvvv(Authority chain ends at Apple Root CA;TeamIdentifier=7S7QY5ND9Q). - Hardened runtime —
flags=0x10000(runtime). Required for notarization. - Notarization — stapled notarization ticket. Verified via
xcrun stapler validate("The validate action worked!") andspctl --assess --type execute("accepted, source=Notarized Developer ID"). - Universal binary — Mach-O universal (x86_64 + arm64).
- Entitlements review — Only the following are present on release:
com.apple.security.app-sandbox— requiredcom.apple.security.network.server— required for localhost bindingcom.apple.security.cs.allow-jit— required for Dart VM (debug only)
- Do not distribute unsigned debug builds. Debug builds emit the Dart VM service URL to stdout, include assert-enabled code paths (e.g.
debug_ttl_seconds), and are not hardened. Policy stands.
Release lineage (for provenance during diligence): v1.0.0 was the first notarized release but predates the hardened approval / scope-enforcement model. v1.1.0 was a UI milestone with no attached binary. v1.2.0 shipped the correct security model but mislabeled its bundle/UI version as 1.0.0 due to a stale Flutter version-config cache. v1.2.1 is the current correct baseline and is the version all other documentation refers to.
Open release-hygiene items (not code-signing, but related):
- Automate signing + notarization in CI.
.github/workflows/ci.ymlnow runs analyze + test and builds an unsigned macOS bundle; it does not codesign, notarize, or staple. Signing is performed manually by the maintainer holding the Developer ID key. A future tag-gated release workflow should invokecodesign --timestamp --options runtime,xcrun notarytool submit --wait, andxcrun stapler stapleso releases are reproducible and auditable. - Developer ID is personal (
Adam Gurley), not organizational (AdamsLocal LLC). The landing page and LICENSE assert trademark/ownership by AdamsLocal LLC; the signing identity on shipped binaries is personal. Transfer to an org Developer ID is recommended before any paid commercial release.
The development branch contains an in-progress Context-as-a-Service monetization layer (ZkCommitmentEngine, WatermarkEngine, and the /api/v1/monetize/settle route). It is not part of the signed v1.2.1 binary and is excluded from the trust model above. Security-relevant caveats for anyone reviewing that code:
- The SHA-256 commitment primitive and the zero-width watermark round-trip are unit-tested in isolation and behave correctly there.
- The wired HTTP flow is not functional end-to-end:
/monetize/lease_offerreturns a commitment hash but does not persist the salt, so/monetize/settleregenerates a fresh salt and the original hash cannot be verified. /monetize/settlecurrently checks only for a valid token, not for themonetize.writescope that/monetize/lease_offerrequires.- The zero-width "watermark" is steganographic provenance tagging, not an access control. It is trivially strippable by any normalizer and must not be relied on as a security boundary.
Do not represent this layer as production until commitment persistence, scope enforcement on settlement, and end-to-end tests land.
| Risk | Severity | Mitigation | Status |
|---|---|---|---|
| No per-agent rate limiting on MCP | Low | MCP is a local subprocess, not network-exposed. User approval and Ed25519 proof gate key-bound access. | Documented |
| MCP proof is not host attestation | Medium | Fresh single-use Ed25519 challenges prove possession of the approved key; compromise of that private key or host remains out of scope. | Documented |
| Mock NER (keyword-based, not AI) | Medium | Regex filter catches structured PII. Mock NER catches common names/cities/orgs. Real on-device NER is a future phase. | Documented |
| Audit log is not tamper-evident | Medium | Audit rows are encrypted-at-rest inside the SQLCipher database but have no hash chain or external attestation. An attacker with the DB key could modify rows without detection. | Documented |
| Debug build stdout contamination | Low | Release builds are clean. test_mcp.sh validates. |
Validated |
| TokenManager is in-memory | Low | By design — tokens don't survive restart. Limits blast radius. | By design |
| No CI signing pipeline | Medium | Release signing + notarization is performed manually by the maintainer holding the Developer ID key. Human-dependent; no audit trail of the signing step. Future CI workflow should automate it. | Open |
| Experimental CaaS escrow wired into routes | Medium | The /monetize/settle route and ZK/watermark engines exist on the development branch but are not in the signed v1.2.1 binary and are not functional end-to-end (commitment salt not persisted across requests). Settlement also does not currently enforce a monetize.write scope check. Must not be presented as production. |
Open / out of scope |
Test counts are not pinned here. Run flutter test for the current number. The suite covers:
| Area | What Is Asserted |
|---|---|
| Redaction pipeline | All supported PII types are stripped at each privacy level |
| Service loop | Token minted with context.read → query → redact → audit, end-to-end, for multiple content flavors |
| Scope enforcement | Tokens without context.read are rejected; tokens without monetize.write cannot submit lease offers |
| Approval flow | Defensive paths: empty scopes, not-found, not-pending, non-pending rejection does not revoke tokens |
| Log hygiene | No tokens, PII, keys, or DB paths appear in log output |
| Auth consistency | Singleton identity, identical validation across transports, identical rejection codes |
| MCP caller proof | Valid Ed25519 proof, wrong-key rejection, exact-payload binding, expiry, malformed-key rejection, and single-use replay prevention |