Universal isomorphic-git receive-pack push path shipped & verified: sshsig.ts +
mem-fs.ts + git-push.ts + commitSigned on GitHost/Gitea/GitHub. Full SW suite
163/163, build:all bundles isomorphic-git into the SW, /security-review clean.
Tasks 3-5 remain HELD on Dev-A + Dev-B merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
PushResult has a singular `error: string | null` and a `refs` map — there
is no top-level `errors: string[]`. Replace the fabricated type and the
`errors`-based rejection guard with a correct PushResultRuntime type and a
belt-and-suspenders refsOk check. The real rejection path is a thrown
GitPushError (isomorphic-git throws, never returns, when a hook declines);
replace the single fiction-testing test with (a) a throw-path test that mocks
push to reject with a GitPushError and (b) a defensive non-throwing test that
mocks push to resolve { ok: false, error: 'unpack failed' }.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
Add CommitFile/CommitSigner types + commitSigned to GitHost interface.
Implement git-push.ts (clone→add→commit({onSign=SSHSIG})→push over http/web)
and wire GiteaHost.commitSigned / GitHubHost.commitSigned. Tests: 7/7 incl.
golden-vector tie against the sshsig.test.ts byte-exact vector.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
Introduces OrgConfig/OrgConfigSummary types, chrome.storage.local backing
via org-config.ts, and the first org SW message (org_list_configs) wired in
all three required places: PopupMessage union, POPUP_ONLY_TYPES set, and a
case arm in popup-only.ts. Handler projects to OrgConfigSummary — apiToken,
hostUrl, repoPath, memberId never leave the SW. 3 new TDD tests (RED→GREEN).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Add `--key-file <path>` to `relicario init` as a mutually exclusive
alternative to `--image`/`--output`. When given, a 32-byte secret is
generated with OsRng, armored via `keyfile_encode`, written to the
supplied path (gitignored, never committed), and `params.json` records
`"second_factor": "keyfile"`. The `--image` arg is now Option<PathBuf>;
existing image-vault callers are unchanged. `ParamsFile::for_new_vault`
now takes the second-factor variant as an explicit argument.
Tests: un-ignore init_keyfile_then_unlock_keyfile_round_trips (full
round-trip), strengthen its assertion to check for distinctive username
"octocat", and add init_keyfile_writes_relkey_and_keyfile_params
(armor, params parse, gitignore). Full workspace green (0 failures),
clippy clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
Mini-spike (isomorphic-git) proved the universal write path end-to-end with no
live creds: Tier 1 commit({onSign}) embeds our SSHSIG -> git verify-commit +
verify-org-commit both PASS; Tier 2 push over git-receive-pack to a bare repo
running the real org pre-receive hook ACCEPTS a member-signed commit and
REJECTS a non-member one.
- Spike doc: + Addendum recording the isomorphic-git Tier 1/2 results + Task 2
implications (in-memory fs only, http/web, onAuth Basic, no new WASM export).
- Plan doc: architecture re-scoped (Gitea has no write Git Data API at any
version -> single universal receive-pack path for both hosts); Task 1 marked
DONE/GO; Task 2 rewritten as the universal commitSigned; Dev-A OrgManifest
contract + Tasks 3-5 HELD-on-A/B recorded.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
clearAll is the security-zero path: each .free() in its own try/catch so a
single throwing handle can't leave the others un-zeroed; state is fully reset
regardless. setOrg frees any prior handle for the same org before overwriting
so a re-unlock can't orphan an un-zeroed WASM key. Tests updated to the
best-effort contract (session.test.ts) plus independent-mock, throwing-free,
and re-unlock cases (org-session.test.ts).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
PM contract refinement: org_list_items returns a DEDICATED OrgManifestEntry[]
(collection REQUIRED), not the personal ManifestEntry-with-optional-collection.
Dev-A is mirroring OrgManifest/OrgManifestEntry into shared/types.ts — the list
imports that type at integration (no hand-rolling). Behaviors unaffected; scaffold
stays RED (6 fail / 3 pass). Fixture-only change, no src.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W3b8DA2mXmLWkd4zDbHXHg
Adds three WASM bindings for the pluggable second-factor work:
- unlock_with_secret: mirrors unlock() but accepts a raw 32-byte secret
instead of a carrier image, skipping imgsecret::extract
- keyfile_encode: armors a 32-byte secret into relicario-keyfile-v1 format
- keyfile_decode: decodes keyfile armor back to the raw 32-byte secret
Adds native equivalence test proving that unlock_with_secret and unlock
derive an identical Argon2id master key when given the same secret,
passphrase, salt, and KDF params. Comparison is direct (master-key bytes),
not cross-decrypt, so the test is both native-safe and unambiguous.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
README: open with the two-independent-secrets-into-KDF thesis; reframe the
second factor as pluggable (reference image or key file); keep the stego
deep-dive as a distinctive option; update all scenario tables, recovery section,
env-var hint, and roadmap line to use "second-factor secret" language.
docs/CRYPTO.md: add "Pluggable second-factor containers" subsection explaining
that the 32-byte input to Argon2id is identical regardless of container;
reference-image / key-file / recovery-QR are interchangeable carriers.
Update org-vs-personal comparison table and entropy labels.
docs/FORMATS.md: document the .relkey armor format (relicario-keyfile-v1 +
base64 line) and the params.json second_factor field ("image"|"keyfile",
absent means "image" for back-compat). Cite crates/relicario-core/src/keyfile.rs
keyfile_encode/keyfile_decode with TODO-pin comments. Update KDF input formula
from image_secret to second_factor_secret.
DESIGN.md: split secrets-map "Reference image bytes" row to distinguish image
and key-file containers; add key-file row noting keyfileBase64 in
chrome.storage.local has the same in-the-clear posture as imageBase64.
Update vault-layout note and KDF conventions row.
docs/SECURITY.md: add "Second-factor storage posture" subsection explicitly
stating .relkey and keyfileBase64 are the second factor in the clear — same
posture as the reference JPEG / imageBase64, not weaker, protected by the
passphrase invariant. Add RELICARIO_KEYFILE to the env-var trust surface table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014THSV6cA4Gxa7bxFfisHBB
Review fixes for Task 1 (org_unwrap_key). Public signature unchanged.
- Add device::extract_ed25519_seed -> Zeroizing<[u8;32]>: length-checks the
slice then copy_from_slice straight into the Zeroizing buffer, so the raw
seed never lands in a plain [u8;32] (Important #1).
- Refactor device::sign() to call the new helper (pure refactor; device/sign
roundtrip tests unchanged and green) (Important #2).
- org_unwrap_key now calls relicario_core::device::extract_ed25519_seed
directly; drop the duplicated local decode_device_seed helper and remove the
redundant ssh-key dep from relicario-wasm (core already pins it) (Important #2).
- Device-key-form finding (OpenSSH PEM, not base64; spec name matches reality)
moved onto the core helper, condensed copy kept on org_unwrap_key.
- Test helper keeps the private key as Zeroizing<String> and passes &priv
(derefs to &str) — no plain-String copy (Minor #3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Task 1 GO/NO-GO for v0.9.0 Plan C (org write). Proven locally end-to-end: a
commit signed with ONLY the raw sign_for_git primitive + manual SSHSIG framing
(no new WASM export) passes both `git verify-commit` and `relicario-server
verify-org-commit`, incl. item+manifest dual-write to a granted collection with
full path authz. Hook matches on the signing-KEY fingerprint -> members[].
ed25519_pubkey; committer/author text is free-form (not checked).
Transport diverges by host:
- GitHub: Git Data API `POST /git/commits` has a `signature` field, inserted
verbatim into gpgsig -> LIKELY-GO (1 live round-trip to confirm SSHSIG, not
just PGP, is stored byte-for-byte).
- Gitea: `/git` API is GET-only on live 1.25.5 (no create-commit) -> REST
NO-GO. Viable path is a `git-receive-pack` smart-HTTP packfile push
(endpoint reachable, 401 Basic-auth) = bigger Task 2 than planned.
Also flags a pre-existing latent bug: gitea.ts putBlob calls nonexistent
Gitea /git write endpoints. Scope decision (GitHub-only vs build the Gitea
packfile pusher vs defer write) raised to PM.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LKBbrAjmpVXMEK15pNi3Ha
Expose org_unwrap_key(keys_blob, device_private_openssh) to let the
extension SW unwrap a member's ECIES-wrapped org master key into an
ordinary Zeroizing session handle. The returned handle is key-agnostic
and works with the existing item_decrypt/manifest_decrypt AEAD.
Key-form finding: device private keys are OpenSSH PEM blobs produced by
relicario_core::device::generate_keypair(), not base64 raw bytes. The
plan's "device_private_key_base64" parameter name was misleading; all
code and the .d.ts declaration use "device_private_openssh" to match
the spec and reality.
Adds ssh-key dep to relicario-wasm for PEM parsing in decode_device_seed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s527M917W47LDrfQ4t47g
Add SecondFactor enum (Image | Keyfile, serde lowercase) to relicario-core::crypto,
re-export from lib.rs alongside KdfParams. Add #[serde(default)] second_factor field
to ParamsFile in relicario-cli::session so existing params.json files (no field) still
parse as Image. for_new_vault() initialises the field to Image. No unlock logic
changed; that is deferred to Task 4. TDD: tests written RED first, then GREEN.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McMF5pUJbCMricmFEnf2sG
Extension-only patch: fixes vault create/attach in the browser extension, where
chrome.runtime.sendMessage silently dropped ArrayBuffer payloads (carrier JPEG →
0 bytes → opaque "no SOF marker found in JPEG"). See CHANGELOG for the full set.
- Version bumps: extension 0.7.0→0.8.2 (it had no changes in 0.8.0/0.8.1);
relicario-core/-cli/-wasm 0.8.1→0.8.2 for unified-tag consistency. The Rust
core/CLI are functionally unchanged from v0.8.1. relicario-server stays 0.1.1
(independent version line; no functional change).
- CHANGELOG.md: v0.8.2 section. STATUS.md / ROADMAP.md: v0.8.2 landed, v0.9.0
flagged as ready to kick off.
Verify: cargo build/test/clippy -D warnings all green (35 test binaries);
extension 435/435 vitest, build:all clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
clippy 1.93.0 added/tightened several lints that broke `cargo clippy -D warnings`
(the release gate). All behavior-preserving:
- unnecessary_map_or: `signer_parent.map_or(false, |r| …)` → `is_some_and`
(relicario-server/src/main.rs).
- needless_question_mark: drop redundant `Ok(… ?)` wrappers around
`serde_json::from_str(…).context(…)` in org_session.rs load_meta/load_members/
load_collections.
- print_literal: inline the trailing "FLAG" header literal in `org audit`.
- items_after_test_module: move `configure_git_signing` above the
`seed_helper_tests` module in device.rs so the test module is last.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
- Closed vault drawer now negates its 440px via margin-right so it reserves no
layout space — the list pane and its empty state center in the full
post-sidebar width instead of being shifted left by the off-screen drawer.
- .vault-lock-screen gets flex:1 so the locked vault tab grows to full viewport
width instead of collapsing to content width and pinning to the left.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
chrome.runtime.sendMessage serializes with a JSON-like algorithm that drops
ArrayBuffer/TypedArray payloads — they arrive as empty objects, so a carrier
JPEG reached the service worker as 0 bytes and failed with the opaque WASM
error "no SOF marker found in JPEG". This broke extension vault creation.
- shared/message-binary.ts: encodeBinary/decodeBinary deep-walk a message tree
and base64-envelope every binary buffer so it survives the channel (chunked
base64 to avoid stack overflow on multi-MB buffers); plain number[] untouched.
- Wire encode/decode at all four message boundaries: popup, service-worker
onMessage listener (decode request, encode response), setup swSend, vault
postToServiceWorker.
- handleCreateVault: magic-byte guard classifies empty / non-JPEG carriers into
actionable errors (carrier_image_empty / carrier_image_not_jpeg) instead of
the opaque WASM error; error-copy.ts adds user-facing text; setup errors now
route through lookupErrorCopy.
- router SETUP_ALLOWED: add create_vault / attach_vault / generate_recovery_qr —
the Phase-3 setup wizard sends these from the setup page and they were being
rejected as unauthorized_sender.
- Tests: message-binary round-trip suite (7) + carrier-guard (2) + setup
allowlist (3). 435/435 vitest, build:all clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pe8qw5KePDqAEBsAxnVQuJ
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
A standalone, self-triggering skill that acts as Relicario's product
strategist: audits the roadmap and reviews freshly-brainstormed release
specs for product/market fit, emitting PM-ready relay directive blocks.
Advisory only — the user stays the decision-maker.
- Two modes: roadmap audit (default) and spec review (verdict:
PROCEED / RESCOPE / CUT / PIVOT).
- Four-lens engine run as parallel subagents: ground-truth (verify
claims vs code/git, distinguishing an in-flight lift from real drift),
jobs-to-be-done, market/competitive, and strategy synthesis.
- Fast by default; `deep` adds live competitive web research.
- Durable by design: lenses read living docs (README/ROADMAP/STATUS/
CHANGELOG/specs) at runtime, so new surfaces/segments/features are
picked up automatically. The one static asset, competitive-landscape.md,
carries a last-reviewed date + freshness protocol.
- Wires a post-brainstorm product gate into CLAUDE.md's Planning section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQbgrP6KQW5pibjbPEoTSs
All four v0.8.1 streams merged (main 4c0a289): org add now supports all 7 item
types (card/key/totp/document) and org edit is interactive. Flip the two
high-level org sections from "coming" to shipped, grounded in the real merged
`relicario org add <type> --collection …` surface; remove the rebase TODO markers.
Mark v0.8.1 shipped (all four streams merged on 4c0a289, verified against
source): org add/edit parity for all 7 item types (Card/Key/Totp + Document),
collection-scoped attachment storage, and the grant-scoped attachment-write
pre-receive hook. Move org item-type parity from deferred to shipped; relabel
the org-vault row as v0.8.0; reference the new extension-cli parity gap analysis
as the forward plan for deferred extension org read/write. Scope: STATUS.md +
ROADMAP.md only (CHANGELOG + version bumps owned by PM).