Security Model
This describes SenseBridge’s security posture and threat model — what could
go wrong and what mitigates it — not a list of live findings. Specific,
unresolved security findings are tracked privately rather than published
here; see SECURITY.md
for how to report one.
Why the attack surface is small by construction
SenseBridge’s MVP has no backend, no accounts, and no telemetry by default —
see
docs/ARCHITECTURE.md.
That is an architectural fact, not a policy promise, and it removes most of
the classic attack surface before any code is written: no server to breach,
no database to leak, no session or auth system to attack, and no cross-user
data to leak between accounts, because there are no accounts. What remains
in scope is what’s on the device itself, and what’s used to build and ship
it — both covered below.
The outbound paths. Two, both opt-in and off by default, so an install that has not been touched has exactly the surface described above.
Since 2026-07-31 the app links Sentry for crash reporting, starting only when
the user switches on Settings → Diagnostics. When it is on, the addition to
the threat model is: a third-party SDK with signal and exception handlers
installed, and an HTTPS egress to Sentry’s ingest endpoint carrying stack
traces, device model, and OS/app version. Breadcrumbs, screenshots, view
hierarchies, network tracking, and method swizzling are all disabled in code,
and CrashReporting.scrub strips user, request, server-name, and
device-name fields before transmission. See
docs/PRIVACY.md and
app/SenseBridge/App/CrashReporting.swift.
Since 2026-08-11 the app also supports opt-in Local (self-hosted) or Cloud
(BYOK: Anthropic, OpenAI, NVIDIA NIM) reasoning backends, starting only when
the user explicitly switches Settings → Reasoning backend away from
On-Device and, for Cloud, acknowledges that provider’s own terms. The
addition to the threat model when active: an HTTPS (or, for a Local endpoint
the user points at their own LAN server, HTTP under a scoped
NSAllowsLocalNetworking exception — never NSAllowsArbitraryLoads) egress
carrying recognized object labels only, never camera images, audio, depth
data, or location — PerceptionRecord.detectedObjectLabelsForNetwork() is
the one function that builds that payload. ReasoningOutputValidator
inspects every response before it can reach spoken output, rejecting
anything that isn’t a short, hedge-free noun phrase, so a compromised or
malicious endpoint’s reply is a rejected request, not spoken injected text.
Credentials are Keychain-only
(kSecAttrAccessibleWhenUnlockedThisDeviceOnly, excluded from iCloud
sync), never UserDefaults or a log. See
docs/PRIVACY.md
and
docs/superpowers/specs/2026-08-11-awareness-ai-tiers-design.md.
Trust boundaries
Device sensors (camera, microphone) → Perception → Reasoning → Output
|
On-device storage (UserDefaults)
|
(opt-in, disabled by default) Cloud Reasoning
Adapter — user's own provider credential
- Sensors → Perception → Reasoning → Output. Raw sensor data (camera
frames) is processed on-device and discarded; only structured
PerceptionRecordvalues — never raw pixels, audio, or depth buffers — cross from Perception into Reasoning. Seedocs/GLOSSARY.mdfor these types anddocs/ARCHITECTURE.mdfor the full pipeline. - On-device storage. User preferences (output profile, speech/haptic
settings, camera defaults) persist via
UserDefaults(UserDefaultsSettingsStore), never user content.docs/ARCHITECTURE.mdandSettings.swift’s own doc comments describe an optional, settings-only iCloud/CloudKit sync as a later addition behind the sameSettingsStoreprotocol — as of this writing, no CloudKit code exists in the codebase and no sync is wired up; the current build is UserDefaults only. - The optional cloud reasoning adapter.
CloudReasoningAdapteris a protocol with no shipping implementation yet. Per its own doc comment anddocs/PRIVACY.md, it is opt-in only, disabled by default, and nothing in theSenseBridgeCorepackage invokes it on its own — the App layer would only construct one onceSettings.cloudReasoningEnabledis true and the user has configured a provider credential, stored in the Keychain, never shipped as a project secret.
Threat model
Assets worth protecting
- Camera imagery captured for OCR/scene description — processed and discarded, not persisted without a specific, user-visible reason.
- Recognized text — potentially sensitive (mail, medical documents, prescriptions) even though it is never logged or stored beyond the in-session result.
- Future face-enrollment data (designed, not yet built — see
docs/PRIVACY.md): the design commits this to an encrypted, on-device-only store, excluded from sync by default, before any code exists to populate it.
Plausible adversaries and what mitigates each
| Adversary | What they’d want | Mitigation |
|---|---|---|
| A device thief / someone with physical access | Anything cached on the device | No persisted user content by design (images/text are processed and discarded); any future enrollment store is encrypted with Keychain-protected keys behind device unlock |
| A malicious or compromised dependency | Code execution, data exfiltration via a bundled package or model | Minimal SwiftPM dependency tree, Dependabot + OSV scanning, pinned versions, and the dependency-auditor review path (see “Supply chain” below) |
| A future misconfigured opt-in cloud provider | Camera imagery or recognized text sent off-device | The adapter is disabled by default and requires explicit opt-in; per doctrine 2 in AGENTS.md, nothing about the user’s surroundings leaves the phone without consent and a privacy-doc update |
| A committer accidentally exposing a credential | CI or signing secrets in the repo, a log, or a built artifact | Three independent scanners (gitleaks pre-commit, TruffleHog + GitGuardian in CI) plus tools/check-sensitive-files.mjs; see “Secrets posture” below |
Supply chain
- SwiftPM dependencies are tracked by Dependabot
(
.github/dependabot.yml, weekly, with a supply-chain cooldown before a freshly published version is adopted) and scanned for known vulnerabilities by OSV Scanner (security.yml’sosv-scanjob, plus a PR-scopeddependency-reviewjob). - The license gate. AGPL and Apple’s
apple-amlrresearch-only license are hard blockers for anything shipped in the app or as a bundled model — seedocs/AI-MODELS.mdand the model-license-audit skill. An unvetted model license is treated as a security issue, not just a legal one, because an unvetted model is unvetted code. - Review path. New dependencies and bundled models go through the dependency-auditor persona, which checks SwiftPM hygiene (pinned versions, minimal tree), provenance (trusted source, verified maintainer/release history), and known advisories before a new package or model is added.
Secrets posture
The app itself ships no secrets — it is serverless and holds no API keys or credentials of its own. What secrets exist live entirely in CI and local developer tooling:
- CI secrets (
ANTHROPIC_API_KEY,RAILWAY_TOKEN,GITGUARDIAN_API_KEY, the auto-injectedGITHUB_TOKEN) live in GitHub Actions repository secrets, never in the repository itself. - Enforcement is automated, not just documented:
gitleaksandtools/check-sensitive-files.mjsrun pre-commit; TruffleHog and GitGuardian run in CI on every push and PR (security.yml). - The full table of every secret, what uses it, and what breaks if it’s
unset lives in
docs/SECRETS.md— this page doesn’t duplicate it.
Permissions
Verified against
app/SenseBridge/Resources/InfoPlist.xcstrings
and the capture code that requests each one:
| Permission | Usage description | Justification |
|---|---|---|
NSCameraUsageDescription |
“SenseBridge uses the camera to read text and describe scenes for you. Nothing leaves your device.” | Actively used by two paths, both on-device. CameraSource (app/Packages/SenseBridgeCore/.../Sensing/CameraSource.swift) requests camera authorization and drives the Read/Identify/Describe capture flows. AmbientSensingSource (.../Sensing/AmbientSensingSource.swift) runs an ARWorldTrackingConfiguration with .sceneDepth for Obstacle Awareness — ARSession is the only API returning a camera frame and a registered depth map from one session, and it consumes the same camera grant rather than a separate one. The camera image itself never leaves the device under any setting — the “Nothing leaves your device” claim above is about the frame, not about what’s derived from it: since 2026-08-11 a user who opts into a Local/Cloud reasoning backend sends the object labels Vision recognized in that frame, never the frame. See the network-reasoning exception above. |
NSMicrophoneUsageDescription |
“SenseBridge uses the microphone to recognize nearby sounds and announce them. Nothing leaves your device.” | Actively used: MicrophoneSensingSource (app/Packages/SenseBridgeCore/.../Sensing/MicrophoneSensingSource.swift) requests microphone authorization and feeds SoundAlertsView through CombinedSoundClassifier (CustomSoundClassifier + BuiltInSoundClassifier, run concurrently on one capture). Classification is on-device; audio is captured per tap for a bounded duration, never streamed, written to disk, or transmitted. |
No other NS*UsageDescription keys exist in the current build — no photo
library, location, Bluetooth, or biometric-hardware access is requested.
Reporting a vulnerability
Report security issues privately — see
SECURITY.md
at the repository root for what’s in scope and how to report. Do not open a
public issue for a vulnerability.
Data handling
The guarantees around what happens to user content — what’s processed and
discarded, what’s never logged, what optional sync does and doesn’t carry —
live in docs/PRIVACY.md. This page describes the security
posture that backs those guarantees; it does not restate them.
Need help? See SUPPORT.md.