use serde::{Deserialize, Serialize};
use crate::error::{Result, StoreError};
pub const PROVISION_VERSION: u32 = 2;
pub const TYPED_READ_ONLY_WORKER_CAPABILITY: &str = "typed_read_only_tool_policy_v1";
pub const REQUIRED_CHILD_CONTEXT_WORKER_CAPABILITY: &str = "required_child_context_v1";
pub const NATIVE_TOOL_CEILING_WORKER_CAPABILITY: &str = "native_tool_ceiling_v1";
pub const EXEC_SESSION_NATIVE_CEILING_WORKER_CAPABILITY: &str =
"native_tool_ceiling_exec_sessions_v1";
pub const INITIAL_INPUT_RELEASE_WORKER_CAPABILITY: &str = "owned_initial_input_release_v1";
pub const CHILD_CREATION_IDENTITY_WORKER_CAPABILITY: &str = "durable_child_creation_identity_v1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerCapabilityReport {
pub provision_version: u32,
#[serde(default)]
pub capabilities: Vec<String>,
}
impl WorkerCapabilityReport {
pub fn current() -> Self {
Self {
provision_version: PROVISION_VERSION,
capabilities: vec![
INITIAL_INPUT_RELEASE_WORKER_CAPABILITY.to_string(),
TYPED_READ_ONLY_WORKER_CAPABILITY.to_string(),
REQUIRED_CHILD_CONTEXT_WORKER_CAPABILITY.to_string(),
CHILD_CREATION_IDENTITY_WORKER_CAPABILITY.to_string(),
NATIVE_TOOL_CEILING_WORKER_CAPABILITY.to_string(),
EXEC_SESSION_NATIVE_CEILING_WORKER_CAPABILITY.to_string(),
],
}
}
pub fn supports(&self, capability: &str) -> bool {
self.capabilities
.iter()
.any(|candidate| candidate == capability)
}
}
pub const MAX_SPEC_BYTES: u64 = 8 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProvisionSpec {
pub version: u32,
pub identity: ChildIdentity,
pub executor: ExecutorSpec,
pub fabric_dir: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bus: Option<BusEndpoint>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<WorkerOwner>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<ModelRefSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disabled_tools: Option<Vec<String>>,
#[serde(default)]
pub limits: Limits,
#[serde(default)]
pub secrets: SecretsEnvelope,
#[serde(default)]
pub reusable: bool,
#[serde(default)]
pub placement: Placement,
#[serde(default)]
pub capabilities: Capabilities,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Capabilities {
#[serde(default)]
pub initial_input_release_required: bool,
#[serde(default)]
pub child_creation_identity: bool,
#[serde(default)]
pub required_child_context: bool,
#[serde(default)]
pub native_tool_ceiling_required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub native_tool_ceiling: Option<crate::proto::NativeToolCeiling>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skills_dir: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub mcp_proxy: Option<McpProxyConfig>,
#[serde(default)]
pub enforce_permissions: bool,
#[serde(default)]
pub nested_spawn: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_spawn_depth: Option<u32>,
#[serde(default)]
pub bypass: bool,
#[serde(default)]
pub auto_approve_permissions: bool,
#[serde(default)]
pub permission_requested_mode: String,
#[serde(default)]
pub permission_effective_mode: String,
#[serde(default)]
pub no_human_approver: bool,
#[serde(default)]
pub read_only: bool,
#[serde(default)]
pub guardian_read_only: bool,
}
impl Capabilities {
pub fn read_only_enforced(&self) -> bool {
self.read_only || self.guardian_read_only
}
pub fn permission_resolution(
&self,
) -> std::result::Result<bamboo_domain::PermissionModeResolution, String> {
if self.auto_approve_permissions && self.bypass {
return Err(
"provisioned auto_approve_permissions and bypass are mutually exclusive"
.to_string(),
);
}
let has_requested_mode = !self.permission_requested_mode.is_empty();
let has_effective_mode = !self.permission_effective_mode.is_empty();
if has_requested_mode != has_effective_mode {
return Err(
"provisioned requested/effective permission modes must be provided together"
.to_string(),
);
}
let requested = if self.permission_requested_mode.is_empty() {
if self.auto_approve_permissions {
bamboo_domain::SessionPermissionMode::Auto
} else if self.bypass {
bamboo_domain::SessionPermissionMode::Bypass
} else {
bamboo_domain::SessionPermissionMode::Default
}
} else {
bamboo_domain::SessionPermissionMode::from_audit_str(&self.permission_requested_mode)
.ok_or_else(|| {
format!(
"invalid provisioned requested permission mode '{}'",
self.permission_requested_mode
)
})?
};
let effective = if self.permission_effective_mode.is_empty() {
bamboo_domain::resolve_permission_mode(
requested,
bamboo_domain::PermissionMode::Default,
)
.effective
} else {
bamboo_domain::PermissionMode::from_audit_str(&self.permission_effective_mode)
.ok_or_else(|| {
format!(
"invalid provisioned effective permission mode '{}'",
self.permission_effective_mode
)
})?
};
let resolution = bamboo_domain::PermissionModeResolution {
requested,
effective,
};
if !resolution.is_consistent() {
return Err("inconsistent provisioned permission posture".to_string());
}
if has_requested_mode
&& (self.bypass != resolution.bypass_permissions()
|| self.auto_approve_permissions != resolution.suppress_approval_prompts())
{
return Err("provisioned permission flags disagree with typed posture".to_string());
}
Ok(resolution)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BusEndpoint {
pub endpoint: String,
pub token: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkerOwner {
pub process_id: u32,
pub instance_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub process_start_id: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
pub worker_spawned_at: chrono::DateTime<chrono::Utc>,
}
impl WorkerOwner {
pub fn for_current_process(instance_id: String, session_id: Option<String>) -> Self {
Self {
process_id: std::process::id(),
instance_id,
process_start_id: current_process_start_id(),
session_id,
worker_spawned_at: chrono::Utc::now(),
}
}
}
#[cfg(not(windows))]
fn current_process_start_id() -> Option<u64> {
None
}
#[cfg(windows)]
fn current_process_start_id() -> Option<u64> {
use windows_sys::Win32::Foundation::FILETIME;
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
let mut creation: FILETIME = unsafe { std::mem::zeroed() };
let mut exit: FILETIME = unsafe { std::mem::zeroed() };
let mut kernel: FILETIME = unsafe { std::mem::zeroed() };
let mut user: FILETIME = unsafe { std::mem::zeroed() };
if unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut creation,
&mut exit,
&mut kernel,
&mut user,
)
} == 0
{
return None;
}
Some(((creation.dwHighDateTime as u64) << 32) | creation.dwLowDateTime as u64)
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct McpProxyConfig {
pub orchestrator: String,
pub endpoint: String,
pub token: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Placement {
#[default]
Local,
Remote { endpoint: String },
Schedulable { pool: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ChildIdentity {
pub child_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_key: Option<String>,
#[serde(default)]
pub role: String,
#[serde(default)]
pub depth: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ExecutorSpec {
Echo,
BambooRuntime,
CliAdapter { command: String, args: Vec<String> },
ClaudeCode {
#[serde(default, skip_serializing_if = "Option::is_none")]
binary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
permission_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
inherit_user_config: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
forward_env: Option<Vec<String>>,
},
Codex {
#[serde(default, skip_serializing_if = "Option::is_none")]
binary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sandbox: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
inherit_user_config: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
auth_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
wire_api: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_key_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
forward_env: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
approval_policy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
network_access: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
allow_danger_bypass: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
permission_profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
workspace_owned: Option<bool>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ModelRefSpec {
pub provider: String,
pub model: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Limits {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub run_timeout_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idle_timeout_secs: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_rounds: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SecretsEnvelope {
#[serde(default)]
pub provider_credentials: Vec<ScopedCredential>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_auth_token: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ScopedCredential {
pub provider: String,
pub api_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential_ref: Option<String>,
}
impl ProvisionSpec {
pub fn new(identity: ChildIdentity, executor: ExecutorSpec, fabric_dir: String) -> Self {
Self {
version: PROVISION_VERSION,
identity,
executor,
fabric_dir,
bus: None,
owner: None,
storage_dir: None,
workspace: None,
model: None,
disabled_tools: None,
limits: Limits::default(),
secrets: SecretsEnvelope::default(),
reusable: false,
placement: Placement::default(),
capabilities: Capabilities::default(),
}
}
pub fn validate(&self) -> Result<()> {
if self.capabilities.initial_input_release_required
&& (!self.capabilities.required_child_context
|| !self.capabilities.child_creation_identity
|| !self.capabilities.native_tool_ceiling_required
|| !self
.capabilities
.native_tool_ceiling
.as_ref()
.is_some_and(|ceiling| ceiling.tools.is_empty())
|| !self.capabilities.enforce_permissions
|| self.bus.is_none())
{
return Err(StoreError::Invalid(
"initial_input_release_unsupported".into(),
));
}
if (self.capabilities.required_child_context
&& !self.capabilities.native_tool_ceiling_required)
|| self.capabilities.native_tool_ceiling_required
!= self.capabilities.native_tool_ceiling.is_some()
{
return Err(StoreError::Invalid("native_tool_ceiling_required".into()));
}
if let Some(ceiling) = &self.capabilities.native_tool_ceiling {
ceiling
.validate()
.map_err(|code| StoreError::Invalid(code.into()))?;
if !matches!(self.executor, ExecutorSpec::BambooRuntime)
|| !matches!(self.placement, Placement::Local)
|| self.reusable
|| !self.capabilities.required_child_context
|| !self.capabilities.child_creation_identity
|| !self.capabilities.enforce_permissions
|| ceiling.spawn_depth != self.identity.depth
|| self.capabilities.mcp.is_some()
|| self.capabilities.mcp_proxy.is_some()
|| self.capabilities.skills_dir.is_some()
|| self.capabilities.nested_spawn
{
return Err(StoreError::Invalid(
"native_tool_ceiling_unsupported".into(),
));
}
}
if self.capabilities.child_creation_identity
&& (!matches!(self.executor, ExecutorSpec::BambooRuntime)
|| !matches!(self.placement, Placement::Local)
|| (self.storage_dir.is_none()
&& !std::path::Path::new(&self.fabric_dir).is_absolute()))
{
return Err(StoreError::Invalid("Child creation identity requires a local Bamboo runtime and stable host cache root".into()));
}
if self.capabilities.mcp.is_some() && self.capabilities.mcp_proxy.is_some() {
return Err(StoreError::Invalid(
"capabilities.mcp and capabilities.mcp_proxy are mutually exclusive \
(proxy covers all MCP) — set exactly one"
.to_string(),
));
}
self.capabilities
.permission_resolution()
.map_err(StoreError::Invalid)?;
Ok(())
}
pub fn to_json(&self) -> Result<String> {
self.validate()?;
serde_json::to_string(self)
.map_err(|e| StoreError::decode(std::path::Path::new("<provision>"), e))
}
pub fn from_json(s: &str) -> Result<Self> {
serde_json::from_str(s)
.map_err(|e| StoreError::decode(std::path::Path::new("<provision>"), e))
}
pub async fn read_from_stdin() -> Result<Self> {
use tokio::io::AsyncReadExt;
let mut buf = Vec::new();
tokio::io::stdin()
.take(MAX_SPEC_BYTES)
.read_to_end(&mut buf)
.await
.map_err(|e| StoreError::io("<stdin>", e))?;
let text = String::from_utf8_lossy(&buf);
Self::from_json(text.trim())
}
}
#[cfg(test)]
mod tests {
#[test]
fn current_worker_acknowledges_legacy_and_exec_session_native_ceilings() {
let current = WorkerCapabilityReport::current();
assert!(current.supports("native_tool_ceiling_v1"));
assert!(current.supports("native_tool_ceiling_exec_sessions_v1"));
assert_eq!(current.provision_version, PROVISION_VERSION);
let restored: WorkerCapabilityReport =
serde_json::from_slice(&serde_json::to_vec(¤t).unwrap()).unwrap();
assert_eq!(restored, current);
}
use super::*;
fn spec() -> ProvisionSpec {
let mut s = ProvisionSpec::new(
ChildIdentity {
child_id: "c1".into(),
parent_id: Some("p1".into()),
project_key: Some("proj".into()),
role: "researcher".into(),
depth: 0,
},
ExecutorSpec::Echo,
"/tmp/fabric".into(),
);
s.model = Some(ModelRefSpec {
provider: "anthropic".into(),
model: "claude-sonnet-4-6".into(),
});
s.secrets.provider_credentials.push(ScopedCredential {
provider: "anthropic".into(),
api_key: "sk-test".into(),
base_url: None,
provider_type: None,
credential_ref: None,
});
s
}
#[test]
fn initial_release_flag_rejects_inconsistent_provision() {
assert!(WorkerCapabilityReport::current().supports(INITIAL_INPUT_RELEASE_WORKER_CAPABILITY));
let mut spec = spec();
spec.capabilities.initial_input_release_required = true;
assert!(spec.validate().is_err());
spec.capabilities.initial_input_release_required = false;
assert!(spec.validate().is_ok(), "legacy provision unchanged");
}
#[test]
fn validate_rejects_both_mcp_and_mcp_proxy() {
let mut s = spec();
s.capabilities.mcp = Some(serde_json::json!({"servers": []}));
s.capabilities.mcp_proxy = Some(McpProxyConfig {
orchestrator: "bamboo-orchestrator".into(),
endpoint: "ws://127.0.0.1:9600".into(),
token: "t".into(),
});
assert!(matches!(s.validate(), Err(StoreError::Invalid(_))));
assert!(s.to_json().is_err());
s.capabilities.mcp = None;
assert!(s.validate().is_ok());
assert!(s.to_json().is_ok());
}
#[test]
fn round_trips() {
let s = spec();
let parsed = ProvisionSpec::from_json(&s.to_json().unwrap()).unwrap();
assert_eq!(parsed, s);
}
#[test]
fn unknown_fields_are_ignored_forward_compat() {
let mut v: serde_json::Value = serde_json::from_str(&spec().to_json().unwrap()).unwrap();
v["future_field"] = serde_json::json!({"x": 1});
v["identity"]["future_sub"] = serde_json::json!(true);
let parsed = ProvisionSpec::from_json(&v.to_string()).unwrap();
assert_eq!(parsed.identity.child_id, "c1");
}
#[test]
fn missing_optional_fields_default_backward_compat() {
let minimal = serde_json::json!({
"version": 1,
"identity": { "child_id": "c9" },
"executor": { "kind": "echo" },
"fabric_dir": "/tmp/f",
});
let parsed = ProvisionSpec::from_json(&minimal.to_string()).unwrap();
assert_eq!(parsed.identity.child_id, "c9");
assert_eq!(parsed.executor, ExecutorSpec::Echo);
assert!(parsed.model.is_none());
assert!(parsed.secrets.provider_credentials.is_empty());
assert_eq!(parsed.limits, Limits::default());
assert!(parsed.owner.is_none());
assert_eq!(parsed.placement, Placement::Local);
}
#[test]
fn placement_defaults_local_and_remote_round_trips() {
let v: serde_json::Value = serde_json::from_str(&spec().to_json().unwrap()).unwrap();
assert_eq!(v["placement"]["kind"], "local");
let mut s = spec();
s.placement = Placement::Remote {
endpoint: "wss://gpu-host:8443".into(),
};
let parsed = ProvisionSpec::from_json(&s.to_json().unwrap()).unwrap();
assert_eq!(
parsed.placement,
Placement::Remote {
endpoint: "wss://gpu-host:8443".into()
}
);
}
#[test]
fn worker_auth_token_defaults_none_and_round_trips() {
assert!(SecretsEnvelope::default().worker_auth_token.is_none());
let minimal = serde_json::json!({
"version": 1,
"identity": { "child_id": "c" },
"executor": { "kind": "echo" },
"fabric_dir": "/tmp/f",
});
let parsed = ProvisionSpec::from_json(&minimal.to_string()).unwrap();
assert!(parsed.secrets.worker_auth_token.is_none());
let mut s = spec();
assert!(
!s.to_json().unwrap().contains("worker_auth_token"),
"None token must be skipped in serialization"
);
s.secrets.worker_auth_token = Some("T-secret".into());
let parsed = ProvisionSpec::from_json(&s.to_json().unwrap()).unwrap();
assert_eq!(
parsed.secrets.worker_auth_token.as_deref(),
Some("T-secret")
);
}
#[test]
fn capabilities_default_empty_and_round_trip() {
assert_eq!(spec().capabilities, Capabilities::default());
let mut s = spec();
s.capabilities = Capabilities {
child_creation_identity: false,
required_child_context: false,
initial_input_release_required: false,
native_tool_ceiling_required: false,
native_tool_ceiling: None,
mcp: Some(serde_json::json!({ "version": 1, "servers": [] })),
skills_dir: Some("/home/u/.bamboo/skills".into()),
mcp_proxy: None,
enforce_permissions: false,
nested_spawn: false,
max_spawn_depth: None,
bypass: false,
auto_approve_permissions: false,
permission_requested_mode: String::new(),
permission_effective_mode: String::new(),
no_human_approver: false,
read_only: true,
guardian_read_only: false,
};
let parsed = ProvisionSpec::from_json(&s.to_json().unwrap()).unwrap();
assert_eq!(
parsed.capabilities.skills_dir.as_deref(),
Some("/home/u/.bamboo/skills")
);
assert!(parsed.capabilities.mcp.is_some());
assert!(parsed.capabilities.read_only_enforced());
let minimal = serde_json::json!({
"version": 1,
"identity": { "child_id": "c" },
"executor": { "kind": "echo" },
"fabric_dir": "/tmp/f",
});
let parsed = ProvisionSpec::from_json(&minimal.to_string()).unwrap();
assert_eq!(parsed.capabilities, Capabilities::default());
}
#[test]
fn legacy_guardian_read_only_alias_still_enforces_read_only() {
let capabilities: Capabilities = serde_json::from_value(serde_json::json!({
"guardian_read_only": true
}))
.unwrap();
assert!(!capabilities.read_only);
assert!(capabilities.read_only_enforced());
}
#[test]
fn capabilities_reject_partial_typed_permission_mode_pairs() {
let mut requested_only = Capabilities {
permission_requested_mode: "auto".to_string(),
auto_approve_permissions: true,
..Capabilities::default()
};
assert!(requested_only
.permission_resolution()
.unwrap_err()
.contains("provided together"));
let mut invalid_spec = spec();
invalid_spec.capabilities = requested_only.clone();
assert!(matches!(
invalid_spec.validate(),
Err(StoreError::Invalid(_))
));
assert!(invalid_spec.to_json().is_err());
requested_only.permission_requested_mode.clear();
requested_only.permission_effective_mode = "auto".to_string();
assert!(requested_only
.permission_resolution()
.unwrap_err()
.contains("provided together"));
let legacy_auto = Capabilities {
auto_approve_permissions: true,
..Capabilities::default()
};
assert_eq!(
legacy_auto.permission_resolution().unwrap(),
bamboo_domain::resolve_permission_mode(
bamboo_domain::SessionPermissionMode::Auto,
bamboo_domain::PermissionMode::Default,
)
);
}
#[test]
fn enforce_permissions_defaults_false_and_round_trips() {
assert!(!Capabilities::default().enforce_permissions);
let mut s = spec();
s.capabilities.enforce_permissions = true;
let parsed = ProvisionSpec::from_json(&s.to_json().unwrap()).unwrap();
assert!(parsed.capabilities.enforce_permissions);
}
#[test]
fn executor_tags_are_stable() {
let v: serde_json::Value = serde_json::from_str(&spec().to_json().unwrap()).unwrap();
assert_eq!(v["executor"]["kind"], "echo");
let cli = ExecutorSpec::CliAdapter {
command: "claude".into(),
args: vec!["-p".into()],
};
let vv = serde_json::to_value(&cli).unwrap();
assert_eq!(vv["kind"], "cli_adapter");
assert_eq!(
serde_json::to_value(ExecutorSpec::BambooRuntime).unwrap()["kind"],
"bamboo_runtime"
);
let claude_code = ExecutorSpec::ClaudeCode {
binary: Some("/usr/local/bin/claude".into()),
model: Some("claude-sonnet-4-6".into()),
permission_mode: Some("bypassPermissions".into()),
inherit_user_config: Some(true),
forward_env: Some(vec!["ANTHROPIC_API_KEY".into()]),
};
let vcc = serde_json::to_value(&claude_code).unwrap();
assert_eq!(vcc["kind"], "claude_code");
assert_eq!(vcc["binary"], "/usr/local/bin/claude");
assert_eq!(vcc["inherit_user_config"], true);
assert_eq!(vcc["forward_env"][0], "ANTHROPIC_API_KEY");
assert_eq!(
serde_json::from_value::<ExecutorSpec>(vcc).unwrap(),
claude_code
);
let minimal = ExecutorSpec::ClaudeCode {
binary: None,
model: None,
permission_mode: None,
inherit_user_config: None,
forward_env: None,
};
let vmin = serde_json::to_value(&minimal).unwrap();
assert_eq!(vmin, serde_json::json!({"kind": "claude_code"}));
let codex = ExecutorSpec::Codex {
binary: Some("/usr/local/bin/codex".into()),
model: Some("gpt-5-codex".into()),
mode: Some("exec".into()),
sandbox: Some("workspace-write".into()),
inherit_user_config: Some(true),
auth_mode: Some("inherit".into()),
base_url: None,
wire_api: None,
provider_key_ref: None,
forward_env: Some(vec!["OPENAI_API_KEY".into()]),
approval_policy: Some("never".into()),
network_access: Some(true),
allow_danger_bypass: Some(false),
permission_profile: Some("restricted".into()),
workspace_owned: Some(true),
};
let vc = serde_json::to_value(&codex).unwrap();
assert_eq!(vc["kind"], "codex");
assert_eq!(vc["binary"], "/usr/local/bin/codex");
assert_eq!(vc["sandbox"], "workspace-write");
assert_eq!(vc["forward_env"][0], "OPENAI_API_KEY");
assert_eq!(vc["approval_policy"], "never");
assert_eq!(vc["network_access"], true);
assert_eq!(vc["permission_profile"], "restricted");
assert_eq!(serde_json::from_value::<ExecutorSpec>(vc).unwrap(), codex);
let minimal_codex = ExecutorSpec::Codex {
binary: None,
model: None,
mode: None,
sandbox: None,
inherit_user_config: None,
auth_mode: None,
base_url: None,
wire_api: None,
provider_key_ref: None,
forward_env: None,
approval_policy: None,
network_access: None,
allow_danger_bypass: None,
permission_profile: None,
workspace_owned: None,
};
assert_eq!(
serde_json::to_value(&minimal_codex).unwrap(),
serde_json::json!({"kind": "codex"})
);
}
#[test]
fn native_ceiling_closed_startup_payload() {
use crate::proto::NativeToolCeiling;
let mut s = spec();
s.executor = ExecutorSpec::BambooRuntime;
s.identity.depth = 1;
s.storage_dir = Some("/isolated/native-fixture".into());
s.capabilities.enforce_permissions = true;
s.capabilities.required_child_context = true;
s.capabilities.child_creation_identity = true;
s.capabilities.native_tool_ceiling_required = true;
let ceiling = NativeToolCeiling {
version: 1,
child_session_id: "ceiling-child".into(),
parent_session_id: "ceiling-root".into(),
root_session_id: "ceiling-root".into(),
created_at: chrono::Utc::now(),
spawn_depth: 1,
project_id: None,
tools: vec!["Read".into()],
};
s.capabilities.native_tool_ceiling = Some(ceiling.clone());
assert!(s.validate().is_ok());
let mut missing = serde_json::to_value(&s).unwrap();
missing["capabilities"]
.as_object_mut()
.unwrap()
.remove("native_tool_ceiling_required");
assert!(ProvisionSpec::from_json(&missing.to_string())
.unwrap()
.validate()
.is_err());
let wire = s.to_json().unwrap();
assert!(ProvisionSpec::from_json(&wire).unwrap().validate().is_ok());
let mut value = serde_json::to_value(&s).unwrap();
value["capabilities"]["native_tool_ceiling"]
.as_object_mut()
.unwrap()
.remove("project_id");
assert!(ProvisionSpec::from_json(&value.to_string()).is_err());
for (key, malformed) in [
("version", serde_json::json!(2)),
("parent_session_id", serde_json::json!("ceiling-child")),
("tools", serde_json::json!(["Read", "Glob"])),
("tools", serde_json::json!(["Read", "Read"])),
("tools", serde_json::json!(["read_file"])),
(
"child_session_id",
serde_json::json!("x".repeat(NativeToolCeiling::MAX_BYTES)),
),
] {
let mut value = serde_json::to_value(&s).unwrap();
value["capabilities"]["native_tool_ceiling"][key] = malformed;
assert!(ProvisionSpec::from_json(&value.to_string())
.unwrap()
.validate()
.is_err());
}
let payload = serde_json::to_string(&ceiling).unwrap();
assert!(serde_json::from_str::<NativeToolCeiling>(&payload.replacen(
"\"version\":1",
"\"version\":1,\"version\":1",
1
))
.is_err());
let mut value = serde_json::to_value(&s).unwrap();
value["capabilities"]["native_tool_ceiling"]["unknown"] = serde_json::json!(true);
assert!(ProvisionSpec::from_json(&value.to_string()).is_err());
let mut value = serde_json::to_value(&s).unwrap();
value["capabilities"]["native_tool_ceiling"] = serde_json::Value::Null;
assert!(ProvisionSpec::from_json(&value.to_string())
.unwrap()
.validate()
.is_err());
s.capabilities.native_tool_ceiling_required = false;
assert!(s.validate().is_err());
s.capabilities.native_tool_ceiling_required = true;
s.capabilities.nested_spawn = true;
assert!(s.validate().is_err());
s.capabilities.nested_spawn = false;
assert!(WorkerCapabilityReport::current().supports(NATIVE_TOOL_CEILING_WORKER_CAPABILITY));
}
}