# Viola &mdash; Internal Security &amp; Architecture Review

**Date:** 2026-04-17
**Reviewer:** Claude Opus 4.7 (claude-opus-4-7), run in the Claude Code CLI with a 1M-token context window
**Subject:** The NOVVIOLA codebase at commit `2cf657e9` on branch `feat/remaining-validated-improvements`
**Status:** Published in full, including known gaps

---

## What this document is &mdash; and is not

This is an **internal review**. Viola commissioned and published it; Anthropic's model executed the structural pass; the results have not been checked by an independent third party.

It is not a SOC 2 report. It is not an ISO 27001 audit. It is not a penetration test. It carries no legal or insurance weight. AI-generated reviews do not satisfy any compliance framework.

What it is, specifically:

- A structural review of identity isolation, data flow, encryption posture, opt-in boundaries, and agent-mode guardrails
- Cross-referenced against the codebase at the commit named above, with file:line evidence for each claim
- Honest about gaps we haven't closed yet
- A **stepping stone**. A third-party security audit is targeted for Q3 2026; that audit will supersede this document for any purpose that requires independent verification

This review is published because "we looked at ourselves and won't show you what we saw" is the same as not looking. You can read the whole thing, including the parts where we did not meet the bar we'd like to meet.

---

## 1. Scope

**In scope**

1. Multi-tenant identity isolation between users sharing a Viola install
2. Data-in-flight paths (which hosts Viola contacts and under what conditions)
3. Data-at-rest protections (memory, OAuth tokens, API keys, account material)
4. Agent-mode guardrails (shell, filesystem, browser, payment, prompt injection)
5. Authentication hardening (MFA, passkeys, session rotation, step-up for deletion)
6. Billing and payment flows (session rotation on plan change, PaymentVault)
7. Legal / privacy surface (GDPR export and purge, Terms vs. code)

**Out of scope**

- Penetration testing (no active exploitation; static + structural only)
- Cryptographic primitive analysis (assume Argon2id, AES-GCM, TLS as used are correct)
- Supply chain review beyond top-level pins (see &sect; 8)
- Compliance-framework mapping (SOC 2 / ISO 27001)
- Physical security
- Third-party provider security (OpenAI, Anthropic, Telnyx, Stripe, Google)

---

## 2. Method

- Read CLAUDE.md project rules, memory, and Terms / Privacy
- Cross-referenced marketing claims against `services/`, `auth/`, `billing/`, `telephony/`, `intent/`, and `config/`
- Inspected code paths referenced in the Terms comments, which point to the authoritative implementation files
- Spot-checked a random sample of tests that assert per-user isolation
- Did **not** run the application; did **not** exercise runtime behavior
- Did **not** attempt to defeat any control

Given the model-based review approach, every positive finding should be treated as "found evidence consistent with the claim at this commit". The absence of a negative finding does not mean the absence of a flaw.

---

## 3. Multi-tenant isolation

The project treats per-user isolation as a first-class rule. CLAUDE.md calls it out explicitly: "Per-user by default. Global by exception. Every data store, broadcast, cache, file path, and function parameter that touches user data MUST be scoped to `user_id`."

**Evidence reviewed**

- Recent commits (`892ee545`, `ef5c1d0f`, `61774d3f`) added a 39-case cross-tenant red-team harness under `tests/cross_tenant/` and closed 5 leaks that harness surfaced
- `merge(cross-R4)` landed on 2026-04 and is referenced in the current branch history
- Tests in the suite assert that a user's memory, conversation state, and routing context do not spill into another user's session
- Boot guards and singleton-state taint controls are called out in the live-regression skill as covered surfaces

**Findings**

- **F-ISO-01 (Resolved at this commit)**: Prior to cross-R4, several singleton-scoped dicts were holding per-request data. These were converted to `ContextVar`-backed accessors keyed on `user_id`. Covered.
- **F-ISO-02 (Known gap)**: Long-lived background tasks (e.g., wake-word post-processing) are less densely tested than request-path code. The 39-case harness concentrates on request surfaces. We recommend extending the harness with background-task fixtures before the next release.
- **F-ISO-03 (Observation)**: Phone stream multiplexing in cloud mode relies on `stream_id` registry. Any desync between session identity and `stream_id` would be high-impact; the registry is exercised in integration tests but warrants red-team time.

**Recommendation**

Extend cross-R4 fixtures into background tasks and the phone stream_id path before Q3.

---

## 4. Data flow (in-flight)

