Skip to content

fix: Gmail auth-resume failure — stop run-borking + restore persistent-approval grant - #5051

Merged
henrypark133 merged 2 commits into
mainfrom
firat/practical-pare-959dc7
Jun 17, 2026
Merged

henrypark133 merged 2 commits into
mainfrom
firat/practical-pare-959dc7

Conversation

@serrrfirat

Copy link
Copy Markdown
Collaborator

Problem

Connecting Gmail failed with a misleading "the execution driver was temporarily unavailable" error, even though OAuth completed successfully (and the account connected on a later retry). Two distinct defects compounded:

  1. The auth-resume re-authorization was denied — the root cause.
  2. That denial borked the entire run instead of surfacing cleanly — the symptom the user saw.

Root cause

In the reborn-cli / admin-config FirstParty local-dev runtime, extension_activate is authorized only by a persistent-approval grant. The initial dispatch injects that grant (apply_persistent_approval_policy) before authorizing; the handler then raises AuthRequired (missing Google token) and the run parks as BlockedAuth with a credential gate.

On resume, auth_resume_capability rebuilt the execution context without re-applying the persistent-approval policy. With approval_request_id = None there is no approval lease to carry a grant, so dispatch_resumed_capability re-authorized a grant-less context → AuthorizationDenied. The denial then hit a second bug: the denied reason-kind was built from RuntimeFailureKind::Authorization.as_str() = "authorization", which the loop-safe identifier validator rejects as a sensitive marker → internal "could not be represented" error → mapped to HostUnavailable → terminal driver-unavailable failure.

Fixes

fix(loop) — stop authorization denials from borking the run
Map Authorization/PolicyDenied runtime failures to leak-safe denied reason tags (auth_denied/policy_denied) via denied_reason_kind_for() instead of passing the raw kind string through the identifier validator. Authorization denials now surface to the model as a clean Denied outcome and the run continues.

fix(host-runtime) — re-apply persistent-approval grant on auth-resume
auth_resume_capability now calls apply_persistent_approval_policy (action = Dispatch) before building the resume request, mirroring dispatch_capability. The helper is a no-op when no matching policy/grant exists, so capabilities requiring fresh approval are unaffected.

resume_spawn_capability is intentionally not changed: it only handles BlockedApproval resumes, which always carry an approval_request_id and claim a fingerprinted approval lease that injects the grant — so it does not have this gap.

Tests

  • runtime_failure_to_loop_honors_model_visible_disposition — added an Authorization case (existing coverage was PolicyDenied-only, which validates cleanly and missed the bug).
  • default_runtime_uses_persistent_policy_as_auth_resume_authority — drives auth_resume_capability end-to-end against a BlockedAuth run authorized only by a persistent grant; asserts the run completes (fails pre-fix). The persistent-approvals contract previously covered only dispatch and spawn authority.

Verification

  • host_runtime_persistent_approvals_contract: 11/11 pass.
  • ironclaw_loop_support lib runtime_failure_to_loop tests pass.
  • cargo clippy -p ironclaw_host_runtime --tests --all-features: clean.
  • cargo fmt --all --check: clean.

Scope note: verification covered the two affected packages and their tests, not a full-workspace gate.

🤖 Generated with Claude Code

serrrfirat and others added 2 commits June 17, 2026 22:23
A capability that failed with RuntimeFailureKind::Authorization (observed
when a Gmail extension activation failed authorization on auth-resume)
borked the entire run with a misleading "execution driver was temporarily
unavailable" message instead of surfacing a clean, model-visible denial.

Root cause: runtime_model_visible_failure_to_loop built the denied
outcome's reason_kind from RuntimeFailureKind::as_str(), which for the
Authorization variant is the literal string "authorization". The
loop-safe identifier validator rejects "authorization" as a sensitive
marker (it guards against leaking Authorization: header material), so
CapabilityDeniedReasonKind::unknown("authorization") failed, producing an
internal "capability denied reason kind could not be represented" error.
The executor mapped that to HostUnavailable { stage: Capability } and the
planned driver recorded a terminal driver-unavailable failure.

Fix: map Authorization/PolicyDenied runtime failures to leak-safe denied
reason tags ("auth_denied"/"policy_denied") via denied_reason_kind_for()
instead of passing the raw kind string through the identifier validator.
The denial now surfaces to the model as a normal Denied outcome and the
run continues.

Adds an Authorization regression case to
runtime_failure_to_loop_honors_model_visible_disposition (the existing
test only covered PolicyDenied, which validates cleanly and so missed
this).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A capability authorized only by a persistent-approval grant (e.g.
extension_activate under admin-config FirstParty trust, as in the
reborn-cli local-dev runtime) failed authorization on the credential
auth-resume path, even though the initial dispatch was authorized.

The initial dispatch injects the persistent-approval grant into the
execution context via apply_persistent_approval_policy before
authorizing (production.rs dispatch_capability / spawn_capability). When
the handler then raised AuthRequired (missing credential), the run was
parked as BlockedAuth and a credential gate opened. On resume,
auth_resume_capability rebuilt the context WITHOUT re-applying the
persistent-approval policy, so dispatch_resumed_capability re-authorized
a grant-less context and returned AuthorizationDenied. With
approval_request_id = None there is no approval lease to carry a grant,
so the persistent policy is the only authority — and it was dropped.

Observed connecting Gmail: OAuth completed, but the extension_activate
auth-resume failed with an authorization error; a later fresh dispatch
(which re-injects the grant) succeeded. Combined with the separate
denied-reason representation bug, the failure surfaced as a misleading
"execution driver was temporarily unavailable".

Fix: auth_resume_capability now calls apply_persistent_approval_policy
(action = Dispatch) before building the resume request, mirroring
dispatch_capability. The helper is a no-op when no matching policy/grant
exists, so capabilities requiring fresh approval are unaffected.

resume_spawn_capability is intentionally not changed: it only handles
BlockedApproval resumes, which always carry an approval_request_id and
claim a fingerprinted approval lease that injects the grant — so it does
not have this gap.

Adds default_runtime_uses_persistent_policy_as_auth_resume_authority to
the persistent-approvals contract, which previously covered only the
dispatch and spawn authority paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5051 June 17, 2026 21:28 Destroyed
@railway-app

railway-app Bot commented Jun 17, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jun 17, 2026 at 9:35 pm

@github-actions github-actions Bot added size: M 50-199 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jun 17, 2026

@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 addresses two key issues: it ensures persistent-approval grants are re-applied during auth-resume preflights, and it maps authorization and policy runtime failures to leak-safe identifiers ('auth_denied' and 'policy_denied') to prevent internal errors caused by sensitive marker validation. Regression tests have been added to verify both fixes. I have no further feedback to provide as the changes are well-implemented and covered by tests.

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.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d786d4be-4248-44b1-8b5a-ddc04149681a

📥 Commits

Reviewing files that changed from the base of the PR and between 99803e9 and 840a2a0.

📒 Files selected for processing (3)
  • crates/ironclaw_host_runtime/src/production.rs
  • crates/ironclaw_host_runtime/tests/host_runtime_persistent_approvals_contract.rs
  • crates/ironclaw_loop_support/src/capability_port.rs

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed persistent-approval grants to correctly apply during authentication resumption
    • Improved authorization and policy denial error handling for secure information disclosure
  • Tests

    • Added regression test for persistent approval behavior during authentication resumption

Walkthrough

DefaultHostRuntime::auth_resume_capability gains a apply_persistent_approval_policy(Dispatch) call inserted after trust evaluation so persistent-approval grants are re-applied on auth-resume preflight. A new denied_reason_kind_for helper in capability_port.rs replaces the previous string-form RuntimeFailureKind lookup for Authorization/PolicyDenied, mapping to stable identifiers. Both fixes are covered by new regression tests.

Changes

Persistent-approval re-injection on auth-resume + safe denial mapping

Layer / File(s) Summary
Leak-safe denied-reason mapping
crates/ironclaw_loop_support/src/capability_port.rs
runtime_model_visible_failure_to_loop now routes Authorization and PolicyDenied through a new denied_reason_kind_for helper returning "auth_denied" / "policy_denied", bypassing the prior string-form path that could produce a "could not be represented" failure. Regression test added.
Persistent-approval re-injection in auth_resume_capability
crates/ironclaw_host_runtime/src/production.rs, crates/ironclaw_host_runtime/tests/host_runtime_persistent_approvals_contract.rs
Inserts apply_persistent_approval_policy(PersistentApprovalAction::Dispatch) after trust evaluation and before CapabilityHost::auth_resume_json. New regression test seeds a persistent dispatch policy, parks a run into BlockedAuth, calls auth_resume_capability with approval_request_id = None, and asserts RunStatus::Completed and a dispatcher request.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • nearai/ironclaw#4839: Modifies the same DefaultHostRuntime::auth_resume_capability site in production.rs; this PR layers persistent-approval re-application on top of the auth-resume identity/lease handling introduced there.

