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
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>.enconly (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 asProtected(members/collections/org.json),Item { collection }(items/<slug>/<id>.enc— grant-authorized),Rejected, orUnrestricted(everything else — gated only by the per-commit member-signature check). Anattachments/...path therefore currently falls through toUnrestricted: 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
SecretSourceresolution: 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) → interactiveprompt_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-populatedItem(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 ItemCoreslice in place and record field history via the existing synthetic-FieldIdscheme (commands/edit.rs::push_history), reused by both personal and org edit. - Personal
add.rs/edit.rsare refactored to call these helpers with no behavior change (existing personal tests stay green), then gain--*-stdinflags.
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--*-stdinflag 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, mirroringitems/<slug>/<id>.enc. org_sessionmethods: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_capsdefault; 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 recognizeattachments/<slug>/<item-id>/<att-id>.encand classify it asPathClass::Item { collection: slug }— reusing the existing grant + slug-existence authorization for items. Apply the same defenses as theitems/branch: exact segment count and a.-free slug guard (path-traversal defense). - This converts attachment writes from
Unrestrictedto 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 remainUnrestricteduntil 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--*-stdinflags; secret masking verified inorg getwithout--show; a grant-denied attachment-write case.crates/relicario-serverlib tests:classify_path("attachments/eng/<id>/<att>.enc") == Item { collection: "eng" }; rejection cases for malformed attachment paths.- Existing personal
add/edittests 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 shareditem_buildmodule + per-type orgadd/edit.docs/SECURITY.md— attachment writes are now grant-scoped (closing theUnrestrictedgap).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.