Wire Reborn auth consumers through staged credentials - #4231
Conversation
There was a problem hiding this comment.
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.
serrrfirat
left a comment
There was a problem hiding this comment.
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).
- 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.
Fix notes (commit
|
| 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.
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
left a comment
There was a problem hiding this comment.
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 propagatesOutputDecodewithout releasing the reservation. Every exit path must callrelease_first_party_reservationorgovernor.reconcile. Add cleanup in themap_errclosure. (conf 97) - T6
services.rs—RegisteredRuntimeHealth::missing_runtime_backendson an empty-available list never tested. Addtests::services::registered_runtime_health_reports_missing_backends_when_available_is_empty. (conf 93) - T7
gsuite/handlers.rs—merge_attendeeshas no test for the no-email addition path (appended unconditionally) or empty existing list. Addtests::handlers::merge_attendees_appends_addition_without_email_fieldand::merge_attendees_with_empty_existing_list. (conf 93)
🔵 Nit (summary-only)
- P2
ironclaw_mcp/src/lib.rs:573—credential_injectionsVec cloned for preflight validation becauseMcpJsonRpcMethod::credential_injectionsconsumes it. Extractable as a borrow-basedvalidate_tools_call_credential_injections(&[...])helper to avoid the clone. (conf 90) - C4
services.rs:228— Doc comment onstage_secret_errorsays "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 bystage_secret_once." (conf 80)
Reviewers failed: none. Intent confidence: high.
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.
Review — wire Reborn auth consumers through staged credentialsCombined pass (Claude Code + Codex 5.5 High —
|
henrypark133
left a comment
There was a problem hiding this comment.
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
- 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).
- 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
- 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
- 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
- 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
- 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.
Pass-2 review findings addressed (
|
| 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.
|
Addressed the review comments in Fixed Review Feedback
CIClippy Validation
|
|
Addressed abbyshekit and henrypark133 review comments in Fixed Review Feedback
Validation
|
- 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.
d324be3 to
157bec6
Compare
- 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
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.
157bec6 to
6a3156c
Compare
- 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.
* 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>
Summary
Refs #4176.
This PR wires the runtime credential consumer paths that depend on host-owned staged credentials:
InjectSecretOncebefore first-party host HTTP egress consumes theStagedObligation;DispatchError::AuthRequiredso the host runtime can returnRuntimeCapabilityOutcome::AuthRequiredinstead of a generic failed runtime dispatch;tools/callJSON-RPC body once before handshake and thread that validated plan into the eventualtools/callsend;Open-question decisions
CredentialAccountService; staticruntime_credentialsare deferred until account bindings are stable.github_tokenmanifest-driven with manual-token product-auth as the first account model; the existing staged host HTTP path is covered bygithub_wasm_runtime_contract.CredentialAccountService::refresh_account.Tests
cargo test -p ironclaw_reborn_composition --test gsuitecargo test -p ironclaw_mcpcargo test -p ironclaw_host_runtime first_party_adapter_maps_handler_auth_required_to_dispatch_auth_requiredcargo test -p ironclaw_host_runtime --test github_wasm_runtime_contractcargo test -p ironclaw_first_party_extensions -p ironclaw_host_api -p ironclaw_capabilitiesgit diff --check