Skip to content

Wire Reborn auth consumers through staged credentials - #4231

Merged
serrrfirat merged 16 commits into
reborn-integrationfrom
codex/issue-4176-product-auth-runtime
May 29, 2026
Merged

serrrfirat merged 16 commits into
reborn-integrationfrom
codex/issue-4176-product-auth-runtime

Conversation

@serrrfirat

Copy link
Copy Markdown
Collaborator

Summary

Refs #4176.

This PR wires the runtime credential consumer paths that depend on host-owned staged credentials:

  • adds a composition-owned GSuite credential stager that resolves the selected Google product-auth account and satisfies InjectSecretOnce before first-party host HTTP egress consumes the StagedObligation;
  • projects recoverable first-party credential failures through DispatchError::AuthRequired so the host runtime can return RuntimeCapabilityOutcome::AuthRequired instead of a generic failed runtime dispatch;
  • fixes MCP HTTP/SSE credential planning to plan the real tools/call JSON-RPC body once before handshake and thread that validated plan into the eventual tools/call send;
  • documents the open-question decisions for GSuite dynamic selection, GitHub manual-token-first modeling, server-scoped MCP account selection, recovery semantics, refresh ownership, and credential target scope.

Open-question decisions

  • GSuite stays dynamic through CredentialAccountService; static runtime_credentials are deferred until account bindings are stable.
  • GitHub first-party WASM remains github_token manifest-driven with manual-token product-auth as the first account model; the existing staged host HTTP path is covered by github_wasm_runtime_contract.
  • MCP auth selection is composition-owned and server-scoped; MCP protocol code only consumes host-planned staged credential injections.
  • Multiple matching accounts return account-selection-required; missing/expired/revoked/refresh-failed credentials map to auth recovery/AuthRequired; backend failures fail closed.
  • Refresh stays provider-specific behind CredentialAccountService::refresh_account.
  • Header/query targets are preferred; path placeholders remain compatibility-only where an upstream protocol requires them.

Tests

  • cargo test -p ironclaw_reborn_composition --test gsuite
  • cargo test -p ironclaw_mcp
  • cargo test -p ironclaw_host_runtime first_party_adapter_maps_handler_auth_required_to_dispatch_auth_required
  • cargo test -p ironclaw_host_runtime --test github_wasm_runtime_contract
  • cargo test -p ironclaw_first_party_extensions -p ironclaw_host_api -p ironclaw_capabilities
  • git diff --check

@github-actions github-actions Bot added scope: docs Documentation size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels May 29, 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 introduces runtime credential staging for GSuite first-party capabilities and refactors MCP HTTP/SSE credential planning to ensure handshakes remain credential-free. Key feedback includes addressing a potential build failure in offline or airgapped deployments where product auth ports are resolved unconditionally, safely unwrapping those ports during GSuite handler registration, using exhaustive matching instead of a catch-all wildcard when mapping dispatch errors, and optimizing PlannedMcpJsonRpc by storing constructed headers directly to avoid redundant session ID lookups.

Comment thread crates/ironclaw_reborn_composition/src/factory.rs
Comment thread crates/ironclaw_reborn_composition/src/factory.rs Outdated
Comment thread crates/ironclaw_capabilities/src/error.rs
Comment thread crates/ironclaw_mcp/src/lib.rs
@github-actions github-actions Bot added size: XL 500+ changed lines and removed size: L 200-499 changed lines labels May 29, 2026

@serrrfirat serrrfirat left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Multi-Agent Code Review

Intent: Wire Reborn auth consumers through staged credentials, covering GSuite credential stager, AuthRequired projection, and MCP HTTP/SSE credential planning.

Stats: 11 findings → 0 Critical, 2 High, 7 Medium, 2 Low, 0 Nit
Reviewers: security, bugs, performance, tests, conventions (5/5 OK)
Head: 1f2e04735

🐛 Bugs (2)

  • [High] crates/ironclaw_host_runtime/src/services.rs:226 — stage_secret_error maps SecretExpired/LeaseRevoked to Backend instead of AuthRequired
  • [Medium] crates/ironclaw_host_runtime/src/services.rs:216 — consume() errors collapsed to Backend hide expired/revoked credentials

🧪 Tests (7)

  • [High] crates/ironclaw_host_runtime/src/services.rs:204 — stage_secret_once AuthRequired/Backend error branches untested
  • [Medium] crates/ironclaw_host_runtime/src/production.rs:1638 — dispatch_kind_to_failure pin test omits new AuthRequired variant
  • [Medium] crates/ironclaw_host_api/src/dispatch.rs:186 — DispatchError::AuthRequired -> DispatchFailureKind contract untested
  • [Medium] crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:110 — post-refresh stage_credential failure path untested
  • [Medium] crates/ironclaw_host_runtime/src/production.rs:983 — required_secrets propagation through auth_required_outcome from FirstParty path untested
  • [Low] crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:245 — GsuiteDispatchError::auth_requirement per-reason branches untested
  • [Low] crates/ironclaw_capabilities/src/error.rs:114 — FromDispatchError::AuthRequired preserves required_secrets untested

📐 Conventions (2)

  • [Medium] crates/ironclaw_capabilities/src/host.rs:1642 — Obligation-derived AuthorizationRequiresAuth silently fills required_secrets with Vec::new()
  • [Medium] crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:245 — GsuiteDispatchError::auth_requirement collapses Recovery projection to empty default with no rationale

Posted by code-review skill v1. Multi-agent review with intent analyzer + 5 parallel reviewers (deduplicated, confidence ≥ 50).

Comment thread crates/ironclaw_host_runtime/src/services.rs
Comment thread crates/ironclaw_host_runtime/src/services.rs
Comment thread crates/ironclaw_host_api/src/dispatch.rs
Comment thread crates/ironclaw_capabilities/src/host.rs
Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
Comment thread crates/ironclaw_host_runtime/src/production.rs
Comment thread crates/ironclaw_host_runtime/src/services.rs
Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
Comment thread crates/ironclaw_capabilities/src/error.rs
serrrfirat added a commit that referenced this pull request May 29, 2026
- services.rs: stage_secret_error now treats SecretExpired/LeaseRevoked/
  LeaseExpired as AuthRequired (was Backend); consume() failure path
  routes through the same classifier instead of collapsing to Backend.
  Recoverable credential conditions now surface through the runtime auth
  gate as intended by this PR.
- host.rs: obligation-derived AuthorizationRequiresAuth records a
  silent-ok rationale per .claude/rules/error-handling.md, naming why
  required_secrets is empty on the obligation path (unit variant, no
  payload to forward).
- gsuite/handlers.rs: documented why GsuiteAuthRequirement intentionally
  drops the Recovery projection details (preserved via reason()).

Tests:
- stage_secret_once now exercises UnknownSecret, SecretExpired,
  LeaseRevoked, LeaseExpired, BackendMisconfigured via a FailingSecretStore
  test double.
- DispatchError::AuthRequired -> DispatchFailureKind::AuthRequired and the
  'AuthRequired' as_str literal are now pinned in
  host_api_contract.rs.
- dispatch_kind_to_failure_pins_dispatch_error_top_level_kinds now covers
  DispatchFailureKind::AuthRequired -> RuntimeFailureKind::Authorization.
- error.rs: From<DispatchError::AuthRequired> preserves required_secrets
  (populated and empty).
- handlers.rs: auth_requirement_classifies_each_credential_dispatch_reason
  pins Recovery/MissingScopes/MissingAccessSecret -> Some(default),
  AuthRequired -> Some(secrets), BackendAuth/HostApi/None -> None.

Refs PR #4231.
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Fix notes (commit 62851fd1)

Addressed the Medium-and-higher findings from the multi-agent review above.

🐛 Bug fixes

stage_secret_error misrouted recoverable credential conditions (services.rs:226+:220)

stage_secret_error only matched is_unknown_secret() → AuthRequired; everything else fell through to Backend. SecretExpired, LeaseRevoked, and LeaseExpired are all user-actionable re-auth conditions (their stable_reason() returns CredentialExpired/CredentialRevoked) — mapping them to Backend meant the runtime auth gate never fired for expired or revoked credentials, defeating the AuthRequired projection this PR introduces.

Fixed: stage_secret_error now also matches is_expired() and is_revoked(). The consume() failure path was also hardcoded to Backend; it now routes through the same classifier.

📐 Convention fixes

host.rs:1642 — required_secrets: Vec::new() without rationale

The obligation-path CapabilityObligationError::AuthRequired conversion is a unit variant with no payload to forward. Added a // silent-ok: comment per .claude/rules/error-handling.md naming why the empty list is structurally inevitable here (dispatch-path conversion in error.rs forwards the real list).

handlers.rs:245 — Recovery projection collapse undocumented

GsuiteAuthRequirement intentionally only carries required_secrets; the richer recovery context (provider, accounts, reason) is preserved via reason() and consumed separately by the runtime for telemetry/user-facing state. Added a doc-comment on auth_requirement() naming this split and why each variant maps the way it does.

🧪 New tests (356 LoC, 7 files)

Test What it pins
stage_secret_once_returns_auth_required_for_unknown_handle UnknownSecret → AuthRequired on lease
stage_secret_once_returns_auth_required_for_expired_secret SecretExpired → AuthRequired on lease
stage_secret_once_returns_auth_required_for_revoked_lease_on_consume LeaseRevoked → AuthRequired on consume
stage_secret_once_returns_auth_required_for_expired_lease_on_consume LeaseExpired → AuthRequired on consume
stage_secret_once_returns_backend_on_secret_store_failure BackendMisconfigured → Backend
dispatch_kind_to_failure_pins_… (extended) DispatchFailureKind::AuthRequired → RuntimeFailureKind::Authorization
dispatch_errors_preserve_typed_failure_kind (extended) DispatchError::AuthRequired → DispatchFailureKind::AuthRequired
dispatch_failure_kind_display_preserves_stable_literals (extended) DispatchFailureKind::AuthRequired.as_str() == "AuthRequired"
from_dispatch_auth_required_preserves_required_secrets From<DispatchError::AuthRequired> forwards non-empty required_secrets
from_dispatch_auth_required_preserves_empty_required_secrets Same for empty list
auth_requirement_classifies_each_credential_dispatch_reason Pins all 6 GsuiteCredentialDispatchReason variants against auth_requirement()

All PR-body test suites green post-fix.

serrrfirat added a commit that referenced this pull request May 29, 2026
Critical:
- fix resume_spawn_capability: missing AuthorizationRequiresAuth arm caused
  spawned-capability auth failures to surface as Failed instead of
  AuthRequired (symmetric fix with resume_capability)

Structural / type model:
- FirstPartyCapabilityError: struct with kind+auth-Option replaced by a
  proper enum (Dispatch / AuthRequired), making the two failure modes
  explicit. The dead 'kind' field on auth paths is gone; Display is now
  correct for both variants.
- GsuiteAuthRequirement struct deleted: auth_requirement() now returns
  Option<Vec<SecretHandle>> directly, removing one layer of indirection.
- FirstPartyAuthRequirement struct deleted: auth_required_with() takes
  Vec<SecretHandle> directly. Removes identical duplicate of GsuiteAuthRequirement.
- GsuiteCredentialStageError and ProductAuthCredentialStageError unified:
  both are now type aliases for ironclaw_host_api::CredentialStageError;
  the identity-mapping stage_runtime_credential_error function in
  reborn_composition is deleted.
- ironclaw_host_api: added CredentialStageError enum (shared staging error
  type) and DispatchError::event_kind() method (canonical event-token source).

Eliminated duplication:
- dispatch_error_kind() free function deleted from ironclaw_dispatcher and
  ironclaw_host_runtime/process_executor; call sites now use
  error.event_kind() from ironclaw_host_api.
- dispatch_kind_to_failure() free function replaced with
  From<DispatchFailureKind> for RuntimeFailureKind impl; call sites use
  RuntimeFailureKind::from(kind) / .into().

Test quality:
- FailingSecretStore (187-line SecretStore impl) deleted; stage_secret_error
  is now pub(crate) and tested directly with 6 synchronous table-driven
  cases — no async harness needed.
- Two near-identical from_dispatch_auth_required_* tests collapsed into one
  table-driven test.
- dispatch_kind_to_failure pin test converted to table-driven form.
- auth_requirement test assertions updated to Option<Vec<SecretHandle>>.

Refs PR #4231.

@serrrfirat serrrfirat left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Pass 2 (e56e714)

Reviewers: Security · Bugs · Performance · Tests · Conventions
Findings: 18 total · 2 High · 9 Medium · 5 Low · 2 Nit
Inline: 13 · Summary-only: 5


🔴 High

# File Finding
B1 services/runtime_adapters.rs:323 AuthRequired swallowed by Resource when reconcile fails after handler error — see inline
T1 services/tests/first_party_runtime_adapter.rs:54 required_secrets not asserted; reservation cleanup not verified — see inline

🟠 Medium (summary-only)

  • B2 runtime_adapters.rs — serde_json::to_vec(&result.output) failure at the success path propagates OutputDecode without releasing the reservation. Every exit path must call release_first_party_reservation or governor.reconcile. Add cleanup in the map_err closure. (conf 97)
  • T6 services.rs — RegisteredRuntimeHealth::missing_runtime_backends on an empty-available list never tested. Add tests::services::registered_runtime_health_reports_missing_backends_when_available_is_empty. (conf 93)
  • T7 gsuite/handlers.rs — merge_attendees has no test for the no-email addition path (appended unconditionally) or empty existing list. Add tests::handlers::merge_attendees_appends_addition_without_email_field and ::merge_attendees_with_empty_existing_list. (conf 93)

🔵 Nit (summary-only)

  • P2 ironclaw_mcp/src/lib.rs:573 — credential_injections Vec cloned for preflight validation because McpJsonRpcMethod::credential_injections consumes it. Extractable as a borrow-based validate_tools_call_credential_injections(&[...]) helper to avoid the clone. (conf 90)
  • C4 services.rs:228 — Doc comment on stage_secret_error says "pub(crate) for testing" but the function is called on production paths at lines 214 and 219. Update to "pub(crate) so it can be unit-tested directly; used in production by stage_secret_once." (conf 80)

Reviewers failed: none. Intent confidence: high.

Comment thread crates/ironclaw_host_runtime/src/services/tests/first_party_runtime_adapter.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/services/runtime_adapters.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/first_party.rs
Comment thread crates/ironclaw_reborn_composition/src/gsuite.rs
Comment thread crates/ironclaw_host_api/tests/host_api_contract.rs
Comment thread crates/ironclaw_host_runtime/src/first_party.rs
Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
Comment thread crates/ironclaw_reborn_composition/src/gsuite.rs
Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
serrrfirat added a commit that referenced this pull request May 29, 2026
Bugs:
- runtime_adapters: absorb resource-accounting failure when handler returns
  AuthRequired so the auth signal survives governor.reconcile() failures
  (was: propagated via ? and returned Resource instead of AuthRequired)
- runtime_adapters: release reservation in serde_json::to_vec map_err
  closure so every exit path either reconciles or releases

Security:
- handlers.rs auth_requirement(): Recovery(Configured) now returns None
  (backend infra failure, not user-actionable); all other Recovery kinds
  (SetupRequired, ReauthorizeRequired, AccountSelectionRequired) retain
  Some(vec![]) behaviour
