feat(ai): Mission Grant rollout gating — flag, alerts, runbook, user docs

Phase 4 — everything needed to flip the Mission Key-Grant feature on
safely per deployment. No new behaviour; purely operational plumbing.

- PUBLIC_AI_MISSION_GRANTS feature flag (default off). hooks.server.ts
  injects window.__PUBLIC_AI_MISSION_GRANTS__, api/config.ts exposes
  isMissionGrantsEnabled(). Grant UI (dialog + status box) and the
  Workbench "Datenzugriff" tab both hide when the flag is off.
- PUBLIC_MANA_AI_URL added to the injection set so the webapp can reach
  the new audit endpoint from production.
- Prometheus alerts (new mana_ai_alerts group):
  - ManaAIServiceDown (warning, 2m)
  - ManaAIGrantScopeViolation (critical, 0m) — MUST stay at 0; any
    increment pages immediately
  - ManaAIGrantSkipsHigh (warning, 15m) — flags keypair drift
  - ManaAIPlannerParseFailures (warning, 10m) — prompt/LLM drift
- Runbook in docs/plans/ai-mission-key-grant.md: initial keypair gen,
  leak-response procedure (rotate + invalidate all grants + audit),
  scope-violation triage.
- User-facing doc in apps/docs security.mdx: new "AI Mission Grants"
  section with the three hard constraints (ZK users blocked, scope
  changes invalidate cryptographically, revocation is one click) plus
  an honest threat-model comparison column showing where grants shift
  the tradeoff.

Rollout remaining (not code): generate keypair on Mac Mini, provision
MANA_AI_PRIVATE_KEY_PEM + MANA_AI_PUBLIC_KEY_PEM via Docker secrets,
flip PUBLIC_AI_MISSION_GRANTS=true starting with till-only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 14:02:47 +02:00
parent 74bbfda212
commit bb3da78d5c
7 changed files with 204 additions and 15 deletions

View file

@ -184,6 +184,58 @@ Each row carries the IP address, user-agent, HTTP status code, and a free-form c
| User loses recovery code | n/a | ❌ Data lost |
| User loses password but vault is in ZK mode | Recovery via password reset | ❌ Data lost (vault is keyed to recovery code) |
## AI Mission Grants (opt-in, per mission)
By default, AI missions that depend on encrypted data (notes, tasks,
calendar events, journal entries, your Kontext document) run **only
when your browser tab is open** — the background runner on our server
sees ciphertext and physically cannot read them.
Some missions are more useful when they run continuously, even while
you're offline. For those, you can opt in — per mission, not globally
— to a **Mission Key-Grant**. Here is exactly what that does:
1. Your browser derives a fresh key that is bound to:
- The mission's ID.
- The specific table names referenced.
- The specific record IDs referenced.
2. The derived key is wrapped with the mana-ai service's public key
and attached to the mission record.
3. When the mana-ai runner ticks for that mission, it unwraps the
key in memory, decrypts **only the allowlisted records**, plans
the next iteration, and forgets the key at the end of the tick.
4. Every decrypt is logged. You see the full log under **Workbench
→ Datenzugriff**.
Hard constraints — enforced by the code, not by policy:
- **Zero-knowledge users cannot issue grants.** The mana-auth server
has no usable master key in ZK mode; the endpoint refuses.
- **Scope changes invalidate the key cryptographically.** Add a new
record to a mission → the derived key is different → the existing
grant stops working → you're prompted to re-consent. It is not
possible for the runner to "silently expand" its scope.
- **Grants expire.** Default lifetime is 7 days, renewed on every
successful run. Missions that go idle lose their grant automatically;
you re-consent on the next edit.
- **Revocation is one click.** The lock icon in the Workbench removes
the grant; the mission keeps its history but stops running
server-side until you re-grant.
- **The runner never writes under a grant** — it only reads. All
changes still go through the normal proposal-approve flow you
control.
| Threat | Standard | With a Mission Grant | Zero-Knowledge |
|--------|----------|----------------------|----------------|
| Mana operator reads an unrelated record of the same user | ⚠️ Could decrypt with KEK | ✅ Cannot — key is scoped | ✅ Cannot |
| Mana operator reads the granted records of the grant-enabled mission | ⚠️ Could decrypt with KEK | ⚠️ Could decrypt with the grant key + record ciphertext | ✅ Cannot |
| Court order against Mana for the granted-mission records | ⚠️ Could be compelled | ⚠️ Could be compelled (while grant is active) | ✅ Mana physically cannot comply |
| Runner RAM-dump during the 60s tick | ⚠️ n/a | ⚠️ Could expose the grant key for one tick window | ✅ n/a |
The tradeoff is deliberate: you exchange a small, scoped privacy
reduction for autonomy on one mission. Missions without a grant keep
the full standard / ZK guarantees.
## Implementation references
For the architectural deep dive, code locations, and the complete rollout history (Phases 19 + the backlog sweep), see [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/mana-monorepo/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md).