Poem

An approval once granted should stick through the storm,
Resume-preflight now checks before changing the norm.
No "could not be represented" to trip up the flow,
auth_denied maps cleanly wherever we go.
🦀 The borrow of trust is re-checked — just so.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title follows Conventional Commits style and clearly describes both bugs fixed (auth-resume failure, run-borking denial handling) with concrete specificity.
Description check ✅ Passed Description covers problem/root cause, fixes, tests, and verification. All required sections present. However, PR author omitted the template structure (no checkbox completion for Change Type, Linked Issue, Validation, etc.).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator

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 Gmail auth-resume so persistent approval is reapplied and authorization denials surface cleanly instead of failing the run.

Stats: 0 findings (from 0 raw, 0 after dedup) across 0 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0

No actionable findings from the multi-agent review.

@henrypark133
henrypark133 merged commit c8aae65 into main Jun 17, 2026
67 of 68 checks passed
@henrypark133
henrypark133 deleted the firat/practical-pare-959dc7 branch June 17, 2026 21:54
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
…t-approval grant (nearai#5051)

* fix(loop): stop authorization denials from borking the run

A capability that failed with RuntimeFailureKind::Authorization (observed
when a Gmail extension activation failed authorization on auth-resume)
borked the entire run with a misleading "execution driver was temporarily
unavailable" message instead of surfacing a clean, model-visible denial.

Root cause: runtime_model_visible_failure_to_loop built the denied
outcome's reason_kind from RuntimeFailureKind::as_str(), which for the
Authorization variant is the literal string "authorization". The
loop-safe identifier validator rejects "authorization" as a sensitive
marker (it guards against leaking Authorization: header material), so
CapabilityDeniedReasonKind::unknown("authorization") failed, producing an
internal "capability denied reason kind could not be represented" error.
The executor mapped that to HostUnavailable { stage: Capability } and the
planned driver recorded a terminal driver-unavailable failure.

Fix: map Authorization/PolicyDenied runtime failures to leak-safe denied
reason tags ("auth_denied"/"policy_denied") via denied_reason_kind_for()
instead of passing the raw kind string through the identifier validator.
The denial now surfaces to the model as a normal Denied outcome and the
run continues.

Adds an Authorization regression case to
runtime_failure_to_loop_honors_model_visible_disposition (the existing
test only covered PolicyDenied, which validates cleanly and so missed
this).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(host-runtime): re-apply persistent-approval grant on auth-resume

A capability authorized only by a persistent-approval grant (e.g.
extension_activate under admin-config FirstParty trust, as in the
reborn-cli local-dev runtime) failed authorization on the credential
auth-resume path, even though the initial dispatch was authorized.

The initial dispatch injects the persistent-approval grant into the
execution context via apply_persistent_approval_policy before
authorizing (production.rs dispatch_capability / spawn_capability). When
the handler then raised AuthRequired (missing credential), the run was
parked as BlockedAuth and a credential gate opened. On resume,
auth_resume_capability rebuilt the context WITHOUT re-applying the
persistent-approval policy, so dispatch_resumed_capability re-authorized
a grant-less context and returned AuthorizationDenied. With
approval_request_id = None there is no approval lease to carry a grant,
so the persistent policy is the only authority — and it was dropped.

Observed connecting Gmail: OAuth completed, but the extension_activate
auth-resume failed with an authorization error; a later fresh dispatch
(which re-injects the grant) succeeded. Combined with the separate
denied-reason representation bug, the failure surfaced as a misleading
"execution driver was temporarily unavailable".

Fix: auth_resume_capability now calls apply_persistent_approval_policy
(action = Dispatch) before building the resume request, mirroring
dispatch_capability. The helper is a no-op when no matching policy/grant
exists, so capabilities requiring fresh approval are unaffected.

resume_spawn_capability is intentionally not changed: it only handles
BlockedApproval resumes, which always carry an approval_request_id and
claim a fingerprinted approval lease that injects the grant — so it does
not have this gap.

Adds default_runtime_uses_persistent_policy_as_auth_resume_authority to
the persistent-approvals contract, which previously covered only the
dispatch and spawn authority paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5051 — 840a2a0d Deployed Jun 17, 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 size: M 50-199 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants