implementation_plan.md
Rama main · réplica de lectura del repositorio original.
---
id: implementation-plan
type: roadmap
status: accepted
version: 1.0.0
created: 2026-09-06
owns: [docs/, .claude/, .github/, AGENTS.md, CONTRIBUTING.md, README.md]
read_when: [starting any refactoring micro-step, resuming work, checking gate status]
supersedes: [docs/IMPLEMENTATION_PLAN.md]
---
# Guardyn — Full Revision & Refactoring Execution Plan
**Audience:** AI coding agents. **Language:** English only.
**Status:** Approved. Preflight blockers P-1 and P-2 are OPEN and require human action.
> **Reading contract.** This is the execution SSOT until `docs/roadmap/roadmap.yaml`
> exists (PR-14), after which `roadmap.yaml` becomes the machine SSOT and this file
> becomes the narrative reference. Do not begin any micro-step without reading
> §"Micro-stepping contract" and §"Preflight".
---
## 1. Context
Guardyn is a **large existing codebase**, not a greenfield project.
| Area | Scale |
|---|---|
| Tracked files | 1,073 |
| Backend (Rust) | 10 crates, 57,456 LOC (~43,600 hand-written, ~13,860 committed generated) |
| Mobile (Flutter) | 85,469 LOC, 240 files |
| Desktop (Tauri 2 + SolidJS) | 30,745 LOC TS/TSX + 19,278 LOC Rust |
| Landing (static HTML) | 6,465 LOC |
| Infra (k8s/Kustomize/Envoy) | 5,380 LOC |
| Tracked documentation | ~19,000 lines of Markdown |
It ships as `v1.0.1` and claims production readiness.
The originating brief was written for a **new** project. It is therefore executed here as
**reverse-documentation**: the AI-first documentation core is derived **from committed code**,
never invented ahead of it. Any document describing intent rather than shipped behaviour is a
hallucination vector and is out of scope.
### 1.1 Motivating defects (all verified)
1. **No AI-agent contract.** No `.claude/`, no `AGENTS.md`, no `CLAUDE.md`. `.agent/workflows/`
is an empty directory. The de-facto agent constitution is `.github/copilot-instructions.md`
(684 lines) — unversioned, with zero enforcement behind it.
2. **Documentation has no spine.** 38 files / 15,828 lines under `docs/` with **no index**,
**no ADRs**, **no glossary**. Roadmap content is smeared across `README.md:245`,
`CHANGELOG.md:484`, and a 2,001-line `docs/IMPLEMENTATION_PLAN.md`. Five files are dated
status snapshots that are already rotting. Tracked `docs/archive/README.md` links into
`_local/done/`, which is gitignored and invisible to every cloner.
3. **Nothing is enforced.** `main` has **no branch protection and no rulesets**; all recent
commits landed directly on it. `build.yml` marks clippy, tests **and** cargo-audit
`continue-on-error: true`, so only `cargo fmt` can fail the build. No CODEOWNERS, no
dependabot.
### 1.2 Domain invariants (NON-NEGOTIABLE)
| # | Invariant | Current status |
|---|---|---|
| I-1 | **Zero-Knowledge** — server never accesses decrypted payloads | At risk: no log redaction (§Phase 3.1) |
| I-2 | **Always-On E2EE** — cannot be disabled | **VIOLATED**: non-E2EE handler path is the registered one; `GUARDYN_E2EE_ENABLED=false` in dev compose (§Phase 3.3) |
| I-3 | **Post-Quantum** — forward secrecy vs Harvest-Now-Decrypt-Later | **UNMET**: PQXDH unreachable end-to-end (§1.4) |
| I-4 | **Data Sovereignty** — 100 % self-hostable, no vendor lock-in | Holds. All components self-hosted. |
Any micro-step that would weaken I-1…I-4 is forbidden and must be escalated to the user.
### 1.3 Architecture decisions (user-approved 2026-09-06)
The brief's target architecture diverged from the code in two of four stores. Resolved:
| Concern | Decision | Consequence |
|---|---|---|
| Metadata / auth / RBAC | **Keep TiKV**, amend the spec | `tikv-client 0.3` + PD/TiKV v7.5.0 are load-bearing (auth, MLS group state, presence, media metadata). **No PostgreSQL migration.** TiKV already satisfies the multi-region goal the brief assigned to CockroachDB/TiDB. → ADR-0001 |
| Dragonfly / Redis L1 cache | **Defer post-launch** | Documented as ADR-approved scale target only. Ships on the current NATS/TiKV path. → ADR-0006 |
| Event bus | **Keep both, roles separated** | NATS = ephemeral low-latency signalling; Redpanda = durable ingestion log. No code churn. → ADR-0003 |
| Post-quantum | **Dedicated phase, hybrid KEM** | See §1.4. → ADR-0005 |
### 1.4 Post-quantum: verified state
`backend/crates/crypto/src/pqxdh.rs` (539 L) is a **real** hybrid X25519 + ML-KEM-768
implementation following the Signal PQXDH spec. It is nonetheless **entirely unreachable**:
- `crypto/Cargo.toml` — `default = ["std"]`; `pq = ["ml-kem"]` is opt-in.
- `auth-service`, `messaging-service`, `call-service` all declare
`guardyn-crypto = { path = "../crypto" }` **with no features** → PQ is compiled out of every
backend service.
- `crypto-ffi` does enable it (`default = ["full"] = ["pq"]`), so only the Flutter client can
reach `crypto_generate_hybrid_key_bundle`.
- **Decisive blocker:** `grep -i 'ml_kem|kyber|pqxdh|post_quantum' backend/proto/` returns
**NONE**. `common.KeyBundle` carries X25519/Ed25519 only. The server has no field in which to
publish an ML-KEM public key, so the handshake cannot complete regardless of feature flags.
**Phase 4 is therefore wire-and-enable + prove, not implement.**
---
## 2. Preflight — hard blockers
These are **prerequisites, not tasks**. P-1 and P-2 require human action no agent can perform.
### P-1 · GitHub Project automation has no usable credential ⚠️ HUMAN ACTION REQUIRED
Zero-touch Project-board sync is a hard requirement. Verified state:
- `guardyn` is an **Organization**; repo is public; `has_projects: true`.
- The active `GITHUB_TOKEN` is a fine-grained PAT and is **rejected outright** by the org:
*"forbids access via fine-grained personal access tokens if the token's lifetime is greater
than 366 days."*
- The fallback keyring OAuth token authenticates but carries scopes
`gist, read:org, repo, workflow` — **`read:project` / `project` are absent**, so
`gh project list` and every `projectsV2` GraphQL field fail with `INSUFFICIENT_SCOPES`.
**Required from the human architect:**
1. A classic PAT (or GitHub App installation token) with `repo` + `project` scopes.
2. Stored as repository secret `GUARDYN_PROJECT_TOKEN`.
3. A Project v2 board created under the `guardyn` org, its number recorded in
`docs/roadmap/roadmap.yaml` as `project_number`.
**Until this exists**, all Project-sync automation is authored and unit-tested but **cannot
run**. PR-18 ships it behind a guard that logs a warning and exits 0 rather than failing CI.
### P-2 · `main` is unprotected ⚠️ HUMAN ACTION REQUIRED
`GET /repos/guardyn/guardyn/branches/main/protection` → `404 Branch not protected`.
`GET /repos/guardyn/guardyn/rulesets` → `[]`.
The "no direct commits to main" rule is currently unenforceable at the platform level.
**Required ruleset on `main`:**
- Require a pull request before merging; require 1 approval.
- Require status checks: `lint-and-test`, `security-audit`, `docs-verify`.
- Block force-push and branch deletion. No bypass actors.
**Agent-side belt-and-braces** (PR-06, agent-authorable, does not replace the ruleset):
a `pre-commit` hook plus a `.claude/settings.json` `PreToolUse` Bash hook rejecting any
`git commit` / `git push` while `HEAD` is on `main` or `master`.
### P-3 · `_local/` is OUT OF SCOPE — do not delete
`_local/` is gitignored, ~5.2 MB, 44 Markdown files / 18,669 lines. It includes
`_local/grant/opentech.fund/` — **4.7 MB of live OTF grant application material**: proposal
drafts, budgets, funding determinations and correspondence.
**The "destroy old docs" directive applies to *tracked* documentation only.**
`_local/` is never touched, moved, or referenced from a tracked file.
---
## 3. Micro-stepping contract
Codified in `AGENTS.md` (PR-04) and `.claude/rules/10-git-workflow.md` (PR-05).
- **1 micro-step = 1 branch = 1 PR = 1 GitHub Issue.**
- **Branch naming:** `<type>/<issue-id>-<slug>` — e.g. `docs/42-adr-tikv-metadata-store`.
`<type>` ∈ `feat | fix | docs | refactor | chore | security`.
- **PR budget: ≤ 400 changed lines** of hand-written diff, **or** ≤ 8 files.
Bulk deletions and generated/vendored files are exempt but must be the PR's *only* content.
- A PR that would exceed budget **must be split before opening**. No exceptions.
- Each PR is independently revertable and leaves `main` green.
- **Never commit directly to `main` or `master`.** Ever.
- **Approval gate** at the end of every phase: the agent **STOPS**, posts a phase report to
the Project board, and **waits for explicit user approval**. Gates are G1–G4.
**Rationale (token economy):** agents have finite context. A 400-line ceiling keeps an entire
micro-step — diff, tests, and the docs it touches — inside a single session window, so no
step ever requires reconstructing lost context.
---
## 4. Phase 1 — Purge & Initialization
**Goal:** delete legacy tracked docs; stand up the AI-first documentation core and the
enforcement layer. **No source code changes in this phase.**
### 4.1 Docs purge — PR-01 … PR-03
| PR | Action |
|---|---|
| **PR-01** | Delete the 24 top-level `docs/*.md`, `docs/testing/*`, `docs/archive/*`. Salvaged content is *quoted into* the new specs (§4.3), never moved wholesale. |
| **PR-02** | Delete `cicd/github/workflows/{build,release,test}.yml` — verified **byte-identical** duplicates of `.github/workflows/`; dead, since GitHub executes only `.github/`. Remove the stale `cicd/github/workflows/test.yml` path filter from `.github/workflows/test.yml`. Delete the orphaned `cicd/github/actions/reproducible-build/action.yml` and the unreferenced `cicd/docker/Dockerfile.ci`. |
| **PR-03** | Delete committed build artifacts: `client-desktop/e2e-report/`, `client-desktop/test-results/`, `client-mobile/flutter_0{1,2}.png`, `client-mobile/test-reports/`. Extend `.gitignore` accordingly. |
**Retained deliberately** (security-relevant, externally cited, or licence-bound):
`docs/security/**` (6 files — rewritten in place in §4.3, not deleted), `SECURITY.md`,
`LICENSE`, `NOTICE`, `CODE_OF_CONDUCT.md`, `CHANGELOG.md`, `landing/**`.
### 4.2 AI-agent contract — PR-04 … PR-07
```
AGENTS.md # canonical, tool-agnostic agent constitution
CLAUDE.md # 3-line pointer -> AGENTS.md (no duplication)
.claude/
settings.json # permissions + PreToolUse guard hooks
rules/
00-invariants.md # I-1..I-4 verbatim; first-read, always
10-git-workflow.md # branch/PR law (§3)
20-code-style.md # forbidden patterns, Rust/TS/Dart idiom
30-zk-logging.md # zero-knowledge logging law
40-doc-sync.md # the auto-maintenance algorithm (§4.4)
skills/
doc-sync/SKILL.md # executes the §4.4 algorithm
adr-new/SKILL.md # scaffolds an ADR, assigns the next number
issue-sync/SKILL.md # roadmap.yaml -> Issues/Project (§5.1)
commands/
micro-step.md # open branch+issue, enforce PR budget
.github/
CODEOWNERS
dependabot.yml
```
- **PR-04** — `AGENTS.md` + `CLAUDE.md`. `AGENTS.md` supersedes
`.github/copilot-instructions.md`, which is reduced to a pointer stub (**kept**, so Copilot
keeps working — not deleted). Its three load-bearing sections — Language Policy, Naming
Conventions, File Organization — migrate into `.claude/rules/20-code-style.md`.
- **PR-05** — `.claude/rules/*`. Every rule is written as a **testable predicate**, not prose.
- **PR-06** — `.claude/settings.json` guard hooks + `pre-commit` hook (P-2 enforcement).
- **PR-07** — `CODEOWNERS`; `dependabot.yml` (cargo / npm / pub / github-actions); rewritten
`CONTRIBUTING.md` (agent-first; drops human-onboarding prose); rewritten `README.md`
(drops the embedded Roadmap section — it moves to §4.3).
### 4.3 Documentation core — PR-08 … PR-14
```
docs/
INDEX.md # machine-readable router: path | purpose | read_when | owns
GLOSSARY.md # SSOT ubiquitous language; every domain term + its code symbol
spec/
PRD.md # features as they exist, user-story form
SRS.md # THE algorithmic SSOT: rules, edge cases, invariants
SAD.md # blueprint: crates, topology, data flow, store-per-concern
adr/
index.md
ADR-0000-template.md
ADR-0001 … ADR-0009
api/
INDEX.md # proto -> service -> 89 RPCs; generated, not hand-written
ops/
DEPLOYMENT.md # k3d/k8s deploy, Kustomize overlays
RUNBOOK.md # incident playbooks, on-call procedures
OBSERVABILITY.md # tracing/metrics/logs, ZK-logging rules
roadmap/
ROADMAP.md # narrative phases
roadmap.yaml # MACHINE SSOT -> drives Issues + Project board
STATE.md # generated; current phase/gate/progress telemetry
```
**Every file carries YAML frontmatter.** This is what makes the base token-efficient and
indexable:
```yaml
---
id: adr-0005
type: adr # adr | spec | ops | roadmap | index | glossary
status: accepted # draft | accepted | superseded | deprecated
owns: [backend/crates/crypto/src/pqxdh.rs, backend/proto/common.proto]
read_when: [touching crypto, changing key bundles]
tokens: 820 # measured; INDEX.md sorts by cost so agents can budget reads
supersedes: []
---
```
`docs/INDEX.md` is the **only file an agent must read cold**. It routes to everything else via
`read_when`, so a typical session loads 2–3 documents instead of 38. This is the primary
token-economy mechanism of the whole system.
**ADRs to be written.** Each records a decision **already embodied in the code**, plus the four
taken on 2026-09-06:
| ADR | Subject |
|---|---|
| 0001 | TiKV as metadata / auth / MLS-state store (supersedes the brief's PostgreSQL) |
| 0002 | ScyllaDB for message / call / notification history |
| 0003 | Dual bus: NATS ephemeral signalling + Redpanda durable log |
| 0004 | OpenMLS 0.6 for group E2EE; custom X3DH + Double Ratchet over libsignal |
| 0005 | Hybrid PQXDH (X25519 + ML-KEM-768) — target state, gap documented |
| 0006 | Dragonfly / Redis L1 cache deferred post-launch |
| 0007 | Zero-knowledge logging and mandatory redaction |
| 0008 | Generated-protobuf strategy (`include_proto!` vs committed `src/generated/`) |
| 0009 | Micro-step PR budget and branch isolation |
| PR | Deliverable |
|---|---|
| **PR-08** | `INDEX.md` + `GLOSSARY.md` — glossary mined mechanically from proto messages and crate types, never invented |
| **PR-09** | `spec/SAD.md` |
| **PR-10** | `spec/SRS.md` |
| **PR-11** | `spec/PRD.md` |
| **PR-12** | All nine ADRs (budget-exempt: single-purpose, template-driven) |
| **PR-13** | `ops/DEPLOYMENT.md`, `ops/RUNBOOK.md`, `ops/OBSERVABILITY.md` |
| **PR-14** | `roadmap/ROADMAP.md`, `roadmap/roadmap.yaml`, `roadmap/STATE.md` |
**Language gate.** `docs/TESTING_GUIDE.md:8` is the repo's **only** genuine Cyrillic violation
(a stray Russian phrase); it dies with PR-01. The Cyrillic in
`.github/copilot-instructions.md` (lines 43–44) and `.github/.copilot-commit-message-instructions.md`
(line 50) is **intentional** — it is the policy statement listing forbidden languages, and a
bad-commit-message example. **The CI language check must allowlist those two files**, or it
will flag its own policy.
### 4.4 Auto-maintenance protocol — PR-15 … PR-16
The mechanism that makes documentation self-updating. Deterministic and CI-enforced — a
mechanism, not a convention.
**`docs/.manifest.yaml`** maps source globs to the documents that own them:
```yaml
mappings:
- source: backend/proto/**
requires: [docs/api/INDEX.md, docs/spec/SRS.md]
- source: backend/crates/crypto/**
requires: [docs/adr/ADR-0004*, docs/adr/ADR-0005*, docs/spec/SRS.md]
- source: backend/crates/*/src/db.rs
requires: [docs/spec/SAD.md, docs/adr/ADR-0001*, docs/adr/ADR-0002*]
- source: backend/crates/common/src/observability.rs
requires: [docs/ops/OBSERVABILITY.md, docs/adr/ADR-0007*]
- source: infra/k8s/**
requires: [docs/ops/DEPLOYMENT.md]
```
**`just docs-verify`** — a new recipe (the Justfile currently has **zero** docs recipes) —
runs five deterministic checks, wired as required CI job `docs-verify`:
1. **Frontmatter** — every `docs/**/*.md` parses; `id` unique; `status` within enum.
2. **Impact** — for each changed source path, at least one mapped doc is also changed in the
same PR, **or** the PR carries label `docs-impact:none` with a stated reason. **Hard fail.**
This is what converts documentation drift from a review nit into a build failure.
3. **Glossary** — no term defined in `GLOSSARY.md` appears in docs under a synonym listed in
its `forbidden_aliases`. Kills `User`/`Player`-class naming drift.
4. **Links** — every relative link resolves, and **no tracked doc may link into `_local/`**
(the existing `docs/archive/README.md` defect, caught structurally so it cannot recur).
5. **Language** — no Cyrillic outside the two allowlisted policy files (§4.3).
`INDEX.md`, `roadmap/STATE.md`, and per-document `tokens:` counts are **regenerated**, never
hand-edited; `docs-verify` fails if a regenerated file is stale.
- **PR-15** — `docs/.manifest.yaml`, `just docs-verify`, the check scripts.
- **PR-16** — `.github/workflows/docs.yml` running it on every PR.
### 4.5 CI truthfulness — PR-17
`build.yml` currently sets `continue-on-error: true` on clippy, tests **and** cargo-audit —
the job is decorative. Remove it from clippy and tests; keep it on `cargo-audit` alone
(advisory-DB churn), governed by `deny.toml`'s 6 existing RUSTSEC ignores.
**Without this, no later gate means anything.** This PR is a prerequisite for G1.
> ### 🚦 GATE G1 — STOP. Post phase report to the Project board. Await explicit user approval.
---
## 5. Phase 2 — Project Restructuring & Tracking
**Goal:** SSOT codebase rules plus the zero-touch GitHub tracking loop.
### 5.1 Roadmap → Issues → Project sync — PR-18 … PR-21
`docs/roadmap/roadmap.yaml` is the single source of truth:
```yaml
project_number: null # set by human after P-1
phases:
- id: P3
title: Architectural Scaffolding
gate: G3
steps:
- id: P3-01
title: Unify protobuf codegen strategy
status: todo # todo | in-progress | review | done | blocked
pr: null
issue: null
adr: [ADR-0008]
est_lines: 300
```
- **PR-18** — `.github/workflows/roadmap-sync.yml`. On push to `main` touching `roadmap.yaml`,
reconcile Issues (close with `state_reason`, reopen) and their **milestones**, then the
Project v2 board. **Idempotent** — reconciles toward desired state, never blindly appends.
**Guarded:** `project_sync_enabled` gates the board half alone; issue state and milestones
are plain REST and reconcile with the ambient token.
- **PR-19** — `.github/workflows/pr-link.yml`. On PR open, parse the issue id from the branch
name, add `Closes #N` when absent, and set the issue's milestone on the PR. The board move
(`In Review` → `Done`) is a guarded stub while P-1 is open.
- **PR-20** — `.claude/skills/issue-sync/SKILL.md`: the agent's local path. Edit
`roadmap.yaml`, run `just roadmap-sync`. **Never touch the board by hand.**
- **PR-21** — Label taxonomy, **then its correction**. The taxonomy shipped early during board
bootstrap: `type:{feat,fix,docs,refactor,chore,security}`, `gate:G1`…`gate:G4`,
`docs-impact:none`, `blocked`. The `phase:1`…`phase:4` labels it also created are
**retired**: phase is now tracked by native GitHub **Milestones**, and a label encoding the
same fact is a second source of truth that nothing keeps in sync.
**Phase tracking is a milestone, not a label or a board field.** Four milestones titled
`Phase N — …`, one per phase, each naming its gate. A step's `phase: N` in `roadmap.yaml`
selects one. This replaces both the `phase:*` labels and the custom `Phase` field the brief
put on the Project v2 board — three encodings of one fact, of which only the milestone gives
a progress bar and a native board grouping for free.
**Result:** the human architect sees exact project state on the board at all times, with zero
manual maintenance. Every phase maps 1:1 to tracked tickets.
### 5.2 Codebase SSOT rules — PR-22 … PR-23
- **PR-22** — Codify in `.claude/rules/20-code-style.md`, each as a CI-checkable predicate:
no `unwrap()` / `expect()` in non-test code; all errors via `thiserror` +
`guardyn-common::error`; every public item carries a doc comment; one handler per file under
`handlers/`; **no new datastore without an ADR**.
- **PR-23** — Resolve **ADR-0008**. `messaging-service` uses `tonic::include_proto!` (OUT_DIR)
while five services `include!` **~13,800 lines of committed generated code** under
`src/generated/` — a live drift risk, since all seven crates still run `tonic_build` in
`build.rs`. Standardise on OUT_DIR for all six; delete the committed copies.
Mechanical, large, budget-exempt.
> ### 🚦 GATE G2 — STOP. Post phase report. Await explicit user approval.
---
## 6. Phase 3 — Architectural Scaffolding & ZK Hardening
**Goal:** make the data layer and observability match the now-documented architecture, and
close the zero-knowledge gaps. **Highest-risk phase.**
### 6.1 Zero-knowledge logging — PR-24 … PR-26 — HIGHEST PRIORITY
`grep -E 'redact|mask|sanitiz|scrub'` across the entire backend returns **zero hits**, while
`common/src/observability.rs` emits **JSON structured logs** carrying `user_id`, device ids and
free-form handler fields. This directly threatens invariant **I-1**.
- **PR-24** — `common/src/redact.rs`: a `Redacted<T>` newtype whose `Debug` / `Display` /
`Serialize` impls emit `[REDACTED]`, plus a `tracing` layer dropping a denylist of field
names (`ciphertext`, `payload`, `key`, `token`, `password`, `pre_key`, `ml_kem_private`, …).
- **PR-25** — Apply `Redacted<T>` across the crypto, auth and messaging types carrying key or
payload material. Split per-crate if over budget.
- **PR-26** — `call-service/src/main.rs:19` and `notification-service/src/main.rs:18` bypass
`observability::init_tracing` with a raw `FmtSubscriber`, so they get neither JSON logs nor
OTel traces **nor the new redaction layer**. Migrate both. Add a grep-based CI check
forbidding direct `FmtSubscriber` construction.
### 6.2 Store & bus hygiene — PR-27 … PR-29
Per the approved ADRs — **no store is added or removed in this phase**.
- **PR-27** — Document and health-check the four live stores (TiKV, ScyllaDB, MinIO, and the
NATS/Redpanda pair). `media-service` is the **only** service with no `Health` RPC — add it.
- **PR-28** — Redpanda is started in compose and `auth-service` `depends_on` it, yet only
`guardyn.user.events` is actually produced and consumed; the other four topic constants in
`common/src/events.rs` are declared and unwired. Either wire or delete them — no dead
constants. The Kafka producer currently degrades **silently** to `None` on an unreachable
broker (`auth-service/src/main.rs:340-350`); make that explicit and alertable.
- **PR-29** — `presence-service/src/db.rs:106` carries
`/// In production, this would use Redis or similar with TTL support`. Per ADR-0006 Dragonfly
is deferred, so replace the comment with a TTL-correct TiKV implementation plus a link to the
ADR. **No silent gaps.**
### 6.3 E2EE correctness — PR-30 … PR-32
**Re-scoped.** All three steps below were written on two premises that turned out to be false,
and the corrected architecture is recorded in
[ADR-0010](docs/adr/ADR-0010-pure-relay-server.md). The server becomes a **pure relay**: it
routes opaque bytes and holds no key material. MLS and the Double Ratchet run on the clients.
The false premises, both verified against the code before re-scoping:
1. The `_e2ee` handlers were assumed to be the encrypted path. They encrypted **server-side**
and held the ratchet state; the unsuffixed handler was already the zero-knowledge relay.
2. OpenMLS 0.6 was assumed unable to deserialize a group. `MlsGroup::load` exists
(`openmls-0.6.0/src/group/mls_group/mod.rs:417`) — and using it would have been the breach,
since it restores `group_epoch_secrets`.
- **PR-30′** (#41) — **deletion, not implementation.** The dependency edge inverts: once PR-32a
removes the `_e2ee` handlers, `messaging-service/src/crypto.rs` has no caller.
`CryptoManager::with_storage` had zero call sites, `SessionManager::new` hard-coded
`key_storage: None`, and `get_or_create_session` errored on every new peer pair — there was no
working path to preserve. Persistent key storage belongs to the clients via `crypto/src/ffi.rs`
(Phase 4).
- **PR-31a…d** (#42, #151, #152, #153) — remove server-side MLS: the dead `*_mls.rs` handlers,
the server-held group secrets and `create_test_credential`, the crate-wide `dead_code` allow
that hid them, and the ADR.
- **PR-32a…c** (#43, #154, #155) — **invariant I-2 repair.** Delete the server-side E2EE
handlers, then the `E2eeConfig`/`MlsConfig` flags, then the deployment variables.
**PR-32c is a behaviour change → requires explicit user sign-off at G3**, and is additionally
blocked on #163: `client-desktop` sends plaintext in `encrypted_content`, which the server
currently encrypts on its behalf.
### 6.4 Security testing — PR-33 … PR-35
Standard unit tests are insufficient for E2EE. Current state: **no fuzz targets anywhere in the
repo**, and `proptest = "1.4"` is declared in `crypto/Cargo.toml` dev-deps but **referenced by
zero `.rs` files** — a dead dependency.
- **PR-33** — `cargo-fuzz` targets for every parser touching attacker-controlled bytes:
`X3DHPrekeyMessage` deserialization, Double Ratchet header parsing, `sealed_sender` envelope
decode, PADMÉ `unpad`.
- **PR-34** — Real proptest suites: Double Ratchet round-trip under arbitrary skip/reorder
≤ `MAX_SKIP` (1000); PADMÉ `pad` ∘ `unpad` identity across 32 B – 16 MB; X3DH agreement
symmetry.
- **PR-35** — `notification-service` has **0 unit tests** and no e2e coverage; `call-service`
has 9 unit tests and no e2e. Add baseline coverage for both. Extend benches beyond the
current padding + X3DH-keygen only.
**Also cleaned in this phase:** `crypto/src/mls.rs.backup` (20 KB dead file); the orphaned
`crypto/src/frb_generated.rs` (not declared in `lib.rs`); and the `sframe` feature flag that
gates nothing (there is no `sframe.rs` — SFrame key *distribution* lives in
`call-service/src/session.rs`).
> ### 🚦 GATE G3 — STOP. **Behaviour-change sign-off required** (PR-32). Await approval.
---
## 7. Phase 4 — Post-Quantum & Launch Optimization
**Goal:** satisfy invariant I-3 end-to-end, then trim launch surface. PQ lands **before**
optimization — the invariant outranks speed-to-market.
### 7.1 Hybrid PQXDH end-to-end — PR-36 … PR-40
- **PR-36** — Extend `backend/proto/common.proto`'s `KeyBundle` with `ml_kem_public` and its
Ed25519 signature; extend `auth.proto` `UploadPreKeysRequest` to accept ML-KEM material.
**Additive field numbers only** — wire-compatible with deployed v1.0.1 clients.
- **PR-37** — `auth-service`: persist and serve ML-KEM public keys through `db.rs` and
`handlers/`. `store_key_bundle` writes both halves under `/devices/{user}/{device}/`,
deleting them when absent so stale material cannot outlive a re-registration;
`validate_for_store` refuses a half-pair. The check is **structural only** — no byte
lengths, no signature verification, because ADR-0010 makes the server a relay.
**This step no longer carries the `device_id` fix.** It used to read "Also fixes the
hardcoded `device_id = "default"` at `handlers/mls_key_package.rs:57`" — a real defect
(every one of a user's devices collides on one storage path) but an MLS key-package one,
sharing nothing with ML-KEM but the crate. Bundling it would have made one PR close one
issue while fixing two unrelated things, which is what §2.2 exists to prevent. Split out
as **PR-102** ([#272](https://github.com/guardyn/guardyn/issues/272)) during PR-37's
execution and recorded here rather than dropped silently.
- **PR-38** — Enable `features = ["pq"]` on `guardyn-crypto` in `auth-service`,
`messaging-service` and `call-service`; make `pq` part of `crypto`'s `default`.
- **PR-39** — Wire `PqxdhProtocol` into `messaging-service/src/crypto.rs` session
establishment: negotiate hybrid when the peer bundle carries ML-KEM, fall back to classical
X3DH otherwise. **Classical security remains the floor — hybrid is never weaker.**
- **PR-40** — Fuzz + proptest for the hybrid path (extends PR-33/34). Extend `bench_pqxdh`,
which today exercises only `generate_classical_bundle` unless built `--features pq`.
Client integration is already ~90 % complete: `crypto_generate_hybrid_key_bundle` and
`crypto_is_pq_available` already exist in `crypto-ffi/src/api.rs`.
**Explicitly out of scope:** ML-DSA / Dilithium. Signatures remain Ed25519. Recorded as a known
limitation in ADR-0005 with a follow-up issue — **not silently ignored**.
### 7.2 Launch optimization — PR-41 … PR-44
- **PR-41** — `common/src/rate_limit.rs` (623 L) is a **process-local `HashMap`** behind a
`parking_lot::RwLock`. It does not hold across replicas, so the k8s `prod` overlay's HPA
renders it ineffective. Either move to a shared backend or document the single-replica
constraint in `ops/RUNBOOK.md`. **Decision required at G4.**
- **PR-42** — Secrets hygiene. `infra/secrets/.gitignore` is **inverted relative to intent**:
it ignores `*.enc.yaml` (the SOPS-**encrypted** files, which `infra/secrets/README.md`
states are safe to commit) while the **plaintext `infra/secrets/app-secrets.yaml` is
tracked**. Plaintext k8s Secrets are also committed at
`infra/k8s/base/apps/backend-secrets.yaml` (`jwt-secret: "guardyn-jwt-secret-changeme-in-production…"`).
All values are dev placeholders — **not a live leak** — but the mechanism is backwards and
must be corrected before any real secret is added.
- **PR-43** — Observability. `docker-compose.dev.yml` lines 370–394 are entirely commented-out
dead YAML, and the referenced `./infra/observability/prometheus.yml` **does not exist** (the
real files live under `infra/k8s/base/observability/`). Every service sets
`GUARDYN_OBSERVABILITY__OTLP_ENDPOINT: ""`. Uncomment and wire the stack. Note also
`Sampler::AlwaysOn` — full-volume tracing, which needs a head-sampling decision before
production.
- **PR-44** — Deployment parity. `call-service` runs in compose but has **no k8s Deployment**
under `infra/k8s/base/apps/`. Prod overlay images are `ghcr.io/guardyn/<service>:latest` —
digest-pin them. Add the missing Flutter/mobile CI workflow; none exists today, and mobile is
tested only via manual `client-mobile/scripts/*.sh`.
> ### 🚦 GATE G4 — STOP. Final report. Await explicit user approval.
---
## 8. Verification
Every PR is gated on `just verify` (new aggregate recipe) plus the CI matrix:
| Layer | Command | Enforced by |
|---|---|---|
| Docs | `just docs-verify` | `docs.yml` (new, required) |
| Rust fmt / lint | `cargo fmt --check && cargo clippy -- -D warnings` | `build.yml` (`continue-on-error` removed, PR-17) |
| Rust tests | `cargo test --workspace --all-features` | `build.yml` |
| Fuzz smoke | `cargo fuzz run <target> -- -runs=10000` | nightly workflow (PR-33) |
| Property | `cargo test --features proptest-suite` | `build.yml` (PR-34) |
| Desktop | `npm test` + `tsc --noEmit` + Playwright | `desktop-ci.yml` (exists) |
| Mobile | `flutter test` + `flutter analyze` | **new workflow (PR-44)** |
| Security | cargo-audit, cargo-deny, Trivy, CodeQL, TruffleHog, SBOM | `security.yml` (exists, daily cron) |
| Integration | `just dc-up && cargo test -p guardyn-e2e-tests` | `test.yml` |
| Board sync | `roadmap.yaml` → Issues → Project reconciles | `roadmap-sync.yml` (new, PR-18) |
**Phase-gate verification** — run manually at each of G1–G4:
1. `just dc-up` → all 9 infra containers healthy; all 6 services report `Health`.
2. Two-client E2EE round trip: `just test-two-client-messaging` (Android ↔ Linux).
3. `just docs-verify` green; `docs/roadmap/STATE.md` matches the live board state.
4. **ZK-logging spot check:** run the stack, then
`grep -iE 'ciphertext|BEGIN|eyJ' logs` → **zero hits**.
---
## 9. Risks
| Risk | Mitigation |
|---|---|
| P-1 credential unresolved → board automation cannot run | Automation ships guarded and unit-tested; activates the moment the secret exists. Surfaced at G1, not discovered at the end. |
| PR-32 removes the non-E2EE path → breaks deployed v1.0.1 clients | Explicit behaviour-change sign-off at G3 before merge. |
| PR-23 deletes 13.8k lines of committed protobuf | Single-purpose, trivially revertable PR; CI proves regeneration before the deletion lands. |
| PR-31 blocked by OpenMLS 0.6 deserialization limits | Fallback: gate the MLS path off behind a flag with a tracked upstream issue rather than fake a fix. |
| Documentation drifts from code after the rebuild | `docs-verify` check #2 makes drift a **CI failure**, not a review nit. |
| Agent exceeds context on a large step | 400-line PR budget (§3) is enforced by `.claude/commands/micro-step.md` before a branch is opened. |
---
## 10. Sequencing
```
Preflight (P-1, P-2 — human action)
│
├─ Phase 1 PR-01 … PR-17 docs purge, agent contract, doc core, CI truth → G1
├─ Phase 2 PR-18 … PR-23 board sync, SSOT rules, protobuf unification → G2
├─ Phase 3 PR-24 … PR-35 ZK logging, store hygiene, E2EE repair, fuzzing → G3 ⚠ sign-off
└─ Phase 4 PR-36 … PR-44 hybrid PQXDH end-to-end, launch optimization → G4
```
**44 micro-steps, 4 gates.**
Phases 1–2 touch no source code. Phase 3 carries the security-critical work.
Phase 4 closes invariant I-3.
---
## 11. Agent start-up checklist
Before **every** micro-step:
1. Read `docs/INDEX.md` → load only the documents whose `read_when` matches the task.
2. Read `.claude/rules/00-invariants.md`. Confirm the step weakens none of I-1…I-4.
3. Confirm `git branch --show-current` is **not** `main` / `master`. If it is, create the
step's branch first.
4. Confirm the step's Issue exists and is `in-progress` in `roadmap.yaml`.
5. Verify the planned diff fits the 400-line / 8-file budget. If not, **split it**.
6. On completion: update the mapped docs (`docs/.manifest.yaml`), run `just docs-verify`,
open the PR, and let `roadmap-sync.yml` move the board.
7. At a gate: **STOP**. Post the report. **Wait for the user.** Do not proceed.