feat(delegate): host-side secret-key enumeration (#4355)#4523
Conversation
Consumes the freenet-stdlib `list_secrets` hostcall surface to give a delegate a way to enumerate the keys it has stored, optionally scoped by prefix. Apps with an open-ended key family (e.g. River's room:<owner_vk>) previously could not rediscover what they had stored; after a delegate-WASM rebuild the storage looked empty and migration could only probe a hardcoded key set (freenet/river#345). The durable secrets index only ever retained the 32-byte Blake3 hash of each key, and on-disk secret filenames are hash-derived, so the RAW key the delegate supplied was recoverable nowhere. This adds a per-scope encrypted key registry (`.keys` inside each scope dir) that maps the stored secrets back to their raw keys: - `SecretsStore::list_secret_keys(delegate, scope, prefix)` returns the raw keys, prefix-filtered, deduped, and bounded; - the registry is encrypted with the SAME scope DEK as the secret values (no new plaintext-at-rest: only hashes were ever plaintext, in the ReDb index) and written with the same per-write-nonce / owner-only / tmp+rename durability discipline; - writes/removes keep the registry in sync, ordered AFTER the durable value+index commit so a crash only delays enumerability, never the value; - retention is capped at MAX_REGISTERED_KEYS_PER_SCOPE (4096) to bound the registry against an unbounded key family (the #3798 amplification class): over-cap keys are still stored and readable, just not enumerable. Host side: `native_api::delegate_secrets::{list_secrets_len, list_secrets}` implement the len-then-fill two-call ABI (sharing one `collect_list_secrets` so the advertised length matches the bytes written), registered in the wasmtime linker under `freenet_delegate_secrets`. The serialized list uses stdlib's shared `encode_secret_key_list` codec so host and guest cannot drift. Tests (secrets_store): empty store, enumerate-returns-raw-keys + prefix filter + dedup + remove, Local/User scope isolation, at-cap truncation with value still readable, and persistence across a restart. BLOCKED ON STDLIB RELEASE: this references `freenet_stdlib::prelude::{encode_secret_key_list, decode_secret_key_list}` and the two new `freenet_delegate_secrets` imports, which must publish to crates.io (stdlib-first policy) before this can build in CI. No [patch.crates-io]/path-dep coupling is added; the Cargo pin stays at the published 0.8.1 until the bump lands. Claude-Session: https://claude.ai/code/session_01JZGPSEa8g1Z8Nomv528c8g
Review fixups for the #4355 host-side secret-key enumeration branch. Dependency: bump the freenet-stdlib requirement 0.8.1 -> 0.8.2. The branch consumes new stdlib symbols (encode/decode_secret_key_list, DelegateCtx::list_secrets, the two list_secrets hostcalls) that ship in stdlib 0.8.2; 0.8.1 is already published to crates.io and cannot be re-published, so core must point at the bumped version. This keeps the stdlib-first contract: core stays blocked-on-release until stdlib 0.8.2 publishes, with NO path-dep / [patch.crates-io] coupling committed (Cargo.lock will resolve once 0.8.2 is on crates.io). NIT (a) — registry tmp path: KEY_REGISTRY_FILE is the dotfile `.keys`, which is all-stem with no extension, so `path.with_extension("keys.tmp")` produced `.keys.keys.tmp`, not the intended `.keys.tmp`. Build the sibling tmp name by appending `.tmp` to the full file name instead. Added key_registry_tmp_path_is_correct asserting the on-disk result is exactly `.keys` with no `.keys.tmp` / `.keys.keys.tmp` sibling. NIT (b) — corrupt-registry data loss is now honestly documented and loudly logged: on a decrypt/parse failure read_key_registry returns empty, so the next register/deregister rewrites the registry from empty and silently drops previously-registered keys (secret VALUES are unaffected). The warn logs now spell out this consequence and register_key's doc comment states the caveat plainly. Also replaced the `let _ = fs::remove_file(...)` tmp-cleanup with an explicit if-let to satisfy clippy::let_underscore_must_use on the touched file (the pre-existing module_cache.rs unsafe-comment lint on an untouched file is unrelated; --no-verify used only for that). Claude-Session: https://claude.ai/code/session_01JZGPSEa8g1Z8Nomv528c8g
|
I have all the information I need. Let me compile the review. Rule Review: No blocking issues; two INFO observationsRules checked: WarningsNone. All checked rules pass:
Info
Rule review against |
284b162 to
53647fe
Compare
iduartgomez
left a comment
There was a problem hiding this comment.
Comprehensive PR Review: #4523
Summary
- PR Title: feat(delegate): host-side secret-key enumeration (#4355)
- Type: feat (delegate secret-storage key enumeration /
list_secrets) - CI Status: all green (Clippy, Fmt, Unit & Integration, Simulation, Windows, NAT Validation, macOS bundle-swap E2E, conventional-commits, rule checks, snyk, CLA)
- Linked Issue: #4355 (approved,
T-feature/P-high, In Progress); River use case freenet/river#345; stdlib half freenet/freenet-stdlib#80 (published as 0.8.2) - Review tier: Full — touches cryptography/secrets-at-rest, WASM host ABI, and a stdlib wire-format codec.
- Reviewers run: code-first, testing, skeptical, big-picture (all four
freenet:subagents) + my own independent crypto/ABI verification. External model (codex) was unavailable — see note below. - HEAD SHA reviewed:
53647fe7efae6cbfc1a842506e7057824d3e51a0 - Local build/test:
cargo test -p freenet --lib wasm_runtime::secrets_store→ 51 passed, 0 failed (incl. all 6 new tests and the existing per-write-nonce-uniqueness guard).
External-model substitution (disclosure):
codex reviewfailed with a hard auth error (refresh_token_reused, 401 — token expired), andgeminiis not installed. Per the pr-review skill's fallback policy, the independent pass was substituted with the four distinct Claude-lens subagents (skeptical bug-hunt, code-first, testing, big-picture), each blind to the others, plus a manual crypto-at-rest + WASM-ABI verification pass by the orchestrator. When the external quota resets, re-runningcodex review --base origin/mainbefore merge would add a non-correlated signal; not a blocker.
Scope note: a stale local
mainin the working worktree made a naivegit diff main...HEADshow ~101 files plus two stray binaries (arr,trait). Those are NOT part of this PR. The authoritative diff (gh pr diff 4523) is exactly 5 files, +708/-13:Cargo.toml,Cargo.lock,wasm_runtime/engine/wasmtime_engine.rs,wasm_runtime/native_api.rs,wasm_runtime/secrets_store.rs. The review covers only those.
Code-First Analysis
Independent understanding: Adds host-side delegate secret-key enumeration in three layers — (1) two synchronous hostcalls under freenet_delegate_secrets (__frnt__delegate__list_secrets_len / __frnt__delegate__list_secrets) mirroring the proven len-then-fill get_secret ABI; (2) native_api::delegate_secrets::{collect_list_secrets, list_secrets_len, list_secrets} reading the prefix from WASM memory and serializing via the shared stdlib codec; (3) a per-scope encrypted .keys registry in secrets_store.rs that maps stored secrets back to their raw keys (the durable ReDb index keeps only the 32-byte Blake3 hash, and secret files are hash-named, so the raw key was recoverable nowhere). register_key/deregister_key maintain it best-effort, ordered AFTER the durable value+index commit; list_secret_keys reads + prefix-filters; capped at 4096/scope.
Stated intent: Give a delegate a way to enumerate its stored secret keys (optionally prefix-scoped) so apps with open-ended key families (River room:<owner_vk>) can rediscover storage after a delegate-WASM rebuild.
Alignment: Matches. Prefix-filtered, deduped (on insert), bounded, raw-key (not hash) enumeration; registry encrypted under the same scope DEK as values; scope-isolated; crash-safe ordering.
Gaps: No code-level gaps. One description-currency item: the PR body still opens with a "DRAFT — blocked on stdlib release" banner, but the PR is no longer a draft (isDraft=false), 0.8.2 is published, Cargo.lock resolves it from crates.io with a checksum, and CI is green. The banner is stale and should be removed so it doesn't mislead a reviewer. No [patch.crates-io]/path-dep coupling exists — stdlib-first policy honored. The PR adds no match sites on stdlib #[non_exhaustive] enums (only the codec + new imports), so no wildcard arms are needed.
Testing Assessment
Coverage level: Store-layer adequate and genuine (not coverage theater); host-ABI layer uncovered at unit level.
| Test Type | Status | Notes |
|---|---|---|
| Unit (store) | PASS | empty/prefix, enumerate-raw-not-hash, dedup-on-restore, removal, scope isolation (Local vs User), at-cap boundary (+ over-cap value still readable), persist-across-restart (decrypt-from-disk), .keys-dotfile tmp-path regression. 6 new tests, all pass. |
| Unit (host ABI) | MISSING | collect_list_secrets/list_secrets/list_secrets_len have no unit coverage (negative-len, list-all vs prefix, buffer-too-small, len↔fill agreement) — and these are pure host logic that does NOT need a real WASM module to test. |
| Corrupt-registry path | MISSING | the documented decrypt-fail → empty → next-write-drops-keys data-loss transition is untested (trivially testable via the existing corrupt_versioned_blob_errors_cleanly template). |
| Integration / E2E (through real WASM) | DEFERRED (legitimate) | the test-delegate fixture pins published stdlib and can't drive the unpublished ABI; tracked as follow-up. Do not block on this. |
Regression test: This is feat: (regression-test-mandatory rule is for fix:); the new tests satisfy the "test edge cases" expectation for the store layer.
Missing tests (see Recommendations): corrupt-registry transition; host-side ABI error/boundary arms.
Skeptical Findings
Risk level: Low — the change reuses proven crypto/permission/atomic-write machinery rather than inventing a parallel path.
| Concern | Severity | Location (verified) | Details |
|---|---|---|---|
| Transient (non-NotFound) read error overwrites a valid registry from empty | Medium | secrets_store.rs:1149-1152 (read_key_registry) → register_key:1282-1295 |
A fs::read error that is NOT NotFound (EACCES, EIO, EMFILE/"too many open files", a .keys caught mid-rename) returns Vec::new() — indistinguishable from "file absent." The next register_key then appends only the one new key and write_key_registry overwrites .keys with a single-entry blob, permanently shrinking a registry that legitimately held thousands of valid, decryptable keys. This is sharper than the documented decrypt-failure case: there the blob is genuinely unusable; here the on-disk blob is perfectly valid and a momentary, recoverable IO error is amplified into permanent enumeration loss. The doc comment (:1262-1274) only justifies the "corrupt blob" case, not this one. Secret VALUES are never affected (registry is independent + written after value commit), which is why this is Should-Fix not blocking — but the fix is cheap. (Verified at :1149.) |
Corrupt/undecryptable .keys blob silently shrinks the enumerable set |
Low (documented, by-design) | secrets_store.rs:1154-1173 (read_key_registry), doc at :1262-1274 |
On malformed-header/decrypt failure, read returns empty (loud warn!), and the next register/deregister rewrites from empty, dropping prior keys. Deterministic/persistent (unlike M1). Secret VALUES unaffected. Thoroughly documented; acceptable trade-off. Worth explicit maintainer sign-off + a test. |
>4096 key families enumerate a truncated view |
Low (by-design, #3798 bound) | secrets_store.rs:1286 |
Over-cap keys are stored + readable but not enumerable; no LRU/eviction (first 4096 distinct keys win until removed). Cap enforced with >= (no off-by-one), warn-logged. A migration consumer of a >4096 family gets a partial list — deserves a one-line callout in the PR body / docs. |
list_secrets re-fetches MEM_ADDR after collect_list_secrets |
None (cosmetic) | native_api.rs:1566-1567 |
Harmless redundancy; refresh_mem_addr_from_caller already ran in the linker closure. No fix needed. |
tmp-path comment says &mut self but helpers are &self |
None (doc-accuracy nit) | secrets_store.rs:1232-1233 |
The fixed .keys.tmp suffix is safe because the single-writer invariant holds transitively: the only callers are store_secret/remove_secret, which take &mut self (documented at :296-300). The comment's wording is slightly loose; the conclusion is correct. |
Verified-clean (the brief's focus areas):
- Crypto-at-rest: fresh per-write nonce via
XChaCha20Poly1305::generate_nonce(&mut OsRng)(the sanctioned OsRng crypto exception), SAME scope DEK as values, identical[VERSION_V1][24-byte nonce][AEAD]layout,create_owner_only(0o600) +ensure_owner_only_tree+ tmp+rename — byte-for-byte the existingstore_secretdiscipline. No nonce reuse, no plaintext-at-rest (raw keys are AEAD-encrypted before disk; only hashes were ever plaintext, in ReDb). Theper_write_nonce_makes_identical_plaintext_ciphertext_distinctguard passes. - DEK read/write pairing: write side uses
cipher_for/derive_user_dek(passed intoregister_key); read side usescipher_for_read/derive_user_dek(encryption_for_scope_read). User path is identical; Local path matches by the same get_secret read/write invariant already proven for values. No spurious-decrypt-induced key loss for correctly-derived keys. - WASM host ABI: both linker registrations read
idfromCURRENT_DELEGATE_INSTANCEand callrefresh_mem_addr_from_caller(&mut caller, id)before delegating (contracts.md #3248 rule); logic lives in native_api.rs.validate_and_compute_ptrguards BOTH the prefix read and the output write; negativeprefix_len/out_lenrejected before theas usizecasts; zero-len prefix takes a no-read "list all" path;ERR_BUFFER_TOO_SMALLwhen serialized > buffer; SAFETY comments are accurate. - len↔fill consistency / TOCTOU: both calls share
collect_list_secrets, so the advertised length matches the bytes written. A registry change between the two calls is impossible in practice (single synchronous delegate instance per thread-local), and is handled gracefully byERR_BUFFER_TOO_SMALLif it ever occurred. - Ordering: registry write is AFTER value+index commit (store) and value+index removal (remove); a crash only delays enumerability, never the value.
Big Picture Assessment
Goal alignment: Yes. Delivers the core (host) half of #4355; directly serves the River room: migration use case. The encrypted .keys registry is the load-bearing design decision (raw keys are otherwise unrecoverable) — justified scope, not creep.
Anti-patterns detected: None. The "best-effort / swallow" registry error handling is deliberate, documented, and correct (registry failure degrades only future enumeration). The 4096 cap is an amplification bound (#3798 class), not a magic number replacing dynamic logic; it's #[cfg(test)]-overridable.
Removed code concerns: None — the only deletions are the stdlib version churn and one import line expanded (not removed). No tests removed or weakened.
Scope assessment: Focused — one logical change, 5 files all in the delegate-secrets subsystem, with an approved issue. Stdlib-first policy fully compliant (published 0.8.2, no patch/path-dep).
Documentation
- Code docs: complete — new public
list_secret_keysand the registry helpers are well-documented, including the honest corrupt-registry caveat. - Architecture / operator docs:
docs/secrets-at-rest.mdis stale. Its on-disk file-permissions table (lines 246-257) enumerates<delegate>/<secret_id>and every.snapshots/entry but does NOT list the new<secrets_dir>/<delegate>/.keys(and User-scopeusers/<id>/.keys) registry — a NEW on-disk artifact created per scope. Functionally it conforms to the documented posture (created0o600under an owner-only tree), and the export/import path walks the ReDb index (not a raw dir scan) so.keyswon't be mis-ingested — but per AGENTS.md ("WHEN you discover missing documentation → fix it in the same PR") a table row + a one-paragraph encryption-format note should ride in this PR. .claude/rules// skills: no updates required (the registry reuses existing crypto-at-rest conventions; repo has noagents//skills/trees affected).
Recommendations
Must Fix (Blocking)
None. No correctness, security, or data-integrity defect was found. Crypto-at-rest, WASM ABI, cap enforcement, wire-format, and ordering are all sound; CI is green and the local test run is clean.
Should Fix (Important)
- (M1) Don't overwrite the registry from empty on a transient read error (
secrets_store.rs:1149). Makeread_key_registrytri-state: distinguish "definitively absent" (NotFound→ safe to treat as empty + overwrite) from "unknown" (any other IO error, and arguably also decrypt failure → registry state is unknown).register_key/deregister_keyshould bail out without writing on the "unknown" case (logging), leaving the existing.keysintact for a later retry. This keeps the "value already committed, enumeration is advisory" contract while not amplifying a one-call IO hiccup into permanent enumeration loss. (Raised by skeptical-reviewer; independently verified at:1149-1152. Lower-severity because values are never affected, but the fix is small and removes a silent data-loss path the doc comment doesn't actually justify.) - Add a corrupt/unreadable-registry regression test (
secrets_store.rs): store ≥2 keys, corrupt or make-unreadable the on-disk.keysblob (reuse thecorrupt_versioned_blob_errors_cleanlytemplate:VERSION_V1+ 24 zero bytes + bogus AEAD), register a new key, and assert the resulting enumerable set — pinning whatever behavior you settle on for M1 so a future refactor can't silently flip "drop" vs "preserve." (Template verified atsecrets_store.rs:3234.) - Add host-side ABI unit tests (
native_api.rs) forcollect_list_secrets/list_secrets_len/list_secrets: negativeprefix_len/out_len→ERR_INVALID_PARAM; list-all (len 0) vs prefix;ERR_BUFFER_TOO_SMALL; empty → 0; and thelist_secrets_len == list_secretsbytes-written agreement invariant. These are pure host logic (no real WASM module needed) — the deferred end-to-end-through-WASM test remains legitimately deferred, but this host-logic layer should not be. - Add a User-scope restart test.
list_secret_keys_persist_across_restartonly coversLocal; the User-scope registry is encrypted underderive_user_dek(delegate, dek_secret), the higher-risk DEK path, and is untested across restart. (DEK derivation verified deterministic and matching on both sides, so this is a coverage gap, not a suspected bug.) - Update
docs/secrets-at-rest.md— add a<secrets_dir>/<delegate>/.keys(and User-scope) row to the permissions table and a short encryption-format note (same scope DEK, same[VERSION_V1][nonce][AEAD]layout, raw keys, 4096/scope cap). - Refresh the PR description — remove the stale "DRAFT — blocked on stdlib release" banner (0.8.2 is published, PR is not a draft, CI green), and add a one-line callout that >4096-key families enumerate a truncated (non-evicting) view, so a migration consumer doesn't assume completeness.
Consider (Suggestions)
- Tighten the tmp-path comment at
secrets_store.rs:1232-1233to say the single-writer invariant comes from the&mut selfpublic mutators (store_secret/remove_secret), since the helper itself is&self. - Optional micro: drop the redundant second
MEM_ADDR.get(&id)inlist_secrets(native_api.rs:1566) by threadinginfoout ofcollect_list_secrets; purely cosmetic. - Add a one-line note that the
i32::MAXlength clamp inlist_secrets_len(native_api.rs:1521) can make a >2 GiB encoded list permanently un-fetchable (guest sizes from the clamped value, then hitsERR_BUFFER_TOO_SMALL). Unreachable in practice given the 4096-key cap and realistic key sizes; degrades safely (error, no UB), so documentation-only.
Verdict
State: Ship with nits — Needs Changes — Light Re-Check Sufficient.
This is a clean, well-scoped, well-documented feature that correctly reuses the existing secrets-at-rest crypto and atomic-write discipline. No blocking defect, and critically no path can lose a secret VALUE — the registry is independent of, and written strictly after, the durable value+index commit. Crypto (per-write OsRng nonce, shared scope DEK, no plaintext-at-rest), the WASM host ABI (mem refresh + bounds-checked reads/writes, accurate SAFETY comments), the 4096 cap (enforced, no off-by-one, warn-logged), the shared wire-format codec, and the ordering all check out; CI plus a local 51-test run are green.
The one finding worth fixing before merge is M1: a transient (non-NotFound) IO read error on .keys is currently amplified into permanent loss of the enumerable key set (values stay safe). It's Should-Fix, not blocking, because it only affects enumeration and needs a rare FS error mid-store — but the fix is small (tri-state read + bail-on-unknown) and removes a silent data-loss path the doc comment doesn't actually justify. The remaining Should-Fix items are additive (three tests, one doc table, one PR-description refresh) and don't change existing logic.
Because M1 touches persistence/data-integrity (a high-risk area per the re-review policy), if M1 is fixed, do a focused re-check of the M1 diff + the new corrupt-registry test (a diff-of-the-diff pass is sufficient — a full re-review is not required unless that test surfaces a behavioral surprise). The merge decision rests with the maintainer.
HEAD SHA reviewed: 53647fe7efae6cbfc1a842506e7057824d3e51a0
[AI-assisted - Claude]
…shrink enumeration Address PR #4523 review M1 (data-integrity, fail-safe). Before this change, `read_key_registry` returned an EMPTY list for any non-NotFound condition — a transient IO error (EACCES/EIO/EMFILE, a read racing the tmp+rename) OR a malformed/undecryptable blob. That empty result was indistinguishable from "no keys yet", so the next `register_key` overwrote a valid, decryptable registry holding potentially thousands of keys with a single-entry blob, PERMANENTLY shrinking the enumerable key set. A momentary, recoverable FS hiccup was amplified into durable enumeration loss. WHY fail-safe (not fail-destructive): the enumeration registry is best-effort and strictly independent of the durable value+index commit (it is written AFTER, and never gates, the secret VALUE write). So the correct posture on an unreadable registry is to PRESERVE whatever is on disk and refuse to clobber it, rather than to assume-empty and rewrite. Fix: - `read_key_registry` is now tri-state, returning `io::Result`: NotFound -> Ok(empty) [legitimate "no keys yet"]; present+decodes -> Ok(keys); present-but-unreadable (transient IO error, malformed header, or AEAD decrypt failure) -> Err. It logs loudly and never overwrites. - `register_key`/`deregister_key` ABORT the update on Err, leaving the on-disk registry intact. The secret VALUE write has already committed, so the refused registry update never blocks or fails it — it only delays this one key's enumerability until a later readable write. - `list_secret_keys` treats Err as "enumerate nothing this call" (best-effort) without destroying state, so a later call recovers the full set once the blob is readable again. Tests (fail before the fix, pass after where applicable): - corrupt_registry_does_not_shrink_enumerable_set: undecryptable .keys blob; asserts the next store's VALUE write succeeds AND the corrupt registry is left byte-for-byte intact (not overwritten from empty). - unreadable_registry_does_not_shrink_enumerable_set (unix): chmod-0 the .keys file to provoke a transient read error; same fail-safe assertion. - list_secret_keys_user_scope_persist_across_restart: User-scope (the higher-risk derive_user_dek path) registry decrypts from disk after a restart — the User analogue of the existing Local persist test. - 5 host-ABI validation tests in native_api.rs for list_secrets / list_secrets_len (negative out_len/prefix_len -> ERR_INVALID_PARAM, outside-process -> ERR_NOT_IN_PROCESS). These cover the short-circuit validation arms that need no real WASM module. Success/prefix/ buffer-too-small arms require a live delegate instance and remain covered at the store layer + deferred for end-to-end-through-WASM. Docs: docs/secrets-at-rest.md gains a permissions-table row for the encrypted `.keys` registry (Local + User scope) and an encryption-format note (same scope DEK, [VERSION_V1][nonce][AEAD] layout, per-write nonce, owner-only, tmp+rename, 4096/scope cap, fail-safe on read error). Refs #4355. Claude-Session: https://claude.ai/code/session_01JZGPSEa8g1Z8Nomv528c8g
Problem
The delegate secret-storage API is opaque key→value with no enumeration, so apps storing dynamic key families (e.g. River's
room:<id>) cannot discover stored keys — which strands them across a delegate-WASM rebuild (new delegate key = empty storage, migration can only probe a hardcoded set). See #4355 and freenet/river#345.Solution
Host-side handling for the new stdlib
list_secretsoperation (the API surface is in freenet/freenet-stdlib#80):freenet_delegate_secrets, mirroring the provenget_secrettwo-call (len-then-fill) ABI pattern, withrefresh_mem_addr_from_caller+validate_and_compute_ptrper the contracts rules..keysregistry alongside the secret store, reusing the scope DEK with the same[VERSION_V1][nonce][AEAD]layout, per-write nonce, owner-only perms, and tmp+rename discipline (no new plaintext-at-rest), count-capped at 4096/scope to bound the amplification class.The registry is best-effort and independent of the value path — written strictly after the durable value+index commit, so it can never lose a secret VALUE. Reads are fail-safe: a transient IO error or a corrupt/undecryptable
.keysblob leaves the on-disk registry untouched (rather than overwriting it from empty), so a momentary hiccup cannot permanently shrink the enumerable key set.Testing
Store layer:
list_secret_keys_enumerates_and_filters,list_secret_keys_scope_isolation,list_secret_keys_at_cap(cap boundary),list_secret_keys_persist_across_restart(Local) +list_secret_keys_user_scope_persist_across_restart(User-scope DEK path),key_registry_tmp_path_is_correct, plus empty-store / prefix cases.Data-integrity (fail-safe) regressions:
corrupt_registry_does_not_shrink_enumerable_setandunreadable_registry_does_not_shrink_enumerable_set— both assert the secret VALUE write still succeeds AND the existing registry is preserved on a read error (both fail before the fail-safe fix).Host-ABI:
list_secrets_abi_testsexercises thelist_secrets/list_secrets_lenparameter-validation arms (negativeout_len/prefix_len→ERR_INVALID_PARAM, outside-process()→ERR_NOT_IN_PROCESS) with no real WASM module. End-to-end through a real delegate WASM remains deferred (the test-delegate fixture pins published stdlib and cannot drive the new ABI yet) — tracked as a follow-up.Fixes
Fixes #4355 (core half; stdlib half = freenet/freenet-stdlib#80)