Product audit (product-expert skill) recommended two priority items; this lands the audit record plus the two approved design specs that will drive the v0.9.0 multi-agent train. - reviews/2026-06-20-product-audit.md — the roadmap audit (reality check, recommendations, PM brief) that drove the two items. - specs/2026-06-20-extension-org-gui-design.md — bring the org vault to the extension at read+write parity. Org write is gated on a Day-1 signing spike (the org hook rejects unsigned commits; the extension pushes unsigned today; sign_for_git exists in WASM but is unused). Spike-fail degrades to read-only + write follow-up. - specs/2026-06-20-pluggable-second-factor-design.md — key file as an alternative second factor (same 32-byte secret, same KDF; crypto-light), chosen at setup via a non-secret params hint, plus the positioning pivot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQbgrP6KQW5pibjbPEoTSs
10 KiB
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:
mainpost-v0.8.1 (2fa4d68tag; HEAD59ebc28) - Driver: Product audit
docs/superpowers/reviews/2026-06-20-product-audit.mdrecommendation #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-sidegit verify-commitandrelicario-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-commitfails) → 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— unwrapkeys/<member-id>.encinto the org master key held in WASMZeroizing, returning an opaque slot handle in the same pattern as the personalSessionHandle(the key never crosses to JS).org_item_encrypt/decrypt(OrgHandle, …)andorg_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.tsis a single module-scopeSessionHandle | 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 andlockzero every handle. Org handles are never written tolocalStorage,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 personalvaultConfig. (The device key inchrome.storage.local.device_private_keyis reused to unwrap org keys; no new device identity.) - Org GitHost. One
GitHostper org repo via the existingcreateGitHostfactory. - Read flow. On switch to an org: read public
members.json+collections.json(unencrypted) → locate this device's member record by ed25519 fingerprint → take itscollectionsgrant list →org_unwrap_key→ fetch +org_manifest_decrypt→ filter manifest entries to granted collection slugs → cache. Items decrypt on demand viaorg_item_decrypt. - Offline. If the org
GitHostfetch throws a network error, serve the last-pulled manifest read-only and set anorgOfflineflag. - 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 thePopupMessageunion ANDPOPUP_ONLY_TYPESAND 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 sendsorg_switchand reloads the list. - Reuse. The
popup/components/*renderers areStateHost-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 fromchrome.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 pathitems/<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 ". - 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
- Org-write signing spike (§ above) — the gating unknown.
- 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. - 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.