Skip to content

fix(reborn): honest extension connection state across search, surface, and model (#5416) - #5528

Closed
henrypark133 wants to merge 2 commits into
mainfrom
fix/reborn-connection-state-5416
Closed

henrypark133 wants to merge 2 commits into
mainfrom
fix/reborn-connection-state-5416

Conversation

@henrypark133

Copy link
Copy Markdown
Collaborator

Fixes #5416 — the Reborn agent claimed Gmail (or any credentialed extension) was "already configured or active — do not ask the user for credentials" while no credential account existed, then contradicted itself with an auth gate on first use. Third patch on this oscillating predicate (#4996 → #5037 → here); this one replaces the predicate class instead of re-tuning it.

Root cause

extension_search's search_installation_phase read the lifecycle axis only: an Enabled-but-uncredentialed installation short-circuited to Active without ever consulting the credential gate, and suppress_search_credential_onboarding blanket-cleared the credential hints — so the model-visible "ready, don't ask for credentials" message fired on a disconnected extension.

What this does (three shippable slices, one design)

Phase 0 — collapsed model-facing availability (search surface).
One ExtensionAvailability state (available / needs_auth / not_installed / unknown) projected from installed? × credential-readiness, replacing the leaked lifecycle installation_phase on search summaries. The blanket suppression helpers are deleted; the ready message requires a genuinely available projection, and how-to-connect detail stays on everything else. Settings extension_info now fails closed on Unknown readiness (Defect B) with a phase-gated ReverifyRequired onboarding state.

Phase 2 — credential-aware capability surface (host runtime).
New VisibleCapabilityAccess::NeedsAuth: an authorized+installed capability whose required credentials are missing is downgraded on the visible surface — generic secret-handle credentials and product-auth (OAuth) accounts alike, via a dependency-inverted CapabilityCredentialPresence port. Highlights:

  • Side-effect-free presence: new RuntimeCredentialAccountResolver::account_configured (select-only). The existing resolve_access_secret refreshes OAuth tokens and must never run on surface render — pinned by a panic-guard regression test.
  • Scope-aware cache: short-TTL, owner-scoped, conclusive-only CredentialPresenceCache; the product-auth key includes sorted provider scopes so gmail readonly/send/modify cannot alias (regression-tested).
  • Fingerprint stability: surface_version is computed from authorization-only access before the downgrade pass, so credential flips never churn the surface version ([Reborn] Capability execution fails with driver_unavailable after first successful tool call #4789 discipline, regression-pinned).
  • Fail-open: backend errors / missing wiring are indeterminate, never "missing" — a blip cannot burn a false sign-in prompt; the dispatch-time obligation check remains the enforcing backstop.

Phase 3 — carry the state to the model.
describe_with_access folds a fixed, host-authored [Not connected: …] sentence into the model-visible tool description at the single host→loop mapping seam (feeds both the provider tool definition and the descriptor view from one binding).

Tests (all written red-first)

  • Group scenario across credential types: gmail (Google OAuth), github (GitHub OAuth), notion (Notion OAuth + MCP) seeded Enabled with no credential account must NOT render the ready message; credential-free web-access control still must (over-suppression guard).
  • Caller-level NeedsAuth contract tests through HostRuntimeServices (generic secret absent, product-auth AuthRequired, backend-error fail-open).
  • Panic-guard: surface presence path never invokes the token-refreshing resolver.
  • Cache scope-aliasing and sweep-on-insert regressions; fingerprint-stability pin.
  • E2E: captured model-visible tool definition for an uncredentialed github.create_issue carries the not-connected marker through the production wiring.

Review process

Implemented per docs/plans/2026-07-01-reborn-google-connection-state-5416.md (interim) + docs/plans/2026-07-01-reborn-tool-lifecycle-unified-pipeline-northstar.md (north star — phases 1/4 and the structured-schema variant of phase 3 tracked there). Three adversarial review rounds; two blockers found and fixed red-first (OAuth-refresh-on-render, scope-aliased cache key).

Verification

  • ironclaw_host_runtime: 303 lib + all contract targets green (3 pre-existing docker-socket sandbox_process failures only, untouched)
  • ironclaw_product_workflow: 201 contract + 87 lib green
  • ironclaw_reborn_composition: product_auth suites green
  • ironclaw_loop_support: all targets green
  • reborn_group_extensions + reborn_integration_auth_gate: 22/22 each
  • clippy clean on all touched files; cargo fmt --check clean

🤖 Generated with Claude Code

…, and model (#5416)

Fixes the contradictory Google/Gmail authentication flow where the agent
claimed an extension was "already configured or active — do not ask the
user for credentials" while no credential account existed, then hit an
auth gate on first use. Third patch on this oscillating predicate
(#4996 -> #5037 -> here); this one replaces the predicate class instead
of re-tuning it.

Phase 0 — collapsed model-facing availability (search surface).
- New `ExtensionAvailability` enum (available / needs_auth /
  not_installed / unknown) replaces the leaked lifecycle
  `installation_phase` on `LifecycleSearchExtensionSummary`; projected
  from installed? x credential-readiness in the new
  `extension_availability` composition module. Lifecycle phase stays
  internal.
- Deletes `suppress_search_credential_onboarding` /
  `search_installation_phase` / `search_credentials_configured`: the
  "ready" message now requires a genuinely Available projection, and
  credential requirements/onboarding stay on non-Available results.
- Defect B: settings `extension_info` fails closed on Unknown
  credential readiness (authenticated=false, needs_setup=true) with a
  new gated ReverifyRequired onboarding state for plausibly-connected
  phases only.

Phase 2 — credential-aware capability surface (host runtime).
- New `VisibleCapabilityAccess::NeedsAuth`: an authorized+installed
  capability whose required credentials are missing is downgraded on
  the visible surface, for generic secret-handle credentials and
  product-auth (OAuth) accounts alike.
- Presence is resolved through a new dependency-inverted
  `CapabilityCredentialPresence` port; the production impl composes
  `secret_present` and the new side-effect-free
  `RuntimeCredentialAccountResolver::account_configured` (select-only —
  the existing `resolve_access_secret` refreshes OAuth tokens and must
  never run on surface render) behind a short-TTL, owner-scoped,
  conclusive-only `CredentialPresenceCache` whose product-auth key
  includes sorted provider scopes (gmail readonly/send/modify must not
  alias).
- Surface fingerprint stays credential-independent by construction:
  `surface_version` is computed from authorization-only access before
  the downgrade pass (see #4789), regression-pinned.
- Fail-open discipline throughout: backend errors and missing wiring
  are indeterminate, never "missing" — a blip cannot burn a false
  sign-in prompt.

Phase 3 — carry the state to the model.
- `describe_with_access` folds a fixed, host-authored "[Not connected:
  ...]" sentence into the model-visible tool description at the single
  host->loop mapping seam, so both the provider tool definition and the
  descriptor view read the same folded string.

Regression tests (all red-first): cross-credential-type group scenario
(gmail Google OAuth / github GitHub OAuth / notion Notion OAuth+MCP
must not render the ready message when Enabled-but-uncredentialed;
credential-free web-access control still must), caller-level
NeedsAuth contract tests through `HostRuntimeServices`, a panic-guard
pinning that the surface path never calls the refreshing resolver, a
cache scope-aliasing regression, the fingerprint-stability pin, and an
e2e assertion that the captured model-visible tool definition carries
the not-connected marker.

Plan docs: docs/plans/2026-07-01-reborn-google-connection-state-5416.md
(interim) and docs/plans/2026-07-01-reborn-tool-lifecycle-unified-pipeline-northstar.md
(north star; phases 1/3+/4 tracked there).

Closes #5416

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 2, 2026 06:42
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5528 July 2, 2026 06:42 Destroyed
@github-actions github-actions Bot added scope: docs Documentation size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Extension search results and tool surfaces now report availability (available / needs_auth / not_installed / unknown) and drive UI changes accordingly.
    • Tool descriptions can include a “not connected” marker when credentials aren’t satisfied.
  • Bug Fixes

    • Visible capabilities now downgrade to “needs_auth” only when credentials are conclusively missing; backend uncertainty no longer incorrectly marks extensions as connected.
    • Added pre-flight credential presence checks with short-lived caching to prevent cross-capability state mix-ups.
    • Unknown credential readiness now routes to a dedicated “reverify required” onboarding flow for active/connected extensions.

Walkthrough

Adds credential-presence checks into runtime capability rendering, then switches extension search and onboarding to an availability-based projection with NeedsAuth, Unknown, and ReverifyRequired handling.

Changes

Credential presence and capability surface

Layer / File(s) Summary
Presence cache and keys
crates/ironclaw_host_runtime/src/credential_presence_cache.rs
Adds the short-TTL cache, owner-scope projection, setup-kind separation, and product-auth cache-key canonicalization.
Credential presence implementation
crates/ironclaw_host_runtime/src/credential_presence.rs, .../lib.rs
Implements fail-open presence checks over secret store and account resolver, with regression coverage for provider-scope aliasing and duplicate-scope canonicalization.
Runtime wiring and resolver contract
crates/ironclaw_host_runtime/src/obligations.rs, .../production.rs, .../services.rs, .../tests/*
Adds account_configured, forwards it through host runtime wiring, and updates resolver test doubles and caller-level credential-presence tests.
Capability surface downgrade and marker fold
crates/ironclaw_host_runtime/src/surface.rs, crates/ironclaw_loop_support/src/capability_port.rs, .../lib.rs, tests/reborn_integration_auth_gate.rs, tests/support/reborn/*
Adds NeedsAuth, downgrades visible capabilities after fingerprinting, folds the not-connected marker into descriptions, and captures tool definitions in the auth-gate test.
Caller-level visible-capability regression
crates/ironclaw_host_runtime/tests/host_runtime_visible_capability_credential_presence_contract.rs
Exercises real host-runtime wiring for secret and product-auth downgrade paths, fail-open backend behavior, and cache invalidation across renders.

Availability projection and search

Layer / File(s) Summary
Availability type and onboarding state
crates/ironclaw_product_workflow/src/lifecycle.rs, .../reborn_services/types.rs, .../reborn_services/extension_onboarding.rs, .../reborn_services/extensions.rs, tests/reborn_services_contract.rs
Adds ExtensionAvailability, ReverifyRequired, and fail-closed handling for Unknown readiness in extension info and onboarding.
Availability projection module
crates/ironclaw_reborn_composition/src/extension_availability.rs, .../lib.rs
Adds the projection helpers that map installation and credential-gate outcomes into availability states.
Search integration
crates/ironclaw_reborn_composition/src/extension_lifecycle.rs, .../extension_lifecycle_capabilities.rs, .../extension_lifecycle_command.rs
Computes per-result availability, changes ready-result detection, and updates search assertions and fixtures from installation_phase to availability.
Cross-thread scenarios
tests/reborn_group_extensions/*
Updates the reborn end-to-end scenarios to assert availability values and adds the ready-message regression scenario.
Design plan docs
docs/plans/2026-07-01-reborn-google-connection-state-5416.md, docs/plans/2026-07-01-reborn-tool-lifecycle-unified-pipeline-northstar.md
Adds planning documents for the interim fix and the longer-term unified connection-state pipeline.

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

Possibly related PRs

  • nearai/ironclaw#4939: Changes the same product-auth runtime account selection path that now backs account_configured.
  • nearai/ironclaw#5034: Touches the same extension_search readiness/messaging path and the installation_phase-to-availability transition area.
  • nearai/ironclaw#5433: Covers the same reborn cross-thread lifecycle scenario updated here for availability.

Poem

Cache says: present, absent, or unsure;
Search says: needs_auth, available, unknown — all pure.
No more phase-smuggled state,
No more “already connected” bait.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the bug, approach, tests, and verification, but omits most required template sections and checkbox items. Add the template sections: Summary bullets, Change Type, Linked Issue, Security Impact, Trust-Boundary Checklist, Database Impact, Blast Radius, Rollback Plan, and Review Track.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is Conventional Commits style and accurately summarizes the main #5416 connection-state fix.
Linked Issues check ✅ Passed The changes directly address #5416 by replacing leaked lifecycle state with availability, downgrading missing credentials to NeedsAuth, and fixing model-visible messaging.
Out of Scope Changes check ✅ Passed I don't see unrelated code; the changes stay focused on connection-state projection, credential presence, and supporting tests/docs for #5416.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/reborn-connection-state-5416

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 implements Phase 2 and Phase 3 of the reborn connection-state unification plan (issue #5416). It introduces a short-TTL CredentialPresenceCache and a side-effect-free account_configured check to verify credential presence during capability-surface rendering without triggering expensive token refreshes. It also collapses model-facing connection states into a single ExtensionAvailability enum and appends a fixed not-connected marker to model-visible capability descriptions when required credentials are missing. The review feedback suggests deduplicating provider scopes after sorting them in ProductionCredentialPresence to ensure cache keys are canonical and independent of redundant entries.

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 +92 to +93
let mut scopes = requirement.provider_scopes.clone();
scopes.sort();

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

To ensure the cache key is truly canonical and independent of duplicate scopes that might be declared in different orders or with redundant entries, consider deduplicating the scopes after sorting them.

            let mut scopes = requirement.provider_scopes.clone();
            scopes.sort();
            scopes.dedup();

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.

Pull request overview

This PR fixes Reborn’s contradictory “already connected” messaging by making extension/capability connection state credential-aware across (1) extension search results, (2) host runtime capability surfaces, and (3) the model-visible tool descriptions.

Changes:

  • Replace model-facing installation_phase with a collapsed ExtensionAvailability (available/needs_auth/not_installed/unknown) and gate the “ready” message + onboarding suppression on that projection.
  • Add credential-aware capability surfacing via VisibleCapabilityAccess::NeedsAuth, backed by a side-effect-free product-auth presence check (account_configured) and a short-TTL, owner-scoped presence cache (including provider-scope keying).
  • Propagate NeedsAuth to the model by folding a fixed “[Not connected…]” sentence into tool descriptions at the host→loop mapping seam; expand harness/test seams to assert against shipped tool definitions.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/support/reborn/harness.rs Expose extension installation store seam for tests; add account_configured to the fixed resolver.
tests/support/reborn/group.rs Retain TraceLlm handle for later inspection of shipped tool definitions.
tests/support/reborn/builder.rs Add captured_tool_definitions() to integration harness via TraceLlm.
tests/reborn_integration_auth_gate.rs Assert the model-visible tool description includes the NeedsAuth marker.
tests/reborn_group_extensions/scenario_search_ready_message_requires_credential.rs New regression scenario: search “ready” message must require credentials across multiple providers + control case.
tests/reborn_group_extensions/scenario_remove_then_absent_cross_thread.rs Update cross-thread assertions to use availability instead of installation_phase.
tests/reborn_group_extensions/scenario_install_then_visible_cross_thread.rs Update cross-thread install visibility assertions to availability.
tests/reborn_group_extensions/scenario_activate_then_active_cross_thread.rs Update activation persistence assertions; add durable-store read to prove activation specifically.
tests/reborn_group_extensions/main.rs Register the new #5416 regression scenario and update scenario descriptions for availability.
docs/plans/2026-07-01-reborn-tool-lifecycle-unified-pipeline-northstar.md Add north-star design doc describing unified connection-state pipeline.
docs/plans/2026-07-01-reborn-google-connection-state-5416.md Add interim plan doc for fixing the Gmail connection-state contradiction.
crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs Implement side-effect-free account_configured for product-auth presence checks.
crates/ironclaw_reborn_composition/src/lib.rs Register new extension_availability module.
crates/ironclaw_reborn_composition/src/extension_lifecycle.rs Fan out search summaries with bounded concurrency; compute and apply availability; gate “ready” message via any_available.
crates/ironclaw_reborn_composition/src/extension_lifecycle_command.rs Update test payload shape for availability field rename.
crates/ironclaw_reborn_composition/src/extension_lifecycle_capabilities.rs Update local-dev tests to assert onboarding/message behavior via availability.
crates/ironclaw_reborn_composition/src/extension_availability.rs New module: project (installed × credential readiness) → ExtensionAvailability and “any available?” predicate.
crates/ironclaw_product_workflow/tests/reborn_services_contract.rs Document/validate Unknown credential readiness behavior in contract test.
crates/ironclaw_product_workflow/src/reborn_services/types.rs Add ReverifyRequired onboarding state for unknown credential readiness.
crates/ironclaw_product_workflow/src/reborn_services/extensions.rs Fail closed on Unknown readiness; keep Unknown actionable (needs_setup=true); update tests.
crates/ironclaw_product_workflow/src/reborn_services/extension_onboarding.rs Add Unknown→reverify onboarding for plausibly-connected phases; add regression test.
crates/ironclaw_product_workflow/src/lifecycle.rs Add ExtensionAvailability enum and replace installation_phase with availability on search summaries.
crates/ironclaw_product_workflow/src/lib.rs Re-export ExtensionAvailability.
crates/ironclaw_loop_support/src/lib.rs Re-export CAPABILITY_NEEDS_AUTH_DESCRIPTION_MARKER.
crates/ironclaw_loop_support/src/capability_port.rs Fold NeedsAuth marker into model-visible descriptions and descriptor views; add unit+integration coverage.
crates/ironclaw_host_runtime/tests/runtime_http_egress_contract.rs Implement new account_configured method on test resolver.
crates/ironclaw_host_runtime/tests/host_runtime_visible_capability_credential_presence_contract.rs New caller-level contract tests for NeedsAuth downgrade + side-effect guard.
crates/ironclaw_host_runtime/tests/github_wasm_runtime_contract.rs Add account_configured to test resolvers.
crates/ironclaw_host_runtime/tests/builtin_obligation_handler_contract.rs Add account_configured to test resolvers.
crates/ironclaw_host_runtime/src/surface.rs Add VisibleCapabilityAccess::NeedsAuth and credential-presence port; keep fingerprint stable by downgrading post-hash.
crates/ironclaw_host_runtime/src/services.rs Wire runtime credential resolver into host runtime for surface presence checks.
crates/ironclaw_host_runtime/src/production.rs Add resolver+cache fields; build/wire ProductionCredentialPresence into visible_capabilities.
crates/ironclaw_host_runtime/src/obligations.rs Extend resolver trait with side-effect-free account_configured.
crates/ironclaw_host_runtime/src/lib.rs Register new credential presence modules.
crates/ironclaw_host_runtime/src/credential_presence.rs New production credential presence implementation (secret store + account resolver + cache).
crates/ironclaw_host_runtime/src/credential_presence_cache.rs New short-TTL, owner-scoped presence cache with scope-aware keys (incl. provider scopes).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +88
let create_issue_tool = first_call_tools
.iter()
.find(|tool| tool.name.contains("create_issue"))
.expect("github.create_issue is on the model-visible tool list");

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68cdb4702d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +94 to +98
let key = CredentialPresenceKey::ProductAuth(
owner_scope.clone(),
requirement.provider.clone(),
requirement.requester_extension.clone(),
scopes,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include setup in product-auth presence cache keys

When an extension declares two product-auth requirements with the same owner/provider/requester/scopes but different setup modes, this cache key aliases their answers even though account_configured is called with requirement.setup and selection behavior depends on it (for example ManualToken ignores stored scopes while OAuth requires them). The first rendered requirement can therefore cache true/false and make the other capability show the wrong Available/NeedsAuth state for the TTL; include the setup in the key.

Useful? React with 👍 / 👎.

Comment on lines +596 to +597
Ok(account) => Ok(account.status == CredentialAccountStatus::Configured
&& account.access_secret.is_some()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat missing access_secret as backend corruption

If the account store returns a Configured account with no access_secret, the dispatch path classifies that as Backend corruption rather than a user auth problem, but this presence check returns Ok(false). That false result is cached and downgrades the model surface to NeedsAuth, causing a re-auth prompt for a backend/data-integrity issue that re-auth may not fix; mirror resolve_access_secret/runtime_credential_auth_requirement_configured and return Err(CredentialStageError::Backend) when the selected account lacks an access secret.

Useful? React with 👍 / 👎.

@railway-app

railway-app Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 2, 2026 at 4:10 pm

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Fix Reborn extension connection state so uncredentialed extensions are shown as needing auth across search, capability surface, and model tool descriptions.
Stats: 6 net-new findings (from 10 raw, 6 after live-thread duplicate suppression) across 4 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0. Suppressed as already covered by live unresolved threads: 3.

Existing live threads not duplicated: product-auth cache key omits setup mode, configured account without access_secret should be backend corruption, and duplicate scope canonicalization are already open on this head.

performance

  1. Medium Credential presence checks serialize storage I/O per tool render (crates/ironclaw_host_runtime/src/surface.rs:252-260, confidence 78) — anchor: crates/ironclaw_host_runtime/src/surface.rs:252
    visible_capabilities now runs on every model surface/tool render, but the new downgrade pass awaits required_credentials_present one capability at a time. On a cold cache or after the 10s TTL, each credentialed capability can perform secret-store or product-auth account lookups, so a surface with N credentialed tools adds N serialized backend round trips before the model can see tools. The cache only helps after the first completed pass and does not prevent the first render after expiry from paying the full serial latency.

tests

  1. Medium Secret-store backend errors lack fail-open coverage (crates/ironclaw_host_runtime/src/credential_presence.rs:63-74, confidence 75) — anchor: crates/ironclaw_host_runtime/src/credential_presence.rs:63
    The new secret-handle presence path treats secret_present errors as indeterminate, but the caller-level tests cover product-auth backend failures and dispatch-time secret failures without covering this surface-render path. A regression could downgrade a capability to NeedsAuth on secret-store metadata failure despite the stated fail-open contract.
  2. Low Owner-scope projection lacks ResourceScope churn test (crates/ironclaw_host_runtime/src/credential_presence_cache.rs:56-62, confidence 75) — anchor: crates/ironclaw_host_runtime/src/credential_presence_cache.rs:56
    The cache relies on CredentialOwnerScope::from_scope dropping invocation/thread/mission ids so presence survives per-render ResourceScope churn, but existing cache tests build owner scopes manually and never exercise this projection from real ResourceScope values.

local-patterns

  1. Low Cache docs describe the old product-auth presence contract (crates/ironclaw_host_runtime/src/credential_presence_cache.rs:10-12, confidence 75) — anchor: crates/ironclaw_host_runtime/src/credential_presence_cache.rs:10
    The module docs still say product-auth conclusive results are AuthRequired-vs-resolved, but the new local contract is account_configured returning Ok(true)/Ok(false) and treating backend failures as indeterminate. That stale wording points future maintainers back toward the dispatch-time resolver semantics this PR is trying to avoid.
  2. Low Failure text points at a deleted lifecycle helper (tests/reborn_group_extensions/scenario_search_ready_message_requires_credential.rs:107-114, confidence 75) — anchor: tests/reborn_group_extensions/scenario_search_ready_message_requires_credential.rs:107
    The regression failure still says search_installation_phase must consult the credential gate for LifecyclePhase::Active, but this PR removed that search-summary shape and now routes through the availability projection. If this test fails later, the message sends the maintainer to a deleted symbol instead of the current credential-gated availability path.

maintainability

  1. Low Do not encode credential presence as Option (crates/ironclaw_host_runtime/src/surface.rs:117-126, confidence 75) — anchor: crates/ironclaw_host_runtime/src/surface.rs:117
    The new credential-presence port exposes a three-state contract as Option<bool>: present, missing, and indeterminate/fail-open. The caller has to rely on == Some(false) to preserve fail-open behavior, which hides the core invariant at the boundary between surface.rs and credential_presence.rs.

&capabilities,
)?;

if let Some(presence) = self.credential_presence {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Credential presence checks serialize storage I/O per tool render.

visible_capabilities now runs on every model surface/tool render, but the new downgrade pass awaits required_credentials_present one capability at a time. On a cold cache or after the 10s TTL, each credentialed capability can perform secret-store or product-auth account lookups, so a surface with N credentialed tools adds N serialized backend round trips before the model can see tools. The cache only helps after the first completed pass and does not prevent the first render after expiry from paying the full serial latency.

Fix: Collect available capabilities needing credential checks, deduplicate equivalent requirements, and resolve them with bounded concurrency before applying the NeedsAuth downgrades.

any_missing |= !present;
continue;
}
match secret_present(store, scope, handle).await {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Secret-store backend errors lack fail-open coverage.

The new secret-handle presence path treats secret_present errors as indeterminate, but the caller-level tests cover product-auth backend failures and dispatch-time secret failures without covering this surface-render path. A regression could downgrade a capability to NeedsAuth on secret-store metadata failure despite the stated fail-open contract.

Fix: Add caller-level coverage for ProductionCredentialPresence/visible capability rendering where SecretStore::metadata errors return indeterminate/Available and do not cache a false missing credential.

//! far less often than that — so the naive approach would issue N redundant
//! presence lookups per step for no benefit.
//!
//! Only CONCLUSIVE presence results are cached (`Ok(true)`/`Ok(false)` from the

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Cache docs describe the old product-auth presence contract.

The module docs still say product-auth conclusive results are AuthRequired-vs-resolved, but the new local contract is account_configured returning Ok(true)/Ok(false) and treating backend failures as indeterminate. That stale wording points future maintainers back toward the dispatch-time resolver semantics this PR is trying to avoid.

Fix: Update the sentence to describe the side-effect-free account_configured result shape, for example: Ok(true)/Ok(false) from the product-auth account resolver; backend errors are never cached.

}

impl CredentialOwnerScope {
pub(crate) fn from_scope(scope: &ResourceScope) -> Self {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Owner-scope projection lacks ResourceScope churn test.

The cache relies on CredentialOwnerScope::from_scope dropping invocation/thread/mission ids so presence survives per-render ResourceScope churn, but existing cache tests build owner scopes manually and never exercise this projection from real ResourceScope values.

Fix: Add a cache test that builds two ResourceScopes with the same owner axes but different invocation/thread/mission ids and verifies CredentialOwnerScope::from_scope produces the same cache key.

/// `RuntimeCredentialAccountResolver` behind a short-TTL cache.
#[async_trait]
pub(crate) trait CapabilityCredentialPresence: Send + Sync {
/// `Some(true)` = all required credentials present (or none required);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Do not encode credential presence as Option.

The new credential-presence port exposes a three-state contract as Option<bool>: present, missing, and indeterminate/fail-open. The caller has to rely on == Some(false) to preserve fail-open behavior, which hides the core invariant at the boundary between surface.rs and credential_presence.rs.

Fix: Replace the Option<bool> return with a small crate-local enum such as CredentialPresenceStatus::{Present, Missing, Indeterminate} and have visible_capabilities match only Missing for the NeedsAuth downgrade.


if !bugs.is_empty() {
return Err(format!(
"bug #5416: `builtin.extension_search` mis-reported credential readiness for: {bugs:?}. \

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Failure text points at a deleted lifecycle helper.

The regression failure still says search_installation_phase must consult the credential gate for LifecyclePhase::Active, but this PR removed that search-summary shape and now routes through the availability projection. If this test fails later, the message sends the maintainer to a deleted symbol instead of the current credential-gated availability path.

Fix: Rewrite the failure text around availability/resolve_extension_availability or the credential-gated projection, rather than search_installation_phase and LifecyclePhase::Active.

…ption classification, concurrent presence resolution (#5416)

Review-comment fixes on the connection-state PR:

- Cache key: ProductAuth entries now also key on the credential setup
  kind (ManualToken vs OAuth select differently) and provider scopes
  are sorted AND deduplicated, so equivalent declarations share one
  canonical key and differing requirements never alias.
- account_configured: a Configured account with no access_secret is
  data corruption, not a missing credential — classify it Backend
  (indeterminate, uncached, capability stays Available) mirroring
  resolve_access_secret, instead of Ok(false) which rendered a
  NeedsAuth re-auth prompt that re-auth cannot fix.
- Surface downgrade pass resolves credential presence with bounded
  concurrency (buffered(8), order-preserving positional apply) instead
  of N serialized backend round trips per cold render — mirrors the
  search path's EXTENSION_READINESS_CONCURRENCY pattern.
- Presence port returns a three-state CredentialPresenceStatus
  (Present/Missing/Indeterminate) instead of Option<bool>; the
  downgrade matches only Missing, making the fail-open invariant a
  type-level contract.
- Tests: fail-open coverage for secret-store metadata errors on the
  render path (toggleable failing store, asserts no stale cache
  entry); owner-scope projection churn test from real ResourceScopes;
  setup-kind key-distinctness; 20-capability concurrent downgrade
  ordering regression; exact tool-name assertion (github__create_issue,
  no create_issue_comment collision).
- Docs: cache module contract rewritten to the side-effect-free
  account_configured shape; group-scenario failure text no longer
  references deleted search_installation_phase symbols.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5528 July 2, 2026 16:00 Destroyed
@henrypark133

Copy link
Copy Markdown
Collaborator Author

All review comments addressed in 6206ee9 — thanks, every one was valid:

Comment Fix
scopes dedup (gemini) sort() + dedup() — canonical key regardless of duplicate declarations
setup mode missing from cache key (codex P2) New CredentialSetupKind (ManualToken/OAuth discriminant) as a key field; exhaustive match so a future variant is a compile error. OAuth's inner consent-scope payload deliberately excluded — selection semantics read the separately-keyed provider_scopes, including it would double-encode
Configured-without-access_secret → NeedsAuth (codex P2) Now Err(Backend) (indeterminate, uncached, stays Available) mirroring resolve_access_secret's corruption classification — re-auth can't fix corrupt state
serialized presence I/O per render Bounded-concurrent resolution (buffered(8), order-preserving positional apply), mirroring the search path's EXTENSION_READINESS_CONCURRENCY pattern; 20-capability ordering regression test
missing fail-open coverage for secret-store errors Caller-level test with a toggleable failing store: stays Available, nothing cached, recovers on next render
Option<bool> port contract CredentialPresenceStatus { Present, Missing, Indeterminate }; downgrade matches only Missing
owner-scope projection untested Churn test from real ResourceScopes (same owner axes, different invocation/thread/mission → same key)
contains("create_issue") (Copilot) Exact tool.name == "github__create_issue"
stale cache docs / deleted-symbol failure text Both rewritten to the current contract/path

Verification: host_runtime 307 lib + all contracts green (3 pre-existing docker-socket sandbox_process failures only, reproduced on the base commit via stash), composition product_auth 120/120, both e2e suites 22/22, clippy byte-identical to base (stash-diffed), fmt clean.

🤖 Generated with Claude Code

@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 `@docs/plans/2026-07-01-reborn-google-connection-state-5416.md`:
- Around line 189-231: The design doc contains duplicated near-verbatim
paragraphs for the “Concurrency (round-2 finding B)” and “Behavior-flip to lock
with a test” sections, likely from a merge artifact. Collapse the repeated text
into a single authoritative copy, keeping one version of each point in the
discussion around search readiness and the
`search()`/`lifecycle_extension_infos` behavior so the plan reads cleanly and
unambiguously.
🪄 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: 761b86ff-5c8f-454d-9988-46e9ef1a5141

📥 Commits

Reviewing files that changed from the base of the PR and between 5678364 and 6206ee9.

📒 Files selected for processing (37)
  • crates/ironclaw_host_runtime/src/credential_presence.rs
  • crates/ironclaw_host_runtime/src/credential_presence_cache.rs
  • crates/ironclaw_host_runtime/src/lib.rs
  • crates/ironclaw_host_runtime/src/obligations.rs
  • crates/ironclaw_host_runtime/src/production.rs
  • crates/ironclaw_host_runtime/src/services.rs
  • crates/ironclaw_host_runtime/src/surface.rs
  • crates/ironclaw_host_runtime/tests/builtin_obligation_handler_contract.rs
  • crates/ironclaw_host_runtime/tests/github_wasm_runtime_contract.rs
  • crates/ironclaw_host_runtime/tests/host_runtime_visible_capability_credential_presence_contract.rs
  • crates/ironclaw_host_runtime/tests/runtime_http_egress_contract.rs
  • crates/ironclaw_loop_support/src/capability_port.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_product_workflow/src/lib.rs
  • crates/ironclaw_product_workflow/src/lifecycle.rs
  • crates/ironclaw_product_workflow/src/reborn_services/extension_onboarding.rs
  • crates/ironclaw_product_workflow/src/reborn_services/extensions.rs
  • crates/ironclaw_product_workflow/src/reborn_services/types.rs
  • crates/ironclaw_product_workflow/tests/reborn_services_contract.rs
  • crates/ironclaw_reborn_composition/src/extension_availability.rs
  • crates/ironclaw_reborn_composition/src/extension_lifecycle.rs
  • crates/ironclaw_reborn_composition/src/extension_lifecycle_capabilities.rs
  • crates/ironclaw_reborn_composition/src/extension_lifecycle_command.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rs
  • crates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rs
  • docs/plans/2026-07-01-reborn-google-connection-state-5416.md
  • docs/plans/2026-07-01-reborn-tool-lifecycle-unified-pipeline-northstar.md
  • tests/reborn_group_extensions/main.rs
  • tests/reborn_group_extensions/scenario_activate_then_active_cross_thread.rs
  • tests/reborn_group_extensions/scenario_install_then_visible_cross_thread.rs
  • tests/reborn_group_extensions/scenario_remove_then_absent_cross_thread.rs
  • tests/reborn_group_extensions/scenario_search_ready_message_requires_credential.rs
  • tests/reborn_integration_auth_gate.rs
  • tests/support/reborn/builder.rs
  • tests/support/reborn/group.rs
  • tests/support/reborn/harness.rs

Comment on lines +189 to +231
**Concurrency (round-2 finding B).** Today the sequential per-extension credential
check in `search()` (`extension_lifecycle.rs:194` — `for extension { push(search_
summary(..).await) }`) only fires for `Installed`-phase extensions (A1 skips
`Active`). Projecting `availability` for **every** result widens a live
O(extensions × requirements) sequential await chain (each `missing_requirements`
hits account selection per requirement). The list path already solved this one file
away: `lifecycle_extension_infos` (`reborn_services/extensions.rs:192-213`) fans out
with `stream::iter(..).buffered(EXTENSION_READINESS_CONCURRENCY)` (const = 8,
ceiling test at `:515`; `futures = "0.3"` is already a composition dep,
`Cargo.toml:111`). Adopt the same pattern for the search loop. Lives in the
extracted module (§4.6).

**Backend degrade.** A `Backend` blip currently **fails the entire search**
(`map_search_credential_stage_error` → `Transient`). New: it becomes `unknown`
(search still returns; that extension shown not-`available`). Fail-closed for the
"ready" claim, search stays up.

**Behavior-flip to lock with a test (round-1 finding #7):** the old
`search_credentials_configured` short-circuited `requirements.is_empty() → false`
*before* consulting the gate, so a **credential-less** extension never reached
"ready" via search. Under the projection it becomes `available` for an installed
credential-less extension. That is the intended, more-correct

**Concurrency (round-2 finding B).** Today the sequential per-extension credential
check in `search()` (`extension_lifecycle.rs:194` — `for extension { push(search_
summary(..).await) }`) only fires for `Installed`-phase extensions (A1 skips
`Active`). Closing A1 means computing readiness for **every** result, widening a
live O(extensions × requirements) sequential await chain (each
`missing_requirements` hits account selection per requirement). The list path
already solved this one file away: `lifecycle_extension_infos`
(`reborn_services/extensions.rs:192-213`) fans out with
`stream::iter(..).buffered(EXTENSION_READINESS_CONCURRENCY)` (const = 8, with a
ceiling test at `:515`). Adopt the **same** canonical pattern for the search loop —
don't leave a widened sequential fan-out. This lives naturally in the extracted
module (§4.6).

**Behavior-flip to lock with a test (round-1 finding #7):** the old
`search_credentials_configured` short-circuited `requirements.is_empty() → false`
*before* consulting the gate, so a **credential-less** extension never reached
"ready" via search. Under the projection it becomes `available` for an installed
credential-less extension. That is the intended, more-correct behavior (an
extension needing no credentials is legitimately "don't ask for credentials"), but
it is a silent change — pin it explicitly in §6.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated paragraph in the design doc.

Lines 189-199 and 212-223 ("Concurrency (round-2 finding B)") and lines 206-211 and 225-231 ("Behavior-flip to lock with a test") are near-verbatim duplicates — looks like a merge artifact from combining review rounds. Worth collapsing to one copy so future readers don't wonder which is authoritative.

🤖 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 `@docs/plans/2026-07-01-reborn-google-connection-state-5416.md` around lines
189 - 231, The design doc contains duplicated near-verbatim paragraphs for the
“Concurrency (round-2 finding B)” and “Behavior-flip to lock with a test”
sections, likely from a merge artifact. Collapse the repeated text into a single
authoritative copy, keeping one version of each point in the discussion around
search readiness and the `search()`/`lifecycle_extension_infos` behavior so the
plan reads cleanly and unambiguously.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

Closing as stale — no activity in over three weeks. The branch is untouched; reopen if this is still needed.

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5528 — 6206ee97 Deployed Jul 2, 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.

[QA] Incorrect Google connection state causes contradictory authentication flow

2 participants