use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use crate::hel_config::{ExecutionPolicy, HarnessKind};
pub const DISCOVER_LOGIN_PATH_ENV: &str = "MJ_DISCOVER_LOGIN_PATH";
pub const REVIEWER_DIR: &str = "reviewer";
pub const REVIEWER_PROFILE_DIR: &str = "profile";
#[must_use]
pub fn reviewer_staging_profile_home(worker_root: &Path, generation: u64) -> PathBuf {
let root = worker_root.join(REVIEWER_DIR);
if generation == 0 {
root.join(REVIEWER_PROFILE_DIR)
} else {
root.join(format!("{REVIEWER_PROFILE_DIR}-{generation}"))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnessRuntimePolicy {
#[default]
Ambient,
#[serde(rename = "managed_remote", alias = "managed")]
Managed,
}
impl HarnessRuntimePolicy {
pub const fn is_ambient(&self) -> bool {
matches!(self, Self::Ambient)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkerOwnership {
pub version: u32,
#[serde(default = "default_worker_workspace_id")]
pub workspace_id: String,
pub session_id: String,
pub profile_id: String,
pub bundle_id: String,
pub target_template_id: String,
}
impl WorkerOwnership {
pub const VERSION: u32 = 2;
pub fn write(&self, path: &Path) -> Result<()> {
let body = serde_json::to_vec(self)?;
crate::hel_config::atomic_write(path, &body)
}
}
fn default_worker_workspace_id() -> String {
crate::hel_workspace::DEFAULT_WORKSPACE_ID.to_owned()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkerLaunchConfig {
pub session_id: String,
pub harness: HarnessKind,
pub bridge_command: PathBuf,
pub bridge_args: Vec<String>,
#[serde(default, skip_serializing_if = "HarnessRuntimePolicy::is_ambient")]
pub harness_runtime: HarnessRuntimePolicy,
pub environment: std::collections::BTreeMap<String, String>,
pub cwd: PathBuf,
#[serde(default)]
pub additional_directories: Vec<PathBuf>,
#[serde(default)]
pub native_session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub project_memory: Option<ProjectMemoryLaunchConfig>,
#[serde(
alias = "force_unrestricted_mode",
deserialize_with = "deserialize_execution_policy"
)]
pub execution_policy: ExecutionPolicy,
}
fn deserialize_execution_policy<'de, D>(deserializer: D) -> Result<ExecutionPolicy, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum WirePolicy {
Current(ExecutionPolicy),
Legacy(bool),
}
Ok(match WirePolicy::deserialize(deserializer)? {
WirePolicy::Current(policy) => policy,
WirePolicy::Legacy(true) => ExecutionPolicy::Unconstrained,
WirePolicy::Legacy(false) => ExecutionPolicy::ConfiguredApprovals,
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProjectMemoryLaunchConfig {
pub project_key: String,
pub root: PathBuf,
#[serde(default)]
pub baseline_root: PathBuf,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub repository_roots: std::collections::BTreeMap<String, PathBuf>,
#[serde(default, skip_serializing_if = "ProjectMemoryMcpDelivery::is_acp")]
pub mcp_delivery: ProjectMemoryMcpDelivery,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProjectMemoryMcpDelivery {
#[default]
Acp,
HarnessProfile,
}
impl ProjectMemoryMcpDelivery {
fn is_acp(&self) -> bool {
*self == Self::Acp
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewerLaunchConfig {
pub profile_id: String,
pub harness: HarnessKind,
pub bridge_command: PathBuf,
pub bridge_args: Vec<String>,
#[serde(default)]
pub environment: std::collections::BTreeMap<String, String>,
pub execution_policy: ExecutionPolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effort: Option<String>,
#[serde(default)]
pub generation: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mcp_servers: Vec<ReviewMcpServer>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReviewMcpServer {
pub name: String,
pub command: PathBuf,
#[serde(default)]
pub args: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReviewMcpDelivery {
Acp,
HarnessProfile,
}
impl ReviewMcpDelivery {
#[must_use]
pub const fn for_harness(harness: HarnessKind) -> Self {
match harness {
HarnessKind::Claude | HarnessKind::Kimi => Self::HarnessProfile,
_ => Self::Acp,
}
}
}
impl ReviewerLaunchConfig {
#[must_use]
pub fn reusable_for(&self, other: &Self) -> bool {
self.profile_id == other.profile_id
&& self.harness == other.harness
&& self.generation == other.generation
}
}
impl WorkerLaunchConfig {
pub fn read(path: &Path) -> Result<Self> {
let body = std::fs::read(path)
.with_context(|| format!("read worker launch config {}", path.display()))?;
serde_json::from_slice(&body)
.with_context(|| format!("parse worker launch config {}", path.display()))
}
pub fn write(&self, path: &Path) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)?;
let body = serde_json::to_vec_pretty(self)?;
std::fs::write(path, body)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
}
Ok(())
}
}
pub fn worker_executable_digest(path: &Path) -> Result<String> {
use sha2::Digest;
let mut file = std::fs::File::open(path)
.with_context(|| format!("open worker executable {}", path.display()))?;
let mut digest = sha2::Sha256::new();
std::io::copy(&mut file, &mut digest)
.with_context(|| format!("hash worker executable {}", path.display()))?;
Ok(format!("{:x}", digest.finalize()))
}
pub fn running_executable_digest() -> Option<String> {
let path = if cfg!(target_os = "linux") {
PathBuf::from("/proc/self/exe")
} else {
match std::env::current_exe() {
Ok(path) => path,
Err(error) => {
tracing::warn!(%error, "could not resolve this executable to report its build");
return None;
}
}
};
match worker_executable_digest(&path) {
Ok(digest) => Some(digest),
Err(error) => {
tracing::warn!(
error = format!("{error:#}"),
"could not hash this executable to report its build"
);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn launch_json() -> serde_json::Value {
serde_json::json!({
"session_id": "session",
"harness": "codex",
"bridge_command": "codex-acp",
"bridge_args": [],
"environment": {},
"cwd": "/workspace/project",
"execution_policy": "configured_approvals"
})
}
#[test]
fn old_launch_configs_default_to_ambient_harnesses() {
let launch: WorkerLaunchConfig = serde_json::from_value(launch_json()).unwrap();
assert_eq!(launch.harness_runtime, HarnessRuntimePolicy::Ambient);
}
#[test]
fn managed_policy_accepts_new_and_legacy_wire_names() {
let mut value = launch_json();
value["harness_runtime"] = serde_json::json!("managed_remote");
let launch: WorkerLaunchConfig = serde_json::from_value(value).unwrap();
assert_eq!(launch.harness_runtime, HarnessRuntimePolicy::Managed);
assert_eq!(
serde_json::to_value(&launch).unwrap()["harness_runtime"],
"managed_remote"
);
let mut value = launch_json();
value["harness_runtime"] = serde_json::json!("managed");
let launch: WorkerLaunchConfig = serde_json::from_value(value).unwrap();
assert_eq!(launch.harness_runtime, HarnessRuntimePolicy::Managed);
}
}