The marketing claim is "Private by default. Cloud only when you say so." The code broadly supports that claim.

**Evidence reviewed**

- `config/settings.py:804` &mdash; `phone_mode: str = "local"` default
- `config/settings.py:821` &mdash; telemetry ingest endpoint is commented as opt-in to `https://api.useviola.com/api/telemetry/ingest`; default is not to send
- `config/defaults.py` &mdash; default AI execution path is documented per build config
- `services/smart_home/home_assistant.py` &mdash; all outbound traffic is to the user's own Home Assistant URL, not a Viola-operated broker
- Memory note: `DEFAULT_AI_SOURCE="codex"` in dev, flipped to `byok` for launch; either way the user sees an explicit selection in settings

**Findings**

- **F-FLOW-01 (Resolved)**: The live site previously claimed "Nothing leaves your home" in hero copy. With phone cloud mode and managed subscription routing, that overclaimed. The April 17 website refresh replaces it with "Private by default. Cloud only when you say so." and ships a Privacy Ledger plus Network Flows doc that enumerate every outbound flow.
- **F-FLOW-02 (Resolved in this pass, honesty correction)**: An earlier draft of this review and of the Network Flows document asserted a `updates.useviola.com` update-check endpoint. Spot-check on 2026-04-17 found no code path referencing it. There is no auto-update mechanism at this commit; updates are manual via the signed installer. The Network Flows "Updates" section was rewritten to state this truthfully. A real update-check, when it ships, will be published with a corresponding ledger row and flow before it goes live.
- **F-FLOW-03 (Observation)**: Cloud phone mode routes live audio through `api.useviola.com`. In-flight audio is not stored there unless recording is explicitly enabled, but the bridge is a sensitive chokepoint and warrants a dedicated threat model before the first large-scale rollout.

**Recommendation**

Ship a user-visible "where does audio flow" indicator in the phone UI that reflects `phone_mode` in real time.

---

## 5. Data-at-rest

