Skip to content

fix(reborn): auto-activate web-access and Brave-backed web_search so agents discover real web search - #6232

Open
pranavraja99 wants to merge 6 commits into
mainfrom
fix/reborn-web-access-default-active
Open

pranavraja99 wants to merge 6 commits into
mainfrom
fix/reborn-web-access-default-active

Conversation

@pranavraja99

@pranavraja99 pranavraja99 commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

reborn treats every first-party integration (Gmail, Slack, GitHub, web-access, ...) as an opt-in extension-catalog entry. extension_search/extension_install/extension_activate are always in the model's tool list (they're in the hardcoded CORE_TOOL_NAMES progressive-disclosure allowlist — this is not a tool-disclosure gap), but nothing in the base prompt/toolset ever points the model at that catalog, so it never has a reason to go looking for web-access (or web_search, added below). Any production reborn agent asked a question needing current information silently falls back to whatever raw tools it already has (e.g. http) with no way to discover a URL.

web-access is already a zero-config, trust-policy-approved first-party extension (Exa-MCP-backed search + get_content, no credentials needed). This PR auto-installs and activates it once at runtime-composition time, the same way bootstrap_nearai_mcp already auto-activates the nearai extension when NEAR AI credentials are configured — just simpler, since there's no credential-submit step or durable-storage gate to worry about.

  • web_access_bootstrap::bootstrap_web_access (new): checks the current lifecycle phase for the runtime's tenant-operator identity, installs (if Discovered) then activates (if not already Active) — and respects an explicit prior Disabled/Removed choice rather than overriding it.
  • Called once from build_local_runtime, immediately after the existing bootstrap_nearai_mcp call (same call site, same pattern).

Update: real, credentialed Brave Search (web_search), gated on BRAVE_API_KEY

