Compare commits

..

13 Commits

Author SHA1 Message Date
adlee-was-taken
9b38aac188 docs(specs): v0.9.0 design — extension org GUI + pluggable second factor
Product audit (product-expert skill) recommended two priority items; this
lands the audit record plus the two approved design specs that will drive
the v0.9.0 multi-agent train.

- reviews/2026-06-20-product-audit.md — the roadmap audit (reality check,
  recommendations, PM brief) that drove the two items.
- specs/2026-06-20-extension-org-gui-design.md — bring the org vault to the
  extension at read+write parity. Org write is gated on a Day-1 signing
  spike (the org hook rejects unsigned commits; the extension pushes
  unsigned today; sign_for_git exists in WASM but is unused). Spike-fail
  degrades to read-only + write follow-up.
- specs/2026-06-20-pluggable-second-factor-design.md — key file as an
  alternative second factor (same 32-byte secret, same KDF; crypto-light),
  chosen at setup via a non-secret params hint, plus the positioning pivot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQbgrP6KQW5pibjbPEoTSs
2026-06-20 23:01:53 -04:00
adlee-was-taken
c3044ed5af feat(skills): add product-expert roadmap-audit + spec-review strategist
A standalone, self-triggering skill that acts as Relicario's product
strategist: audits the roadmap and reviews freshly-brainstormed release
specs for product/market fit, emitting PM-ready relay directive blocks.
Advisory only — the user stays the decision-maker.

- Two modes: roadmap audit (default) and spec review (verdict:
  PROCEED / RESCOPE / CUT / PIVOT).
- Four-lens engine run as parallel subagents: ground-truth (verify
  claims vs code/git, distinguishing an in-flight lift from real drift),
  jobs-to-be-done, market/competitive, and strategy synthesis.
- Fast by default; `deep` adds live competitive web research.
- Durable by design: lenses read living docs (README/ROADMAP/STATUS/
  CHANGELOG/specs) at runtime, so new surfaces/segments/features are
  picked up automatically. The one static asset, competitive-landscape.md,
  carries a last-reviewed date + freshness protocol.
- Wires a post-brainstorm product gate into CLAUDE.md's Planning section.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQbgrP6KQW5pibjbPEoTSs
2026-06-20 22:30:48 -04:00
adlee-was-taken
59ebc28e7e docs(user): fix getting-started blockers (add git prereq + git clone command) + clarify sync remote setup 2026-06-20 22:30:07 -04:00
adlee-was-taken
df488a3d7c merge: feature/user-docs (v0.8.1 fast-follow) — 12-page end-user guide 2026-06-20 22:27:57 -04:00
adlee-was-taken
2fa4d6824c release: v0.8.1 — org item-type parity + collection-scoped attachments 2026-06-20 22:24:22 -04:00
adlee-was-taken
783e3493f0 merge: feature/extension-cli-parity-gap-analysis (v0.8.1) — extension<->CLI parity gap analysis (forward plan referenced by STATUS/ROADMAP) 2026-06-20 22:21:05 -04:00
adlee-was-taken
4cca9b465c merge: feature/v0.8.1-status-roadmap (v0.8.1 wrap-up) — mark org item-type parity + attachments shipped; reference parity-gap analysis 2026-06-20 22:14:36 -04:00
adlee-was-taken
5be3043ab5 merge: feature/v0.8.1-cleanup-item-build (v0.8.1 wrap-up) — shared encrypt_document_file helper (F1 DRY + F2 zeroize plaintext) 2026-06-20 22:07:38 -04:00
adlee-was-taken
cf89bf8ca4 merge: feature/v0.8.1-cli-arch-doc (v0.8.1 wrap-up) — document org item-type parity + attachment storage in cli ARCHITECTURE 2026-06-20 22:02:13 -04:00
adlee-was-taken
a91ceea0ed refactor(cli): shared encrypt_document_file helper (DRY org/personal Document build; zeroize source plaintext) 2026-06-20 22:02:07 -04:00
adlee-was-taken
415d8ed9ef docs(cli): document v0.8.1 org item-type parity surface in ARCHITECTURE.md
- org.rs bullet: full Card/Key/Document/Totp org add/edit parity via the
  shared item_build builders + edit helpers; interactive per-type edit;
  --*-stdin secret convention; purge removes attachments. Replaces the stale
  'Login/SecureNote/Identity only' + flag-driven-edit + deferred text.
- org_session.rs bullet: collection-scoped attachment storage (attachment_path/
  save/load/remove + DEFAULT_ORG_ATTACHMENT_MAX_BYTES).
- main.rs bullet: OrgCommands + OrgAddKind clap surface.

Source-line citations pinned per the code-constant-pinning discipline.
2026-06-20 22:00:29 -04:00
adlee-was-taken
b54aaea239 docs(status): v0.8.1 org item-type parity landed — update STATUS + ROADMAP
Mark v0.8.1 shipped (all four streams merged on 4c0a289, verified against
source): org add/edit parity for all 7 item types (Card/Key/Totp + Document),
collection-scoped attachment storage, and the grant-scoped attachment-write
pre-receive hook. Move org item-type parity from deferred to shipped; relabel
the org-vault row as v0.8.0; reference the new extension-cli parity gap analysis
as the forward plan for deferred extension org read/write. Scope: STATUS.md +
ROADMAP.md only (CHANGELOG + version bumps owned by PM).
2026-06-20 21:59:47 -04:00
adlee-was-taken
c5b1917eb0 docs(spec): extension<->CLI parity gap analysis — matrix, gap classification, prioritized work list
Forward-planning survey (not v0.8.1 scope). Two independent sweeps (PM 3-agent +
Dev-D 4-reader workflow with adversarial critic), reconciled and hand-verified
against source at b09e0ce. Headline: core item-CRUD at full parity (extension
often ahead); genuine extension gaps cluster in metadata management (group/tag/
filter editing limited to specific forms, zero favorites UI), backend-exists-no-
wire items (remove_attachment, per-item purge, isInTab attachment gate), autofill
hostname matching, and the org vault (largest, already specced/deferred).
2026-06-20 21:00:43 -04:00
21 changed files with 1193 additions and 61 deletions

View File

@@ -0,0 +1,175 @@
---
name: product-expert
description: >-
Acts as Relicario's product strategist — audits the roadmap and reviews new
release specs for product/market fit, then hands you PM-ready directives.
Use this whenever the user wants to step back from execution and think about
PRODUCT direction: "look over the roadmap", "what should we build next",
"suggest pivots / modifications", "re-prioritize", "is this worth building",
"does this fit the product", "product-market fit", "how do we compare to
Bitwarden / 1Password / vaultwarden", "review this spec before we plan it",
or any post-brainstorming gut-check on a freshly written design spec. Trigger
even when the user doesn't say "product expert" — any roadmap-direction,
prioritization, positioning, or pivot question counts. Do NOT use this for
code review (use /code-review), security review (use /security-review), or
actually implementing a feature (that's the release workflow).
---
# Product Expert
You are Relicario's product strategist. Your job is to look at the product the
way a sharp, opinionated head-of-product would: ground yourself in what's
*actually* built, understand who it's for and what jobs it does for them, weigh
it against the competitive landscape, and then make concrete calls about what to
build, cut, reorder, or pivot. You serve the builder (the user) — you are not a
cheerleader. A recommendation the user disagrees with but has to think about is
worth more than ten that flatter the current plan.
You run **standalone and on-demand**. You do not join the relay. Your output is
analysis plus **paste-ready directive blocks** the user hands to their PM, who
routes the work. The user stays the decision-maker.
## Two modes
Pick the mode from how you were invoked. When ambiguous, ask one question.
| Mode | Invoked by | What you produce |
|------|-----------|------------------|
| **Roadmap audit** (default) | `/product-expert`, "look over the roadmap", "what's next", "suggest pivots" | A reality-adjusted assessment + tagged roadmap modifications + a PM brief block |
| **Spec review** | `/product-expert review <spec-path>`, "review this spec before we plan it", auto-fires after brainstorming a new release | A PROCEED / RESCOPE / CUT / PIVOT verdict on one spec + rationale |
Both modes share the same analytical engine (the four lenses below). Spec review
just points the lenses at one proposed spec instead of the whole roadmap.
## Fast vs deep
Default is **fast**: reason from the repo plus your built-in knowledge of the
password-manager landscape (see `references/competitive-landscape.md` so you're
grounded, not guessing). Fast is right for a quick gut-check and runs every time.
If the invocation includes **`deep`** (e.g. `/product-expert deep`), the market
lens additionally fans out live web research on current competitor features,
pricing, and market shifts — use `WebSearch`/`WebFetch`. Deep is for when the
market view actually needs to be current (a positioning decision, a pivot bet),
not for routine prioritization. Say which mode you ran so the user can calibrate
how much to trust the market read.
## The four lenses
A real product judgment is multi-lens, and the lenses are independent, so run
them as **parallel subagents** (the `Agent` tool) and synthesize the results
yourself. This matches how the rest of this repo works and keeps each lens
focused. Read `references/lenses.md` for the full dispatch prompt for each lens —
the summaries below are just orientation.
1. **Ground-truth** — Reconcile what the docs *claim* (ROADMAP.md, STATUS.md,
CHANGELOG.md, plan checkboxes) against what's *actually* in the code and git
history. This repo has a documented history of drift — work that stealth-
shipped weeks before anyone ticked a box, and status files that lagged
`main`. **Never build a recommendation on a claim you haven't verified.**
But distinguish genuine drift from an **in-flight lift**: if a release is
still being built, merged-but-undocumented code on `main` is expected, not a
defect — check for an active lift (open release label, `coordination/`
artifacts, in-progress checkboxes, open feature branches) before flagging
doc-lag as drift, and never recommend "sync the docs" in a way that would cut
across a running lift. Every strategic call downstream depends on an accurate
picture of reality.
2. **Use-case / jobs-to-be-done** — Who is Relicario for (privacy-conscious
self-hoster, family-vault admin, and now enterprise-org admin), what jobs
they hire it for, and where the product *over-serves* (gold-plating, YAGNI)
or *under-serves* (missing table stakes). CLI/extension parity is a stated
design value here — uneven surfaces are a real product gap, not a nitpick.
3. **Market / competitive** — Position against Bitwarden/vaultwarden,
KeePassXC, 1Password, Proton Pass. Is the wedge (two-factor stego image +
git-backed + server-sees-only-ciphertext) a durable differentiator for the
target user, or a gimmick that adds friction? What's table stakes that's
missing? In `deep` mode this lens does live web research.
4. **Strategy** — The synthesis lens. Given reality + users + market: what to
ADD, CUT, REORDER, or PIVOT, each with a rough impact-vs-effort read, the
risk, and where it sits relative to a v1.0 line. This is where opinion earns
its keep — make the call, don't just list options.
You synthesize all four into the output. Don't outsource the final judgment to a
subagent; the lenses gather and assess, you decide.
## How to run
1. **Orient.** Read ROADMAP.md, STATUS.md, and CHANGELOG.md headers, and skim
the relevant `docs/superpowers/specs/`. In spec-review mode, read the target
spec in full first.
2. **Dispatch the lenses.** Spawn the four lens subagents in parallel using the
prompts in `references/lenses.md`. In fast mode the market lens uses the
cheat-sheet; in deep mode it also does web research.
3. **Synthesize.** Reconcile the lens findings. Where ground-truth contradicts
a claim, the ground-truth wins and you flag the drift explicitly.
4. **Write the output** in the right format (next section). Lead with the
sharpest, highest-leverage point — don't bury the pivot recommendation under
ten small reorderings.
5. **Offer to persist.** For a roadmap audit, offer to save the brief to
`docs/superpowers/reviews/YYYY-MM-DD-product-audit.md` so there's a record of
what was considered (use the repo's local date; do not invent one — read it
from the environment). Don't write it unasked.
## Output
Use the exact templates in `references/output-templates.md`. In short:
**Roadmap audit** produces, in this order:
- **Reality check** — one paragraph on where the product *actually* stands,
plus any roadmap-vs-code drift you found.
- **Assessment** — the product's current strengths, gaps, and risks through the
use-case and market lenses.
- **Recommendations** — a tagged list (**ADD / CUT / REORDER / PIVOT**), each
with a one-line rationale and an impact/effort read. Highest-leverage first.
- **PM brief** — a paste-ready block (see template) the user drops to the PM.
**Spec review** produces:
- **Verdict** — one of PROCEED / RESCOPE / CUT / PIVOT, stated up front.
- **Rationale** — does the spec serve a real user job, fit the positioning, and
justify its opportunity cost versus what else is on the roadmap? Is it scoped
right, or is it gold-plating?
- **If not PROCEED** — concrete rescope/pivot suggestions, then hand back to the
brainstorming → writing-plans flow.
## Staying current
This skill is built to stay relevant as Relicario grows, because the four lenses
read **living docs at runtime** (README, ROADMAP, STATUS, CHANGELOG, the specs)
rather than hardcoding the product's state. New item types, surfaces (mobile,
Safari), segments, and shipped tracks are picked up automatically the next time
you run — you should not need to edit the lens prompts when the product evolves.
Two things *can* go stale, and you should actively keep them fresh:
- **`references/competitive-landscape.md`** is the only static asset, and the
market moves underneath it. It carries a `last-reviewed` date and a freshness
protocol: if it's more than ~6 months old, prefer `deep` mode and offer to
refresh the file; whenever a `deep` run proves it wrong or incomplete, offer to
fold the correction back in and bump the date. Treat the cheat-sheet as
something that should improve every time it's used in anger.
- **This repo's conventions** (relay block headers, doc locations, the release
workflow) are cited in the templates and prompts. If a citation here stops
matching reality — a renamed doc, a changed relay format — fix it in the same
pass, the way the rest of this codebase pins code constants to their source.
When in doubt about whether the skill's framing still fits the product, trust the
living docs over this skill's prose, and flag the mismatch so it can be corrected.
## Principles
- **Verify before you opine.** The ground-truth lens is non-negotiable. A
confident recommendation built on a stale STATUS line is worse than useless.
- **Have a point of view.** Lead with a recommendation, not a survey. The user
has CLAUDE.md set to "default to the recommended option" — give them one.
- **YAGNI is a product tool, not just an engineering one.** Cutting or deferring
is a legitimate, often the highest-leverage, recommendation. Look for it.
- **Respect the wedge, test the wedge.** The two-factor / self-host / git
thesis is the product's reason to exist. Take it seriously — and be honest
when a proposed feature dilutes it or when the wedge isn't paying off for the
target user.
- **Stay in your lane.** You assess product direction. You don't write feature
code, you don't merge, and you don't redesign the crypto. Bugs and security
belong to `/code-review` and `/security-review`.

