Skip to content

fix(host-runtime): wire WASM secret-exists to staged credentials (#7307) - #7329

Merged
serrrfirat merged 2 commits into
mainfrom
fix-wasm-secret-exists-staged-credentials
Aug 7, 2026
Merged

serrrfirat merged 2 commits into
mainfrom
fix-wasm-secret-exists-staged-credentials

Conversation

@serrrfirat

Copy link
Copy Markdown
Collaborator

Summary

  • Registry (ironhub) WASM tools such as attio gate on the secret-exists host import before issuing any request, but production wired the WASM sandbox with the deny-all secrets default — the probe always returned false, so every attio.invoke call aborted pre-network with "Attio API key not configured" and surfaced as an opaque operation_failed/generic_failure, never auth_required.
  • Add StagedWasmHostSecrets: a production WasmHostSecrets implementation answering exists(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 in WasmRuntimeAdapter::host_for_scope on every host variant; the shared store is plumbed through the builder.
  • Credential staging rejects empty resolved material as 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.
  • Tests: 6 unit tests for probe semantics; 3 WASM contract tests with a secret-exists probe component (staged → true, absent → false, empty material → AuthRequired staging error).

Change Type

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • CI/Infrastructure
  • Security
  • Dependencies

Linked Issue

Closes #7307

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all --benches --tests --examples --all-features -- -D warnings
  • cargo build
  • Relevant tests pass: cargo 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 integration if database-backed or runtime-integration behavior changed (the root integration feature is empty — the flag is per-crate) — Not applicable: no DB-backed behavior changed
  • Manual testing: Not applicable: covered by WASM contract tests with a probe component
  • If a coding agent was used and supports it, review-pr or pr-shepherd --fix was run before requesting review

Test Strategy

User behavior: A user installing an API-key registry extension (e.g. attio from ironhub) gets working calls when the key is valid, and a typed auth_required signal when the credential is missing or blank, instead of an opaque operation_failed with no actionable path.

Risk areas:

  • Model behavior — failure kind surfaced to the model changes from operation_failed to auth_required when a credential is empty
  • Browser
  • Side effect
  • Persistence
  • Security or permissions — the probe is fail-closed and reads staged material non-destructively; no new secret material exposure (exists is boolean only)
  • External provider — registry/ironhub WASM guests that probe secret-exists
  • Cross-component behavior

Tests added or updated:

  • Unit or contract: 6 unit tests in 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)
  • Reborn integration: Not applicable: host-runtime lane change, covered by contract tests
  • Recorded fixture: Not applicable
  • Browser E2E: Not applicable
  • Backend or runtime: 3 WASM contract tests in tests/host_runtime_services_contract.rs using a WIT component probing secret-exists("attio_api_key") (staged → true, absent → false) plus empty-material staging → AuthRequired; full host_runtime_services_contract (119), github_wasm_runtime_contract (55), builtin_obligation_handler_contract (33) suites pass
  • Live canary: Not applicable

What the tests prove: the secret-exists host import reflects staged credentials per invocation (previously always false), probes never consume the staged material the HTTP egress needs, and empty credential material is rejected at staging as the typed AuthRequired signal rather than handed to the guest as an unusable slot.

Commands run:

  • cargo test -p ironclaw_host_runtime --lib wasm_secrets
  • cargo test -p ironclaw_host_runtime --test host_runtime_services_contract
  • cargo test -p ironclaw_host_runtime --test github_wasm_runtime_contract
  • cargo test -p ironclaw_host_runtime --test builtin_obligation_handler_contract
  • cargo test -p ironclaw_architecture_tests
  • cargo clippy -p ironclaw_host_runtime --all-targets
  • cargo check -p ironclaw_composition

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).
@railway-app

railway-app Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-7329 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Aug 7, 2026 at 11:35 am

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7329 August 7, 2026 10:08 Destroyed
@github-actions github-actions Bot added scope: docs Documentation size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Aug 7, 2026
@ironloopai

ironloopai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
Contributor

🧭 IronLoop Run · Review

This comment updates in place as the Run moves through its stages.

