Fix Reborn credential delete and same-run reauth - #5174
henrypark133 wants to merge 5 commits into
Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds conditional credential mutation methods, carries credential-account metadata through staging and egress, adds typed unauthorized markers to HTTP egress responses, and wires 401 recovery plus reauth into composition. Also adds a product-auth accounts revoke route and updates affected fixtures and tests. ChangesCredential Unauthorized Recovery & Conditional Account Mutation
Sequence Diagram(s)sequenceDiagram
participant Guest as Runtime guest
participant Pipeline as Egress pipeline
participant Inject as apply_credential_injections
participant Recover as RuntimeCredentialUnauthorizedRecoveryEgress
participant Accounts as CredentialAccountService
participant Bridge as RuntimeCredentialReauthBridge
participant Reauth as RuntimeCredentialReauthHostRuntime
Guest->>Pipeline: HTTP egress request
Pipeline->>Inject: apply_credential_injections(...)
Inject-->>Pipeline: CredentialInjectionResult
Pipeline->>Recover: execute(request)
Recover->>Accounts: revoke_if_unchanged(...) or refresh_if_unchanged(...)
Accounts-->>Recover: Some(...) or None
Recover->>Bridge: record_recovered_auth_required(...)
Pipeline-->>Guest: RuntimeHttpEgressResponse
Reauth->>Reauth: invoke/spawn/resume → AuthRequired when bridge has records
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a robust mechanism to handle unauthorized (401) responses for injected credentials in host-staged runtimes. It adds conditional revocation and refresh capabilities (revoke_if_unchanged and refresh_if_unchanged) to the CredentialAccountService, and integrates an unauthorized recovery pipeline via RuntimeCredentialUnauthorizedRecoveryEgress and a re-authentication bridge. Feedback focuses on addressing potential race conditions in the default non-atomic implementations of the new service methods, and replacing warn! and info! logging with debug! in REPL/TUI-reachable code to prevent interface corruption.
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.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a3ed3d17c
ℹ️ 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".
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
crates/ironclaw_auth/src/credential.rs (1)
967-984: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReturn
Nonewhen terminal refresh races a newer account.After
provider.refresh_token(...).await, another actor may have reauthorized or updated the account. In theInvalidGrant/RefreshFailedarms,report_terminal_refresh_status()returns a report for the changed current account, and Lines 967/976 wrap that asSome, so recovery treats a stale marker as handled instead of skipped. Threadexpected_updated_atinto this terminal-status path and returnOk(None)on mismatch.Suggested shape
- Err(AuthProductError::InvalidGrant) => self - .report_terminal_refresh_status( + Err(AuthProductError::InvalidGrant) => self + .report_terminal_refresh_status_if_unchanged( &lookup_request, &account, + expected_updated_at, request.requester_extension.as_ref(), CredentialAccountStatus::Revoked, ) - .await - .map(Some), + .await,Apply the same pattern to the
RefreshFailed | TokenExchangeFailedarm.Based on PR objectives and the recovery wrapper contract, stale
expected_updated_atmismatches must surface asOk(None).🤖 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_auth/src/credential.rs` around lines 967 - 984, The InvalidGrant and RefreshFailed error arms currently always wrap the report_terminal_refresh_status result as Some, but they should check for expected_updated_at mismatches and return Ok(None) when the account has been updated by another actor since the refresh attempt. Thread the expected_updated_at value into the report_terminal_refresh_status calls in both error arms (line 967 and line 976), then add logic to check if the returned report indicates a mismatch with expected_updated_at; if mismatched, return Ok(None) instead of Ok(Some(...)) to properly surface stale markers as skipped rather than handled.crates/ironclaw_host_runtime/src/egress/host_port.rs (1)
155-221: 📐 Maintainability & Code Quality | 🟠 MajorRemove dead marker-computation code from
stage_credentials; the inner pipeline already computes it.Lines 178–220 duplicate the exact ambiguity-suppression + marker-selection logic that
apply_credential_injectionsperforms inegress/credential.rsviarecord_unauthorized_identity. However, thestaged_unauthorized_identitycomputed here is never passed to the request. The innerruntime_http_egress.execute()(which isHostHttpEgressService, runningpipeline::execute) independently reads each credential's stagedcredential_accountand recomputes the marker, returning it inCredentialInjectionResult. By the time the outer.map(...)at lines 159–165 callsattach_credential_unauthorized_on_401, the pipeline has already setresponse.credential_unauthorized, so the guardis_none()prevents the host_port's copy from ever being applied — making those 43 lines dead code.The test passes only because
RecordingRuntimeHttpEgressis a stub that skips the pipeline entirely; thus the test exercises logic that never fires in production (violates "test through the caller").Fix: Delete the
stage_credentialsmarker logic (lines 178–220); it adds no value and masks the real source of truth.🤖 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/egress/host_port.rs` around lines 155 - 221, In the `stage_credentials` method, remove all the marker computation logic including the declarations of `unauthorized_identity` and `ambiguous_unauthorized_identity`, the conditional block that checks for `credential_account` and calls `marker_on_unauthorized()`, and the final return statement that evaluates the ambiguity condition. Instead, simply return `Ok(None)` at the end of the method after staging the credentials and pushing all credential injections, since the pipeline's `apply_credential_injections` function already handles marker computation independently.
🤖 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_auth/src/credential.rs`:
- Around line 510-549: The revoke_if_unchanged and refresh_if_unchanged methods
perform a read-then-write operation that is not atomic. They check if
account.updated_at matches expected_updated_at, then call self.update_status or
self.refresh_account separately, creating a race condition where the account
could be modified between the check and mutation. To fix this, refactor the
mutation operations (self.update_status and self.refresh_account) to accept the
expected_updated_at timestamp as a parameter and perform the revision check
atomically within the mutation call itself, using a compare-and-swap or
transactional approach so that the timestamp validation and state mutation
happen as a single atomic operation.
In `@crates/ironclaw_host_api/src/http.rs`:
- Around line 117-128: The account_id field is currently defined as a raw String
type, but according to coding guidelines identifiers should use newtypes for
type safety. Create a new validated newtype called RuntimeCredentialAccountId
(or reuse an existing account-id type from a crate dependency if the dependency
direction allows it), and then replace the account_id String field in the struct
definition shown in the diff with this new typed identifier. Also apply the same
change to the account_id fields in the other structs mentioned in lines 354-383
to ensure consistency across all identity, unauthorized marker, and key structs.
In `@crates/ironclaw_host_runtime/src/egress/credential.rs`:
- Around line 287-292: The early return when marker_on_unauthorized() returns
None (due to missing account_updated_at or auth_requirement) causes incomplete
credential-account metadata to be treated permissively, allowing markerable
credentials to win when paired with unmarkerable ones whose 401s cannot be
safely attributed. Remove the early return pattern that bypasses marker
suppression when marker_on_unauthorized() is None, and instead treat incomplete
account metadata as an ambiguous scenario where the marker should be suppressed,
ensuring the function fails closed by not proceeding with the marker when
account metadata is incomplete.
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 1228-1234: The RuntimeCredentialReauthBridge wrapper is being
applied to services via attach_runtime_credential_unauthorized_recovery, but
subsequent code paths are bypassing this wrapper by using
product_auth_runtime_ports.runtime_http_egress() directly instead of
services.runtime_http_egress(). Locate all places where
product_auth_runtime_ports.runtime_http_egress() is assigned or passed after the
attach_runtime_credential_unauthorized_recovery call, including assignments to
local_runtime.runtime_http_egress and ProductAuthRuntimeGsuiteCredentialStager
initialization at lines 1305, 1327, 3857 and elsewhere. Verify whether these
code paths intentionally require unwrapped egress for OAuth provider
interactions, and if not, replace
product_auth_runtime_ports.runtime_http_egress() with
services.runtime_http_egress() to ensure the credential recovery wrapper is
applied consistently.
In `@crates/ironclaw_reborn_composition/src/runtime_credential_reauth.rs`:
- Around line 123-126: The invoke_capability method (and similarly
spawn_capability, resume_capability, auth_resume_capability, and
resume_spawn_capability methods) currently calls
self.inner.invoke_capability(request).await? which exits early on error before
apply_reauth can be called. This leaves stale bridge records in memory. Modify
the error handling to drain the matching bridge record using the scope and
capability_id before returning the error, either by wrapping the invocation in a
match statement or extracting a helper function that handles both the success
path (calling apply_reauth) and error path (draining the record). Apply this
pattern consistently across all four methods mentioned in the review comment.
- Around line 45-48: The RuntimeCredentialReauthRecord struct needs to store the
full ResourceScope to satisfy the scoped-state invariant. Add a field to the
RuntimeCredentialReauthRecord struct to hold the complete ResourceScope, then
update the record creation around line 45 to include the full scope from the
scope object (alongside invocation_id and capability_id), and finally update the
matching/lookup logic around lines 66-68 to compare the full ResourceScope in
addition to invocation_id and capability_id to prevent reused invocation IDs
from consuming another owner's recovered auth requirements.
In `@crates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rs`:
- Around line 51-52: Add a validation check to ensure that the scope derived
from unauthorized.scope matches the request_scope before proceeding with
credential mutations. After creating the scope variable using
AuthProductScope::credential_owner with unauthorized.scope, add a guard
condition that compares this scope against request_scope and returns an error if
they do not match. Apply the same scope validation pattern to the code block at
lines 64-72 that also performs credential operations. This prevents mis-scoped
markers from mutating credentials across different authorization contexts.
- Around line 112-118: The Err branches in the recovery function (around line
112-118 and lines 161-167) are currently logging warnings and returning false,
which silently swallows authentication and state infrastructure failures.
Instead of warn-and-continue, these error cases should propagate the error to
the caller. Modify the Err(error) branches where tracing::warn! is called to
return an error result instead of returning false, allowing the recovery
operation to fail loudly in execute() as intended. Keep the Ok(None) case
unchanged as a valid no-op for stale accounts.
- Around line 40-53: The code currently treats the presence of
credential_unauthorized field as sufficient authority to revoke or refresh
credentials, but it should also verify the HTTP status code is 401 before
proceeding with credential mutation. Add an additional guard condition to check
that response.status equals 401 before allowing the credential_unauthorized
processing logic to continue. This ensures that only genuine 401 Unauthorized
responses trigger credential mutation, preventing malformed or buggy responses
with other status codes (like 403 or 200) from incorrectly revoking credentials.
Place this status code check at the beginning of the function, ideally before or
immediately after the existing credential_unauthorized check.
- Around line 177-178: The map_err closure on the AuthProviderId::new call is
ignoring the actual parse error by using an underscore binding, which drops
valuable debugging information. Replace the ignored error binding with a named
binding (e.g., err) and either include that error detail in the
AuthProductError::MalformedConfig error type if it supports carrying causes, or
log the specific error at debug! level before mapping to the generic
MalformedConfig error. This ensures the underlying cause of the AuthProviderId
parse failure is preserved for troubleshooting.
---
Outside diff comments:
In `@crates/ironclaw_auth/src/credential.rs`:
- Around line 967-984: The InvalidGrant and RefreshFailed error arms currently
always wrap the report_terminal_refresh_status result as Some, but they should
check for expected_updated_at mismatches and return Ok(None) when the account
has been updated by another actor since the refresh attempt. Thread the
expected_updated_at value into the report_terminal_refresh_status calls in both
error arms (line 967 and line 976), then add logic to check if the returned
report indicates a mismatch with expected_updated_at; if mismatched, return
Ok(None) instead of Ok(Some(...)) to properly surface stale markers as skipped
rather than handled.
In `@crates/ironclaw_host_runtime/src/egress/host_port.rs`:
- Around line 155-221: In the `stage_credentials` method, remove all the marker
computation logic including the declarations of `unauthorized_identity` and
`ambiguous_unauthorized_identity`, the conditional block that checks for
`credential_account` and calls `marker_on_unauthorized()`, and the final return
statement that evaluates the ambiguity condition. Instead, simply return
`Ok(None)` at the end of the method after staging the credentials and pushing
all credential injections, since the pipeline's `apply_credential_injections`
function already handles marker computation independently.
🪄 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: cfa2ebe2-5b96-4baa-af8c-ee20eeb9bda7
📒 Files selected for processing (47)
crates/ironclaw_auth/src/credential.rscrates/ironclaw_auth/src/fakes.rscrates/ironclaw_auth/tests/auth_product_contract/refresh_contract.rscrates/ironclaw_first_party_extensions/src/gsuite/handlers.rscrates/ironclaw_first_party_extensions/src/web_access.rscrates/ironclaw_first_party_extensions/tests/support/mod.rscrates/ironclaw_host_api/src/http.rscrates/ironclaw_host_api/tests/host_api_contract.rscrates/ironclaw_host_runtime/src/egress/credential.rscrates/ironclaw_host_runtime/src/egress/host_port.rscrates/ironclaw_host_runtime/src/egress/mod.rscrates/ironclaw_host_runtime/src/egress/pipeline.rscrates/ironclaw_host_runtime/src/obligations.rscrates/ironclaw_host_runtime/src/services/tests.rscrates/ironclaw_host_runtime/src/wasm_credentials.rscrates/ironclaw_host_runtime/tests/builtin_obligation_handler_contract.rscrates/ironclaw_host_runtime/tests/first_party_builtin_tools.rscrates/ironclaw_host_runtime/tests/github_wasm_runtime_contract.rscrates/ironclaw_host_runtime/tests/host_runtime_services_contract.rscrates/ironclaw_host_runtime/tests/runtime_http_egress_contract.rscrates/ironclaw_mcp/tests/mcp_adapter_contract.rscrates/ironclaw_reborn_composition/src/auth_dcr_tests.rscrates/ironclaw_reborn_composition/src/extension_lifecycle.rscrates/ironclaw_reborn_composition/src/extension_lifecycle/hosted_mcp_test_support.rscrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/factory/auth_tests.rscrates/ironclaw_reborn_composition/src/lib.rscrates/ironclaw_reborn_composition/src/oauth_dcr.rscrates/ironclaw_reborn_composition/src/oauth_provider_client.rscrates/ironclaw_reborn_composition/src/oauth_provider_client/tests.rscrates/ironclaw_reborn_composition/src/product_auth_durable/accounts.rscrates/ironclaw_reborn_composition/src/product_auth_providers.rscrates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rscrates/ironclaw_reborn_composition/src/product_auth_runtime_credentials/tests.rscrates/ironclaw_reborn_composition/src/product_auth_serve/accounts.rscrates/ironclaw_reborn_composition/src/product_auth_serve/mod.rscrates/ironclaw_reborn_composition/src/product_auth_serve/oauth_start_tests.rscrates/ironclaw_reborn_composition/src/projection/tests/turn_stream_auth.rscrates/ironclaw_reborn_composition/src/runtime_credential_reauth.rscrates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rscrates/ironclaw_reborn_composition/src/runtime_credential_unauthorized/tests.rscrates/ironclaw_reborn_composition/src/slack_egress.rscrates/ironclaw_reborn_composition/tests/gsuite.rscrates/ironclaw_scripts/tests/script_http_adapter_contract.rscrates/ironclaw_wasm/tests/wasm_dispatch_integration.rscrates/ironclaw_wasm/tests/wasm_http_adapter_contract.rstests/support/reborn/harness.rs
|
🚅 Deployed to the ironclaw-pr-5174 environment in ironclaw-ci-preview
|
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (multi-agent)
Intent: Fix Reborn runtime credential deletion and same-run reauth so 401 recovery immediately gates the current run without breaking OAuth refresh behavior.
Stats: 3 findings (from 8 raw reviewer findings; 5 after local validation; 3 after deduping against current live review threads) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0.
Dedup note: current live threads already cover the non-atomic conditional mutation concern, the anonymous map_err(|_| ...) provider parse mapping, and incomplete-marker behavior, so this review does not repost those.
Tests
- Medium Conditional revoke lacks requester-extension coverage (
crates/ironclaw_auth/src/credential.rs:510-529, confidence 78) — anchor:crates/ironclaw_auth/src/credential.rs:510
The newrevoke_if_unchangedpath is only exercised for the no-requester case. There is no test proving that a carriedrequester_extensionis included in the lookup and still revokes an extension-owned account when the timestamp matches.
Maintainability
-
Medium Collapse the split staged-secret metadata API (
crates/ironclaw_host_runtime/src/obligations.rs:100-210, confidence 84) — anchor:crates/ironclaw_host_runtime/src/obligations.rs:130
This turns one secret-injection store into two parallel API families (insert/take/clone_materialand their*_with_metadatavariants) plus a wrapper type that just carriesmaterialandcredential_account. That makes every caller choose between near-identical paths and keeps the metadata flow duplicated across the store, the stager, and the egress code. -
Low Split the stale-aware refresh path from the normal refresh path (
crates/ironclaw_auth/src/credential.rs:875-987, confidence 68) — anchor:crates/ironclaw_auth/src/credential.rs:875
refresh_account_from_snapshotnow uses anOption<Timestamp>as a hidden mode switch, and the stale check is repeated before the lock, after the reread, and again in the provider-refresh branch. That makes one helper carry two contracts at once and forces readers to keep theNone/Somemeaning in their head while following the refresh flow.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
crates/ironclaw_auth/src/credential.rs (1)
815-819: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winUpdate the stale-write contract comment.
The helper now has two behaviors when another writer wins: normal refresh reports the current state, but expected-revision refresh returns
Ok(None). The comment still describes only the old plain-report path.As per coding guidelines, “Comments that promise guarantees across layers must either be enforced by code/tests or softened to describe intent.”
Suggested comment update
- /// first and bails to a plain report if another writer changed it under us. + /// first; normal refresh reports the current state if another writer changed + /// it, while expected-revision refresh returns `Ok(None)`.🤖 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_auth/src/credential.rs` around lines 815 - 819, The comment for the helper function at lines 815-819 in credential.rs describes outdated behavior for handling stale writes. Update the comment to reflect the current two-path behavior: clarify that when another writer changes the account under us, normal refresh reports the current state while expected-revision refresh returns Ok(None). Remove references to the old plain-report path behavior and ensure the comment accurately describes both code paths that handle the stale-write condition.Source: Coding guidelines
crates/ironclaw_host_runtime/src/egress/host_port.rs (2)
61-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPreserve the secret-staging failure cause.
map_err(|_| RuntimeSecretStageError::Backend)drops the insert failure at the secret/egress boundary. Bind the error and carry it through a contextual variant, or log sanitized debug context before mapping.As per coding guidelines, “Do not use
.map_err(|_| OtherError)… Carry the cause instead,” and as per path instructions, “Fail loud.”🤖 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/egress/host_port.rs` around lines 61 - 69, The map_err call in the insert_with_credential_account method is dropping the error information by using the ignore pattern |_|, which prevents debugging of secret staging failures. Modify the map_err to capture the actual error (bind it to a variable instead of using underscore) and either pass it through a contextual error variant that preserves the cause or log the sanitized error details before mapping to RuntimeSecretStageError::Backend. This ensures error context is preserved for failure diagnosis while maintaining security by avoiding sensitive data exposure in logs.Sources: Coding guidelines, Path instructions
166-174: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winReject cross-scope credential-account metadata before staging.
Line 166 accepts
credential_accountwithout proving it belongs torequest.scope; the new 401 fixtures create marker/account scopes independently fromhost_request(), proving this path accepts mismatched metadata. That breaks same-run recovery and can bind staged secret material to the wrong auth recovery record. Validate the scope beforeinsert_with_credential_account, and update the 401 tests to derive marker/account scope fromrequest.request.scope.As per coding guidelines, “Preserve tenant/user/agent/project/mission/thread scope on authority, state, memory, process, network, outbound, resource, and event records.”
Suggested validation
for credential in credentials { + if let Some(account) = credential.credential_account.as_ref() { + if &account.scope != &request.scope { + return Err(RuntimeHttpEgressError::Credential { + reason: "host credential account metadata did not match request scope" + .to_string(), + }); + } + } let credential_account = credential.credential_account.clone(); self.secret_stager .stage_secret_material_once_with_account(Also applies to: 449-486, 535-570
🤖 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/egress/host_port.rs` around lines 166 - 174, The credential_account parameter is being passed to stage_secret_material_once_with_account without validating that it belongs to the same scope as request.scope, which can cause secret material to be bound to the wrong auth recovery record. Add a validation check before the call to stage_secret_material_once_with_account that verifies the credential_account scope matches request.scope, and reject it with appropriate error handling if they do not match. Apply this same validation pattern to the other similar credential staging calls in the file at the locations noted in the comment.Source: Coding guidelines
crates/ironclaw_reborn_composition/src/runtime_credential_unauthorized/tests.rs (1)
717-744: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winMake the no-op refresh fake validate the full request.
This fake ignores
request.requester_extensionandrequest.provider, so it can pass even if recovery calls refresh for the wrong extension/provider. Mirror production validation before returning the no-op report.Validate requester and provider in the fake
- let Some(account) = self - .inner - .get_account(CredentialAccountLookupRequest::new( - request.scope.clone(), - request.account_id, - )) - .await? + let mut lookup = + CredentialAccountLookupRequest::new(request.scope.clone(), request.account_id); + if let Some(requester_extension) = request.requester_extension.clone() { + lookup = lookup.for_extension(requester_extension); + } + let Some(account) = self.inner.get_account(lookup).await? else { return Ok(None); }; + if account.provider != request.provider { + return Err(AuthProductError::CrossScopeDenied); + } + if account.status != CredentialAccountStatus::Configured { + return Err(AuthProductError::CredentialMissing); + } if account.updated_at != expected_updated_at { return Ok(None); }As per path instructions, “When mocking a multi-arg runtime API, the mock must capture every argument the production caller passes.”
🤖 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/runtime_credential_unauthorized/tests.rs` around lines 717 - 744, The fake refresh_if_unchanged method currently validates the account.updated_at against expected_updated_at but does not validate the request.requester_extension and request.provider fields that the production implementation would validate. Add validation checks for request.requester_extension and request.provider before returning the Ok(Some(CredentialRefreshReport)) to ensure the fake properly validates the full request and aligns with the production behavior, preventing invalid recovery calls from passing through undetected.Source: Path instructions
crates/ironclaw_host_runtime/src/egress/credential.rs (1)
282-289: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winTreat missing account metadata as ambiguous too.
Line 287 returns without suppressing a marker when another injected credential has no
credential_account. A 401 from that request is still unattributable, so the account-backed credential can be revoked/refreshed incorrectly. Fail closed the same way as incomplete metadata.Fail closed for non-account-backed injected credentials
let Some(account) = account else { + *ambiguous_unauthorized_identity = true; return; };As per coding guidelines, “Fail closed for auth, approvals, trust, filesystem containment, network policy, secret leases, runtime selection, and adapter identity.”
🤖 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/egress/credential.rs` around lines 282 - 289, The record_unauthorized_identity function currently returns early without setting any marker when the account is None, but missing credential_account metadata makes the unauthorized identity unattributable and requires following the "fail closed" principle for auth decisions. Modify the early return logic (the let Some(account) = account else block) to set ambiguous_unauthorized_identity to true before returning, ensuring that credentials without account metadata are treated the same way as those with incomplete metadata.Source: Coding guidelines
🤖 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_api/tests/host_api_contract.rs`:
- Around line 1150-1153: In the round-trip test for
RuntimeCredentialUnauthorized, change the `account_surface` field from the serde
default variant `RuntimeCredentialAccountSurface::Api` to a non-default variant
so that deserialization regressions would be caught. Then add an assertion after
deserialization to verify that the `account_surface` field in the deserialized
object matches the non-default variant that was set. Apply the same change to
both occurrences of this pattern in the test (around lines 1150-1153 and
1172-1190).
In `@crates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rs`:
- Around line 51-57: The uuid::Uuid::parse_str call in the unauthorized marker
validation silently handles parse failures by returning Ok(()) instead of
failing loudly, which can hide contract breakage. Replace the silent downgrade
pattern (where parsing unauthorized.account_id fails and returns Ok(())) with
explicit error handling that propagates or clearly signals the failure, and
apply the same fix to the similar pattern around line 136 that currently returns
Ok(false) on parse failure.
---
Outside diff comments:
In `@crates/ironclaw_auth/src/credential.rs`:
- Around line 815-819: The comment for the helper function at lines 815-819 in
credential.rs describes outdated behavior for handling stale writes. Update the
comment to reflect the current two-path behavior: clarify that when another
writer changes the account under us, normal refresh reports the current state
while expected-revision refresh returns Ok(None). Remove references to the old
plain-report path behavior and ensure the comment accurately describes both code
paths that handle the stale-write condition.
In `@crates/ironclaw_host_runtime/src/egress/credential.rs`:
- Around line 282-289: The record_unauthorized_identity function currently
returns early without setting any marker when the account is None, but missing
credential_account metadata makes the unauthorized identity unattributable and
requires following the "fail closed" principle for auth decisions. Modify the
early return logic (the let Some(account) = account else block) to set
ambiguous_unauthorized_identity to true before returning, ensuring that
credentials without account metadata are treated the same way as those with
incomplete metadata.
In `@crates/ironclaw_host_runtime/src/egress/host_port.rs`:
- Around line 61-69: The map_err call in the insert_with_credential_account
method is dropping the error information by using the ignore pattern |_|, which
prevents debugging of secret staging failures. Modify the map_err to capture the
actual error (bind it to a variable instead of using underscore) and either pass
it through a contextual error variant that preserves the cause or log the
sanitized error details before mapping to RuntimeSecretStageError::Backend. This
ensures error context is preserved for failure diagnosis while maintaining
security by avoiding sensitive data exposure in logs.
- Around line 166-174: The credential_account parameter is being passed to
stage_secret_material_once_with_account without validating that it belongs to
the same scope as request.scope, which can cause secret material to be bound to
the wrong auth recovery record. Add a validation check before the call to
stage_secret_material_once_with_account that verifies the credential_account
scope matches request.scope, and reject it with appropriate error handling if
they do not match. Apply this same validation pattern to the other similar
credential staging calls in the file at the locations noted in the comment.
In
`@crates/ironclaw_reborn_composition/src/runtime_credential_unauthorized/tests.rs`:
- Around line 717-744: The fake refresh_if_unchanged method currently validates
the account.updated_at against expected_updated_at but does not validate the
request.requester_extension and request.provider fields that the production
implementation would validate. Add validation checks for
request.requester_extension and request.provider before returning the
Ok(Some(CredentialRefreshReport)) to ensure the fake properly validates the full
request and aligns with the production behavior, preventing invalid recovery
calls from passing through undetected.
🪄 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: 8f122753-d5fc-455b-94a0-694e019cbc81
📒 Files selected for processing (14)
crates/ironclaw_auth/src/credential.rscrates/ironclaw_auth/src/fakes.rscrates/ironclaw_auth/tests/auth_product_contract/refresh_contract.rscrates/ironclaw_host_api/src/http.rscrates/ironclaw_host_api/tests/host_api_contract.rscrates/ironclaw_host_runtime/src/egress/credential.rscrates/ironclaw_host_runtime/src/egress/host_port.rscrates/ironclaw_reborn_composition/src/auth.rscrates/ironclaw_reborn_composition/src/factory.rscrates/ironclaw_reborn_composition/src/product_auth_durable/accounts.rscrates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rscrates/ironclaw_reborn_composition/src/runtime_credential_reauth.rscrates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rscrates/ironclaw_reborn_composition/src/runtime_credential_unauthorized/tests.rs
Address PR #5174 review feedback: - Replace raw `String` credential account id with a validated `RuntimeCredentialAccountId` newtype (via the existing `uuid_id!` macro) across the three host-api marker structs (`RuntimeCredentialAccountIdentity`, `RuntimeCredentialUnauthorized`, `RuntimeCredentialUnauthorizedAccountKey`). Validation moves to the boundary, so the recovery consumer drops its `Uuid::parse_str` branch. Wire format is unchanged (`#[serde(transparent)]` over the uuid string). - Collapse the duplicated staged-secret API families into single methods that always carry the optional credential-account metadata: `insert`/`take`/`clone_material` on `RuntimeSecretInjectionStore` and `stage_secret_material_once` on the stager. Deletes the thin `*_with_credential_account` / `*_with_metadata` pass-throughs; `RuntimeStagedSecretMaterial` is now the single staged return shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…surface Address two newer PR #5174 review comments: - `refresh_if_unchanged` no longer swallows a malformed provider id in the unauthorized marker as `Ok(false)`; the `MalformedConfig` error now propagates so recovery fails loud instead of silently skipping same-run re-auth. (The account-id parse no-op was already removed by the `RuntimeCredentialAccountId` newtype.) - The `RuntimeCredentialUnauthorized` round-trip test now uses the non-default `Callback` account surface and asserts it survives the round-trip, so a dropped-field deserialization regression is caught. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_host_runtime/src/egress/host_port.rs (1)
132-156: 🎯 Functional Correctness | 🟡 MinorCover the staged-account 401 path, not just marker pass-through. The host-port tests only use
RecordingRuntimeHttpEgress::responding_with_marker(...), so they never provecredential_accountstaging feedsRuntimeCredentialAccountIdentity::marker_on_unauthorized()into the 401 response. Add a caller-level test that drives the real egress pipeline, or soften the line 132 comment.🤖 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/egress/host_port.rs` around lines 132 - 156, The host-port coverage only verifies marker pass-through and does not exercise the staged-account 401 path, so it never proves that staged credential_account data is converted into RuntimeCredentialAccountIdentity::marker_on_unauthorized() during unauthorized responses. Update the tests around host_port::stage_credentials and the RuntimeHttpEgress execution path to drive the real egress pipeline with staged credentials, or adjust the comment in the host-port wrapper to match the behavior actually covered. Use the existing RecordingRuntimeHttpEgress::responding_with_marker setup as a reference, but add a caller-level test that asserts the 401 response reflects the staged account identity rather than only the marker.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_host_runtime/src/egress/host_port.rs`:
- Around line 132-156: The host-port coverage only verifies marker pass-through
and does not exercise the staged-account 401 path, so it never proves that
staged credential_account data is converted into
RuntimeCredentialAccountIdentity::marker_on_unauthorized() during unauthorized
responses. Update the tests around host_port::stage_credentials and the
RuntimeHttpEgress execution path to drive the real egress pipeline with staged
credentials, or adjust the comment in the host-port wrapper to match the
behavior actually covered. Use the existing
RecordingRuntimeHttpEgress::responding_with_marker setup as a reference, but add
a caller-level test that asserts the 401 response reflects the staged account
identity rather than only the marker.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 26afc585-686f-483a-a360-3f75c52ba7cf
📒 Files selected for processing (12)
crates/ironclaw_host_api/src/http.rscrates/ironclaw_host_api/src/ids.rscrates/ironclaw_host_api/tests/host_api_contract.rscrates/ironclaw_host_runtime/src/egress/credential.rscrates/ironclaw_host_runtime/src/egress/host_port.rscrates/ironclaw_host_runtime/src/obligations.rscrates/ironclaw_host_runtime/src/services.rscrates/ironclaw_host_runtime/src/services/tests.rscrates/ironclaw_host_runtime/src/wasm_credentials.rscrates/ironclaw_reborn_composition/src/product_auth_runtime_credentials.rscrates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rscrates/ironclaw_reborn_composition/src/runtime_credential_unauthorized/tests.rs
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (multi-agent)
Intent: Fix Reborn credential deletion and same-run reauthentication so revoked or refreshed runtime credentials gate the current run immediately.
Stats: 3 findings (from 6 raw, 3 after validation/dedup) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0
No High/Critical findings. This is a non-blocking review; the two Medium items are worth fixing before merge if you want the contract/test story tight, and the Low item is cleanup.
conventions
- Medium Public host-api contract changed without matching docs update (
crates/ironclaw_host_api/src/http.rs:113-185, confidence 72) — anchor: AGENTS.md:78
This adds public host-api contract types for runtime credential identity and unauthorized recovery markers, but the branch does not update the matching Reborn contract docs. The repo rule requires behavior changes to update relevant docs/specs in the same branch, and docs/reborn/contracts/AGENTS.md lists host-api.md as the neutral host API vocabulary contract.
tests
- Medium Refresh recovery needs a stale-marker skip test (
crates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rs:120-141, confidence 76) — anchor: crates/ironclaw_reborn_composition/src/runtime_credential_unauthorized.rs:120
The new refresh recovery branch handles fresh markers and unchanged-refresh reauth, but the current caller-level stale-marker test only covers RevokeAccount. A stale RefreshAccount marker should be proven to skip same-run AuthRequired when the account has changed before recovery runs.
maintainability
- Low Delete the unused unauthorized key wrapper (
crates/ironclaw_host_api/src/http.rs:385-405, confidence 100) — anchor: crates/ironclaw_host_api/src/http.rs:385
RuntimeCredentialUnauthorizedAccountKey and account_key() have no call sites in the tree. They export a second subset shape for the marker without removing complexity elsewhere, which makes the new public API larger than the PR currently needs.
| pub required: bool, | ||
| } | ||
|
|
||
| /// Product-auth account identity associated with a host-staged runtime |
There was a problem hiding this comment.
Medium — Public host-api contract changed without matching docs update.
This adds public host-api contract types for runtime credential identity and unauthorized recovery markers, but the branch does not update the matching Reborn contract docs. The repo rule requires behavior changes to update relevant docs/specs in the same branch, and docs/reborn/contracts/AGENTS.md lists host-api.md as the neutral host API vocabulary contract.
Fix: Update docs/reborn/contracts/host-api.md and docs/reborn/contracts/host-runtime.md to describe the staged credential unauthorized marker and same-run recovery handoff.
| } | ||
| } | ||
|
|
||
| async fn refresh_if_unchanged( |
There was a problem hiding this comment.
Medium — Refresh recovery needs a stale-marker skip test.
The new refresh recovery branch handles fresh markers and unchanged-refresh reauth, but the current caller-level stale-marker test only covers RevokeAccount. A stale RefreshAccount marker should be proven to skip same-run AuthRequired when the account has changed before recovery runs.
Fix: Add runtime_credential_unauthorized_recovery_skips_stale_refresh_marker covering RefreshAccount when the account was updated after staging.
| pub unauthorized_policy: RuntimeCredentialUnauthorizedPolicy, | ||
| } | ||
|
|
||
| impl RuntimeCredentialUnauthorized { |
There was a problem hiding this comment.
Low — Delete the unused unauthorized key wrapper.
RuntimeCredentialUnauthorizedAccountKey and account_key() have no call sites in the tree. They export a second subset shape for the marker without removing complexity elsewhere, which makes the new public API larger than the PR currently needs.
Fix: Remove RuntimeCredentialUnauthorizedAccountKey and account_key(); compare RuntimeCredentialUnauthorized directly until a real second consumer needs a reduced key type.
|
Closing in favor of a focused fix. Root cause of the user-facing bug ("Could not save the token" on credential re-auth) is narrow: WASM/runtime This PR addressed it via a heavy parallel pipeline (egress 401 marker → recovery egress → revoke/refresh → reauth bridge → HostRuntime AuthRequired override), which was also dead in practice: the scope guard compared the ephemeral The leaner fix — populate |
…on/surface Addresses the CI failure and the remaining review feedback on the credential_requirements enrichment PR. - github_wasm_runtime_contract.rs: the two google-drive WASM 401 tests asserted credential_requirements.is_empty() — the pre-fix, un-wired contract from #4969 (provider-null, unsubmittable gate, #5174). The enrichment now populates the gate from the single credential obligation, so assert one requirement with provider=google + OAuth setup. This is the runtime-401 re-auth fallback; proactive refresh (inline + background keepalive) already runs before injection. - host.rs: document the reactive-refresh-on-runtime-401 follow-up on the enrichment helper, and correct the downstream-consumer note (OAuth setup launches the OAuth flow, ManualToken renders the token card). - credential.rs: add direct unit tests for binding_scope_owns_account covering the session_id and surface exact-match branches. The durable filesystem caller tests partition account records by surface+session path, so those axes only ever returned CredentialMissing and never executed the guard's equality branches (coderabbit review point). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reborn): populate provider on runtime auth-required gates A WASM/runtime capability whose injected credential returns 401 raises an `auth_required` gate with empty `credential_requirements` (`runtime_adapters.rs` `wasm_guest_dispatch_error`). That left `AuthPromptView.provider` null, so the WebUI manual-token card threw client-side (`useChat.submitAuthToken` requires `provider`) and never sent the submit — surfacing as "Could not save the token" with no network request. Enrich an empty `DispatchError::AuthRequired.credential_requirements` from the capability's already-declared credential obligations (`InjectCredentialAccountOnce` -> `RuntimeCredentialAuthRequirement`) in the capability host, where both the dispatch result and the obligations are in scope. Runtime-agnostic; never overrides a populated list. This reuses the same declared-requirement data the credential-missing path already surfaces, so re-auth gates become submittable. Adds a caller-level regression test driving `CapabilityHost::invoke_json` that asserts the gate carries the provider (fails before the fix), plus a non-override test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(host-api): move auth-requirement enrichment onto host_api types; fix take-one Address code-review findings on the runtime auth-required enrichment: - Correctness: emit at most one credential requirement. The downstream consumer (auth_prompt_from_credential_requirement) matches exactly one (`let [requirement] = ...`); emitting >1 for capabilities with multiple credential obligations made it fall through and leave the gate unsubmittable. Enrich with `.take(1)`. - Altitude/duplication: move the logic onto the types that own it in ironclaw_host_api — `Obligation::credential_auth_requirement()` and `DispatchError::enrich_auth_requirements(&[Obligation])`. Delete the free helper from the capability host (host.rs shrinks; the two call sites become one-liners and can't drift). - Tests: add resume-path coverage (auth_resume_json -> dispatch_resumed_ capability), a multi-obligation test locking the take-one contract, and host_api unit tests for both new methods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): owner-granularity scope check for manual-token/selection completion Folds the second link of runtime credential re-auth into this PR. Manual-token (and credential-selection) submit mints a fresh per-request `invocation_id`, so completing a flow that reconnects to a credential account created in an earlier flow failed `scope_matches` full-equality with CrossScopeDenied (HTTP 403) — the "Could not save the token" follow-on once the gate became submittable. This is #4935 defect A on the unbound/reusable path. - `complete_manual_token` and `complete_credential_selection` (product_auth_durable/flows.rs) now use `binding_scope_owns_account`: owner-granularity (tenant/user/agent/project hard-required, session + surface exact-matched) while ignoring the ephemeral invocation_id (and thread/mission, intentional for owner-reusable accounts). - Mirror the same fix in the in-memory fake (fakes.rs) so it cannot mask the divergence in unit tests. - Tests: cross-invocation reconnect succeeds (both paths); genuinely foreign owner still rejected; cross-session and cross-surface still rejected (path-partitioned on disk). Follow-ups (not in this PR): rename `binding_scope_owns_account` -> `scope_owns_account` (now used on unbound paths too); extract a shared completion-account validation helper to unify the three call sites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: move auth-requirement enrichment policy to capabilities; tighten condition Address PR #5180 review (thermo-nuclear + multi-agent): - Altitude: keep the neutral `Obligation::credential_auth_requirement` mapper in ironclaw_host_api, but move the enrichment POLICY out of `DispatchError::enrich_auth_requirements` (product-workflow cardinality has no place in the neutral vocab crate per its guardrail) into a private helper in ironclaw_capabilities. - Correctness: synthesize the auth-gate credential requirement ONLY when the runtime gave no auth signal of its own (both `required_secrets` and `credential_requirements` empty) AND the capability declares EXACTLY ONE credential obligation. Raw-secret gates (required_secrets populated) are no longer mis-prompted as product-auth; multi-credential capabilities no longer get a wrong-provider gate (was `.take(1)` guessing the first). - Tests: unit tests for all helper branches; updated the multi-obligation contract test to assert the gate is left unmodified (empty) rather than pointed at an arbitrary provider. Also adds durable rejection coverage for complete_credential_selection (foreign owner reaches binding_scope_owns_account -> CrossScopeDenied; session/ surface are path-partitioned -> CredentialMissing, guard exact-match is defense-in-depth). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): fix runtime-401 reauth-gate contract; cover guard session/surface Addresses the CI failure and the remaining review feedback on the credential_requirements enrichment PR. - github_wasm_runtime_contract.rs: the two google-drive WASM 401 tests asserted credential_requirements.is_empty() — the pre-fix, un-wired contract from #4969 (provider-null, unsubmittable gate, #5174). The enrichment now populates the gate from the single credential obligation, so assert one requirement with provider=google + OAuth setup. This is the runtime-401 re-auth fallback; proactive refresh (inline + background keepalive) already runs before injection. - host.rs: document the reactive-refresh-on-runtime-401 follow-up on the enrichment helper, and correct the downstream-consumer note (OAuth setup launches the OAuth flow, ManualToken renders the token card). - credential.rs: add direct unit tests for binding_scope_owns_account covering the session_id and surface exact-match branches. The durable filesystem caller tests partition account records by surface+session path, so those axes only ever returned CredentialMissing and never executed the guard's equality branches (coderabbit review point). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(reborn): fix stale symbol/line refs in auth scope comments Address two low-severity review nits: - capability_host_auth_required_enrichment_contract.rs: header referenced the old helper name enrich_auth_required_from_obligations; rename to the real enrich_dispatch_error_credential_requirements. - fakes.rs / flows.rs: scope comments hard-coded credential.rs:580, which is already stale (binding_scope_owns_account is now at line 607). Drop the line number and point at the symbol only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(capabilities): cover raw-secret gate preservation; soften refresh doc Address two review nits on the enrichment helper: - host.rs: the OAuth runtime-401 follow-up doc stated cross-layer refresh (inline injection + background keepalive) as a guarantee, but those live in other crates and are not enforced here. Soften to "may already have been attempted". - capability_host_auth_required_enrichment_contract.rs: add invoke_json_preserves_required_secrets_from_dispatcher — a caller-level test driving CapabilityHost::invoke_json with a raw-secret AuthRequired (required_secrets populated, credential_requirements empty) while an InjectCredentialAccountOnce obligation is declared, asserting the gate is left unmodified (secrets preserved, not rewritten into a provider prompt). Previously covered only at the private-helper level. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… pack, triggered auth delivery, attachments, golden/synthetic expansions (#5610) * test(reborn): doc/text + multi-attachment coverage (W4-ATTACH-VARIANTS) Adds submit_turn_with_attachments (generalizes the image-only submit_turn_with_image_attachment to N attachments of any mime type) and two int-tier tests: a text/plain attachment's extracted text reaching the model, and two attachments in one turn both reaching the model with distinct index ordinals. Closes the doc/multi-attachment gap in C-ATTACH (only single-image coverage existed before). * test(reborn): W4-AUTHGATE-WIRE — runtime-401 provider-gate + cancel-no-replay (wave-4 row 1) Pins the #5174/#5180 bug class (empty credential_requirements leaving AuthPromptView.provider null, "Could not save the token" with no network request) through the FULL scripted-gateway integration harness — a tier below the existing crate-level pins, which drive CapabilityHost::invoke_json or HostRuntimeServices::invoke_capability directly and never exercise the real submit_turn -> BlockedAuth wire the WebUI depends on. - tests/reborn_integration_auth_gate.rs: new runtime_401_after_injection_populates_provider_credential_requirement (github credential resolves OK but the runtime HTTP call 401s; asserts the resulting BlockedAuth gate's credential_requirements carries provider=github + ManualToken setup), cancel_blocked_auth_gate_leaves_no_stale_replay (cancelling a BlockedAuth run lands directly on Cancelled with no active worker, and the SAME real gate ref can no longer resume it afterward — closes the #5067/#4957 class of gates staying "live"), and deny_auth_gate_rejects_a_non_auth_gate_ref_prefix (negative companion). Flip-check: temporarily bypassed the host.rs enrichment call site, confirmed the flagship test fails with the exact pre-fix empty-list shape, restored (crates/ironclaw_capabilities/src/host.rs left byte-identical — no production diff). - tests/support/reborn/harness.rs: RecordingNetworkHttpEgress gains an additive FIFO status_queue (default empty -> unchanged hardcoded-200 behavior) + install_network_status_script accessor. Needed because GithubIssueTools' real WASM HTTP call flows through the network-egress lane, not the runtime-egress lane the existing ScriptedHttpResponse matcher scripts (try_with_host_http_egress overwrites the runtime port — see reborn_integration_secret_injection.rs's module doc) — the prior double had no way to script a non-200 status on that lane at all. - tests/support/reborn/builder.rs: with_github_network_status(status) builder method (FIFO) threading github_network_statuses through RebornCapabilityBackend::install. - tests/support/reborn/capability_backend.rs: wires keyed_http_responses (previously dropped for this backend) and the new github_network_statuses into the GithubIssueTools install arm; no-op for existing empty-vec callers. - tests/support/reborn/assertions.rs: assert_network_egress_count, sibling of assert_egress_count for the network-lane call-count proofs above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): unknown extension_id fails extension_install safely (W4-EXT-MANIFEST-ERR) Narrowed from the originally-scoped manifest-content arms (schema mismatch/reserved id/forbidden trust level): extension_install's only input is a catalog-resolved extension_id over a fixed, compile-time-embedded bundled catalog, so raw manifest TOML never reaches ManifestV2Error validation through this capability in production. The one reachable, wired arm is an unknown extension_id, which fails Failed{invalid_input} rather than panicking or no-oping. * test(reborn): W4-PROVIDER-VALIDATE — password/traceback caller-gap coverage #5001 (PinchBench bucket D) removed the crude SENSITIVE_PROVIDER_TEXT_MARKERS substring scan on provider reasoning/response_reasoning/signature text (bare words like "password"/"traceback" were false-positive-rejected, driving retry/give-up loops); the entropy-based LeakDetector is the real guard now. That contract was pinned only at the private free-function level (capability_port/provider_validation.rs's own unit test calling validate_provider_tool_call directly) — the #5001 caller gap. Adds provider_tool_call_registration_accepts_password_and_traceback_reasoning_text in crates/ironclaw_loop_support/src/capability_port.rs's existing test module, alongside the crate's other caller-level `port.validate_provider_tool_call(&call)` tests: drives the REAL production caller (LoopCapabilityPort::validate_provider_tool_call / register_provider_tool_call / invoke_capability on HostRuntimeLoopCapabilityPort, the same port the agent loop calls) with "password"/"traceback" in all three metadata fields, and proves genuine acceptance through to a real Completed dispatch (not just a non-error return). Flip-check: temporarily bloated response_reasoning past PROVIDER_METADATA_TEXT_MAX_BYTES to confirm the assertion mechanism discriminates a genuine rejection (fails with the expected "exceeds 16384 bytes" error), then restored the password/traceback content. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): W4-MCP-SSO-WIRING — NEAR AI host-managed fallback through build_reborn_services #5439 fixed NEAR AI MCP token resolution for SSO users: a Google-SSO user in the same tenant/agent as the boot owner, with no NEAR AI token of their own, now falls back to the host-managed (boot-owner) NEAR AI credential instead of being prompted for one. That contract was pinned only at the private rule/selector level (product_auth_runtime_credentials/tests.rs never calls build_reborn_services) — the composition-wiring gap this row targets. Adds local_dev_nearai_runtime_selection_falls_back_to_host_managed_account_for_sso_user to extension_lifecycle_capabilities_auth_tests.rs (extending the existing in-crate #[cfg(test)] composition-test file — same pattern as the sibling github manual-token test above, template: product_auth_refresh_composition.rs's "drive build_reborn_services directly" style). Drives ONLY the public surface: build_reborn_services (local-dev always derives nearai_mcp_host_managed_scope from the boot owner, so no live NEAR AI config injection is needed) plus the crate-internal runtime_credential_account_selection_service() accessor this file already had precedent for calling. Two discriminating arms on one composed `services`: an SSO user in the owner's tenant/agent (different project -- local-dev's host scope is project-unscoped by design) resolves via fallback; an SSO user under a different tenant does not (CredentialMissing) -- proving the positive arm is a real scope match, not the selector always succeeding. Flip-check: temporarily short-circuited RebornProductAuthServices::runtime_credential_account_selection_service to always return the un-decorated selector (pre-#5439 behavior), confirmed the new test's positive arm fails with CredentialMissing, restored (auth.rs left byte-identical -- no production diff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): C-SYNTH deferred arms — AmbiguousSkill seeding + project_create fault-injection (wave-4 lane C) Two carry-over arms deferred from wave-3 PR #5584: - skill_activate AmbiguousSkill: seed a system-scoped AND a user-scoped skill sharing one name (both SkillTrust::Trusted per FilesystemSkillBundleRoot::system/user) so the real validate_explicit_mentions_are_unambiguous reject path fires end-to-end, not just at the skill_activation.rs unit-test level. New seed_user_skill_for_test harness helper (additive, mirrors seed_system_skill_for_test). - project_create fault-injection: new FaultInjectingProjectService test double (project_service_fault.rs) wrapping the real ProjectService at the production-wired Arc<dyn ProjectService> seam, forcing ProjectServiceError::Denied for a sentinel project name and delegating everything else to the real store. New project_tools_with_fault_injection()/project_lifecycle_fault_injected() harness+group constructors (additive). Deliberately NOT ProjectServiceError::Unavailable/Internal: investigation found both route through DefaultRecoveryStrategy's capability-retry branch, whose retry re-dispatch hits a real, confirmed production bug for provider-tool-call-originated invocations under local-dev composition — LocalDevCapabilityIo::resolve_capability_input rejects the reused input_ref on the retry with InvalidInvocation/"capability input ref was not staged for this loop run", collapsing the documented "retry twice, then a model-visible Failed" contract into an immediate terminal driver_unavailable. Documented in project_service_fault.rs; reported separately (not fixed — production change, out of this lane's scope). Both flip-checked (mutated seed/fault-injection to prove discriminating failure) and reverted before commit. * test(reborn): golden payload expansions — parallel tool_calls, image attachment, gated-turn resume (wave-4 lane C) Three scenario expansions to tests/reborn_integration_golden_payload.rs (carry-over from wave-3 PR #5584): - golden_parallel_tool_calls: new RebornScriptedReply::tool_calls([..]) constructor (additive to reply.rs) scripts ONE assistant response with TWO tool_calls[] entries, pinning that multiple calls in one turn each get a distinct id and each following tool-role message's tool_call_id lines up in order — a shape the existing single-call golden_tool_call_feedback can't exercise. - golden_image_attachment_turn: an inline image landed through the real submit_inbound_with_attachments entry point (RebornIntegrationGroup::attachment_tools()), routed through a vision-pattern model id, pinning the multimodal ContentPart::ImageUrl data: URL alongside the text part byte-for-byte. - golden_gated_turn_approve: a real BlockedApproval gate raised, approved, and resumed (RebornIntegrationGroup::live_approvals()), snapshotting BOTH inference calls around the gate — proving the resume doesn't drop, duplicate, or reorder accumulated turn history. Two normalization fixes to golden.rs, both needed for these scenarios to be reproducible (discovered while authoring, not pre-existing regressions): - Attachment-landing scenarios embed today's real UTC date in the landed project path (chrono::Utc::now(), no test seam) — added a second <DATE> filter alongside the existing loop-start-clock <TIMESTAMP> filter, or the image golden would bit-rot on every day boundary. - Tool-call ids come from a NEXT_TOOL_CALL_ID counter shared by every test in this one compiled binary; running more than one tool-call-scripting golden test concurrently (the default `cargo test` thread pool) makes the raw id values order-dependent. Added normalize_tool_call_ids: renumbers every call-<N> to a canonical call-1, call-2, … in order of first appearance per rendered payload, preserving the id/tool_call_id linkage the golden actually cares about without depending on the racy raw value. Confirmed behavior-preserving for the four pre-existing snapshots (no diff) and confirmed the race is fixed (5 consecutive full-suite green runs). Flip-checked (forced two parallel tool_calls to share one id; golden correctly failed) and reverted before commit. * test(reborn): W4-ASK-EACH-ONCE — ask-each-time approval resumes exactly once #5306 fixed an unresumable BlockedApproval loop: require_approval_for_profile_policy checked the explicit ask_each_time override (and the hard-floor force-approval class) BEFORE consulting the matching one-shot approval lease a resume carries, so an approved AskEachTime-gated resume re-hit the ask_each_time branch and re-gated instead of completing. Only a Python E2E test (test_tool_approval.py) exercised this class before; no Rust harness coverage existed. Adds scenario_ask_each_time_resumes_once.rs to the reborn_group_approvals binary (both approvals_group_e2e and its libsql variant), run LAST because it installs a persistent, group-wide ToolPermissionOverride::AskEachTime override on builtin.write_file that would force-gate every sibling scenario's plain-Ask-mode writes. Submits under the override, approves the resulting BlockedApproval gate, and proves the resume reaches Completed in ONE round trip with the write actually persisted — plus a companion "resumes exactly once" proof that re-approving the same now-resolved gate_ref fails NotPending (not a fresh re-raised gate). tests/support/reborn/harness.rs: adds a generic tool_permission_overrides: Option<Arc<dyn ToolPermissionOverrideStore>> field (mirrors the existing auto_approve_settings field's pattern — populated only by new_with_options, None elsewhere) and set_ask_each_time_override_for_test, generalizing disable_outbound_target_set_tool's override-store access beyond outbound_target_tools() to any host-runtime-backed harness/group. Flip-check: temporarily restored the pre-#5306 check order in profile_approval_authorization.rs (ask_each_time/hard-floor before the one-shot lease), confirmed the new scenario fails (the approved write never persists), restored (file left byte-identical — no production diff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): triggered-origin chained gated journey (wave-4 lane C) Carry-over from wave-3 PR #5584: a triggered fire whose run raises a BlockedApproval gate, gets resolved, then CHAINS into a SECOND BlockedApproval gate in the SAME run (the post-resume model call issues another gated tool call instead of finalizing), driven through submit_triggered_turn_scripted (E-TRIGGERED-SUBMIT). New scenario_triggered_chained_gate::run_chained_approve, registered as its own live_approvals group in reborn_group_triggers::triggered_gate_group. Re-reads TurnOriginKind::ScheduledTrigger fresh at the coordinator boundary at THREE checkpoints (first park, second/chained park, final Completed) — not just trusting the initial TriggeredSubmission — closing the gap that a resume path rebuilding product_context from a non-trigger-aware default on the SECOND hop would otherwise slip through undetected. Also asserts both gate_refs are genuinely distinct, both chained writes persisted, and the final reply persisted in the trigger's own thread. Flip-checked (asserted the wrong origin kind; the checkpoint helper correctly failed with the real ScheduledTrigger value in the diagnostic) and reverted before commit. Also folds in `cargo fmt` whitespace-only fixes surfaced while formatting this new file (golden.rs, harness.rs, reply.rs, and two golden/skill-activate test files touched by prior lane-C commits) — no semantic change, reran their test bins green after formatting. * test(reborn): extract trigger-prompt materializer test-support helper (wave-4 lane C) Committed follow-up on PR #5584's review thread: submit_triggered_turn_scripted hand-mirrored ConversationContentRefMaterializer::materialize_prompt (trigger_resolve_request + record_trigger_prompt + the content-ref shape, field-by-field) instead of reusing it, and — as flagged — deliberately SKIPPED authorize_trigger_fire and validate_trusted_trigger_prompt. Flagged as a drift trap (trusted-trigger materialization is an ownership boundary, AGENTS.md:61); the review agreed the fix is a #[cfg(feature = "test-support")] materializer helper returning (TriggerMaterializedPrompt, TurnScope) living beside the real materializer, held out of #5584 as a fast-follow with this exact shape. New production-crate (test-support-gated, compiles out of default builds) surface in ironclaw_reborn_composition: - trigger_poller_trusted_submit.rs: materialize_trigger_prompt_for_test, #[cfg(any(test, feature = "test-support"))] — runs the REAL production pipeline via ConversationContentRefMaterializer::materialize_prompt (authorize + validate + resolve + record + content-ref), then an idempotent second resolve_or_create_binding_with_trusted_scope call (safe — same request, same already-created binding) to also return the TurnScope the trait method computes internally but never exposes. Plus two crate-tier unit tests: positive (returned scope/content-ref match an independent ground-truth resolve) and negative (an unsafe prompt is rejected by the REAL safety validator). - test_support/trigger_materializer.rs: pub, feature="test-support"-gated thin wrapper re-exported from test_support/mod.rs — the established wrap_project_create_capability_for_test-style pattern. tests/support/reborn/triggered_submit.rs: submit_triggered_turn_scripted now calls this ONE production-owned helper instead of hand-mirroring; deletes ~90 net lines of duplicated resolve/thread-record/content-ref logic. Verified default-features build of ironclaw_reborn_composition stays warning-free (function/import correctly compile out). Flip-checked at the INTEGRATION level (not just the new crate-unit tests): forced an injection-pattern prompt through submit_triggered_turn_scripted — every triggered-gate scenario correctly failed with "rejected by safety scan", proving the old hand-mirrored path's skip of validate_trusted_trigger_prompt is now closed. Reverted before commit. All touched integration test bins (reborn_group_triggers, reborn_integration_triggered_submit, plus every other wave-4 lane-C bin) rerun green after the extraction. * test(reborn): W4-TRIGSLACK-SETTLE — auth-gate coverage for TriggeredRunDeliveryDriver TriggeredRunDeliveryDriver was exercised by exactly one crate-tier test (triggered_approval_prompt_route_resolves_dm_approve_on_foreign_scope), covering only the approval-gate path. Add the auth-gate twin: a BlockedAuth triggered run whose auth-prompt preference resolves to the creator's DM must carry the OAuth setup link (triggered_auth_prompt_route_delivers_dm_setup_link_on_foreign_scope), mirroring slack_dm_delivers_auth_prompt_with_setup_link_after_immediate_ack's assertion shape but driven through the real triggered-delivery driver. TriggeredRunDeliveryDriver only ever targets the creator's personal DM (never a channel), so there is no literal "channel" arm to mirror slack_channel_auth_prompt_omits_setup_link_after_immediate_ack. The discriminating negative arm instead exercises the driver's own send-time OAuth-DM backstop (triggered_auth_prompt_oauth_target_not_dm_suppresses_setup_link_and_cancels_run): when the resolved auth-prompt target is not a personal DM, the setup link must never be posted and the blocked run must be cancelled instead. ScriptedTriggerCoordinator gains an additive new_with_first_poll constructor (script an arbitrary first-poll status/gate_ref instead of the hardcoded BlockedApproval/GATE pair) and a functional cancel_run (previously unreachable!, since the approval-only scenario never called it) to support the OAuth-not-DM arm. Test code only; no production changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reborn): extract trigger-prompt materializer test-support helper (wave-4 lane C) Committed follow-up on PR #5584's review thread: submit_triggered_turn_scripted hand-mirrored ConversationContentRefMaterializer::materialize_prompt (trigger_resolve_request + record_trigger_prompt + the content-ref shape, field-by-field) instead of reusing it, and — as flagged — deliberately SKIPPED authorize_trigger_fire and validate_trusted_trigger_prompt. Flagged as a drift trap (trusted-trigger materialization is an ownership boundary, AGENTS.md:61); the review agreed the fix is a #[cfg(feature = "test-support")] materializer helper returning (TriggerMaterializedPrompt, TurnScope) living beside the real materializer, held out of #5584 as a fast-follow with this exact shape. New production-crate (test-support-gated, compiles out of default builds) surface in ironclaw_reborn_composition: - trigger_poller_trusted_submit.rs: materialize_trigger_prompt_for_test, #[cfg(any(test, feature = "test-support"))] — runs the REAL production pipeline via ConversationContentRefMaterializer::materialize_prompt (authorize + validate + resolve + record + content-ref), then an idempotent second resolve_or_create_binding_with_trusted_scope call (safe — same request, same already-created binding) to also return the TurnScope the trait method computes internally but never exposes. Plus two crate-tier unit tests: positive (returned scope/content-ref match an independent ground-truth resolve) and negative (an unsafe prompt is rejected by the REAL safety validator). - test_support/trigger_materializer.rs: pub, feature="test-support"-gated thin wrapper re-exported from test_support/mod.rs — the established wrap_project_create_capability_for_test-style pattern. tests/support/reborn/triggered_submit.rs: submit_triggered_turn_scripted now calls this ONE production-owned helper instead of hand-mirroring; deletes ~90 net lines of duplicated resolve/thread-record/content-ref logic. Verified default-features build of ironclaw_reborn_composition stays warning-free (function/import correctly compile out). Flip-checked at the INTEGRATION level (not just the new crate-unit tests): forced an injection-pattern prompt through submit_triggered_turn_scripted — every triggered-gate scenario correctly failed with "rejected by safety scan", proving the old hand-mirrored path's skip of validate_trusted_trigger_prompt is now closed. Reverted before commit. All touched integration test bins (reborn_group_triggers, reborn_integration_triggered_submit, plus every other wave-4 lane-C bin) rerun green after the extraction. * test(reborn): review fixes — consolidate slack e2e poll helpers, cite #5608 in fault-injection rationale Factor the three near-identical bounded-poll-for-chat.postMessage helpers in slack_serve/e2e_tests.rs into one predicate-parameterized wait_for_post_messages_matching, and replace "Lane C final report" citations with the filed issue (#5608) in the local-dev retry-path rationale comments. * test(reborn): address wave4 review comments * test(reborn): relax auth gate harness wait --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uth interaction services (#5654) * test(reborn): composition test-support accessors for WebUI approval/auth interaction services Adds `RebornServices::local_dev_approval_interaction_service_for_test` / `local_dev_auth_interaction_service_for_test`, unblocking W5-WEBUI-API-2 (RESOLVE_GATE coverage for both approval and auth gate kinds) — the #5174 bug class. A harness that builds its own planned runtime directly (e.g. via `build_default_planned_runtime`, bypassing `build_reborn_runtime`) previously had no way to get a real `DefaultApprovalInteractionService` / auth-interaction-service pair, only the fail-closed `Rejecting*`/`Unavailable*` fallbacks. Production-crate touch: `crates/ironclaw_reborn_composition` gains a new `runtime/test_support.rs` file (the two accessors, ~75 lines) plus a 3-line `#[cfg(feature = "test-support")] #[path = "runtime/test_support.rs"] mod test_support;` in `runtime.rs`. Both the module declaration and every method inside are gated behind `#[cfg(feature = "test-support")]` (off by default; confirmed via rlib symbol diff that the module compiles to zero bytes without the feature). The accessors live in a `runtime`-tree submodule rather than `factory.rs` because the recipe they mirror depends on five module-private types only reachable from code inside `crate::runtime` (Rust's private-item visibility extends to descendant modules, not just the defining file) — and it's a new file rather than inlined into the 10k+-line `runtime.rs` for the same reason this crate already splits its own unit tests into `runtime/tests/*.rs` submodules. Zero behavior change: every call reuses the exact production constructors (`DefaultApprovalInteractionService::new`, `ApprovalResolverPort::new`, `LocalDevApprovalLeaseTermsProvider::new`, `build_webui_auth_interaction_service`) with the exact arguments the production `build_reborn_runtime` recipe already assembles. `turn_coordinator` is an explicit `Arc<dyn TurnCoordinator>` parameter, not `self.turn_coordinator`: a `RebornServices` built by `build_reborn_services` alone carries a different `TurnCoordinator` instance than a caller-built planned runtime (e.g. `RebornIntegrationGroup`'s own coordinator) drives its turns through. Exercised by a new smoke test in `crates/ironclaw_reborn_composition/tests/runtime.rs` (`local_dev_test_support_interaction_service_accessors_build_real_services`) that builds a live local-dev runtime, calls both accessors with the runtime's own coordinator, and asserts each returns a real, working service (`Ok` with an empty pending list) rather than a fail-closed fallback — mutation-verified by temporarily forcing each accessor to its fail-closed path and confirming the test goes red, then reverting. The actual RESOLVE_GATE test scenarios these accessors unblock are W5-WEBUI-API-2's own follow-on PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(tests): trim comments to context-economy rule Compress test_support.rs and runtime.rs comments/doc-comments from multi-paragraph narratives to their load-bearing crux (audit-sink divergence, turn_coordinator instance rationale, module-privacy note); tighten the corresponding CLAUDE.md reference bullets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn_composition): stop collapsing approval test-accessor errors into None local_dev_approval_interaction_service_for_test masked local-dev capability-policy and grantee-resolver construction failures behind `.ok()?`, diverging from production (which propagates via `?`), and hand-duplicated the DefaultApprovalInteractionService wiring recipe. Extract build_local_dev_approval_interaction_service as the single shared recipe for build_reborn_runtime and the test accessor; the accessor now returns Result<Option<...>, RebornRuntimeError> so only a genuinely-absent local-dev runtime maps to Ok(None). Also covers two other review gaps: a caller-level test proving the supplied TurnCoordinator (not a stale/runtime-owned one) drives approval resolve/resume, and a test for the non-local-dev None branch on both *_for_test accessors. Doc comments now note test-only/ test-support gating explicitly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
Fixes Reborn credential delete and reliable re-auth behavior for runtime credentials.
AuthRequiredafter a recovered 401 instead of waiting for the next tool useRuntimeCredentialReauthBridge; host runtime does not own product-auth reauth stateRuntimeCredentialUnauthorizedmarkers for ambiguity so conflicting recovery metadata does not pick the wrong account flowUser experience
Example: GitHub PAT expires or is revoked while an extension tool is running.
Before:
After:
OAuth providers keep their refresh path: if refresh produces a new configured token, the run continues; if refresh fails or leaves the rejected token unchanged, the same-run auth gate opens.
Validation
cargo fmt --checkcargo check -p ironclaw_host_runtime -p ironclaw_reborn_compositioncargo test -p ironclaw_reborn_composition runtime_credential_ --libcargo test -p ironclaw_host_runtime host_runtime_http_egress_skips_credential_unauthorized_marker_when_recovery_metadata_differs --libcargo test -p ironclaw_host_runtime host_runtime_http_egress --lib --test reborn_e2e_gate --test runtime_http_egress_contract --test github_wasm_runtime_contract --test builtin_obligation_handler_contract --no-runcargo test -p ironclaw_host_api runtime_http_egress_response_round_trips_optional_credential_unauthorized --test host_api_contractcargo test -p ironclaw_auth refresh_contract --test auth_product_contractFinal thermo review loop: no Critical/High findings.