Files
relicario/docs/superpowers/specs/2026-06-20-relicario-v0.8.1-parity.md
adlee-was-taken b2f3739673 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
2026-06-20 16:38:05 -04:00

9.9 KiB

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.