- dispatch.rs DispatchError: removed derive(Debug), added manual Debug impl
  that redacts required_secrets to "[N handle(s) redacted]" to prevent
  secret-handle identifiers leaking into logs
- services.rs stage_secret_error: adds is_consumed() and is_unknown_lease()
  to the AuthRequired predicate (both are user-actionable, align with
  SecretStoreError::stable_reason() taxonomy)

Conventions:
- gsuite/handlers.rs map_stage_error: Backend staging failure tagged
  BackendAuth (not HostApi) to match map_credential_error's category scheme
- services.rs stage_secret_error: doc comment clarified to say function is
  used in production by stage_secret_once (not 'for testing only')
- reborn_composition/gsuite.rs gsuite_error: doc comment explaining that
  CredentialRecoveryProjection context is intentionally dropped at this
  composition boundary (follow-up required for structured recovery hints)

Performance:
- mcp/lib.rs: extracted validate_tools_call_credential_injections(&[...])
  so the credential_injections Vec is validated by borrow, removing the
  .clone() before the MCP handshake

Tests added:
- first_party_runtime_adapter: assert required_secrets forwarded through
  AuthRequired (not silently dropped); reservation released after auth-required
  handler failure; required_secrets forwarded from handler with specific handle;
  panic handler contained as Backend with reservation released
- first_party.rs: kind() returns None for AuthRequired (both variants);
  with_usage() on AuthRequired preserves required_secrets; with_usage() on
  Dispatch variant round-trips kind()
- reborn_composition/gsuite.rs: gsuite_error projects stage AuthRequired to
  FirstPartyCapabilityError::AuthRequired; stage Backend to Dispatch{Backend}
- host_api_contract: event_kind() for AuthRequired pinned as 'auth_required'
  (empty and non-empty required_secrets both covered)
- services/tests: stage_secret_error covers LeaseConsumed and UnknownLease;
  RegisteredRuntimeHealth empty-available, deduplicate, all-available cases
- gsuite/handlers.rs: merge_attendees empty-existing, no-email addition,
  no-additions cases

Refs PR #4231.
@abbyshekit

Copy link
Copy Markdown
Contributor

Review — wire Reborn auth consumers through staged credentials

Combined pass (Claude Code + Codex 5.5 gpt-5.5). The staging→egress handoff, scope isolation, enum exhaustiveness and fail-closed mapping are verified correct, and it compiles on default + libsql/postgres. But two wired-behavior bugs (both Codex, both verified) break auth-resume and the add_attendees capability.

High — AuthRequired persists Failed, not BlockedAuth, so auth-resume can't resume (Codex P1, verified)

On the dispatch-error path, crates/ironclaw_capabilities/src/host.rs:473 unconditionally calls fail_run_if_configured(..., "Dispatch") and then return Err(CapabilityInvocationError::from(error)). When a first-party handler returns DispatchError::AuthRequired (e.g. a GSuite staging miss), the caller gets an auth gate (AuthorizationRequiresAuth), but the run was already marked Failed. The correct transition exists — apply_run_state_transition_if_configured (helpers.rs:169-196) maps AuthorizationRequiresAuth → block_auth(..., "AuthRequired") → RunStatus::BlockedAuth — but the dispatch path doesn't use it. So turn-blocking / auth-resume paths that rely on BlockedAuth can't resume the blocked invocation. Fix: on the dispatch-error path, route AuthRequired through apply_run_state_transition_if_configured (BlockAuth) instead of the unconditional fail_run("Dispatch").

High — one-shot staging breaks the two-request add_attendees capability (Codex P2, verified)

crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:85 stages the credential once before capability_execution. But execute_add_attendees (handlers.rs:336-374) performs two egress calls in one execution — a GET (line 342) then a PATCH (line 367). The staged-obligation store is one-shot (take removes the secret on first read), so the GET consumes it and the PATCH fails with a missing required credential. Stage before each runtime HTTP request (or restage before the PATCH) so the capability works with the production stager. (The AuthExpired refresh branch already restages at line 112; the normal GET→PATCH path doesn't.)

Medium — staged secret leaks on pre-egress error (Claude)

gsuite/handlers.rs:85-87 stages before capability_execution(...); if input parsing fails there (or any pre-egress error), the staged secret is left in RuntimeSecretInjectionStore with no explicit cleanup — it only clears via the short TTL. The egress-boundary discard_staged_secret_injections only fires once egress is invoked. Not exploitable (scope+capability+handle keyed, TTL-bounded, not stealable), but a real cleanup gap. Stage after capability_execution succeeds, or discard_for_capability on the handler's error paths. (Fixing the P2 restaging should subsume this.)

Low

  • crates/ironclaw_reborn_composition/src/factory.rs:492 — require_product_auth_runtime_ports(&services)? is now called unconditionally in build_local_dev; benign (local-dev always wires egress + secret store), but the error string ("Google OAuth provider backend requires host runtime HTTP egress") is misleading if it fires without OAuth configured.
  • crates/ironclaw_host_api/src/dispatch.rs:698-708 — DispatchError::Debug redacts required_secrets names, but SecretHandle/RuntimeAuthGate derive value-revealing Debug elsewhere; inconsistent intent (no real leak — never serialized/logged).

What's good

Correct staging→egress handoff (the same secret_injection_store Arc survives all builder transforms incl. process-port rebinding); thorough fail-closed mapping (missing/expired/revoked → AuthRequired, backend → closed); MCP plan-once-and-reuse removes the old double-plan nondeterminism; scope isolation verified (tenant+user+agent+project+mission+thread+invocation); compiles on default + libsql/postgres; also fixes a pre-existing reservation leak on the OutputDecode path; no production unwrap/expect/panic.

Coverage gaps

A test that a dispatch-level AuthRequired persists BlockedAuth and is resumable (the P1); an add_attendees end-to-end through the production stager asserting the PATCH still has its credential (the P2); the pre-egress staging-leftover cleanup; the refresh path staging the second handle and egress consuming the refreshed value; a production-wiring (libsql) test that a composition-staged GSuite secret is readable by host_runtime egress through the captured ports.

Codex 5.5 caught both the auth-resume run-state bug (P1) and the two-request staging break (P2) — both verified against source; the cleanup-gap and nits are from my pass.

@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: Wire Reborn runtime credential consumers through staged credentials (auth propagation + MCP credential planning for first-party host HTTP egress).

Stats: 6 findings (from 9 raw, 6 after dedup). Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, pattern-refactor. Reviewers failed: none. Body-only: 0.

No findings from: security, bugs, conventions, maintainability, pattern-refactor.

performance

  1. Low auth_requirement() allocates a Vec on every is_auth_required() call (handlers.rs:248-272, conf 72) — anchor: crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:248
    • is_auth_required() is self.auth_requirement().is_some(), so every call runs the full match and allocates/clones a Vec only to discard it. Called at least once per failed first-party dispatch (runtime_adapters.rs:316).
  2. Low std::Mutex guards session_ids in async MCP client (lib.rs:501-538, conf 58) — anchor: crates/ironclaw_mcp/src/lib.rs:501
    • McpHostHttpClientState uses std::sync::Mutex for session_ids; current_session_id() and capture_session_id() lock synchronously in async context. Lock is not held across .await (not a correctness bug), but under concurrent MCP dispatch sharing one Arc, all callers serialize on this lock per handshake and per session-id capture. PR adds the session_ids map + cleanup drop handler.

tests

  1. Medium auth_requirement() misses Recovery(Configured) -> None branch (handlers.rs:250-258, conf 90) — anchor: crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:253
    • auth_requirement_classifies_each_credential_dispatch_reason exercises Recovery(SetupRequired) (returns Some(Vec::new())) but never constructs Recovery(Configured) to assert auth_requirement() returns None. The Configured branch (handlers.rs:253) suppresses the auth gate for backend infrastructure failures; a regression would incorrectly trigger the user-facing re-auth prompt for a backend config error.
    • Also flagged by: performance/Low
  2. Medium Custom Debug impl for DispatchError::AuthRequired has no redaction test (dispatch.rs:230-250, conf 85) — anchor: crates/ironclaw_host_api/src/dispatch.rs:230
    • The PR adds a manual fmt::Debug for DispatchError that deliberately omits required_secrets from AuthRequired output (replacing with "[N handle(s) redacted]"). This is a log-safety invariant; a regression would leak secret handle identifiers into tracing logs. No test asserts format!("{:?}", ...) excludes the handle name.
    • Also flagged by: maintainability/Low
  3. Medium stage_credential failure on the refresh-retry path is not exercised (handlers.rs:112-114, conf 80) — anchor: crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs:112
    • GsuiteExecutor::dispatch calls stage_credential twice: after initial resolution (line 82) and after token refresh retry (line 112). Tests cover staging failure on the first call but use noop_credential_stager() on the refresh path. No test drives a 401-triggered refresh where the post-refresh staging call fails, leaving that error path untested.
    • Also flagged by: conventions/Medium

local-patterns

  1. Nit // silent-ok: annotation used on a non-error-swallowing code path (host.rs:1643-1648, conf 75) — anchor: crates/ironclaw_capabilities/src/host.rs:1643
    • // silent-ok: is established in this repo exclusively for paths that swallow/ignore an error result (.ok(), unwrap_or_default(), warn-and-continue). Here it annotates a match arm that constructs required_secrets: Vec::new() — no error is swallowed. A future reader grepping silent-ok to audit error silencing hits a false positive.

Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
Comment thread crates/ironclaw_host_api/src/dispatch.rs
Comment thread crates/ironclaw_first_party_extensions/src/gsuite/handlers.rs
Comment thread crates/ironclaw_mcp/src/lib.rs
Comment thread crates/ironclaw_capabilities/src/host.rs Outdated
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Pass-2 review findings addressed (7690a38a9)

All 18 findings from the second review pass fixed. Summary:

Bugs fixed

B1 — AuthRequired swallowed by Resource when reconcile fails (runtime_adapters.rs)
account_or_release_failed_first_party_execution)?; was propagating via ?, returning Resource instead of AuthRequired when the resource governor failed to reconcile. Changed to if let Err(...) = ... { warn!(...) } so the auth signal survives accounting failures.