View File

@@ -0,0 +1,114 @@
# Competitive landscape — password managers
> **last-reviewed: 2026-06-20.** This file is the only static, rot-prone asset in
> the skill (the four lenses otherwise read living docs at runtime). The market
> moves: competitors ship features, get breached, change pricing, appear, and
> die. Treat every claim below as "true as of last-reviewed, verify if it
> matters."
**Freshness protocol:**
- If `last-reviewed` is **more than ~6 months** before today, treat this file as
suspect: prefer running the market lens in **deep** mode (live web research)
over trusting the snapshot, and at the end of the run *offer to refresh this
file* (re-research the competitors, rewrite the entries, bump `last-reviewed`).
- Any time a **deep**-mode run surfaces something this file gets wrong or misses
(a new competitor, a shipped feature, a breach), offer to fold it back in and
bump the date. The cheat-sheet should improve every time it's proven stale.
A grounding cheat-sheet for the market lens in **fast** mode so it reasons from a
real map, not vibes.
The goal isn't to rank these for everyone — it's to locate Relicario's wedge
honestly: where the two-factor / self-host / git-backed / server-sees-ciphertext
thesis genuinely wins for the target user, and where Relicario is simply behind
on table stakes.
---
## The field
### Bitwarden
- Open-source, freemium, cloud-hosted by default; self-host possible (official
server is heavy; **vaultwarden** is the popular lightweight Rust reimpl).
- Single-factor KDF: master password (optionally with 2FA gating *login*, not the
KDF). Server breach entropy rests on the master password alone.
- Strong on: ubiquity, mature mobile + browser autofill, painless import/export,
organizations & sharing, low/zero price.
- The default thing a privacy-conscious technical user reaches for. **This is
Relicario's primary reference competitor** — most "why not just use X" pressure
comes from here (specifically self-hosted vaultwarden).
### vaultwarden
- Community Rust server compatible with Bitwarden clients; trivial to self-host
(single container). Inherits Bitwarden's polished clients for free.
- This is the sharpest comparison for Relicario's self-host story: a user who
wants self-hosted secrets already has a turnkey, full-featured option with
mobile apps and autofill. Relicario must justify what it adds *over* this.
### KeePassXC (+ KeePass ecosystem)
- Local-first, file-based (`.kdbx`), no server at all; sync is BYO (Dropbox,
Syncthing, git, etc.). Open-source, free.
- Single-factor by default but supports key files / hardware keys as a second
factor — conceptually the closest mainstream analog to Relicario's "something
you have" image secret (a key file is the unglamorous version of the stego
image).
- Strong on: zero-trust-server (there is no server), longevity, plugin ecosystem.
- Weak on: clunky cross-device sync, dated UX, mobile is third-party.
- The other user Relicario competes for: the "I don't trust any cloud" crowd.
### 1Password
- Commercial, polished, cloud-only (no self-host). **Two-factor KDF**: master
password + a 128-bit Secret Key — the mainstream product whose security model
is closest in spirit to Relicario's (two factors into the key derivation).
- Strong on: best-in-class UX, mobile, autofill, family/team sharing, support.
- Relevant because it proves the two-factor-KDF idea is marketable — but it does
it with a boring random Secret Key, not steganography, and gives up self-host.
### Proton Pass
- Newer, from Proton (Mail/VPN); privacy-positioned, cloud, freemium, open-source
clients. Single-factor KDF; leans on brand trust and the Proton bundle.
- Relevant as the "privacy brand" competitor — it wins on trust + ecosystem, not
on a novel crypto model.
### LastPass (cautionary tale, not a competitor to chase)
- Repeated breaches (notably 2022) where exfiltrated vaults were only as strong
as users' master passwords — the canonical argument *for* a second KDF factor.
- Useful in positioning: Relicario's README already uses LastPass as the "~4060
bits, single factor" baseline. The market lesson is real and on Relicario's
side, but invoking it is marketing, not differentiation.
---
## Where Relicario can win (the honest version)
- **Server-sees-only-ciphertext + no metadata** against a self-host backend that
still stores structured data. This is a genuine, explainable edge over
vaultwarden for the threat-model-literate user.
- **Two factors into the KDF** (not just 2FA on login) — only 1Password really
matches this, and it isn't self-hostable. That intersection (two-factor KDF +
self-host) is close to empty. That's the wedge.
- **Git as audit log** — "when was this rotated?" answered by `git log` and field
history. Niche, but unique and real for the audit-conscious user.
## Where Relicario is behind (table stakes to be honest about)
- **Mobile.** Bitwarden/1Password/Proton all have first-class mobile apps with
autofill. Relicario is CLI + browser extension; the Rust core compiles to ARM
but there's no shipped mobile client. For most users this alone is
disqualifying — weigh it heavily.
- **Autofill quality & breadth.** Browser-extension autofill maturity is a moat
the incumbents have spent years on.
- **Frictionless import** from the incumbents (Bitwarden, 1Password) — LastPass
CSV exists; the others are on the roadmap. Import friction is a real adoption
tax.
- **Sharing / multi-user polish.** The org-vault track is new; incumbents have
mature org/family sharing.
## The uncomfortable question to keep asking
For a user who wants self-hosted secrets, **vaultwarden already exists and is
turnkey with great clients.** Every Relicario feature should be weighed against:
"does this widen the gap on the thesis (two-factor KDF, no-metadata, git audit),
or is it just trying to catch up to vaultwarden on table stakes I'll never win?"
The strategy lens should treat *catching up to vaultwarden's client polish* and
*deepening the unique thesis* as different bets with very different ROI.

View File

