use crate::error::{EngineError, Result};
use crate::events::EventKind;
use crate::orchestrator::MissionEngine;
use crate::types::{MissionStatus, WorkerIsolation, WorkspaceConfig, WorkspacePin};
use crate::workspace_contract::WorkspaceContract;
use crate::workspace_gate::{
self, CommandOutcome, GatePhase, BOOTSTRAP_SUMMARY_PREFIX, READINESS_SUMMARY_PREFIX,
};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceProviderKind {
LocalWorktree,
Container,
Remote,
}
impl WorkspaceProviderKind {
pub fn as_str(self) -> &'static str {
match self {
WorkspaceProviderKind::LocalWorktree => "local-worktree",
WorkspaceProviderKind::Container => "container",
WorkspaceProviderKind::Remote => "remote",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TeardownMode {
Keep,
Hibernate,
Destroy,
}
impl TeardownMode {
pub fn as_str(self) -> &'static str {
match self {
TeardownMode::Keep => "keep",
TeardownMode::Hibernate => "hibernate",
TeardownMode::Destroy => "destroy",
}
}
pub fn success_state(self) -> &'static str {
match self {
TeardownMode::Keep => "kept",
TeardownMode::Hibernate => "stopped",
TeardownMode::Destroy => "destroyed",
}
}
}
pub fn teardown_mode(config: &WorkspaceConfig) -> Result<TeardownMode> {
match config.teardown_mode.as_deref() {
None => Ok(TeardownMode::Keep),
Some(name) if name == TeardownMode::Keep.as_str() => Ok(TeardownMode::Keep),
Some(name) if name == TeardownMode::Hibernate.as_str() => Ok(TeardownMode::Hibernate),
Some(name) if name == TeardownMode::Destroy.as_str() => Ok(TeardownMode::Destroy),
Some(other) => Err(EngineError::Config(format!(
"workspace.teardownMode {other:?} is not a known teardown mode \
(this build provides {:?}, {:?}, and {:?} only; owner: operator — fix the \
workspace.teardownMode config key); refusing rather than silently \
defaulting to {:?}",
TeardownMode::Keep.as_str(),
TeardownMode::Hibernate.as_str(),
TeardownMode::Destroy.as_str(),
TeardownMode::Keep.as_str()
))),
}
}
pub(crate) fn effective_teardown_mode(
kind: WorkspaceProviderKind,
run_terminal: bool,
configured: TeardownMode,
) -> TeardownMode {
match kind {
WorkspaceProviderKind::LocalWorktree => TeardownMode::Keep,
WorkspaceProviderKind::Container | WorkspaceProviderKind::Remote if run_terminal => {
configured
}
WorkspaceProviderKind::Container | WorkspaceProviderKind::Remote => TeardownMode::Keep,
}
}
#[derive(Debug, Clone)]
pub struct GateEnvPolicy {
pub home: PathBuf,
pub passthrough: Vec<String>,
}
impl GateEnvPolicy {
pub fn for_mission(runtime_dir: &std::path::Path, passthrough: &[String]) -> Self {
Self {
home: crate::workspace_gate::mission_gate_home(runtime_dir),
passthrough: passthrough.to_vec(),
}
}
}
#[derive(Debug, Clone)]
pub struct ProvisionSpec {
pub mission_id: String,
pub repo_root: PathBuf,
pub runtime_dir: PathBuf,
pub base_sha: Option<String>,
pub contract: Option<WorkspaceContract>,
pub gate_env: GateEnvPolicy,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PreviewPlaceholder {
pub name: String,
pub url_template: String,
}
#[derive(Debug, Clone)]
pub struct WorkspaceHandle {
pub cwd: PathBuf,
pub env: HashMap<String, String>,
pub previews: Vec<PreviewPlaceholder>,
pub contract: Option<WorkspaceContract>,
pub detail: Option<String>,
pub container: Option<crate::workspace_container::ContainerWorkspace>,
pub remote: Option<crate::workspace_remote::RemoteWorkspace>,
pub gate_env: GateEnvPolicy,
}
#[derive(Debug)]
pub enum ReadinessOutcome {
Ready,
Failed {
kind: &'static str,
failed: CommandOutcome,
},
DataSkew {
failed: CommandOutcome,
},
ProviderFailed {
detail: String,
},
}
pub type ProgressSink<'a> = dyn FnMut(&str, Option<String>) -> Result<()> + Send + 'a;
#[async_trait::async_trait]
pub trait WorkspaceProvider: Send + Sync {
fn kind(&self) -> WorkspaceProviderKind;
async fn provision(&self, spec: &ProvisionSpec) -> Result<WorkspaceHandle>;
async fn readiness(
&self,
handle: &WorkspaceHandle,
progress: &mut ProgressSink<'_>,
) -> Result<ReadinessOutcome>;
async fn teardown(&self, handle: WorkspaceHandle, mode: TeardownMode) -> Result<()>;
async fn run_data_hook(
&self,
handle: &WorkspaceHandle,
hook: crate::workspace_data::DataHookKind,
command: &str,
progress: &mut ProgressSink<'_>,
) -> Result<Option<CommandOutcome>> {
let env = crate::workspace_gate::gate_command_env(
&handle.gate_env,
&handle.env,
handle.contract.as_ref(),
);
let (code, output_tail) =
crate::command_exec::run_shell_command_with_code_cleared(&handle.cwd, command, &env)
.await;
crate::workspace_data::hook_outcome(hook, command, code, output_tail, progress)
}
}
pub fn resolve(config: &WorkspaceConfig) -> Result<Box<dyn WorkspaceProvider>> {
match config.provider.as_deref() {
None => Ok(Box::new(LocalWorktreeProvider)),
Some(name) if name == WorkspaceProviderKind::LocalWorktree.as_str() => {
Ok(Box::new(LocalWorktreeProvider))
}
Some(name) if name == WorkspaceProviderKind::Container.as_str() => Ok(Box::new(
crate::workspace_container::LocalContainerProvider::new(),
)),
Some(name) if name == WorkspaceProviderKind::Remote.as_str() => Ok(Box::new(
crate::workspace_remote::RemoteWorkspaceProvider::from_config(config.remote.as_ref())?,
)),
Some(other) => Err(EngineError::Config(format!(
"workspace.provider {other:?} is not a known workspace provider \
(this build provides {:?}, {:?}, and {:?} only; owner: operator — fix the \
workspace.provider config key); refusing rather than silently \
falling back",
WorkspaceProviderKind::LocalWorktree.as_str(),
WorkspaceProviderKind::Container.as_str(),
WorkspaceProviderKind::Remote.as_str()
))),
}
}
pub fn pin(
config: &WorkspaceConfig,
isolation: WorkerIsolation,
contract: Option<&WorkspaceContract>,
) -> Result<WorkspacePin> {
let resolved = resolve(config)?;
if resolved.kind() == WorkspaceProviderKind::Remote {
let remote = crate::workspace_remote::RemoteConfig::require(config.remote.as_ref())?;
return Ok(WorkspacePin {
provider: resolved.kind().as_str().to_string(),
template: remote.template,
version: crate::workspace_remote::ADAPTER_VERSION.to_string(),
});
}
let template = match isolation {
WorkerIsolation::Worktree => "worktree",
WorkerIsolation::Checkout => "checkout",
};
let version = match contract {
Some(contract) => contract.schema_version.to_string(),
None => "none".to_string(),
};
Ok(WorkspacePin {
provider: resolved.kind().as_str().to_string(),
template: template.to_string(),
version,
})
}
pub struct LocalWorktreeProvider;
#[async_trait::async_trait]
impl WorkspaceProvider for LocalWorktreeProvider {
fn kind(&self) -> WorkspaceProviderKind {
WorkspaceProviderKind::LocalWorktree
}
async fn provision(&self, spec: &ProvisionSpec) -> Result<WorkspaceHandle> {
if !spec.repo_root.is_dir() {
return Err(EngineError::InvalidState(format!(
"local-worktree provision: execution cwd {} does not exist",
spec.repo_root.display()
)));
}
let env = crate::runner::contract_env(spec.base_sha.as_deref());
let previews = spec
.contract
.iter()
.flat_map(|contract| &contract.previews)
.map(|preview| PreviewPlaceholder {
name: preview.name.clone(),
url_template: preview.url_template.clone(),
})
.collect();
Ok(WorkspaceHandle {
cwd: spec.repo_root.clone(),
env,
previews,
contract: spec.contract.clone(),
detail: None,
container: None,
remote: None,
gate_env: spec.gate_env.clone(),
})
}
async fn readiness(
&self,
handle: &WorkspaceHandle,
progress: &mut ProgressSink<'_>,
) -> Result<ReadinessOutcome> {
let Some(contract) = &handle.contract else {
return Ok(ReadinessOutcome::Ready);
};
if let Some(data) = &contract.data {
for (hook, command) in [
(crate::workspace_data::DataHookKind::Clone, &data.clone),
(crate::workspace_data::DataHookKind::Migrate, &data.migrate),
] {
if let Some(command) = command {
if let Some(failed) =
self.run_data_hook(handle, hook, command, progress).await?
{
return Ok(ReadinessOutcome::Failed {
kind: hook.gate_kind(),
failed,
});
}
}
}
}
if let Some(failed) = run_gate_phase(
&GatePhase {
kind: "bootstrap command",
unit: "command",
plural: "commands",
prefix: BOOTSTRAP_SUMMARY_PREFIX,
commands: &contract.bootstrap,
stop_at_first_failure: true,
},
handle,
progress,
)
.await?
{
return Ok(ReadinessOutcome::Failed {
kind: "bootstrap command",
failed,
});
}
if let Some(failed) = run_gate_phase(
&GatePhase {
kind: "readiness check",
unit: "check",
plural: "checks",
prefix: READINESS_SUMMARY_PREFIX,
commands: &contract.readiness,
stop_at_first_failure: false,
},
handle,
progress,
)
.await?
{
return Ok(ReadinessOutcome::Failed {
kind: "readiness check",
failed,
});
}
if let Some(command) = contract
.data
.as_ref()
.and_then(|data| data.skew_check.as_ref())
{
if let Some(failed) = self
.run_data_hook(
handle,
crate::workspace_data::DataHookKind::SkewCheck,
command,
progress,
)
.await?
{
return Ok(ReadinessOutcome::DataSkew { failed });
}
}
Ok(ReadinessOutcome::Ready)
}
async fn teardown(&self, _handle: WorkspaceHandle, _mode: TeardownMode) -> Result<()> {
Ok(())
}
}
async fn run_gate_phase(
phase: &GatePhase<'_>,
handle: &WorkspaceHandle,
progress: &mut ProgressSink<'_>,
) -> Result<Option<CommandOutcome>> {
progress(
&format!(
"{} running {} {}",
phase.prefix,
phase.commands.len(),
phase.plural
),
None,
)?;
let outcomes = workspace_gate::run_gate_commands(
&handle.cwd,
phase,
&handle.gate_env,
&handle.env,
handle.contract.as_ref(),
)
.await;
report_gate_outcomes(phase, outcomes, progress)
}
pub(crate) fn report_gate_outcomes(
phase: &GatePhase<'_>,
outcomes: Vec<CommandOutcome>,
progress: &mut ProgressSink<'_>,
) -> Result<Option<CommandOutcome>> {
let n = phase.commands.len();
match outcomes.iter().find(|o| !o.ok()) {
None => {
progress(
&format!("{} {n}/{n} {} ok", phase.prefix, phase.plural),
Some(workspace_gate::outcomes_detail(phase.kind, &outcomes)),
)?;
Ok(None)
}
Some(failed) => {
progress(
&format!(
"{} FAILED at {} {}/{n} — blocking mission (owner: repo-setup)",
phase.prefix, phase.unit, failed.ordinal
),
Some(workspace_gate::outcomes_detail(phase.kind, &outcomes)),
)?;
Ok(Some(failed.clone()))
}
}
}
impl MissionEngine {
pub(crate) async fn provision_workspace(
&mut self,
provider: &dyn WorkspaceProvider,
) -> Result<Option<MissionStatus>> {
let base_branch = self.state.mission.base_branch.clone();
let contract =
crate::workspace_contract::load_workspace_contract_at_ref(&self.repo, &base_branch)?;
let runtime_dir = self.paths.mission_dir();
let spec = ProvisionSpec {
mission_id: self.state.mission.id.clone(),
repo_root: self.active_root().to_path_buf(),
gate_env: GateEnvPolicy::for_mission(
&runtime_dir,
&self.state.config.contract_env_passthrough,
),
runtime_dir,
base_sha: self.state.mission.base_sha.clone(),
contract,
};
let handle = provider.provision(&spec).await?;
self.emit(EventKind::WorkspaceProvisioned {
provider: provider.kind().as_str().to_string(),
cwd: handle.cwd.display().to_string(),
detail: handle.detail.clone(),
takeover: handle
.remote
.as_ref()
.and_then(|remote| remote.takeover.clone()),
previews: handle.remote.as_ref().map(|remote| remote.previews.clone()),
})?;
let has_contract = handle.contract.is_some();
let outcome = {
let mut progress = |summary: &str, detail: Option<String>| -> Result<()> {
self.emit_decision(summary, detail)
};
provider.readiness(&handle, &mut progress).await?
};
self.workspace_handle = Some(handle);
match outcome {
ReadinessOutcome::Ready if !has_contract => Ok(None),
ReadinessOutcome::Ready => {
self.emit(EventKind::WorkspaceReadinessReport {
outcome: "ready".to_string(),
detail: None,
})?;
self.lift_gate_block()?;
Ok(None)
}
ReadinessOutcome::Failed { kind, failed } => {
self.emit(EventKind::WorkspaceReadinessReport {
outcome: "failed".to_string(),
detail: Some(workspace_gate::gate_block_reason(kind, &failed)),
})?;
self.block_on_gate_failure(kind, &failed)
}
ReadinessOutcome::DataSkew { failed } => {
let data = self
.workspace_handle
.as_ref()
.and_then(|handle| handle.contract.as_ref())
.and_then(|contract| contract.data.clone());
let reason = crate::workspace_data::skew_block_reason(data.as_ref(), &failed);
self.emit(EventKind::WorkspaceReadinessReport {
outcome: "skew".to_string(),
detail: Some(reason.clone()),
})?;
self.block_with_gate_reason(reason)
}
ReadinessOutcome::ProviderFailed { detail } => {
let reason = crate::workspace_remote::provider_block_reason(&detail);
self.emit(EventKind::WorkspaceReadinessReport {
outcome: "failed".to_string(),
detail: Some(reason.clone()),
})?;
self.block_with_gate_reason(reason)
}
}
}
pub(crate) async fn teardown_workspace(
&mut self,
provider: &dyn WorkspaceProvider,
mode: TeardownMode,
) {
let Some(handle) = self.workspace_handle.take() else {
return; };
let gate_home = handle.gate_env.home.clone();
let state = match provider.teardown(handle, mode).await {
Ok(()) => mode.success_state(),
Err(e) => {
if let Err(append) = self.emit_decision(
&format!(
"workspace teardown ({}) failed — the mission outcome stands; the \
workspace may still be live (owner: operator — release it manually)",
mode.as_str()
),
Some(e.to_string()),
) {
tracing::warn!(error = %append, "workspace teardown failure decision append failed");
}
"failed"
}
};
workspace_gate::remove_gate_home(&gate_home);
if let Err(e) = self.emit(EventKind::WorkspaceTeardown {
mode: mode.as_str().to_string(),
state: Some(state.to_string()),
}) {
tracing::warn!(error = %e, "workspace.teardown append failed");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_shell::{file_exists, if_file_exists, write_line};
use crate::workspace_contract::parse_workspace_contract;
fn ws_config(provider: Option<&str>) -> WorkspaceConfig {
WorkspaceConfig {
provider: provider.map(str::to_string),
remote: None,
teardown_mode: None,
}
}
fn remote_block() -> crate::types::RemoteWorkspaceConfig {
crate::types::RemoteWorkspaceConfig {
base_url: Some("https://coder.internal.example.com".to_string()),
template: Some("tmpl-baked-ami".to_string()),
token_env: Some("CODER_SESSION_TOKEN".to_string()),
idle_after_hours: None,
}
}
fn spec(
root: PathBuf,
base_sha: Option<&str>,
contract: Option<WorkspaceContract>,
) -> ProvisionSpec {
spec_with_passthrough(root, base_sha, contract, &[])
}
fn spec_with_passthrough(
root: PathBuf,
base_sha: Option<&str>,
contract: Option<WorkspaceContract>,
passthrough: &[String],
) -> ProvisionSpec {
let runtime_dir = root.join(".kranz").join("missions").join("m-test");
ProvisionSpec {
mission_id: "m-test".to_string(),
gate_env: GateEnvPolicy::for_mission(&runtime_dir, passthrough),
runtime_dir,
repo_root: root,
base_sha: base_sha.map(|s| s.to_string()),
contract,
}
}
fn contract(json: &[u8]) -> WorkspaceContract {
parse_workspace_contract(json).expect("valid contract")
}
#[derive(Default)]
struct Progress(Vec<(String, Option<String>)>);
impl Progress {
fn sink(&mut self) -> impl FnMut(&str, Option<String>) -> Result<()> + Send + use<'_> {
|summary, detail| {
self.0.push((summary.to_string(), detail));
Ok(())
}
}
fn summaries(&self) -> Vec<&str> {
self.0.iter().map(|(s, _)| s.as_str()).collect()
}
}
#[test]
fn resolve_defaults_to_local_worktree_and_fails_closed_on_unknown_names() {
assert_eq!(
resolve(&ws_config(None))
.expect("absent = local-worktree")
.kind(),
WorkspaceProviderKind::LocalWorktree
);
assert_eq!(
resolve(&ws_config(Some("local-worktree")))
.expect("explicit local-worktree")
.kind(),
WorkspaceProviderKind::LocalWorktree
);
for unknown in ["coder", "local-container", "Local-Worktree"] {
let err = resolve(&ws_config(Some(unknown)))
.err()
.expect("unknown providers fail closed");
let msg = err.to_string();
assert!(msg.contains("workspace.provider"), "{msg}");
assert!(msg.contains(&format!("{unknown:?}")), "{msg}");
assert!(msg.contains("\"local-worktree\""), "{msg}");
assert!(msg.contains("\"container\""), "{msg}");
assert!(msg.contains("\"remote\""), "{msg}");
assert!(msg.contains("only"), "{msg}");
assert!(msg.contains("owner: operator"), "{msg}");
}
}
#[test]
fn resolve_container_picks_the_local_container_provider() {
assert_eq!(
resolve(&ws_config(Some("container")))
.expect("container is a known provider")
.kind(),
WorkspaceProviderKind::Container
);
assert_eq!(WorkspaceProviderKind::Container.as_str(), "container");
}
#[test]
fn remote_workspace_resolve_picks_the_adapter_only_with_complete_config() {
let complete = WorkspaceConfig {
provider: Some("remote".to_string()),
remote: Some(remote_block()),
teardown_mode: None,
};
assert_eq!(
resolve(&complete)
.expect("complete remote config resolves")
.kind(),
WorkspaceProviderKind::Remote
);
assert_eq!(WorkspaceProviderKind::Remote.as_str(), "remote");
for (remote, missing_key) in [
(None, "workspace.remote.baseUrl"),
(
Some(crate::types::RemoteWorkspaceConfig {
template: Some("tmpl".to_string()),
token_env: Some("CODER_SESSION_TOKEN".to_string()),
..Default::default()
}),
"workspace.remote.baseUrl",
),
(
Some(crate::types::RemoteWorkspaceConfig {
base_url: Some("https://coder.internal.example.com".to_string()),
token_env: Some("CODER_SESSION_TOKEN".to_string()),
..Default::default()
}),
"workspace.remote.template",
),
(
Some(crate::types::RemoteWorkspaceConfig {
base_url: Some("https://coder.internal.example.com".to_string()),
template: Some("tmpl".to_string()),
..Default::default()
}),
"workspace.remote.tokenEnv",
),
] {
let config = WorkspaceConfig {
provider: Some("remote".to_string()),
remote,
teardown_mode: None,
};
let err = resolve(&config)
.err()
.expect("incomplete remote config fails closed");
let msg = err.to_string();
assert!(msg.contains(missing_key), "names the missing key: {msg}");
assert!(msg.contains("owner: operator"), "{msg}");
assert!(
msg.contains("refusing rather than silently falling back"),
"{msg}"
);
}
}
#[test]
fn teardown_mode_config_parses_modes_and_refuses_unknown_names() {
assert_eq!(
teardown_mode(&ws_config(None)).expect("absent = keep"),
TeardownMode::Keep
);
for (name, expected) in [
("keep", TeardownMode::Keep),
("hibernate", TeardownMode::Hibernate),
("destroy", TeardownMode::Destroy),
] {
let config = WorkspaceConfig {
teardown_mode: Some(name.to_string()),
..ws_config(None)
};
assert_eq!(teardown_mode(&config).expect("known mode parses"), expected);
}
let config: WorkspaceConfig =
serde_json::from_str(r#"{"teardownMode": "hibernate"}"#).expect("config JSON parses");
assert_eq!(
teardown_mode(&config).expect("wire mode parses"),
TeardownMode::Hibernate
);
for unknown in ["stop", "Hibernate", "down", "pause"] {
let config = WorkspaceConfig {
teardown_mode: Some(unknown.to_string()),
..ws_config(None)
};
let err = teardown_mode(&config).expect_err("unknown teardown modes fail closed");
let msg = err.to_string();
assert!(msg.contains("workspace.teardownMode"), "{msg}");
assert!(msg.contains(&format!("{unknown:?}")), "{msg}");
assert!(msg.contains("\"keep\""), "{msg}");
assert!(msg.contains("\"hibernate\""), "{msg}");
assert!(msg.contains("\"destroy\""), "{msg}");
assert!(msg.contains("only"), "{msg}");
assert!(msg.contains("owner: operator"), "{msg}");
}
}
#[test]
fn effective_teardown_mode_is_terminal_gated_and_local_is_always_keep() {
for kind in [
WorkspaceProviderKind::Container,
WorkspaceProviderKind::Remote,
] {
assert_eq!(
effective_teardown_mode(kind, true, TeardownMode::Hibernate),
TeardownMode::Hibernate,
"terminal drives the configured mode for {kind:?}"
);
assert_eq!(
effective_teardown_mode(kind, true, TeardownMode::Destroy),
TeardownMode::Destroy
);
assert_eq!(
effective_teardown_mode(kind, true, TeardownMode::Keep),
TeardownMode::Keep
);
for configured in [
TeardownMode::Keep,
TeardownMode::Hibernate,
TeardownMode::Destroy,
] {
assert_eq!(
effective_teardown_mode(kind, false, configured),
TeardownMode::Keep,
"a non-terminal end keeps the workspace for resume ({kind:?}, {configured:?})"
);
}
}
for run_terminal in [true, false] {
for configured in [
TeardownMode::Keep,
TeardownMode::Hibernate,
TeardownMode::Destroy,
] {
assert_eq!(
effective_teardown_mode(
WorkspaceProviderKind::LocalWorktree,
run_terminal,
configured
),
TeardownMode::Keep,
"local-worktree is always Keep (terminal={run_terminal}, {configured:?})"
);
}
}
}
#[test]
fn pin_records_isolation_mode_and_contract_version() {
let contract = contract(br#"{"schemaVersion": 1, "readiness": ["exit 0"]}"#);
let pinned =
pin(&ws_config(None), WorkerIsolation::Worktree, Some(&contract)).expect("pin");
assert_eq!(
pinned,
WorkspacePin {
provider: "local-worktree".to_string(),
template: "worktree".to_string(),
version: "1".to_string(),
}
);
let pinned = pin(
&ws_config(Some("local-worktree")),
WorkerIsolation::Checkout,
Some(&contract),
)
.expect("explicit local-worktree pins too");
assert_eq!(pinned.template, "checkout");
assert_eq!(pinned.version, "1");
let pinned =
pin(&ws_config(None), WorkerIsolation::Worktree, None).expect("pin without contract");
assert_eq!(pinned.provider, "local-worktree");
assert_eq!(pinned.version, "none");
let pinned = pin(
&ws_config(Some("container")),
WorkerIsolation::Worktree,
Some(&contract),
)
.expect("container pins at approval (pure resolution)");
assert_eq!(pinned.provider, "container");
assert_eq!(pinned.version, "1");
let err = pin(&ws_config(Some("coder")), WorkerIsolation::Worktree, None)
.expect_err("a misspelled provider never silently defaults");
let msg = err.to_string();
assert!(msg.contains("workspace.provider"), "{msg}");
assert!(msg.contains("\"coder\""), "{msg}");
assert!(msg.contains("owner: operator"), "{msg}");
}
#[test]
fn remote_workspace_pin_populates_provider_template_and_adapter_version() {
let contract = contract(br#"{"schemaVersion": 1, "readiness": ["exit 0"]}"#);
let config = WorkspaceConfig {
provider: Some("remote".to_string()),
remote: Some(remote_block()),
teardown_mode: None,
};
let pinned = pin(&config, WorkerIsolation::Worktree, Some(&contract))
.expect("remote pins at approval");
assert_eq!(
pinned,
WorkspacePin {
provider: "remote".to_string(),
template: "tmpl-baked-ami".to_string(),
version: "coder-v1".to_string(),
},
"configured template + adapter version, not the contract schema"
);
let incomplete = WorkspaceConfig {
provider: Some("remote".to_string()),
remote: Some(crate::types::RemoteWorkspaceConfig {
template: Some("tmpl".to_string()),
token_env: Some("CODER_SESSION_TOKEN".to_string()),
..Default::default()
}),
teardown_mode: None,
};
let err = pin(&incomplete, WorkerIsolation::Worktree, Some(&contract))
.expect_err("incomplete remote config refuses approval");
assert!(
err.to_string().contains("workspace.remote.baseUrl"),
"{err}"
);
}
#[tokio::test]
async fn local_worktree_provision_carries_cwd_env_previews_and_contract() {
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"readiness": ["echo ok"],
"previews": [{ "name": "app", "urlTemplate": "http://localhost:{port}/" }]
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(
dir.path().to_path_buf(),
Some("deadbeefcafe"),
Some(contract),
))
.await
.expect("provision");
assert_eq!(handle.cwd, dir.path());
assert_eq!(
handle.env.get("KRANZ_BASE_SHA").map(String::as_str),
Some("deadbeefcafe"),
"the handle env carries the pinned base sha (contract_env idiom)"
);
assert_eq!(handle.env.len(), 1, "nothing but KRANZ_BASE_SHA");
assert_eq!(
handle.previews,
vec![PreviewPlaceholder {
name: "app".to_string(),
url_template: "http://localhost:{port}/".to_string(),
}]
);
assert!(handle.contract.is_some());
assert!(
handle.detail.is_none() && handle.container.is_none(),
"local-worktree records no provider detail or container state"
);
}
#[tokio::test]
async fn local_worktree_provision_without_base_sha_or_contract_is_minimal() {
let dir = tempfile::tempdir().expect("tempdir");
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, None))
.await
.expect("provision");
assert!(handle.env.is_empty(), "no base sha pinned ⇒ no env");
assert!(handle.previews.is_empty());
assert!(handle.contract.is_none());
}
#[tokio::test]
async fn local_worktree_provision_refuses_a_missing_cwd() {
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("no-such-worktree");
let err = LocalWorktreeProvider
.provision(&spec(missing.clone(), None, None))
.await
.expect_err("a missing execution cwd is a provision error");
assert!(err.to_string().contains("does not exist"), "{err}");
}
#[tokio::test]
async fn readiness_without_contract_is_ready_and_silent() {
let dir = tempfile::tempdir().expect("tempdir");
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, None))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
assert!(matches!(outcome, ReadinessOutcome::Ready));
assert!(
progress.0.is_empty(),
"no contract ⇒ no gate decision lines (byte-identical pre-gate behavior)"
);
}
fn base_sha_assertion_command(expected: &str) -> String {
#[cfg(unix)]
{
format!("test \"$KRANZ_BASE_SHA\" = '{expected}'")
}
#[cfg(windows)]
{
format!("if \"%KRANZ_BASE_SHA%\"==\"{expected}\" (exit /b 0) else (exit /b 1)")
}
}
#[tokio::test]
async fn readiness_runs_bootstrap_then_readiness_with_gate_progress() {
let dir = tempfile::tempdir().expect("tempdir");
let contract_json = format!(
r#"{{
"schemaVersion": 1,
"bootstrap": ["echo boot > .boot-marker"],
"readiness": [{}]
}}"#,
serde_json::to_string(&base_sha_assertion_command("deadbeefcafe")).unwrap()
);
let contract = contract(contract_json.as_bytes());
let handle = LocalWorktreeProvider
.provision(&spec(
dir.path().to_path_buf(),
Some("deadbeefcafe"),
Some(contract),
))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
assert!(
matches!(outcome, ReadinessOutcome::Ready),
"readiness passed ⇒ the KRANZ_BASE_SHA assertion saw the handle env: {outcome:?}"
);
assert!(
dir.path().join(".boot-marker").exists(),
"bootstrap ran in the provisioned cwd"
);
assert_eq!(
progress.summaries(),
vec![
"workspace bootstrap: running 1 commands",
"workspace bootstrap: 1/1 commands ok",
"workspace readiness: running 1 checks",
"workspace readiness: 1/1 checks ok",
]
);
}
#[cfg(unix)]
#[tokio::test]
async fn readiness_commands_see_only_the_contracts_declared_secrets() {
let _guard = crate::agent_env::EnvTestGuard::engage(&[
("KRANZ_SECRET_TEST", "undeclared-and-must-not-cross"),
("GH_TOKEN", "ghp_poison"),
("DATABASE_URL", "postgres://declared"),
]);
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"secrets": ["DATABASE_URL"],
"readiness": [
"test -z \"$KRANZ_SECRET_TEST\"",
"test -z \"$GH_TOKEN\"",
"test \"$DATABASE_URL\" = 'postgres://declared'"
]
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec_with_passthrough(
dir.path().to_path_buf(),
None,
Some(contract),
&["DATABASE_URL".to_string()],
))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
assert!(
matches!(outcome, ReadinessOutcome::Ready),
"undeclared ambient secrets must not cross, the two-party one must: {outcome:?}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_repo_declared_secret_the_operator_never_granted_does_not_cross() {
let _guard = crate::agent_env::EnvTestGuard::engage(&[("GH_TOKEN", "ghp_poison")]);
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"secrets": ["GH_TOKEN"],
"readiness": ["test -z \"$GH_TOKEN\""]
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
assert!(
matches!(outcome, ReadinessOutcome::Ready),
"the repo named GH_TOKEN and the operator did not, so it must not cross: {outcome:?}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn bootstrap_output_in_home_survives_into_readiness_and_the_data_hooks() {
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"bootstrap": ["mkdir -p \"$HOME/bin\" && echo installed > \"$HOME/bin/turbo\""],
"readiness": ["test -f \"$HOME/bin/turbo\""],
"data": {
"migrate": "true",
"skewCheck": "test -f \"$HOME/bin/turbo\""
}
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
assert!(
matches!(outcome, ReadinessOutcome::Ready),
"readiness and the skewCheck hook must see what bootstrap installed in HOME: \
{outcome:?}"
);
assert_eq!(
handle.gate_env.home,
dir.path()
.join(".kranz")
.join("missions")
.join("m-test")
.join("runs")
.join("workspace-gate"),
"the shared home lives under the mission's own writable runs/ dir"
);
assert!(
handle.gate_env.home.join("bin").join("turbo").is_file(),
"the home outlives the phases; teardown is what removes it"
);
}
#[cfg(unix)]
#[tokio::test]
async fn data_hooks_see_only_the_contracts_declared_secrets() {
let _guard =
crate::agent_env::EnvTestGuard::engage(&[("KRANZ_SECRET_TEST", "must-not-cross")]);
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"data": {"clone": "test -z \"$KRANZ_SECRET_TEST\""}
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let failed = LocalWorktreeProvider
.run_data_hook(
&handle,
crate::workspace_data::DataHookKind::Clone,
"test -z \"$KRANZ_SECRET_TEST\"",
&mut progress.sink(),
)
.await
.expect("hook ran");
assert!(
failed.is_none(),
"an ambient secret reached the data hook: {failed:?}"
);
}
#[tokio::test]
async fn readiness_bootstrap_failure_stops_before_readiness() {
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"bootstrap": ["exit 42"],
"readiness": ["echo never-runs > .readiness-marker"]
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
let ReadinessOutcome::Failed { kind, failed } = outcome else {
panic!("bootstrap failure must be Failed, got {outcome:?}");
};
assert_eq!(kind, "bootstrap command");
assert_eq!(failed.code, Some(42));
assert!(
!dir.path().join(".readiness-marker").exists(),
"bootstrap stop-at-first-failure: readiness never ran"
);
assert_eq!(
progress.summaries(),
vec![
"workspace bootstrap: running 1 commands",
"workspace bootstrap: FAILED at command 1/1 — blocking mission (owner: repo-setup)",
]
);
}
#[tokio::test]
async fn readiness_check_failure_reports_after_bootstrap_passed() {
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{
"schemaVersion": 1,
"bootstrap": ["echo boot > .boot-marker"],
"readiness": ["exit 3"]
}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
let ReadinessOutcome::Failed { kind, failed } = outcome else {
panic!("readiness failure must be Failed, got {outcome:?}");
};
assert_eq!(kind, "readiness check");
assert_eq!(failed.code, Some(3));
assert!(
dir.path().join(".boot-marker").exists(),
"bootstrap ran to completion first"
);
assert_eq!(
progress.summaries(),
vec![
"workspace bootstrap: running 1 commands",
"workspace bootstrap: 1/1 commands ok",
"workspace readiness: running 1 checks",
"workspace readiness: FAILED at check 1/1 — blocking mission (owner: repo-setup)",
]
);
}
#[tokio::test]
async fn readiness_runs_data_hooks_in_lifecycle_order() {
let dir = tempfile::tempdir().expect("tempdir");
let clone_cmd = write_line("cloned", ".clone-marker");
let migrate_cmd = if_file_exists(".clone-marker", &write_line("mig", ".migrate-marker"));
let skew_cmd = if_file_exists(".boot-marker", &write_line("checked", ".skew-marker"));
let bootstrap_cmd = if_file_exists(".migrate-marker", &write_line("boot", ".boot-marker"));
let readiness_cmd = file_exists(".boot-marker");
let contract = contract(
format!(
r#"{{
"schemaVersion": 1,
"data": {{
"clone": "{clone_cmd}",
"migrate": "{migrate_cmd}",
"skewCheck": "{skew_cmd}"
}},
"bootstrap": ["{bootstrap_cmd}"],
"readiness": ["{readiness_cmd}"]
}}"#
)
.as_bytes(),
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
assert!(
matches!(outcome, ReadinessOutcome::Ready),
"every marker assertion passed ⇒ the hooks ran in order: {outcome:?}"
);
for marker in [".clone-marker", ".migrate-marker", ".skew-marker"] {
assert!(dir.path().join(marker).exists(), "{marker} written");
}
assert_eq!(
progress.summaries(),
vec![
format!("workspace data: clone `{clone_cmd}` → ok (exit code 0)"),
format!("workspace data: migrate `{migrate_cmd}` → ok (exit code 0)"),
"workspace bootstrap: running 1 commands".to_string(),
"workspace bootstrap: 1/1 commands ok".to_string(),
"workspace readiness: running 1 checks".to_string(),
"workspace readiness: 1/1 checks ok".to_string(),
format!("workspace data: skewCheck `{skew_cmd}` → ok (exit code 0)"),
]
);
}
#[tokio::test]
async fn readiness_data_clone_failure_stops_before_migrate_and_bootstrap() {
let dir = tempfile::tempdir().expect("tempdir");
let readiness_cmd = file_exists(".boot-marker");
let contract = contract(
format!(
r#"{{
"schemaVersion": 1,
"data": {{
"clone": "exit 42",
"migrate": "echo mig > .migrate-marker"
}},
"bootstrap": ["echo boot > .boot-marker"],
"readiness": ["{readiness_cmd}"]
}}"#
)
.as_bytes(),
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
let ReadinessOutcome::Failed { kind, failed } = outcome else {
panic!("a clone failure must be Failed, got {outcome:?}");
};
assert_eq!(kind, "data clone hook");
assert_eq!(failed.code, Some(42));
assert!(
!dir.path().join(".migrate-marker").exists(),
"the data phase stops at the first failure: migrate never ran"
);
assert!(
!dir.path().join(".boot-marker").exists(),
"bootstrap never ran after a data-hook failure"
);
assert_eq!(
progress.summaries(),
vec![
"workspace data: clone `exit 42` → FAILED (exit code 42) — blocking mission (owner: repo-setup)",
]
);
}
#[tokio::test]
async fn readiness_skew_failure_is_the_distinct_skew_outcome() {
let dir = tempfile::tempdir().expect("tempdir");
let readiness_cmd = file_exists(".boot-marker");
let contract = contract(
format!(
r#"{{
"schemaVersion": 1,
"data": {{
"migrate": "echo mig > .migrate-marker",
"skewCheck": "exit 1"
}},
"bootstrap": ["echo boot > .boot-marker"],
"readiness": ["{readiness_cmd}"]
}}"#
)
.as_bytes(),
);
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, Some(contract)))
.await
.expect("provision");
let mut progress = Progress::default();
let outcome = LocalWorktreeProvider
.readiness(&handle, &mut progress.sink())
.await
.expect("readiness");
let ReadinessOutcome::DataSkew { failed } = outcome else {
panic!("a skewCheck failure must be DataSkew, got {outcome:?}");
};
assert_eq!(failed.code, Some(1));
assert!(
dir.path().join(".boot-marker").exists(),
"bootstrap and readiness passed before the skew check ran"
);
assert_eq!(
progress.summaries(),
vec![
"workspace data: migrate `echo mig > .migrate-marker` → ok (exit code 0)",
"workspace bootstrap: running 1 commands",
"workspace bootstrap: 1/1 commands ok",
"workspace readiness: running 1 checks",
"workspace readiness: 1/1 checks ok",
"workspace data: skewCheck `exit 1` → FAILED (exit code 1) — blocking mission (owner: repo-setup)",
]
);
}
#[tokio::test]
async fn run_data_hook_executes_in_the_workspace_with_handle_env() {
let dir = tempfile::tempdir().expect("tempdir");
let contract = contract(
br#"{"schemaVersion": 1, "data": {"reset": "seed", "resetBetweenRounds": true}}"#,
);
let handle = LocalWorktreeProvider
.provision(&spec(
dir.path().to_path_buf(),
Some("deadbeefcafe"),
Some(contract),
))
.await
.expect("provision");
let mut progress = Progress::default();
let failed = LocalWorktreeProvider
.run_data_hook(
&handle,
crate::workspace_data::DataHookKind::Reset,
&base_sha_assertion_command("deadbeefcafe"),
&mut progress.sink(),
)
.await
.expect("run_data_hook");
assert!(
failed.is_none(),
"the KRANZ_BASE_SHA assertion saw the handle env: {failed:?}"
);
assert_eq!(progress.summaries().len(), 1);
assert!(
progress.summaries()[0].starts_with("workspace data: reset `"),
"{:?}",
progress.summaries()
);
}
#[tokio::test]
async fn teardown_accepts_every_mode_as_a_recorded_no_op() {
let dir = tempfile::tempdir().expect("tempdir");
for mode in [
TeardownMode::Keep,
TeardownMode::Hibernate,
TeardownMode::Destroy,
] {
let handle = LocalWorktreeProvider
.provision(&spec(dir.path().to_path_buf(), None, None))
.await
.expect("provision");
LocalWorktreeProvider
.teardown(handle, mode)
.await
.expect("teardown is always Ok for local-worktree");
assert!(
dir.path().exists(),
"teardown never touches the filesystem ({mode:?})"
);
}
assert_eq!(TeardownMode::Keep.as_str(), "keep");
assert_eq!(TeardownMode::Hibernate.as_str(), "hibernate");
assert_eq!(TeardownMode::Destroy.as_str(), "destroy");
assert_eq!(
WorkspaceProviderKind::LocalWorktree.as_str(),
"local-worktree"
);
}
}