Files
relicario/docs/superpowers/specs/2026-06-20-extension-org-gui-design.md
adlee-was-taken 9b38aac188 docs(specs): v0.9.0 design — extension org GUI + pluggable second factor
Product audit (product-expert skill) recommended two priority items; this
lands the audit record plus the two approved design specs that will drive
the v0.9.0 multi-agent train.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQbgrP6KQW5pibjbPEoTSs
2026-06-20 23:01:53 -04:00

10 KiB
Raw Blame History

Extension Org Vault GUI — Design Spec

  • Date: 2026-06-20
  • Status: Approved (brainstorming) — ready for writing-plans
  • Release target: v0.9.0 (one multi-agent train, alongside the Pluggable Second Factor spec)
  • Anchor: main post-v0.8.1 (2fa4d68 tag; HEAD 59ebc28)
  • Driver: Product audit docs/superpowers/reviews/2026-06-20-product-audit.md recommendation #1 — the org vault backend (v0.8.0 + v0.8.1) is fully shipped but has zero extension presence; the enterprise feature is stranded behind the CLI.
  • Builds on: 2026-06-06-relicario-enterprise-org-vault-design.md (§ Extension — Org Context), 2026-06-20-extension-cli-parity-gap-analysis.md (P3 cluster), extension/ARCHITECTURE.md.

Purpose & scope

Bring the org (enterprise) vault to the browser extension at read + write parity, so org members can browse, view, add, edit, and delete shared credentials from the popup and vault tab — not only the CLI. Org admin operations (member/collection/grant/rotate/audit) stay CLI-only by design (high-trust, low-frequency; org spec § Extension scopes them out).

In scope: org context switching; grant-filtered browse/read of org items (all 7 types) in popup + vault tab; org item write (add/edit/rm, all 7 types); offline read-only indicator; SW acceptance tests.

Out of scope: org admin in the extension; per-collection cryptographic isolation, SSO/LDAP, read audit, HTTP plane (all org phase-2); webcam recovery scan.

The org-write signing gate (highest risk — read first)

The org pre-receive hook rejects unsigned commits unconditionally: it shells git verify-commit and requires a device ed25519/SSH signature from a current member (crates/relicario-server/src/main.rs:95-102). The extension today pushes unsigned commits via the host Contents API (extension/src/service-worker/gitea.ts/api/v1/.../contents/{path}), authored under the API token. The ed25519 signing primitive exists in WASM (sign_for_git, crates/relicario-wasm/src/lib.rs:253) but is unused by the extension — there is no signed-commit push path, and the Contents API cannot carry a caller-supplied signature.

Therefore org write from the extension must construct and push a signed commit via the Git Data API (blob → tree → commit-with-signature → update-ref). This is feasible but unproven against the host APIs.

Stream A3 begins with a spike, gating the rest of A3:

Prove that a commit signed in the SW with sign_for_git, pushed via the Gitea Git Data API, passes server-side git verify-commit and relicario-server verify-org-commit. Then repeat for GitHub.

  • Spike passes → A3 proceeds (signed-push GitHost path + org write UI); v0.9.0 ships read + write.
  • Spike fails (host API strips/normalizes the SSH signature such that git verify-commit fails) → org write degrades to a follow-up lift; v0.9.0 still ships org read (A0A2, A4-read) + the full Pluggable Second Factor spec. The spike is ~1 day and read + the other spec are unblocked regardless, so a failed spike wastes nothing.

The spike result is recorded back into this spec and STATUS.md before A3 build work starts.

Architecture

An org vault is a second git repo alongside the personal vault, cryptographically isolated (org spec § Architecture). The org master key is a random 256-bit key, wrapped per member via ECIES (X25519 + XChaCha20-Poly1305) to their device ed25519 key. The extension mirrors the CLI: unwrap the org key with the device private key, decrypt items exactly as the personal vault does — but with the org key, not the Argon2id-derived personal key.

The extension keeps its existing architecture (SW is the crypto fortress; popup/vault are StateHost-driven view shells over chrome.runtime.sendMessage). Org support is an additive context, not a rewrite.

Stream decomposition

A0 · WASM org bridge (prerequisite)

relicario-core::org already performs ECIES unwrap and org item crypto for the CLI; none of it is exposed over relicario-wasm today (confirmed — no org exports in crates/relicario-wasm/src/lib.rs). Expose:

  • org_unwrap_key(keys_blob: &[u8], device_private_openssh: &str) -> OrgHandle — unwrap keys/<member-id>.enc into the org master key held in WASM Zeroizing, returning an opaque slot handle in the same pattern as the personal SessionHandle (the key never crosses to JS).
  • org_item_encrypt/decrypt(OrgHandle, …) and org_manifest_encrypt/decrypt(OrgHandle, …) — XChaCha20-Poly1305 with the org key directly (no Argon2id), reusing core.
  • org_handle_free(OrgHandle) — zero the slot.

Everything else depends on A0.

A1 · SW org foundation

  • Multi-context session. extension/src/service-worker/session.ts is a single module-scope SessionHandle | null ("one vault per install"). Replace with a context map — { personal: Handle | null, orgs: Map<orgId, OrgHandle> } plus a current-context pointer. The inactivity timer and lock zero every handle. Org handles are never written to localStorage, IndexedDB, or any persistent store (org spec line 231).
  • Org config storage. chrome.storage.local.orgConfigs: Array<{ orgId, displayName, hostType, hostUrl, repoPath, apiToken, memberId }> — mirrors the personal vaultConfig. (The device key in chrome.storage.local.device_private_key is reused to unwrap org keys; no new device identity.)
  • Org GitHost. One GitHost per org repo via the existing createGitHost factory.
  • Read flow. On switch to an org: read public members.json + collections.json (unencrypted) → locate this device's member record by ed25519 fingerprint → take its collections grant list → org_unwrap_key → fetch + org_manifest_decryptfilter 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 ".
  • 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.