Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
354 changes: 354 additions & 0 deletions crates/ironclaw_host_runtime/src/credential_presence.rs

Large diffs are not rendered by default.

484 changes: 484 additions & 0 deletions crates/ironclaw_host_runtime/src/credential_presence_cache.rs

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions crates/ironclaw_host_runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ use std::{collections::BTreeMap, env, fmt};
use thiserror::Error;

mod capability_catalog;
mod credential_presence;
mod credential_presence_cache;
mod document_output;
mod egress;
mod extension_contracts;
Expand Down
11 changes: 11 additions & 0 deletions crates/ironclaw_host_runtime/src/obligations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ pub trait RuntimeCredentialAccountResolver: Send + Sync + fmt::Debug {
&self,
request: RuntimeCredentialAccountRequest<'_>,
) -> Result<RuntimeCredentialAccessSecret, CredentialStageError>;

/// Side-effect-free presence check: is a configured account available for
/// this requirement WITHOUT staging or refreshing any secret material? Safe
/// to call repeatedly on every capability-surface render. `Ok(true)` =
/// configured account present; `Ok(false)` = no configured account (user must
/// sign in); `Err(Backend)` = indeterminate backend failure (caller fails
/// open). Must NOT perform token refresh, network staging, or lease consumption.
async fn account_configured(
&self,
request: RuntimeCredentialAccountRequest<'_>,
) -> Result<bool, CredentialStageError>;
}

/// Runtime secret material staged after `InjectSecretOnce` lease consumption.
Expand Down
52 changes: 51 additions & 1 deletion crates/ironclaw_host_runtime/src/production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ use crate::{
RuntimeCapabilityCompleted, RuntimeCapabilityFailure, RuntimeCapabilityOutcome,
RuntimeCapabilityRequest, RuntimeCapabilityResumeRequest, RuntimeFailureKind, RuntimeGateId,
RuntimeStatusRequest, RuntimeWorkId, RuntimeWorkSummary, VisibleCapabilityRequest,
VisibleCapabilitySurface, obligations::secret_present, plan_capability,
VisibleCapabilitySurface,
credential_presence::ProductionCredentialPresence,
credential_presence_cache::CredentialPresenceCache,
obligations::{RuntimeCredentialAccountResolver, secret_present},
plan_capability,
surface::CapabilityCatalog,
};

Expand Down Expand Up @@ -142,6 +146,22 @@ pub struct DefaultHostRuntime {
// arch-exempt: optional_arc, credential pre-flight is disabled in minimal/test
// host-runtime graphs that do not wire a secret store, plan #4539 (Fix B)
credential_preflight_store: Option<Arc<dyn SecretStore>>,
/// Optional product-auth account resolver used for capability-surface
/// credential-presence checks (`visible_capabilities` `NeedsAuth`
/// downgrade — issue #5416, Phase 2).
///
/// Distinct from the dispatch-time resolver wired into
/// `BuiltinObligationHandler`: that one stages the actual credential
/// material for one capability invocation; this one only answers "is an
/// account configured" for surface rendering. Production composition
/// wires both from the same underlying resolver.
// arch-exempt: optional_arc, credential presence is disabled in minimal/test
// host-runtime graphs that do not wire a resolver, plan #5416 (Phase 2)
credential_account_resolver: Option<Arc<dyn RuntimeCredentialAccountResolver>>,
/// Short-TTL cache backing the credential-presence checks above. Always
/// constructed (cheap, in-memory); it is inert unless a presence check
/// actually runs.
credential_presence_cache: Arc<CredentialPresenceCache>,
surface_version: CapabilitySurfaceVersion,
runtime_policy: EffectiveRuntimePolicy,
}
Expand Down Expand Up @@ -208,6 +228,8 @@ impl DefaultHostRuntime {
runtime_health: None,
obligation_handler: None,
credential_preflight_store: None,
credential_account_resolver: None,
credential_presence_cache: Arc::new(CredentialPresenceCache::new()),
surface_version,
runtime_policy,
}
Expand Down Expand Up @@ -385,6 +407,22 @@ impl DefaultHostRuntime {
self
}

/// Attaches the product-auth account resolver used for capability-surface
/// credential-presence checks (`visible_capabilities` `NeedsAuth`
/// downgrade).
///
/// Production code must use `HostRuntimeServices::build_host_runtime()`
/// which wires the resolver automatically. This setter is `pub(crate)` for
/// the same reason as [`Self::with_credential_preflight_store`]: prevent a
/// second public seam for this wiring.
pub(crate) fn with_credential_account_resolver(
mut self,
resolver: Arc<dyn RuntimeCredentialAccountResolver>,
) -> Self {
self.credential_account_resolver = Some(resolver);
self
}

/// Spawns an already-authorized process request through the configured
/// process manager.
pub async fn spawn_process(
Expand Down Expand Up @@ -1044,6 +1082,18 @@ impl HostRuntime for DefaultHostRuntime {
Some(filesystem) => catalog.with_filesystem(filesystem),
None => catalog,
};
let presence = ProductionCredentialPresence {
secret_store: self.credential_preflight_store.as_deref(),
resolver: self.credential_account_resolver.as_deref(),
cache: &self.credential_presence_cache,
};
let catalog = if self.credential_preflight_store.is_some()
|| self.credential_account_resolver.is_some()
{
catalog.with_credential_presence(&presence)
} else {
catalog
};
catalog.visible_capabilities(request).await
}

Expand Down
7 changes: 7 additions & 0 deletions crates/ironclaw_host_runtime/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,13 @@ where
if let Some(secret_store) = &self.secret_store {
runtime = runtime.with_credential_preflight_store(Arc::clone(secret_store));
}
// Wire the same product-auth account resolver used by the obligation
// handler (below) into the capability-surface credential-presence
// check, so `visible_capabilities` and dispatch-time staging agree on
// "is this account connected" (issue #5416, Phase 2).
if let Some(resolver) = &self.runtime_credential_account_resolver {
runtime = runtime.with_credential_account_resolver(Arc::clone(resolver));
}
runtime.with_obligation_handler(Arc::new(self.builtin_obligation_handler()))
}

Expand Down
Loading
Loading