docs(spec): v0.8.1 org item-type parity (Card/Key/Document/Totp) design

Card/Key/Totp = CLI-only parity via shared item-build module; Document
adds org attachment storage + a relicario-server hook change that
grant-scopes attachment paths (closing the Unrestricted gap). Secrets
via interactive prompts + --*-stdin escape hatches. Four suggested dev
streams (A foundation, B Card/Key/Totp, C Document+attachments, D hook).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L5JvzEse4xUxLZKhofyeCD
This commit is contained in:
adlee-was-taken
2026-06-20 16:38:05 -04:00
parent 50b5c01291
commit b2f3739673

View File

@@ -0,0 +1,107 @@
# Relicario v0.8.1 — Org Vault Item-Type Parity (Design Spec)
**Date:** 2026-06-20
**Status:** Approved (design) — implementation plan to follow
**Predecessor:** `docs/superpowers/specs/2026-06-06-relicario-enterprise-org-vault-design.md` (org vault shipped v0.8.0, `50b5c01`)
**Tracked-from:** the org-vault plan's deferral note — `docs/superpowers/plans/2026-06-06-enterprise-org-vault.md:3839` and the v0.8.1 follow-up list at `:6132`.
## Goal
Bring `relicario org add` and `relicario org edit` to **full item-type parity** with the personal vault. Today the org surface supports only **Login, SecureNote, Identity**; this milestone adds **Card, Key, Document, Totp**.
Secrets are entered via **interactive prompts by default**, with **`--*-stdin` escape hatches** for non-interactive scripting and the acceptance tests — matching the personal vault's secret-input philosophy while staying fully testable.
Document support additionally requires org-side **attachment storage** and a **`relicario-server` pre-receive hook change** that grant-scopes attachment write paths (closing a latent authorization gap).
## Background — current state (verified)
| Surface | Coverage today | Mechanism |
|---|---|---|
| Personal `add` (`commands/add.rs`) | All 7 types | per-type `build_*_item`; flags + `prompt_secret`/stdin |
| Personal `edit` (`commands/edit.rs`) | All 7 types | interactive per-type, "blank to keep", **field history** (synthetic `core:<field>` FieldIds) |
| Org `add` (`commands/org.rs::build_org_item`) | Login / SecureNote / Identity | plain value flags only (incl. `--password`) |
| Org `edit` (`commands/org.rs::run_edit`) | Login / SecureNote / Identity | flat `Option<String>` flag args |
- **Org storage** is `items/<slug>/<id>.enc` only (`org_session.rs::item_path`). There is **no attachment support and no settings/caps** on the org side.
- **Hook** (`crates/relicario-server/src/lib.rs::classify_path`) classifies paths as `Protected` (members/collections/org.json), `Item { collection }` (`items/<slug>/<id>.enc` — grant-authorized), `Rejected`, or **`Unrestricted`** (everything else — gated only by the per-commit member-signature check). An `attachments/...` path therefore currently falls through to **`Unrestricted`**: any member could push attachment blobs regardless of collection grants. Document parity must close this.
Why the four types were deferred: their personal builders read secrets interactively (`prompt_secret` / multiline stdin), so they had no non-interactive path the org acceptance tests could drive. Document additionally has no org storage target.
## Design
### Approach
**Shared builder/edit module + parity on both surfaces.** Extract the per-type item construction, secret-resolution, and interactive-edit logic into one CLI module that *both* the personal commands and the org commands call. This eliminates the existing personal↔org builder duplication and prevents the two surfaces from drifting again. `--*-stdin` is added to **both** surfaces (true parity), not org-only.
Rejected alternatives: (B) duplicate org-specific builders in `org.rs` — smaller blast radius but locks in two diverging builder sets, which is exactly the drift this milestone is paying down; (C) push builders into `relicario-core` — overkill, since prompt/stdin/storage logic does not belong in the bytes-in/bytes-out core.
### 1. Shared item-build module — `crates/relicario-cli/src/commands/item_build.rs`
- **`SecretSource` resolution**: a helper that resolves a secret field in priority order — explicit flag value → `--*-stdin` (read a single line, or multiline-to-EOF for key material / note body) → interactive `prompt_secret`/`prompt`. If a required secret has no flag, no stdin flag, and the process is non-interactive (no TTY), it errors clearly rather than hanging.
- **Builders** `build_login`, `build_secure_note`, `build_identity`, `build_card`, `build_key`, `build_totp` → return a fully-populated `Item` (no storage side effects).
- **`build_document`** → returns `(Item, EncryptedAttachment)` so each caller writes the encrypted blob with *its own* master key and *its own* path layout (personal: `vault.root()/attachments/<item-id>/…`; org: `attachments/<slug>/<item-id>/…`).
- **Shared per-type interactive edit helpers** — mutate a `&mut ItemCore` slice in place and record field history via the existing synthetic-`FieldId` scheme (`commands/edit.rs::push_history`), reused by both personal and org edit.
- Personal `add.rs` / `edit.rs` are refactored to call these helpers with **no behavior change** (existing personal tests stay green), then gain `--*-stdin` flags.
### 2. CLI surface — `org add`
Extend `OrgAddKind` (in `main.rs`) with `card` / `key` / `document` / `totp` subcommands mirroring the personal `AddKind` flags, plus the org-required `--collection` and the secret-stdin flags:
- `org add card --collection <s> --title <t> [--holder <h>] [--expiry YYYY-MM] [--kind credit|debit|gift|loyalty|other] [--number-stdin] [--cvv-stdin] [--pin-stdin]` — secrets prompted when a TTY is present and no `--*-stdin` flag is set.
- `org add key --collection <s> --title <t> [--label <l>] [--algorithm <a>] [--public-key <p>] [--material-stdin]`
- `org add totp --collection <s> --title <t> [--issuer <i>] [--label <l>] [--period 30] [--digits 6] [--algorithm sha1] [--secret <b32> | --secret-stdin]`
- `org add document --collection <s> --title <t> --file <path>` — no secret; file bytes encrypted with the org key and written to the collection-scoped attachment path.
Retrofit `org add login` to accept `--password` / `--password-stdin` (+ prompt fallback) so the existing type matches the new convention. All paths flow through the shared builders and are committed via the existing signed `org_git_run` path with the same `Relicario-*` trailers as today's `run_add`.
### 3. CLI surface — `org edit`
Restructure `run_edit` to dispatch per item type (mirroring personal `edit`): interactive "blank to keep" by default, with flag / `--*-stdin` overrides for scripts and tests. Field history is recorded with the same synthetic-key scheme as personal edit. Document edit accepts an optional `--file` that re-encrypts and replaces the primary attachment (re-points `DocumentCore.primary_attachment` + `AttachmentRef`, stages old + new paths). Grant + collection-existence checks are unchanged.
### 4. Org attachment storage + cap
- **Layout:** `attachments/<slug>/<item-id>/<att-id>.enc` — collection-scoped, mirroring `items/<slug>/<id>.enc`.
- **`org_session` methods:** `attachment_path`, `save_attachment`, `load_attachment`, `remove_item_attachments` (purge removes an item's attachment directory).
- **Cap:** a **default constant** in the CLI org path (mirroring the personal-vault `attachment_caps` default; the spec/code cites the source line per the code-constant-pinning rule). Per-org configurable caps are out of scope for v0.8.1.
### 5. Hook change — `relicario-server`
- Extend `classify_path` (`lib.rs`) to recognize `attachments/<slug>/<item-id>/<att-id>.enc` and classify it as `PathClass::Item { collection: slug }` — reusing the existing grant + slug-existence authorization for items. Apply the same defenses as the `items/` branch: exact segment count and a `.`-free slug guard (path-traversal defense).
- This converts attachment writes from `Unrestricted` to grant-scoped, closing the gap.
- **Version bump** for `relicario-server`; the release notes must call out a **coordinated server redeploy** (the deployed pre-receive hook must be rebuilt) — Document writes to a not-yet-upgraded server still succeed but remain `Unrestricted` until the hook is updated.
### 6. Tests (acceptance)
- `crates/relicario-cli/tests/org_items.rs`: non-interactive add → get → edit → rm round-trips for **Card, Key, Totp, Document** driven through the `--*-stdin` flags; secret masking verified in `org get` without `--show`; a grant-denied attachment-write case.
- `crates/relicario-server` lib tests: `classify_path("attachments/eng/<id>/<att>.enc") == Item { collection: "eng" }`; rejection cases for malformed attachment paths.
- Existing personal `add`/`edit` tests stay green after the shared-module refactor (behavior-preserving).
- Green across all crates (`cargo test`).
### 7. Living-docs updates (per CLAUDE.md discipline)
- `docs/FORMATS.md` — org attachment path layout + the default cap constant (cite source line).
- `crates/relicario-cli/ARCHITECTURE.md` — the shared `item_build` module + per-type org `add`/`edit`.
- `docs/SECURITY.md` — attachment writes are now grant-scoped (closing the `Unrestricted` gap).
- `STATUS.md` / `ROADMAP.md` / `CHANGELOG.md` — on release; mark org item-type parity landed, move Document/attachment + hook change to shipped.
- Extension docs untouched — extension org **writes** remain deferred (Plan B-2).
## Out of scope (v0.8.1)
- Extension org **writes** (`Plan B-2`).
- Per-collection subkeys, read audit, SSO/SAML/LDAP, HTTP management plane (phase 2).
- Per-org **configurable** attachment cap (a default constant ships now).
## Suggested execution decomposition (for the plan)
Four parallel dev streams; Dev-A is the dependency gate for B and C, Dev-D is fully independent:
| Stream | Scope | Depends on |
|---|---|---|
| **Dev-A** | Shared `item_build` module (SecretSource, builders, shared edit helpers); refactor personal `add`/`edit`; add `--*-stdin` to personal CLI | — (foundation) |
| **Dev-B** | Org `add`/`edit` parity for **Card / Key / Totp**; secret-stdin flags; field history; `org_items` tests | Dev-A module interface |
| **Dev-C** | Org **Document** + attachment storage (`org_session` methods, default cap, doc add/edit via `--file`); Document tests | Dev-A (`build_document`) |
| **Dev-D** | `relicario-server` hook: `classify_path` attachment grant-scoping; server tests; version bump | — (independent) |
## Open questions
None blocking. The cap value and the exact `--*-stdin` flag spellings are finalized in the plan against the personal-vault source.