@@ -0,0 +1,155 @@
# The four lenses — dispatch prompts
Spawn these as parallel subagents (the `Agent` tool). Each returns a written
findings block; you (the orchestrator) synthesize them. Give each subagent the
mode (fast/deep) and, in spec-review mode, the path to the spec under review.
Use a read-only agent type (`Explore`) for lenses 12, `general-purpose` for the
market lens (it may need web access in deep mode), and run the strategy lens
*after* the first three return — it consumes their output, so it isn't parallel
with them. Keep each lens's prompt scoped to its question; the value of running
them separately is that none of them tries to do everything.
---
## Lens 1 — Ground-truth
> You are auditing what is *actually built* in the Relicario repo versus what the
> project docs claim. This is a reality check, not a code review — do not hunt
> for bugs.
>
> Do this:
> 1. Read ROADMAP.md, STATUS.md, CHANGELOG.md and note every claim about what has
> shipped, what's in flight, and what's next.
> 2. Cross-check those claims against reality: `git log --oneline -40`, the tags
> (`git tag`), the actual presence of the files/modules/commands the claims
> describe, and whether the test suite is green (`cargo test` may be too slow —
> instead check for a recent green signal: CI config, recent test commits, or
> run a targeted `cargo test -p <crate>` only if quick).
> 3. Check plan checkboxes in `docs/superpowers/plans/` against the commits that
> would have ticked them.
>
> This repo has a documented history of *drift*: work that merged weeks before
> anyone updated STATUS, and "up next" lists that lagged `main`. Specifically
> look for: (a) claimed-shipped work that isn't actually in the code, (b) work
> that's in the code but not reflected in the roadmap/status, (c) plan boxes that
> contradict git history.
>
> CRITICAL distinction — drift vs. in-flight lift. Docs lagging the code is NOT
> automatically "drift to fix." A release lift that is *still in progress* will
> legitimately have merged code on `main` while ROADMAP/STATUS/CHANGELOG haven't
> been synced and no tag has been cut — that's expected, and flagging it as
> stale-docs-to-fix is wrong (and could disrupt an active lift). Before you label
> any doc-lag as drift, check whether a lift is currently running: look for a
> recent unfinished release label, coordination artifacts in
> `docs/superpowers/coordination/` (a `*-launch.sh`, dev/PM prompt files dated
> now), in-progress plan checkboxes, or feature branches still open for the
> current release (`git branch -a`). If the work belongs to an active,
> not-yet-tagged release, report it as "in flight (lift running) — docs sync at
> wrap," NOT as drift. Reserve "drift" for docs that contradict *finished/tagged*
> reality.
>
> Return: a concise "reality-adjusted state of the product" — what is genuinely
> shipped (tagged), what is genuinely in flight (and whether a lift is actively
> running), and a bulleted list of every genuine drift you found (claim vs.
> finished reality, with the commit or file that proves it). Be specific;
> downstream strategy depends entirely on this being accurate.
---
## Lens 2 — Use-case / jobs-to-be-done
> You are assessing Relicario as a *product for its users* — not its code.
>
> Relicario is a git-backed, self-hostable password manager with two-factor
> vault decryption (a memorized passphrase + a reference JPEG carrying a hidden
> 256-bit secret via DCT steganography). The server only ever sees ciphertext.
> Read README.md and `docs/superpowers/specs/` for the threat model and intended
> users — let those living docs define the current segments and client surfaces
> rather than assuming. As of this writing the segments are the privacy-conscious
> self-hoster, the family-vault admin, and the enterprise-org admin, on a CLI +
> browser-extension surface — but treat the docs as authoritative if the project
> has since grown new segments or surfaces (e.g. mobile, Safari).
>
> Answer:
> 1. Who is this really for, and what jobs do they hire it for? Map the major
> features to the jobs they serve.
> 2. Where does the product *over-serve* — features that are gold-plated, niche,
> or YAGNI relative to the jobs the target users actually have?
> 3. Where does it *under-serve* — table-stakes capabilities a user in this
> segment would expect and not find, or flows with real friction?
> 4. CLI/extension parity is an explicit design value in this project. Flag any
> place a capability exists on one surface but not the other — that's a
> product gap here, not a nitpick.
>
> Return: a crisp jobs-to-be-done map, the over-served list, the under-served /
> friction list, and any parity gaps. Prioritize by how much each affects a real
> user's decision to adopt or stay.
---
## Lens 3 — Market / competitive
> You are positioning Relicario against the password-manager market.
>
> Relicario's wedge: two independent decryption factors (passphrase + a
> steganographic reference image that can live as a "dead drop" on social
> media), git as the sync/audit backbone, full self-hostability, and a server
> that only ever holds opaque ciphertext (no metadata). It's GPL and open-source;
> check README.md / ROADMAP.md for the current release stage, client surfaces,
> and which tracks (e.g. enterprise org vault, mobile) have shipped versus are
> still in flight — don't assume from this prompt.
>
> Read `references/competitive-landscape.md` in this skill for a grounded map of
> the competitors (Bitwarden/vaultwarden, KeePassXC, 1Password, Proton Pass, and
> the LastPass cautionary tale) before you reason — don't work from vibes.
>
> {FAST MODE}: reason from that cheat-sheet plus your own knowledge.
> {DEEP MODE}: additionally run live web research (WebSearch/WebFetch) on current
> competitor feature sets, pricing, recent breaches/news, and any market shifts
> in self-hosted or privacy-first password management. Cite what you find.
>
> Answer:
> 1. Where does Relicario genuinely win for its target user, and where is it
> merely at parity or behind?
> 2. Is the two-factor / stego wedge a *durable differentiator* for that user, or
> a gimmick that adds friction more than security value? Argue it honestly.
> 3. What is table stakes in this market that Relicario lacks (e.g. mobile
> clients, autofill quality, painless import, secure sharing)?
> 4. What positioning / messaging actually lands for the people who'd choose this
> over Bitwarden or KeePassXC?
>
> Return: a positioning read (wins / parity / behind), an honest verdict on the
> wedge, the table-stakes gap list, and a one-paragraph positioning statement.
> State whether you ran fast or deep.
---
## Lens 4 — Strategy (synthesis input)
Run this lens *after* lenses 13 return; paste their findings into its prompt.
> You are the strategy synthesis for a Relicario product review. You are given
> three findings blocks: ground-truth (what's actually built + drift),
> jobs-to-be-done (over/under-served), and market (positioning + gaps). Below
> them is the current ROADMAP.md "up next" list.
>
> Produce a prioritized set of roadmap moves. Tag each move exactly one of:
> - **ADD** — new work that should be on the roadmap and isn't.
> - **CUT** — work that should be dropped or indefinitely deferred (YAGNI, off-
> thesis, or low-value for the target user). Cutting is a first-class call.
> - **REORDER** — work that's correctly scoped but mis-sequenced; say what should
> come before what and why.
> - **PIVOT** — a larger directional change (segment, positioning, or thesis).
> Use sparingly and argue it hard.
>
> For each move give: a one-line rationale grounded in the lens findings, a rough
> impact-vs-effort read (high/med/low each), and the main risk. Order the whole
> list by leverage — the single highest-impact move first.
>
> Be opinionated and specific. "Consider exploring options around mobile" is
> useless; "ADD: ship a read-only Android client before any new desktop feature —
> the market lens shows mobile is the #1 table-stakes gap and the Rust core
> already compiles to ARM, so effort is medium / impact high" is the bar.
>
> Return: the tagged, leverage-ordered move list, nothing else.

View File

@@ -0,0 +1,94 @@
# Output templates
Use these verbatim in structure. Fill the brackets. Keep prose tight — the user
reads this to make a decision, not to admire it. Lead with the highest-leverage
point in every section.
---
## Roadmap-audit output
```markdown
# Product Audit — Relicario — [YYYY-MM-DD] · [fast | deep]
## Reality check
[One paragraph: where the product *actually* stands, reconciled against code +
git, not the docs' self-description.]
**Drift found:** [bulleted list of every claim-vs-reality mismatch, each with the
commit/file that proves it. Write "none — docs match reality" if clean.]
## Assessment
**Strengths:** [24 bullets — what's genuinely working, through the user + market lenses.]
**Gaps:** [24 bullets — table-stakes misses, friction, parity gaps.]
**Risks:** [13 bullets — what could undermine the product or the thesis.]
## Recommendations
[Leverage-ordered. Highest-impact first. Each line:]
- **[ADD|CUT|REORDER|PIVOT]** — [the move]. *Why:* [one line]. *Impact/Effort:* [H/M/L · H/M/L]. *Risk:* [one line].
## PM brief
[The paste-ready block — see "PM brief block" below.]
```
---
## PM brief block
This is what the user pastes to their PM (the relay entry point). It mirrors the
repo's relay block conventions (`## DIRECTIVE TO …`, ISO timestamp) but is a
*strategy brief*, not a dev task — the PM reads it and decides how to route work
to the devs. Keep it self-contained: the PM may act on it without the full audit.
```markdown
## PRODUCT DIRECTIVE TO PM
Time: [ISO-8601 local timestamp]
Source: /product-expert roadmap audit ([fast|deep])
Reality note: [one line — any drift the PM must know before acting, e.g. "STATUS
claims Plan X shipped; it hasn't — verify before scheduling dependent work."]
Roadmap changes (in priority order):
1. [ADD|CUT|REORDER|PIVOT] [the move] — [one-line why].
2.
Recommended next slice: [the single thing the PM should queue first, and why it's
first.]
Out of scope / explicitly deferred: [what to NOT pick up, so the PM doesn't
re-add cut work.]
```
The user edits this before relaying. Never invent commit SHAs or claim something
is merged unless the ground-truth lens verified it — per this repo's relay
conventions, unverified SHAs in PM messages cause real confusion.
---
## Spec-review output
```markdown
# Spec Review — [spec filename] — [YYYY-MM-DD] · [fast | deep]
## Verdict: [PROCEED | RESCOPE | CUT | PIVOT]
[One sentence stating the call plainly.]
## Rationale
- **User job:** [does it serve a real job for a real segment? which?]
- **Positioning fit:** [does it strengthen or dilute the wedge?]
- **Opportunity cost:** [what does building this displace on the roadmap? is that
trade worth it?]
- **Scope:** [right-sized, gold-plated, or under-built? cite the YAGNI risks.]
## [If not PROCEED] Suggested changes
[Concrete rescope / cut-line / pivot direction. Be specific enough that the next
step is obvious.]
## Next step
[If PROCEED: "Spec holds up — proceed to writing-plans." Otherwise: "Revise the
spec per above, then re-review or proceed."]
```
A PROCEED verdict hands straight back to the normal brainstorming → writing-plans
flow. A RESCOPE/CUT/PIVOT verdict should be specific enough that the user can act
without a second round of analysis.

View File

@@ -1,5 +1,56 @@
# Changelog # Changelog
## v0.8.1 — 2026-06-20 — org item-type parity + collection-scoped attachments
Brings `relicario org add` / `relicario org edit` to **full item-type parity** with the
personal vault: the org surface now supports **all 7 item types** (previously Login /
SecureNote / Identity only), adds collection-scoped attachment storage for Document
items, and grant-scopes attachment write paths in the pre-receive hook — closing a latent
authorization gap. Secrets are entered via interactive prompts by default, with `--*-stdin`
escape hatches for non-interactive scripting. Tracked under
`docs/superpowers/plans/2026-06-20-relicario-v0.8.1-parity.md`.
> **⚠️ Coordinated server redeploy required.** The `relicario-server` pre-receive hook
> (now `0.1.1`) must be rebuilt and redeployed for attachment writes to be grant-scoped in
> production. Until the updated hook is installed, `attachments/…` pushes remain
> `Unrestricted` (gated only by the per-commit member-signature check).
### Added
- **Shared `item_build` CLI module** (`crates/relicario-cli/src/commands/item_build.rs`):
centralizes per-type secret resolution, item builders (`build_*`), and interactive edit
helpers (`edit_*`) consumed by **both** the personal and org command surfaces, eliminating
the prior personal↔org builder duplication.
- **Org `add` / `edit` parity for Card, Key, TOTP, and Document** — `relicario org add` now
creates all 7 item types; `relicario org edit` is interactive per-type ("blank to keep",
field-history capture) instead of flat flags.
- **`--*-stdin` secret flags** on personal and org `add` for non-interactive entry of
passwords, card number/CVV/PIN, key material, TOTP secrets, and note bodies.
- **Collection-scoped org attachment storage** (`crates/relicario-cli/src/org_session.rs`):
attachments stored at `attachments/<slug>/<item-id>/<att-id>.enc` with a default
per-attachment cap (10 MiB, mirroring the personal default at
`crates/relicario-core/src/settings.rs`). `org add document --file`, `org edit --file`
(replace), and `org purge` (removes the item's attachment directory) round-trip with
git-status-clean staging.
### Security
- **Grant-scoped attachment writes** (`relicario-server` `0.1.1`): `classify_path` now
recognizes `attachments/<slug>/<item-id>/<att-id>.enc` (exactly 3 path segments, `.`-free
slug guard) as `Item { collection }`, bringing attachment writes under the same grant +
slug-existence check as `items/` blobs. Previously such paths fell through to
`Unrestricted`. The Document source plaintext is read into a `Zeroizing` buffer and wiped
after encryption. See `docs/SECURITY.md`.
### Changed
- Personal `add secure-note` `--body-prompt` flag renamed to `--body-stdin` (unified
multiline-secret model).
### Docs
- Updated cli `ARCHITECTURE.md`, `docs/FORMATS.md` (org attachment layout + cap citation),
`docs/SECURITY.md`, `STATUS.md`, and `ROADMAP.md`. New
`docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md` is the forward
plan for extension↔CLI parity (org read/write plus a cluster of personal-side extension
gaps). End-user `user_docs/` guide lands as a fast-follow.
## v0.8.0 — 2026-06-20 — enterprise org vault ## v0.8.0 — 2026-06-20 — enterprise org vault
Git-native multi-user **org vaults**: a separate org git repository alongside each Git-native multi-user **org vaults**: a separate org git repository alongside each

View File

@@ -90,6 +90,8 @@ Source code: `ssh://git@git.adlee.work:2222/alee/relicario.git`
**Before starting any planning or implementation task**, search `docs/superpowers/specs/` for a spec covering the feature area, and `docs/superpowers/plans/` for any existing implementation plan. The specs are the authoritative design record; plans track per-milestone implementation details. Once a plan exists, execute it via the release workflow (see **Release lifecycle** below) — not directly via subagent-driven-development or executing-plans unless the workflow is unavailable. **Before starting any planning or implementation task**, search `docs/superpowers/specs/` for a spec covering the feature area, and `docs/superpowers/plans/` for any existing implementation plan. The specs are the authoritative design record; plans track per-milestone implementation details. Once a plan exists, execute it via the release workflow (see **Release lifecycle** below) — not directly via subagent-driven-development or executing-plans unless the workflow is unavailable.
**Product gate.** After brainstorming a new release spec — before handing it to writing-plans — run `/product-expert review <spec-path>` for a product/market-fit gut-check (PROCEED / RESCOPE / CUT / PIVOT). The `product-expert` skill also runs standalone (`/product-expert`) any time you want to audit the whole roadmap and get PM-ready directives for modifications or pivots; add `deep` for live competitive web research. It only advises — you stay the decision-maker.
Core references (read before touching crypto, data model, or architecture): Core references (read before touching crypto, data model, or architecture):
- `docs/superpowers/specs/2026-04-11-relicario-design.md` — threat model, entropy analysis, crypto pipeline, crate layout - `docs/superpowers/specs/2026-04-11-relicario-design.md` — threat model, entropy analysis, crypto pipeline, crate layout
- `docs/superpowers/specs/2026-04-18-relicario-typed-items-design.md` — typed-item data model and envelope - `docs/superpowers/specs/2026-04-18-relicario-typed-items-design.md` — typed-item data model and envelope

6
Cargo.lock generated
View File

@@ -2156,7 +2156,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "relicario-cli" name = "relicario-cli"
version = "0.8.0" version = "0.8.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arboard", "arboard",
@@ -2188,7 +2188,7 @@ dependencies = [
[[package]] [[package]]
name = "relicario-core" name = "relicario-core"
version = "0.8.0" version = "0.8.1"
dependencies = [ dependencies = [
"argon2", "argon2",
"base64", "base64",
@@ -2235,7 +2235,7 @@ dependencies = [
[[package]] [[package]]
name = "relicario-wasm" name = "relicario-wasm"
version = "0.8.0" version = "0.8.1"
dependencies = [ dependencies = [
"base64", "base64",
"ed25519-dalek", "ed25519-dalek",

View File

@@ -7,7 +7,8 @@
| Version | Highlights | | Version | Highlights |
|---|---| |---|---|
| *(untagged, 2026-06-20)* | **Enterprise org vault — backend complete** (`7392795`): relicario-core `org` module (ECIES X25519 key wrap/unwrap, `OrgRole`/`OrgMember`/`OrgManifest` types, `filter_for_member`, `schema_version: 1`); relicario-server org hook (`verify-org-commit`: signature verification, path-scoped authz, `enforce_owner_only_elevation` on parent role, `enforce_schema_monotonicity`, `generate-org-hook`, new `[lib]` target); relicario-cli — all 19 `relicario org` subcommands: init, add-member/remove-member/set-role, create-collection/grant/revoke, rotate-key (re-encrypts all blobs), transfer-ownership, delete-org, status, audit, and item CRUD (add/get/list/edit/rm/restore/purge). **Not yet shipped:** `org add`/`edit` for Card/SshKey/Document/Totp; extension org parity (Dev-D); phase 2 (SSO/LDAP, read audit, per-collection subkeys, HTTP plane). | | **v0.8.1** *(2026-06-20, tag pending PM)* | **Org item-type parity + collection-scoped attachments + grant-scoped hook** (`4c0a289`, four parallel streams): `relicario org add`/`edit` now cover **all 7 item types** — Card/Key/Totp (Dev-B `6e73c5e`) and Document (Dev-C `4c0a289`) on the shared `item_build` foundation (Dev-A `b09e0ce`); org attachments stored collection-scoped at `attachments/<slug>/<item-id>/<att-id>.enc` with a default cap (Dev-C); `relicario-server` `classify_path` grant-scopes those attachment writes (Dev-D `db4e05a`, server `0.1.1`**requires pre-receive hook redeploy**). **Still deferred:** extension org read/write (forward plan: `docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md`); org phase 2. |
| v0.8.0 *(2026-06-20)* | **Enterprise org vault — backend complete** (`7392795`): relicario-core `org` module (ECIES X25519 key wrap/unwrap, `OrgRole`/`OrgMember`/`OrgManifest` types, `filter_for_member`, `schema_version: 1`); relicario-server org hook (`verify-org-commit`: signature verification, path-scoped authz, `enforce_owner_only_elevation` on parent role, `enforce_schema_monotonicity`, `generate-org-hook`, new `[lib]` target); relicario-cli — all 19 `relicario org` subcommands: init, add-member/remove-member/set-role, create-collection/grant/revoke, rotate-key (re-encrypts all blobs), transfer-ownership, delete-org, status, audit, and item CRUD (add/get/list/edit/rm/restore/purge). Org item-type parity for Card/Key/Document/Totp shipped subsequently in v0.8.1; extension org parity + phase 2 (SSO/LDAP, read audit, per-collection subkeys, HTTP plane) remain deferred. |
| v0.7.0 *(2026-06-01)* | Extension restructure (Plan C) complete — Phases 3/4/6 merged via 3 parallel worktree streams under PM coordination: setup wizard crypto migrated into the SW (`create_vault`/`attach_vault`; `setup.ts` 1230→58 LOC + step registry); `vault.ts` split 1037→194 LOC into 5 focused + 2 support modules; `vault_locked` intercept lifted into `shared/state.ts`; `get_vault_status` SW message + sidebar status indicator closing the last `relicario status` CLI/extension parity gap | | v0.7.0 *(2026-06-01)* | Extension restructure (Plan C) complete — Phases 3/4/6 merged via 3 parallel worktree streams under PM coordination: setup wizard crypto migrated into the SW (`create_vault`/`attach_vault`; `setup.ts` 1230→58 LOC + step registry); `vault.ts` split 1037→194 LOC into 5 focused + 2 support modules; `vault_locked` intercept lifted into `shared/state.ts`; `get_vault_status` SW message + sidebar status indicator closing the last `relicario status` CLI/extension parity gap |
| v0.6.0 *(2026-05-30)* | Security audit fixes; device authentication; backup/restore + LastPass import; fullscreen UX Phases 1+2A+2B; v0.5.1 Streams A/B/C (3-column vault layout + bottom-sheet picker + toast system; left-nav settings; Recovery QR end-to-end + setup wizard Style C); 1C-γ (attachments + Document type + device registration + trash + field history); Plan B multi-stream refactor (commands/ split, prompt_or_flag, core/WASM seam); vault-tab management surfaces revamp (settings synced/local split, devices fingerprint, trash purge countdown, field-history polish, item-history-index, `#history/<id>` routing); doc-structure redesign (rename to DESIGN/CRYPTO/docs/FORMATS, scope headers + Next: footers); GPL-3.0-or-later license | | v0.6.0 *(2026-05-30)* | Security audit fixes; device authentication; backup/restore + LastPass import; fullscreen UX Phases 1+2A+2B; v0.5.1 Streams A/B/C (3-column vault layout + bottom-sheet picker + toast system; left-nav settings; Recovery QR end-to-end + setup wizard Style C); 1C-γ (attachments + Document type + device registration + trash + field history); Plan B multi-stream refactor (commands/ split, prompt_or_flag, core/WASM seam); vault-tab management surfaces revamp (settings synced/local split, devices fingerprint, trash purge countdown, field-history polish, item-history-index, `#history/<id>` routing); doc-structure redesign (rename to DESIGN/CRYPTO/docs/FORMATS, scope headers + Next: footers); GPL-3.0-or-later license |
| v0.2.0 | Typed-item rewrite (Plans 1A/1B/1C-α/β₁/β₂) | | v0.2.0 | Typed-item rewrite (Plans 1A/1B/1C-α/β₁/β₂) |
@@ -16,11 +17,11 @@ See `CHANGELOG.md` for tagged-release detail and `STATUS.md` for the per-train c
## Up next ## Up next
All three 2026-05-04 architecture-review specs are shipped; enterprise org vault backend is shipped (2026-06-20). Pending items in rough priority order: All three 2026-05-04 architecture-review specs are shipped; the enterprise org vault backend (v0.8.0) and org item-type parity + collection-scoped attachments (v0.8.1) are shipped. Forward plan for extension parity: `docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md`. Pending items in rough priority order:
- **Org-vault item-type parity** — `org add`/`edit` support for Card, SshKey, Document, Totp (Login/SecureNote/Identity ship today)
- **Extension org parity — read** — org switch + collection-filtered browse in the popup/vault tab (Dev-D, deferred) - **Extension org parity — read** — org switch + collection-filtered browse in the popup/vault tab (Dev-D, deferred)
- **Extension org parity — write** — `org add`/`edit`/`rm` from the extension - **Extension org parity — write** — `org add`/`edit`/`rm` from the extension (Plan B-2; the CLI side reached all-7-type org write in v0.8.1, so this is unblocked CLI-side)
- **Personal-side extension gaps** — favorites UI, group/tag/filter editing across all type forms, attachment-remove router wire + per-item purge UI, autofill registrable-domain matching (per the parity gap analysis)
- **Phase 4: command palette** — ⌘K global search + action dispatch across the vault tab (no spec yet) - **Phase 4: command palette** — ⌘K global search + action dispatch across the vault tab (no spec yet)
## Medium-term ## Medium-term

View File

@@ -5,10 +5,23 @@
## Version ## Version
**Last release tagged:** v0.6.0 — rolled up Phase 2B, v0.5.1 Streams A/B/C, 1C-γ, Plan B refactor (Cycles 1+2), management-surfaces revamp, and the doc-structure redesign into one tag. **Last release tagged:** v0.6.0 — rolled up Phase 2B, v0.5.1 Streams A/B/C, 1C-γ, Plan B refactor (Cycles 1+2), management-surfaces revamp, and the doc-structure redesign into one tag.
**Active track:** **extension restructure (Plan C) — COMPLETE.** All six phases merged. Phases 1, 2, 5 merged 2026-05-30; Phases 3, 4, 6 merged 2026-05-31/06-01 via three parallel worktree streams (Dev-A/B/C under PM coordination). Versions bumped to v0.7.0; tag pending. **Active track:** **v0.8.1 — org item-type parity — COMPLETE (on `main` `4c0a289`; tag pending PM).** All four parallel streams merged: shared item-build foundation + personal add/edit refactor (Dev-A, `b09e0ce`); org add/edit parity for Card/Key/Totp (Dev-B, `6e73c5e`); org Document + collection-scoped attachment storage (Dev-C, `4c0a289`); grant-scoped attachment write-path hook (Dev-D, `db4e05a`). See the v0.8.1 landing section below.
## What landed on main since the v0.5.0 version bump ## What landed on main since the v0.5.0 version bump
### v0.8.1 — org item-type parity + collection-scoped attachments + grant-scoped hook (merged 2026-06-20, `4c0a289`)
Spec: `docs/superpowers/specs/2026-06-20-relicario-v0.8.1-parity.md`; plan: `docs/superpowers/plans/2026-06-20-relicario-v0.8.1-parity.md`. Four parallel streams under PM coordination (relay-bus):
- **Dev-A — shared item-build foundation** (merge `b09e0ce`): `commands/item_build.rs` (shared secret-resolution, type parsers, per-type `build_*`/`edit_*` helpers, `push_history`); personal `add`/`edit` refactored onto it; personal `--*-stdin` flags for non-interactive scripting/tests.
- **Dev-B — org Card/Key/Totp parity** (merge `6e73c5e`): `OrgAddKind` gains Card/Key/Totp; `org edit` becomes per-type interactive dispatch (the old "login/secure-note/identity only" bail is gone).
- **Dev-C — org Document + collection-scoped attachments** (merge `4c0a289`): `OrgAddKind::Document`; `org_session.rs` attachment storage (`attachment_path`/`save_attachment`/`load_attachment`/`remove_item_attachments`) writing `attachments/<slug>/<item-id>/<att-id>.enc`; default org attachment cap; `org add document --file` + `org edit --file`; purge removes the item's attachment dir.
- **Dev-D — grant-scoped attachment hook** (merge `db4e05a`): `relicario-server` `classify_path` recognizes `attachments/<slug>/<item-id>/<att-id>.enc` (3 segments, slug-only `.`-free guard) as `Item { collection }`, converting attachment writes from `Unrestricted` to grant-scoped — closing a latent authz gap. Bumped `relicario-server` to 0.1.1; `docs/SECURITY.md` documents the required pre-receive hook redeploy.
Result: `relicario org add`/`edit` now reach **all 7 item types** (Login, Secure Note, Identity, Card, Key, TOTP, Document); org attachments are collection-scoped on disk and grant-enforced at the hook. The C↔D path contract held in the merge — Dev-C's `save_attachment` emitter (`attachments/{slug}/{item}/{att}.enc`) exactly matches Dev-D's `classify_path` authorization. **Deploy note:** the pre-receive hook must be rebuilt on the server for attachment writes to be grant-scoped in production.
**Still deferred — forward plan in `docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md`:** extension org **read** (Dev-D) and **write** (Plan B-2) — the extension has no org concept yet; org phase-2 (SSO/LDAP, read audit, per-collection subkeys, HTTP plane). That parity gap analysis is the authoritative forward plan for extension↔CLI parity (org read/write plus a cluster of personal-side extension gaps: favorites UI, group/tag/filter editing, attachment-remove router wire, per-item purge).
### Phase 2B — polish foundation + form layout (merged 2026-05-02, `5da1e52`) ### Phase 2B — polish foundation + form layout (merged 2026-05-02, `5da1e52`)
Spec: `docs/superpowers/specs/2026-05-02-phase-2b-form-layout-design.md` Spec: `docs/superpowers/specs/2026-05-02-phase-2b-form-layout-design.md`
@@ -114,10 +127,10 @@ Item CRUD commands (B9B14): `org add` (`OrgAddKind`: Login/SecureNote/Identit
**A5 doc-fix** (`enforce_owner_only_elevation` parent-role close, `519e503`) and this living-docs sweep also landed. **A5 doc-fix** (`enforce_owner_only_elevation` parent-role close, `519e503`) and this living-docs sweep also landed.
**Tracked follow-ups (deferred, not shipped):** **Tracked follow-ups:**
- `org add` / `org edit` parity for Card, SshKey, Document, Totp item types (Login/SecureNote/Identity only today; `get`/`list` can display all types if present) - `org add` / `org edit` parity for Card, Key, Document, Totp — ✅ **SHIPPED v0.8.1** (`4c0a289`; all 7 item types now supported)
- Extension org-vault switch + read parity (Dev-D deferred) - Extension org-vault switch + read parity (Dev-D) — still deferred; forward plan in the parity gap analysis
- Extension org write operations - Extension org write operations — still deferred (Plan B-2)
- Phase 2: SSO/LDAP federation, read audit log, per-collection subkeys (true cryptographic scope separation), HTTP management plane - Phase 2: SSO/LDAP federation, read audit log, per-collection subkeys (true cryptographic scope separation), HTTP management plane
**Known limitations (by design in phase 1):** shared org master key — reads are not cryptographically scoped per collection (hook scopes writes; client filters manifest); no read audit (git records writes only); `delete-org` is a local tombstone only (hook rejects protected-file deletion on push). **Known limitations (by design in phase 1):** shared org master key — reads are not cryptographically scoped per collection (hook scopes writes; client filters manifest); no read audit (git records writes only); `delete-org` is a local tombstone only (hook rejects protected-file deletion on push).
@@ -169,10 +182,10 @@ Per the 2026-05-30 post-v0.6.0 audit of the three 2026-05-04 architecture-review
**Enterprise org vault** — ✅ **COMPLETE (backend)** — all 19 CLI subcommands + core + server hook merged `7392795` 2026-06-20. Deferred follow-ups tracked in the landing section above. **Enterprise org vault** — ✅ **COMPLETE (backend)** — all 19 CLI subcommands + core + server hook merged `7392795` 2026-06-20. Deferred follow-ups tracked in the landing section above.
Pending org-vault follow-ups (in rough priority order): Pending follow-ups (in rough priority order; **forward plan:** `docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md`):
- `org add`/`edit` parity for Card, SshKey, Document, Totp - **Extension org parity — read** (Dev-D): org context switch + collection-filtered browse in the popup/vault tab
- Extension org switch + read parity (Dev-D) - **Extension org parity — write** (Plan B-2): `org add`/`edit`/`rm` from the extension — blocked behind extension org-read landing (and now unblocked on the CLI side, which reached all-7-type org write in v0.8.1)
- Extension org write operations - **Personal-side extension gaps** (from the parity analysis): favorites UI, group/tag editing on all type forms, popup type/tag filters, attachment-remove router wire + per-item purge UI, autofill registrable-domain matching
- **Phase 4: command palette** — ⌘K global search + action dispatch across the vault tab (no spec yet) - **Phase 4: command palette** — ⌘K global search + action dispatch across the vault tab (no spec yet)
Long-term: relay server, mobile. See `ROADMAP.md` for the longer arc and `CHANGELOG.md` for tagged-release history (current head: `v0.6.0`; the `v0.7.0` entry covers extension-restructure completion). Long-term: relay server, mobile. See `ROADMAP.md` for the longer arc and `CHANGELOG.md` for tagged-release history (the `v0.8.1` CHANGELOG entry + version bump are owned by the PM in this lift).

View File

@@ -24,7 +24,10 @@ under `src/commands/`. Each source file has one job.
- **`src/main.rs`** (`main.rs:1-492`) — clap surface and the flat dispatcher. - **`src/main.rs`** (`main.rs:1-492`) — clap surface and the flat dispatcher.
Owns the top-level `Cli` / `Commands` enum and every subcommand enum Owns the top-level `Cli` / `Commands` enum and every subcommand enum
(`AddKind`, `TrashAction`, `SettingsAction`, `BackupAction`, `ImportAction`, (`AddKind`, `TrashAction`, `SettingsAction`, `BackupAction`, `ImportAction`,
`DeviceAction`, `RecoveryQrCmd`). `main()` is a single `match` that `DeviceAction`, `RecoveryQrCmd`), plus the org clap surface `OrgCommands`
(`main.rs:448`) and `OrgAddKind` (`main.rs:556`) — the latter's Card / Key /
Document / Totp variants carry `--collection` and the `--*-stdin` secret flags.
`main()` is a single `match` that
delegates each variant to `commands::<verb>::cmd_<verb>(...)`. Also owns the delegates each variant to `commands::<verb>::cmd_<verb>(...)`. Also owns the
three test-only env-var hooks (`test_passphrase_override`, three test-only env-var hooks (`test_passphrase_override`,
`test_item_secret_override`, `test_backup_passphrase_override`) — each is `test_item_secret_override`, `test_backup_passphrase_override`) — each is
@@ -94,7 +97,14 @@ under `src/commands/`. Each source file has one job.
(`items/<collection-slug>/<id>.enc` — the leading slug is what the pre-receive (`items/<collection-slug>/<id>.enc` — the leading slug is what the pre-receive
hook authorizes against, never decrypting), fingerprint-based member matching hook authorizes against, never decrypting), fingerprint-based member matching
(`relicario_core::fingerprint`, tolerant of OpenSSH whitespace/comment (`relicario_core::fingerprint`, tolerant of OpenSSH whitespace/comment
differences), `atomic_write`, and `org_git_run`. Note `org_git_run` runs differences), `atomic_write`, and `org_git_run`. As of v0.8.1 it also owns
**collection-scoped attachment storage**`attachment_path` /
`save_attachment` / `load_attachment` / `remove_item_attachments`
(`org_session.rs:125-157`) at layout
`attachments/<collection-slug>/<item-id>/<att-id>.enc` (the same leading slug
the pre-receive hook authorizes against as for `item_path`), capped
per-attachment by `DEFAULT_ORG_ATTACHMENT_MAX_BYTES` (10 MiB,
`org_session.rs:20`). Note `org_git_run` runs
**bare git** — unlike `helpers::git_run` it does NOT inject **bare git** — unlike `helpers::git_run` it does NOT inject
`commit.gpgsign=false`, because org commits MUST be signed (the hook verifies `commit.gpgsign=false`, because org commits MUST be signed (the hook verifies
every commit's signature); signing config is established by every commit's signature); signing config is established by
@@ -111,19 +121,38 @@ under `src/commands/`. Each source file has one job.
concurrent-rotation abort), `transfer-ownership`, `delete-org`, `status` / concurrent-rotation abort), `transfer-ownership`, `delete-org`, `status` /
`audit` (verified-signer attribution + `TAMPERED` flag). `audit` (verified-signer attribution + `TAMPERED` flag).
*Item CRUD (7):* `org add` creates typed items via `OrgAddKind` *Item CRUD (7):* full item-type parity with the personal vault (v0.8.1).
(`commands/org.rs:749`) — **Login / SecureNote / Identity only**; Card / `org add` creates **all seven types** (Login / SecureNote / Identity / Card /
SshKey / Document / Totp creation is a deferred follow-up. `get` / `list` can Key / Document / Totp) via `OrgAddKind` (`commands/org.rs:751`); each arm
display any item type if present. `org get <query> [--show]` masks secrets delegates to the shared `item_build::build_*` builders through `build_org_item`
unless `--show`; `org list [--trashed]` filters by the caller's collection (`commands/org.rs:799`), and `run_add` (`commands/org.rs:823`) sets tags
grants; `org edit <query>` is flag-driven (blank flags keep current values); post-build. Document is special-cased in `run_add` (`commands/org.rs:839`): its
`org rm` soft-deletes, `org restore` undoes, `org purge` permanently removes builder also yields an `EncryptedAttachment` that is written via
the encrypted blob. All item ops are collection-scoped and grant-enforced. The `save_attachment` and git-staged before the signed commit. Single-line secrets
audit trail emits `item-create` / `item-update` / `item-delete` / (card number/CVV/PIN, TOTP secret, login password) accept a `--*-stdin` flag;
`item-restore` / `item-purge`. multiline secrets (Key material, SecureNote body) read stdin to EOF — the same
`resolve_secret_line` / `resolve_secret_multiline` convention as personal `add`
(`commands/item_build.rs`).
Deferred: Card / SshKey / Document / Totp `org add` / `edit` parity; `org edit <query>` (`run_edit`, `commands/org.rs:1004`) is **interactive
extension org reads and writes (Dev-D). per-type** as of v0.8.1 (it was flag-driven before): it prompts Title, then
dispatches on `&mut item.core` to the shared `item_build::edit_*` helpers
("blank keeps current", field-history capture via `push_history`), mirroring
personal `cmd_edit`. `--totp-qr` sets a Login TOTP from a QR image; `--file`
replaces a Document's primary attachment (`commands/org.rs:1039`, rejected for
non-Document items at `commands/org.rs:1018`). The edit commit carries
`Relicario-Action: item-update`.
`org get <query> [--show]` masks every secret unless `--show`; `org list
[--trashed]` filters by the caller's collection grants; `org rm` soft-deletes,
`org restore` undoes, `org purge` (`run_purge`, `commands/org.rs:1164`)
permanently removes the encrypted blob **and** the item's attachment directory
(`remove_item_attachments`, `commands/org.rs:1173`). All item ops are
collection-scoped and grant-enforced (`filter_for_member` over the manifest +
`ensure_grant` before any load/mutate). The audit trail emits `item-create` /
`item-update` / `item-delete` / `item-restore` / `item-purge`.
Deferred: extension org reads and writes (Plan B-2 / phase 2).
- **`src/helpers.rs`** (`helpers.rs:1-101`) — pure, no-state plumbing: - **`src/helpers.rs`** (`helpers.rs:1-101`) — pure, no-state plumbing:
`find_vault_dir_from` (`helpers.rs:14-28`) walks up parent directories `find_vault_dir_from` (`helpers.rs:14-28`) walks up parent directories

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "relicario-cli" name = "relicario-cli"
version = "0.8.0" version = "0.8.1"
edition = "2021" edition = "2021"
description = "CLI for relicario password manager" description = "CLI for relicario password manager"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View File

@@ -3,7 +3,7 @@
//! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting. //! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting.
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use zeroize::Zeroizing; use zeroize::Zeroizing;
@@ -255,23 +255,39 @@ pub(crate) fn build_totp(
}))) })))
} }
/// Read a file and encrypt it as an attachment under `key`, deriving its display
/// metadata. The plaintext is held in a `Zeroizing` buffer so it is wiped after
/// encryption. Returns the encrypted blob plus (filename, mime_type, size).
pub(crate) fn encrypt_document_file(
path: &Path,
key: &Zeroizing<[u8; 32]>,
max_bytes: u64,
) -> Result<(EncryptedAttachment, String, String, u64)> {
use relicario_core::encrypt_attachment;
let bytes = Zeroizing::new(
std::fs::read(path).with_context(|| format!("failed to read {}", path.display()))?,
);
let enc = encrypt_attachment(&bytes, key, max_bytes)?;
let filename = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", path.display()))?
.to_string_lossy()
.into_owned();
let mime_type = crate::parse::guess_mime(&filename);
Ok((enc, filename, mime_type, bytes.len() as u64))
}
pub(crate) fn build_document( pub(crate) fn build_document(
title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64, title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64,
) -> Result<(Item, EncryptedAttachment)> { ) -> Result<(Item, EncryptedAttachment)> {
use relicario_core::item_types::DocumentCore; use relicario_core::item_types::DocumentCore;
use relicario_core::{encrypt_attachment, AttachmentRef}; use relicario_core::AttachmentRef;
let bytes = std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?; let (enc, filename, mime_type, size) = encrypt_document_file(&file, key, max_bytes)?;
let enc = encrypt_attachment(&bytes, key, max_bytes)?;
let filename = file.file_name()
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", file.display()))?
.to_string_lossy().into_owned();
let mime_type = crate::parse::guess_mime(&filename);
let primary_attachment = enc.id.clone();
let mut item = Item::new(title, ItemCore::Document(DocumentCore { let mut item = Item::new(title, ItemCore::Document(DocumentCore {
filename: filename.clone(), mime_type: mime_type.clone(), primary_attachment: primary_attachment.clone(), filename: filename.clone(), mime_type: mime_type.clone(), primary_attachment: enc.id.clone(),
})); }));
item.attachments.push(AttachmentRef { item.attachments.push(AttachmentRef {
id: primary_attachment, filename, mime_type, size: bytes.len() as u64, created: item.created, id: enc.id.clone(), filename, mime_type, size, created: item.created,
}); });
Ok((item, enc)) Ok((item, enc))
} }

View File

@@ -1038,25 +1038,18 @@ pub fn run_edit(dir: &Path, query: &str, totp_qr: Option<std::path::PathBuf>, fi
ItemCore::Key(k) => ib::edit_key(k, history)?, ItemCore::Key(k) => ib::edit_key(k, history)?,
ItemCore::Document(d) => { ItemCore::Document(d) => {
if let Some(path) = &file { if let Some(path) = &file {
let bytes = std::fs::read(path) let (enc, filename, mime_type, size) = ib::encrypt_document_file(
.with_context(|| format!("read {}", path.display()))?; path, vault.key(), crate::org_session::DEFAULT_ORG_ATTACHMENT_MAX_BYTES)?;
let enc = relicario_core::encrypt_attachment(
&bytes, vault.key(), crate::org_session::DEFAULT_ORG_ATTACHMENT_MAX_BYTES)?;
vault.remove_item_attachments(&collection, &id)?; vault.remove_item_attachments(&collection, &id)?;
let rel = vault.save_attachment(&collection, &id, &enc)?; let rel = vault.save_attachment(&collection, &id, &enc)?;
let filename = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("file path has no filename: {}", path.display()))?
.to_string_lossy()
.into_owned();
d.mime_type = crate::parse::guess_mime(&filename);
d.primary_attachment = enc.id.clone();
d.filename = filename.clone(); d.filename = filename.clone();
d.mime_type = mime_type.clone();
d.primary_attachment = enc.id.clone();
new_doc_attachments = Some(vec![relicario_core::AttachmentRef { new_doc_attachments = Some(vec![relicario_core::AttachmentRef {
id: enc.id, id: enc.id,
filename, filename,
mime_type: d.mime_type.clone(), mime_type,
size: bytes.len() as u64, size,
created: now_unix(), created: now_unix(),
}]); }]);
doc_attachment_rel = Some(rel); doc_attachment_rel = Some(rel);

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "relicario-core" name = "relicario-core"
version = "0.8.0" version = "0.8.1"
edition = "2021" edition = "2021"
description = "Core library for relicario password manager" description = "Core library for relicario password manager"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "relicario-wasm" name = "relicario-wasm"
version = "0.8.0" version = "0.8.1"
edition = "2021" edition = "2021"
description = "WASM bindings for relicario password manager" description = "WASM bindings for relicario password manager"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"

View File

@@ -0,0 +1,128 @@
# Product Audit — Relicario — 2026-06-20 · fast
> Generated by the `product-expert` skill (roadmap audit, fast mode). Competitive
> read grounded in `references/competitive-landscape.md` (last-reviewed 2026-06-20).
> Advisory only — record of what was considered, not a commitment.
## Reality check
v0.8.1 tagged today: `relicario org add`/`edit` now covers **all 7 item types**
with collection-scoped, grant-enforced attachments — sitting on the
cryptographically serious v0.8.0 org backend (ECIES per-member key wrap,
signature-verifying pre-receive hook). The personal vault is genuinely complete
with full CLI↔extension parity. But the **defining reality is an asymmetry**:
Relicario has now built an entire enterprise org vault that *cannot be touched
from a browser* — the extension has zero org concept. The biggest recent
investment has no GUI surface. No lift is currently active.
**Drift found** (low severity, but catching it is this skill's job):
- `STATUS.md:7` — "Last release tagged: **v0.6.0**". Stale: v0.8.0 and v0.8.1 are
both tagged (`git tag`; release commit `2fa4d68`).
- `STATUS.md:8` + `ROADMAP.md:10` — "tag pending PM". Stale: the v0.8.1 tag is cut.
- `docs/user_docs/` (12-page end-user guide) merged as a fast-follow *after* the
tag — fine, just not inside the v0.8.1 tag.
## Assessment
**Strengths:** the wedge sits in a near-empty competitive cell — two factors
*into the KDF* + self-host + **zero server metadata** + git audit log (1Password
has the 2-factor KDF but is cloud-only; vaultwarden self-hosts but is
single-factor KDF). Personal vault is complete. Org backend is real cryptographic
work, now feature-broad.
**Gaps:** (1) the org vault is **invisible in the GUI** — extension has no org
read or write; the whole enterprise feature is stranded behind the CLI (rated
*critical*; traces to `docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md`).
(2) Personal-side parity holes that make a "parity-is-a-design-value" product feel
unfinished — favorites (no UI on either surface), group/tag editing only on some
forms, and autofill matching by **exact hostname** (so `www.github.com` misses a
login saved as `github.com`). (3) The pitch leads with steganography — the most
friction-heavy, least load-bearing part of the wedge.
**Risks:** mobile absence caps total addressable market — but for Relicario's
*self-selected* desktop/CLI audience that's a ceiling, not a bleeding wound, and
treating it as an emergency would import mass-market logic that doesn't fit this
product. The sharper risk is that a GUI-less org vault only ever reaches
CLI-native shops — a fraction of the market the org spec implies — stranding the
investment.
## Recommendations (leverage-ordered)
1. **REORDER — Put a GUI on the org vault you already built: extension org *read*
next, then *write*.** *Why:* the v0.8.0+v0.8.1 backend is stranded without it;
"unlock value already built" is the highest-ROI class of move; it's already
roadmap item #1, and CLI reached all-7-type org write in v0.8.1 so the write
path is unblocked. Outranks the command palette and personal-parity polish.
*Impact/Effort:* H / M. *Risk:* browser GitHost has no commit-signing path, so
write is harder than read — ship read first as its own slice. *Refinement:*
scope to org **item usage** (read/add/edit a shared credential), NOT admin ops
(member/key management staying CLI-only is a legitimate design choice; item
usage being CLI-only is not).
2. **PIVOT (positioning) — Re-lead with the thesis, demote stego to an *option*.**
*Why:* the most important thing the roadmap doesn't mention. A plain key file
delivers the identical 256-bit second factor; stego's only marginal benefit is
the niche "dead-drop on social media" story, while it carries the most unlock
friction and a SPOF the project already had to paper over with the recovery-QR.
The README leads with the gimmick and buries the moat. *Impact/Effort:* H / L
(messaging; keep the feature). *Risk:* stego is the product's identity — keep
it first-class-*optional*, don't delete it. *Adjacent thesis-level call:*
offering a plain key file as an alternative second factor would lower
onboarding friction for users who find "hide a secret in a JPEG" too weird — a
real ADD candidate, not just messaging.
3. **ADD (cheap, high-ROI) — Autofill matches by registrable domain (eTLD+1), not
exact hostname.** *Why:* exact-equality silently fails on the most common case
(`www.` vs apex), making the extension feel broken; small, contained fix.
*Impact/Effort:* M / L. *Risk:* use a public-suffix list to avoid over-matching.
4. **ADD — Close the personal parity holes: favorites UI + group/tag editing on
every item-type form.** *Why:* CLI↔extension parity is a stated design value;
family/individual users organize by exactly these. *Impact/Effort:* M / M.
5. **REORDER (defer) — Keep org phase-2 (SSO/LDAP, read audit, per-collection
subkeys, HTTP plane) parked behind extension org parity.** *Why:* high-effort,
no demand, pointless while the org feature has no GUI. *Impact/Effort:* M / H.
6. **CUT (future investment, not deletion) — Stop *deepening* the over-served
areas:** no more stego-robustness work, no recovery-QR elaboration, leave
field-history's knobs alone. Don't remove working features — just stop
investing in them.
7. **Housekeeping — sync `STATUS.md` and `ROADMAP.md:10`** to reflect v0.8.1 as
tagged. Five minutes; it's the exact drift this audit exists to catch.
**On mobile & v1.0:** mobile is the single biggest TAM ceiling, but a high-effort,
post-v1.0 bet that partly contradicts the desktop/CLI shape of the product — a
separate-product-scale investment, not the next move. Frame **v1.0 = the thesis,
fully usable on the surfaces you already support**: extension org parity +
personal parity holes closed + positioning sharpened. Mobile is a v1.x conversation.
## PM brief
```markdown
## PRODUCT DIRECTIVE TO PM
Time: 2026-06-20 (local)
Source: /product-expert roadmap audit (fast)
Reality note: v0.8.1 is TAGGED (org item-type parity). The org vault backend is
fully shipped but has ZERO extension GUI — the whole enterprise feature is
CLI-only. STATUS.md still says "Last release tagged: v0.6.0" and "tag pending PM";
sync those (5-min housekeeping) before anything else.
Roadmap changes (priority order):
1. REORDER — extension org READ (org switch + collection-filtered browse) is the
next slice; org WRITE follows as its own slice. Scope to item usage, not admin
ops. This outranks the command palette and personal-parity polish.
2. PIVOT (positioning) — re-lead messaging with "two secrets into the KDF +
self-host + zero server metadata + git audit"; present the stego image as an
optional second-factor flavor, not the headline. Keep the feature.
3. ADD — autofill: match by registrable domain (eTLD+1), not exact hostname.
4. ADD — favorites UI + group/tag editing across all item-type forms (parity).
Recommended next slice: extension org READ (H impact / M effort — puts a usable
face on the backend you already paid for).
Out of scope / do NOT pick up: org phase-2 (SSO/LDAP, read audit, per-collection
subkeys) until org has a GUI; further stego/recovery-QR hardening; mobile (post-v1.0).
```

View File

@@ -0,0 +1,169 @@
# Extension ↔ CLI Parity Gap Analysis
- **Date:** 2026-06-20
- **Author:** Dev-D, reconciled against an independent PM parity sweep
- **Status:** Draft for review — **forward-planning**, NOT v0.8.1 scope
- **Anchor commit:** `origin/main` `b09e0ce` (v0.8.0 org vault + v0.8.1 Dev-A foundation merged; Dev-B/C/D in flight)
- **Scope note:** This plans a *future* milestone. Extension org **writes** remain explicitly out of scope for v0.8.1 per `docs/superpowers/specs/2026-06-20-relicario-v0.8.1-parity.md` (Plan B-2).
## Purpose
Survey the gap between the Relicario **CLI** (`relicario`) and the **browser extension**, classify every gap as a *real parity gap*, an *intended CLI-only* capability, or *already-planned-in-a-spec*, and produce a prioritized work list (with rough sizing) to bring the extension up to CLI parity. The driver is the project's **CLI/extension parity philosophy**: features should not ship "CLI-first, extension-later" without an explicit, recorded decision — this doc is that record for the current backlog.
## Method
Two **independent** surveys were run and then reconciled:
- **PM sweep** — 3 inventory agents + synthesis.
- **Dev-D sweep** — 4 parallel readers (CLI / extension-UI / extension-SW / specs+roadmap) → synthesis → an adversarial completeness/accuracy critic, all reading from a worktree pinned at `b09e0ce`.
The two sweeps were deliberately blind to each other. All load-bearing claims in this document were **hand-verified against source** (greps + line reads); where the two sweeps disagree, the disagreement is flagged explicitly in §Reconciliation. Line citations are point-in-time against `b09e0ce` and may drift.
## Executive summary
Core item-CRUD parity is **excellent**. All 7 item types (Login, Secure Note, Identity, Card, Key, TOTP, Document) and the add / edit / view / list / trash / restore lifecycle are at full parity, and in several places the **extension is the richer surface** (live TOTP codes, custom fields/sections, TOTP-from-QR, password coloring, session auto-lock, autofill/capture). Where a *per-type* gap exists it is most often on the **CLI** side, not the extension's.
The genuine **extension-side** gaps cluster into three buckets:
1. **Metadata-management gaps (the headline finding):** editing **groups**, **tags**, and **filtering** is wired into only specific forms/surfaces in the extension, while the CLI offers them uniformly across all types; **favorites** has *zero* extension UI (strictly worse than the CLI). These are real, currently-shipping parity gaps on the *personal* vault.
2. **Backend-exists-but-no-wire/UI:** attachment **removal** (`removeAttachmentsFromItem` helper exists, no `remove_attachment` router message), **per-item purge** (`purge_item` handler exists, only a bulk "empty trash" UI), and the `isInTab()` popup-mode gate that hides login/secure-note attachment editing in the popup window.
3. **The org (enterprise) vault** — the single largest gap. The entire org feature (shipped CLI-only in v0.8.0) has **no extension presence** (no org routes, no org context). This is fully specced and explicitly deferred (Dev-D org-read / Plan B-2 org-write).
Plus one quality gap on the personal side surfaced by the PM sweep: **autofill hostname matching** is a naive exact-equality match.
Intentionally **CLI-only by design** (not gaps): real `git pull`/`push` and `.git`-history backup bundling (the extension writes straight to the host Contents API and keeps no local repo), the `imgsecret embed` recovery subcommand, recovery-QR's deliberate no-file-write contract, org **admin** (members/collections/grants/rotate/audit), and shell completions.
## Reconciliation with the PM sweep
The PM sweep concluded: extension at *near-full parity* on the personal surface, ahead in places, with the **org vault as the one material gap** and **autofill hostname matching as the only personal-side quality gap**.
**Agreements (both sweeps, independently):**
- Org vault is the largest gap; it is fully specced and deferred (Dev-D read / Plan B-2 write).
- The extension leads on live TOTP, custom fields/sections, password coloring.
- The intended-CLI-only set: git sync/push, `.git` backup bundling, device-key deploy-key plumbing, org admin, shell completions.
**Dev-D refines / partially refutes the PM:** the personal surface is **not** "near-full parity with autofill as the only gap." There is a real cluster of **personal-side extension gaps** the PM sweep understated:
- **Favorites — none in the extension** (`favorite` only round-trips through save fns; no toggle, no star in lists, no filter). The CLI is itself only add-only, so the extension is *strictly worse*. The PM hypothesis did not list this.
- **Group editing — Login-form only** (`f-group` + `wireGroupAutocomplete` live in `login.ts` only; card/key/identity/totp/document forms pass `group` through without an input).
- **Tag editing — Document-form only** (`f-tags` in `document.ts` only; other forms preserve-but-don't-edit).
- **Filter — popup has no type filter** (vault-tab only) and **no tag filter** anywhere.
- **Per-item purge** and **attachment add/remove** have working backends but no popup-reachable UI / no router wire.
**PM caught, Dev-D's taxonomy missed:** **autofill hostname matching.** `service-worker/vault.ts` (`findByHostname`, equality at `:344`) matches credentials by exact `icon_hint` equality (`(e.icon_hint ?? '').toLowerCase() === hostname`) — no `www.` strip, no registrable-domain (eTLD+1) match, so `www.example.com` will not match an item stored as `example.com`. Confirmed; folded in as a real LOW-MED personal-side gap. (Dev-D's capability taxonomy centered on item-CRUD/features and under-weighted the content-script autofill path — the PM sweep is the reason it appears here.)
**Methodology correction (a Dev-D self-sweep error, struck here):** the Dev-D extension-SW inventory referred to a `messages.ts` "that does not exist at that path." **That is false** — the file exists at `extension/src/shared/messages.ts` (227 lines): it holds the `PopupMessage` union (with `delete_item // soft-delete` at line 23), `POPUP_ONLY_TYPES` (line 168), and `CONTENT_CALLABLE_TYPES` (line 224). The inventory had merely dropped the `shared/` directory prefix. The substantive findings it supported (the unwired `searchItems`/`removeAttachmentsFromItem` helpers) are independently verified correct; the "file doesn't exist" caveat is removed from this document.
## Parity matrix
Support: **full** / **partial** / **none** / **n/a**. `gap_class`: **at-parity** · **real-gap** (extension work) · **real-gap (CLI-side)** (extension already ahead; CLI backlog) · **cli-only-by-design** · **already-planned**.
### Item types
| Capability | CLI | Ext | gap_class | Notes (evidence @ `b09e0ce`) |
|---|---|---|---|---|
| Login: create/view/edit | full | full | at-parity | CLI `add/get/edit login`; ext form + `add_item`/`update_item`/`get_item`. |
| Secure Note: create/view/edit | full | full | at-parity | Both complete. |
| Identity: create | full | full | at-parity | Both; ext also exposes `address`. |
| Identity: view | full | full | at-parity | Both. |
| Identity: edit | partial | full | real-gap (CLI-side) | CLI `edit_identity` omits `date_of_birth` + records no history; ext edits all. CLI backlog. |
| Card: create/view | full | full | at-parity | Both. |
| Card: edit | partial | full | real-gap (CLI-side) | CLI `edit_card` = holder+number only (no CVV/PIN/expiry/kind); ext edits all. CLI backlog. |
| Key: create/view | full | full | at-parity | Both; ext takes `public_key` interactively. |
| Key: edit | partial | full | real-gap (CLI-side) | CLI `edit_key` = key-material only (no label/algorithm/public_key); ext edits all. CLI backlog. |
| TOTP: create | full | full | at-parity | Both; ext adds Steam Guard kind. |
| TOTP: view | partial | full | real-gap (CLI-side) | CLI shows metadata only; ext shows live rotating code. See "TOTP live code". |
| TOTP: edit | full | full | at-parity | Both. |
| Document: create | full | full | at-parity | CLI encrypts file as attachment; ext `upload_attachment`. |
| Document: view | partial | full | real-gap (CLI-side) | CLI metadata + `extract`; ext inline image preview. CLI backlog. |
| Document: edit | none | full | real-gap (CLI-side) | CLI `edit` on Document is a no-op redirect to attach/extract; ext changes primary/supplementary files. CLI backlog. |
### Operations
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| add / edit / get / list | full | full | at-parity | All 7 types both surfaces. |
| rm / soft-delete | full | full | at-parity | CLI `rm`; ext `delete_item` (`messages.ts:23`, handler `popup-only.ts`). |
| trash (list) | full | full | at-parity | CLI `trash`; ext trash view. |
| restore from trash | full | full | at-parity | CLI `restore`; ext `restore_item`. |
| purge (permanent) | full | partial | **real-gap** | Ext UI only bulk "empty trash" (`purge_all_trash`, `popup-only.ts:420`); **no per-item purge UI**, though `purge_item` handler exists (`popup-only.ts:409`). CLI has single + bulk. |
### Features
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| Attachments: add | full | partial | **real-gap** | Login/secure-note attachment editing gated behind `isInTab()` (`login.ts:370,388`; `secure-note.ts:123,140`) — unavailable in popup window; document renders unconditionally (`document.ts`). SW `upload_attachment` is full. |
| Attachments: view/download | full | full | at-parity | CLI `extract`; ext download + `download_attachment`. |
| Attachments: remove | full | partial | **real-gap** | SW helper `removeAttachmentsFromItem` (`vault.ts:492`) has **no router wire** (`remove_attachment` absent — confirmed). UI removes refs at form-save only, with the same `isInTab()` caveat. CLI `detach` is full. |
| TOTP: live code | none | full | real-gap (CLI-side) | CLI reveals raw base32 only; ext computes live codes. Extension leads. No spec mandates CLI OTP. |
| Generator: password / passphrase | full | full | at-parity | CLI `generate`; ext generator-panel + `generate_password`. |
| Settings: view / edit | full | full | at-parity | CLI `settings`; ext `get/set_vault_settings`. |
| Search | partial | partial | at-parity | CLI: title-substring. Ext: client-side over title/group/tags/icon_hint; SW `searchItems` (`vault.ts:316`) exists but **unwired** (no `search_items` message). Neither does field-value full-text. |
| Filter | full | partial | **real-gap** | CLI `list` filters type/group/tag/trashed. Ext: type filter is **vault-tab-only**; popup has none; **no tag filter anywhere**. SW `list_items` filters by `group` only. |
| Favorites | partial | **none** | **real-gap** | CLI add-only (`--favorite` on Login add, `*` in list; no toggle/filter). Ext: **zero UI**`favorite` only round-trips. Ext strictly worse; needs a paired CLI+ext design. |
| Tags | full | partial | **real-gap** | CLI full create+filter all types. Ext: only Document form edits tags (`f-tags` in `document.ts`); no tag chips in lists; no tag filter. SW round-trips tags. |
| Groups/folders | full | partial | **real-gap** | CLI all types `--group`, `list --group`. Ext: only Login form has `f-group`+autocomplete; other forms set no group; vault-tab "group" filter is actually a type filter. SW `list_groups`/group-filter full. |
| Field history (view) | full | full | at-parity | CLI `history`; ext `get_field_history`. |
| Custom fields / sections | none | full | real-gap (CLI-side) | CLI has no custom-field/section commands (core supports them); ext `renderSectionsEditor` covers all 7 types. CLI backlog. |
| Autofill hostname matching | n/a | partial | **real-gap** | Ext-only feature; matcher (`vault.ts` `findByHostname`, `:344`) is exact `icon_hint` equality — no `www.` strip / eTLD+1. `www.x.com``x.com`. (PM-surfaced.) |
### Org (enterprise) vault
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| Org: read items | full | none | already-planned | CLI `org get/list` grant-filtered, all 7 types. Ext has zero org code. Planned Dev-D. Spec: `2026-06-06-relicario-enterprise-org-vault-design.md` § Extension — Org Context; ROADMAP "Extension org parity — read". |
| Org: write (add/edit/rm) | partial | none | already-planned | CLI write = Login/SecureNote/Identity only (`OrgAddKind`, `main.rs:560`; Card/Key/Document/Totp absent — v0.8.1 lift in flight). Ext none. Planned Plan B-2. Spec: `2026-06-20-relicario-v0.8.1-parity.md` § Out of scope. |
| Org: member/collection mgmt | full | none | cli-only-by-design | CLI full lifecycle (~19 subcommands). Ext none — org **admin** is intended CLI-only (high-trust, low-frequency). |
### Vault lifecycle / infra
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| Vault init / setup | full | full | at-parity | CLI `init`; ext setup wizard + `create_vault`/`attach_vault`. |
| Git sync (pull/push) | full | partial | cli-only-by-design | CLI real `git pull --rebase`/`push`. Ext writes straight to host Contents API; no local graph (`ahead`/`behind` always 0). Functionally syncs; architecturally different by design (`extension/ARCHITECTURE.md`). |
| Device management | full | full | at-parity | CLI `device`; ext `renderDevices` + SW device CRUD. (GitHub/GitLab deploy-key API is the deferred edge.) |
| Backup / restore | full | full | at-parity | CLI `.relbak` + git-history bundling; ext `export/restore_backup`. `.git` bundling sub-aspect is cli-only-by-design (ext has no local repo). |
| Import (LastPass) | partial | partial | at-parity | Both LastPass-CSV only; other importers deferred both surfaces by policy. |
| Recovery QR | full | full | at-parity | CLI generate/unwrap; ext `generate/unwrap_recovery_qr`. Webcam scan deferred both. |
| Standalone generate (no vault) | full | none | cli-only-by-design (low-confidence) | CLI `generate`/`rate` work outside a vault; ext generator is embedded in login form + settings (needs unlocked vault). A browser extension lacks the "no vault" generator use-case a shell has. No spec; flag if user demand appears. |
### Intended CLI-only (no taxonomy row; recorded so they are not re-litigated as gaps)
| Capability | gap_class | Notes / spec |
|---|---|---|
| Recovery-QR file-write (`--out`) | cli-only-by-design | Negative API contract — no surface writes the payload to disk; absence *is* the security property. `2026-05-01-recovery-qr-design.md`. |
| Org delete-org push to remote | cli-only-by-design | Phase-1 delete-org is a local tombstone; pre-receive hook rejects protected-file deletion. Pushable delete-org is phase-2. `2026-06-06-...-design.md`. |
| `imgsecret embed` subcommand | cli-only-by-design | CLI disaster-recovery tool; the extension setup wizard's image flow covers the equivalent. |
| Password coloring (CLI TTY) | cli-only-by-design (inverted) | Ext shipped it (v0.5.1); CLI TTY parity deferred until demand. `2026-05-01-password-coloring-design.md` § Out of scope. |
| Shell completions | cli-only-by-design | No extension analogue. |
## Gap classification summary
- **Real extension gaps (extension work closes them):** per-item purge UI; attachment add/remove UI + `remove_attachment` wire + `isInTab()` gate; popup type filter + tag filter; tag editing on all forms; group editing on all forms; favorites UI; autofill registrable-domain matching; **org read** (specced); **org write** (specced, behind CLI type parity).
- **CLI-side gaps (extension already ahead — separate CLI backlog, NOT extension work):** Identity/Card/Key edit field coverage; Document view/edit; live TOTP code; custom fields/sections commands.
- **Intended CLI-only (not gaps):** git pull/push, `.git` backup bundling, org admin, `imgsecret embed`, recovery-QR file-write, shell completions, standalone generate.
- **Already planned / deferred:** org read (Dev-D), org write + org item-type breadth (Plan B-2), org attachments/multi-vault (behind org).
## Prioritized forward work (extension)
Only items where **extension work** closes the gap. CLI-side gaps and intended-CLI-only items are excluded.
| Pri | Item | Size | Why | Depends on |
|---|---|---|---|---|
| **P0** | **Attachment remove + un-gate popup:** wire a `remove_attachment` router message to `removeAttachmentsFromItem` (`vault.ts:492`); drop the `isInTab()` gate so login/secure-note attachment **add & remove** work in the popup. Closes *both* the add half (row "Attachments: add") and remove half. | M | Backend exists and is unreachable — highest value-per-effort. The popup-mode gate is a UX cliff (can't manage login attachments without popping out). | — |
| **P0** | **Per-item purge UI:** surface the existing `purge_item` handler (`popup-only.ts:409`) as a per-row permanent-delete in the trash view (today only bulk `purge_all_trash`). | S | Pure UI wiring over an existing handler; CLI has single-item purge. | — |
| **P1** | **Group editing on all type forms:** add `f-group` + `wireGroupAutocomplete` to card/key/identity/totp/document (Login-only today). | M | SW (`list_groups`, group filter) already full; replicate one existing form pattern across 5 forms. | — |
| **P1** | **Tag editing on all type forms + tag chips in lists:** promote Document's `f-tags` to a shared affordance on all 7 forms. | M | SW round-trips tags fully; only Document edits them today. | — |
| **P1** | **Filter parity:** add a type filter to the popup (vault-tab has it) and a tag filter to popup + vault tab; optionally push type/tag params into `list_items`. | M | CLI filters type/group/tag; ext type filter is fullscreen-only, tag filter absent. | Tag editing (so tags exist to filter on) |
| **P2** | **Favorites (paired CLI + ext):** favorite toggle in detail/edit, favorites filter, star in list rows — and extend the CLI beyond add-only, to reach *true* parity per the parity philosophy. | M | Ext strictly worse than CLI (none vs partial); both surfaces weak. Write a short spec first. | — (spec TBD) |
| **P2** | **Autofill registrable-domain matching:** replace exact `icon_hint` equality (`vault.ts` `findByHostname:344`) with `www.`-strip + eTLD+1 matching. | SM | `www.x.com``x.com` today; the one personal-side quality gap. | — |
| **P2** | **Search wire-up (hardening):** expose `searchItems` (`vault.ts:316`) via a `search_items` message, or formally adopt client-side filtering and remove the dangling helper. | S | Functionally at-parity, but an unwired helper is dead-code drift. | — |
| **P3** | **Org read in extension (Dev-D):** org context switcher + SW org handlers (unwrap org master key into a `Zeroizing` session handle) + grant-filtered manifest browse/read in popup + vault tab. | XL | Largest single gap; entire org feature is CLI-only in the extension. Specced, deferred. | — |
| **P3** | **Org offline read-only indicator:** "org offline — writes disabled" banner when the git remote is unreachable in org context. | S | Spec-mandated UX. | Org read |
| **P3** | **Org SW acceptance tests:** org context replaces personal manifest cleanly; org master key never in localStorage/IndexedDB; offline mode triggers on network error. | M | Spec-mandated coverage following the feature. | Org read |
| **P3** | **Org write in extension (Plan B-2):** org add/edit/rm including Card/Key/Document/Totp. | XL | Closes org write + item breadth. Deferred past v0.8.1. | Org read **and** CLI reaching Card/Key/Document/Totp org-write parity (v0.8.1) |
## Caveats
- Line citations are point-in-time against `b09e0ce` and drift with edits.
- This is a planning artifact, not a commitment; sizes are rough (S ≈ hours, M ≈ a day, L ≈ days, XL ≈ a multi-stream lift).
- Two analytical errors caught during cross-check and corrected here: (1) the struck `messages.ts`-doesn't-exist claim (file exists at `extension/src/shared/messages.ts`); (2) a few inventory line numbers were off by single digits and have been replaced with hand-verified ones.

View File

@@ -0,0 +1,104 @@
# Extension Org Vault GUI — Design Spec
- **Date:** 2026-06-20
- **Status:** Approved (brainstorming) — ready for writing-plans
- **Release target:** v0.9.0 (one multi-agent train, alongside the Pluggable Second Factor spec)
- **Anchor:** `main` post-v0.8.1 (`2fa4d68` tag; HEAD `59ebc28`)
- **Driver:** Product audit `docs/superpowers/reviews/2026-06-20-product-audit.md` recommendation #1 — the org vault backend (v0.8.0 + v0.8.1) is fully shipped but has **zero extension presence**; the enterprise feature is stranded behind the CLI.
- **Builds on:** `2026-06-06-relicario-enterprise-org-vault-design.md` (§ Extension — Org Context), `2026-06-20-extension-cli-parity-gap-analysis.md` (P3 cluster), `extension/ARCHITECTURE.md`.
## Purpose & scope
Bring the org (enterprise) vault to the browser extension at **read + write** parity, so org members can browse, view, add, edit, and delete shared credentials from the popup and vault tab — not only the CLI. Org **admin** operations (member/collection/grant/rotate/audit) stay CLI-only by design (high-trust, low-frequency; org spec § Extension scopes them out).
**In scope:** org context switching; grant-filtered browse/read of org items (all 7 types) in popup + vault tab; org item write (add/edit/rm, all 7 types); offline read-only indicator; SW acceptance tests.
**Out of scope:** org admin in the extension; per-collection cryptographic isolation, SSO/LDAP, read audit, HTTP plane (all org phase-2); webcam recovery scan.
## The org-write signing gate (highest risk — read first)
The org pre-receive hook **rejects unsigned commits unconditionally**: it shells `git verify-commit` and requires a device ed25519/SSH signature from a current member (`crates/relicario-server/src/main.rs:95-102`). The extension today pushes **unsigned** commits via the host Contents API (`extension/src/service-worker/gitea.ts``/api/v1/.../contents/{path}`), authored under the API token. The ed25519 signing primitive **exists in WASM** (`sign_for_git`, `crates/relicario-wasm/src/lib.rs:253`) but is **unused** by the extension — there is no signed-commit push path, and the Contents API cannot carry a caller-supplied signature.
Therefore org **write** from the extension must construct and push a **signed commit via the Git Data API** (blob → tree → commit-with-signature → update-ref). This is feasible but unproven against the host APIs.
**Stream A3 begins with a spike, gating the rest of A3:**
> Prove that a commit signed in the SW with `sign_for_git`, pushed via the **Gitea** Git Data API, passes server-side `git verify-commit` **and** `relicario-server verify-org-commit`. Then repeat for **GitHub**.
- **Spike passes** → A3 proceeds (signed-push GitHost path + org write UI); v0.9.0 ships read + write.
- **Spike fails** (host API strips/normalizes the SSH signature such that `git verify-commit` fails) → org write degrades to a **follow-up lift**; v0.9.0 still ships **org read (A0A2, A4-read)** + the full Pluggable Second Factor spec. The spike is ~1 day and read + the other spec are unblocked regardless, so a failed spike wastes nothing.
The spike result is recorded back into this spec and `STATUS.md` before A3 build work starts.
## Architecture
An org vault is a second git repo alongside the personal vault, cryptographically isolated (org spec § Architecture). The org master key is a random 256-bit key, wrapped per member via ECIES (X25519 + XChaCha20-Poly1305) to their device ed25519 key. The extension mirrors the CLI: unwrap the org key with the device private key, decrypt items exactly as the personal vault does — but with the org key, not the Argon2id-derived personal key.
The extension keeps its existing architecture (SW is the crypto fortress; popup/vault are StateHost-driven view shells over `chrome.runtime.sendMessage`). Org support is an additive context, not a rewrite.
## Stream decomposition
### A0 · WASM org bridge (prerequisite)
`relicario-core::org` already performs ECIES unwrap and org item crypto for the CLI; none of it is exposed over `relicario-wasm` today (confirmed — no org exports in `crates/relicario-wasm/src/lib.rs`). Expose:
- `org_unwrap_key(keys_blob: &[u8], device_private_openssh: &str) -> OrgHandle` — unwrap `keys/<member-id>.enc` into the org master key held in WASM `Zeroizing`, returning an opaque slot handle in the same pattern as the personal `SessionHandle` (the key never crosses to JS).
- `org_item_encrypt/decrypt(OrgHandle, …)` and `org_manifest_encrypt/decrypt(OrgHandle, …)` — XChaCha20-Poly1305 with the org key directly (no Argon2id), reusing core.
- `org_handle_free(OrgHandle)` — zero the slot.
Everything else depends on A0.
### A1 · SW org foundation
- **Multi-context session.** `extension/src/service-worker/session.ts` is a single module-scope `SessionHandle | null` ("one vault per install"). Replace with a context map — `{ personal: Handle | null, orgs: Map<orgId, OrgHandle> }` plus a current-context pointer. The inactivity timer and `lock` zero **every** handle. Org handles are never written to `localStorage`, `IndexedDB`, or any persistent store (org spec line 231).
- **Org config storage.** `chrome.storage.local.orgConfigs: Array<{ orgId, displayName, hostType, hostUrl, repoPath, apiToken, memberId }>` — mirrors the personal `vaultConfig`. (The device key in `chrome.storage.local.device_private_key` is reused to unwrap org keys; no new device identity.)
- **Org GitHost.** One `GitHost` per org repo via the existing `createGitHost` factory.
- **Read flow.** On switch to an org: read public `members.json` + `collections.json` (unencrypted) → locate this device's member record by ed25519 fingerprint → take its `collections` grant list → `org_unwrap_key` → fetch + `org_manifest_decrypt`**filter manifest entries to granted collection slugs** → cache. Items decrypt on demand via `org_item_decrypt`.
- **Offline.** If the org `GitHost` fetch throws a network error, serve the last-pulled manifest read-only and set an `orgOffline` flag.
- **New SW messages (popup-only):** `org_list_configs`, `org_switch { context: 'personal' | orgId }`, `org_list_items` (grant-filtered), `org_get_item`, `org_list_collections`. Each must be added to the `PopupMessage` union AND `POPUP_ONLY_TYPES` AND a handler arm (`extension/src/shared/messages.ts` — the three-place rule).
### A2 · Org read UI
- **Context switcher.** A top-level Personal / `<org>`… selector in the vault-tab sidebar (primary surface per org spec) and the popup header. Switching sends `org_switch` and reloads the list.
- **Reuse.** The `popup/components/*` renderers are `StateHost`-driven, so org item detail/type views render unchanged once the host projects org state (items, collections). A collection facet in the sidebar mirrors the existing type-category nav.
- **Offline indicator.** "org offline — writes disabled" banner when `orgOffline`.
### A3 · Org write (gated on the signing spike)
- **Signed-push GitHost path.** A new method that builds a commit through the Git Data API, signs the commit object with `sign_for_git` (device key from `chrome.storage.local`), pushes it, and updates the ref. (Generic enough that personal device-auth writes could later adopt it.)
- **SW write handlers.** `org_add_item`, `org_update_item`, `org_delete_item`: encrypt with the org handle, write to the **collection-scoped** path `items/<slug>/<id>.enc`, update the org manifest — **both writes** via signed push (the personal "manifest + item both written" invariant applies, `extension/ARCHITECTURE.md` § Invariants).
- **UI.** Add/edit reuse the existing per-type item forms; add gains a **granted-collection picker**. Delete = soft-delete (trash) in the org manifest, mirroring personal trash semantics — the org CLI already ships `rm`/`restore`/`purge` (v0.8.0), so the backend is ready.
### A4 · Org SW acceptance tests (vitest)
Per org spec § Extension Tests, plus write coverage: org context switch replaces the personal manifest with no cross-contamination; org master key appears only in the `Zeroizing` session, never in `localStorage`/`IndexedDB`; offline read-only triggers on a git network error; grant filtering hides ungranted collections; a write produces a signed commit the hook accepts (mock the hook contract).
## Data flow (read)
```
popup/vault → org_switch(orgId) → SW:
read members.json + collections.json (public)
→ match device fingerprint → grants
→ org_unwrap_key(keys/<member-id>.enc, device_priv) → OrgHandle (Zeroizing, in WASM)
→ fetch manifest.enc → org_manifest_decrypt → filter to grants → cache
popup/vault → org_list_items → SW returns grant-filtered projection (titles/collections, no secrets)
popup/vault → org_get_item(id) → SW org_item_decrypt → resolved item
```
## Error handling
- **Device not a member** (fingerprint absent from `members.json`) → `not_an_org_member` — clear "this device isn't a member of <org>".
- **Ungranted collection** → filtered out on read; rejected client-side on write before push (and by the hook as defense in depth).
- **Offline** → read-only banner; writes blocked client-side with the indicator.
- **Signed push / hook rejection** → surfaced verbatim; the manifest write is not attempted if the item write fails (no half-mutation).
- Reuse the existing snake_case SW error convention + `humanizeError`.
## Living-docs impact
`extension/ARCHITECTURE.md` (org context, multi-context session, signed-push path, new messages), `docs/SECURITY.md` (extension org key handling + signed-commit write path), `ROADMAP.md`/`STATUS.md` (org parity shipped), `CHANGELOG.md`. The org spec's § Extension scope note (read-only phase-1) is superseded by this spec's read+write decision — note that in the org spec.
## Open risks
1. **Org-write signing spike** (§ above) — the gating unknown.
2. **Multi-context session refactor** touches the SW's most security-sensitive module (`session.ts`); the lock/timer-zeroes-all invariant must be preserved and tested.
3. **Git Data API divergence** Gitea vs GitHub for signed commits — the spike must cover both; if only one host works, ship org write for that host and record the limitation.

View File

@@ -0,0 +1,76 @@
# Pluggable Second Factor (Key File) + Positioning Pivot — Design Spec
- **Date:** 2026-06-20
- **Status:** Approved (brainstorming) — ready for writing-plans
- **Release target:** v0.9.0 (one multi-agent train, alongside the Extension Org Vault GUI spec)
- **Anchor:** `main` post-v0.8.1 (`2fa4d68` tag; HEAD `59ebc28`)
- **Driver:** Product audit `docs/superpowers/reviews/2026-06-20-product-audit.md` recommendation #2 (PIVOT) — re-lead positioning with the durable thesis (two secrets into the KDF + zero server metadata + git audit) and treat the steganographic image as **one option** for the second factor, with a plain key file as the alternative.
- **Builds on:** `2026-04-11-relicario-design.md` (crypto pipeline), `docs/CRYPTO.md`, `docs/FORMATS.md`, `extension/ARCHITECTURE.md` (setup wizard).
## Purpose & scope
Make the vault's second factor **pluggable**: the 256-bit secret can be carried by the existing steganographic reference image (default) **or** by a plain **key file** — chosen at vault creation, with the same secret and the same KDF underneath. Re-lead the project's positioning on the durable thesis and frame stego as an option rather than the headline.
**Key insight — this is crypto-light.** The second factor is *already* just 32 bytes (`image_secret`); stego is only the storage/transport. The Argon2id KDF (`passphrase || image_secret → master_key`) and everything downstream are **byte-for-byte unchanged**. Only the *source* of the 32 bytes changes. No new crypto primitive.
**Mental model (chosen in brainstorming):** at creation you pick the container — **Reference Image** (default) or **Key File** — and both materialize the same random 32-byte secret. The vault records a non-secret container-type hint so unlock prompts for the right thing. Because it is literally the same secret, the recovery-QR already fits this model, and export/convert between containers is a natural (optional) add-on.
**In scope:** key-file generation at init; unlock from a key file; the non-secret container hint; CLI + extension support; the positioning/docs pivot.
**Out of scope (optional stretch, not core):** `keyfile export` / convert-an-existing-image-vault-to-keyfile. It needs the secret in hand (a re-provide-then-write flow), so it is deferred to keep the lift tight; noted as a fast-follow.
## Crypto model
- **Container hint.** `.relicario/params.json` (non-secret, already holds Argon2id params) gains `"second_factor": "image" | "keyfile"`. **Absent ⇒ `"image"`** (back-compat for every existing vault). Read pre-unlock to choose the prompt; reveals nothing secret (container type is not a secret).
- **Raw-secret unlock path.** Today `unlock(passphrase, jpeg_bytes, salt, params)` extracts the 32-byte secret from the JPEG internally (`extension/ARCHITECTURE.md` notes "unlock takes JPEG bytes … extracts internally"). Add an explicit `unlock_with_secret(passphrase, secret: &[u8;32], salt, params)` that skips extraction. The KDF and AEAD are identical; this is the only core seam.
- **Key-file armor.** Core owns the format so CLI and WASM share it: `keyfile_encode(secret) -> Vec<u8>` and `keyfile_decode(bytes) -> [u8;32]`. Layout: a `relicario-keyfile-v1` header line + base64 of the 32 bytes + trailing newline. `keyfile_decode` validates the header, rejects malformed input, and holds the secret in `Zeroizing`. Suggested extension: `.relkey`.
## Stream decomposition
### B1 · core + WASM
- `relicario-core`: `keyfile_encode` / `keyfile_decode` (Zeroizing), `unlock_with_secret`, and read/write of the `second_factor` field in the params struct (default `image`).
- `relicario-wasm`: bind `keyfile_encode`, `keyfile_decode`, `unlock_with_secret`.
- Equivalence test: for a given 32-byte secret, `unlock_with_secret(pass, secret, …)` derives the **same** master key as unlocking from a JPEG that embeds that secret — proves the seam is transport-only.
### B2 · CLI
- `relicario init`: a container choice — `--key-file <path>` (or interactive) generates the 32-byte secret, writes the `.relkey` via `keyfile_encode`, and sets params `second_factor: "keyfile"`; the existing `--image`/`--output` path stays the default and sets `"image"`.
- `unlock` across all commands: read the factor per the params hint — if `keyfile`, from `--key-file` or `RELICARIO_KEYFILE` (mirroring `RELICARIO_IMAGE`); `keyfile_decode``unlock_with_secret`.
- Help text + `docs/user_docs/` reflect the choice.
### B3 · extension
- **Setup wizard step 3** gains a container choice (Reference Image | Key File). Key-file mode: generate the secret, offer the `.relkey` for download, set the params hint.
- **Unlock**: per the params hint, prompt for the key file (file picker) instead of the image; `keyfile_decode``unlock_with_secret`.
- **Local storage (chosen default):** store the key-file bytes in `chrome.storage.local` as `keyfileBase64`, re-read each unlock — exactly as `imageBase64` works today. Same "something you have" threat model and the same offline behavior; documented as equivalent, not weaker.
### B4 · docs / positioning pivot
- **README** re-led: open with the thesis (two independent secrets into the KDF, self-host, zero server metadata, git audit); present the steganographic image as a distinctive **option** for the second factor (with the key file as the plain alternative), not the headline. Keep the dead-drop story as flavor, not the lead.
- **DESIGN.md** secrets-map + **docs/CRYPTO.md** (pluggable-transport framing: "the second factor is 32 bytes; image/key-file/recovery-QR are interchangeable containers") + **docs/FORMATS.md** (`.relkey` armor + params `second_factor` field).
## Security-review gate (before merge)
A focused `/security-review` pass on the key-file path:
- **No weaker than stego:** same 32-byte entropy, same Argon2id, same AEAD. The equivalence test (B1) is the evidence.
- **Armor parsing** rejects malformed/short input without panics or oracles.
- **Threat-model honesty:** `.relkey` and `keyfileBase64` are the second factor *in the clear* — exactly the same posture as the reference JPEG / `imageBase64` today. Document this in `docs/SECURITY.md`; do not imply the key file is encrypted (it is the "something you have", protected by needing the passphrase too).
- **No oracle differences** between the image and key-file unlock failure paths (both surface the deliberately-ambiguous "wrong passphrase or reference image/key").
## Error handling
- Malformed/empty key file → `invalid_key_file` (CLI + extension), distinct from a wrong-secret AEAD failure.
- Missing key file at unlock when params say `keyfile` → prompt/`RELICARIO_KEYFILE` guidance.
- Params `second_factor` present but unknown value → reject with a clear message (forward-compat guard).
## Testing
- **core:** keyfile encode/decode round-trip; `unlock_with_secret` master-key equivalence vs JPEG unlock; params back-compat (absent ⇒ image); malformed-armor rejection.
- **CLI:** `init --key-file → unlock --key-file` lifecycle against a temp vault; `RELICARIO_KEYFILE` env path; image vault still unlocks unchanged.
- **extension (vitest):** setup key-file path writes the hint + offers download; unlock reads `keyfileBase64` and derives a session; an existing image vault is unaffected.
## Living-docs impact
`README.md`, `DESIGN.md`, `docs/CRYPTO.md`, `docs/FORMATS.md`, `docs/SECURITY.md`, `crates/relicario-core/ARCHITECTURE.md` (new core functions), `extension/ARCHITECTURE.md` (setup container choice + `keyfileBase64`), `CHANGELOG.md`.

View File

@@ -6,9 +6,21 @@ This page walks you through building Relicario from source, creating your first
## 1. Build and install the CLI ## 1. Build and install the CLI
Relicario is currently distributed as source code, so you will need the Rust toolchain (`cargo`) installed. If you do not have it yet, visit [rustup.rs](https://rustup.rs) and follow the instructions there. Relicario is currently distributed as source code, so you will need two tools installed first:
Clone the Relicario repository, then build the CLI: - **git** — used to clone the source now, and to sync your vault later. If you don't have it, see [git-scm.com/downloads](https://git-scm.com/downloads).
- **The Rust toolchain** (`cargo`) — if you don't have it yet, visit [rustup.rs](https://rustup.rs) and follow the instructions there.
Clone the Relicario repository and move into it:
```bash
git clone ssh://git@git.adlee.work:2222/alee/relicario.git
cd relicario
```
> Substitute the URL of whichever Relicario repository you were given access to.
Then, from the project root, build the CLI:
```bash ```bash
# Fast debug build (good for trying things out) # Fast debug build (good for trying things out)

View File

@@ -12,7 +12,7 @@ Relicario stores your vault in a git repo. Every item you add, edit, or delete i
relicario sync relicario sync
``` ```
Under the hood, this does a `git pull --rebase` followed by a push to your configured git remote — the same remote you set up when you initialized your vault. Any machine that has the vault cloned and a copy of your reference image can sync and stay up to date. Under the hood, this does a `git pull --rebase` followed by a push to your configured git remote. Your vault is a normal git repository, so you add its remote yourself the usual way — for example, from inside the vault directory, `git remote add origin <your-server-url>` (a self-hosted Gitea/GitHub repo, or any git host you control). Once a remote is configured, any machine that has the vault cloned and a copy of your reference image can `relicario sync` and stay up to date.
**Check what's in your vault at a glance:** **Check what's in your vault at a glance:**