B2 — Reservation leaked on output serialization failure (runtime_adapters.rs)
serde_json::to_vec(&result.output) could fail (OOM/serde) and return OutputDecode without releasing the reservation. Added release_first_party_reservation to the map_err closure — every exit path now either reconciles or releases.

Security fixes

S1 — Recovery(Configured) wrongly triggered AuthRequired gate (gsuite/handlers.rs)
Catch-all Recovery(_) arm in auth_requirement() returned Some(vec![]) for every CredentialRecoveryKind, including Configured (a backend infra failure). Now returns None for Configured; all other recovery kinds (SetupRequired, ReauthorizeRequired, AccountSelectionRequired) retain Some(vec![]).

S2 — DispatchError Debug leaked secret handle names into logs (dispatch.rs)
Replaced derive(Debug) with a manual Debug impl that prints "[N handle(s) redacted]" for required_secrets instead of the actual handle identifiers.

S3 — stage_secret_error missed LeaseConsumed / UnknownLease (services.rs)
Both have user-actionable stable_reason values (CredentialExpired, MissingCredential) but were mapping to Backend. Added is_consumed() and is_unknown_lease() to the AuthRequired predicate to align with the store's own taxonomy.

Convention / quality fixes

  • map_stage_error Backend staging failure tagged BackendAuth (not HostApi) to match map_credential_error's category scheme
  • stage_secret_error doc comment corrected — function is used in production by stage_secret_once, not test-only
  • gsuite_error doc comment documents that CredentialRecoveryProjection context is intentionally dropped at the composition boundary (structured recovery hints deferred to follow-up)
  • validate_tools_call_credential_injections(&[...]) extracted in mcp/lib.rs — removes the .clone() on the credential injections Vec before the MCP handshake

Tests added (23 new)

Crate Tests
ironclaw_host_runtime (adapter) required_secrets forwarded through AuthRequired; reservation released on auth-required failure; specific handle forwarded; panic handler → Backend + reservation released
ironclaw_host_runtime (first_party) kind() returns None for AuthRequired; with_usage preserves required_secrets
ironclaw_host_runtime (services) stage_secret_error covers LeaseConsumed and UnknownLease; RegisteredRuntimeHealth empty-available, dedup, all-available
ironclaw_reborn_composition gsuite_error projects stage AuthRequired → FirstPartyCapabilityError::AuthRequired; stage Backend → Dispatch{Backend}
ironclaw_host_api event_kind() pinned as "auth_required" for both empty and non-empty handle lists
ironclaw_first_party_extensions merge_attendees empty-existing, no-email addition, no-additions edge cases

All PR-body test suites green. Pre-existing first_party_builtin_tools failures unaffected.

@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Addressed the review comments in 4fb4cc6a8.

Fixed Review Feedback

Reviewer Direct quote Fix
henrypark133 "never constructs Recovery(Configured) to assert auth_requirement() returns None" Already fixed in 78e0294a9 — added explicit Recovery(Configured) → None assertion in auth_requirement_classifies_each_credential_dispatch_reason.
henrypark133 "No test asserts format!("{:?}", ...) excludes the handle name" Added dispatch_error_auth_required_debug_redacts_required_secrets in host_api_contract.rs — asserts handle name absent and redaction count present.
henrypark133 "No test drives a 401-triggered refresh where the post-refresh staging call fails" Added gsuite_handler_stage_credential_failure_on_refresh_retry_returns_auth_required using a new FailOnNthCallStager test double that fails on the 2nd call; verifies auth_required() is true and only one egress request was made.
henrypark133 "all callers serialize on this lock per handshake and per session-id capture" Added doc comment explaining why std::sync::Mutex is appropriate: O(1) hold time, never held across .await, key includes invocation_id so concurrent invocations use disjoint map entries.
henrypark133 "A future reader grepping silent-ok to audit error silencing hits a false positive" Replaced // silent-ok: with a plain explanatory comment. silent-ok: is reserved for error-swallowing paths; this site constructs Vec::new() and swallows nothing.

CI

Clippy result_large_err failure fixed in d6ff73182: boxed CredentialRecoveryProjection in GsuiteCredentialDispatchReason::Recovery to reduce the variant from 240 bytes to pointer-sized (0 warnings on base branch, 30 introduced by this PR).

Validation

  • cargo fmt --check ✓
  • cargo test -p ironclaw_host_api -p ironclaw_capabilities -p ironclaw_first_party_extensions -p ironclaw_mcp ✓ (all green)

@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Addressed abbyshekit and henrypark133 review comments in d324be3dc and prior commits.

Fixed Review Feedback

Reviewer Direct quote Fix
abbyshekit "dispatch path doesn't use it. So turn-blocking / auth-resume paths that rely on BlockedAuth can't resume the blocked invocation" P1 fixed: both dispatch paths in host.rs (~L473, ~L810) now call apply_run_state_transition_if_configured instead of unconditional fail_run_if_configured("Dispatch"). AuthRequired → BlockedAuth, not Failed. Pinned by capability_host_blocks_auth_when_dispatch_returns_auth_required.
abbyshekit "GET consumes it and the PATCH fails with a missing required credential" P2 fixed: GsuiteCredentialStager passed through CapabilityExecution::execute into execute_add_attendees; re-staged before PATCH with network bytes accounting. Pinned by add_attendees_restages_credential_before_patch.
abbyshekit "staged secret is left in RuntimeSecretInjectionStore with no explicit cleanup" Fixed (Medium, subsumed by P2): moved stage_credential to after capability_execution() parse succeeds on both initial and refresh-retry paths. Parse failure no longer leaves a staged credential behind.
abbyshekit "require_product_auth_runtime_ports ... error string ... is misleading" Fixed (Low1): updated error message to name both prerequisites (HTTP egress + secret store) without tying it to Google OAuth.
abbyshekit "DispatchError::Debug redacts required_secrets names, but SecretHandle/RuntimeAuthGate derive value-revealing Debug elsewhere" Noted (Low2): acknowledged inconsistency; no secret leaks (never serialized/logged), deferring uniform redaction audit to a follow-up.
henrypark133 "never constructs Recovery(Configured) to assert auth_requirement() returns None" Already fixed in 78e0294a9 — added explicit Recovery(Configured) → None assertion in auth_requirement_classifies_each_credential_dispatch_reason.
henrypark133 "No test asserts format!("{:?}", ...) excludes the handle name" Added dispatch_error_auth_required_debug_redacts_required_secrets in host_api_contract.rs — asserts handle name absent and redaction count present.
henrypark133 "No test drives a 401-triggered refresh where the post-refresh staging call fails" Added gsuite_handler_stage_credential_failure_on_refresh_retry_returns_auth_required using FailOnNthCallStager; verifies auth_required() true and only one egress request made.

Validation

  • cargo test -p ironclaw_capabilities -p ironclaw_first_party_extensions -p ironclaw_host_api ✓ (all green)
  • cargo test -p ironclaw_host_runtime --lib ✓ (164 passed)
  • cargo fmt --check ✓

serrrfirat added a commit that referenced this pull request May 29, 2026
- services.rs: stage_secret_error now treats SecretExpired/LeaseRevoked/
  LeaseExpired as AuthRequired (was Backend); consume() failure path
  routes through the same classifier instead of collapsing to Backend.
  Recoverable credential conditions now surface through the runtime auth
  gate as intended by this PR.
- host.rs: obligation-derived AuthorizationRequiresAuth records a
  silent-ok rationale per .claude/rules/error-handling.md, naming why
  required_secrets is empty on the obligation path (unit variant, no
  payload to forward).
- gsuite/handlers.rs: documented why GsuiteAuthRequirement intentionally
  drops the Recovery projection details (preserved via reason()).

Tests:
- stage_secret_once now exercises UnknownSecret, SecretExpired,
  LeaseRevoked, LeaseExpired, BackendMisconfigured via a FailingSecretStore
  test double.
- DispatchError::AuthRequired -> DispatchFailureKind::AuthRequired and the
  'AuthRequired' as_str literal are now pinned in
  host_api_contract.rs.
- dispatch_kind_to_failure_pins_dispatch_error_top_level_kinds now covers
  DispatchFailureKind::AuthRequired -> RuntimeFailureKind::Authorization.
- error.rs: From<DispatchError::AuthRequired> preserves required_secrets
  (populated and empty).
- handlers.rs: auth_requirement_classifies_each_credential_dispatch_reason
  pins Recovery/MissingScopes/MissingAccessSecret -> Some(default),
  AuthRequired -> Some(secrets), BackendAuth/HostApi/None -> None.

Refs PR #4231.
serrrfirat added a commit that referenced this pull request May 29, 2026
Critical:
- fix resume_spawn_capability: missing AuthorizationRequiresAuth arm caused
  spawned-capability auth failures to surface as Failed instead of
  AuthRequired (symmetric fix with resume_capability)

Structural / type model:
- FirstPartyCapabilityError: struct with kind+auth-Option replaced by a
  proper enum (Dispatch / AuthRequired), making the two failure modes
  explicit. The dead 'kind' field on auth paths is gone; Display is now
  correct for both variants.
- GsuiteAuthRequirement struct deleted: auth_requirement() now returns
  Option<Vec<SecretHandle>> directly, removing one layer of indirection.
- FirstPartyAuthRequirement struct deleted: auth_required_with() takes
  Vec<SecretHandle> directly. Removes identical duplicate of GsuiteAuthRequirement.
- GsuiteCredentialStageError and ProductAuthCredentialStageError unified:
  both are now type aliases for ironclaw_host_api::CredentialStageError;
  the identity-mapping stage_runtime_credential_error function in
  reborn_composition is deleted.
- ironclaw_host_api: added CredentialStageError enum (shared staging error
  type) and DispatchError::event_kind() method (canonical event-token source).

Eliminated duplication:
- dispatch_error_kind() free function deleted from ironclaw_dispatcher and
  ironclaw_host_runtime/process_executor; call sites now use
  error.event_kind() from ironclaw_host_api.
- dispatch_kind_to_failure() free function replaced with
  From<DispatchFailureKind> for RuntimeFailureKind impl; call sites use
  RuntimeFailureKind::from(kind) / .into().

Test quality:
- FailingSecretStore (187-line SecretStore impl) deleted; stage_secret_error
  is now pub(crate) and tested directly with 6 synchronous table-driven
  cases — no async harness needed.
- Two near-identical from_dispatch_auth_required_* tests collapsed into one
  table-driven test.
- dispatch_kind_to_failure pin test converted to table-driven form.
- auth_requirement test assertions updated to Option<Vec<SecretHandle>>.

Refs PR #4231.
serrrfirat added a commit that referenced this pull request May 29, 2026
Bugs:
- runtime_adapters: absorb resource-accounting failure when handler returns
  AuthRequired so the auth signal survives governor.reconcile() failures
  (was: propagated via ? and returned Resource instead of AuthRequired)
- runtime_adapters: release reservation in serde_json::to_vec map_err
  closure so every exit path either reconciles or releases

Security:
- handlers.rs auth_requirement(): Recovery(Configured) now returns None
  (backend infra failure, not user-actionable); all other Recovery kinds
  (SetupRequired, ReauthorizeRequired, AccountSelectionRequired) retain
  Some(vec![]) behaviour
- dispatch.rs DispatchError: removed derive(Debug), added manual Debug impl
  that redacts required_secrets to "[N handle(s) redacted]" to prevent
  secret-handle identifiers leaking into logs
- services.rs stage_secret_error: adds is_consumed() and is_unknown_lease()
  to the AuthRequired predicate (both are user-actionable, align with
  SecretStoreError::stable_reason() taxonomy)

Conventions:
- gsuite/handlers.rs map_stage_error: Backend staging failure tagged
  BackendAuth (not HostApi) to match map_credential_error's category scheme
- services.rs stage_secret_error: doc comment clarified to say function is
  used in production by stage_secret_once (not 'for testing only')
- reborn_composition/gsuite.rs gsuite_error: doc comment explaining that
  CredentialRecoveryProjection context is intentionally dropped at this
  composition boundary (follow-up required for structured recovery hints)

Performance:
- mcp/lib.rs: extracted validate_tools_call_credential_injections(&[...])
  so the credential_injections Vec is validated by borrow, removing the
  .clone() before the MCP handshake

Tests added:
- first_party_runtime_adapter: assert required_secrets forwarded through
  AuthRequired (not silently dropped); reservation released after auth-required
  handler failure; required_secrets forwarded from handler with specific handle;
  panic handler contained as Backend with reservation released
- first_party.rs: kind() returns None for AuthRequired (both variants);
  with_usage() on AuthRequired preserves required_secrets; with_usage() on
  Dispatch variant round-trips kind()
- reborn_composition/gsuite.rs: gsuite_error projects stage AuthRequired to
  FirstPartyCapabilityError::AuthRequired; stage Backend to Dispatch{Backend}
- host_api_contract: event_kind() for AuthRequired pinned as 'auth_required'
  (empty and non-empty required_secrets both covered)
- services/tests: stage_secret_error covers LeaseConsumed and UnknownLease;
  RegisteredRuntimeHealth empty-available, deduplicate, all-available cases