web-access's Exa MCP backend is zero-config but rate-shared/free-tier. web_search is a separate, already-registry-published, fully-built first-party WASM tool that calls the real Brave Search API — it just had no auto-activation path either, so an operator who set BRAVE_API_KEY never actually got it used.

  • web_search_bootstrap::bootstrap_web_search_brave (new): same install-then-activate pattern as web_access_bootstrap, but gated on a real credential — only runs if BRAVE_API_KEY is set (web_search_bootstrap_api_key_from_env returns None for unset/empty, and the bootstrap short-circuits to NotConfigured with no install/activate at all in that case).
  • web-access and web_search share the same model-facing web_search display name (see ironclaw_runner::tool_disclosure), so only one may be active — build_local_runtime tries the Brave bootstrap first and only falls back to bootstrap_web_access (Exa) when WebSearchBootstrapOutcome::leaves_web_access_available() is true (i.e. no key configured).
  • The brave_api_key secret is provisioned under the tenant-shared scope (owner_scope.tenant_shared_managed_scope()), not the bootstrap caller's own (tenant, user) — the dispatch-time credential pre-flight (secret_owner_scope) only ever checks the real invocation caller's own scope, then falls back to the tenant-shared scope, never an arbitrary third scope. Provisioning anywhere else left the secret invisible to every real capability invocation (silent AuthRequired regardless of who called it).
  • WasmHostSecrets implementation (new WasmRuntimeSecretsAdapter in ironclaw_host_runtime): a WASM guest's own secret-exists/secret-get host calls (e.g. web_search's own near::agent::host::secret_exists pre-flight before attempting its Brave HTTP call) are a separate mechanism from the existing WasmRuntimeCredentialProvider (which only injects credentials into the tool's own outbound HTTP headers). DenyWasmHostSecrets (unconditional false) was the only implementation anywhere in the codebase, so web_search could never proceed past its own credential check even once the secret was genuinely provisioned and visible everywhere else. Fixed by resolving via the same secret_owner_scope helper that already backs the other two paths, swapped into host_for_scope per-invocation alongside the existing HTTP adapter swap.

Verification (Brave update)

Verified end-to-end against a real 165-task GAIA benchmark run (via nearai/nearai-bench), not just unit tests:

  • 1,405 real web_search.search calls across the run, 99.9% succeeded (1,404/1,405) — genuine Brave Search API responses (arxiv, USGS, Nature, MapQuest, etc.), not stubs or errors.
  • Confirmed BRAVE_API_KEY unset → falls back to web-access/Exa correctly (existing behavior preserved, regression-tested).
  • Full ironclaw_reborn_composition test suite passes (1387 unit tests + all integration binaries), cargo check --workspace --all-targets, cargo clippy -- -D warnings clean, cargo fmt --check clean.

Verification (original web-access/Exa fix)

Verified end-to-end against a real benchmark (GAIA, via nearai/nearai-bench) rather than just unit tests:

  • Before: a task needing a real web lookup scored 0 — the agent had only blind http with no way to find a URL to fetch.
  • After: the same task's trace shows web-access.search → web-access.get_content calls, and it passes.

Full ironclaw_reborn_composition test suite passes (1562 unit tests + all integration binaries), plus cargo check --workspace.

Two existing tests needed updates since they assumed web-access starts un-installed:

  • local_dev_web_access_installs_activates_and_dispatches_through_host_runtime → renamed ..._is_auto_installed_activated_and_dispatches_through_host_runtime; now asserts the auto-bootstrap guarantee directly instead of manually installing/activating first.
  • local_dev_extension_lifecycle_tools_manage_visible_extension_surface (uses web-access as its example for the generic install/activate/remove capability-handler round trip): now asserts the already-active starting state, then does a remove → reinstall → reactivate round trip (still exercises all four lifecycle capability handlers), using the real tenant-operator identity for the ownership-sensitive remove/reinstall calls (the auto-bootstrapped install is Tenant-owned, not owned by the generic test-fixture caller the file's other tests use).

Test plan

  • cargo test -p ironclaw_reborn_composition --features test-support — 1387 passed, 0 failed
  • cargo test -p ironclaw_host_runtime — all passed
  • cargo check --workspace --all-targets
  • cargo clippy -p ironclaw_reborn_composition -p ironclaw_host_runtime -p ironclaw_first_party_extensions --features test-support --tests -- -D warnings — clean
  • cargo fmt --check
  • End-to-end verification via GAIA benchmark tasks (0 → 1 score flips for both web-access and web_search paths; live 165-task run confirms Brave search working at scale)

Known follow-up: this branch is currently based on an older main snapshot and shows as conflicting against current main — needs a rebase before merge (tracked separately; the underlying architecture moved the extension-package catalog to a new self-contained per-package module system in ironclaw_first_party_extensions::packages, which this PR's web_search package will need to move into as well).

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 18, 2026 06:02
@ironloopai

ironloopai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: a395ca9402fa408c459e9f16eb16e817b47da01c
Result: One or more review results were superseded by a newer PR head.
Next: Run @ironloopai review on the latest PR head.
Updated: 2026-07-24T06:18:20.084Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Superseded N/A N/A 2026-07-23T00:58:40.511Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Superseded by a newer PR head. New head: bcf92cf. Previous verdict: Changes requested.
Recent activity
Time Reviewer State Detail
2026-07-18T06:02:04.766Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head 05bd583.
2026-07-18T06:02:04.766Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-18T06:02:05.767Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-18T06:02:08.359Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at b37dd1a.
2026-07-18T06:07:10.396Z ironloop/common-reviewer (reviewer) Result captured Changes requested; 2 blocking findings.
2026-07-18T06:07:10.396Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
2026-07-23T00:58:40.511Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (bcf92cf).
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6232 July 18, 2026 06:02 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added built-in web search with configurable queries, filters, and structured results.
    • Web access is now automatically installed and activated during local setup.
    • Web search activates automatically when a Brave API key is configured.
    • Credentials are securely provisioned for web search requests.
  • Bug Fixes
    • Preserved explicitly removed or disabled extensions during automatic setup.
    • Improved extension lifecycle handling and capability availability after installation changes.
  • Documentation
    • Added guidance for web search inputs, results, and usage limitations.

Walkthrough

Local runtime construction now bundles and bootstraps web_search and web-access, provisions scoped Brave credentials, and supplies secret access to WASM guests. Tests cover automatic activation and tenant-operator lifecycle operations.

Changes

First-party extension bootstrap

Layer / File(s) Summary
Web-search extension contracts
crates/ironclaw_first_party_extensions/assets/web-search/*, crates/ironclaw_reborn_composition/src/extension_host/available_extensions.rs
Adds the web_search.search manifest, schemas, prompt, embedded assets, and catalog registration.
Scoped WASM secret access
crates/ironclaw_host_runtime/src/services.rs, crates/ironclaw_host_runtime/src/services/builder.rs, crates/ironclaw_host_runtime/src/services/runtime_adapters.rs
Passes the secret store into WasmRuntimeAdapter and resolves guest secret handles using the invocation owner scope.
Bootstrap flow and runtime wiring
crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs, crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs, crates/ironclaw_reborn_composition/src/factory.rs
Adds lifecycle-aware web-search and web-access installation and static activation, including Brave secret provisioning and preservation of explicit removed or disabled states.
Lifecycle validation
crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs, crates/ironclaw_reborn_composition/src/factory.rs
Updates tests for construction-time activation and tenant-operator remove, reinstall, reactivation, scope, secret, and invocation behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant build_local_runtime
  participant bootstrap_web_search_brave
  participant AdminSecretProvisioner
  participant RebornLocalExtensionManagementPort
  participant bootstrap_web_access
  build_local_runtime->>bootstrap_web_search_brave: bootstrap web_search
  bootstrap_web_search_brave->>AdminSecretProvisioner: provision brave_api_key
  bootstrap_web_search_brave->>RebornLocalExtensionManagementPort: install and activate web_search
  build_local_runtime->>bootstrap_web_access: bootstrap web-access when available
  bootstrap_web_access->>RebornLocalExtensionManagementPort: inspect, install, and activate web-access
Loading

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#6279: Also modifies build_local_runtime and its runtime-construction inputs.
  • nearai/ironclaw#6520: Provides the extension lifecycle readiness model used by these install, activation, and capability-set tests.

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has strong summary and testing detail, but it omits required template sections like Change Type, Linked Issue, Security Impact, and Rollback Plan. Complete the missing template sections, especially Change Type, Linked Issue, Validation, Security Impact, Trust-Boundary Checklist, Database Impact, Blast Radius, and Rollback Plan.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits style and matches the PR’s main change set.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/reborn-web-access-default-active

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces auto-activation for the zero-config 'web-access' extension during runtime composition, eliminating the need for manual discovery. It adds a new 'web_access_bootstrap' module, integrates it into the local runtime build process, and updates the test suite to reflect this auto-bootstrapped behavior. Feedback on the changes suggests avoiding an unnecessary clone of the 'UserId' string in the bootstrap module by passing it as a reference.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +72 to +126
let caller: UserId = extension_management.tenant_operator_user_id().clone();
let package_ref =
LifecyclePackageRef::new(LifecyclePackageKind::Extension, WEB_ACCESS_EXTENSION_ID)
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web-access package ref is invalid: {error}"),
})?;

let phase = extension_management
.project(package_ref.clone(), &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web-access extension projection failed: {error}"),
})?
.phase;

match phase {
LifecyclePhase::Discovered | LifecyclePhase::Installed => {}
LifecyclePhase::Active => return Ok(WebAccessBootstrapOutcome::AlreadyActive),
LifecyclePhase::Removed => {
tracing::debug!(
"web-access was explicitly removed; preserving that state rather than \
re-installing it"
);
return Ok(WebAccessBootstrapOutcome::SkippedPreservedRemoved);
}
LifecyclePhase::Disabled => {
tracing::debug!(
"web-access is explicitly disabled; preserving that state rather than \
re-activating it"
);
return Ok(WebAccessBootstrapOutcome::SkippedDisabled);
}
other => {
tracing::debug!(
phase = ?other,
"web-access is not in an auto-activatable phase; skipping bootstrap"
);
return Ok(WebAccessBootstrapOutcome::SkippedNonActivatable);
}
}

if phase == LifecyclePhase::Discovered {
extension_management
.install(package_ref.clone(), &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web-access extension install failed: {error}"),
})?;
}
extension_management
.activate(package_ref, ExtensionActivationMode::Static, &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web-access extension activation failed: {error}"),
})?;

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.

medium

The caller identity (UserId) is cloned unnecessarily. Since project, install, and activate all accept &UserId, we can pass a reference directly and avoid cloning the heap-allocated UserId string.

    let caller = extension_management.tenant_operator_user_id();
    let package_ref =
        LifecyclePackageRef::new(LifecyclePackageKind::Extension, WEB_ACCESS_EXTENSION_ID)
            .map_err(|error| RebornBuildError::InvalidConfig {
                reason: format!("web-access package ref is invalid: {error}"),
            })?;

    let phase = extension_management
        .project(package_ref.clone(), caller)
        .await
        .map_err(|error| RebornBuildError::InvalidConfig {
            reason: format!("web-access extension projection failed: {error}"),
        })?
        .phase;

    match phase {
        LifecyclePhase::Discovered | LifecyclePhase::Installed => {}
        LifecyclePhase::Active => return Ok(WebAccessBootstrapOutcome::AlreadyActive),
        LifecyclePhase::Removed => {
            tracing::debug!(
                "web-access was explicitly removed; preserving that state rather than \
                 re-installing it"
            );
            return Ok(WebAccessBootstrapOutcome::SkippedPreservedRemoved);
        }
        LifecyclePhase::Disabled => {
            tracing::debug!(
                "web-access is explicitly disabled; preserving that state rather than \
                 re-activating it"
            );
            return Ok(WebAccessBootstrapOutcome::SkippedDisabled);
        }
        other => {
            tracing::debug!(
                phase = ?other,
                "web-access is not in an auto-activatable phase; skipping bootstrap"
            );
            return Ok(WebAccessBootstrapOutcome::SkippedNonActivatable);
        }
    }

    if phase == LifecyclePhase::Discovered {
        extension_management
            .install(package_ref.clone(), caller)
            .await
            .map_err(|error| RebornBuildError::InvalidConfig {
                reason: format!("web-access extension install failed: {error}"),
            })?;
    }
    extension_management
        .activate(package_ref, ExtensionActivationMode::Static, caller)
        .await
        .map_err(|error| RebornBuildError::InvalidConfig {
            reason: format!("web-access extension activation failed: {error}"),
        })?;

@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: 1

🤖 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/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs`:
- Around line 79-120: Replace the caller-masked project preflight in web_access
bootstrap with a host-only lifecycle check that distinguishes never-installed,
removal-tombstone, disabled, and privately owned states without changing
ownership; only auto-install or activate eligible states and preserve removed,
disabled, and private installations. In
crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs
lines 79-120, update the phase/installation flow accordingly. In
crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs
lines 606-695, add production-caller rebuild tests proving removal stays
removed, disabled stays disabled, and a member-private installation is neither
activated nor promoted.
🪄 Autofix (Beta)

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: 1054c3e8-6ecd-4b79-89dd-be0a9b21b624

📥 Commits

Reviewing files that changed from the base of the PR and between ba31ee1 and 05bd583.

📒 Files selected for processing (5)
  • crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle.rs
  • crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs
  • crates/ironclaw_reborn_composition/src/extension_host/mod.rs
  • crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs
  • crates/ironclaw_reborn_composition/src/factory.rs

Comment on lines +79 to +120
let phase = extension_management
.project(package_ref.clone(), &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web-access extension projection failed: {error}"),
})?
.phase;

match phase {
LifecyclePhase::Discovered | LifecyclePhase::Installed => {}
LifecyclePhase::Active => return Ok(WebAccessBootstrapOutcome::AlreadyActive),
LifecyclePhase::Removed => {
tracing::debug!(
"web-access was explicitly removed; preserving that state rather than \
re-installing it"
);
return Ok(WebAccessBootstrapOutcome::SkippedPreservedRemoved);
}
LifecyclePhase::Disabled => {
tracing::debug!(
"web-access is explicitly disabled; preserving that state rather than \
re-activating it"
);
return Ok(WebAccessBootstrapOutcome::SkippedDisabled);
}
other => {
tracing::debug!(
phase = ?other,
"web-access is not in an auto-activatable phase; skipping bootstrap"
);
return Ok(WebAccessBootstrapOutcome::SkippedNonActivatable);
}
}

if phase == LifecyclePhase::Discovered {
extension_management
.install(package_ref.clone(), &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web-access extension install failed: {error}"),
})?;
}

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 | 🟠 Major | 🏗️ Heavy lift

Do not bootstrap from the caller-masked projection.

project() reports Discovered both after removal (the installation and manifest are deleted) and for a private installation hidden from the operator. The bootstrap therefore resurrects explicitly removed Web Access on restart and can convert another member’s private installation into a tenant-shared installation through install().

  • crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs#L79-L120: use a host-only lifecycle preflight that distinguishes never-installed, removal tombstone, disabled, and private-owned states without changing ownership.
  • crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs#L606-L695: add caller-level rebuild tests proving removal remains removed, disabled remains disabled, and a member-private install is neither activated nor promoted.

As per coding guidelines, lifecycle states must remain distinct, restart reconstruction must revalidate ownership, and removal/restart behavior must be tested through the production caller.

📍 Affects 2 files
  • crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs#L79-L120 (this comment)
  • crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs#L606-L695
🤖 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/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs`
around lines 79 - 120, Replace the caller-masked project preflight in web_access
bootstrap with a host-only lifecycle check that distinguishes never-installed,
removal-tombstone, disabled, and privately owned states without changing
ownership; only auto-install or activate eligible states and preserve removed,
disabled, and private installations. In
crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs
lines 79-120, update the phase/installation flow accordingly. In
crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs
lines 606-695, add production-caller rebuild tests proving removal stays
removed, disabled stays disabled, and a member-private installation is neither
activated nor promoted.

Source: Coding guidelines

@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: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
❌ Changes requested 2 0 2 05bd583dfcd8

Head: 05bd583dfcd8dce4fff19c400c9ed60b0e09c615
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Changes requested: the bootstrap cannot distinguish a truly absent extension from a removal or masked private installation, so it can override opt-outs and widen private visibility on restart.

Findings

Blocking: 2 / Notes: 0

Blocking findings

1. ❌ [HIGH] Preserve removal across restarts

Location: crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs:113-115
A successful removal deletes the installation (and then its manifest), so the next startup's project returns Discovered. This branch consequently installs and activates web-access again. An operator who explicitly removed web access will therefore have its network capability restored after restart, contrary to the promised Removed preservation. Persist/query an explicit removal tombstone (or otherwise distinguish it from absence) and add a remove → restart regression test.

2. ❌ [MEDIUM] Do not promote masked private installs at boot

Location: crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs:79-80
project deliberately masks a foreign private installation as Discovered. Bootstrap then treats that result as absent and calls install as the tenant operator; the install policy converts an existing InstallationOwner::Users row to Tenant. Thus a restart silently turns a member's private web-access installation into a tenant-wide tool, despite the SkippedNonActivatable comment describing the opposite behavior. Use host-visible state that can distinguish masked private ownership from absence, and skip rather than promote it; cover this restart scenario in tests.

Developer follow-up

After fixing this feedback:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

}
}

if phase == LifecyclePhase::Discovered {

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.

Removal deletes the installation and manifest, so the next startup projects this as Discovered and this branch reinstalls it. That overrides an explicit remove after restart; preserve a durable opt-out/tombstone and test remove → restart.

reason: format!("web-access package ref is invalid: {error}"),
})?;

let phase = extension_management

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.

A foreign private installation is intentionally masked by project as Discovered. The subsequent operator install promotes that existing private row to tenant ownership, so a restart widens a user's private tool to all users. Distinguish this state from true absence and skip it.

@railway-app

railway-app Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 24, 2026 at 6:29 am

pranavraja99 and others added 5 commits July 20, 2026 10:21
reborn treats every first-party integration (Gmail, Slack, GitHub,
web-access, ...) as an opt-in extension-catalog entry. `extension_search`/
`extension_install`/`extension_activate` are always in the model's tool
list (hardcoded CORE_TOOL_NAMES in the progressive-disclosure catalog —
this is not a tool-disclosure gap), but nothing in the base prompt ever
points the model at that catalog, so it never has a reason to go looking
for `web-access`. Any production reborn agent asked a question needing
current information silently falls back to whatever raw tools it already
has (e.g. `http`) with no way to find a URL.

`web-access` is already a zero-config, trust-policy-approved first-party
extension (Exa-MCP-backed search + get_content, no credentials). Add
`web_access_bootstrap::bootstrap_web_access`, called once from
`build_local_runtime` right after `bootstrap_nearai_mcp` (same call site,
same install-then-activate-against-current-phase pattern, but simpler:
no credential-submit step and no durable-storage gate, since there's
nothing to configure). Respects an explicit prior Disabled/Removed choice
rather than overriding it.

Verified end-to-end against a GAIA benchmark run (nearai/nearai-bench):
before this change, tasks needing a real web lookup scored 0 (blind
`http`, no way to discover a search capability); after, the same tasks
show `web-access.search`/`web-access.get_content` in the trace and pass.

Updated two tests whose assumptions the new default-active behavior
changes: `local_dev_web_access_installs_activates_and_dispatches_through_host_runtime`
no longer needs to install/activate manually (asserts the auto-bootstrap
guarantee directly), and
`local_dev_extension_lifecycle_tools_manage_visible_extension_surface`
(which uses web-access as its example for the generic install/activate/
remove capability-handler round trip) now asserts the already-active
starting state before doing remove-then-reinstall, using the real
tenant-operator identity for the ownership-sensitive calls.
…figured

reborn's own registry already ships a complete, versioned web_search WASM
tool (registry/tools/web_search.json, tagged "default") that calls the real
Brave Search API — but it was never wired into ironclaw_reborn_composition's
extension catalog at all (from_first_party_assets_with_nearai_mcp_config had
no web_search_package()), so it was unreachable from reborn regardless of
whether an operator configured BRAVE_API_KEY.

Vendor the tool's assets (manifest.toml + input/output schemas + prompt doc +
the prebuilt wasm32-wasip2 binary, sha256-verified against the registry's
declared hash) under ironclaw_first_party_extensions/assets/web-search/,
matching the existing bundled-extension convention (github, slack, google-*),
and register web_search_package() in the catalog.

Add web_search_bootstrap::bootstrap_web_search_brave, mirroring
web_access_bootstrap's install-then-activate pattern but gated on a real
credential (BRAVE_API_KEY) the way bootstrap_nearai_mcp is: seeds the
brave_api_key secret via the existing AdminSecretProvisioner when configured,
then installs/activates web_search for the tenant-operator identity unless
the user already explicitly disabled or removed it.

web_search and web-access share the same model-facing "web_search" display
name (see ironclaw_runner::tool_disclosure's capability-id aliasing), so only
one may be active at a time without a name collision. build_local_runtime now
prefers Brave whenever BRAVE_API_KEY is configured — a real, quota'd backend
beats Exa's zero-config but rate-shared free MCP tier — and falls back to
bootstrap_web_access's existing zero-config Exa path otherwise, preserving
prior behavior for anyone without a Brave key.

The manifest's runtime_credentials entry omits `source` (defaults to
RuntimeCredentialRequirementSource::SecretHandle), which
package_runtime_credential_auth_requirements does not surface as an OAuth
requirement — so activation does not block on it, matching how github/gmail/
etc.'s OAuth-gated credentials differ from a bare secret-store lease. The
actual credential injection into the outbound X-Subscription-Token header
happens at HTTP-egress time via the manifest's declared audience + target,
already-existing host-runtime machinery this change doesn't touch.

Also rebases the earlier web-access auto-activation commit onto current main
and fixes two unrelated call sites in extension_lifecycle_capabilities.rs
that had drifted against approval_test_support::invoke_json_with_local_dev_approval's
current (4-arg, no TrustDecision) signature, plus reborn_runner.rs's rename
of RebornRuntimeProfileOptions on the benchmarks side.

Verified: cargo test -p ironclaw_reborn_composition --features test-support
--lib -> 1643 passed, 0 failed. cargo clippy -p ironclaw_reborn_composition
--features test-support --tests -- -D warnings -> clean. cargo fmt --check
clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
secret_owner_scope (the dispatch-time credential pre-flight) only ever
checks the invocation caller's own (tenant, user) scope, then falls
back to tenant_shared_managed_scope() -- never an arbitrary third
scope. Provisioning the bootstrapped brave_api_key secret under the
bootstrap caller's own owner_scope left it invisible to every real
capability invocation, which kept surfacing AuthRequired instead of
ever calling Brave. Provision under owner_scope.tenant_shared_managed_scope()
instead, matching every other secret-store write in this crate.

Also threads a real RebornLocalRuntimeIdentity through the regression
test (matching ExecutionContext::local_default's tenant/agent) and adds
an end-to-end invocation assertion that the credential gate itself
clears, bounded by a short timeout so a slow/failing egress attempt
can't turn the test into a multi-minute hang.
DenyWasmHostSecrets (unconditional false) was the only implementation
of WasmHostSecrets anywhere in the codebase, so any WASM tool that
pre-flights its own credential via near::agent::host::secret_exists
before attempting a call (as the web_search Brave tool does) could
never proceed, even once the secret was genuinely provisioned and
visible to the separate WasmRuntimeCredentialProvider / dispatch-time
AuthRequired pre-flight paths.

Add WasmRuntimeSecretsAdapter, resolved via the same secret_owner_scope
helper (own scope, falling back to tenant-shared managed scope) that
already backs those other two paths, and swap it into host_for_scope
per-invocation alongside the existing HTTP adapter swap. Thread a new
secret_store: Option<Arc<dyn SecretStore>> through WasmRuntimeAdapter::
new/try_new from the one real call site (services/builder.rs).

WasmHostSecrets::exists is a synchronous guest-facing call; bridged to
the async secret store via Handle::current().block_on(...), safe here
because WASM guest execution runs inside its own spawn_blocking
context (see wasm_execution.rs), not a normal async worker thread.
# Conflicts:
#	crates/ironclaw_host_runtime/src/services/runtime_adapters.rs
#	crates/ironclaw_reborn_composition/src/extension_host/available_extensions.rs
#	crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle.rs
#	crates/ironclaw_reborn_composition/src/factory.rs
@pranavraja99
pranavraja99 force-pushed the fix/reborn-web-access-default-active branch from 05bd583 to bcf92cf Compare July 23, 2026 00:58
Copilot AI review requested due to automatic review settings July 23, 2026 00:58

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added scope: docs Documentation size: XL 500+ changed lines and removed size: L 200-499 changed lines labels Jul 23, 2026
@pranavraja99 pranavraja99 changed the title fix(reborn): auto-activate web-access so agents discover real web search fix(reborn): auto-activate web-access and Brave-backed web_search so agents discover real web search Jul 23, 2026

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/ironclaw_reborn_composition/src/extension_host/available_extensions.rs (2)

1892-1943: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Extend the asset-ref integrity test to cover web_search.

bundled_first_party_manifest_asset_refs_are_packaged validates every capability's input_schema_ref/output_schema_ref/prompt_doc_ref resolves to a packaged asset for each listed id, but the new web_search id isn't in the list — a broken schema/prompt path for the new capability would ship undetected.

✅ Proposed fix
         let mut extension_ids = vec![
             "github",
             "notion",
             "web-access",
+            "web_search",
             NEARAI_EXTENSION_ID,
🤖 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/ironclaw_reborn_composition/src/extension_host/available_extensions.rs`
around lines 1892 - 1943, Extend the extension_ids list in
bundled_first_party_manifest_asset_refs_are_packaged to include the web_search
extension id, using its existing identifying constant if available, so its
capability schema and prompt asset references are validated.

1802-1807: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Add "web_search" to reserved_host_bundled_extension_id.

Every other bundled first-party id (github, notion, web-access, slack_bot, NEARAI_EXTENSION_ID, gsuite) is reserved so filesystem discovery can't load a same-id InstalledLocal manifest that would then catalog.extend()-replace the compiled-in HostBundled entry. The new web_search id is missing, leaving the freshly-bundled Brave tool (with real credential injection) open to the exact trust-laundering this function exists to block.

🔒 Proposed fix
 pub(crate) fn reserved_host_bundled_extension_id(extension_id: &ExtensionId) -> bool {
     matches!(
         extension_id.as_str(),
-        "github" | "notion" | "web-access" | "slack_bot" | NEARAI_EXTENSION_ID
+        "github" | "notion" | "web-access" | "web_search" | "slack_bot" | NEARAI_EXTENSION_ID
     ) || is_gsuite_extension_id(extension_id)
 }
🤖 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/ironclaw_reborn_composition/src/extension_host/available_extensions.rs`
around lines 1802 - 1807, Add the "web_search" identifier to the match list in
reserved_host_bundled_extension_id, preserving the existing reservations and
gsuite handling so filesystem discovery cannot replace the bundled HostBundled
entry.
🤖 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/ironclaw_host_runtime/src/services/runtime_adapters.rs`:
- Around line 917-921: Restrict WASM secret probing to manifest-declared handles
by threading the capability’s declared runtime-credential handle set into
WasmRuntimeSecretsAdapter from the surrounding dispatch path. Update the
adapter’s exists/access checks to return false before calling
secret_owner_scope() for undeclared names, while preserving allowed owner-scoped
access and tenant-shared fallback. Add caller-level WASM dispatch tests covering
both allowed cases and denial of undeclared handles.

In
`@crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs`:
- Around line 97-199: The phase check in bootstrap_web_search_brave must stop
relying on project(), which hides removal tombstones and foreign-private
ownership. In
crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs#L97-199,
add a host-only lifecycle preflight that distinguishes never-installed,
removal-tombstone, disabled, and foreign-private-owned states before
provisioning brave_api_key or installing/activating; preserve removed and
private states without ownership changes. Apply the same preflight to
bootstrap_web_access in
crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs#L69-128.
Add caller-level restart coverage for both extensions proving explicit removal
remains removed and member-private installs are neither activated nor promoted.

In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 7100-7209: Add the missing conditional compilation gate to the
test containing the bootstrap_web_search_brave regression coverage, matching the
gate used by its surrounding local-development tests. Keep the tenant-shared
scope assertions and arbitrary-caller credential-gate invocation unchanged,
while ensuring the test only compiles and runs in the intended configuration.
- Around line 7065-7099: Add a `#[cfg(any(feature = "libsql", feature =
"postgres"))]` attribute to the
`local_dev_web_search_brave_is_auto_installed_and_activated_when_configured`
test, matching the durable-backend gating used by sibling tests, so it is
excluded when `admin_secret_provisioner` is unavailable.

---

Outside diff comments:
In
`@crates/ironclaw_reborn_composition/src/extension_host/available_extensions.rs`:
- Around line 1892-1943: Extend the extension_ids list in
bundled_first_party_manifest_asset_refs_are_packaged to include the web_search
extension id, using its existing identifying constant if available, so its
capability schema and prompt asset references are validated.
- Around line 1802-1807: Add the "web_search" identifier to the match list in
reserved_host_bundled_extension_id, preserving the existing reservations and
gsuite handling so filesystem discovery cannot replace the bundled HostBundled
entry.
🪄 Autofix (Beta)

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: 29523368-a0c9-4e3b-9a4b-f4366245b8ee

📥 Commits

Reviewing files that changed from the base of the PR and between 05bd583 and bcf92cf.

⛔ Files ignored due to path filters (1)
  • crates/ironclaw_first_party_extensions/assets/web-search/wasm/web_search_tool.wasm is excluded by !**/*.wasm, !**/*.wasm
📒 Files selected for processing (14)
  • crates/ironclaw_first_party_extensions/assets/web-search/manifest.toml
  • crates/ironclaw_first_party_extensions/assets/web-search/prompts/web_search/search.md
  • crates/ironclaw_first_party_extensions/assets/web-search/schemas/web_search/search.input.v1.json
  • crates/ironclaw_first_party_extensions/assets/web-search/schemas/web_search/search.output.v1.json
  • crates/ironclaw_host_runtime/src/services.rs
  • crates/ironclaw_host_runtime/src/services/builder.rs
  • crates/ironclaw_host_runtime/src/services/runtime_adapters.rs
  • crates/ironclaw_reborn_composition/src/extension_host/available_extensions.rs
  • crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle.rs
  • crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs
  • crates/ironclaw_reborn_composition/src/extension_host/mod.rs
  • crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs
  • crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs
  • crates/ironclaw_reborn_composition/src/factory.rs

Comment on lines +917 to +921
match &self.secret_store {
Some(store) => host.with_secrets(Arc::new(WasmRuntimeSecretsAdapter {
store: Arc::clone(store),
scope: scope.clone(),
})),

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 | 🟠 Major | ⚡ Quick win

Restrict WASM secret probes to manifest-declared handles.

Line 918 installs a scope-wide oracle: exists() accepts any valid handle, while web_search only declares brave_api_key in manifest.toml Lines 22-24. An untrusted WASM extension can probe other caller or tenant-shared secret names. Thread the capability’s declared runtime-credential handles into WasmRuntimeSecretsAdapter and return false before secret_owner_scope() for undeclared names. Add caller-level WASM dispatch tests for allowed owner-scoped access, allowed tenant-shared fallback, and denied undeclared handles.

As per coding guidelines, credentials must remain host-side and be injected only at the narrowest egress boundary.

🤖 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/ironclaw_host_runtime/src/services/runtime_adapters.rs` around lines
917 - 921, Restrict WASM secret probing to manifest-declared handles by
threading the capability’s declared runtime-credential handle set into
WasmRuntimeSecretsAdapter from the surrounding dispatch path. Update the
adapter’s exists/access checks to return false before calling
secret_owner_scope() for undeclared names, while preserving allowed owner-scoped
access and tenant-shared fallback. Add caller-level WASM dispatch tests covering
both allowed cases and denial of undeclared handles.

Source: Coding guidelines

Comment on lines +97 to +199
pub(crate) async fn bootstrap_web_search_brave(
api_key: Option<SecretString>,
extension_management: &Arc<RebornLocalExtensionManagementPort>,
admin_secret_provisioner: Option<&Arc<dyn AdminSecretProvisioner>>,
owner_scope: &ironclaw_host_api::ResourceScope,
) -> Result<WebSearchBootstrapOutcome, RebornBuildError> {
let Some(api_key) = api_key else {
return Ok(WebSearchBootstrapOutcome::NotConfigured);
};

let caller: UserId = extension_management.tenant_operator_user_id().clone();
let package_ref =
LifecyclePackageRef::new(LifecyclePackageKind::Extension, WEB_SEARCH_EXTENSION_ID)
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web_search package ref is invalid: {error}"),
})?;

let phase = extension_management
.project(package_ref.clone(), &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web_search extension projection failed: {error}"),
})?
.phase;

match phase {
LifecyclePhase::Discovered | LifecyclePhase::Installed => {}
LifecyclePhase::Active => return Ok(WebSearchBootstrapOutcome::AlreadyActive),
LifecyclePhase::Removed => {
tracing::debug!(
"web_search was explicitly removed; preserving that state rather than \
re-installing it"
);
return Ok(WebSearchBootstrapOutcome::SkippedPreservedRemoved);
}
LifecyclePhase::Disabled => {
tracing::debug!(
"web_search is explicitly disabled; preserving that state rather than \
re-activating it"
);
return Ok(WebSearchBootstrapOutcome::SkippedDisabled);
}
other => {
tracing::debug!(
phase = ?other,
"web_search is not in an auto-activatable phase; skipping bootstrap"
);
return Ok(WebSearchBootstrapOutcome::SkippedNonActivatable);
}
}

// Seed the secret before activation so the tool's declared credential
// injection has something to lease. Best-effort: a provisioner is only
// wired up on backends with durable secret-store crypto; if it's absent
// we still install/activate (the tool itself fails closed per-call with
// a clear "Brave API key not found" message rather than silently
// pretending to work), so a missing provisioner is not fatal here.
//
// Provision under the TENANT-SHARED scope, not the bootstrap caller's own
// (tenant, user) pair: `web_search` installs Tenant-owned (same as
// `web-access`), and the host's dispatch-time credential pre-flight
// (`secret_owner_scope`) only ever checks the model-invocation caller's
// own scope, then falls back to `caller_scope.tenant_shared_managed_scope()`
// — never an arbitrary third scope. Provisioning under the bootstrap's
// own owner scope left the secret invisible to every real invocation
// (dispatch-time pre-flight always reported it absent, surfacing
// AuthRequired instead of ever calling Brave).
if let Some(provisioner) = admin_secret_provisioner {
let handle = SecretHandle::new(BRAVE_API_KEY_SECRET_NAME).map_err(|error| {
RebornBuildError::InvalidConfig {
reason: format!("brave_api_key secret handle is invalid: {error}"),
}
})?;
let shared_scope = owner_scope.tenant_shared_managed_scope();
provisioner
.put(
&shared_scope.tenant_id,
&shared_scope.user_id,
handle,
api_key,
)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("brave_api_key secret provisioning failed: {error}"),
})?;
}

if phase == LifecyclePhase::Discovered {
extension_management
.install(package_ref.clone(), &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web_search extension install failed: {error}"),
})?;
}
extension_management
.activate(package_ref, ExtensionActivationMode::Static, &caller)
.await
.map_err(|error| RebornBuildError::InvalidConfig {
reason: format!("web_search extension activation failed: {error}"),
})?;
Ok(WebSearchBootstrapOutcome::Activated)
}

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 | 🔴 Critical | 🏗️ Heavy lift

Both auto-bootstrap modules branch on a project() phase that cannot represent "removed" or "foreign-private", so neither actually honors an explicit removal or protects a private install across a restart.

project() (see extension_lifecycle.rs's phase_for_activation_state) only returns Discovered | Installed | Disabled | Active; removal deletes the installation row so it is indistinguishable from never-installed, and a foreign owner's private install is masked to Discovered too. Fixing only one of these two near-identical modules leaves the same restart-time resurrection/ownership-widening hazard in the other.

  • crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs#L97-L199: replace the project()-based phase check with a host-only lifecycle preflight that distinguishes never-installed, removal-tombstone, disabled, and foreign-private-owned states without changing ownership, before deciding to install/activate or provision brave_api_key.
  • crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs#L69-L128: apply the same host-only preflight here (this is the already-flagged, still-unresolved instance).

Add a caller-level restart test proving an explicit removal stays removed and a member-private install is neither activated nor promoted, for both extensions.

📍 Affects 2 files
  • crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs#L97-L199 (this comment)
  • crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs#L69-L128
🤖 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/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs`
around lines 97 - 199, The phase check in bootstrap_web_search_brave must stop
relying on project(), which hides removal tombstones and foreign-private
ownership. In
crates/ironclaw_reborn_composition/src/extension_host/web_search_bootstrap.rs#L97-199,
add a host-only lifecycle preflight that distinguishes never-installed,
removal-tombstone, disabled, and foreign-private-owned states before
provisioning brave_api_key or installing/activating; preserve removed and
private states without ownership changes. Apply the same preflight to
bootstrap_web_access in
crates/ironclaw_reborn_composition/src/extension_host/web_access_bootstrap.rs#L69-128.
Add caller-level restart coverage for both extensions proving explicit removal
remains removed and member-private installs are neither activated nor promoted.

Comment on lines +7065 to +7099
#[tokio::test]
async fn local_dev_web_search_brave_is_auto_installed_and_activated_when_configured() {
let dir = tempfile::tempdir().expect("tempdir");
let owner = "local-dev-web-search-owner";
// Match `ExecutionContext::local_default`'s tenant/agent below (both
// resolve to `LOCAL_DEFAULT_TENANT_ID`/`LOCAL_DEFAULT_AGENT_ID` when no
// explicit identity is threaded through) — this is exactly the
// scope-mismatch bug this test now guards against: the bootstrap
// must provision the secret under the SAME (tenant, user) a real
// capability invocation will look it up under, not whatever internal
// fallback identity composition happens to default to.
let runtime_identity = RebornLocalRuntimeIdentity {
tenant_id: ironclaw_host_api::TenantId::new(ironclaw_host_api::LOCAL_DEFAULT_TENANT_ID)
.expect("valid tenant id"),
agent_id: ironclaw_host_api::AgentId::new(ironclaw_host_api::LOCAL_DEFAULT_AGENT_ID)
.expect("valid agent id"),
};
let services = build_reborn_services(
RebornBuildInput::local_dev(owner, dir.path().join("local-dev"))
.with_local_runtime_identity(
runtime_identity.tenant_id.clone(),
runtime_identity.agent_id.clone(),
),
)
.await
.expect("local-dev services build");
let local_runtime = services.local_runtime.as_ref().expect("local runtime");
let extension_management = local_runtime
.extension_management
.as_ref()
.expect("extension management");
let admin_secret_provisioner = local_runtime
.admin_secret_provisioner
.as_ref()
.expect("admin secret provisioner");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Missing #[cfg(any(feature = "libsql", feature = "postgres"))] on this test — will panic on a no-durable-backend build.

admin_secret_provisioner is only ever Some when the durable backend path populates it (lines 1725-1737); the no-durable build leaves it None. This test unconditionally does local_runtime.admin_secret_provisioner.as_ref().expect("admin secret provisioner") at lines 7096-7099 with no cfg gate, unlike every sibling durable-dependent test in this file (e.g. local_dev_nearai_mcp_auto_bootstraps_from_injected_config at line 7618-7619, resolve_local_dev_secret_master_key_rejects_malformed_file_with_path_context at line 6534-6535). Under cargo test --no-default-features --features test-support this .expect() panics.

🐛 Proposed fix
+    #[cfg(any(feature = "libsql", feature = "postgres"))]
     #[tokio::test]
     async fn local_dev_web_search_brave_is_auto_installed_and_activated_when_configured() {
📝 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
#[tokio::test]
async fn local_dev_web_search_brave_is_auto_installed_and_activated_when_configured() {
let dir = tempfile::tempdir().expect("tempdir");
let owner = "local-dev-web-search-owner";
// Match `ExecutionContext::local_default`'s tenant/agent below (both
// resolve to `LOCAL_DEFAULT_TENANT_ID`/`LOCAL_DEFAULT_AGENT_ID` when no
// explicit identity is threaded through) — this is exactly the
// scope-mismatch bug this test now guards against: the bootstrap
// must provision the secret under the SAME (tenant, user) a real
// capability invocation will look it up under, not whatever internal
// fallback identity composition happens to default to.
let runtime_identity = RebornLocalRuntimeIdentity {
tenant_id: ironclaw_host_api::TenantId::new(ironclaw_host_api::LOCAL_DEFAULT_TENANT_ID)
.expect("valid tenant id"),
agent_id: ironclaw_host_api::AgentId::new(ironclaw_host_api::LOCAL_DEFAULT_AGENT_ID)
.expect("valid agent id"),
};
let services = build_reborn_services(
RebornBuildInput::local_dev(owner, dir.path().join("local-dev"))
.with_local_runtime_identity(
runtime_identity.tenant_id.clone(),
runtime_identity.agent_id.clone(),
),
)
.await
.expect("local-dev services build");
let local_runtime = services.local_runtime.as_ref().expect("local runtime");
let extension_management = local_runtime
.extension_management
.as_ref()
.expect("extension management");
let admin_secret_provisioner = local_runtime
.admin_secret_provisioner
.as_ref()
.expect("admin secret provisioner");
#[cfg(any(feature = "libsql", feature = "postgres"))]
#[tokio::test]
async fn local_dev_web_search_brave_is_auto_installed_and_activated_when_configured() {
let dir = tempfile::tempdir().expect("tempdir");
let owner = "local-dev-web-search-owner";
// Match `ExecutionContext::local_default`'s tenant/agent below (both
// resolve to `LOCAL_DEFAULT_TENANT_ID`/`LOCAL_DEFAULT_AGENT_ID` when no
// explicit identity is threaded through) — this is exactly the
// scope-mismatch bug this test now guards against: the bootstrap
// must provision the secret under the SAME (tenant, user) a real
// capability invocation will look it up under, not whatever internal
// fallback identity composition happens to default to.
let runtime_identity = RebornLocalRuntimeIdentity {
tenant_id: ironclaw_host_api::TenantId::new(ironclaw_host_api::LOCAL_DEFAULT_TENANT_ID)
.expect("valid tenant id"),
agent_id: ironclaw_host_api::AgentId::new(ironclaw_host_api::LOCAL_DEFAULT_AGENT_ID)
.expect("valid agent id"),
};
let services = build_reborn_services(
RebornBuildInput::local_dev(owner, dir.path().join("local-dev"))
.with_local_runtime_identity(
runtime_identity.tenant_id.clone(),
runtime_identity.agent_id.clone(),
),
)
.await
.expect("local-dev services build");
let local_runtime = services.local_runtime.as_ref().expect("local runtime");
let extension_management = local_runtime
.extension_management
.as_ref()
.expect("extension management");
let admin_secret_provisioner = local_runtime
.admin_secret_provisioner
.as_ref()
.expect("admin secret provisioner");
🤖 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/ironclaw_reborn_composition/src/factory.rs` around lines 7065 - 7099,
Add a `#[cfg(any(feature = "libsql", feature = "postgres"))]` attribute to the
`local_dev_web_search_brave_is_auto_installed_and_activated_when_configured`
test, matching the durable-backend gating used by sibling tests, so it is
excluded when `admin_secret_provisioner` is unavailable.

Comment on lines +7100 to +7209
let owner_scope = local_dev_nearai_mcp_owner_scope(
UserId::new(owner).expect("valid owner"),
Some(&runtime_identity),
)
.expect("owner scope");

// Without a configured key, `build_reborn_services` above already ran
// `bootstrap_web_search_brave(None, ...)` — assert it left `web_search`
// untouched (still just `Discovered`, matching the fallback-to-Exa
// behavior the mutual-exclusion logic in `build_local_runtime` relies
// on) before exercising the configured path directly.
let web_search_ref =
LifecyclePackageRef::new(LifecyclePackageKind::Extension, "web_search")
.expect("valid ref");
let phase_before = extension_management
.project(
web_search_ref.clone(),
extension_management.tenant_operator_user_id_for_test(),
)
.await
.expect("project web_search before bootstrap")
.phase;
assert_eq!(phase_before, LifecyclePhase::Discovered);

let outcome = crate::extension_host::web_search_bootstrap::bootstrap_web_search_brave(
Some(secrecy::SecretString::from("test-brave-key".to_string())),
extension_management,
Some(admin_secret_provisioner),
&owner_scope,
)
.await
.expect("brave bootstrap succeeds");
assert_eq!(
outcome,
crate::extension_host::web_search_bootstrap::WebSearchBootstrapOutcome::Activated
);
assert!(!outcome.leaves_web_access_available());

let phase_after = extension_management
.project(
web_search_ref,
extension_management.tenant_operator_user_id_for_test(),
)
.await
.expect("project web_search after bootstrap")
.phase;
assert_eq!(phase_after, LifecyclePhase::Active);

// Must be provisioned under the TENANT-SHARED scope, not the
// bootstrap caller's own (tenant, user) — `secret_owner_scope` (the
// dispatch-time credential pre-flight in ironclaw_host_runtime) only
// ever checks the real invocation caller's own scope, then falls back
// to `caller_scope.tenant_shared_managed_scope()`, never an arbitrary
// third scope. Provisioning anywhere else leaves the secret invisible
// to every real capability invocation regardless of who calls it —
// this regressed once already (dispatch-time pre-flight kept
// reporting the secret absent and surfacing AuthRequired, so Brave
// was never actually called end-to-end despite `web_search`
// reporting `Active`).
let shared_scope = owner_scope.tenant_shared_managed_scope();
let secrets = admin_secret_provisioner
.list(&shared_scope.tenant_id, &shared_scope.user_id)
.await
.expect("list secrets");
assert!(
secrets
.iter()
.any(|meta| meta.handle.as_str() == "brave_api_key"),
"expected brave_api_key to be seeded at the tenant-shared scope, got: {secrets:?}"
);

// End-to-end regression guard for the scope bug itself: invoke
// `web_search.search` as an ARBITRARY caller (not the bootstrap/tenant-
// operator identity) and assert the credential pre-flight does not
// gate it on `AuthRequired`. Before the tenant-shared-scope fix this
// always returned `AuthRequired` regardless of who called it, because
// the secret was provisioned under a scope no real invocation could
// ever read back. Whatever happens after the credential check (a real
// network call to Brave with a fake key, which can be slow to fail)
// is out of scope here — bounded by a short timeout so a slow/hanging
// egress attempt can't turn this into a multi-minute test; a timeout
// still proves the credential gate itself was cleared.
let context = web_search_context("web_search.search");
enable_global_auto_approve_for_context(local_runtime, &context).await;
let invocation = services
.host_runtime
.as_ref()
.expect("host runtime")
.invoke_capability(RuntimeCapabilityRequest::new(
context,
CapabilityId::new("web_search.search").unwrap(),
ResourceEstimate::default(),
serde_json::json!({ "query": "ironclaw reborn" }),
));
match tokio::time::timeout(std::time::Duration::from_secs(10), invocation).await {
Ok(result) => {
let outcome = result.expect("runtime invocation completes");
assert!(
!matches!(outcome, RuntimeCapabilityOutcome::AuthRequired(_)),
"expected the brave_api_key credential pre-flight to resolve \
(regardless of what the actual HTTP call then does), got: {outcome:?}"
);
}
Err(_) => {
// Timed out past the credential gate (presumably stuck on the
// real network call with a fake key) — the gate itself was
// cleared, which is all this test asserts.
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Rest of the test — scope-mismatch regression coverage is solid.

The tenant-shared-scope assertion (7148-7169) and the arbitrary-caller end-to-end credential-gate check (7171-7209) directly guard the exact scope bug called out in the comments. Contingent on the missing cfg gate fix above being applied so this actually runs where it's meant to.

🤖 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/ironclaw_reborn_composition/src/factory.rs` around lines 7100 - 7209,
Add the missing conditional compilation gate to the test containing the
bootstrap_web_search_brave regression coverage, matching the gate used by its
surrounding local-development tests. Keep the tenant-shared scope assertions and
arbitrary-caller credential-gate invocation unchanged, while ensuring the test
only compiles and runs in the intended configuration.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_reborn_composition/src/factory.rs (1)

7065-7208: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Drive configured Brave bootstrap through the runtime build

Configuration is read by a BRAVE_API_KEY override/env helper in bootstrap_web_search_brave; this test bypasses that gate by calling the helper with Some(...), and it still uses fake real Brave egress while accepting a timeout as credential-pre-flight success. Configure Brave through a test helper/input (or hermetically via the env override) so build_local_runtime chooses Brave, then use controlled egress/assert an execution failure instead of AuthRequired.

🤖 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/ironclaw_reborn_composition/src/factory.rs` around lines 7065 - 7208,
Update
local_dev_web_search_brave_is_auto_installed_and_activated_when_configured to
configure BRAVE_API_KEY through the build test input or scoped environment
override, then exercise build_reborn_services/build_local_runtime without
directly calling bootstrap_web_search_brave. Replace the real Brave request and
timeout with controlled egress, asserting the invocation reaches execution and
fails there rather than returning AuthRequired, while preserving the activation
and tenant-shared secret assertions.

Sources: Coding guidelines, Path instructions

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

Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 7065-7208: Update
local_dev_web_search_brave_is_auto_installed_and_activated_when_configured to
configure BRAVE_API_KEY through the build test input or scoped environment
override, then exercise build_reborn_services/build_local_runtime without
directly calling bootstrap_web_search_brave. Replace the real Brave request and
timeout with controlled egress, asserting the invocation reaches execution and
fails there rather than returning AuthRequired, while preserving the activation
and tenant-shared secret assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2a46ef10-4bd6-49ae-b62c-476fe07aca39

📥 Commits

Reviewing files that changed from the base of the PR and between bcf92cf and 2d9ffac.

📒 Files selected for processing (1)
  • crates/ironclaw_reborn_composition/src/factory.rs

@coderabbitai coderabbitai Bot mentioned this pull request Jul 23, 2026
29 tasks
# Conflicts:
#	crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle.rs
#	crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle_capabilities.rs
#	crates/ironclaw_reborn_composition/src/factory.rs
#	crates/ironclaw_reborn_composition/src/factory/tests.rs
Copilot AI review requested due to automatic review settings July 24, 2026 06:18
@pranavraja99
pranavraja99 force-pushed the fix/reborn-web-access-default-active branch from 2d9ffac to a395ca9 Compare July 24, 2026 06:18

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

This branch was successfully deployed

1 active deployment
ironclaw-ci-preview / ironclaw-pr-6232 — a395ca94 Deployed Jul 24, 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: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants