Compare commits
13 Commits
feature/us
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b38aac188 | ||
|
|
c3044ed5af | ||
|
|
59ebc28e7e | ||
|
|
df488a3d7c | ||
|
|
2fa4d6824c | ||
|
|
783e3493f0 | ||
|
|
4cca9b465c | ||
|
|
5be3043ab5 | ||
|
|
cf89bf8ca4 | ||
|
|
a91ceea0ed | ||
|
|
415d8ed9ef | ||
|
|
b54aaea239 | ||
|
|
c5b1917eb0 |
175
.claude/skills/product-expert/SKILL.md
Normal file
175
.claude/skills/product-expert/SKILL.md
Normal 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`.
|
||||
@@ -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 "~40–60
|
||||
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.
|
||||
155
.claude/skills/product-expert/references/lenses.md
Normal file
155
.claude/skills/product-expert/references/lenses.md
Normal 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 1–2, `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 1–3 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.
|
||||
94
.claude/skills/product-expert/references/output-templates.md
Normal file
94
.claude/skills/product-expert/references/output-templates.md
Normal 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:** [2–4 bullets — what's genuinely working, through the user + market lenses.]
|
||||
**Gaps:** [2–4 bullets — table-stakes misses, friction, parity gaps.]
|
||||
**Risks:** [1–3 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.
|
||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,5 +1,56 @@
|
||||
# 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
|
||||
|
||||
Git-native multi-user **org vaults**: a separate org git repository alongside each
|
||||
|
||||
@@ -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.
|
||||
|
||||
**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):
|
||||
- `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
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2156,7 +2156,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "relicario-cli"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -2188,7 +2188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "relicario-core"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"base64",
|
||||
@@ -2235,7 +2235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "relicario-wasm"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"ed25519-dalek",
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
|
||||
| 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.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-α/β₁/β₂) |
|
||||
@@ -16,11 +17,11 @@ See `CHANGELOG.md` for tagged-release detail and `STATUS.md` for the per-train c
|
||||
|
||||
## 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 — 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)
|
||||
|
||||
## Medium-term
|
||||
|
||||
33
STATUS.md
33
STATUS.md
@@ -5,10 +5,23 @@
|
||||
## 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.
|
||||
**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
|
||||
|
||||
### 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`)
|
||||
|
||||
Spec: `docs/superpowers/specs/2026-05-02-phase-2b-form-layout-design.md`
|
||||
@@ -114,10 +127,10 @@ Item CRUD commands (B9–B14): `org add` (`OrgAddKind`: Login/SecureNote/Identit
|
||||
|
||||
**A5 doc-fix** (`enforce_owner_only_elevation` parent-role close, `519e503`) and this living-docs sweep also landed.
|
||||
|
||||
**Tracked follow-ups (deferred, not shipped):**
|
||||
- `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)
|
||||
- Extension org-vault switch + read parity (Dev-D deferred)
|
||||
- Extension org write operations
|
||||
**Tracked follow-ups:**
|
||||
- `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) — still deferred; forward plan in the parity gap analysis
|
||||
- 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
|
||||
|
||||
**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.
|
||||
|
||||
Pending org-vault follow-ups (in rough priority order):
|
||||
- `org add`/`edit` parity for Card, SshKey, Document, Totp
|
||||
- Extension org switch + read parity (Dev-D)
|
||||
- Extension org write operations
|
||||
Pending follow-ups (in rough priority order; **forward plan:** `docs/superpowers/specs/2026-06-20-extension-cli-parity-gap-analysis.md`):
|
||||
- **Extension org parity — read** (Dev-D): org context switch + collection-filtered browse in the popup/vault tab
|
||||
- **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)
|
||||
- **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)
|
||||
|
||||
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).
|
||||
|
||||
@@ -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.
|
||||
Owns the top-level `Cli` / `Commands` enum and every subcommand enum
|
||||
(`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
|
||||
three test-only env-var hooks (`test_passphrase_override`,
|
||||
`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
|
||||
hook authorizes against, never decrypting), fingerprint-based member matching
|
||||
(`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
|
||||
`commit.gpgsign=false`, because org commits MUST be signed (the hook verifies
|
||||
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` /
|
||||
`audit` (verified-signer attribution + `TAMPERED` flag).
|
||||
|
||||
*Item CRUD (7):* `org add` creates typed items via `OrgAddKind`
|
||||
(`commands/org.rs:749`) — **Login / SecureNote / Identity only**; Card /
|
||||
SshKey / Document / Totp creation is a deferred follow-up. `get` / `list` can
|
||||
display any item type if present. `org get <query> [--show]` masks secrets
|
||||
unless `--show`; `org list [--trashed]` filters by the caller's collection
|
||||
grants; `org edit <query>` is flag-driven (blank flags keep current values);
|
||||
`org rm` soft-deletes, `org restore` undoes, `org purge` permanently removes
|
||||
the encrypted blob. All item ops are collection-scoped and grant-enforced. The
|
||||
audit trail emits `item-create` / `item-update` / `item-delete` /
|
||||
`item-restore` / `item-purge`.
|
||||
*Item CRUD (7):* full item-type parity with the personal vault (v0.8.1).
|
||||
`org add` creates **all seven types** (Login / SecureNote / Identity / Card /
|
||||
Key / Document / Totp) via `OrgAddKind` (`commands/org.rs:751`); each arm
|
||||
delegates to the shared `item_build::build_*` builders through `build_org_item`
|
||||
(`commands/org.rs:799`), and `run_add` (`commands/org.rs:823`) sets tags
|
||||
post-build. Document is special-cased in `run_add` (`commands/org.rs:839`): its
|
||||
builder also yields an `EncryptedAttachment` that is written via
|
||||
`save_attachment` and git-staged before the signed commit. Single-line secrets
|
||||
(card number/CVV/PIN, TOTP secret, login password) accept a `--*-stdin` flag;
|
||||
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;
|
||||
extension org reads and writes (Dev-D).
|
||||
`org edit <query>` (`run_edit`, `commands/org.rs:1004`) is **interactive
|
||||
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:
|
||||
`find_vault_dir_from` (`helpers.rs:14-28`) walks up parent directories
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "relicario-cli"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
edition = "2021"
|
||||
description = "CLI for relicario password manager"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! (`commands/org.rs`). Centralizing it keeps the two surfaces from drifting.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
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(
|
||||
title: String, file: PathBuf, key: &Zeroizing<[u8; 32]>, max_bytes: u64,
|
||||
) -> Result<(Item, EncryptedAttachment)> {
|
||||
use relicario_core::item_types::DocumentCore;
|
||||
use relicario_core::{encrypt_attachment, AttachmentRef};
|
||||
let bytes = std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
|
||||
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();
|
||||
use relicario_core::AttachmentRef;
|
||||
let (enc, filename, mime_type, size) = encrypt_document_file(&file, key, max_bytes)?;
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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::Document(d) => {
|
||||
if let Some(path) = &file {
|
||||
let bytes = std::fs::read(path)
|
||||
.with_context(|| format!("read {}", path.display()))?;
|
||||
let enc = relicario_core::encrypt_attachment(
|
||||
&bytes, vault.key(), crate::org_session::DEFAULT_ORG_ATTACHMENT_MAX_BYTES)?;
|
||||
let (enc, filename, mime_type, size) = ib::encrypt_document_file(
|
||||
path, vault.key(), crate::org_session::DEFAULT_ORG_ATTACHMENT_MAX_BYTES)?;
|
||||
vault.remove_item_attachments(&collection, &id)?;
|
||||
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.mime_type = mime_type.clone();
|
||||
d.primary_attachment = enc.id.clone();
|
||||
new_doc_attachments = Some(vec![relicario_core::AttachmentRef {
|
||||
id: enc.id,
|
||||
filename,
|
||||
mime_type: d.mime_type.clone(),
|
||||
size: bytes.len() as u64,
|
||||
mime_type,
|
||||
size,
|
||||
created: now_unix(),
|
||||
}]);
|
||||
doc_attachment_rel = Some(rel);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "relicario-core"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
edition = "2021"
|
||||
description = "Core library for relicario password manager"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "relicario-wasm"
|
||||
version = "0.8.0"
|
||||
version = "0.8.1"
|
||||
edition = "2021"
|
||||
description = "WASM bindings for relicario password manager"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
128
docs/superpowers/reviews/2026-06-20-product-audit.md
Normal file
128
docs/superpowers/reviews/2026-06-20-product-audit.md
Normal 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).
|
||||
```
|
||||
@@ -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. | S–M | `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.
|
||||
104
docs/superpowers/specs/2026-06-20-extension-org-gui-design.md
Normal file
104
docs/superpowers/specs/2026-06-20-extension-org-gui-design.md
Normal 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 (A0–A2, 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.
|
||||
@@ -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`.
|
||||
@@ -6,9 +6,21 @@ This page walks you through building Relicario from source, creating your first
|
||||
|
||||
## 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
|
||||
# Fast debug build (good for trying things out)
|
||||
|
||||
@@ -12,7 +12,7 @@ Relicario stores your vault in a git repo. Every item you add, edit, or delete i
|
||||
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:**
|
||||
|
||||
|
||||
Reference in New Issue
Block a user