- gsuite/handlers.rs: merge_attendees empty-existing, no-email addition,
  no-additions cases

Refs PR #4231.
@serrrfirat
serrrfirat force-pushed the codex/issue-4176-product-auth-runtime branch from d324be3 to 157bec6 Compare May 29, 2026 22:11
serrrfirat and others added 7 commits May 30, 2026 00:14
- services.rs: stage_secret_error now treats SecretExpired/LeaseRevoked/
  LeaseExpired as AuthRequired (was Backend); consume() failure path
  routes through the same classifier instead of collapsing to Backend.
  Recoverable credential conditions now surface through the runtime auth
  gate as intended by this PR.
- host.rs: obligation-derived AuthorizationRequiresAuth records a
  silent-ok rationale per .claude/rules/error-handling.md, naming why
  required_secrets is empty on the obligation path (unit variant, no
  payload to forward).
- gsuite/handlers.rs: documented why GsuiteAuthRequirement intentionally
  drops the Recovery projection details (preserved via reason()).

Tests:
- stage_secret_once now exercises UnknownSecret, SecretExpired,
  LeaseRevoked, LeaseExpired, BackendMisconfigured via a FailingSecretStore
  test double.
- DispatchError::AuthRequired -> DispatchFailureKind::AuthRequired and the
  'AuthRequired' as_str literal are now pinned in
  host_api_contract.rs.
- dispatch_kind_to_failure_pins_dispatch_error_top_level_kinds now covers
  DispatchFailureKind::AuthRequired -> RuntimeFailureKind::Authorization.
- error.rs: From<DispatchError::AuthRequired> preserves required_secrets
  (populated and empty).
- handlers.rs: auth_requirement_classifies_each_credential_dispatch_reason
  pins Recovery/MissingScopes/MissingAccessSecret -> Some(default),
  AuthRequired -> Some(secrets), BackendAuth/HostApi/None -> None.

Refs PR #4231.
Critical:
- fix resume_spawn_capability: missing AuthorizationRequiresAuth arm caused
  spawned-capability auth failures to surface as Failed instead of
  AuthRequired (symmetric fix with resume_capability)

Structural / type model:
- FirstPartyCapabilityError: struct with kind+auth-Option replaced by a
  proper enum (Dispatch / AuthRequired), making the two failure modes
  explicit. The dead 'kind' field on auth paths is gone; Display is now
  correct for both variants.
- GsuiteAuthRequirement struct deleted: auth_requirement() now returns
  Option<Vec<SecretHandle>> directly, removing one layer of indirection.
- FirstPartyAuthRequirement struct deleted: auth_required_with() takes
  Vec<SecretHandle> directly. Removes identical duplicate of GsuiteAuthRequirement.
- GsuiteCredentialStageError and ProductAuthCredentialStageError unified:
  both are now type aliases for ironclaw_host_api::CredentialStageError;
  the identity-mapping stage_runtime_credential_error function in
  reborn_composition is deleted.
- ironclaw_host_api: added CredentialStageError enum (shared staging error
  type) and DispatchError::event_kind() method (canonical event-token source).

Eliminated duplication:
- dispatch_error_kind() free function deleted from ironclaw_dispatcher and
  ironclaw_host_runtime/process_executor; call sites now use
  error.event_kind() from ironclaw_host_api.
- dispatch_kind_to_failure() free function replaced with
  From<DispatchFailureKind> for RuntimeFailureKind impl; call sites use
  RuntimeFailureKind::from(kind) / .into().

Test quality:
- FailingSecretStore (187-line SecretStore impl) deleted; stage_secret_error
  is now pub(crate) and tested directly with 6 synchronous table-driven
  cases — no async harness needed.
- Two near-identical from_dispatch_auth_required_* tests collapsed into one
  table-driven test.
- dispatch_kind_to_failure pin test converted to table-driven form.
- auth_requirement test assertions updated to Option<Vec<SecretHandle>>.

Refs PR #4231.
Bugs:
- runtime_adapters: absorb resource-accounting failure when handler returns
  AuthRequired so the auth signal survives governor.reconcile() failures
  (was: propagated via ? and returned Resource instead of AuthRequired)
- runtime_adapters: release reservation in serde_json::to_vec map_err
  closure so every exit path either reconciles or releases

Security:
- handlers.rs auth_requirement(): Recovery(Configured) now returns None
  (backend infra failure, not user-actionable); all other Recovery kinds
  (SetupRequired, ReauthorizeRequired, AccountSelectionRequired) retain
  Some(vec![]) behaviour
- dispatch.rs DispatchError: removed derive(Debug), added manual Debug impl
  that redacts required_secrets to "[N handle(s) redacted]" to prevent
  secret-handle identifiers leaking into logs
- services.rs stage_secret_error: adds is_consumed() and is_unknown_lease()
  to the AuthRequired predicate (both are user-actionable, align with
  SecretStoreError::stable_reason() taxonomy)

Conventions:
- gsuite/handlers.rs map_stage_error: Backend staging failure tagged
  BackendAuth (not HostApi) to match map_credential_error's category scheme
- services.rs stage_secret_error: doc comment clarified to say function is
  used in production by stage_secret_once (not 'for testing only')
- reborn_composition/gsuite.rs gsuite_error: doc comment explaining that
  CredentialRecoveryProjection context is intentionally dropped at this
  composition boundary (follow-up required for structured recovery hints)

Performance:
- mcp/lib.rs: extracted validate_tools_call_credential_injections(&[...])
  so the credential_injections Vec is validated by borrow, removing the
  .clone() before the MCP handshake

Tests added:
- first_party_runtime_adapter: assert required_secrets forwarded through
  AuthRequired (not silently dropped); reservation released after auth-required
  handler failure; required_secrets forwarded from handler with specific handle;
  panic handler contained as Backend with reservation released
- first_party.rs: kind() returns None for AuthRequired (both variants);
  with_usage() on AuthRequired preserves required_secrets; with_usage() on
  Dispatch variant round-trips kind()
- reborn_composition/gsuite.rs: gsuite_error projects stage AuthRequired to
  FirstPartyCapabilityError::AuthRequired; stage Backend to Dispatch{Backend}
- host_api_contract: event_kind() for AuthRequired pinned as 'auth_required'
  (empty and non-empty required_secrets both covered)
- services/tests: stage_secret_error covers LeaseConsumed and UnknownLease;
  RegisteredRuntimeHealth empty-available, deduplicate, all-available cases
- gsuite/handlers.rs: merge_attendees empty-existing, no-email addition,
  no-additions cases

Refs PR #4231.
- handlers.rs: add Recovery(Configured) -> None assertion to
  auth_requirement_classifies_each_credential_dispatch_reason so the S1
  fix has an explicit pin; SetupRequired still asserted as Some(vec![])
- first_party_runtime_adapter: add ReconcileFailingGovernor test double
  and first_party_adapter_releases_reservation_when_reconcile_fails_after_success
  to verify the reconcile-failure exit path releases the reservation and
  returns DispatchError::FirstParty { kind: Resource }
- first_party_runtime_adapter: add SucceedingFirstPartyHandler for the above
serrrfirat and others added 6 commits May 30, 2026 00:14
Box<CredentialRecoveryProjection> in GsuiteCredentialDispatchReason::Recovery
to reduce the variant size from 240 bytes to pointer-sized.  This eliminates
all 30 clippy::result_large_err warnings in ironclaw_first_party_extensions
that were promoted to errors by -D warnings in CI.

The base branch (reborn-integration) had 0 such warnings; all 30 were
introduced by this PR's GsuiteCredentialDispatchReason type.
H1 (already fixed): Recovery(Configured)->None test was added in the prior
commit that covered pass-2 regressions.

H2: Add dispatch_error_auth_required_debug_redacts_required_secrets test —
asserts format!("{:?}", AuthRequired { required_secrets: [handle] }) does
not contain the handle string and contains the redaction count.

H3: Add gsuite_handler_stage_credential_failure_on_refresh_retry_returns_auth_required —
tests the second stage_credential call (post-refresh, handlers.rs:112) by
using FailOnNthCallStager (new test double that fails on the 2nd call).
Egress returns 401 on first call to trigger refresh; stager fails before the
retry egress call so dispatch surfaces auth_required.

H4: Document why std::sync::Mutex is appropriate for session_ids in
McpHostHttpClientState: O(1) hold time, never held across .await, invocation_id
in key means concurrent dispatches act on disjoint entries (uncontested).

H5: Replace misapplied '// silent-ok:' comment in host.rs:1643 with a plain
explanatory comment. 'silent-ok:' is reserved in this codebase for code paths
that swallow error results; this site constructs required_secrets: Vec::new()
and swallows nothing.
The import was at the top-level module but DispatchError is only used
inside #[cfg(test)]. With -D warnings in CI this caused an unused-import
error in the lib target. Moved to the test module's use block.
…ees restage)

P1 — AuthRequired dispatch path now uses apply_run_state_transition_if_configured
instead of unconditional fail_run_if_configured("Dispatch"). When a first-party
handler returns DispatchError::AuthRequired, the run transitions to BlockedAuth
(not Failed), so auth-resume can pick it up.

Both dispatch paths patched:
- crates/ironclaw_capabilities/src/host.rs (~L473): invoke path
- crates/ironclaw_capabilities/src/host.rs (~L810): resume path

P2 — add_attendees one-shot staging break fixed. The staged-obligation store is
one-shot; the GET egress consumed the credential leaving the PATCH without one.

Fix: pass the GsuiteCredentialStager through CapabilityExecution::execute into
execute_add_attendees. Re-stage before the PATCH using the same access_secret,
with network_egress_bytes accumulation on error.

Medium (pre-egress cleanup) — moved stage_credential to after capability_execution()
parse succeeds on both the initial and refresh-retry paths. A parse failure no
longer leaves a staged credential in the injection store until TTL expiry.

Closes abbyshekit review findings P1, P2, and Medium.
…ekit Low1)

The old message 'Google OAuth provider backend requires host runtime HTTP egress'
implied Google OAuth was the reason, but product auth runtime ports back all
first-party credential staging. Use a message that names both prerequisites
(HTTP egress + secret store) without tying it to a specific provider.
…dees restage)

P1 regression pin:
- capability_host_blocks_auth_when_dispatch_returns_auth_required
  Uses AuthRequiredDispatcher that returns DispatchError::AuthRequired.
  Asserts run transitions to RunStatus::BlockedAuth with error_kind=AuthRequired.
  Before the fix this would have been RunStatus::Failed.

P2 regression pin:
- add_attendees_restages_credential_before_patch
  Uses FailOnNthCallStager::fail_on_second_call() to verify restaging happens
  before the PATCH egress. Without the fix, only one staging call is made and
  the dispatch returns success; with the fix, the second staging call is attempted
  (and fails AuthRequired here), proving the restage is in place.
@serrrfirat
serrrfirat force-pushed the codex/issue-4176-product-auth-runtime branch from 157bec6 to 6a3156c Compare May 29, 2026 22:14
- Keep both first_party_registry and product_auth_filesystem setup
- Always compose product_auth with filesystem fallback (origin)
- Register GSuite first-party handlers unconditionally (HEAD)
- Gate FilesystemAuthProductServices/UnavailableAuthProviderClient imports
  on libsql|postgres features to silence unused-import warnings
- Apply rustfmt to resolved hunks
Use direct binding instead of Some(...).unwrap() when registering
GSuite first-party handlers in the production composition path.
@serrrfirat
serrrfirat merged commit 362c87a into reborn-integration May 29, 2026
21 checks passed
@serrrfirat
serrrfirat deleted the codex/issue-4176-product-auth-runtime branch May 29, 2026 22:35
theredspoon pushed a commit to theredspoon/ironclaw that referenced this pull request Jun 21, 2026
* Wire product auth staged credentials into runtime consumers

* Refine product auth runtime staging boundaries

* Tighten runtime auth planning shapes

* fix: address code review findings

- services.rs: stage_secret_error now treats SecretExpired/LeaseRevoked/
  LeaseExpired as AuthRequired (was Backend); consume() failure path
  routes through the same classifier instead of collapsing to Backend.
  Recoverable credential conditions now surface through the runtime auth
  gate as intended by this PR.
- host.rs: obligation-derived AuthorizationRequiresAuth records a
  silent-ok rationale per .claude/rules/error-handling.md, naming why
  required_secrets is empty on the obligation path (unit variant, no
  payload to forward).
- gsuite/handlers.rs: documented why GsuiteAuthRequirement intentionally
  drops the Recovery projection details (preserved via reason()).

Tests:
- stage_secret_once now exercises UnknownSecret, SecretExpired,
  LeaseRevoked, LeaseExpired, BackendMisconfigured via a FailingSecretStore
  test double.
- DispatchError::AuthRequired -> DispatchFailureKind::AuthRequired and the
  'AuthRequired' as_str literal are now pinned in
  host_api_contract.rs.
- dispatch_kind_to_failure_pins_dispatch_error_top_level_kinds now covers
  DispatchFailureKind::AuthRequired -> RuntimeFailureKind::Authorization.
- error.rs: From<DispatchError::AuthRequired> preserves required_secrets
  (populated and empty).
- handlers.rs: auth_requirement_classifies_each_credential_dispatch_reason
  pins Recovery/MissingScopes/MissingAccessSecret -> Some(default),
  AuthRequired -> Some(secrets), BackendAuth/HostApi/None -> None.

Refs PR nearai#4231.

* refactor: address thermo-nuclear code quality findings

Critical:
- fix resume_spawn_capability: missing AuthorizationRequiresAuth arm caused
  spawned-capability auth failures to surface as Failed instead of
  AuthRequired (symmetric fix with resume_capability)

Structural / type model:
- FirstPartyCapabilityError: struct with kind+auth-Option replaced by a
  proper enum (Dispatch / AuthRequired), making the two failure modes
  explicit. The dead 'kind' field on auth paths is gone; Display is now
  correct for both variants.
- GsuiteAuthRequirement struct deleted: auth_requirement() now returns
  Option<Vec<SecretHandle>> directly, removing one layer of indirection.
- FirstPartyAuthRequirement struct deleted: auth_required_with() takes
  Vec<SecretHandle> directly. Removes identical duplicate of GsuiteAuthRequirement.
- GsuiteCredentialStageError and ProductAuthCredentialStageError unified:
  both are now type aliases for ironclaw_host_api::CredentialStageError;
  the identity-mapping stage_runtime_credential_error function in
  reborn_composition is deleted.
- ironclaw_host_api: added CredentialStageError enum (shared staging error
  type) and DispatchError::event_kind() method (canonical event-token source).

Eliminated duplication:
- dispatch_error_kind() free function deleted from ironclaw_dispatcher and
  ironclaw_host_runtime/process_executor; call sites now use
  error.event_kind() from ironclaw_host_api.
- dispatch_kind_to_failure() free function replaced with
  From<DispatchFailureKind> for RuntimeFailureKind impl; call sites use
  RuntimeFailureKind::from(kind) / .into().

Test quality:
- FailingSecretStore (187-line SecretStore impl) deleted; stage_secret_error
  is now pub(crate) and tested directly with 6 synchronous table-driven
  cases — no async harness needed.
- Two near-identical from_dispatch_auth_required_* tests collapsed into one
  table-driven test.
- dispatch_kind_to_failure pin test converted to table-driven form.
- auth_requirement test assertions updated to Option<Vec<SecretHandle>>.

