Skip to content
Merged
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
17 changes: 14 additions & 3 deletions account-manager/core/src/providers/launch_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ use super::{
launch_spec_for, spawn_command, spawn_launch_with, LaunchEnvironment, ProviderAdapter,
};

mod external;
pub use external::ExternalLaunchCustody;

/// Closed finalization reasons. An unconfirmed exit is never a finalization.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LaunchFinalization {
Refused,
SpawnFailed,
Expand All @@ -27,7 +30,7 @@ pub enum LaunchFinalization {
Cancelled,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum LaunchPhase {
Prepared,
Constructing,
Expand All @@ -37,7 +40,7 @@ pub enum LaunchPhase {
}

/// Names and sources only; no inherited or resolved environment values.
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum CredentialEnvironmentInput {
SelectedCredential(String),
Removed(String),
Expand All @@ -62,6 +65,14 @@ pub struct LaunchLifecycleView {
#[derive(Clone)]
pub struct LaunchLifecycleHandle(Arc<Mutex<LaunchLifecycleView>>);

impl PartialEq for LaunchLifecycleHandle {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}

impl Eq for LaunchLifecycleHandle {}

impl LaunchLifecycleHandle {
pub fn snapshot(&self) -> LaunchLifecycleView {
self.0.lock().expect("launch lifecycle lock").clone()
Expand Down
172 changes: 172 additions & 0 deletions account-manager/core/src/providers/launch_lifecycle/external.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//! Selected-use custody for synchronous external process runners.

use crate::account_authority::SelectedUseLease;

use super::*;
use crate::providers::LaunchSpec;

/// A declared launch whose process is owned by an external runner. That runner
/// must confirm the entire owned process tree is empty before finalization.
/// Dropping unknown custody retains the selected-use barrier until process exit.
#[must_use = "external launch custody must be settled from live process evidence"]
pub struct ExternalLaunchCustody {
lifecycle: LaunchLifecycle,
lease: Option<SelectedUseLease>,
}

impl LaunchLifecycle {
/// Adopt an adapter declaration under a fresh exact-selection lease. Resolve
/// vendor-home capabilities only in `construct`, while admission is pending.
/// Credential-store launches use [`Self::spawn`] instead.
pub fn prepare_external<T, E: From<Error>>(
self,
registry: &StoredAccountRegistry,
adapter: &dyn ProviderAdapter,
construct: impl FnOnce(&LaunchSpec) -> std::result::Result<T, E>,
) -> std::result::Result<(ExternalLaunchCustody, T), E> {
let binding = {
let mut view = self.handle.0.lock().expect("launch lifecycle lock");
self.check_admission(&view)?;
view.phase = LaunchPhase::Constructing;
view.binding.clone()
};
if binding.provider_id != adapter.id() {
return Err(Error::UnknownProvider(binding.provider_id).into());
}
let lease = registry.acquire_selected_use(&binding)?;
let account = registry.complete(&binding.provider_id, &binding.account_id)?;
let spec = launch_spec_for(adapter, &account)?;
if spec.requires_credential() {
return Err(refused("external runner requires a vendor-home declaration").into());
}
{
let mut view = self.handle.0.lock().expect("launch lifecycle lock");
self.check_admission(&view)?;
view.environment = spec
.environment
.iter()
.filter_map(|entry| match entry {
LaunchEnvironment::Remove { name } => {
Some(CredentialEnvironmentInput::Removed(name.clone()))
}
_ => None,
})
.collect();
}
let material = construct(&spec)?;
self.check_admission(&self.handle.0.lock().expect("launch lifecycle lock"))?;
Ok((
ExternalLaunchCustody {
lifecycle: self,
lease: Some(lease),
},
material,
))
}
}

impl ExternalLaunchCustody {
pub fn lifecycle(&self) -> LaunchLifecycleHandle {
self.lifecycle.handle()
}

/// Open an external credential capability while construction is pending.
/// Revocation or expiry during construction refuses the resulting material.
pub fn construct<T, E: From<Error>>(
&self,
construct: impl FnOnce() -> std::result::Result<T, E>,
) -> std::result::Result<T, E> {
{
let view = self
.lifecycle
.handle
.0
.lock()
.expect("launch lifecycle lock");
if view.phase != LaunchPhase::Constructing {
return Err(refused("external launch construction is no longer pending").into());
}
self.lifecycle.check_admission(&view)?;
}
let material = construct()?;
self.lifecycle.check_admission(
&self
.lifecycle
.handle
.0
.lock()
.expect("launch lifecycle lock"),
)?;
Ok(material)
}

/// Admission and revocation serialize before the runner can release a child.
pub fn begin_spawn(&mut self) -> Result<()> {
let mut view = self
.lifecycle
.handle
.0
.lock()
.expect("launch lifecycle lock");
if view.phase != LaunchPhase::Constructing {
return Err(refused("external launch is not pending construction"));
}
self.lifecycle.check_admission(&view)?;
view.phase = LaunchPhase::Running;
Ok(())
}

/// The caller has live evidence that no child was released, or that the
/// complete owned tree has exited. Unknown exit must use `exit_unknown`.
pub fn finalize(&mut self, reason: LaunchFinalization) {
let mut view = self
.lifecycle
.handle
.0
.lock()
.expect("launch lifecycle lock");
let allowed = match reason {
LaunchFinalization::Refused => {
matches!(view.phase, LaunchPhase::Constructing | LaunchPhase::Running)
}
LaunchFinalization::SpawnFailed => view.phase == LaunchPhase::Running,
_ => matches!(view.phase, LaunchPhase::Running | LaunchPhase::ExitUnknown),
};
if allowed {
view.phase = LaunchPhase::Finalized(reason);
self.lease.take();
}
}

/// Also applies to an unconfirmed preflight child during construction.
pub fn exit_unknown(&mut self) {
let mut view = self
.lifecycle
.handle
.0
.lock()
.expect("launch lifecycle lock");
if !matches!(view.phase, LaunchPhase::Finalized(_)) {
view.phase = LaunchPhase::ExitUnknown;
}
}
}

impl Drop for ExternalLaunchCustody {
fn drop(&mut self) {
let mut view = self
.lifecycle
.handle
.0
.lock()
.expect("launch lifecycle lock");
if matches!(view.phase, LaunchPhase::Running | LaunchPhase::ExitUnknown) {
view.phase = LaunchPhase::ExitUnknown;
if let Some(lease) = self.lease.take() {
// An external runner failed to prove exit. Releasing this lease
// would permit replacing credentials still reachable by a child.
std::mem::forget(lease);
}
}
}
}
25 changes: 22 additions & 3 deletions account-manager/docs/MACO_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,30 @@ finalize once. `LeasedChild::cancel` kills only its owned child and waits for
exit. Wait failure or abandoned custody with unknown exit retains the use lease
and reports `ExitUnknown`, without claiming cancellation or finalization. The
existing account mutation barrier continues to refuse replacement during use.
Coordinated credential rotation/revocation, durable recovery, work-proposal
operation dispatch, and the root external executor bridge remain separate
integration work. The closed service launch path uses asynchronous cancellation
The root Grok external executor transfers that lease to the same closed operation
without a gap. It opens the selected vendor-home capability during guarded
construction, checks expiry/revocation before runner dispatch, and records only
run/account identity, inherited-input names, and lifecycle state in public run
metadata. A confirmed empty process tree is required to finalize normal/nonzero
exit or cancellation; unconfirmed preflight or target cleanup retains custody.
Setup deadline exhaustion and tee conflicts are confirmed pre-release refusals;
they do not create unknown-child custody or erase an earlier uncertain child.
The outer selected-use lease also remains held until launch-authority teardown,
covering retained credential descriptors and later reporting after child exit.
Restored metadata cannot recreate live custody or authorize finalization.

Active-child rotation or revocation must use the existing cancellation path and
wait for confirmed exit before replacing/deleting the account incarnation. An
admission revoke cannot cancel an active child. The account mutation barrier
refuses replacement while that child or an unknown-exit lease remains active.
The closed service launch path uses asynchronous cancellation
requests through the same child custody; a successful signal alone cannot
release the lease or finalize the guard.
This does not introduce a credential rotation service. Coordinated credential
rotation/revocation, durable recovery, work-proposal operation dispatch, and
equivalent root-executor lifecycle integration for the other provider-specific
account paths remain separate work. Synthetic fixtures do not qualify a real
provider account or replace provider-specific acceptance.

The initial operation kind is `work-proposal`: one fresh proposal turn, with no
provider-owned workspace mutation, nested orchestration, or independent review.
Expand Down
Loading
Loading