use std::path::{Path, PathBuf};
use std::time::Duration;
use chrono::{DateTime, TimeZone, Utc};
use crate::daemon::opencode_desktop_service::{Detection, ServiceState, UnarmedCause};
use crate::daemon::opencode_plugin_seen::{self, RelayArming};
use crate::error::{
ERR_OPENCODE_DESKTOP_SERVICE_FAILED, ERR_OPENCODE_DESKTOP_SERVICE_UNARMED,
ERR_OPENCODE_HOOK_UNREACHABLE, ERR_OPENCODE_PLUGIN_NOT_LOADED, ERR_OPENCODE_RELAY_UNPROVEN,
ERR_OPENCODE_RELAY_UNSUPPORTED, ERR_OPENCODE_SURFACE_UNDETERMINED,
ERR_OPENCODE_VERSION_UNSUPPORTED,
};
use crate::hooks::cline::SurfaceState;
use crate::hooks::cursor_attestation::{Reachability, RelayDelivery};
use crate::hooks::hook_files::{self, Existing};
use crate::hooks::opencode::{self, Installed, Located};
pub const AGENT: &str = "opencode";
const VERSION_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliReport {
pub state: SurfaceState,
pub version: Option<String>,
pub supported: Option<bool>,
pub prerelease: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DesktopReport {
pub state: SurfaceState,
pub version: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginFileState {
Present,
Absent,
Foreign,
}
impl PluginFileState {
pub fn as_str(self) -> &'static str {
match self {
Self::Present => "present",
Self::Absent => "absent",
Self::Foreign => "foreign",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpencodeAttestation {
pub as_of: DateTime<Utc>,
pub config_root: Option<(PathBuf, Located)>,
pub cli: CliReport,
pub desktop: DesktopReport,
pub plugin_file: PluginFileState,
pub plugin_seen: bool,
pub last_seen: Option<DateTime<Utc>>,
pub surfaces_seen: Vec<String>,
pub other_plugins: Vec<PathBuf>,
pub managed: Vec<PathBuf>,
pub reachability: Reachability,
pub relay: Option<OpencodeRelay>,
pub bypass_suspected: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WebsocketMode {
Off,
DeveloperOn,
Forced,
}
impl WebsocketMode {
pub fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::DeveloperOn => "developer_on",
Self::Forced => "forced",
}
}
pub fn from_facts(developer_value: Option<&str>, prerelease: bool) -> Self {
if prerelease {
Self::Forced
} else if developer_value.is_some_and(|v| v.trim().eq_ignore_ascii_case("true")) {
Self::DeveloperOn
} else {
Self::Off
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpencodeRelay {
pub cli: RelayDelivery,
pub desktop: DesktopDelivery,
pub desktop_line: DesktopLine,
pub desktop_traffic_proven: bool,
pub traffic_proven: bool,
pub last_traffic_ms: Option<i64>,
pub since_daemon_start: bool,
pub websocket: WebsocketMode,
pub tunnelled_hosts: Vec<String>,
pub loopback_hosts: Vec<String>,
pub unwired: Option<(&'static str, String)>,
pub launchers_off_path: bool,
}
pub const OPEN_NEW_TERMINAL_REMEDY: &str = "The OpenLatch launchers are not first on your user \
PATH yet. The daemon puts them there on its next wiring pass (`openlatch doctor --fix` \
restarts it); then open a new terminal so it picks up the OpenLatch launchers on PATH, and \
rerun `openlatch doctor`.";
pub const RELAY_UNPROVEN_REMEDY: &str = "Run one OpenCode turn from a shell that sources \
`env.sh` (PowerShell: `env.ps1`), then rerun `openlatch doctor`.";
pub const DESKTOP_UNPROVEN_DETAIL: &str = "no Desktop turn recorded yet";
pub const DESKTOP_UNPROVEN_REMEDY: &str =
"Run one turn in OpenCode Desktop, then rerun `openlatch doctor`.";
pub const SERVICE_ENV_LIMIT: &str = "OpenCode's service runs with the OpenLatch daemon's \
environment, not your login shell: a provider key exported only in a shell profile is \
missing for Desktop's turns; file-based credentials (`~/.aws`, OpenCode's own sign-in) \
still work";
pub const DESKTOP_TRAFFIC_BASIS: &str = "a correlation in time: an OpenCode turn recorded after \
Desktop armed (node line) or after the OpenLatch daemon started OpenCode's service (service \
line); a CLI or TUI turn in that window counts too";
pub const DESKTOP_UNARMED_REMEDY: &str = "OpenCode's service is running without the OpenLatch \
local model relay, so Desktop's model calls are not recorded yet; OpenCode keeps working. \
Quit OpenCode Desktop and every OpenCode terminal session: the OpenLatch daemon restarts \
OpenCode's service through the OpenLatch local model relay. Then open OpenCode Desktop and \
rerun `openlatch doctor`.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DesktopLine {
Node,
Service,
Absent,
}
impl DesktopLine {
pub fn as_str(self) -> &'static str {
match self {
Self::Node => "node",
Self::Service => "service",
Self::Absent => "absent",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DesktopUnwired {
NotInstalled,
RelayStopped,
ServiceFailed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DesktopDelivery {
Wired,
Unwired(DesktopUnwired),
Unarmed(UnarmedCause),
Unsupported(String),
}
impl DesktopDelivery {
pub fn as_str(&self) -> &'static str {
match self {
Self::Wired => "wired",
Self::Unwired(_) => "unwired",
Self::Unarmed(_) => "unarmed",
Self::Unsupported(_) => "unsupported",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotManaged {
Isolated,
Elevated,
}
impl NotManaged {
pub fn as_str(self) -> &'static str {
match self {
Self::Isolated => "isolated",
Self::Elevated => "elevated",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceReport {
State(ServiceState),
NotManaged(NotManaged),
}
pub const DESKTOP_SERVICE_ROUTE: &str = "/admin/opencode/desktop-service";
impl ServiceReport {
pub fn to_json(&self) -> serde_json::Value {
use serde_json::json;
match self {
Self::State(ServiceState::Absent) => json!({"state": "absent"}),
Self::State(ServiceState::Unsupported(r)) => {
json!({"state": "unsupported", "reason": r})
}
Self::State(ServiceState::Starting) => json!({"state": "starting"}),
Self::State(ServiceState::Armed) => json!({"state": "armed"}),
Self::State(ServiceState::Unarmed(c)) => {
json!({"state": "unarmed", "cause": c.as_str()})
}
Self::State(ServiceState::Failed(r)) => json!({"state": "failed", "reason": r}),
Self::NotManaged(n) => json!({"state": "not_managed", "reason": n.as_str()}),
}
}
pub fn from_json(v: &serde_json::Value) -> Option<Self> {
let reason = || {
v.get("reason")
.and_then(serde_json::Value::as_str)
.map(str::to_string)
};
Some(match v.get("state")?.as_str()? {
"absent" => Self::State(ServiceState::Absent),
"unsupported" => Self::State(ServiceState::Unsupported(reason()?)),
"starting" => Self::State(ServiceState::Starting),
"armed" => Self::State(ServiceState::Armed),
"unarmed" => Self::State(ServiceState::Unarmed(match v.get("cause")?.as_str()? {
"foreign_attached" => UnarmedCause::ForeignAttached,
"identity_unverified" => UnarmedCause::IdentityUnverified,
"update_window" => UnarmedCause::UpdateWindow,
"awaiting_respawn" => UnarmedCause::AwaitingRespawn,
_ => return None,
})),
"failed" => Self::State(ServiceState::Failed(reason()?)),
"not_managed" => Self::NotManaged(match reason()?.as_str() {
"isolated" => NotManaged::Isolated,
"elevated" => NotManaged::Elevated,
_ => return None,
}),
_ => return None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DesktopFacts {
pub installed: bool,
pub detection: Detection,
pub service: Option<ServiceReport>,
pub relay_keys: bool,
pub relay_live: bool,
pub node_arming: Option<RelayArming>,
pub service_started_at: Option<i64>,
}
fn sandboxed_reason(packaging: &str) -> String {
format!(
"OpenCode Desktop is installed as a {packaging} package, which the OpenLatch daemon \
cannot start OpenCode's service inside"
)
}
const ISOLATED_REASON: &str = "this OpenLatch instance is isolated from the machine's own \
install, so it does not start OpenCode's service; the machine's own install does";
const ELEVATED_REASON: &str =
"the OpenLatch daemon runs with elevated rights, so it does not start OpenCode's service";
const NO_REPORT_REASON: &str = "the OpenLatch daemon did not report OpenCode's service";
pub fn classify_desktop(
facts: &DesktopFacts,
last_traffic_ms: Option<i64>,
) -> (DesktopLine, DesktopDelivery, bool) {
let line = match &facts.detection {
Detection::Found(_) | Detection::Unsupported(_) => DesktopLine::Service,
Detection::Absent if facts.installed => DesktopLine::Node,
Detection::Absent => DesktopLine::Absent,
};
let delivery = match line {
DesktopLine::Absent => DesktopDelivery::Unwired(DesktopUnwired::NotInstalled),
DesktopLine::Node if facts.relay_keys && facts.relay_live => DesktopDelivery::Wired,
DesktopLine::Node => DesktopDelivery::Unwired(DesktopUnwired::RelayStopped),
DesktopLine::Service => match (&facts.detection, &facts.service) {
(Detection::Unsupported(packaging), _) => {
DesktopDelivery::Unsupported(sandboxed_reason(packaging))
}
(_, Some(ServiceReport::NotManaged(NotManaged::Isolated))) => {
DesktopDelivery::Unsupported(ISOLATED_REASON.to_string())
}
_ if !facts.relay_live => DesktopDelivery::Unwired(DesktopUnwired::RelayStopped),
(_, Some(ServiceReport::NotManaged(NotManaged::Elevated))) => {
DesktopDelivery::Unwired(DesktopUnwired::ServiceFailed(ELEVATED_REASON.to_string()))
}
(_, None) => DesktopDelivery::Unwired(DesktopUnwired::ServiceFailed(
NO_REPORT_REASON.to_string(),
)),
(_, Some(ServiceReport::State(state))) => match state {
ServiceState::Armed if facts.relay_keys => DesktopDelivery::Wired,
ServiceState::Armed => DesktopDelivery::Unarmed(UnarmedCause::AwaitingRespawn),
ServiceState::Unarmed(cause) => DesktopDelivery::Unarmed(*cause),
ServiceState::Starting | ServiceState::Absent => {
DesktopDelivery::Unarmed(UnarmedCause::AwaitingRespawn)
}
ServiceState::Failed(reason) => {
DesktopDelivery::Unwired(DesktopUnwired::ServiceFailed(reason.clone()))
}
ServiceState::Unsupported(packaging) => {
DesktopDelivery::Unsupported(sandboxed_reason(packaging))
}
},
},
};
let proven = delivery == DesktopDelivery::Wired
&& match line {
DesktopLine::Node => facts.node_arming.as_ref().is_some_and(|a| {
a.runtime == "node"
&& a.relay_arming == "armed_in_process"
&& last_traffic_ms.is_some_and(|t| t > a.at_ms)
}),
DesktopLine::Service => facts
.service_started_at
.zip(last_traffic_ms)
.is_some_and(|(started, t)| t > started),
DesktopLine::Absent => false,
};
(line, delivery, proven)
}
pub fn unarmed_cause_sentence(cause: UnarmedCause) -> &'static str {
match cause {
UnarmedCause::ForeignAttached => {
"An OpenCode service the OpenLatch daemon did not start is running while OpenCode \
is open."
}
UnarmedCause::IdentityUnverified => {
"The running OpenCode service could not be verified, so the OpenLatch daemon leaves \
it alone."
}
UnarmedCause::UpdateWindow => {
"OpenCode Desktop was updated, and its service is waiting to be replaced."
}
UnarmedCause::AwaitingRespawn => {
"OpenCode's service is waiting to be restarted with the current OpenLatch local \
model relay settings."
}
}
}
impl OpencodeRelay {
pub fn findings(&self) -> Vec<(&'static str, &'static str, &'static str, String)> {
let mut out = Vec::new();
match self.cli {
RelayDelivery::Wired => {}
RelayDelivery::Unsupported => out.push((
"cli",
self.cli.as_str(),
ERR_OPENCODE_RELAY_UNSUPPORTED,
"Nothing to do: this build does not route the OpenCode CLI through the relay \
here; its plugin still enforces."
.to_string(),
)),
RelayDelivery::Unwired => {
let (code, mut remedy) = self.cli_unwired_cause();
if self.launchers_off_path {
remedy = OPEN_NEW_TERMINAL_REMEDY.to_string();
}
out.push(("cli", self.cli.as_str(), code, remedy));
}
}
if let Some(finding) = self.desktop_finding() {
out.push(finding);
}
if self.desktop == DesktopDelivery::Wired && !self.desktop_traffic_proven {
out.push((
"desktop_traffic",
"unproven",
ERR_OPENCODE_RELAY_UNPROVEN,
DESKTOP_UNPROVEN_REMEDY.to_string(),
));
}
if self.cli == RelayDelivery::Wired && !self.traffic_proven {
out.push((
"traffic",
"unproven",
ERR_OPENCODE_RELAY_UNPROVEN,
RELAY_UNPROVEN_REMEDY.to_string(),
));
}
if self.websocket != WebsocketMode::Off {
out.push((
"websocket",
self.websocket.as_str(),
ERR_OPENCODE_RELAY_UNSUPPORTED,
match self.websocket {
WebsocketMode::Forced => {
"This pre-release OpenCode sends OpenAI turns over a WebSocket the relay \
cannot capture. Use a stable OpenCode build."
}
_ => {
"OpenAI turns travel over a WebSocket the relay cannot capture. Unset \
OPENCODE_EXPERIMENTAL_WEBSOCKETS."
}
}
.to_string(),
));
}
out
}
fn cli_unwired_cause(&self) -> (&'static str, String) {
self.unwired.clone().unwrap_or((
crate::error::ERR_MODEL_RELAY_PREFLIGHT_FAILED,
"Run `openlatch doctor --fix`; the relay wires OpenCode once its preflight passes."
.to_string(),
))
}
pub fn desktop_finding(&self) -> Option<(&'static str, &'static str, &'static str, String)> {
let state = self.desktop.as_str();
let (code, remedy) = match &self.desktop {
DesktopDelivery::Wired | DesktopDelivery::Unwired(DesktopUnwired::NotInstalled) => {
return None
}
DesktopDelivery::Unwired(DesktopUnwired::RelayStopped) => self.cli_unwired_cause(),
DesktopDelivery::Unwired(DesktopUnwired::ServiceFailed(reason)) => (
ERR_OPENCODE_DESKTOP_SERVICE_FAILED,
format!(
"{}. OpenCode keeps working, but Desktop's model calls are not recorded until \
OpenCode's service starts through the OpenLatch local model relay. Open \
OpenCode Desktop once so it stages its CLI, or, after repeated exits, fix \
the cause named and restart the OpenLatch daemon (`openlatch restart`); \
then rerun `openlatch doctor`.",
reason.trim_end_matches('.')
),
),
DesktopDelivery::Unarmed(cause) => (
ERR_OPENCODE_DESKTOP_SERVICE_UNARMED,
format!(
"{} {DESKTOP_UNARMED_REMEDY}",
unarmed_cause_sentence(*cause)
),
),
DesktopDelivery::Unsupported(reason) => (
ERR_OPENCODE_RELAY_UNSUPPORTED,
format!(
"Nothing to do: {reason}. Desktop's model calls go direct, and the plugin \
still enforces."
),
),
};
Some(("desktop", state, code, remedy))
}
fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"cli": self.cli.as_str(),
"desktop": self.desktop.as_str(),
"desktop_line": self.desktop_line.as_str(),
"desktop_cause": match self.desktop {
DesktopDelivery::Unarmed(cause) => Some(cause.as_str()),
_ => None,
},
"desktop_traffic": if self.desktop_traffic_proven { "proven" } else { "unproven" },
"desktop_traffic_basis": DESKTOP_TRAFFIC_BASIS,
"traffic": if self.traffic_proven { "proven" } else { "unproven" },
"websocket": self.websocket.as_str(),
"since_daemon_start": self.since_daemon_start,
"tunnelled_hosts": self.tunnelled_hosts,
"loopback_hosts": self.loopback_hosts,
"findings": self.findings().into_iter().map(|(field, state, code, remedy)| {
serde_json::json!({"field": field, "state": state, "code": code, "remedy": remedy})
}).collect::<Vec<_>>(),
})
}
}
pub fn is_prerelease(version_text: &str) -> bool {
version_text
.split_whitespace()
.find(|t| {
t.trim_start_matches('v')
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit())
})
.is_some_and(|t| t.split('+').next().unwrap_or_default().contains('-'))
}
pub fn configured_provider_hosts(
root: Option<&Path>,
cwd: Option<&Path>,
intercepted: &[&str],
patterns: &[crate::model_relay::host_match::HostRule],
) -> (Vec<String>, Vec<String>) {
let (mut tunnelled, mut loopback) = (Vec::new(), Vec::new());
for dir in [root, cwd].into_iter().flatten() {
for name in ["opencode.json", "opencode.jsonc"] {
let Ok(raw) = std::fs::read_to_string(dir.join(name)) else {
continue;
};
let Ok(doc) = crate::hooks::jsonc::parse_settings_value(&raw) else {
continue;
};
let Some(providers) = doc.get("provider").and_then(serde_json::Value::as_object) else {
continue;
};
for provider in providers.values() {
let Some(host) = provider
.pointer("/options/baseURL")
.and_then(serde_json::Value::as_str)
.and_then(url_host)
else {
continue;
};
let list = if is_loopback_host(&host) {
&mut loopback
} else if intercepted.iter().any(|h| h.eq_ignore_ascii_case(&host))
|| crate::model_relay::host_match::matches(patterns, &host)
{
continue;
} else {
&mut tunnelled
};
if !list.contains(&host) {
list.push(host);
}
}
}
}
tunnelled.sort();
loopback.sort();
(tunnelled, loopback)
}
fn url_host(url: &str) -> Option<String> {
let rest = url.split_once("://")?.1;
let authority = &rest[..rest.find(['/', '\\', '?', '#']).unwrap_or(rest.len())];
let host_port = authority.rsplit('@').next()?;
let host = if let Some(v6) = host_port.strip_prefix('[') {
v6.split(']').next()?
} else {
host_port.split(':').next()?
};
(!host.is_empty() && !host.contains(['{', '}', ' '])).then(|| host.to_ascii_lowercase())
}
fn is_loopback_host(host: &str) -> bool {
host == "localhost"
|| host.ends_with(".localhost")
|| host == "::1"
|| host
.parse::<std::net::Ipv4Addr>()
.is_ok_and(|ip| ip.is_loopback())
}
pub struct Inputs {
pub openlatch_dir: PathBuf,
pub root: Option<(PathBuf, Located)>,
pub installed: Installed,
pub cli_version: Option<Result<String, String>>,
pub desktop_version: Option<String>,
pub managed_roots: Vec<PathBuf>,
pub now: DateTime<Utc>,
}
pub fn probe(openlatch_dir: &Path) -> OpencodeAttestation {
let installed = opencode::installed();
let cli_version = installed.cli.as_deref().map(run_version);
let desktop_version = installed.desktop.as_deref().and_then(desktop_version);
from_inputs(Inputs {
openlatch_dir: openlatch_dir.to_path_buf(),
root: opencode::root_located(),
installed,
cli_version,
desktop_version,
managed_roots: managed_roots(),
now: Utc::now(),
})
}
pub fn from_inputs(inputs: Inputs) -> OpencodeAttestation {
let cli = match (&inputs.installed.cli, &inputs.cli_version) {
(None, _) => CliReport {
state: if inputs.root.is_some() {
SurfaceState::Absent
} else {
SurfaceState::Undetermined
},
version: None,
supported: None,
prerelease: false,
},
(Some(_), Some(Ok(text))) => match opencode::parse_version(text) {
Some(version) => CliReport {
state: SurfaceState::Present,
version: Some(format!("{}.{}.{}", version.0, version.1, version.2)),
supported: Some(opencode::version_supported(version)),
prerelease: is_prerelease(text),
},
None => CliReport {
state: SurfaceState::Undetermined,
version: None,
supported: None,
prerelease: false,
},
},
(Some(_), _) => CliReport {
state: SurfaceState::Undetermined,
version: None,
supported: None,
prerelease: false,
},
};
let desktop = DesktopReport {
state: match (&inputs.installed.desktop, &inputs.root) {
(Some(_), _) => SurfaceState::Present,
(None, Some(_)) => SurfaceState::Absent,
(None, None) => SurfaceState::Undetermined,
},
version: inputs.desktop_version.clone(),
};
let plugin_path = inputs
.root
.as_ref()
.map(|(root, _)| opencode::plugin_path(root));
let plugin_file = match plugin_path.as_deref().map(hook_files::whatever_is_there) {
Some(Existing::Ours) => PluginFileState::Present,
Some(Existing::Theirs) => PluginFileState::Foreign,
_ => PluginFileState::Absent,
};
let written_ms = plugin_path
.as_deref()
.filter(|_| plugin_file == PluginFileState::Present)
.and_then(opencode_plugin_seen::mtime_ms);
let seen = opencode_plugin_seen::read_seen(&inputs.openlatch_dir);
let plugin_seen =
written_ms.is_some_and(|written| opencode_plugin_seen::seen_since(&seen, written));
let last_seen =
opencode_plugin_seen::last_seen(&seen).and_then(|ms| Utc.timestamp_millis_opt(ms).single());
let installed_at = written_ms.and_then(|ms| Utc.timestamp_millis_opt(ms).single());
let other_plugins = match opencode_plugin_seen::reported_plugins(&inputs.openlatch_dir) {
Some(reported) => reported_other_plugins(&reported, plugin_path.as_deref()),
None => inputs
.root
.as_ref()
.map(|(root, _)| other_plugins(root, plugin_path.as_deref()))
.unwrap_or_default(),
};
OpencodeAttestation {
as_of: inputs.now,
config_root: inputs.root.clone(),
cli,
desktop,
plugin_file,
plugin_seen,
last_seen,
surfaces_seen: seen.keys().cloned().collect(),
other_plugins,
managed: inputs
.managed_roots
.into_iter()
.filter(|p| p.exists())
.collect(),
reachability: reachability(&inputs.openlatch_dir, installed_at),
relay: None,
bypass_suspected: None,
}
}
fn run_version(binary: &Path) -> Result<String, String> {
run_version_within(binary, VERSION_TIMEOUT)
}
fn run_version_within(binary: &Path, timeout: Duration) -> Result<String, String> {
use std::io::BufRead;
let mut child = std::process::Command::new(binary)
.arg("--version")
.env("OPENCODE_DISABLE_AUTOUPDATE", "1")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| e.to_string())?;
let deadline = std::time::Instant::now() + timeout;
let (tx, rx) = std::sync::mpsc::channel();
if let Some(stdout) = child.stdout.take() {
std::thread::spawn(move || {
let mut line = String::new();
let _ = std::io::BufReader::new(stdout).read_line(&mut line);
let _ = tx.send(line);
});
}
let status = loop {
match child.try_wait() {
Ok(Some(status)) => break status,
Ok(None) if std::time::Instant::now() < deadline => {
std::thread::sleep(Duration::from_millis(25));
}
Ok(None) => {
let _ = child.kill();
let _ = child.wait();
return Err("`opencode --version` did not answer in time".to_string());
}
Err(e) => return Err(e.to_string()),
}
};
if !status.success() {
return Err(format!("`opencode --version` exited with {status}"));
}
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
rx.recv_timeout(remaining)
.map_err(|_| "`opencode --version` did not answer in time".to_string())
}
fn desktop_version(location: &Path) -> Option<String> {
let plist = std::fs::read_to_string(location.join("Contents").join("Info.plist")).ok()?;
let after_key = plist
.split("<key>CFBundleShortVersionString</key>")
.nth(1)?;
let value = after_key
.split("<string>")
.nth(1)?
.split("</string>")
.next()?;
let value = value.trim();
(!value.is_empty()).then(|| value.to_string())
}
fn managed_roots() -> Vec<PathBuf> {
opencode::managed_dir().into_iter().collect()
}
fn reported_other_plugins(reported: &[String], ours: Option<&Path>) -> Vec<PathBuf> {
let canonical = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
let ours = ours.map(canonical);
reported
.iter()
.map(|entry| file_url_path(entry).unwrap_or_else(|| PathBuf::from(entry)))
.filter(|path| ours.as_deref() != Some(canonical(path).as_path()))
.collect()
}
fn file_url_path(entry: &str) -> Option<PathBuf> {
let rest = entry.strip_prefix("file://")?;
let rest = match rest.as_bytes() {
[b'/', drive, b':', ..] if drive.is_ascii_alphabetic() && cfg!(windows) => &rest[1..],
_ => rest,
};
let bytes = rest.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 2 < bytes.len() {
let hex = std::str::from_utf8(&bytes[i + 1..i + 3]).ok();
if let Some(byte) = hex.and_then(|h| u8::from_str_radix(h, 16).ok()) {
out.push(byte);
i += 3;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
Some(PathBuf::from(String::from_utf8_lossy(&out).into_owned()))
}
fn other_plugins(root: &Path, ours: Option<&Path>) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = ["plugin", opencode::PLUGINS_DIR_NAME]
.iter()
.filter_map(|dir| std::fs::read_dir(root.join(dir)).ok())
.flatten()
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| matches!(path.extension().and_then(|e| e.to_str()), Some("js" | "ts")))
.filter(|path| Some(path.as_path()) != ours)
.collect();
out.sort();
out
}
fn reachability(openlatch_dir: &Path, installed_at: Option<DateTime<Utc>>) -> Reachability {
let since = |at: &DateTime<Utc>| installed_at.is_none_or(|installed| *at >= installed);
if crate::daemon::agent_liveness::read(openlatch_dir)
.get(AGENT)
.is_some_and(since)
{
return Reachability::Proven;
}
if fallback_holds_event(&openlatch_dir.join("logs").join("fallback.jsonl"), &since) {
return Reachability::Failed;
}
Reachability::Pending
}
fn fallback_holds_event(path: &Path, since: &dyn Fn(&DateTime<Utc>) -> bool) -> bool {
use std::io::BufRead;
let Ok(file) = std::fs::File::open(path) else {
return false;
};
std::io::BufReader::new(file)
.lines()
.map_while(Result::ok)
.filter(|line| line.contains(AGENT))
.filter_map(|line| serde_json::from_str::<serde_json::Value>(&line).ok())
.filter(|event| event["source"].as_str() == Some(AGENT))
.filter(|event| event["data"]["opencode_hook"].as_str() != Some("plugin_loaded"))
.any(|event| {
event["time"]
.as_str()
.and_then(|t| DateTime::parse_from_rfc3339(t).ok())
.is_some_and(|at| since(&at.with_timezone(&Utc)))
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AbsentSurface {
pub surface: &'static str,
pub state: SurfaceState,
pub code: Option<&'static str>,
pub remedy: String,
}
impl OpencodeAttestation {
pub fn surfaces_absent(&self) -> Vec<AbsentSurface> {
[("cli", self.cli.state), ("desktop", self.desktop.state)]
.into_iter()
.filter(|(_, state)| *state != SurfaceState::Present)
.map(|(surface, state)| match state {
SurfaceState::Undetermined => AbsentSurface {
surface,
state,
code: Some(ERR_OPENCODE_SURFACE_UNDETERMINED),
remedy: format!(
"Start OpenCode once on the {surface} surface, then rerun `openlatch doctor`."
),
},
_ => AbsentSurface {
surface,
state,
code: None,
remedy: format!(
"Nothing to do unless you use the OpenCode {surface}; install it, then rerun `openlatch init --agent opencode`."
),
},
})
.collect()
}
pub fn to_json(&self) -> serde_json::Value {
use serde_json::json;
json!({
"as_of": self.as_of.to_rfc3339(),
"config_root": self.config_root.as_ref().map(|(path, located)| json!({
"path": crate::core::path_compat::display_path(path),
"located": located.as_str(),
})),
"cli": {
"state": self.cli.state.as_str(),
"version": self.cli.version,
"supported": self.cli.supported,
},
"desktop": {
"state": self.desktop.state.as_str(),
"version": self.desktop.version,
},
"plugin_file": {
"state": self.plugin_file.as_str(),
"ours": self.plugin_file == PluginFileState::Present,
},
"plugin_seen": {
"state": if self.plugin_seen { "seen" } else { "pending" },
"last_seen": self.last_seen.map(|t| t.to_rfc3339()),
"surfaces": self.surfaces_seen,
},
"other_plugins": self.other_plugins.iter().map(|path| json!({
"name": path.file_name().map(|n| n.to_string_lossy().into_owned()),
"path": crate::core::path_compat::display_path(path),
})).collect::<Vec<_>>(),
"managed": self.managed.iter().map(|p| crate::core::path_compat::display_path(p)).collect::<Vec<_>>(),
"reachability": self.reachability.as_str(),
"surfaces_absent": self.surfaces_absent().iter().map(|s| json!({
"surface": s.surface,
"state": s.state.as_str(),
"code": s.code,
"remedy": s.remedy,
})).collect::<Vec<_>>(),
"relay": self.relay.as_ref().map(OpencodeRelay::to_json),
"bypass_suspected": self.bypass_suspected,
})
}
pub fn summary(&self) -> String {
let cli = match (&self.cli.state, &self.cli.version) {
(SurfaceState::Present, Some(v)) => format!("CLI {v}"),
(state, _) => format!("CLI {}", state.as_str()),
};
let desktop = match (&self.desktop.state, &self.desktop.version) {
(SurfaceState::Present, Some(v)) => format!("Desktop {v}"),
(state, _) => format!("Desktop {}", state.as_str()),
};
format!(
"OpenCode: {cli}, {desktop}; plugin {} ({})",
self.plugin_file.as_str(),
if self.plugin_seen { "seen" } else { "pending" }
)
}
pub fn human_lines(&self) -> Vec<String> {
let mut lines = Vec::new();
if let Some((path, located)) = &self.config_root {
lines.push(format!(
"Config root: {} ({})",
crate::core::path_compat::display_path(path),
located.as_str()
));
}
if let Some(at) = self.last_seen {
lines.push(format!(
"Plugin last loaded: {} ({})",
at.to_rfc3339(),
self.surfaces_seen.join(", ")
));
}
for path in &self.other_plugins {
lines.push(format!(
"Other plugin: {}",
crate::core::path_compat::display_path(path)
));
}
for path in &self.managed {
lines.push(format!(
"Managed config (read only): {}",
crate::core::path_compat::display_path(path)
));
}
lines.push(format!("Reachability: {}", self.reachability.as_str()));
if let Some(relay) = &self.relay {
lines.push(format!(
"Relay: CLI {}, Desktop {} ({} line), traffic {}, WebSocket {}{}",
relay.cli.as_str(),
relay.desktop.as_str(),
relay.desktop_line.as_str(),
if relay.traffic_proven {
"proven"
} else {
"unproven"
},
relay.websocket.as_str(),
if relay.since_daemon_start {
" (since daemon start)"
} else {
""
}
));
for host in &relay.tunnelled_hosts {
lines.push(format!(
"Custom provider host, tunnelled and not captured: {host}"
));
}
for host in &relay.loopback_hosts {
lines.push(format!("Loopback provider, not captured: {host}"));
}
}
lines
}
pub fn not_loaded(&self, written_ms: Option<i64>) -> Option<(&'static str, String)> {
let relay = self.relay.as_ref()?;
let traffic_since_write = !self.plugin_seen
&& self.plugin_file == PluginFileState::Present
&& relay
.last_traffic_ms
.zip(written_ms)
.is_some_and(|(traffic, written)| traffic >= written);
let signal = if self.bypass_suspected == Some(true) {
"the relay recorded OpenCode model traffic for a session the plugin never reported \
(bypass suspected)"
} else if traffic_since_write {
"the relay recorded OpenCode model traffic since the plugin was written, and the \
plugin has not reported loading"
} else {
return None;
};
Some((
ERR_OPENCODE_PLUGIN_NOT_LOADED,
format!("{signal}. Restart OpenCode and check that it is not started with `--pure`."),
))
}
pub fn version_unsupported(&self) -> Option<(&'static str, String)> {
(self.cli.supported == Some(false)).then(|| {
(
ERR_OPENCODE_VERSION_UNSUPPORTED,
format!(
"OpenCode {} is below the supported floor 1.17.0. Run `opencode upgrade`.",
self.cli.version.as_deref().unwrap_or("unknown")
),
)
})
}
pub fn unreachable(&self) -> Option<(&'static str, String)> {
(self.reachability == Reachability::Failed).then(|| {
let likely = if cfg!(windows) {
"on Windows, check that `openlatch-hook.exe` exists under the OpenLatch `bin` folder and is not blocked"
} else {
"check that the daemon is running and that `openlatch-hook` exists under the OpenLatch `bin` folder"
};
(
ERR_OPENCODE_HOOK_UNREACHABLE,
format!(
"The OpenLatch plugin ran the hook but it could not reach the daemon; {likely}. Run `openlatch doctor --fix`, then restart OpenCode."
),
)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
fn script(dir: &Path, name: &str, body: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let path = dir.join(name);
std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[cfg(unix)]
#[test]
fn the_version_probe_is_bounded_when_a_descendant_holds_stdout() {
let dir = tempfile::tempdir().unwrap();
let answers = script(dir.path(), "answers", "(sleep 30) &\necho 1.18.35");
let started = std::time::Instant::now();
let out = run_version_within(&answers, Duration::from_secs(3)).expect("version");
assert_eq!(out.trim(), "1.18.35");
assert!(started.elapsed() < Duration::from_secs(3));
let silent = script(dir.path(), "silent", "(sleep 30) &\nexit 0");
let started = std::time::Instant::now();
assert!(run_version_within(&silent, Duration::from_millis(500)).is_err());
assert!(started.elapsed() < Duration::from_secs(3));
}
struct Host {
_dir: tempfile::TempDir,
ol: PathBuf,
root: PathBuf,
}
fn host() -> Host {
let dir = tempfile::tempdir().expect("tempdir");
let ol = dir.path().join("openlatch");
let root = dir.path().join("config").join("opencode");
std::fs::create_dir_all(&ol).unwrap();
std::fs::create_dir_all(&root).unwrap();
Host {
ol,
root,
_dir: dir,
}
}
fn inputs(host: &Host, cli_version: Option<Result<String, String>>) -> Inputs {
Inputs {
openlatch_dir: host.ol.clone(),
root: Some((host.root.clone(), Located::Xdg)),
installed: Installed {
cli: cli_version
.as_ref()
.map(|_| PathBuf::from("/usr/local/bin/opencode")),
desktop: None,
},
cli_version,
desktop_version: None,
managed_roots: vec![],
now: Utc::now(),
}
}
#[test]
fn other_plugins_follow_the_load_report_then_the_folders() {
let host = host();
let plugins = opencode::plugins_dir(&host.root);
crate::hooks::opencode_plugin::install(&plugins, &host.ol).expect("install");
std::fs::write(plugins.join("global.js"), "export default async () => ({})").unwrap();
let before = from_inputs(inputs(&host, None));
assert_eq!(before.other_plugins, vec![plugins.join("global.js")]);
let ours = format!("file://{}", opencode::plugin_path(&host.root).display());
let repo = std::env::temp_dir().join("repo with space/.opencode/plugins/repo.ts");
let repo_url = format!("file://{}", repo.display()).replace(' ', "%20");
crate::daemon::opencode_plugin_seen::record_report(
&host.ol,
"cli",
1,
Some(&["opencode-foo@1.0.0".to_string(), repo_url, ours]),
None,
None,
)
.unwrap();
let after = from_inputs(inputs(&host, None));
assert_eq!(
after.other_plugins,
vec![PathBuf::from("opencode-foo@1.0.0"), repo]
);
}
#[test]
fn pending_until_a_load_report_follows_the_write_then_seen() {
let host = host();
crate::hooks::opencode_plugin::install(&opencode::plugins_dir(&host.root), &host.ol)
.expect("install");
let a = from_inputs(inputs(&host, Some(Ok("1.18.35\n".into()))));
assert_eq!(a.plugin_file, PluginFileState::Present);
assert!(!a.plugin_seen);
let json = a.to_json();
assert_eq!(json["plugin_seen"]["state"], "pending");
assert_eq!(json["plugin_file"]["ours"], true);
assert_eq!(json["cli"]["version"], "1.18.35");
assert_eq!(json["cli"]["supported"], true);
assert_eq!(json["config_root"]["located"], "xdg");
assert_eq!(json["reachability"], "pending");
let later = Utc::now().timestamp_millis() + 1000;
opencode_plugin_seen::record(&host.ol, "cli", later).unwrap();
let a = from_inputs(inputs(&host, Some(Ok("1.18.35".into()))));
assert!(a.plugin_seen);
assert_eq!(a.to_json()["plugin_seen"]["state"], "seen");
assert_eq!(
a.to_json()["plugin_seen"]["surfaces"],
serde_json::json!(["cli"])
);
}
#[test]
fn every_c5_field_is_rendered() {
let host = host();
let json = from_inputs(inputs(&host, Some(Ok("1.18.35".into())))).to_json();
for field in [
"as_of",
"config_root",
"cli",
"desktop",
"plugin_file",
"plugin_seen",
"other_plugins",
"reachability",
"surfaces_absent",
"relay",
"bypass_suspected",
] {
assert!(json.get(field).is_some(), "{field} missing");
}
assert!(json["relay"].is_null(), "filled by doctor from the relay");
assert!(json["bypass_suspected"].is_null());
assert_eq!(json["plugin_file"]["state"], "absent");
}
#[test]
fn a_cli_below_the_floor_is_unsupported() {
let host = host();
let a = from_inputs(inputs(&host, Some(Ok("opencode 1.16.2".into()))));
assert_eq!(a.cli.supported, Some(false));
let (code, remedy) = a.version_unsupported().expect("finding");
assert_eq!(code, ERR_OPENCODE_VERSION_UNSUPPORTED);
assert!(remedy.contains("opencode upgrade"));
assert!(from_inputs(inputs(&host, Some(Ok("1.17.0".into()))))
.version_unsupported()
.is_none());
}
#[test]
fn an_unanswering_cli_and_an_unresolved_root_are_undetermined() {
let host = host();
let a = from_inputs(inputs(&host, Some(Err("timed out".into()))));
assert_eq!(a.cli.state, SurfaceState::Undetermined);
let absent = a.surfaces_absent();
let cli = absent.iter().find(|s| s.surface == "cli").expect("cli");
assert_eq!(cli.code, Some(ERR_OPENCODE_SURFACE_UNDETERMINED));
let desktop = absent
.iter()
.find(|s| s.surface == "desktop")
.expect("desktop");
assert_eq!(desktop.state, SurfaceState::Absent);
assert_eq!(desktop.code, None);
let mut no_root = inputs(&host, None);
no_root.root = None;
let a = from_inputs(no_root);
assert_eq!(a.cli.state, SurfaceState::Undetermined);
assert_eq!(a.desktop.state, SurfaceState::Undetermined);
}
#[test]
fn a_foreign_file_and_other_plugins_are_named() {
let host = host();
let plugins = opencode::plugins_dir(&host.root);
std::fs::create_dir_all(&plugins).unwrap();
std::fs::write(
plugins.join("openlatch.js"),
"export default async () => ({})\n",
)
.unwrap();
std::fs::write(plugins.join("other.ts"), "x").unwrap();
std::fs::write(plugins.join("notes.md"), "x").unwrap();
std::fs::create_dir_all(host.root.join("plugin")).unwrap();
std::fs::write(host.root.join("plugin").join("legacy.js"), "x").unwrap();
let a = from_inputs(inputs(&host, None));
assert_eq!(a.plugin_file, PluginFileState::Foreign);
assert_eq!(a.to_json()["plugin_file"]["ours"], false);
let names: Vec<_> = a
.other_plugins
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(names, ["legacy.js", "other.ts"]);
}
#[test]
fn reachability_is_proven_by_a_live_event_and_failed_by_fallback_only() {
let host = host();
crate::hooks::opencode_plugin::install(&opencode::plugins_dir(&host.root), &host.ol)
.expect("install");
let logs = host.ol.join("logs");
std::fs::create_dir_all(&logs).unwrap();
let future = (Utc::now() + chrono::Duration::minutes(5)).to_rfc3339();
std::fs::write(
logs.join("fallback.jsonl"),
format!(
"{}\n",
serde_json::json!({"source": "opencode", "time": future, "data": {"opencode_hook": "plugin_loaded"}})
),
)
.unwrap();
assert_eq!(
from_inputs(inputs(&host, None)).reachability,
Reachability::Pending
);
std::fs::write(
logs.join("fallback.jsonl"),
format!(
"{}\n",
serde_json::json!({"source": "opencode", "time": future, "data": {"opencode_hook": "tool.execute.before"}})
),
)
.unwrap();
let a = from_inputs(inputs(&host, None));
assert_eq!(a.reachability, Reachability::Failed);
assert_eq!(
a.unreachable().map(|(c, _)| c),
Some(ERR_OPENCODE_HOOK_UNREACHABLE)
);
std::fs::write(
host.ol.join(crate::daemon::agent_liveness::FILE_NAME),
serde_json::json!({"opencode": future}).to_string(),
)
.unwrap();
assert_eq!(
from_inputs(inputs(&host, None)).reachability,
Reachability::Proven
);
}
#[test]
fn the_desktop_version_is_read_from_the_bundle_metadata() {
let dir = tempfile::tempdir().unwrap();
let app = dir.path().join("OpenCode.app");
std::fs::create_dir_all(app.join("Contents")).unwrap();
std::fs::write(
app.join("Contents").join("Info.plist"),
"<plist><dict><key>CFBundleName</key><string>OpenCode</string>\
<key>CFBundleShortVersionString</key>\n<string>1.18.35</string></dict></plist>",
)
.unwrap();
assert_eq!(desktop_version(&app).as_deref(), Some("1.18.35"));
assert_eq!(desktop_version(dir.path()), None);
}
fn relay(cli: RelayDelivery, traffic: bool, websocket: WebsocketMode) -> OpencodeRelay {
OpencodeRelay {
cli,
desktop: DesktopDelivery::Unwired(DesktopUnwired::NotInstalled),
desktop_line: DesktopLine::Absent,
desktop_traffic_proven: false,
traffic_proven: traffic,
last_traffic_ms: traffic.then_some(2_000),
since_daemon_start: true,
websocket,
tunnelled_hosts: vec!["llm.corp.test".into()],
loopback_hosts: vec!["localhost".into()],
unwired: None,
launchers_off_path: false,
}
}
#[test]
fn the_websocket_transport_is_read_from_the_channel_and_the_environment() {
assert!(is_prerelease("0.0.0-dev-202610092229"));
assert!(is_prerelease("opencode 0.0.0-beta-202608110357"));
assert!(!is_prerelease("1.18.35"));
assert!(!is_prerelease("v1.18.35+build.7"));
assert!(!is_prerelease(""));
assert_eq!(WebsocketMode::from_facts(None, false), WebsocketMode::Off);
assert_eq!(
WebsocketMode::from_facts(Some(""), false),
WebsocketMode::Off
);
assert_eq!(
WebsocketMode::from_facts(Some("false"), false),
WebsocketMode::Off
);
assert_eq!(
WebsocketMode::from_facts(Some("TRUE"), false),
WebsocketMode::DeveloperOn
);
assert_eq!(
WebsocketMode::from_facts(Some("false"), true),
WebsocketMode::Forced
);
}
#[test]
fn the_relay_findings_carry_their_codes() {
let codes = |r: &OpencodeRelay| -> Vec<(&str, &str)> {
r.findings()
.into_iter()
.map(|(f, _, c, _)| (f, c))
.collect()
};
let green = relay(RelayDelivery::Wired, true, WebsocketMode::Off);
assert_eq!(
codes(&green),
vec![],
"no Desktop installed: no Desktop finding (AGT-6 C-8 Doctor)"
);
let unproven = relay(RelayDelivery::Wired, false, WebsocketMode::Off);
assert!(codes(&unproven).contains(&("traffic", ERR_OPENCODE_RELAY_UNPROVEN)));
for mode in [WebsocketMode::DeveloperOn, WebsocketMode::Forced] {
assert!(codes(&relay(RelayDelivery::Wired, true, mode))
.contains(&("websocket", ERR_OPENCODE_RELAY_UNSUPPORTED)));
}
let off_path = OpencodeRelay {
launchers_off_path: true,
..relay(RelayDelivery::Unwired, false, WebsocketMode::Off)
};
let cli = off_path
.findings()
.into_iter()
.find(|(f, ..)| *f == "cli")
.expect("a cli finding");
assert_eq!(cli.1, "unwired");
assert_eq!(
cli.3, OPEN_NEW_TERMINAL_REMEDY,
"Windows: the launchers are not first on PATH"
);
let unwired = relay(RelayDelivery::Unwired, false, WebsocketMode::Off);
assert!(
!codes(&unwired).iter().any(|(f, _)| *f == "traffic"),
"an unwired CLI is the finding, not its traffic"
);
assert!(green
.findings()
.iter()
.chain(unproven.findings().iter())
.all(|(_, _, _, remedy)| !remedy.is_empty()));
}
#[test]
fn not_loaded_fires_on_traffic_since_the_write_or_a_suspected_bypass() {
let host = host();
let plugins = opencode::plugins_dir(&host.root);
crate::hooks::opencode_plugin::install(&plugins, &host.ol).expect("install");
let mut att = from_inputs(inputs(&host, None));
assert!(!att.plugin_seen);
assert!(att.not_loaded(Some(1_000)).is_none(), "no relay, no signal");
att.relay = Some(relay(RelayDelivery::Wired, true, WebsocketMode::Off));
let (code, detail) = att.not_loaded(Some(1_000)).expect("traffic since write");
assert_eq!(code, ERR_OPENCODE_PLUGIN_NOT_LOADED);
assert!(detail.contains("--pure"));
assert!(
att.not_loaded(Some(5_000)).is_none(),
"traffic before the write proves nothing"
);
att.relay = Some(relay(RelayDelivery::Wired, false, WebsocketMode::Off));
assert!(att.not_loaded(Some(1_000)).is_none());
att.bypass_suspected = Some(true);
let (_, detail) = att.not_loaded(Some(1_000)).expect("bypass");
assert!(detail.contains("bypass suspected"));
att.bypass_suspected = Some(false);
att.plugin_seen = true;
att.relay = Some(relay(RelayDelivery::Wired, true, WebsocketMode::Off));
assert!(att.not_loaded(Some(1_000)).is_none());
}
#[test]
fn the_json_carries_relay_and_bypass_suspected() {
let host = host();
let mut att = from_inputs(inputs(&host, Some(Ok("1.18.35".into()))));
let json = att.to_json();
assert!(json["relay"].is_null());
assert!(json["bypass_suspected"].is_null());
att.relay = Some(relay(RelayDelivery::Wired, true, WebsocketMode::Off));
att.bypass_suspected = Some(false);
let json = att.to_json();
assert_eq!(json["relay"]["cli"], "wired");
assert_eq!(json["relay"]["desktop"], "unwired");
assert_eq!(json["relay"]["desktop_line"], "absent");
assert_eq!(json["relay"]["traffic"], "proven");
assert_eq!(json["relay"]["websocket"], "off");
assert_eq!(
json["relay"]["tunnelled_hosts"],
serde_json::json!(["llm.corp.test"])
);
assert_eq!(
json["relay"]["loopback_hosts"],
serde_json::json!(["localhost"])
);
assert_eq!(json["bypass_suspected"], false);
assert!(att
.human_lines()
.iter()
.any(|l| l.contains("tunnelled and not captured: llm.corp.test")));
}
#[test]
fn configured_provider_hosts_split_tunnelled_from_loopback() {
let dir = tempfile::tempdir().unwrap();
let (root, repo) = (dir.path().join("root"), dir.path().join("repo"));
std::fs::create_dir_all(&root).unwrap();
std::fs::create_dir_all(&repo).unwrap();
std::fs::write(
root.join("opencode.json"),
r#"{"provider":{
"corp":{"options":{"baseURL":"https://user:pw@LLM.Corp.test:8443/v1?k=SECRET","apiKey":"SENTINEL_KEY"}},
"anthropic":{"options":{"baseURL":"https://api.anthropic.com/v1"}},
"ollama":{"options":{"baseURL":"http://127.0.0.1:11434/v1"}}
}}"#,
)
.unwrap();
std::fs::write(
repo.join("opencode.jsonc"),
"// repo config\n{\"provider\":{\"gw\":{\"options\":{\"baseURL\":\"https://gw.example.com/openai\"}},\"lm\":{\"options\":{\"baseURL\":\"http://localhost:1234/v1\"}},\"az\":{\"options\":{\"baseURL\":\"https://my-res.openai.azure.com/openai/v1\"}},\"br\":{\"options\":{\"baseURL\":\"https://bedrock-runtime.eu-west-1.amazonaws.com\"}},\"azx\":{\"options\":{\"baseURL\":\"https://a.b.openai.azure.com/v1\"}}}}",
)
.unwrap();
std::fs::write(repo.join("opencode.json"), "{ not json").unwrap();
let (tunnelled, loopback) = configured_provider_hosts(
Some(&root),
Some(&repo),
crate::hooks::bindings::opencode::OPENCODE_INTERCEPT_HOSTS,
crate::hooks::bindings::opencode::OPENCODE_INTERCEPT_PATTERNS,
);
assert_eq!(
tunnelled,
vec!["a.b.openai.azure.com", "gw.example.com", "llm.corp.test"]
);
assert_eq!(loopback, vec!["127.0.0.1", "localhost"]);
let all = format!("{tunnelled:?}{loopback:?}");
for leak in ["SENTINEL_KEY", "SECRET", "pw", "8443", "/v1"] {
assert!(!all.contains(leak), "{leak}");
}
}
fn facts(detection: Detection, service: Option<ServiceReport>, keys: bool) -> DesktopFacts {
DesktopFacts {
installed: true,
detection,
service,
relay_keys: keys,
relay_live: keys,
node_arming: None,
service_started_at: None,
}
}
fn found() -> Detection {
Detection::Found(crate::daemon::opencode_desktop_service::BundledCli {
path: PathBuf::from("/Applications/OpenCode.app/Contents/Resources/opencode-cli"),
version: "2.0.1".into(),
})
}
fn state(s: ServiceState) -> Option<ServiceReport> {
Some(ServiceReport::State(s))
}
#[test]
fn desktop_line_and_delivery_follow_the_facts() {
use DesktopDelivery as D;
let mut absent = facts(Detection::Absent, None, true);
absent.installed = false;
let cases: Vec<(DesktopFacts, DesktopLine, DesktopDelivery)> = vec![
(
absent,
DesktopLine::Absent,
D::Unwired(DesktopUnwired::NotInstalled),
),
(
facts(Detection::Absent, None, true),
DesktopLine::Node,
D::Wired,
),
(
facts(Detection::Absent, None, false),
DesktopLine::Node,
D::Unwired(DesktopUnwired::RelayStopped),
),
(
facts(found(), state(ServiceState::Armed), true),
DesktopLine::Service,
D::Wired,
),
(
facts(found(), state(ServiceState::Armed), false),
DesktopLine::Service,
D::Unwired(DesktopUnwired::RelayStopped),
),
(
facts(found(), state(ServiceState::Starting), true),
DesktopLine::Service,
D::Unarmed(UnarmedCause::AwaitingRespawn),
),
(
facts(
found(),
state(ServiceState::Failed("starting x: denied".into())),
true,
),
DesktopLine::Service,
D::Unwired(DesktopUnwired::ServiceFailed("starting x: denied".into())),
),
(
facts(found(), None, true),
DesktopLine::Service,
D::Unwired(DesktopUnwired::ServiceFailed(NO_REPORT_REASON.into())),
),
(
facts(
found(),
Some(ServiceReport::NotManaged(NotManaged::Elevated)),
true,
),
DesktopLine::Service,
D::Unwired(DesktopUnwired::ServiceFailed(ELEVATED_REASON.into())),
),
(
facts(
found(),
Some(ServiceReport::NotManaged(NotManaged::Isolated)),
true,
),
DesktopLine::Service,
D::Unsupported(ISOLATED_REASON.into()),
),
(
facts(Detection::Unsupported("flatpak"), None, true),
DesktopLine::Service,
D::Unsupported(sandboxed_reason("flatpak")),
),
];
let mut stale_node = facts(Detection::Absent, None, true);
stale_node.relay_live = false;
let mut stale_service = facts(found(), state(ServiceState::Armed), true);
stale_service.relay_live = false;
let mut held = facts(
found(),
state(ServiceState::Unarmed(UnarmedCause::ForeignAttached)),
false,
);
held.relay_live = true;
let mut held_armed = facts(found(), state(ServiceState::Armed), false);
held_armed.relay_live = true;
let cases = cases.into_iter().chain([
(
stale_node,
DesktopLine::Node,
D::Unwired(DesktopUnwired::RelayStopped),
),
(
stale_service,
DesktopLine::Service,
D::Unwired(DesktopUnwired::RelayStopped),
),
(
held,
DesktopLine::Service,
D::Unarmed(UnarmedCause::ForeignAttached),
),
(
held_armed,
DesktopLine::Service,
D::Unarmed(UnarmedCause::AwaitingRespawn),
),
]);
for (f, line, delivery) in cases {
let (l, d, _) = classify_desktop(&f, None);
assert_eq!((l, &d), (line, &delivery), "{f:?}");
}
for cause in [
UnarmedCause::ForeignAttached,
UnarmedCause::IdentityUnverified,
UnarmedCause::UpdateWindow,
UnarmedCause::AwaitingRespawn,
] {
let (_, d, _) = classify_desktop(
&facts(found(), state(ServiceState::Unarmed(cause)), true),
None,
);
assert_eq!(d, D::Unarmed(cause));
}
}
#[test]
fn desktop_traffic_is_a_correlation_in_time_per_line() {
let mut node = facts(Detection::Absent, None, true);
node.node_arming = Some(RelayArming {
relay_arming: "armed_in_process".into(),
runtime: "node".into(),
at_ms: 1_000,
});
assert!(classify_desktop(&node, Some(1_001)).2);
assert!(
!classify_desktop(&node, Some(1_000)).2,
"traffic must follow the arming"
);
assert!(!classify_desktop(&node, None).2);
node.node_arming.as_mut().unwrap().relay_arming = "not_armed".into();
assert!(!classify_desktop(&node, Some(5_000)).2);
let mut service = facts(found(), state(ServiceState::Armed), true);
service.service_started_at = Some(2_000);
assert!(classify_desktop(&service, Some(2_001)).2);
assert!(!classify_desktop(&service, Some(1_999)).2);
service.service = state(ServiceState::Unarmed(UnarmedCause::UpdateWindow));
assert!(
!classify_desktop(&service, Some(9_000)).2,
"only a wired Desktop is proven"
);
}
#[test]
fn the_service_report_round_trips() {
let all = [
ServiceReport::State(ServiceState::Absent),
ServiceReport::State(ServiceState::Unsupported("snap".into())),
ServiceReport::State(ServiceState::Starting),
ServiceReport::State(ServiceState::Armed),
ServiceReport::State(ServiceState::Unarmed(UnarmedCause::ForeignAttached)),
ServiceReport::State(ServiceState::Unarmed(UnarmedCause::IdentityUnverified)),
ServiceReport::State(ServiceState::Unarmed(UnarmedCause::UpdateWindow)),
ServiceReport::State(ServiceState::Unarmed(UnarmedCause::AwaitingRespawn)),
ServiceReport::State(ServiceState::Failed("why".into())),
ServiceReport::NotManaged(NotManaged::Isolated),
ServiceReport::NotManaged(NotManaged::Elevated),
];
for r in all {
assert_eq!(
ServiceReport::from_json(&r.to_json()),
Some(r.clone()),
"{r:?}"
);
}
for bad in [
serde_json::json!({}),
serde_json::json!({"state": "armed_maybe"}),
serde_json::json!({"state": "unarmed", "cause": "other"}),
serde_json::json!({"state": "failed"}),
] {
assert_eq!(ServiceReport::from_json(&bad), None, "{bad}");
}
}
#[test]
fn each_desktop_value_carries_its_code_and_remedy() {
let with = |desktop: DesktopDelivery, line: DesktopLine, proven: bool| OpencodeRelay {
desktop,
desktop_line: line,
desktop_traffic_proven: proven,
..relay(RelayDelivery::Wired, true, WebsocketMode::Off)
};
let desktop = |r: &OpencodeRelay| -> Vec<(&str, &str, &str, String)> {
r.findings()
.into_iter()
.filter(|(f, ..)| f.starts_with("desktop"))
.collect()
};
assert!(desktop(&with(DesktopDelivery::Wired, DesktopLine::Node, true)).is_empty());
let unproven = desktop(&with(DesktopDelivery::Wired, DesktopLine::Service, false));
assert_eq!(unproven.len(), 1);
assert_eq!(
(unproven[0].0, unproven[0].1, unproven[0].2),
("desktop_traffic", "unproven", ERR_OPENCODE_RELAY_UNPROVEN)
);
assert_eq!(unproven[0].3, DESKTOP_UNPROVEN_REMEDY);
for cause in [
UnarmedCause::ForeignAttached,
UnarmedCause::IdentityUnverified,
UnarmedCause::UpdateWindow,
UnarmedCause::AwaitingRespawn,
] {
let r = with(DesktopDelivery::Unarmed(cause), DesktopLine::Service, false);
let f = desktop(&r);
assert_eq!(f.len(), 1, "{cause:?}");
assert_eq!(
(f[0].1, f[0].2),
("unarmed", ERR_OPENCODE_DESKTOP_SERVICE_UNARMED)
);
assert!(f[0].3.starts_with(unarmed_cause_sentence(cause)));
assert!(f[0].3.ends_with(DESKTOP_UNARMED_REMEDY));
assert_eq!(r.to_json()["desktop_cause"], cause.as_str());
}
let failed = with(
DesktopDelivery::Unwired(DesktopUnwired::ServiceFailed(
"starting /opt/OpenCode/resources/opencode-cli: permission denied".into(),
)),
DesktopLine::Service,
false,
);
let f = desktop(&failed);
assert_eq!(
(f[0].1, f[0].2),
("unwired", ERR_OPENCODE_DESKTOP_SERVICE_FAILED)
);
assert!(f[0]
.3
.contains("/opt/OpenCode/resources/opencode-cli: permission denied"));
assert!(f[0].3.contains("OpenCode keeps working"));
let stopped = with(
DesktopDelivery::Unwired(DesktopUnwired::RelayStopped),
DesktopLine::Node,
false,
);
let f = desktop(&stopped);
assert_eq!(
(f[0].1, f[0].2),
("unwired", crate::error::ERR_MODEL_RELAY_PREFLIGHT_FAILED)
);
let unsupported = with(
DesktopDelivery::Unsupported(sandboxed_reason("snap")),
DesktopLine::Service,
false,
);
let f = desktop(&unsupported);
assert_eq!(
(f[0].1, f[0].2),
("unsupported", ERR_OPENCODE_RELAY_UNSUPPORTED)
);
assert!(f[0].3.contains("snap package"));
let absent = with(
DesktopDelivery::Unwired(DesktopUnwired::NotInstalled),
DesktopLine::Absent,
false,
);
assert!(desktop(&absent).is_empty());
let json = absent.to_json();
assert_eq!(json["desktop_line"], "absent");
assert!(json["desktop_cause"].is_null());
assert_eq!(json["desktop_traffic_basis"], DESKTOP_TRAFFIC_BASIS);
for r in [&failed, &stopped, &unsupported] {
for (.., remedy) in desktop(r) {
assert!(!remedy.contains("proxy"), "C-11: never \"proxy\": {remedy}");
}
}
}
}