Refs PR nearai#4231.

* fix: address pass-2 code review findings

Bugs:
- runtime_adapters: absorb resource-accounting failure when handler returns
  AuthRequired so the auth signal survives governor.reconcile() failures
  (was: propagated via ? and returned Resource instead of AuthRequired)
- runtime_adapters: release reservation in serde_json::to_vec map_err
  closure so every exit path either reconciles or releases

Security:
- handlers.rs auth_requirement(): Recovery(Configured) now returns None
  (backend infra failure, not user-actionable); all other Recovery kinds
  (SetupRequired, ReauthorizeRequired, AccountSelectionRequired) retain
  Some(vec![]) behaviour
- dispatch.rs DispatchError: removed derive(Debug), added manual Debug impl
  that redacts required_secrets to "[N handle(s) redacted]" to prevent
  secret-handle identifiers leaking into logs
- services.rs stage_secret_error: adds is_consumed() and is_unknown_lease()
  to the AuthRequired predicate (both are user-actionable, align with
  SecretStoreError::stable_reason() taxonomy)

Conventions:
- gsuite/handlers.rs map_stage_error: Backend staging failure tagged
  BackendAuth (not HostApi) to match map_credential_error's category scheme
- services.rs stage_secret_error: doc comment clarified to say function is
  used in production by stage_secret_once (not 'for testing only')
- reborn_composition/gsuite.rs gsuite_error: doc comment explaining that
  CredentialRecoveryProjection context is intentionally dropped at this
  composition boundary (follow-up required for structured recovery hints)

Performance:
- mcp/lib.rs: extracted validate_tools_call_credential_injections(&[...])
  so the credential_injections Vec is validated by borrow, removing the
  .clone() before the MCP handshake

Tests added:
- first_party_runtime_adapter: assert required_secrets forwarded through
  AuthRequired (not silently dropped); reservation released after auth-required
  handler failure; required_secrets forwarded from handler with specific handle;
  panic handler contained as Backend with reservation released
- first_party.rs: kind() returns None for AuthRequired (both variants);
  with_usage() on AuthRequired preserves required_secrets; with_usage() on
  Dispatch variant round-trips kind()
- reborn_composition/gsuite.rs: gsuite_error projects stage AuthRequired to
  FirstPartyCapabilityError::AuthRequired; stage Backend to Dispatch{Backend}
- host_api_contract: event_kind() for AuthRequired pinned as 'auth_required'
  (empty and non-empty required_secrets both covered)
- services/tests: stage_secret_error covers LeaseConsumed and UnknownLease;
  RegisteredRuntimeHealth empty-available, deduplicate, all-available cases
- gsuite/handlers.rs: merge_attendees empty-existing, no-email addition,
  no-additions cases

Refs PR nearai#4231.

* test: cover pass-2 fix regressions

- handlers.rs: add Recovery(Configured) -> None assertion to
  auth_requirement_classifies_each_credential_dispatch_reason so the S1
  fix has an explicit pin; SetupRequired still asserted as Some(vec![])
- first_party_runtime_adapter: add ReconcileFailingGovernor test double
  and first_party_adapter_releases_reservation_when_reconcile_fails_after_success
  to verify the reconcile-failure exit path releases the reservation and
  returns DispatchError::FirstParty { kind: Resource }
- first_party_runtime_adapter: add SucceedingFirstPartyHandler for the above

* fix: resolve Clippy result_large_err CI failure

Box<CredentialRecoveryProjection> in GsuiteCredentialDispatchReason::Recovery
to reduce the variant size from 240 bytes to pointer-sized.  This eliminates
all 30 clippy::result_large_err warnings in ironclaw_first_party_extensions
that were promoted to errors by -D warnings in CI.

The base branch (reborn-integration) had 0 such warnings; all 30 were
introduced by this PR's GsuiteCredentialDispatchReason type.

* fix: address henrypark133 review comments

H1 (already fixed): Recovery(Configured)->None test was added in the prior
commit that covered pass-2 regressions.

H2: Add dispatch_error_auth_required_debug_redacts_required_secrets test —
asserts format!("{:?}", AuthRequired { required_secrets: [handle] }) does
not contain the handle string and contains the redaction count.

H3: Add gsuite_handler_stage_credential_failure_on_refresh_retry_returns_auth_required —
tests the second stage_credential call (post-refresh, handlers.rs:112) by
using FailOnNthCallStager (new test double that fails on the 2nd call).
Egress returns 401 on first call to trigger refresh; stager fails before the
retry egress call so dispatch surfaces auth_required.

H4: Document why std::sync::Mutex is appropriate for session_ids in
McpHostHttpClientState: O(1) hold time, never held across .await, invocation_id
in key means concurrent dispatches act on disjoint entries (uncontested).

H5: Replace misapplied '// silent-ok:' comment in host.rs:1643 with a plain
explanatory comment. 'silent-ok:' is reserved in this codebase for code paths
that swallow error results; this site constructs required_secrets: Vec::new()
and swallows nothing.

* fix: move DispatchError import to test module in process_executor.rs

The import was at the top-level module but DispatchError is only used
inside #[cfg(test)]. With -D warnings in CI this caused an unused-import
error in the lib target. Moved to the test module's use block.

* fix: address abbyshekit review findings (P1 BlockAuth + P2 add_attendees restage)

P1 — AuthRequired dispatch path now uses apply_run_state_transition_if_configured
instead of unconditional fail_run_if_configured("Dispatch"). When a first-party
handler returns DispatchError::AuthRequired, the run transitions to BlockedAuth
(not Failed), so auth-resume can pick it up.

Both dispatch paths patched:
- crates/ironclaw_capabilities/src/host.rs (~L473): invoke path
- crates/ironclaw_capabilities/src/host.rs (~L810): resume path

P2 — add_attendees one-shot staging break fixed. The staged-obligation store is
one-shot; the GET egress consumed the credential leaving the PATCH without one.

Fix: pass the GsuiteCredentialStager through CapabilityExecution::execute into
execute_add_attendees. Re-stage before the PATCH using the same access_secret,
with network_egress_bytes accumulation on error.

Medium (pre-egress cleanup) — moved stage_credential to after capability_execution()
parse succeeds on both the initial and refresh-retry paths. A parse failure no
longer leaves a staged credential in the injection store until TTL expiry.

Closes abbyshekit review findings P1, P2, and Medium.

* fix: clarify require_product_auth_runtime_ports error message (abbyshekit Low1)

The old message 'Google OAuth provider backend requires host runtime HTTP egress'
implied Google OAuth was the reason, but product auth runtime ports back all
first-party credential staging. Use a message that names both prerequisites
(HTTP egress + secret store) without tying it to a specific provider.

* test: pin P1/P2 regression coverage (BlockedAuth dispatch + add_attendees restage)

P1 regression pin:
- capability_host_blocks_auth_when_dispatch_returns_auth_required
  Uses AuthRequiredDispatcher that returns DispatchError::AuthRequired.
  Asserts run transitions to RunStatus::BlockedAuth with error_kind=AuthRequired.
  Before the fix this would have been RunStatus::Failed.

P2 regression pin:
- add_attendees_restages_credential_before_patch
  Uses FailOnNthCallStager::fail_on_second_call() to verify restaging happens
  before the PATCH egress. Without the fix, only one staging call is made and
  the dispatch returns success; with the fix, the second staging call is attempted
  (and fails AuthRequired here), proving the restage is in place.

* fmt: fix rustfmt formatting in capability_host_auth_run_state_contract test

* fix: remove .unwrap() in production path for no-panics check

Use direct binding instead of Some(...).unwrap() when registering
GSuite first-party handlers in the production composition path.

---------

Co-authored-by: serrrfirat <serrrfirat@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants