use axum::body::Body;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, StatusCode, header};
use axum::response::{IntoResponse, Response};
use kanade_shared::bin_platform::{
LINUX_SUFFIX_AARCH64, LINUX_SUFFIX_X86_64, MACOS_SUFFIX_AARCH64, platform_of_key,
};
use kanade_shared::kv::OBJECT_AGENT_RELEASES;
use kanade_shared::wire::ServerSettings;
use serde::Deserialize;
use tokio::io::AsyncReadExt;
use tracing::{info, warn};
use super::AppState;
use crate::audit;
use crate::audit::Caller;
const AGENT_TOML_TEMPLATE: &str = include_str!("../../assets/agent.toml");
const DEPLOY_AGENT_PS1: &str = include_str!("../../assets/deploy-agent.ps1");
const SETUP_AGENT_SH: &str = include_str!("../../assets/setup-agent.sh");
const AGENT_SERVICE: &str = include_str!("../../assets/kanade-agent.service");
const SETUP_AGENT_MACOS_SH: &str = include_str!("../../assets/setup-agent-macos.sh");
const AGENT_PLIST: &str = include_str!("../../assets/com.kanade.agent.plist");
const NATS_URL_LINE: &str = "nats_url = 'nats://127.0.0.1:4222'";
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
enum InstallerOs {
#[default]
Windows,
Linux,
Macos,
}
impl InstallerOs {
fn as_str(&self) -> &'static str {
match self {
InstallerOs::Windows => "windows",
InstallerOs::Linux => "linux",
InstallerOs::Macos => "macos",
}
}
}
#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
#[serde(rename_all = "lowercase")]
enum InstallerArch {
#[default]
X86_64,
Aarch64,
}
impl InstallerArch {
fn as_str(&self) -> &'static str {
match self {
InstallerArch::X86_64 => "x86_64",
InstallerArch::Aarch64 => "aarch64",
}
}
fn linux_suffix(&self) -> &'static str {
match self {
InstallerArch::X86_64 => LINUX_SUFFIX_X86_64,
InstallerArch::Aarch64 => LINUX_SUFFIX_AARCH64,
}
}
}
#[derive(Deserialize, Debug, Default)]
pub struct InstallerParams {
#[serde(default)]
os: InstallerOs,
#[serde(default)]
arch: Option<InstallerArch>,
}
impl InstallerParams {
fn resolve_arch(&self) -> Result<InstallerArch, (StatusCode, String)> {
match (self.os, self.arch) {
(InstallerOs::Macos, Some(InstallerArch::X86_64)) => Err((
StatusCode::BAD_REQUEST,
"Intel (x86_64) Macs are not supported — kanade supports Apple Silicon (arm64) \
Macs only; use arch=aarch64"
.to_string(),
)),
(InstallerOs::Macos, None) => Ok(InstallerArch::Aarch64),
(_, arch) => Ok(arch.unwrap_or_default()),
}
}
}
pub async fn installer(
State(state): State<AppState>,
caller: Caller,
Query(params): Query<InstallerParams>,
) -> Result<Response, (StatusCode, String)> {
let arch = params.resolve_arch()?;
let store = state
.jetstream
.get_object_store(OBJECT_AGENT_RELEASES)
.await
.map_err(|e| {
warn!(error = %e, "get_object_store agent_releases");
(
StatusCode::SERVICE_UNAVAILABLE,
format!(
"Object Store '{OBJECT_AGENT_RELEASES}' not found — the backend creates it at startup, so it was removed since or this broker is not the one expected; check `GET /api/jetstream/status` (restarting the backend recreates it)"
),
)
})?;
let key = {
let metas = crate::projector::object_meta::list_bucket(&state.pool, OBJECT_AGENT_RELEASES)
.await
.map_err(|e| {
warn!(error = %e, "object_store_meta list agent_releases");
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?;
let rows: Vec<(String, Option<String>)> =
metas.into_iter().map(|m| (m.key, m.modified)).collect();
match latest_key_for_platform(&rows, params.os, arch) {
Some(k) => k,
None => {
let label = match params.os {
InstallerOs::Windows => "windows".to_string(),
InstallerOs::Linux => format!("linux-{}", arch.as_str()),
InstallerOs::Macos => format!("macos-{}", arch.as_str()),
};
return Err((
StatusCode::NOT_FOUND,
format!(
"no {label} agent releases in {OBJECT_AGENT_RELEASES} — publish one first \
(`kanade agent publish`)"
),
));
}
}
};
check_version(&key)?;
let settings = super::server_settings::load(&state).await.map_err(|e| {
warn!(error = %format!("{e:#}"), "read server_settings for installer");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("read server_settings: {e:#}"),
)
})?;
let (nats_url, nats_token) = resolve_nats(&settings, &state.nats_url)?;
let agent_toml = render_agent_toml(&nats_url)?;
let mut obj = store.get(&key).await.map_err(|e| {
let msg = e.to_string();
if msg.contains("not found") || msg.contains("no objects") {
return (
StatusCode::NOT_FOUND,
format!("release '{key}' not in Object Store"),
);
}
warn!(error = %e, %key, "object_store.get");
(StatusCode::INTERNAL_SERVER_ERROR, msg)
})?;
let mut exe = Vec::with_capacity(obj.info().size);
obj.read_to_end(&mut exe).await.map_err(|e| {
warn!(error = %e, %key, "read agent binary");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("read agent binary '{key}': {e}"),
)
})?;
let keyring = state.commands.keyring_entry();
let command_keys = match &keyring {
Some(entry) => Some(serde_json::to_string(&vec![entry]).map_err(|e| {
warn!(error = %e, "serialize command keyring");
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
})?),
None => None,
};
let enforcement_requested = settings
.agent_install
.as_ref()
.and_then(|ai| ai.require_signed_commands)
.unwrap_or(false);
let enforcement = resolve_enforcement(enforcement_requested, keyring.is_some());
if enforcement == EnforcementPlan::RequestedButNoKey {
warn!(
%key,
"installer: require_signed_commands is set but this backend has no command-signing \
key — the generated installer will not enable enforcement"
);
}
let (content_type, filename, payload, command_keys_embedded) = match params.os {
InstallerOs::Windows => {
let install_ps1 = render_install_ps1(
&key,
nats_token.as_deref(),
command_keys.as_deref(),
enforcement == EnforcementPlan::Embedded,
);
let install_cmd = render_install_cmd(&key);
let readme = render_readme(&key, command_keys.is_some(), enforcement);
let entries: Vec<(&str, Vec<u8>)> = vec![
("kanade-agent.exe", exe),
("agent.toml", agent_toml.into_bytes()),
("deploy-agent.ps1", DEPLOY_AGENT_PS1.as_bytes().to_vec()),
("install-agent.ps1", install_ps1.into_bytes()),
("install.cmd", install_cmd.into_bytes()),
("README.txt", readme.into_bytes()),
];
let zip_bytes = tokio::task::spawn_blocking(move || build_zip(entries))
.await
.map_err(|e| {
warn!(error = %e, "installer zip task join");
(StatusCode::INTERNAL_SERVER_ERROR, format!("zip task: {e}"))
})?
.map_err(|e| {
warn!(error = %e, "build installer zip");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("build installer zip: {e}"),
)
})?;
(
"application/zip",
format!("kanade-agent-installer-{key}.zip"),
zip_bytes,
command_keys.is_some(),
)
}
InstallerOs::Linux | InstallerOs::Macos => {
let entries = unix_tar_entries(
params.os,
&key,
exe,
agent_toml,
nats_token.as_deref(),
command_keys.as_deref(),
enforcement,
);
let tgz_bytes = tokio::task::spawn_blocking(move || build_tar_gz(entries))
.await
.map_err(|e| {
warn!(error = %e, "installer tar task join");
(StatusCode::INTERNAL_SERVER_ERROR, format!("tar task: {e}"))
})?
.map_err(|e| {
warn!(error = %e, "build installer tarball");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("build installer tarball: {e}"),
)
})?;
info!(%key, arch = arch.as_str(), "installer: tar.gz generated");
(
"application/gzip",
format!("kanade-agent-installer-{key}.tar.gz"),
tgz_bytes,
command_keys.is_some(),
)
}
};
info!(%key, os = params.os.as_str(), nats_url = %nats_url, "installer: archive generated");
audit::record(
&state.nats,
"operator",
"agent_installer_download",
Some(&key),
Some(&caller),
serde_json::json!({
"version": key,
"os": params.os.as_str(),
"arch": arch.as_str(),
"nats_url": nats_url,
"token_embedded": nats_token.is_some(),
"command_keys_embedded": command_keys_embedded,
}),
)
.await;
Ok((
[
(header::CONTENT_TYPE, content_type.to_string()),
(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{filename}\""),
),
],
Body::from(payload),
)
.into_response())
}
pub async fn installer_ps1(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Response, (StatusCode, String)> {
let (base, token) = script_context(&state, &headers)?;
Ok((
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
render_installer_ps1(&base, &token),
)
.into_response())
}
pub async fn installer_sh(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Response, (StatusCode, String)> {
let (base, token) = script_context(&state, &headers)?;
Ok((
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],
render_installer_sh(&base, &token),
)
.into_response())
}
fn script_context(
state: &AppState,
headers: &HeaderMap,
) -> Result<(String, String), (StatusCode, String)> {
let base = super::password_setup::link_base(state.public_url.as_deref(), headers).ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
"cannot derive the backend's base URL (no [server] public_url configured and no Host \
header on the request) — set public_url in backend.toml"
.to_string(),
))?;
let token = bearer_token(headers).ok_or((
StatusCode::BAD_REQUEST,
"script generation requires a Bearer Authorization header".to_string(),
))?;
Ok((base, token.to_string()))
}
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
headers
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.filter(|t| !t.is_empty())
}
fn render_installer_ps1(base: &str, token: &str) -> String {
let zip_url = ps_quote(&format!("{base}/api/agents/installer"));
let auth = ps_quote(&format!("Bearer {token}"));
let mut s = String::new();
s.push_str(
"# kanade-agent one-liner installer (generated by kanade-backend — do not edit)\r\n",
);
s.push_str("# Installs the latest agent as a Windows service. The embedded token expires\r\n");
s.push_str("# with the issuer's session.\r\n");
s.push_str("$ErrorActionPreference = 'Stop'\r\n");
s.push_str("$tmp = Join-Path $env:TEMP ('kanade-agent-install-' + [guid]::NewGuid())\r\n");
s.push_str("New-Item -ItemType Directory -Path $tmp | Out-Null\r\n");
s.push_str("$zip = Join-Path $tmp 'installer.zip'\r\n");
s.push_str("try {\r\n");
s.push_str(&format!(
" Invoke-WebRequest -Uri {zip_url} -Headers @{{ Authorization = {auth} }} -OutFile $zip\r\n"
));
s.push_str(" Expand-Archive -Path $zip -DestinationPath $tmp\r\n");
s.push_str(" $installScript = Join-Path $tmp 'install-agent.ps1'\r\n");
s.push_str(" $isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)\r\n");
s.push_str(" if ($isAdmin) {\r\n");
s.push_str(" & $installScript\r\n");
s.push_str(" $code = $LASTEXITCODE\r\n");
s.push_str(" } else {\r\n");
s.push_str(" # Elevate ONLY the install step (UAC prompt); the download above\r\n");
s.push_str(
" # already ran unelevated, so the token never reaches an admin process.\r\n",
);
s.push_str(" $proc = Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File',\"`\"$installScript`\"\" -Wait -PassThru\r\n");
s.push_str(" $code = $proc.ExitCode\r\n");
s.push_str(" }\r\n");
s.push_str(" if ($code -ne 0) { throw \"install failed (exit $code)\" }\r\n");
s.push_str(" Write-Host \"kanade-agent installed.\"\r\n");
s.push_str(" Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue\r\n");
s.push_str("} catch {\r\n");
s.push_str(" # Covers every failure path (download, extract, admin-direct throw,\r\n");
s.push_str(" # and the exit-code check above): $tmp survives so there's something\r\n");
s.push_str(" # to inspect, and install.log is named only once it actually exists.\r\n");
s.push_str(" $log = Join-Path $tmp 'install.log'\r\n");
s.push_str(
" $hint = if (Test-Path $log) { \" See $log for details.\" } else { \" Installer files kept at $tmp.\" }\r\n",
);
s.push_str(" throw \"$($_.Exception.Message)$hint\"\r\n");
s.push_str("}\r\n");
s
}
fn render_installer_sh(base: &str, token: &str) -> String {
let url_os = sh_quote(&format!("{base}/api/agents/installer?os="));
let url_arch = sh_quote("&arch=");
let token_cfg = token.replace('\\', "\\\\").replace('"', "\\\"");
let mut s = String::new();
s.push_str("#!/bin/sh\n");
s.push_str("# kanade-agent one-liner installer (generated by kanade-backend — do not edit)\n");
s.push_str("# Meant to be run via the one-liner on the Agent Install page\n");
s.push_str(&format!(
"# ({base}/agent-install — piped through `sudo bash`).\n"
));
s.push_str("# Linux (systemd) and macOS (launchd) alike.\n");
s.push_str("# install.sh needs root; without the sudo pipe it fails there by design.\n");
s.push_str("# The embedded token expires with the issuer's session.\n");
s.push_str("set -eu\n");
s.push_str("case \"$(uname -s)\" in\n");
s.push_str(" Linux)\n");
s.push_str(" OS=linux\n");
s.push_str(" case \"$(uname -m)\" in\n");
s.push_str(" x86_64) ARCH=x86_64 ;;\n");
s.push_str(" aarch64|arm64) ARCH=aarch64 ;;\n");
s.push_str(" *) echo \"unsupported architecture: $(uname -m)\" >&2; exit 1 ;;\n");
s.push_str(" esac\n");
s.push_str(" ;;\n");
s.push_str(" Darwin)\n");
s.push_str(" OS=macos\n");
s.push_str(
" if [ \"$(sysctl -n hw.optional.arm64 2>/dev/null || true)\" = \"1\" ]; then\n",
);
s.push_str(" ARCH=aarch64\n");
s.push_str(" else\n");
s.push_str(" echo \"Intel Macs are not supported — kanade supports Apple Silicon Macs only\" >&2\n");
s.push_str(" exit 1\n");
s.push_str(" fi\n");
s.push_str(" ;;\n");
s.push_str(" *) echo \"unsupported OS: $(uname -s)\" >&2; exit 1 ;;\n");
s.push_str("esac\n");
s.push_str("TMP=\"$(mktemp -d)\"\n");
s.push_str("trap 'rm -rf \"$TMP\"' EXIT\n");
s.push_str(&format!(
"curl -fsSL -K - -o \"$TMP/installer.tar.gz\" {url_os}\"$OS\"{url_arch}\"$ARCH\" <<'KANADE_CURL_CONFIG'\n"
));
s.push_str(&format!("header = \"Authorization: Bearer {token_cfg}\"\n"));
s.push_str("KANADE_CURL_CONFIG\n");
s.push_str("tar xzf \"$TMP/installer.tar.gz\" -C \"$TMP\"\n");
s.push_str("sh \"$TMP/install.sh\"\n");
s.push_str("echo \"kanade-agent installed.\"\n");
s
}
fn latest_key_for_platform(
rows: &[(String, Option<String>)],
os: InstallerOs,
arch: InstallerArch,
) -> Option<String> {
let mut matches: Vec<&(String, Option<String>)> = rows
.iter()
.filter(|(key, _)| key_matches_platform(key, os, arch))
.collect();
matches.sort_by(|a, b| b.1.cmp(&a.1));
matches.first().map(|(k, _)| k.clone())
}
fn key_matches_platform(key: &str, os: InstallerOs, arch: InstallerArch) -> bool {
match os {
InstallerOs::Windows => platform_of_key(key) == "windows",
InstallerOs::Linux => key.ends_with(arch.linux_suffix()),
InstallerOs::Macos => arch == InstallerArch::Aarch64 && key.ends_with(MACOS_SUFFIX_AARCH64),
}
}
fn resolve_nats(
settings: &ServerSettings,
backend_url: &str,
) -> Result<(String, Option<String>), (StatusCode, String)> {
let Some(ai) = settings.agent_install.as_ref() else {
return Ok((backend_url.to_string(), None));
};
let url = match ai.nats_url.as_deref() {
Some(u) if u.is_empty() || u.contains('\'') || u.contains('\n') || u.contains('\r') => {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"server_settings agent_install.nats_url is invalid (must be non-empty, with no \
single quote or newline) — fix it on the Settings page"
.into(),
));
}
Some(u) => u.to_string(),
None => backend_url.to_string(),
};
let token = ai.nats_token.clone().filter(|t| !t.is_empty());
Ok((url, token))
}
fn check_version(version: &str) -> Result<(), (StatusCode, String)> {
kanade_shared::bin_platform::check_release_key(version)
.map_err(|e| (StatusCode::BAD_REQUEST, e))
}
fn render_agent_toml(nats_url: &str) -> Result<String, (StatusCode, String)> {
if nats_url.is_empty() {
return Err((StatusCode::BAD_REQUEST, "nats_url must not be empty".into()));
}
if nats_url.contains('\'') || nats_url.contains('\n') || nats_url.contains('\r') {
return Err((
StatusCode::BAD_REQUEST,
"nats_url must not contain a single quote or newline (TOML literal-string safety)"
.into(),
));
}
if !AGENT_TOML_TEMPLATE.contains(NATS_URL_LINE) {
warn!("configs/agent.toml no longer contains the expected nats_url line");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
format!(
"configs/agent.toml template changed — expected line `{NATS_URL_LINE}` not found; \
update the installer rewrite to match"
),
));
}
Ok(AGENT_TOML_TEMPLATE.replacen(NATS_URL_LINE, &format!("nats_url = '{nats_url}'"), 1))
}
fn ps_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EnforcementPlan {
Off,
Embedded,
RequestedButNoKey,
}
fn resolve_enforcement(requested: bool, key_present: bool) -> EnforcementPlan {
match (requested, key_present) {
(false, _) => EnforcementPlan::Off,
(true, true) => EnforcementPlan::Embedded,
(true, false) => EnforcementPlan::RequestedButNoKey,
}
}
fn render_install_ps1(
version: &str,
nats_token: Option<&str>,
command_keys: Option<&str>,
require_signed_commands: bool,
) -> String {
let mut args = String::new();
if let Some(token) = nats_token {
args.push_str(&format!(" -NatsToken {}", ps_quote(token)));
}
if let Some(keys) = command_keys {
args.push_str(&format!(" -CommandKeys {}", ps_quote(keys)));
}
if require_signed_commands {
args.push_str(" -RequireSignedCommands");
}
format!(
"# Generated by kanade-backend — do not edit.\r\n\
# Installs kanade-agent {version} as a Windows service. Run as Administrator.\r\n\
$ErrorActionPreference = 'Stop'\r\n\
Start-Transcript -Path (Join-Path $PSScriptRoot 'install.log') -Force | Out-Null\r\n\
try {{\r\n\
\x20 & (Join-Path $PSScriptRoot 'deploy-agent.ps1'){args}\r\n\
\x20 exit $LASTEXITCODE\r\n\
}} finally {{\r\n\
\x20 Stop-Transcript | Out-Null\r\n\
}}\r\n"
)
}
fn render_install_cmd(version: &str) -> String {
let mut s = String::new();
s.push_str("@echo off\r\n");
s.push_str(&format!(
"REM kanade-agent installer (generated by kanade-backend). Installs kanade-agent {version}.\r\n"
));
s.push_str(
"powershell -NoProfile -ExecutionPolicy Bypass -File \"%~dp0install-agent.ps1\"\r\n",
);
s.push_str("if errorlevel 1 (\r\n");
s.push_str(" echo.\r\n");
s.push_str(" echo INSTALL FAILED - see the output above.\r\n");
s.push_str(" pause\r\n");
s.push_str(" exit /b 1\r\n");
s.push_str(")\r\n");
s.push_str("echo.\r\n");
s.push_str(&format!("echo kanade-agent {version} installed.\r\n"));
s.push_str("pause\r\n");
s
}
fn render_readme(version: &str, signing: bool, enforcement: EnforcementPlan) -> String {
let signing_note = if signing {
"This ZIP embeds the backend's command-signing PUBLIC key (provisioned\r\n\
into the agent's keyring by the installer, so signed commands verify\r\n\
from first boot). Public keys are not secrets. Break-glass keys are\r\n\
NEVER included in this ZIP — distribute those separately.\r\n"
} else {
"The backend that generated this ZIP is not signing commands, so no\r\n\
command-signing keyring is provisioned by the installer.\r\n"
};
let enforcement_note = match enforcement {
EnforcementPlan::Off => "",
EnforcementPlan::Embedded => {
"\r\nThis installer passes -RequireSignedCommands: the agent will REFUSE any\r\n\
command it cannot verify against the embedded keyring, starting from\r\n\
first boot.\r\n"
}
EnforcementPlan::RequestedButNoKey => {
"\r\n*** WARNING: RequireSignedCommands was requested in server settings, but\r\n\
*** this backend has no command-signing key configured, so this installer\r\n\
*** does NOT enable enforcement — an agent enforcing against an empty\r\n\
*** keyring would just decline to enforce, which is not what was asked for.\r\n\
*** Run `kanade-backend command-key-generate` on the backend host, then\r\n\
*** re-download this installer.\r\n"
}
};
let mut s = String::new();
s.push_str(&format!("kanade-agent installer (version {version})\r\n"));
s.push_str("==========================================\r\n");
s.push_str("\r\n");
s.push_str("Contents:\r\n");
s.push_str("\r\n");
s.push_str(&format!(
" kanade-agent.exe the agent binary (release {version})\r\n"
));
s.push_str(" agent.toml agent configuration (NATS URL baked in)\r\n");
s.push_str(" deploy-agent.ps1 the canonical install/update script\r\n");
s.push_str(" install-agent.ps1 generated wrapper (tokens and keys baked in)\r\n");
s.push_str(" install.cmd double-click bootstrap\r\n");
s.push_str(" README.txt this file\r\n");
s.push_str("\r\n");
s.push_str("Install:\r\n");
s.push_str("\r\n");
s.push_str(" 1. Extract this ZIP to a folder on the target PC.\r\n");
s.push_str(" 2. Right-click install.cmd and choose \"Run as administrator\".\r\n");
s.push_str("\r\n");
s.push_str("Re-running the installer upgrades the agent in place.\r\n");
s.push_str("\r\n");
s.push_str(signing_note);
s.push_str(enforcement_note);
s
}
fn build_zip(entries: Vec<(&str, Vec<u8>)>) -> Result<Vec<u8>, zip::result::ZipError> {
use std::io::Write as _;
use zip::write::SimpleFileOptions;
let mut zw = zip::ZipWriter::new(std::io::Cursor::new(Vec::<u8>::new()));
let opts = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
for (name, data) in entries {
zw.start_file(name, opts)?;
zw.write_all(&data)?;
}
Ok(zw.finish()?.into_inner())
}
struct TarEntry {
name: &'static str,
mode: u32,
data: Vec<u8>,
}
impl TarEntry {
fn new(name: &'static str, mode: u32, data: Vec<u8>) -> Self {
Self { name, mode, data }
}
}
fn unix_tar_entries(
os: InstallerOs,
key: &str,
exe: Vec<u8>,
agent_toml: String,
nats_token: Option<&str>,
command_keys: Option<&str>,
enforcement: EnforcementPlan,
) -> Vec<TarEntry> {
let (service_entry, setup_name, setup_script, service_kind, readme) =
if os == InstallerOs::Macos {
(
TarEntry::new(
"launchd/com.kanade.agent.plist",
0o644,
AGENT_PLIST.as_bytes().to_vec(),
),
"setup-agent-macos.sh",
SETUP_AGENT_MACOS_SH,
"a launchd daemon",
render_readme_macos(key, command_keys.is_some(), enforcement),
)
} else {
(
TarEntry::new(
"systemd/kanade-agent.service",
0o644,
AGENT_SERVICE.as_bytes().to_vec(),
),
"setup-agent.sh",
SETUP_AGENT_SH,
"a systemd service",
render_readme_linux(key, command_keys.is_some(), enforcement),
)
};
let install_sh = render_install_sh(
nats_token,
command_keys,
enforcement == EnforcementPlan::Embedded,
setup_name,
service_kind,
);
vec![
TarEntry::new("bin/kanade-agent", 0o755, exe),
TarEntry::new("etc/agent.toml", 0o644, agent_toml.into_bytes()),
service_entry,
TarEntry::new(setup_name, 0o755, setup_script.as_bytes().to_vec()),
TarEntry::new("install.sh", 0o755, install_sh.into_bytes()),
TarEntry::new("README.txt", 0o644, readme.into_bytes()),
]
}
fn build_tar_gz(entries: Vec<TarEntry>) -> std::io::Result<Vec<u8>> {
let enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
let mut builder = tar::Builder::new(enc);
for entry in &entries {
let mut header = tar::Header::new_gnu();
header.set_size(entry.data.len() as u64);
header.set_mode(entry.mode);
header.set_mtime(0);
header.set_cksum();
builder.append_data(&mut header, entry.name, entry.data.as_slice())?;
}
let enc = builder.into_inner()?;
enc.finish()
}
fn sh_quote(value: &str) -> String {
format!("'{}'", value.replace('\'', "'\\''"))
}
fn render_install_sh(
nats_token: Option<&str>,
command_keys: Option<&str>,
require_signed_commands: bool,
setup_script: &str,
service: &str,
) -> String {
let mut s = String::new();
s.push_str("#!/bin/sh\n");
s.push_str("# Generated by kanade-backend — do not edit.\n");
s.push_str(&format!(
"# Installs kanade-agent as {service}. Run as root (sudo).\n"
));
s.push_str("set -eu\n");
s.push_str("cd \"$(dirname \"$0\")\"\n");
if let Some(token) = nats_token {
s.push_str(&format!("export KANADE_NATS_TOKEN={}\n", sh_quote(token)));
}
if let Some(keys) = command_keys {
s.push_str(&format!("export KANADE_COMMAND_KEYS={}\n", sh_quote(keys)));
}
if require_signed_commands {
s.push_str("export KANADE_REQUIRE_SIGNED_COMMANDS=1\n");
}
s.push_str(&format!("exec ./{setup_script}\n"));
s
}
fn unix_signing_note(signing: bool, enforcement: EnforcementPlan) -> String {
let mut s = String::new();
if signing {
s.push_str(
"This tarball embeds the backend's command-signing PUBLIC key (provisioned\n\
into the agent's keyring by the installer, so signed commands verify\n\
from first boot). Public keys are not secrets. Break-glass keys are\n\
NEVER included in this tarball — distribute those separately.\n",
);
} else {
s.push_str(
"The backend that generated this tarball is not signing commands, so no\n\
command-signing keyring is provisioned by the installer.\n",
);
}
match enforcement {
EnforcementPlan::Off => {}
EnforcementPlan::Embedded => s.push_str(
"\nThis installer sets KANADE_REQUIRE_SIGNED_COMMANDS=1: the agent will REFUSE\n\
any command it cannot verify against the embedded keyring, starting from\n\
first boot.\n",
),
EnforcementPlan::RequestedButNoKey => s.push_str(
"\n*** WARNING: RequireSignedCommands was requested in server settings, but\n\
*** this backend has no command-signing key configured, so this installer\n\
*** does NOT enable enforcement — an agent enforcing against an empty\n\
*** keyring would just decline to enforce, which is not what was asked for.\n\
*** Run `kanade-backend command-key-generate` on the backend host, then\n\
*** re-download this installer.\n",
),
}
s
}
fn render_readme_linux(key: &str, signing: bool, enforcement: EnforcementPlan) -> String {
let mut s = String::new();
s.push_str(&format!("kanade-agent installer (release {key})\n"));
s.push_str("=================================================\n");
s.push('\n');
s.push_str("Contents:\n");
s.push('\n');
s.push_str(" bin/kanade-agent the agent binary\n");
s.push_str(" etc/agent.toml agent configuration (NATS URL baked in)\n");
s.push_str(" systemd/kanade-agent.service the systemd unit\n");
s.push_str(" setup-agent.sh the canonical install/update script\n");
s.push_str(" install.sh generated wrapper (token baked in, if any)\n");
s.push_str(" README.txt this file\n");
s.push('\n');
s.push_str("Install:\n");
s.push('\n');
s.push_str(" 1. Extract this tarball on the target machine and enter the\n");
s.push_str(" directory, e.g.:\n");
s.push_str(" mkdir kanade-agent-installer && cd kanade-agent-installer\n");
s.push_str(&format!(
" tar xzf ../kanade-agent-installer-{key}.tar.gz\n"
));
s.push_str(" 2. Run the installer as root:\n");
s.push_str(" sudo ./install.sh\n");
s.push('\n');
s.push_str("Re-running the installer upgrades the agent in place (an existing\n");
s.push_str("/etc/kanade/agent.env token and broker URL are preserved).\n");
s.push('\n');
s.push_str(&unix_signing_note(signing, enforcement));
s
}
fn render_readme_macos(key: &str, signing: bool, enforcement: EnforcementPlan) -> String {
let mut s = String::new();
s.push_str(&format!("kanade-agent installer (release {key})\n"));
s.push_str("=================================================\n");
s.push('\n');
s.push_str("Contents:\n");
s.push('\n');
s.push_str(" bin/kanade-agent the agent binary\n");
s.push_str(" etc/agent.toml agent configuration (NATS URL baked in)\n");
s.push_str(" launchd/com.kanade.agent.plist the LaunchDaemon definition\n");
s.push_str(" setup-agent-macos.sh the canonical install/update script\n");
s.push_str(" install.sh generated wrapper (token baked in, if any)\n");
s.push_str(" README.txt this file\n");
s.push('\n');
s.push_str("Install:\n");
s.push('\n');
s.push_str(" 1. Extract this tarball on the target Mac and enter the directory,\n");
s.push_str(" e.g. in Terminal:\n");
s.push_str(" mkdir kanade-agent-installer && cd kanade-agent-installer\n");
s.push_str(&format!(
" tar xzf ../kanade-agent-installer-{key}.tar.gz\n"
));
s.push_str(" 2. Run the installer as root:\n");
s.push_str(" sudo ./install.sh\n");
s.push('\n');
s.push_str("The agent runs as the LaunchDaemon com.kanade.agent\n");
s.push_str("(/Library/LaunchDaemons/com.kanade.agent.plist), binary at\n");
s.push_str("/usr/local/bin/kanade-agent, config + token under /etc/kanade, logs\n");
s.push_str("under /var/log/kanade. Check it with:\n");
s.push_str(" sudo launchctl print system/com.kanade.agent\n");
s.push('\n');
s.push_str("Re-running the installer upgrades the agent in place (an existing\n");
s.push_str("/etc/kanade/agent.env token and broker URL are preserved).\n");
s.push('\n');
s.push_str("Gatekeeper: the agent binary is not notarized. A tarball downloaded\n");
s.push_str("with a browser carries the com.apple.quarantine attribute; the\n");
s.push_str("installer clears it from the installed binary, and launchd runs the\n");
s.push_str("unsigned command-line binary without a Gatekeeper prompt. Apple\n");
s.push_str("Silicon still requires at least an ad-hoc code signature, which the\n");
s.push_str("Rust toolchain's linker applies; a binary modified after the build\n");
s.push_str("must be re-signed (codesign --force --sign - <binary>).\n");
s.push('\n');
s.push_str(&unix_signing_note(signing, enforcement));
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assets_match_the_workspace_originals() {
for (copy, original, path) in [
(
AGENT_TOML_TEMPLATE,
include_str!("../../../../configs/agent.toml"),
"configs/agent.toml",
),
(
DEPLOY_AGENT_PS1,
include_str!("../../../../scripts/deploy/agent.ps1"),
"scripts/deploy/agent.ps1",
),
(
SETUP_AGENT_SH,
include_str!("../../../../deploy/linux/setup-agent.sh"),
"deploy/linux/setup-agent.sh",
),
(
AGENT_SERVICE,
include_str!("../../../../deploy/linux/systemd/kanade-agent.service"),
"deploy/linux/systemd/kanade-agent.service",
),
(
SETUP_AGENT_MACOS_SH,
include_str!("../../../../deploy/macos/setup-agent.sh"),
"deploy/macos/setup-agent.sh",
),
(
AGENT_PLIST,
include_str!("../../../../deploy/macos/launchd/com.kanade.agent.plist"),
"deploy/macos/launchd/com.kanade.agent.plist",
),
] {
assert_eq!(
copy, original,
"crates/kanade-backend/assets/ has drifted from {path} — copy it across",
);
}
}
#[test]
fn agent_toml_url_line_is_rewritten() {
let out = render_agent_toml("nats://broker.corp:4222").unwrap();
assert!(out.contains("nats_url = 'nats://broker.corp:4222'"));
assert!(!out.contains(NATS_URL_LINE));
let template_lines: Vec<&str> = AGENT_TOML_TEMPLATE.lines().collect();
let out_lines: Vec<&str> = out.lines().collect();
assert_eq!(template_lines.len(), out_lines.len());
let differing = template_lines
.iter()
.zip(&out_lines)
.filter(|(a, b)| a != b)
.count();
assert_eq!(differing, 1);
}
#[test]
fn version_charset_is_restricted() {
for bad in [
"",
"0.43.99\n evil",
"0.43.99\revil",
"0.43.99\"x",
"0.43.99'x",
"0.43.99&del",
"0.43.99%PATH%",
] {
let (code, _) = check_version(bad).unwrap_err();
assert_eq!(code, StatusCode::BAD_REQUEST, "{bad:?}");
}
for good in ["0.43.99", "0.43.99-rc.1+build.5", "1.0.0_alpha"] {
check_version(good).unwrap();
}
}
#[test]
fn agent_toml_rejects_literal_string_injection() {
for bad in [
"nats://evil'\n[agent]\nid='x'",
"nats://a\nb",
"nats://a\rb",
] {
let (code, _) = render_agent_toml(bad).unwrap_err();
assert_eq!(code, StatusCode::BAD_REQUEST, "{bad:?}");
}
let (code, _) = render_agent_toml("").unwrap_err();
assert_eq!(code, StatusCode::BAD_REQUEST);
}
#[test]
fn installer_ps1_embeds_base_and_token_crlf_only() {
let out = render_installer_ps1("https://kanade.example.com", "tok-123");
assert!(
out.contains(
"Invoke-WebRequest -Uri 'https://kanade.example.com/api/agents/installer'"
)
);
assert!(out.contains("-Headers @{ Authorization = 'Bearer tok-123' }"));
assert!(out.contains("Expand-Archive"));
assert!(out.contains("-Verb RunAs"));
assert!(out.contains("$LASTEXITCODE"));
assert!(out.contains("token expires"));
for (i, b) in out.bytes().enumerate() {
if b == b'\n' {
assert!(
i > 0 && out.as_bytes()[i - 1] == b'\r',
"bare LF at byte {i}"
);
}
}
}
#[test]
fn installer_ps1_doubles_single_quotes_in_embeds() {
let out = render_installer_ps1("https://k", "it's");
assert!(out.contains("Authorization = 'Bearer it''s'"));
}
#[test]
fn installer_ps1_keeps_tmp_and_points_at_log_on_failure() {
let out = render_installer_ps1("https://kanade.example.com", "tok-123");
assert!(out.contains("install.log"));
assert!(out.contains("throw \"install failed (exit $code)"));
assert!(out.contains("} catch {"));
assert!(out.contains("Installer files kept at $tmp"));
assert_eq!(out.matches("Remove-Item").count(), 1);
}
#[test]
fn installer_sh_embeds_base_token_and_os_arch_map_lf_only() {
let out = render_installer_sh("https://kanade.example.com", "tok-123");
assert!(out.starts_with("#!/bin/sh\n"));
assert!(out.contains("set -eu\n"));
assert!(!out.contains("-H 'Authorization:"));
let curl_line = out.lines().find(|l| l.starts_with("curl ")).unwrap();
assert!(!curl_line.contains("tok-123"), "{curl_line}");
assert!(out.contains("curl -fsSL -K - -o \"$TMP/installer.tar.gz\""));
assert!(out.contains("header = \"Authorization: Bearer tok-123\""));
assert!(out.contains("<<'KANADE_CURL_CONFIG'"));
assert!(out.contains(
"'https://kanade.example.com/api/agents/installer?os='\"$OS\"'&arch='\"$ARCH\""
));
assert!(out.contains("case \"$(uname -s)\" in"));
assert!(out.contains("unsupported OS"));
assert!(out.contains("OS=linux\n"));
assert!(out.contains("case \"$(uname -m)\" in"));
assert!(out.contains("x86_64) ARCH=x86_64 ;;"));
assert!(out.contains("aarch64|arm64) ARCH=aarch64 ;;"));
assert!(out.contains("unsupported architecture"));
let darwin = &out[out.find(" Darwin)\n").expect("Darwin branch")..];
let darwin = &darwin[..darwin.find(";;").unwrap()];
assert!(darwin.contains("OS=macos\n"), "{darwin}");
assert!(
darwin.contains("\"$(sysctl -n hw.optional.arm64 2>/dev/null || true)\" = \"1\""),
"{darwin}"
);
assert!(darwin.contains("ARCH=aarch64\n"), "{darwin}");
assert!(!darwin.contains("uname -m"), "{darwin}");
assert!(!darwin.contains("ARCH=x86_64"), "{darwin}");
assert!(
darwin.contains(
"echo \"Intel Macs are not supported — kanade supports Apple Silicon Macs only\" >&2\n exit 1\n"
),
"{darwin}"
);
assert!(out.contains("tar xzf \"$TMP/installer.tar.gz\" -C \"$TMP\""));
assert!(out.contains("sh \"$TMP/install.sh\""));
assert!(out.contains("sudo bash"));
assert!(!out.contains('\r'));
}
#[test]
fn installer_sh_escapes_the_token_for_the_curl_config() {
let out = render_installer_sh("https://k", "we\"ird\\tok");
assert!(out.contains("header = \"Authorization: Bearer we\\\"ird\\\\tok\""));
}
#[test]
fn bearer_token_parsing() {
let mut h = HeaderMap::new();
assert_eq!(bearer_token(&h), None);
h.insert(header::AUTHORIZATION, "Bearer tok-123".parse().unwrap());
assert_eq!(bearer_token(&h), Some("tok-123"));
h.insert(header::AUTHORIZATION, "Basic dXNlcg==".parse().unwrap());
assert_eq!(bearer_token(&h), None);
h.insert(header::AUTHORIZATION, "Bearer ".parse().unwrap());
assert_eq!(bearer_token(&h), None);
}
#[test]
fn install_ps1_embeds_token_and_keys_when_given() {
let out = render_install_ps1(
"0.43.99",
Some("s3cret"),
Some(r#"[{"kid":"backend-1","public_key":"AAAA","label":"backend"}]"#),
false,
);
assert!(out.contains("# Installs kanade-agent 0.43.99 as a Windows service."));
assert!(out.contains("-NatsToken 's3cret'"));
assert!(out.contains(
"-CommandKeys '[{\"kid\":\"backend-1\",\"public_key\":\"AAAA\",\"label\":\"backend\"}]'"
));
assert!(!out.contains("-RequireSignedCommands"));
assert!(out.contains("exit $LASTEXITCODE"));
assert!(out.contains("Start-Transcript -Path (Join-Path $PSScriptRoot 'install.log')"));
assert!(out.contains("Stop-Transcript"));
}
#[test]
fn install_ps1_embeds_require_signed_commands_when_enforcement_is_embedded() {
let out = render_install_ps1(
"0.43.99",
Some("s3cret"),
Some(r#"[{"kid":"backend-1","public_key":"AAAA"}]"#),
true,
);
assert!(out.contains("-CommandKeys"));
assert!(out.contains("-RequireSignedCommands"));
let line = out
.lines()
.find(|l| l.contains("deploy-agent.ps1"))
.unwrap();
assert!(line.trim_end().ends_with("-RequireSignedCommands"));
}
#[test]
fn resolve_enforcement_off_when_not_requested() {
assert_eq!(resolve_enforcement(false, true), EnforcementPlan::Off);
assert_eq!(resolve_enforcement(false, false), EnforcementPlan::Off);
}
#[test]
fn resolve_enforcement_embeds_only_when_a_key_is_present() {
assert_eq!(resolve_enforcement(true, true), EnforcementPlan::Embedded);
}
#[test]
fn resolve_enforcement_flags_a_request_with_no_key_rather_than_silently_dropping_it() {
assert_eq!(
resolve_enforcement(true, false),
EnforcementPlan::RequestedButNoKey
);
}
#[test]
fn install_ps1_is_crlf_throughout() {
let out = render_install_ps1("0.43.99", Some("tok"), None, false);
for (i, b) in out.bytes().enumerate() {
if b == b'\n' {
assert!(
i > 0 && out.as_bytes()[i - 1] == b'\r',
"bare LF at byte {i}"
);
}
}
}
#[test]
#[cfg(target_os = "windows")]
fn install_agent_ps1_writes_install_log_when_deploy_agent_throws() {
use std::process::Command;
let dir = std::env::temp_dir().join(format!(
"kanade-install-agent-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("install-agent.ps1"),
render_install_ps1("9.9.9", None, None, false),
)
.unwrap();
std::fs::write(
dir.join("deploy-agent.ps1"),
"throw \"simulated deploy failure: file in use\"\r\n",
)
.unwrap();
let status = Command::new("powershell.exe")
.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"])
.arg(dir.join("install-agent.ps1"))
.status()
.expect("failed to spawn powershell.exe");
assert!(
!status.success(),
"the stub throw should propagate as a nonzero exit"
);
let log = std::fs::read_to_string(dir.join("install.log"))
.expect("install.log should exist even though the script failed");
assert!(
log.contains("simulated deploy failure: file in use"),
"install.log is missing the real error text: {log}"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn install_ps1_omits_args_entirely_when_not_given() {
let out = render_install_ps1("0.43.99", None, None, false);
assert!(!out.contains("-NatsToken"));
assert!(!out.contains("-CommandKeys"));
assert!(!out.contains("-RequireSignedCommands"));
assert!(out.contains("& (Join-Path $PSScriptRoot 'deploy-agent.ps1')\r\n"));
}
#[test]
fn install_ps1_doubles_single_quotes() {
let out = render_install_ps1("0.43.99", Some("it's"), None, false);
assert!(out.contains("-NatsToken 'it''s'"));
}
#[test]
fn install_cmd_is_crlf_throughout() {
let out = render_install_cmd("0.43.99");
assert!(!out.is_empty());
for (i, b) in out.bytes().enumerate() {
if b == b'\n' {
assert!(
i > 0 && out.as_bytes()[i - 1] == b'\r',
"bare LF at byte {i}"
);
}
}
assert!(out.contains("%~dp0install-agent.ps1"));
assert!(out.contains("REM kanade-agent installer (generated by kanade-backend). Installs kanade-agent 0.43.99."));
for line in out.lines().filter(|l| l.trim_start().starts_with("echo ")) {
assert!(!line.contains('&'), "echo line with '&': {line}");
}
}
#[test]
fn zip_round_trips_all_entries() {
let agent_toml = render_agent_toml("nats://broker.corp:4222").unwrap();
let install_ps1 =
render_install_ps1("0.43.99", Some("tok"), Some("[{\"kid\":\"k\"}]"), true);
let entries: Vec<(&str, Vec<u8>)> = vec![
("kanade-agent.exe", b"MZ-fake-exe".to_vec()),
("agent.toml", agent_toml.clone().into_bytes()),
("deploy-agent.ps1", DEPLOY_AGENT_PS1.as_bytes().to_vec()),
("install-agent.ps1", install_ps1.clone().into_bytes()),
("install.cmd", render_install_cmd("0.43.99").into_bytes()),
(
"README.txt",
render_readme("0.43.99", true, EnforcementPlan::Embedded).into_bytes(),
),
];
let bytes = build_zip(entries).unwrap();
let mut archive =
zip::ZipArchive::new(std::io::Cursor::new(bytes)).expect("zip reads back");
let names: std::collections::HashSet<String> =
archive.file_names().map(str::to_owned).collect();
for expected in [
"kanade-agent.exe",
"agent.toml",
"deploy-agent.ps1",
"install-agent.ps1",
"install.cmd",
"README.txt",
] {
assert!(names.contains(expected), "missing {expected}");
}
assert_eq!(names.len(), 6);
use std::io::Read as _;
let mut buf = String::new();
archive
.by_name("agent.toml")
.unwrap()
.read_to_string(&mut buf)
.unwrap();
assert_eq!(buf, agent_toml);
buf.clear();
archive
.by_name("install-agent.ps1")
.unwrap()
.read_to_string(&mut buf)
.unwrap();
assert_eq!(buf, install_ps1);
buf.clear();
archive
.by_name("deploy-agent.ps1")
.unwrap()
.read_to_string(&mut buf)
.unwrap();
assert_eq!(buf, DEPLOY_AGENT_PS1);
}
#[test]
fn readme_names_the_signing_state() {
let signed = render_readme("0.43.99", true, EnforcementPlan::Off);
assert!(signed.contains("command-signing PUBLIC key"));
assert!(signed.contains("Break-glass keys are\r\nNEVER included"));
let unsigned = render_readme("0.43.99", false, EnforcementPlan::Off);
assert!(unsigned.contains("not signing commands"));
assert!(!unsigned.contains("PUBLIC key (provisioned"));
}
#[test]
fn readme_names_embedded_enforcement() {
let out = render_readme("0.43.99", true, EnforcementPlan::Embedded);
assert!(out.contains("-RequireSignedCommands"));
assert!(out.contains("REFUSE"));
}
#[test]
fn readme_warns_loudly_when_enforcement_was_requested_but_no_key_exists() {
let out = render_readme("0.43.99", false, EnforcementPlan::RequestedButNoKey);
assert!(out.contains("WARNING"));
assert!(out.contains("command-key-generate"));
assert!(out.contains("does NOT enable enforcement"));
}
#[test]
fn resolve_nats_falls_back_to_the_backend_url() {
for settings in [
ServerSettings::default(),
ServerSettings {
agent_install: Some(kanade_shared::wire::AgentInstallSection {
nats_token: Some("tok".into()),
..Default::default()
}),
..Default::default()
},
] {
let (url, _) = resolve_nats(&settings, "nats://backend:4222").unwrap();
assert_eq!(url, "nats://backend:4222");
}
let (_, token) = resolve_nats(&ServerSettings::default(), "nats://backend:4222").unwrap();
assert_eq!(token, None);
}
#[test]
fn resolve_nats_prefers_the_settings_values() {
let settings = ServerSettings {
agent_install: Some(kanade_shared::wire::AgentInstallSection {
nats_url: Some("nats://broker.corp:4222".into()),
nats_token: Some("s3cret".into()),
nats_token_set: false,
require_signed_commands: None,
}),
..Default::default()
};
let (url, token) = resolve_nats(&settings, "nats://backend:4222").unwrap();
assert_eq!(url, "nats://broker.corp:4222");
assert_eq!(token.as_deref(), Some("s3cret"));
}
#[test]
fn resolve_nats_treats_an_empty_token_as_no_token() {
let settings = ServerSettings {
agent_install: Some(kanade_shared::wire::AgentInstallSection {
nats_token: Some(String::new()),
..Default::default()
}),
..Default::default()
};
let (_, token) = resolve_nats(&settings, "nats://backend:4222").unwrap();
assert_eq!(token, None);
}
#[test]
fn resolve_nats_rejects_a_hostile_stored_url() {
for bad in ["", "nats://evil'\nx='y'", "nats://a\nb", "nats://a\rb"] {
let settings = ServerSettings {
agent_install: Some(kanade_shared::wire::AgentInstallSection {
nats_url: Some(bad.into()),
..Default::default()
}),
..Default::default()
};
let (code, msg) = resolve_nats(&settings, "nats://backend:4222").unwrap_err();
assert_eq!(code, StatusCode::INTERNAL_SERVER_ERROR, "{bad:?}");
assert!(msg.contains("Settings"), "{msg}");
}
}
#[test]
fn params_default_to_windows_x86_64_and_reject_unknowns() {
let p: InstallerParams = serde_urlencoded_defaults();
assert_eq!(p.os, InstallerOs::Windows);
assert_eq!(p.resolve_arch().unwrap(), InstallerArch::X86_64);
assert!(serde_json::from_str::<InstallerOs>(r#""freebsd""#).is_err());
assert!(serde_json::from_str::<InstallerArch>(r#""armv7""#).is_err());
assert_eq!(
serde_json::from_str::<InstallerOs>(r#""linux""#).unwrap(),
InstallerOs::Linux
);
assert_eq!(
serde_json::from_str::<InstallerOs>(r#""macos""#).unwrap(),
InstallerOs::Macos
);
assert_eq!(
serde_json::from_str::<InstallerArch>(r#""aarch64""#).unwrap(),
InstallerArch::Aarch64
);
}
fn serde_urlencoded_defaults() -> InstallerParams {
InstallerParams::default()
}
fn params_from(query: &str) -> InstallerParams {
let uri: axum::http::Uri = format!("/api/agents/installer?{query}").parse().unwrap();
Query::<InstallerParams>::try_from_uri(&uri).unwrap().0
}
#[test]
fn macos_is_apple_silicon_only() {
let (code, msg) = params_from("os=macos&arch=x86_64")
.resolve_arch()
.unwrap_err();
assert_eq!(code, StatusCode::BAD_REQUEST);
assert!(
msg.contains("Intel (x86_64) Macs are not supported"),
"{msg}"
);
assert!(msg.contains("Apple Silicon"), "{msg}");
for q in ["os=macos&arch=aarch64", "os=macos"] {
assert_eq!(
params_from(q).resolve_arch().unwrap(),
InstallerArch::Aarch64,
"{q}"
);
}
assert_eq!(
params_from("os=linux").resolve_arch().unwrap(),
InstallerArch::X86_64
);
assert_eq!(
params_from("os=linux&arch=aarch64").resolve_arch().unwrap(),
InstallerArch::Aarch64
);
let rows: Vec<(String, Option<String>)> = vec![(
"0.45.3-macos-x86_64".into(),
Some("2026-07-04T00:00:00Z".into()),
)];
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Macos, InstallerArch::Aarch64),
None
);
}
#[test]
fn latest_key_filters_by_platform() {
let rows: Vec<(String, Option<String>)> = vec![
("0.44.0".into(), Some("2026-07-01T00:00:00Z".into())),
("0.45.4".into(), Some("2026-07-03T00:00:00Z".into())),
(
"0.45.4-linux-x86_64".into(),
Some("2026-07-02T00:00:00Z".into()),
),
(
"0.45.3-linux-x86_64".into(),
Some("2026-07-04T00:00:00Z".into()),
),
(
"0.45.4-linux-aarch64".into(),
Some("2026-07-05T00:00:00Z".into()),
),
];
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Windows, InstallerArch::X86_64),
Some("0.45.4".into())
);
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Linux, InstallerArch::X86_64),
Some("0.45.3-linux-x86_64".into())
);
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Linux, InstallerArch::Aarch64),
Some("0.45.4-linux-aarch64".into())
);
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Windows, InstallerArch::Aarch64),
Some("0.45.4".into())
);
let bare_only: Vec<(String, Option<String>)> =
vec![("0.44.0".into(), Some("2026-07-01T00:00:00Z".into()))];
assert_eq!(
latest_key_for_platform(&bare_only, InstallerOs::Linux, InstallerArch::X86_64),
None
);
let rc: Vec<(String, Option<String>)> = vec![
(
"0.46.0-rc-linux".into(),
Some("2026-07-01T00:00:00Z".into()),
),
(
"0.46.0-rc.1-linux-x86_64".into(),
Some("2026-07-02T00:00:00Z".into()),
),
];
assert_eq!(
latest_key_for_platform(&rc, InstallerOs::Windows, InstallerArch::X86_64),
Some("0.46.0-rc-linux".into())
);
assert_eq!(
latest_key_for_platform(&rc, InstallerOs::Linux, InstallerArch::X86_64),
Some("0.46.0-rc.1-linux-x86_64".into())
);
}
#[test]
fn latest_key_keeps_macos_and_linux_apart() {
let rows: Vec<(String, Option<String>)> = vec![
("0.45.4".into(), Some("2026-07-01T00:00:00Z".into())),
(
"0.45.4-linux-aarch64".into(),
Some("2026-07-02T00:00:00Z".into()),
),
(
"0.45.4-macos-aarch64".into(),
Some("2026-07-03T00:00:00Z".into()),
),
(
"0.46.0-rc.1-macos-aarch64".into(),
Some("2026-07-05T00:00:00Z".into()),
),
];
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Macos, InstallerArch::Aarch64),
Some("0.46.0-rc.1-macos-aarch64".into())
);
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Linux, InstallerArch::Aarch64),
Some("0.45.4-linux-aarch64".into())
);
assert_eq!(
latest_key_for_platform(&rows, InstallerOs::Windows, InstallerArch::X86_64),
Some("0.45.4".into())
);
let linux_only: Vec<(String, Option<String>)> = vec![(
"0.45.4-linux-x86_64".into(),
Some("2026-07-01T00:00:00Z".into()),
)];
assert_eq!(
latest_key_for_platform(&linux_only, InstallerOs::Macos, InstallerArch::Aarch64),
None
);
}
#[test]
fn install_sh_exports_the_token_only_when_given() {
let with = render_install_sh(
Some("s3cret"),
None,
false,
"setup-agent.sh",
"a systemd service",
);
assert!(with.starts_with("#!/bin/sh\n"));
assert!(with.contains("set -eu\n"));
assert!(with.contains("export KANADE_NATS_TOKEN='s3cret'\n"));
assert!(with.ends_with("exec ./setup-agent.sh\n"));
assert!(!with.contains("KANADE_NATS_URL"));
assert!(!with.contains('\r'));
let without = render_install_sh(None, None, false, "setup-agent.sh", "a systemd service");
assert!(!without.contains("KANADE_NATS_TOKEN"));
assert!(without.ends_with("exec ./setup-agent.sh\n"));
}
#[test]
fn install_sh_shell_escapes_single_quotes() {
let out = render_install_sh(
Some("it's"),
None,
false,
"setup-agent.sh",
"a systemd service",
);
assert!(out.contains("export KANADE_NATS_TOKEN='it'\\''s'\n"));
}
fn tar_round_trip(entries: Vec<TarEntry>) -> std::collections::HashMap<String, (u32, Vec<u8>)> {
let bytes = build_tar_gz(entries).unwrap();
let dec = flate2::read::GzDecoder::new(bytes.as_slice());
let mut archive = tar::Archive::new(dec);
let mut seen = std::collections::HashMap::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let name = entry.path().unwrap().to_string_lossy().into_owned();
let mode = entry.header().mode().unwrap();
let mut data = Vec::new();
use std::io::Read as _;
entry.read_to_end(&mut data).unwrap();
seen.insert(name, (mode, data));
}
seen
}
#[test]
fn linux_tar_gz_round_trips_all_entries_with_modes() {
let agent_toml = render_agent_toml("nats://broker.corp:4222").unwrap();
let seen = tar_round_trip(unix_tar_entries(
InstallerOs::Linux,
"0.45.4-linux-x86_64",
b"\x7fELF-fake".to_vec(),
agent_toml.clone(),
Some("tok"),
None,
EnforcementPlan::Off,
));
for expected in [
"bin/kanade-agent",
"etc/agent.toml",
"systemd/kanade-agent.service",
"setup-agent.sh",
"install.sh",
"README.txt",
] {
assert!(seen.contains_key(expected), "missing {expected}");
}
assert_eq!(seen.len(), 6);
for exe_name in ["bin/kanade-agent", "setup-agent.sh", "install.sh"] {
assert_eq!(seen[exe_name].0, 0o755, "{exe_name} mode");
}
for data_name in [
"etc/agent.toml",
"systemd/kanade-agent.service",
"README.txt",
] {
assert_eq!(seen[data_name].0, 0o644, "{data_name} mode");
}
assert_eq!(seen["bin/kanade-agent"].1, b"\x7fELF-fake");
assert_eq!(seen["etc/agent.toml"].1, agent_toml.as_bytes());
assert_eq!(
seen["install.sh"].1,
render_install_sh(
Some("tok"),
None,
false,
"setup-agent.sh",
"a systemd service"
)
.as_bytes()
);
assert_eq!(seen["setup-agent.sh"].1, SETUP_AGENT_SH.as_bytes());
assert_eq!(
seen["systemd/kanade-agent.service"].1,
AGENT_SERVICE.as_bytes()
);
}
#[test]
fn macos_tar_gz_carries_the_launchd_layout() {
let agent_toml = render_agent_toml("nats://broker.corp:4222").unwrap();
let seen = tar_round_trip(unix_tar_entries(
InstallerOs::Macos,
"0.45.4-macos-aarch64",
b"\xcf\xfa\xed\xfe-fake".to_vec(),
agent_toml.clone(),
Some("tok"),
None,
EnforcementPlan::Off,
));
let mut names: Vec<&str> = seen.keys().map(String::as_str).collect();
names.sort_unstable();
assert_eq!(
names,
[
"README.txt",
"bin/kanade-agent",
"etc/agent.toml",
"install.sh",
"launchd/com.kanade.agent.plist",
"setup-agent-macos.sh",
]
);
for exe_name in ["bin/kanade-agent", "setup-agent-macos.sh", "install.sh"] {
assert_eq!(seen[exe_name].0, 0o755, "{exe_name} mode");
}
for data_name in [
"etc/agent.toml",
"launchd/com.kanade.agent.plist",
"README.txt",
] {
assert_eq!(seen[data_name].0, 0o644, "{data_name} mode");
}
assert_eq!(seen["etc/agent.toml"].1, agent_toml.as_bytes());
assert_eq!(
seen["setup-agent-macos.sh"].1,
SETUP_AGENT_MACOS_SH.as_bytes()
);
assert_eq!(
seen["launchd/com.kanade.agent.plist"].1,
AGENT_PLIST.as_bytes()
);
let install_sh = String::from_utf8(seen["install.sh"].1.clone()).unwrap();
assert!(install_sh.contains("# Installs kanade-agent as a launchd daemon."));
assert!(install_sh.contains("export KANADE_NATS_TOKEN='tok'\n"));
assert!(install_sh.ends_with("exec ./setup-agent-macos.sh\n"));
assert!(!install_sh.contains("KANADE_NATS_URL"));
let readme = String::from_utf8(seen["README.txt"].1.clone()).unwrap();
assert!(readme.contains("release 0.45.4-macos-aarch64"));
}
#[test]
fn readme_linux_documents_the_flow_and_the_signing_state() {
let out = render_readme_linux("0.45.4-linux-x86_64", true, EnforcementPlan::Off);
assert!(out.contains("release 0.45.4-linux-x86_64"));
assert!(out.contains("tar xzf"));
assert!(out.contains("sudo ./install.sh"));
assert!(!out.contains("Windows-only"));
assert!(!out.contains("INACTIVE"));
assert!(out.contains("command-signing PUBLIC key"));
assert!(out.contains("Break-glass keys are"));
assert!(!out.contains('\r'));
}
#[test]
fn readme_macos_documents_the_flow_gatekeeper_and_the_signing_state() {
let out = render_readme_macos("0.45.4-macos-aarch64", false, EnforcementPlan::Off);
assert!(out.contains("release 0.45.4-macos-aarch64"));
assert!(out.contains("tar xzf ../kanade-agent-installer-0.45.4-macos-aarch64.tar.gz"));
assert!(out.contains("sudo ./install.sh"));
assert!(out.contains("launchctl print system/com.kanade.agent"));
assert!(out.contains("com.apple.quarantine"));
assert!(!out.contains("Windows-only"));
assert!(out.contains("is not signing commands"));
assert!(!out.contains('\r'));
}
const KEYS_JSON: &str = r#"[{"kid":"backend-1","public_key":"AAAA","label":"it's"}]"#;
fn install_sh_of(os: InstallerOs, keys: Option<&str>, enforcement: EnforcementPlan) -> String {
let seen = tar_round_trip(unix_tar_entries(
os,
"0.45.4-x",
b"bin".to_vec(),
"toml".into(),
Some("tok"),
keys,
enforcement,
));
String::from_utf8(seen["install.sh"].1.clone()).unwrap()
}
#[test]
fn unix_install_sh_embeds_the_backend_key_and_enforcement() {
for os in [InstallerOs::Linux, InstallerOs::Macos] {
let sh = install_sh_of(os, Some(KEYS_JSON), EnforcementPlan::Embedded);
assert!(sh.contains(&format!(
"export KANADE_COMMAND_KEYS={}\n",
sh_quote(KEYS_JSON)
)));
assert!(sh.contains("export KANADE_REQUIRE_SIGNED_COMMANDS=1\n"));
assert!(sh.find("export KANADE_COMMAND_KEYS").unwrap() < sh.find("exec ./").unwrap());
assert!(!sh.contains('\r'));
let keys_only = install_sh_of(os, Some(KEYS_JSON), EnforcementPlan::Off);
assert!(keys_only.contains("KANADE_COMMAND_KEYS="));
assert!(!keys_only.contains("KANADE_REQUIRE_SIGNED_COMMANDS"));
let none = install_sh_of(os, None, EnforcementPlan::RequestedButNoKey);
assert!(!none.contains("KANADE_COMMAND_KEYS"));
assert!(!none.contains("KANADE_REQUIRE_SIGNED_COMMANDS"));
}
}
#[test]
fn unix_readmes_name_the_signing_and_enforcement_state() {
let embedded = render_readme_linux("k", true, EnforcementPlan::Embedded);
assert!(embedded.contains("KANADE_REQUIRE_SIGNED_COMMANDS=1"));
let warned = render_readme_macos("k", false, EnforcementPlan::RequestedButNoKey);
assert!(warned.contains("*** WARNING"));
assert!(warned.contains("does NOT enable enforcement"));
assert!(!warned.contains('\r'));
}
}