🟩 Final result · Completed

🟨 Queued → 🟦 Working → 🟦 Posting results → 🟩 Completed

Automatic trigger · attempt 1 of 3 · completed in 6m 9s

IronLoop completed the review and posted it to GitHub.

🔗 Result

Open submitted review →

Run details

Run: 1e172318-093a-4b37-8908-5dc32475f9a1
Base: main at 2884543
Head: fix-wasm-secret-exists-staged-credentials at eb38f69
Created: 2026-08-07 10:09 UTC
Updated: 2026-08-07 10:15 UTC

@coderabbitai

coderabbitai Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 55e73682-ee70-40ea-8408-4c9b372c266d

📥 Commits

Reviewing files that changed from the base of the PR and between 389aa99 and 95ce8bb.

📒 Files selected for processing (8)
  • crates/kernel/ironclaw_host_runtime/src/obligations/handler.rs
  • crates/kernel/ironclaw_host_runtime/src/services.rs
  • crates/kernel/ironclaw_host_runtime/src/services/builder.rs
  • crates/kernel/ironclaw_host_runtime/src/services/runtime_adapters.rs
  • crates/kernel/ironclaw_host_runtime/src/services/wasm_secrets.rs
  • crates/kernel/ironclaw_host_runtime/tests/host_runtime_services_contract.rs
  • crates/kernel/ironclaw_host_runtime/tests/support/host_runtime_harness.rs
  • docs/reborn/contracts/wasm.md

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added secure, per-invocation credential availability checks for WASM tools.
    • Authorized tools can verify whether a non-empty credential is available without consuming it.
  • Bug Fixes
    • Empty, missing, malformed, or unauthorized credentials are no longer staged as usable secrets.
    • Credential requests now correctly report when authentication is required.
  • Documentation
    • Documented credential availability rules, authorization scope, and non-consuming secret checks.
  • Tests
    • Added coverage for valid, absent, empty, malformed, and unauthorized credentials.

Walkthrough

The host runtime rejects empty credentials as AuthRequired and exposes authorized, non-empty staged credentials through the WASM secret-exists host capability. Runtime wiring, fixtures, tests, and contract documentation cover this behavior.

Changes

WASM staged secret visibility

Layer / File(s) Summary
Credential staging validation
crates/kernel/ironclaw_host_runtime/src/obligations/handler.rs, crates/kernel/ironclaw_host_runtime/src/services.rs
Credential staging checks resolved material and returns AuthRequired for empty values.
Runtime secret wiring
crates/kernel/ironclaw_host_runtime/src/services/builder.rs, crates/kernel/ironclaw_host_runtime/src/services/runtime_adapters.rs, crates/kernel/ironclaw_host_runtime/src/services.rs
WasmRuntimeAdapter receives the injection store and attaches scoped staged secret views to generated hosts.
Secret probe contract and validation
crates/kernel/ironclaw_host_runtime/src/services/wasm_secrets.rs, crates/kernel/ironclaw_host_runtime/tests/support/host_runtime_harness.rs, crates/kernel/ironclaw_host_runtime/tests/host_runtime_services_contract.rs, docs/reborn/contracts/wasm.md
StagedWasmHostSecrets performs fail-closed, non-consuming lookups. Tests and documentation cover authorization, missing values, empty values, malformed handles, and repeated reads.

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
Loading

Possibly related PRs

Suggested reviewers: benkurrek

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the implementation and validation, but omits required security, trust-boundary, blast-radius, rollback, follow-through, and review-track sections. Complete the required template sections, especially security and trust-boundary impact, blast radius, rollback plan, review follow-through, and review track.
Linked Issues check ⚠️ Warning The PR fixes missing and blank staged credentials [#7307], but provides no handling or distinguishing detail for invalid or expired credentials required by the issue. Add typed auth_required handling or reliable distinguishing error details for invalid and expired credentials, then add contract tests for those cases.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits format and accurately describes wiring WASM secret-exists to staged credentials.
Out of Scope Changes check ✅ Passed All code, tests, and documentation changes directly support staged WASM secret checks and typed AuthRequired staging behavior for [#7307].

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 2884543 and eb38f69.

📒 Files selected for processing (8)
  • crates/kernel/ironclaw_host_runtime/src/obligations/handler.rs
  • crates/kernel/ironclaw_host_runtime/src/services.rs
  • crates/kernel/ironclaw_host_runtime/src/services/builder.rs
  • crates/kernel/ironclaw_host_runtime/src/services/runtime_adapters.rs
  • crates/kernel/ironclaw_host_runtime/src/services/wasm_secrets.rs
  • crates/kernel/ironclaw_host_runtime/tests/host_runtime_services_contract.rs
  • crates/kernel/ironclaw_host_runtime/tests/support/host_runtime_harness.rs
  • docs/reborn/contracts/wasm.md

Comment on lines +147 to +165
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"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

Suggested change
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

@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Railway preview QA — BLOCKED

Tested head: eb38f69daf587af0b46ac9611052765adb10af6d · Railway state: success · Preview: https://ironclaw-ironclaw-pr-7329.up.railway.app · Route required: /chat (capability invocation)

Given/When/Then matrix

Case Acceptance Intended contract Actual contract Result
Valid Attio API key → attio.invoke (e.g. list_records) returns data Required Guest secret-exists("attio_api_key") returns true, request egresses with injected Authorization: Bearer <key>, API data returned Not exercised — no Attio workspace API key available in this run; no browser driver in harness to drive the model flow Not executed → BLOCKED
Missing/blank Attio credential → typed auth_required (not operation_failed) Required Install/configure attio without a usable key, invoke → model-visible failure kind is auth_required; run parks and surfaces re-auth Not exercised — requires model-driven builtin.extension_install flow in the preview plus preview bearer token; neither browser driver nor bearer token available in this run Not executed → BLOCKED
Deployment health Supplemental Preview serves the app shell HTTP 200 on / and /healthz, app shell served Pass

Status derivation

  • Required cases: 0 passed, 0 failed, 2 not executed.
  • Mandatory status gate: any required case not executed → BLOCKED (cannot be upgraded by supplemental evidence).
  • Blocker causes: (1) no browser automation capability in this harness (no browser tool; no browser-capable subagent), (2) preview bearer token not supplied, (3) a live Attio workspace API key is required for the success path and blank-key scenario needs the real install flow.

Evidence that did run (supplemental only)

  • Railway deployment for the exact head is live (success, asset assets/app-BiwQJlad.js; backend-only change, asset baseline comparison not applicable).
  • The changed contract is covered at the runtime layer by this PR's tests: 6 unit tests in services/wasm_secrets.rs (staged non-empty → exists=true; empty → false; missing → false; wrong scope/capability → false; malformed handle → false; probes don't consume staged material) and 3 contract tests in tests/host_runtime_services_contract.rs (WIT component probing secret-exists("attio_api_key") → true with staged credential, false without; empty material staging → AuthRequired). These pass locally but are not browser acceptance.

Remaining risks / how to unblock

  • Provide a browser driver (Playwright/CDP) + preview bearer token + one valid Attio workspace API key, and this run can execute both required cases against the deployed head.
  • Residual risk if merged without browser QA: none specific to the browser surface (change is host-runtime backend; WebUI unaffected); the failure-kind change (operation_failed → auth_required) is verified at the runtime/contract layer.

Cleanup

No test data created; no browser tabs opened. Nothing to clean up.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-7329 August 7, 2026 11:28 Destroyed
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

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.

@serrrfirat
serrrfirat added this pull request to the merge queue Aug 7, 2026
Merged via the queue into main with commit ce2d6f8 Aug 7, 2026
43 checks passed
@serrrfirat
serrrfirat deleted the fix-wasm-secret-exists-staged-credentials branch August 7, 2026 13:09
@serrrfirat serrrfirat mentioned this pull request Aug 10, 2026
21 of 29 tasks
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026
…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).

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-7329 — 95ce8bb3 Deployed Aug 7, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: docs Documentation size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Attio extension calls fail with opaque operation_failed instead of auth_required

2 participants