diff --git a/crates/ironclaw_first_party_extensions/assets/nearai-mcp/manifest.toml b/crates/ironclaw_first_party_extensions/assets/nearai-mcp/manifest.toml index f5c51c3a850..e080575c1a3 100644 --- a/crates/ironclaw_first_party_extensions/assets/nearai-mcp/manifest.toml +++ b/crates/ironclaw_first_party_extensions/assets/nearai-mcp/manifest.toml @@ -15,7 +15,7 @@ id = "nearai.search" description = "Search through the NEAR AI MCP server." effects = ["dispatch_capability", "network", "use_secret"] runtime_credentials = [ - { handle = "llm_nearai_api_key", audience = { scheme = "https", host_pattern = "*.near.ai" }, target = { type = "header", name = "authorization", prefix = "Bearer " } }, + { handle = "llm_nearai_api_key", source = { type = "product_auth_account", provider = "nearai" }, audience = { scheme = "https", host_pattern = "*.near.ai" }, target = { type = "header", name = "authorization", prefix = "Bearer " } }, ] default_permission = "ask" visibility = "model" diff --git a/crates/ironclaw_reborn_composition/src/factory.rs b/crates/ironclaw_reborn_composition/src/factory.rs index 7baaccc5cb0..6eb2c22866e 100644 --- a/crates/ironclaw_reborn_composition/src/factory.rs +++ b/crates/ironclaw_reborn_composition/src/factory.rs @@ -1863,7 +1863,8 @@ mod tests { CapabilityGrant, CapabilityGrantId, CapabilityId, CapabilitySet, EffectKind, ExecutionContext, ExtensionId, GrantConstraints, InvocationId, MountAlias, MountGrant, NetworkPolicy, NetworkScheme, NetworkTargetPattern, Principal, ResourceEstimate, - ResourceScope, RuntimeKind, ScopedPath, SecretHandle, TrustClass, UserId, VirtualPath, + ResourceScope, RuntimeCredentialAccountProviderId, RuntimeCredentialRequirementSource, + RuntimeKind, ScopedPath, SecretHandle, TrustClass, UserId, VirtualPath, }; use ironclaw_host_runtime::{ RuntimeCapabilityOutcome, RuntimeCapabilityRequest, RuntimeFailureKind, @@ -2191,6 +2192,18 @@ mod tests { search.runtime_credentials[0].handle, SecretHandle::new("llm_nearai_api_key").unwrap() ); + // NEAR AI MCP credential is sourced from a product-auth account so that the + // user-facing setup flow is the manual-token product-auth surface (shared + // with GitHub WASM), not an out-of-band SecretStore handle drop. + // The 'handle' field remains the staging slot name the MCP egress planner + // reads from RuntimeSecretInjectionStore after the obligation handler resolves + // the access secret via RuntimeCredentialAccountResolver. + assert_eq!( + search.runtime_credentials[0].source, + RuntimeCredentialRequirementSource::ProductAuthAccount { + provider: RuntimeCredentialAccountProviderId::new("nearai").unwrap(), + } + ); assert_eq!( search.runtime_credentials[0].audience.host_pattern, "private.near.ai" diff --git a/crates/ironclaw_reborn_composition/src/nearai_mcp.rs b/crates/ironclaw_reborn_composition/src/nearai_mcp.rs index 9d1424da98d..a6c9cddc6f4 100644 --- a/crates/ironclaw_reborn_composition/src/nearai_mcp.rs +++ b/crates/ironclaw_reborn_composition/src/nearai_mcp.rs @@ -95,9 +95,17 @@ struct NearAiMcpEgressPlanner { impl McpHostHttpEgressPlanner for NearAiMcpEgressPlanner { fn plan(&self, request: McpHostHttpEgressPlanRequest<'_>) -> McpHostHttpEgressPlan { - // This is a narrow NEAR AI MCP adapter. Do not grow this into the - // generic product-auth-to-MCP planner; nearai/ironclaw#4176 owns that - // bridge, including account selection and typed AuthRequired recovery. + // This is a narrow NEAR AI MCP adapter. Do not grow this into a generic + // product-auth-to-MCP planner. Account selection and typed AuthRequired + // recovery happen upstream: the bundled manifest declares the credential + // with `source = product_auth_account(provider = "nearai")`, so the + // authorization layer emits `Obligation::InjectCredentialAccountOnce` + // before this planner runs. The obligation handler resolves the + // configured nearai product-auth account via + // `RuntimeCredentialAccountResolver`, stages the access secret into + // `RuntimeSecretInjectionStore` under the `llm_nearai_api_key` slot, and + // this planner only references that slot via the `StagedObligation` + // source. Closes the MCP slice of nearai/ironclaw#4176. if request.provider.as_str() != NEARAI_EXTENSION_ID || !nearai_mcp_url_allowed(request.url, &self.endpoint) {