Ctrl K
ComunidadGuías…

AGENTS.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
---
id: agents
type: index
status: accepted
owns: [.claude/, .github/copilot-instructions.md, CONTRIBUTING.md]
read_when: [starting any session, before any commit, before any PR]
---

# AGENTS.md — Guardyn Agent Constitution

**This file is law.** It is tool-agnostic: Claude Code, Copilot, Cursor, Devin and any
other agent working in this repository obey it. Where a tool-specific file exists
(`CLAUDE.md`, `.github/copilot-instructions.md`), it is a pointer to this file, never a
competing source of truth.

**Read this before your first tool call in any session.**

---

## 0. Fast orientation

| Question | Answer |
|---|---|
| What is Guardyn? | Self-hostable, zero-knowledge, end-to-end-encrypted messaging and VoIP. |
| Backend | Rust, 10 crates under `backend/crates/`, gRPC via tonic. |
| Clients | Flutter (`client-mobile/`), Tauri 2 + SolidJS (`client-desktop/`). |
| Data stores | TiKV (metadata/auth/MLS state), ScyllaDB (message history), MinIO (blobs). |
| Buses | NATS (ephemeral signalling), Redpanda (durable log). **Both are intentional.** |
| Edge | Envoy (gRPC-Web gateway). |
| Current work | See [`implementation_plan.md`](implementation_plan.md) — a 44-step, 4-gate refactoring. |
| Task board | <https://github.com/orgs/guardyn/projects/3> |

Once `docs/INDEX.md` exists, read it instead of exploring the tree. It routes to every
document by `read_when`, so a session loads 2–3 files rather than the whole corpus.

---

## 1. Invariants — NON-NEGOTIABLE

Any change that weakens these is forbidden. If a task appears to require it, **stop and
ask the user.** Do not proceed under an assumption.

| # | Invariant | Meaning |
|---|---|---|
| **I-1** | **Zero-Knowledge** | The server must never be able to read message, file or media plaintext. No payload, key, or decrypted metadata may ever reach a log, a metric, a trace, or `stdout`. |
| **I-2** | **Always-On E2EE** | Encryption cannot be disabled — not by flag, not by config, not "for development". |
| **I-3** | **Post-Quantum** | Key agreement must resist Harvest-Now-Decrypt-Later. Hybrid X25519 + ML-KEM-768; classical strength is the floor, never the ceiling. |
| **I-4** | **Data Sovereignty** | The entire stack must be 100% self-hostable with no third-party dependency and no vendor lock-in. |

### Known violations under repair

Two invariants are **currently unmet**. Do not "fix" them ad hoc — they have owned steps:

- **I-2 is violated.** `messaging-service/src/handlers/` carried duplicate handler pairs and a
  `GUARDYN_E2EE_ENABLED` flag selecting between them.
  → Repaired by **PR-32a/b/c** and **PR-30′**. Breaking change, already approved.

  **This entry previously said the opposite of the truth, and the correction is load-bearing.**
  It claimed four such pairs existed and that the fix was to collapse onto the `_e2ee`
  variants. There were **two** (`send_message`, `receive_messages`), and the `_e2ee` variants
  were the violation, not the fix: they encrypted server-side and held the ratchet state
  (`send_message_e2ee.rs:129`, `receive_messages_e2ee.rs:177`). The unsuffixed handler was
  already the zero-knowledge relay. Collapsing the way this file used to direct would have
  traded an I-2 violation for an I-1 one. See
  [ADR-0010](docs/adr/ADR-0010-pure-relay-server.md).
- **I-3 is unmet.** `crypto/src/pqxdh.rs` is a real hybrid implementation, but the `pq`
  feature is off by default and no backend service enables it — and decisively,
  `backend/proto/` contains **zero** ML-KEM fields, so the server cannot publish a PQ
  public key at all. → Repaired by **PR-36…PR-40**.

---

## 2. Git workflow — STRICT

### 2.1 Never commit to `main`

Direct commits to `main` or `master` are **forbidden**. `main` is protected by a ruleset
requiring a pull request. Before any commit:

```sh
git branch --show-current   # must NOT be main or master
```

### 2.2 One micro-step = one branch = one PR = one issue

- Branch name: `<type>/<issue-number>-<slug>` — e.g. `security/43-enforce-always-on-e2ee`.
- `<type>` ∈ `feat | fix | docs | refactor | chore | security`.
- Every PR closes exactly one issue and is tracked on the project board.

### 2.3 PR budget

**≤400 hand-written changed lines, or ≤8 files.** If a step would exceed this, **split it
before opening the PR**. No exceptions.

Bulk deletions and generated/vendored files are exempt from the count, but such a PR must
contain *nothing else*.

Rationale: agents have finite context. A 400-line ceiling keeps a whole step — diff, tests
and the docs it touches — inside one session, so no step needs context reconstruction.

### 2.4 Commit messages

Conventional Commits, English, imperative mood. Reference the issue with `Refs #N`.
Explain **why**, not just what. State anything deliberately left out of scope.

### 2.5 Approval gates

Phases end at gates **G1–G4**. At a gate: **STOP**, post the phase report to the board,
and **wait for explicit user approval**. Never carry on into the next phase unprompted.

### 2.6 Roadmap changes

`docs/roadmap/roadmap.yaml` is the machine source of truth; GitHub Issues, milestones and the
project board are a **projection** of it. Change the YAML and reconcile — never the other way
round. Two states that disagree cannot be told apart, and the next sync reverts the hand-edit.

Use the [`issue-sync`](.claude/skills/issue-sync/SKILL.md) skill. It carries the loop, the
field reference, and the hazard to check before changing any `status:`.

**Never close an issue, set a milestone, or move a board card by hand.**

---

## 3. Language policy — English only

**English is the only permitted language for all project content**: documentation, code
comments, commit messages, PR text, identifiers, and log strings. No transliteration.

**Forbidden:** Russian, Ukrainian, any Cyrillic-script language, any non-English language,
and mixed-language content.

**The only exceptions** are explicitly-marked localization files:
`client-mobile/lib/l10n/`, `landing/i18n/`, and files suffixed `.{locale}.md`
(`README.ru.md` is valid; Russian inside `README.md` is not).

**Allowlisted file** that legitimately contains Cyrillic, because the Cyrillic *is* a
counter-example rather than content — never flag it:
`.github/.copilot-commit-message-instructions.md`.

**Why:** third-party security auditors require English. Ambiguity in a security-critical
implementation is a vulnerability.

If asked to produce non-English content, state this policy and offer a separate,
suffixed translation file.

---

## 4. Zero-knowledge logging — I-1 enforcement

The backend emits **structured JSON logs**. Everything below is a hard prohibition.

**Never log, trace, or emit as a metric label:**
- Message, file or media plaintext — or ciphertext.
- Any key material: identity keys, pre-keys, ratchet state, MLS secrets, ML-KEM private keys.
- Tokens, passwords, password hashes, session secrets.
- PII: email, phone number, IP address, precise location.

**Practices:**
- Never `#[derive(Debug)]` on a type holding key or payload material — implement `Debug` to
  emit `[REDACTED]`. Use the `Redacted<T>` wrapper from `guardyn-common` once PR-24 lands.
- Never interpolate a whole request or response struct into a log line.
- Identifiers (`user_id`, `device_id`) are correlatable metadata: log only when genuinely
  needed for an operation, never at `info` on a hot path.
- Initialize tracing **only** via `guardyn_common::observability::init_tracing`. Constructing
  a `tracing_subscriber::FmtSubscriber` directly bypasses the redaction layer and is forbidden.

When unsure whether a field is sensitive: **do not log it.**

---

## 5. Code standards

### 5.1 Rust

- **No `unwrap()` or `expect()` in non-test code.** Propagate with `Result` and `?`.
- Errors via `thiserror`, built on `guardyn_common::error`. No stringly-typed errors.
- Every public item carries a doc comment.
- One gRPC handler per file under `handlers/`.
- `cargo fmt` and `cargo clippy -- -D warnings` must both pass.
- No `unsafe` outside FFI boundaries; where unavoidable, justify it in a comment.

### 5.2 Protocol Buffers are canonical

`backend/proto/*.proto` is the **single source of truth** for the wire contract.

| Element | Convention |
|---|---|
| Messages, services, RPCs | `PascalCase` |
| Fields | `snake_case` |
| Enum type | `PascalCase` |
| Enum values | `SCREAMING_SNAKE_CASE` |

Prost converts these to Rust automatically. **Never invent an enum variant** — read the
proto and use the converted name:

| ❌ Wrong | ✅ Correct | Proto |
|---|---|---|
| `ErrorCode::Internal` | `ErrorCode::InternalError` | `INTERNAL_ERROR` |
| `ErrorCode::Unauthenticated` | `ErrorCode::Unauthorized` | `UNAUTHORIZED` |
| `ErrorCode::InvalidInput` | `ErrorCode::InvalidRequest` | `INVALID_REQUEST` |
| `ErrorCode::AlreadyExists` | `ErrorCode::Conflict` | `CONFLICT` |

Change the proto first, then regenerate. **Never hand-edit generated code.**

> Codegen is currently inconsistent — `messaging-service` uses `tonic::include_proto!`
> (OUT_DIR) while five services `include!` ~13,800 lines of committed output under
> `src/generated/`. **PR-23** unifies this on OUT_DIR. Until then, treat the `.proto`
> file — never the committed copy — as truth.

### 5.3 No new datastore without an ADR

The store topology is deliberate and user-approved. Adding, replacing or removing a
datastore requires an accepted ADR **and** explicit user approval. In particular:

- **TiKV**, not PostgreSQL, is the metadata/auth store. This is deliberate; do not "fix" it.
- **Dragonfly/Redis is deliberately absent**, deferred post-launch. Do not introduce it.
- **NATS and Redpanda coexist by design** with separated roles. Do not consolidate them.

---

## 6. Domain-agnostic architecture

Guardyn must deploy under **any** domain — this is how I-4 is honoured in practice.

**Never hardcode a domain** in code, manifests, or documentation.

```yaml
host: auth.guardyn.io   # ❌ never
host: auth.${DOMAIN}    # ✅ always
```

The `DOMAIN` environment variable is the **single source of truth**. All service hostnames
derive from it: `auth.${DOMAIN}`, `api.${DOMAIN}`, `ws.${DOMAIN}`, `media.${DOMAIN}`,
`app.${DOMAIN}`.

In documentation use `yourdomain.com` or `example.com`. Never assume a subdomain layout or
a TLD. Changes must work with `.local`, `.test` and real domains alike.

---

## 7. File organization

| Content | Location |
|---|---|
| Documentation | `docs/` (never the repo root, except the files listed below) |
| Infrastructure scripts | `infra/scripts/` |
| E2E / test scripts | `backend/crates/e2e-tests/scripts/` |
| Scratch, WIP, personal notes | `_local/` — **gitignored, never referenced from a tracked file** |

**Permitted root-level Markdown:** `README.md`, `AGENTS.md`, `CLAUDE.md`,
`CONTRIBUTING.md`, `CHANGELOG.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`,
`implementation_plan.md`. Adding another requires justification.

**Naming:** `SCREAMING_SNAKE_CASE.md` for docs, `kebab-case.sh` for scripts,
`snake_case.rs` for Rust, `kebab-case/` for directories.

`_local/` holds live grant material and personal notes. **Never delete it, never move it,
and never link to it from a tracked file** — cloners cannot see it.

---

## 8. Testing

Standard unit tests are **insufficient** for E2EE code.

- Cryptographic modules require **property-based tests** (`proptest`) and **fuzz targets**
  (`cargo-fuzz`) on every parser reachable from attacker-controlled bytes.
- A bug fix lands with a regression test that fails before the fix.
- Never weaken or delete a test to make CI pass. If a test is wrong, say so explicitly and
  explain why in the PR.

---

## 9. Session checklist

Before starting:
1. Read `docs/INDEX.md` (once it exists); load only what `read_when` matches.
2. Confirm the step weakens no invariant in §1.
3. Confirm `git branch --show-current` is not `main`.
4. Confirm the issue exists and is `In Progress` on the board.

Before opening a PR:
5. Diff is within the §2.3 budget — otherwise split it.
6. `cargo fmt --check`, `cargo clippy -- -D warnings`, and the test suite pass.
7. Documentation mapped to the changed paths is updated (see `docs/.manifest.yaml`
   once PR-15 lands), or the PR is labelled `docs-impact:none` with a reason.
8. The PR body states what was deliberately left out of scope.

At a gate: **STOP. Report. Wait.**

---

## 10. When to stop and ask

Stop and ask the user — do not guess — when a task would:

- Weaken any invariant in §1.
- Add, remove or replace a datastore, message bus, or major dependency.
- Change a public wire contract in a non-additive way.
- Delete anything under `_local/`.
- Rewrite git history.
- Require a credential or permission you do not have.

Asking costs one message. Guessing wrong on a security-critical system costs far more.