fix(host-runtime): wire WASM secret-exists to staged credentials (#7307) - #7329
Conversation
Third-party WASM guests (ironhub tools such as attio) gate on the
secret-exists host import before issuing any request, but production
wired the sandbox with the deny-all default, so the probe always
returned false: attio aborted pre-network with "API key not
configured" and the host classified the plain-string guest error as
operation_failed, never auth_required.
Introduce StagedWasmHostSecrets, a per-invocation WasmHostSecrets
implementation over the staged secret injection store: exists(name) is
true exactly when authorization leased and staged non-empty credential
material for (scope, capability_id, handle), read non-destructively so
the HTTP egress still receives the material. Wire it into
WasmRuntimeAdapter::host_for_scope on every host variant and plumb the
shared store through the builder.
Credential staging now rejects empty resolved material as
AuthRequired (obligation handler and host-driven staging), so a
configured-but-blank key surfaces the typed re-auth signal instead of
an opaque guest failure. No prose heuristics: the structured
{"kind":"auth_required"} guest contract remains the fallback.
Adds unit tests for the probe semantics and WASM contract tests with a
secret-exists probe component (staged -> true, absent -> false, empty
material -> AuthRequired staging error).
|
🚅 Deployed to the ironclaw-pr-7329 environment in ironclaw-ci-preview
|
🧭 IronLoop Run · ReviewThis comment updates in place as the Run moves through its stages. 🟩 Final result · Completed
Automatic trigger · attempt 1 of 3 · completed in 6m 9s IronLoop completed the review and posted it to GitHub. 🔗 Result |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (8)
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe host runtime rejects empty credentials as ChangesWASM staged secret visibility
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant HostRuntime
participant WasmRuntimeAdapter
participant WASMTool
participant StagedWasmHostSecrets
participant RuntimeSecretInjectionStore
HostRuntime->>WasmRuntimeAdapter: create host for invocation scope
WasmRuntimeAdapter->>StagedWasmHostSecrets: attach scoped secret view
WASMTool->>StagedWasmHostSecrets: call secret-exists(handle)
StagedWasmHostSecrets->>RuntimeSecretInjectionStore: read staged material
RuntimeSecretInjectionStore-->>StagedWasmHostSecrets: material or lookup error
StagedWasmHostSecrets-->>WASMTool: true or false
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🔍 IronLoop review
🟢 No actionable findings
Reviewed the complete merge-base-to-head diff across all eight changed files. No actionable correctness, security, concurrency, architecture, maintainability, or test-coverage defects were found.
Validation
- ✅ Formatting — cargo fmt --all -- --check completed successfully.
- ✅ WASM secret unit tests — All 6 StagedWasmHostSecrets unit tests passed.
- ✅ WASM secret contract tests — Both staged and absent secret-exists contract tests passed.
- ✅ Empty credential contract test — The empty-material AuthRequired staging contract test passed.
- ✅ Diff integrity — git diff --check reported no whitespace errors.
Review details
- Run:
1e172318-093a-4b37-8908-5dc32475f9a1 - Workflow: Review
- Attempts: 1
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/kernel/ironclaw_host_runtime/src/services/wasm_secrets.rs`:
- Around line 147-165: Extend exists_false_for_other_capability_or_scope by
creating a fresh ResourceScope and asserting that StagedWasmHostSecrets::new
with that scope cannot observe the staged “attio_api_key”. Keep the existing
mismatched-capability and handle assertions unchanged.
In `@crates/kernel/ironclaw_host_runtime/tests/host_runtime_services_contract.rs`:
- Around line 5230-5285: Add a caller-level regression test that exercises
account-backed credential injection through CapabilityObligationHandler::satisfy
or the capability dispatch caller, rather than calling
ProductAuthProviderRuntimePorts::stage_secret_once directly. Configure the
resolved credential material as empty and assert the resulting typed
authentication-required outcome, covering
BuiltinObligationHandler::stage_credential_material through its real call site.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: d07fb70b-e89b-4c88-bf15-c487aca0fe37
📒 Files selected for processing (8)
crates/kernel/ironclaw_host_runtime/src/obligations/handler.rscrates/kernel/ironclaw_host_runtime/src/services.rscrates/kernel/ironclaw_host_runtime/src/services/builder.rscrates/kernel/ironclaw_host_runtime/src/services/runtime_adapters.rscrates/kernel/ironclaw_host_runtime/src/services/wasm_secrets.rscrates/kernel/ironclaw_host_runtime/tests/host_runtime_services_contract.rscrates/kernel/ironclaw_host_runtime/tests/support/host_runtime_harness.rsdocs/reborn/contracts/wasm.md
| fn exists_false_for_other_capability_or_scope() { | ||
| let store = store(); | ||
| let scope = scope(); | ||
| let handle = SecretHandle::new("attio_api_key").unwrap(); | ||
| store | ||
| .insert( | ||
| &scope, | ||
| &capability(), | ||
| &handle, | ||
| ironclaw_secrets::SecretMaterial::from("att-123"), | ||
| ) | ||
| .expect("staging should succeed"); | ||
| let other_capability = CapabilityId::new("other.invoke").unwrap(); | ||
| assert!( | ||
| !StagedWasmHostSecrets::new(Arc::clone(&store), scope.clone(), other_capability) | ||
| .exists("attio_api_key") | ||
| ); | ||
| assert!(!StagedWasmHostSecrets::new(store, scope, capability()).exists("other_secret")); | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Test a mismatched invocation scope.
exists_false_for_other_capability_or_scope does not create another ResourceScope. It tests a different capability and handle only.
Add an assertion with a fresh scope. This verifies that one invocation cannot observe staged credential presence from another invocation.
Proposed test addition
let other_capability = CapabilityId::new("other.invoke").unwrap();
assert!(
!StagedWasmHostSecrets::new(Arc::clone(&store), scope.clone(), other_capability)
.exists("attio_api_key")
);
+ let other_scope = scope();
+ assert!(
+ !StagedWasmHostSecrets::new(Arc::clone(&store), other_scope, capability())
+ .exists("attio_api_key")
+ );
assert!(!StagedWasmHostSecrets::new(store, scope, capability()).exists("other_secret"));As per coding guidelines, credentials must be resolved and injected at the narrowest egress boundary.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fn exists_false_for_other_capability_or_scope() { | |
| let store = store(); | |
| let scope = scope(); | |
| let handle = SecretHandle::new("attio_api_key").unwrap(); | |
| store | |
| .insert( | |
| &scope, | |
| &capability(), | |
| &handle, | |
| ironclaw_secrets::SecretMaterial::from("att-123"), | |
| ) | |
| .expect("staging should succeed"); | |
| let other_capability = CapabilityId::new("other.invoke").unwrap(); | |
| assert!( | |
| !StagedWasmHostSecrets::new(Arc::clone(&store), scope.clone(), other_capability) | |
| .exists("attio_api_key") | |
| ); | |
| assert!(!StagedWasmHostSecrets::new(store, scope, capability()).exists("other_secret")); | |
| } | |
| let other_capability = CapabilityId::new("other.invoke").unwrap(); | |
| assert!( | |
| !StagedWasmHostSecrets::new(Arc::clone(&store), scope.clone(), other_capability) | |
| .exists("attio_api_key") | |
| ); | |
| let other_scope = scope(); | |
| assert!( | |
| !StagedWasmHostSecrets::new(Arc::clone(&store), other_scope, capability()) | |
| .exists("attio_api_key") | |
| ); | |
| assert!(!StagedWasmHostSecrets::new(store, scope, capability()).exists("other_secret")); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/kernel/ironclaw_host_runtime/src/services/wasm_secrets.rs` around
lines 147 - 165, Extend exists_false_for_other_capability_or_scope by creating a
fresh ResourceScope and asserting that StagedWasmHostSecrets::new with that
scope cannot observe the staged “attio_api_key”. Keep the existing
mismatched-capability and handle assertions unchanged.
Source: Coding guidelines
Railway preview QA — BLOCKEDTested head: Given/When/Then matrix
Status derivation
Evidence that did run (supplemental only)
Remaining risks / how to unblock
CleanupNo test data created; no browser tabs opened. Nothing to clean up. |
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
…rai#7307) (nearai#7329) Third-party WASM guests (ironhub tools such as attio) gate on the secret-exists host import before issuing any request, but production wired the sandbox with the deny-all default, so the probe always returned false: attio aborted pre-network with "API key not configured" and the host classified the plain-string guest error as operation_failed, never auth_required. Introduce StagedWasmHostSecrets, a per-invocation WasmHostSecrets implementation over the staged secret injection store: exists(name) is true exactly when authorization leased and staged non-empty credential material for (scope, capability_id, handle), read non-destructively so the HTTP egress still receives the material. Wire it into WasmRuntimeAdapter::host_for_scope on every host variant and plumb the shared store through the builder. Credential staging now rejects empty resolved material as AuthRequired (obligation handler and host-driven staging), so a configured-but-blank key surfaces the typed re-auth signal instead of an opaque guest failure. No prose heuristics: the structured {"kind":"auth_required"} guest contract remains the fallback. Adds unit tests for the probe semantics and WASM contract tests with a secret-exists probe component (staged -> true, absent -> false, empty material -> AuthRequired staging error).
Summary
attiogate on thesecret-existshost import before issuing any request, but production wired the WASM sandbox with the deny-all secrets default — the probe always returnedfalse, so everyattio.invokecall aborted pre-network with "Attio API key not configured" and surfaced as an opaqueoperation_failed/generic_failure, neverauth_required.StagedWasmHostSecrets: a productionWasmHostSecretsimplementation answeringexists(name)from the per-invocation staged secret injection store (true iff authorization leased and staged non-empty material for(scope, capability_id, handle); non-consuming read so HTTP egress still receives the key). Wired per invocation inWasmRuntimeAdapter::host_for_scopeon every host variant; the shared store is plumbed through the builder.AuthRequired(obligation-handler and host-driven staging paths), so a configured-but-blank key surfaces the typed re-auth signal at authorization instead of an opaque guest failure. No prose heuristics; the structured{"kind":"auth_required"}guest contract remains the fallback.secret-existsprobe component (staged →true, absent →false, empty material →AuthRequiredstaging error).Change Type
Linked Issue
Closes #7307
Validation
cargo fmt --all -- --checkcargo clippy --all --benches --tests --examples --all-features -- -D warningscargo buildcargo test -p ironclaw_host_runtime --lib wasm_secrets(6),cargo test -p ironclaw_host_runtime --test host_runtime_services_contract(119),cargo test -p ironclaw_host_runtime --test github_wasm_runtime_contract(55),cargo test -p ironclaw_host_runtime --test builtin_obligation_handler_contract(33),cargo test -p ironclaw_architecture_tests(all suites)cargo test -p <owning-crate> --features integrationif database-backed or runtime-integration behavior changed (the rootintegrationfeature is empty — the flag is per-crate) — Not applicable: no DB-backed behavior changedreview-prorpr-shepherd --fixwas run before requesting reviewTest Strategy
User behavior: A user installing an API-key registry extension (e.g.
attiofrom ironhub) gets working calls when the key is valid, and a typedauth_requiredsignal when the credential is missing or blank, instead of an opaqueoperation_failedwith no actionable path.Risk areas:
operation_failedtoauth_requiredwhen a credential is emptysecret-existsTests added or updated:
services/wasm_secrets.rs(staged non-empty → true; staged empty → false; missing → false; wrong scope/capability → false; malformed handle → false; probes do not consume staged material)tests/host_runtime_services_contract.rsusing a WIT component probingsecret-exists("attio_api_key")(staged →true, absent →false) plus empty-material staging →AuthRequired; fullhost_runtime_services_contract(119),github_wasm_runtime_contract(55),builtin_obligation_handler_contract(33) suites passWhat the tests prove: the
secret-existshost import reflects staged credentials per invocation (previously alwaysfalse), probes never consume the staged material the HTTP egress needs, and empty credential material is rejected at staging as the typedAuthRequiredsignal rather than handed to the guest as an unusable slot.Commands run:
cargo test -p ironclaw_host_runtime --lib wasm_secretscargo test -p ironclaw_host_runtime --test host_runtime_services_contractcargo test -p ironclaw_host_runtime --test github_wasm_runtime_contractcargo test -p ironclaw_host_runtime --test builtin_obligation_handler_contractcargo test -p ironclaw_architecture_testscargo clippy -p ironclaw_host_runtime --all-targetscargo check -p ironclaw_composition