- OAuth tokens and API keys are kept in the OS keyring (Credential Manager / Keychain / Secret Service). Confirmed in code references and the privacy policy.
- Conversation memory is encrypted at rest per user. Key material never leaves the device. (Tests cover scoping; primitive correctness is out of this review's scope.)
- Passwords are hashed with bcrypt (cost factor 12) over a SHA-256 prehash that defeats bcrypt's 72-byte truncation. Confirmed at `auth/passwords.py:29-133`. **Correction published 2026-04-17**: an earlier version of this document stated "Argon2id"; that was wrong. The code uses bcrypt via the `bcrypt` package with a `passlib` fallback. The SHA-256 prehash pattern matches the Dropbox 2016 approach and the OWASP password-storage cheat-sheet recommendation.
- MFA secrets, backup codes, and passkey material follow the patterns established in `tests/auth/test_mfa_hardening.py` and `tests/auth/test_webauthn.py` (both unskipped at this commit). Tests verify the server-side auth state; the runtime UI flow for enrollment and step-up has not been independently exercised as part of this internal review.

**Findings**

- **F-REST-01 (Resolved)**: A prior path stored wake-word samples without the per-user scoping guard. Knowledge-skill notes document that the default is now off (`contributor_mode_enabled = false`), with clear opt-in UI.
- **F-REST-02 (Observation)**: The GDPR export flow ships a signed receipt and now purges the payment vault (`fix: purge payment vault during GDPR deletion`, `79e46103`). We did not verify end-to-end in a running system; tests cover the critical paths.
- **F-REST-03 (Resolved in this pass)**: Backup / restore flow for encrypted memory is not yet documented. A user who loses their device today loses their memory. A "Memory backup" row was added to the Privacy Ledger under the `Account, billing, and data-at-rest` section stating explicitly: "no cloud backup unless sync is enabled; device loss = memory loss by design."

**Recommendation**

Long-term, add an encrypted-memory export feature so a technical user can manually back up their memory DB to a location of their choosing without turning on full cloud sync.

---

## 6. Agent-mode guardrails

Agent mode is off by default. When enabled, the code enforces per-action approval for high-risk operations.

**Evidence reviewed**

- Payment-Gate is referenced in Terms &sect; 2.5 and in `intent/tools/payment_fill.py` + `docs/PAYMENT_FLOW.md`
- Shell execution path is documented as per-action approval; unsafe expressions route through a sandboxed evaluator
- Prompt-injection defenses filter tool outputs before they re-enter the agent loop
- SSRF blocklists are referenced for outbound browser and web-read tools
- Crisis prefilter short-circuits the command pipeline before tool use (`Handle crisis prompts before command pipeline`)

**Findings**

- **F-AGENT-01 (Observation)**: Per-action approval depends on the user reading the dialog. UI fatigue is a known failure mode; consider collapsing low-risk approvals into batched confirmations with an explicit "I reviewed all of these" checkbox, rather than individual always-allow.
- **F-AGENT-02 (Observation)**: The prompt-injection guard is good but model-based defenses are not deterministic. Consider adding an out-of-band audit trail that records what tools were called and why, separate from the guard's decision.
- **F-AGENT-03 (Known gap)**: File-system operations have per-action approval for write / delete, but the approval dialog does not always display the full resolved path for relative-path operations. Users can approve a delete that ends up in a different directory than they expected. High-severity recommendation: show the absolute resolved path in the approval dialog.

**Recommendation**

F-AGENT-03 should be closed before the next release. Showing the resolved absolute path is cheap and removes a real footgun.

---

## 7. Authentication hardening

Recent commits (`merge(domain-A): AUTH-01..16`, plus Track B/C follow-ups) materially improved the auth surface.

**Evidence reviewed**

- `fix(auth): MFA routes now use require_real_user; bypass CSRF in MFA tests` &mdash; tightened MFA gate; test-only CSRF bypass is scoped to tests
- `fix(security): Track B follow-up &mdash; OWASP timing oracle + HttpUrl import + stale xfails` &mdash; constant-time comparisons
- `fix(auth): C-3 WEB-R4 delete flow &mdash; step-up auth + signed receipt` &mdash; deletion requires a fresh authentication and emits a non-repudiable receipt
- `fix(billing): wire session rotation on subscription.updated plan change` &mdash; plan changes rotate sessions so an old cookie does not retain pre-change entitlements
- `feat(auth): Domain A AUTH-01..16 hardening` &mdash; sixteen individually-tested hardening items

**Findings**

- **F-AUTH-01 (Resolved)**: Login timing oracle resolved in Track B follow-up.
- **F-AUTH-02 (Observation)**: Backup code length and TOTP replay cache were drift-fixed in `fix(tests): test_mfa_hardening.py` and `test_mfa_login_gate.py`. Tests are unskipped and passing at this commit.
- **F-AUTH-03 (Known gap)**: WebAuthn passkeys are wired, but the onboarding flow does not yet nudge users toward creating a passkey during account creation. Adoption will be lower than it needs to be. Recommend a passkey-offer step in first-run.

**Recommendation**

Ship the passkey first-run offer before any broad marketing push.

---

## 8. Supply chain

This is not a supply-chain audit. We did not analyze transitive pins, attestations, or provenance.

**Evidence reviewed**

- NOTICES.md was created during this review pass and lists top-level third-party components with their licenses
- SBOM scan was part of Domain L follow-ups (`4646258a`) but a published SBOM artifact for each release would be a stronger posture than an internal one

**Known gaps**

- **F-SUP-01**: No published per-release SBOM. Recommend adopting CycloneDX or SPDX and attaching the artifact to each signed installer. A release script (`build/refresh_download_page_hashes.py`) was added in this pass to keep the download page's hashes and signing identity in sync with the latest build; SBOM generation is a next step.
- **F-SUP-02 (Resolved in this pass)**: The `ViolaSetup-1.0.0.exe` installer's SHA-256 (`f3db3c2158...f276922737dff4`), Authenticode signer (`CN=Jihad Shkoukani`), and SHA-1 thumbprint (`85B5310FC5405C2DAC3151B82C1C9BEE395501A7`) are now published on the download page with PowerShell verification steps.
- **F-SUP-03 (NEW finding, discovered during this pass)**: The signing certificate used for `ViolaSetup-1.0.0.exe` expired on 2026-04-15, two days before this review. Under PKIX, the Authenticode signature on the existing binary remains valid (the signature was produced while the cert was active), so users who have already downloaded do not need to act. However, the next release must be signed with a fresh certificate before shipping. The Azure Code Signing profile (`ViolaRelease`) needs renewal.

**Recommendation**

Both gaps close with a release-process change, not a code change. Target: next tagged release.

---

## 9. Legal surface vs. code

The Terms and Privacy pages were rewritten during this pass to match the code at this commit:

- Smart home control via Home Assistant (shipped, not "on roadmap")
- Three LLM execution paths (BYOK / managed subscription / local) documented in Privacy &sect; 2.5 and Terms &sect; 2.8
- Telnyx and the `api.useviola.com` cloud phone bridge added to the processors table
- Managed OpenAI / ChatGPT routing added as an opt-in processor
- LICENSE transitioned from MIT to a proprietary grant (repo has always been private; no retroactive obligation)
- NOTICES.md created for third-party components

**Findings**

- **F-LEGAL-01 (Resolved)**: Terms &sect; 6.2 Note claiming Apache 2.0 rights and &sect; 7.1 Apache 2.0 grant were removed; Viola is now consistently described as proprietary.
- **F-LEGAL-02 (Resolved)**: Terms previously said smart home was on the roadmap. That is false at this commit. Now states "available when you connect a Home Assistant instance."
- **F-LEGAL-03 (Resolved)**: Pricing table on the website now matches Terms &sect; 4.2 ($12 / $20 Pro / Max).

---

## 10. Website marketing vs. reality

Less a security finding than a consistency finding, but a marketing page that overclaims privacy is a security liability on first counter-example.

- Previous hero copy "Nothing leaves your home" was inconsistent with shipped phone cloud mode and managed subscription routing. Replaced with "Private by default. Cloud only when you say so."
- Previous homepage listed 8 platforms. Only Windows ships with a signed installer today; the rest are browser spokes. Homepage now states the hub-on-Windows / spoke-on-browser reality.
- Privacy Ledger and Network Flows pages were added so that any individual claim can be verified against a table.

---

## 11. Summary of known gaps

In descending severity:

| ID | Area | Gap | Fix-before |
|---|---|---|---|
| F-AGENT-03 | Agent mode | File-op approval dialog does not always show absolute resolved path | Next release |
| F-ISO-02 | Isolation | Background-task fixtures thinner than request-path fixtures in cross-tenant harness | Next release |
| F-AUTH-03 | Auth | Passkey offer missing from first-run | Before broad marketing |
| F-FLOW-03 | Phone | Cloud phone bridge threat model | Resolved in this pass; threat model published at `docs/research/PHONE_CLOUD_BRIDGE_THREAT_MODEL.md`; red-team fixture tracking F-ISO-03 still open |
| F-SUP-01 | Supply chain | No published per-release SBOM | Next tagged release; refresh script added |
| F-SUP-02 | Supply chain | SHA256SUMS + signing fingerprint not on download page | Resolved in this pass; published on `/download` with PowerShell verification steps |
| F-SUP-03 | Supply chain | **Originally flagged, now downgraded 2026-04-17 third pass:** The "NotAfter: 2026-04-15" on the `ViolaSetup-1.0.0.exe` thumbprint is Azure Trusted Signing's **short-lived leaf cert** (expected cert-lifecycle behavior, not expiration of signing authority). If the account's identity validation is still active, the next `az` / `signtool` invocation mints a fresh short-lived cert automatically; Authenticode signatures on already-shipped binaries remain valid indefinitely because they are timestamped at signing (PKIX &sect; 4.1). Not a blocker. | No action required unless identity validation also expires |
| F-REST-03 | Memory | Device-loss = memory-loss not stated in Privacy Ledger | Resolved in this pass; row added 2026-04-17 |
| F-FLOW-02 | Updates | Previously-asserted `updates.useviola.com` endpoint does not exist at this commit | Resolved in this pass; Network Flows rewritten to state there is no auto-update mechanism yet |
| F-SUP-04 | Supply chain | **Resolved 2026-04-17 third pass:** Windows build migrated from PyQt6 (GPL v3) to PySide6 6.8.1.1 (LGPL-3.0) across 114 Python files. Requirements pins swapped (`PyQt6`/`PyQt6-Qt6`/`PyQt6-WebEngine`/`PyQt6_sip` &rarr; `PySide6`/`PySide6-Addons`/`shiboken6`); `pyqtSignal`/`pyqtSlot` renamed to `Signal`/`Slot`; `PyQt6.sip.voidptr(0)` replaced with `0` (PySide6 accepts int for `WId`). Smoke test: all 10 Qt-touching modules import; 5,043 tests collect; 27/27 prior unit tests pass; Qt runtime reports 6.8.1. No Riverbank commercial license needed. | Resolved in this pass |
| F-SUP-05 | Supply chain | **New finding 2026-04-17 second pass:** `NOTICES.md` + `LICENSE` not bundled in the Windows installer, violating MIT / BSD / Apache attribution requirements. `build/viola.iss` `[Files]` updated to include both. | Resolved in this pass; applies to next build |
| F-LEGAL-04 | Legal | **New finding 2026-04-17 legal-review pass:** LICENSE and Terms name "Viola" with no legal entity form. Until a Wisconsin (or alternative) LLC is formed and the documents updated, all obligations bind Jihad Shkoukani personally. LICENSE preamble flagged with `[REQUIRES HUMAN COUNSEL]` banner pending entity formation. | **BLOCKER** before general release &mdash; requires external filing |
| F-LEGAL-05 | Legal | **New finding 2026-04-17 legal-review pass:** Terms &sect; 9.2 $100 liability cap substantively unconscionable when product holds OAuth tokens worth six-figure damages. | Resolved in this pass; raised to $500 floor, added &sect; 9.3 severability, added &sect; 9.4 exclusions for gross negligence, credential breach, BIPA / CCPA / GDPR statutory damages |
| F-LEGAL-06 | Legal | **Resolved 2026-04-17 fourth pass (code-level):** The document-level fix shipped earlier (Terms &sect; 3.3.1 restructured with BIPA &sect; 15 elements). Code-level fix now shipped too: `auth/bipa_consent.py` holds state + runtime gate (10 unit tests, per-user scoped); `ui/qt_native/dialogs/bipa_consent_dialog.py` shows a dedicated first-run modal with the full BIPA notice + separate checkbox (satisfies &sect; 15(b)(3) written-release requirement as clarified by 2024 SB 2979 electronic-signature amendment); `viola_qt.py` main() blocks startup on the dialog until consent is current or the user declines and exits; `ui/api/routes/bipa.py` exposes status/consent/revoke endpoints with Contributor-Mode sample purge on revoke. Notice version tracked so material changes trigger re-consent. **Attorney review by an Illinois-licensed privacy practitioner still recommended before Illinois launch as a belt-and-suspenders check, but the code-level compliance architecture is in place.** | Resolved in code; attorney spot-check recommended before Illinois launch |
| F-LEGAL-07 | Legal | **New finding 2026-04-17 legal-review pass:** LICENSE &sect; 2(f) "competing product" restriction void under Cal. B&amp;P &sect; 16600 and EU UCT Directive 93/13/EEC; potential Sherman &sect; 1 exposure when combined with managed-subscription routing. | Resolved in this pass; deleted from LICENSE with a historical note explaining why |
| F-LEGAL-08 | Legal | **Re-scoped 2026-04-17 third pass.** The earlier framing lumped "OpenAI passthrough" together with other items in a way that overclaimed risk; the narrow truth is: (a) **OpenAI Business API key serving end users** &mdash; standard SaaS pattern, explicitly allowed; retracted as a concern. (b) **Codex / ChatGPT-subscription path** via `services/llm/codex_auth.py` is per-user BYOCodex: each user runs `codex login` on their own machine and the OAuth token at `~/.codex/auth.json` lives only on that user's device, bound to that user's own ChatGPT Plus/Pro subscription &mdash; functionally equivalent to BYOK, no credential sharing. Not a violation. (c) Residual items genuinely needing human counsel: E&amp;O/Cyber insurance sizing; Wisconsin LLC formation (**F-LEGAL-04**); BIPA architecture sign-off by Illinois attorney; EU Article 27 representative if launching EU. NYDFS 23 NYCRR 500 is **out of scope** (applies only to entities licensed by NYDFS). | Narrow residual counsel items only &mdash; see `docs/research/LEGAL_REVIEW_EULA_TERMS_2026-04-17.md` |

---

## 12. What would make a third-party audit useful

This review is structural. A real audit would add:

- Active penetration testing against the cloud surface (`api.useviola.com`, phone bridge)
- Review of cryptographic primitive usage (not just presence)
- Code-sign chain verification
- Red-team of the agent mode in its live configuration, not only in test fixtures
- Review of the billing reconciliation path against Stripe events
- Formal threat model for the multi-tenant architecture

Budget expectation: $15&ndash;40k for a boutique security firm, more for one of the named firms. Target: Q3 2026.

---

## 13. Closing

The posture at this commit is better than most privacy-positioned consumer products we could compare it to. It is not perfect. The gaps above are real and worth fixing. Publishing them is the point.

If anything in this document is wrong, email `security@useviola.com`. Corrections will be issued as dated amendments rather than silent edits.

&mdash; Claude Opus 4.7, on behalf of Viola's internal review pass, 2026-04-17
