clusterflux-public/crates/disasmer-cli/src/main.rs
2026-07-04 12:10:15 +02:00

8813 lines
312 KiB
Rust

use std::collections::BTreeSet;
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use clap::{Args, Parser, Subcommand};
use disasmer_core::{
diagnose_environment_references, BrowserLoginFlow, BundleIdentityInputs, BundleMetadata,
Capability, CliLoginFlow, Digest, EnvironmentReference, LimitKind, NodeCapabilities,
ProjectModel, ResourceLimits, SelectedInput, SourceProviderKind, SourceProviderManifest,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
const DEFAULT_OPERATOR_ENDPOINT: &str = "https://disasmer.michelpaulissen.com:9443";
const DEFAULT_BROWSER_LOGIN_START: &str = "https://disasmer.michelpaulissen.com/auth/browser/start";
const DEFAULT_OIDC_ISSUER_URL: &str = "https://auth.michelpaulissen.com";
const BROWSER_CALLBACK_ADDR: &str = "127.0.0.1:45173";
const BROWSER_CALLBACK_PATH: &str = "/callback";
const DEFAULT_BROWSER_LOGIN_CALLBACK_TIMEOUT_SECONDS: u64 = 300;
const DOCTOR_COORDINATOR_TIMEOUT: Duration = Duration::from_millis(500);
const DEFAULT_ARTIFACT_EXPORT_MAX_BYTES: u64 = 64 * 1024 * 1024;
#[derive(Clone, Debug, Parser)]
#[command(
name = "disasmer",
version,
arg_required_else_help = true,
about = "Disasmer distributed Wasm runtime CLI.",
after_help = "Primary workflow:
1. disasmer login --browser
2. disasmer project init
3. disasmer node enroll; disasmer node attach --worker
4. disasmer run [entry] --project <path>
5. Debug with VS Code \"Disasmer: Launch Virtual Process\" or disasmer dap
6. Inspect with disasmer process status, task list, logs, and artifact list
Use --json on primary commands for scriptable output. Hosted account creation happens in the browser login flow."
)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Clone, Debug, Subcommand)]
enum Commands {
Doctor(DoctorArgs),
Login(LoginArgs),
Logout(AuthLogoutArgs),
Auth {
#[command(subcommand)]
command: AuthCommands,
},
Agent {
#[command(subcommand)]
command: AgentCommands,
},
Key {
#[command(subcommand)]
command: KeyCommands,
},
Project {
#[command(subcommand)]
command: ProjectCommands,
},
Inspect(BundleInspectArgs),
Build(BuildArgs),
Bundle {
#[command(subcommand)]
command: BundleCommands,
},
Run(RunArgs),
Node {
#[command(subcommand)]
command: NodeCommands,
},
Process {
#[command(subcommand)]
command: ProcessCommands,
},
Task {
#[command(subcommand)]
command: TaskCommands,
},
Logs(LogsArgs),
Artifact {
#[command(subcommand)]
command: ArtifactCommands,
},
Dap(DapArgs),
Debug {
#[command(subcommand)]
command: DebugCommands,
},
Quota {
#[command(subcommand)]
command: QuotaCommands,
},
Admin {
#[command(subcommand)]
command: AdminCommands,
},
}
#[derive(Clone, Debug, Parser)]
struct DoctorArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct LoginArgs {
#[arg(long)]
browser: bool,
#[arg(long)]
non_interactive: bool,
#[arg(long)]
plan: bool,
#[arg(long)]
json: bool,
#[arg(long, default_value_t = default_operator_endpoint())]
coordinator: String,
#[arg(long = "complete-browser-code")]
complete_browser_code: Option<String>,
#[arg(long, default_value = "tenant")]
tenant: String,
#[arg(long = "project-id", default_value = "project")]
project: String,
#[arg(long, default_value = "user")]
user: String,
#[arg(long = "oidc-issuer-url")]
oidc_issuer_url: Option<String>,
#[arg(long = "oidc-client-id", default_value = "disasmer")]
oidc_client_id: String,
#[arg(long)]
state: Option<String>,
}
#[derive(Clone, Debug, Subcommand)]
enum AuthCommands {
Status(AuthStatusArgs),
Logout(AuthLogoutArgs),
}
#[derive(Clone, Debug, Parser)]
struct AuthStatusArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct AuthLogoutArgs {
#[arg(long)]
yes: bool,
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Subcommand)]
enum AgentCommands {
Enroll(AgentEnrollArgs),
}
#[derive(Clone, Debug, Subcommand)]
enum KeyCommands {
Add(KeyAddArgs),
List(KeyListArgs),
Revoke(KeyRevokeArgs),
}
#[derive(Clone, Debug, Subcommand)]
enum BundleCommands {
Inspect(BundleInspectArgs),
}
#[derive(Clone, Debug, Parser)]
struct AgentEnrollArgs {
#[arg(long)]
json: bool,
#[arg(long = "public-key")]
public_key: String,
}
#[derive(Clone, Debug, Parser)]
struct KeyAddArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "agent")]
agent: String,
#[arg(long = "public-key")]
public_key: String,
}
#[derive(Clone, Debug, Parser)]
struct KeyListArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct KeyRevokeArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "agent")]
agent: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Subcommand)]
enum ProjectCommands {
Init(ProjectInitArgs),
Status(ProjectStatusArgs),
List(ProjectListArgs),
Select(ProjectSelectArgs),
}
#[derive(Clone, Debug, Parser)]
struct ProjectInitArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "project")]
new_project: String,
#[arg(long, default_value = "Disasmer Project")]
name: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Parser)]
struct ProjectStatusArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct ProjectListArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct ProjectSelectArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(value_name = "PROJECT")]
selected_project: String,
}
#[derive(Clone, Debug, Parser)]
struct BundleInspectArgs {
#[arg(long)]
project: Option<PathBuf>,
#[arg(long = "source-provider")]
source_provider: Option<String>,
#[arg(long = "disable-source-provider")]
disabled_source_providers: Vec<String>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Parser)]
struct BuildArgs {
#[arg(long)]
project: Option<PathBuf>,
#[arg(long = "source-provider")]
source_provider: Option<String>,
#[arg(long = "disable-source-provider")]
disabled_source_providers: Vec<String>,
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Parser)]
struct RunArgs {
entry: Option<String>,
#[arg(long)]
project: Option<PathBuf>,
#[arg(long)]
coordinator: Option<String>,
#[arg(long)]
local: bool,
#[arg(long)]
non_interactive: bool,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Subcommand)]
enum NodeCommands {
Attach(AttachArgs),
Enroll(NodeEnrollArgs),
List(NodeListArgs),
Status(NodeStatusArgs),
Revoke(NodeRevokeArgs),
}
#[derive(Clone, Debug, Parser)]
struct AttachArgs {
#[arg(long)]
coordinator: Option<String>,
#[arg(long, default_value = "tenant")]
tenant: String,
#[arg(long = "project-id", default_value = "project")]
project: String,
#[arg(long)]
node: Option<String>,
#[arg(long = "cap")]
caps: Vec<String>,
#[arg(long = "enrollment-grant")]
enrollment_grant: Option<String>,
#[arg(long = "public-key")]
public_key: Option<String>,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, Parser)]
struct NodeEnrollArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "node-enrollment")]
grant: String,
#[arg(long, default_value_t = 900)]
ttl_seconds: u64,
}
#[derive(Clone, Debug, Parser)]
struct NodeListArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct NodeStatusArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long)]
node: Option<String>,
}
#[derive(Clone, Debug, Parser)]
struct NodeRevokeArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long)]
node: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Subcommand)]
enum ProcessCommands {
Status(ProcessStatusArgs),
Restart(ProcessRestartArgs),
Cancel(ProcessCancelArgs),
}
#[derive(Clone, Debug, Parser)]
struct ProcessStatusArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "vp-current")]
process: String,
}
#[derive(Clone, Debug, Parser)]
struct ProcessRestartArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "vp-current")]
process: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Parser)]
struct ProcessCancelArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "vp-current")]
process: String,
#[arg(long)]
node: Option<String>,
#[arg(long)]
task: Option<String>,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Subcommand)]
enum TaskCommands {
List(TaskListArgs),
Restart(TaskRestartArgs),
}
#[derive(Clone, Debug, Parser)]
struct TaskListArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long)]
process: Option<String>,
}
#[derive(Clone, Debug, Parser)]
struct TaskRestartArgs {
#[command(flatten)]
scope: CliScopeArgs,
task: String,
#[arg(long, default_value = "vp-current")]
process: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Parser)]
struct LogsArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long)]
process: Option<String>,
#[arg(long)]
task: Option<String>,
}
#[derive(Clone, Debug, Subcommand)]
enum ArtifactCommands {
List(ArtifactListArgs),
Download(ArtifactDownloadArgs),
Export(ArtifactExportArgs),
}
#[derive(Clone, Debug, Parser)]
struct ArtifactListArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long)]
process: Option<String>,
}
#[derive(Clone, Debug, Parser)]
struct ArtifactDownloadArgs {
#[command(flatten)]
scope: CliScopeArgs,
artifact: String,
#[arg(long, default_value_t = 64 * 1024 * 1024)]
max_bytes: u64,
}
#[derive(Clone, Debug, Parser)]
struct ArtifactExportArgs {
#[command(flatten)]
scope: CliScopeArgs,
artifact: String,
#[arg(long)]
to: PathBuf,
#[arg(long, default_value = "node-local")]
receiver_node: String,
}
#[derive(Clone, Debug, Parser)]
struct DapArgs {
#[arg(long)]
plan: bool,
#[arg(long)]
json: bool,
#[arg(last = true)]
args: Vec<String>,
}
#[derive(Clone, Debug, Subcommand)]
enum DebugCommands {
Attach(DebugAttachArgs),
}
#[derive(Clone, Debug, Parser)]
struct DebugAttachArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "vp-current")]
process: String,
}
#[derive(Clone, Debug, Subcommand)]
enum QuotaCommands {
Status(QuotaStatusArgs),
}
#[derive(Clone, Debug, Parser)]
struct QuotaStatusArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Subcommand)]
enum AdminCommands {
Status(AdminStatusArgs),
Bootstrap(AdminBootstrapArgs),
RevokeNode(NodeRevokeArgs),
StopProcess(ProcessCancelArgs),
SuspendTenant(AdminSuspendTenantArgs),
}
#[derive(Clone, Debug, Parser)]
struct AdminStatusArgs {
#[command(flatten)]
scope: CliScopeArgs,
}
#[derive(Clone, Debug, Parser)]
struct AdminBootstrapArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long, default_value = "Disasmer Project")]
name: String,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Parser)]
struct AdminSuspendTenantArgs {
#[command(flatten)]
scope: CliScopeArgs,
#[arg(long = "target-tenant")]
target_tenant: Option<String>,
#[arg(long)]
yes: bool,
}
#[derive(Clone, Debug, Args)]
struct CliScopeArgs {
#[arg(long)]
coordinator: Option<String>,
#[arg(long, default_value = "tenant")]
tenant: String,
#[arg(long = "project-id", default_value = "project")]
project: String,
#[arg(long, default_value = "user")]
user: String,
#[arg(long)]
json: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct RunPlan {
project: PathBuf,
entry: String,
coordinator: CoordinatorSelection,
#[serde(skip_serializing_if = "Option::is_none")]
operator_endpoint: Option<String>,
session: CliSession,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct RunExecutionReport {
plan: RunPlan,
boundary: RunBoundaryEvidence,
node_report: serde_json::Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct RunBoundaryEvidence {
cli_process_started_node_process: bool,
cli_process_started_coordinator_process: bool,
coordinator_address: String,
#[serde(skip_serializing_if = "Option::is_none")]
coordinator_process_id: Option<u32>,
spawned_node_process_id: u32,
node_session_requests: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
enum CoordinatorSelection {
Hosted,
LocalOverride(String),
LocalOnly,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
enum CliSession {
Anonymous,
HumanSession,
AgentPublicKey {
agent: String,
public_key_fingerprint: Digest,
browser_interaction_required: bool,
},
}
impl CliSession {
fn is_authenticated(&self) -> bool {
!matches!(self, Self::Anonymous)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct LoginPlan {
coordinator: String,
human_flow: LoginFlowPlan,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct LoginCompletionReport {
plan: LoginPlan,
boundary: LoginCompletionBoundaryEvidence,
coordinator_response: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct LoginCompletionBoundaryEvidence {
cli_contacted_coordinator: bool,
coordinator_address: String,
scoped_cli_session_received: bool,
local_cli_session_file_written: bool,
provider_tokens_persisted_locally: bool,
provider_tokens_exposed_to_cli: bool,
provider_tokens_sent_to_nodes: bool,
coordinator_session_requests: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
enum LoginFlowPlan {
Browser(BrowserLoginFlow),
Device(CliLoginFlow),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct AgentEnrollmentPlan {
public_key_fingerprint: Digest,
browser_interaction_required_each_run: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct BundleInspection {
project: PathBuf,
default_source_providers: Vec<SourceProviderKind>,
source_provider_manifest: SourceProviderManifest,
source_provider_statuses: Vec<SourceProviderStatus>,
environment_diagnostics: Vec<EnvironmentDiagnosticReport>,
pre_schedule_diagnostics: Vec<CliDiagnostic>,
metadata: BundleMetadata,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct SourceProviderStatus {
provider: String,
status: String,
active: bool,
reason: String,
coordinator_checkout_required: bool,
coordinator_receives_source_bytes_by_default: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct EnvironmentDiagnosticReport {
path: String,
reference: EnvironmentReference,
message: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct CliDiagnostic {
severity: String,
category: String,
code: String,
message: String,
next_actions: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct NodeAttachPlan {
node: String,
coordinator: Option<String>,
capabilities: NodeCapabilities,
grant_disclosures: Vec<CapabilityGrantDisclosure>,
enrollment: Option<NodeEnrollmentPlan>,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
struct NodeAttachReport {
command: String,
node: String,
plan: NodeAttachPlan,
grant_disclosures: Vec<CapabilityGrantDisclosure>,
boundary: NodeAttachBoundaryEvidence,
coordinator_response: Value,
heartbeat_response: Value,
capability_response: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct NodeAttachBoundaryEvidence {
cli_contacted_coordinator: bool,
coordinator_address: String,
used_enrollment_exchange: bool,
coordinator_session_requests: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct CapabilityGrantDisclosure {
capability: Capability,
grant: String,
description: String,
risk: String,
coordinator_policy_limited: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
struct NodeEnrollmentPlan {
grant: String,
public_key_fingerprint: Digest,
exchanges_short_lived_grant_for_long_lived_node_identity: bool,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct ProjectConfig {
tenant: String,
project: String,
user: String,
coordinator: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
struct StoredCliSession {
kind: String,
coordinator: String,
tenant: String,
project: String,
user: String,
cli_session_credential_kind: String,
token_expiry_posture: String,
expires_at: Option<String>,
provider_tokens_exposed_to_cli: bool,
provider_tokens_sent_to_nodes: bool,
created_at_unix_seconds: u64,
}
fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?;
let coordinator = args.scope.coordinator.or_else(|| {
config
.as_ref()
.and_then(|config| config.coordinator.clone())
});
let coordinator_reachability = coordinator_reachability(coordinator.as_deref());
let dependencies = json!({
"cargo": command_available("cargo"),
"git": command_available("git"),
"podman": command_available("podman"),
"disasmer-node": command_available("disasmer-node") || sibling_binary("disasmer-node").is_some(),
"disasmer-coordinator": command_available("disasmer-coordinator") || sibling_binary("disasmer-coordinator").is_some(),
"disasmer-debug-dap": command_available("disasmer-debug-dap") || sibling_binary("disasmer-debug-dap").is_some(),
});
let node_readiness = NodeCapabilities::detect_current();
let node_readiness_summary = node_readiness_summary(&node_readiness, &dependencies);
Ok(json!({
"command": "doctor",
"cwd": cwd,
"coordinator": coordinator,
"coordinator_reachability": coordinator_reachability,
"auth": auth_state_value(&cwd)?,
"project": config,
"dependencies": dependencies,
"node_readiness": node_readiness,
"node_readiness_summary": node_readiness_summary,
"next_actions": [
"disasmer login --browser",
"disasmer project init",
"disasmer node attach",
"disasmer run"
]
}))
}
fn node_readiness_summary(capabilities: &NodeCapabilities, dependencies: &Value) -> Value {
let has_command = capabilities.capabilities.contains(&Capability::Command);
let has_wasmtime = capabilities.capabilities.contains(&Capability::Wasmtime);
let has_vfs_artifacts = capabilities
.capabilities
.contains(&Capability::VfsArtifacts);
let has_source_filesystem = capabilities
.capabilities
.contains(&Capability::SourceFilesystem);
let has_container_backend = capabilities
.environment_backends
.contains(&disasmer_core::EnvironmentBackend::Container);
let podman_available = dependencies
.get("podman")
.and_then(Value::as_bool)
.unwrap_or(false);
let git_available = dependencies
.get("git")
.and_then(Value::as_bool)
.unwrap_or(false);
let disasmer_node_available = dependencies
.get("disasmer-node")
.and_then(Value::as_bool)
.unwrap_or(false);
let mut missing = Vec::new();
if has_container_backend && !podman_available {
missing.push("podman");
}
if capabilities.source_providers.contains("git") && !git_available {
missing.push("git");
}
if !disasmer_node_available {
missing.push("disasmer-node");
}
let basic_runtime_ready =
has_command && has_wasmtime && has_vfs_artifacts && has_source_filesystem;
let local_dependencies_ready = missing.is_empty();
let status = if basic_runtime_ready && local_dependencies_ready {
"ready_to_attach"
} else if basic_runtime_ready {
"local_dependencies_missing"
} else {
"limited_capabilities"
};
let next_actions = if status == "ready_to_attach" {
vec!["disasmer node enroll", "disasmer node attach --worker"]
} else {
vec![
"install missing local dependencies",
"rerun disasmer doctor",
]
};
json!({
"status": status,
"explicit_attach_required": true,
"command_execution_capability": has_command,
"wasmtime_capability": has_wasmtime,
"artifact_capability": has_vfs_artifacts,
"source_filesystem_capability": has_source_filesystem,
"container_backend_reported": has_container_backend,
"podman_binary_available": podman_available,
"git_binary_available": git_available,
"node_binary_available": disasmer_node_available,
"missing_local_dependencies": missing,
"source_providers": capabilities.source_providers.iter().collect::<Vec<_>>(),
"next_actions": next_actions,
})
}
fn coordinator_reachability(coordinator: Option<&str>) -> Value {
let Some(coordinator) = coordinator else {
return json!({
"checked": false,
"status": "not_configured",
"next_action": "run disasmer login --browser or pass --coordinator"
});
};
match ping_coordinator(coordinator, DOCTOR_COORDINATOR_TIMEOUT) {
Ok(response) => json!({
"checked": true,
"status": "reachable",
"coordinator": coordinator,
"response": response
}),
Err(err) => json!({
"checked": true,
"status": "unreachable",
"coordinator": coordinator,
"error": err.to_string(),
"next_action": "check the coordinator URL, network, and service status"
}),
}
}
fn ping_coordinator(coordinator: &str, timeout: Duration) -> Result<Value> {
let transport_addr = json_line_transport_addr(coordinator);
let socket_addrs = transport_addr
.to_socket_addrs()
.with_context(|| format!("failed to resolve {coordinator} via {transport_addr}"))?;
let mut saw_addr = false;
let mut last_error = None;
for socket_addr in socket_addrs {
saw_addr = true;
match TcpStream::connect_timeout(&socket_addr, timeout) {
Ok(stream) => {
stream
.set_read_timeout(Some(timeout))
.with_context(|| format!("failed to set read timeout for {socket_addr}"))?;
stream
.set_write_timeout(Some(timeout))
.with_context(|| format!("failed to set write timeout for {socket_addr}"))?;
let mut session = JsonLineSession::from_stream(stream)?;
return session.request(json!({ "type": "ping" })).with_context(|| {
format!("coordinator ping failed for {coordinator} via {socket_addr}")
});
}
Err(err) => last_error = Some(err),
}
}
if !saw_addr {
anyhow::bail!(
"failed to resolve any socket address for {coordinator} via {transport_addr}"
);
}
anyhow::bail!(
"failed to connect to {coordinator} via {transport_addr}: {}",
last_error
.map(|err| err.to_string())
.unwrap_or_else(|| "no socket addresses attempted".to_owned())
);
}
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?;
let stored_session = read_cli_session(&cwd)?;
let active_coordinator = args
.scope
.coordinator
.clone()
.or_else(|| {
config
.as_ref()
.and_then(|config| config.coordinator.clone())
})
.or_else(|| {
stored_session
.as_ref()
.map(|session| session.coordinator.clone())
})
.unwrap_or_else(default_operator_endpoint);
let tenant = effective_scope_value(
&args.scope.tenant,
config
.as_ref()
.map(|config| config.tenant.as_str())
.or_else(|| {
stored_session
.as_ref()
.map(|session| session.tenant.as_str())
}),
"tenant",
);
let project = effective_scope_value(
&args.scope.project,
config
.as_ref()
.map(|config| config.project.as_str())
.or_else(|| {
stored_session
.as_ref()
.map(|session| session.project.as_str())
}),
"project",
);
let principal = effective_scope_value(
&args.scope.user,
config
.as_ref()
.map(|config| config.user.as_str())
.or_else(|| stored_session.as_ref().map(|session| session.user.as_str())),
"user",
);
Ok(json!({
"command": "auth status",
"active_coordinator": active_coordinator,
"principal": principal,
"tenant": tenant,
"project": project,
"session": auth_state_value(&cwd)?,
"project_config": config,
}))
}
fn auth_logout_report(args: AuthLogoutArgs, cwd: PathBuf) -> Result<Value> {
logout_report(args, cwd, "auth logout")
}
fn non_interactive_auth_machine_error(message: &str, next_actions: Vec<&'static str>) -> Value {
let mut machine_error = cli_error_summary_for_category("authentication", message);
if let Some(object) = machine_error.as_object_mut() {
object.insert("next_actions".to_owned(), json!(next_actions));
object.insert("browser_opened".to_owned(), json!(false));
}
machine_error
}
fn non_interactive_browser_login_report(args: &LoginArgs) -> Value {
let message =
"browser login requires an interactive browser, but non-interactive mode is enabled";
let next_actions = vec![
"rerun without --non-interactive to open the browser",
"disasmer login --browser --plan",
"use DISASMER_AGENT_PUBLIC_KEY for automation",
];
json!({
"command": "login",
"status": "authentication_required",
"coordinator": args.coordinator,
"non_interactive": true,
"browser_requested": true,
"browser_opened": false,
"safe_failure": true,
"message": message,
"next_actions": next_actions,
"machine_error": non_interactive_auth_machine_error(message, next_actions),
})
}
fn confirmation_required_report(
command: &str,
operation: &str,
target: Value,
confirm_command: String,
) -> Value {
let message = format!("{command} requires --yes before {operation}");
let next_actions = json!([
confirm_command,
"review the command target before confirming",
"rerun with --json to inspect the safe failure"
]);
let mut machine_error = cli_error_summary_for_category("policy", &message);
if let Some(object) = machine_error.as_object_mut() {
object.insert("confirmation_required".to_owned(), json!(true));
object.insert("next_actions".to_owned(), next_actions.clone());
}
json!({
"command": command,
"status": "confirmation_required",
"operation": operation,
"target": target,
"requires_confirmation": true,
"confirmation_required": true,
"explicit_user_action_required": true,
"coordinator_request_sent": false,
"safe_failure": true,
"next_actions": next_actions,
"machine_error": machine_error,
})
}
fn logout_report(args: AuthLogoutArgs, cwd: PathBuf, command: &str) -> Result<Value> {
let session_file = session_config_file(&cwd);
if !args.yes {
return Ok(confirmation_required_report(
command,
"delete_cli_session",
json!({
"session_file": session_file,
"node_credentials_untouched": true,
}),
format!("disasmer {command} --yes"),
));
}
let existed = session_file.exists();
if existed {
std::fs::remove_file(&session_file)
.with_context(|| format!("failed to remove {}", session_file.display()))?;
}
Ok(json!({
"command": command,
"requires_confirmation": !args.yes,
"removed_cli_session_file": existed,
"node_credentials_untouched": true,
"session_file": session_file,
}))
}
fn key_add_report(args: KeyAddArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let agent = args.agent.clone();
let public_key_fingerprint = Digest::sha256(&args.public_key);
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "register_agent_public_key",
"tenant": tenant,
"project": project,
"user": user,
"agent": agent,
"public_key": args.public_key,
}))?;
let record = response.get("record").cloned().unwrap_or_else(|| {
json!({
"tenant": tenant,
"project": project,
"user": user,
"agent": agent,
"public_key_fingerprint": public_key_fingerprint,
"scopes": ["project:read", "project:run"],
"human_account_creation_privilege": false,
"browser_interaction_required_each_run": false,
})
});
return Ok(json!({
"command": "key add",
"coordinator": coordinator,
"tenant": tenant,
"project": project,
"user": user,
"agent": agent,
"public_key_fingerprint": record
.get("public_key_fingerprint")
.cloned()
.unwrap_or_else(|| json!(public_key_fingerprint)),
"credential_scope": {
"tenant": tenant,
"project": project,
"actions": record
.get("scopes")
.cloned()
.unwrap_or_else(|| json!(["project:read", "project:run"])),
"human_account_creation_privilege": record
.get("human_account_creation_privilege")
.cloned()
.unwrap_or_else(|| json!(false)),
},
"browser_interaction_required_each_run": record
.get("browser_interaction_required_each_run")
.cloned()
.unwrap_or_else(|| json!(false)),
"attribution": {
"registered_by_user": user,
"agent": agent,
"credential_kind": "public_key",
},
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "key add",
"status": "planned_without_coordinator",
"agent": args.agent,
"public_key_fingerprint": Digest::sha256(args.public_key),
"browser_interaction_required_each_run": false,
}))
}
fn key_list_report(args: KeyListArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "list_agent_public_keys",
"tenant": tenant,
"project": project,
"user": user,
}))?;
return Ok(json!({
"command": "key list",
"coordinator": coordinator,
"tenant": tenant,
"project": project,
"user": user,
"records": response
.get("records")
.cloned()
.unwrap_or_else(|| json!([])),
"credential_scope": {
"tenant": tenant,
"project": project,
"listed_for_user": user,
"human_account_creation_privilege": false,
},
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "key list",
"status": "requires_coordinator",
"records": [],
}))
}
fn key_revoke_report(args: KeyRevokeArgs) -> Result<Value> {
if !args.yes {
return Ok(confirmation_required_report(
"key revoke",
"revoke_agent_public_key",
json!({
"coordinator": args.scope.coordinator,
"tenant": args.scope.tenant,
"project": args.scope.project,
"user": args.scope.user,
"agent": args.agent,
}),
format!("disasmer key revoke --agent {} --yes", args.agent),
));
}
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let agent = args.agent.clone();
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "revoke_agent_public_key",
"tenant": tenant,
"project": project,
"user": user,
"agent": agent,
}))?;
return Ok(json!({
"command": "key revoke",
"coordinator": coordinator,
"requires_confirmation": !args.yes,
"tenant": tenant,
"project": project,
"user": user,
"agent": agent,
"revoked": response
.pointer("/record/revoked")
.cloned()
.unwrap_or_else(|| json!(true)),
"credential_scope": {
"tenant": tenant,
"project": project,
"revoked_for_user": user,
"human_account_creation_privilege": false,
},
"attribution": {
"revoked_by_user": user,
"agent": agent,
"credential_kind": "public_key",
},
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "key revoke",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"agent": args.agent,
}))
}
fn project_init_report(args: ProjectInitArgs, cwd: PathBuf) -> Result<Value> {
let config = ProjectConfig {
tenant: args.scope.tenant.clone(),
project: args.new_project.clone(),
user: args.scope.user.clone(),
coordinator: args.scope.coordinator.clone(),
};
let config_file = project_config_file(&cwd);
if config_file.exists() && !args.yes {
anyhow::bail!(
"{} already exists; rerun with --yes to update the project link",
config_file.display()
);
}
let coordinator_response = if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
Some(session.request(json!({
"type": "create_project",
"tenant": args.scope.tenant,
"actor_user": args.scope.user,
"project": args.new_project,
"name": args.name,
}))?)
} else {
None
};
write_project_config(&cwd, &config)?;
Ok(json!({
"command": "project init",
"source": if args.scope.coordinator.is_some() { "public_coordinator_api" } else { "local_project_config" },
"private_website_required": false,
"project_config_written": true,
"project_config": config,
"config_file": config_file,
"coordinator_response": coordinator_response,
}))
}
fn project_status_report(args: ProjectStatusArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?;
let effective_scope = effective_project_scope(&args.scope, config.as_ref());
let inspection = bundle_inspection(
BundleInspectArgs {
project: Some(cwd.clone()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: true,
},
cwd.clone(),
)
.ok();
let coordinator = effective_scope.coordinator.clone();
let attached_nodes =
list_attached_nodes_if_available(coordinator.as_deref(), &effective_scope)?;
let coordinator_response =
list_task_events_if_available(coordinator.as_deref(), &effective_scope, None)?;
let discovered_environments = discovered_environment_names(inspection.as_ref());
let active_process = active_process_from_task_events(coordinator_response.as_ref())
.unwrap_or_else(|| {
if coordinator.is_some() {
"none_observed".to_owned()
} else {
"unknown_without_coordinator".to_owned()
}
});
let quota_posture = project_quota_posture(&attached_nodes, coordinator_response.as_ref());
Ok(json!({
"command": "project status",
"cwd": cwd,
"tenant": effective_scope.tenant,
"project": effective_scope.project,
"user": effective_scope.user,
"coordinator": coordinator,
"project_identity": {
"tenant": effective_scope.tenant,
"project": effective_scope.project,
"user": effective_scope.user,
"source": if config.is_some() { "project_config_with_cli_overrides" } else { "cli_scope" }
},
"project_config": config,
"bundle": inspection,
"discovered_environments": discovered_environments,
"active_process": active_process,
"attached_nodes": attached_nodes,
"quota_posture": quota_posture,
"coordinator_response": coordinator_response,
}))
}
fn project_list_report(args: ProjectListArgs, cwd: PathBuf) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "list_projects",
"tenant": args.scope.tenant,
"actor_user": args.scope.user,
}))?;
let projects = response
.get("projects")
.cloned()
.unwrap_or_else(|| json!([]));
let project_count = projects.as_array().map(Vec::len).unwrap_or(0);
return Ok(json!({
"command": "project list",
"source": "public_coordinator_api",
"coordinator": coordinator,
"tenant": args.scope.tenant,
"user": args.scope.user,
"projects": projects,
"project_count": project_count,
"private_website_required": false,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
let projects = read_project_config(&cwd)?.into_iter().collect::<Vec<_>>();
let project_count = projects.len();
Ok(json!({
"command": "project list",
"source": "local_project_config",
"projects": projects,
"project_count": project_count,
"private_website_required": false,
}))
}
fn project_select_report(args: ProjectSelectArgs, cwd: PathBuf) -> Result<Value> {
let config = ProjectConfig {
tenant: args.scope.tenant.clone(),
project: args.selected_project.clone(),
user: args.scope.user.clone(),
coordinator: args.scope.coordinator.clone(),
};
let coordinator_response = if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
Some(session.request(json!({
"type": "select_project",
"tenant": args.scope.tenant,
"actor_user": args.scope.user,
"project": args.selected_project,
}))?)
} else {
None
};
write_project_config(&cwd, &config)?;
let selected_project = coordinator_response
.as_ref()
.and_then(|response| response.get("project"))
.cloned()
.unwrap_or_else(|| {
json!({
"id": config.project.clone(),
"tenant": config.tenant.clone(),
"name": config.project.clone(),
})
});
Ok(json!({
"command": "project select",
"source": if args.scope.coordinator.is_some() { "public_coordinator_api" } else { "local_project_config" },
"selected_project": selected_project,
"project_config_written": true,
"private_website_required": false,
"project_config": config,
"coordinator_response": coordinator_response,
}))
}
fn build_report(args: BuildArgs, cwd: PathBuf) -> Result<Value> {
let inspection = bundle_inspection(
BundleInspectArgs {
project: args.project.clone(),
source_provider: args.source_provider.clone(),
disabled_source_providers: args.disabled_source_providers.clone(),
json: true,
},
cwd,
)?;
let diagnostics = inspection.pre_schedule_diagnostics.clone();
let blocking_diagnostic = diagnostics
.iter()
.find(|diagnostic| diagnostic.severity == "error")
.cloned();
let status = if blocking_diagnostic.is_some() {
"blocked_before_schedule"
} else {
"built"
};
let mut report = json!({
"command": "build",
"status": status,
"bundle": inspection,
"diagnostics": diagnostics,
"contains_full_repository_upload": false,
"content_addressed": true,
"debug_metadata_available": true,
"scheduled_work": false,
});
if let Some(diagnostic) = blocking_diagnostic.as_ref() {
let machine_category = match diagnostic.category.as_str() {
"environment" => "environment",
"source_provider" | "capability" => "capability",
_ => "unknown",
};
if let Some(report) = report.as_object_mut() {
report.insert(
"machine_error".to_owned(),
cli_error_summary_for_category(machine_category, &diagnostic.message),
);
}
}
if let Some(output) = args.output {
if let Some(parent) = output.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&output, serde_json::to_vec_pretty(&report)?)?;
}
Ok(report)
}
fn node_enroll_report(args: NodeEnrollArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let requested_grant = args.grant.clone();
let ttl_seconds = args.ttl_seconds;
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "create_node_enrollment_grant",
"tenant": tenant,
"project": project,
"actor_user": user,
"grant": requested_grant,
"now_epoch_seconds": 0,
"ttl_seconds": ttl_seconds,
}))?;
let enrollment_grant = node_enrollment_grant_summary(
&response,
&tenant,
&project,
&user,
&requested_grant,
ttl_seconds,
);
return Ok(json!({
"command": "node enroll",
"status": "created",
"coordinator": coordinator,
"tenant": tenant,
"project": project,
"user": user,
"private_website_required": false,
"enrollment_grant": enrollment_grant,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
let enrollment_grant = json!({
"grant": args.grant,
"tenant": args.scope.tenant,
"project": args.scope.project,
"user": args.scope.user,
"scope": "node:attach",
"ttl_seconds": args.ttl_seconds,
"short_lived": true,
"exchange_for_persistent_node_identity": true,
"node_credentials_separate_from_user_session": true,
});
Ok(json!({
"command": "node enroll",
"status": "planned_without_coordinator",
"private_website_required": false,
"enrollment_grant": enrollment_grant,
}))
}
fn node_enrollment_grant_summary(
response: &Value,
tenant: &str,
project: &str,
user: &str,
requested_grant: &str,
ttl_seconds: u64,
) -> Value {
json!({
"grant": response
.get("grant")
.cloned()
.unwrap_or_else(|| json!(requested_grant)),
"tenant": response
.get("tenant")
.cloned()
.unwrap_or_else(|| json!(tenant)),
"project": response
.get("project")
.cloned()
.unwrap_or_else(|| json!(project)),
"user": user,
"scope": response
.get("scope")
.cloned()
.unwrap_or_else(|| json!("node:attach")),
"ttl_seconds": ttl_seconds,
"expires_at_epoch_seconds": response
.get("expires_at_epoch_seconds")
.cloned()
.unwrap_or(Value::Null),
"short_lived": true,
"exchange_for_persistent_node_identity": true,
"node_credentials_separate_from_user_session": true,
})
}
fn node_list_report(args: NodeListArgs) -> Result<Value> {
node_descriptors_report("node list", args.scope, None)
}
fn node_status_report(args: NodeStatusArgs) -> Result<Value> {
node_descriptors_report("node status", args.scope, args.node)
}
fn node_descriptors_report(
command: &str,
scope: CliScopeArgs,
node: Option<String>,
) -> Result<Value> {
if let Some(coordinator) = &scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "list_node_descriptors",
"tenant": scope.tenant,
"project": scope.project,
"actor_user": scope.user,
}))?;
return Ok(json!({
"command": command,
"coordinator": coordinator,
"node": node,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": command,
"status": "local_capability_snapshot",
"node": node.unwrap_or_else(default_node_id),
"capabilities": NodeCapabilities::detect_current(),
}))
}
fn node_revoke_report(args: NodeRevokeArgs) -> Result<Value> {
if !args.yes {
return Ok(confirmation_required_report(
"node revoke",
"revoke_node_credential",
json!({
"coordinator": args.scope.coordinator,
"tenant": args.scope.tenant,
"project": args.scope.project,
"user": args.scope.user,
"node": args.node,
}),
format!("disasmer node revoke --node {} --yes", args.node),
));
}
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let node = args.node.clone();
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "revoke_node_credential",
"tenant": tenant,
"project": project,
"actor_user": user,
"node": node,
}))?;
return Ok(json!({
"command": "node revoke",
"coordinator": coordinator,
"requires_confirmation": !args.yes,
"tenant": tenant,
"project": project,
"user": user,
"node": node,
"credential_revoked": response.get("type").and_then(Value::as_str) == Some("node_credential_revoked"),
"descriptor_removed": response
.get("descriptor_removed")
.cloned()
.unwrap_or_else(|| json!(false)),
"queued_assignments_removed": response
.get("queued_assignments_removed")
.cloned()
.unwrap_or_else(|| json!(0)),
"node_credentials_separate_from_user_session": true,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "node revoke",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"node": args.node,
}))
}
fn process_status_report(args: ProcessStatusArgs) -> Result<Value> {
let events = list_task_events_if_available(
args.scope.coordinator.as_deref(),
&args.scope,
Some(args.process.clone()),
)?;
let current_tasks = task_summaries(events.as_ref());
let current_task_count = current_tasks.as_array().map(Vec::len).unwrap_or(0);
let state = process_state_from_tasks(events.as_ref());
Ok(json!({
"command": "process status",
"process": args.process,
"state": if events.is_some() { state } else { "unknown_without_coordinator" },
"started_entrypoint": "unknown_from_task_events",
"current_task_count": current_task_count,
"current_tasks": current_tasks,
"debug_state": {
"frozen": null,
"status": "unknown_from_cli_task_events"
},
"events": events,
}))
}
fn process_restart_report(args: ProcessRestartArgs) -> Result<Value> {
if !args.yes {
return Ok(confirmation_required_report(
"process restart",
"restart_virtual_process",
json!({
"coordinator": args.scope.coordinator,
"tenant": args.scope.tenant,
"project": args.scope.project,
"process": args.process,
}),
format!("disasmer process restart --process {} --yes", args.process),
));
}
if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "start_process",
"tenant": args.scope.tenant,
"project": args.scope.project,
"process": args.process,
"restart": true,
}))?;
let restart_request = process_restart_request_summary(&response, !args.yes);
return Ok(json!({
"command": "process restart",
"coordinator": coordinator,
"process": args.process,
"requires_confirmation": !args.yes,
"restart_request": restart_request,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "process restart",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"process": args.process,
"restart_request": {
"status": "requires_coordinator",
"operation": "restart_virtual_process",
"explicit_user_action": true,
},
}))
}
fn process_cancel_report(args: ProcessCancelArgs) -> Result<Value> {
if !args.yes {
return Ok(confirmation_required_report(
"process cancel",
"cancel_virtual_process",
json!({
"coordinator": args.scope.coordinator,
"tenant": args.scope.tenant,
"project": args.scope.project,
"process": args.process,
"node": args.node,
"task": args.task,
}),
format!("disasmer process cancel --process {} --yes", args.process),
));
}
if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "cancel_process",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"process": args.process,
}))?;
let cancel_request = process_cancel_request_summary(&response, !args.yes);
return Ok(json!({
"command": "process cancel",
"coordinator": coordinator,
"requires_confirmation": !args.yes,
"process": args.process,
"node": args.node,
"task": args.task,
"cancel_request": cancel_request,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "process cancel",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"process": args.process,
"node": args.node,
"task": args.task,
"cancel_request": {
"status": "requires_coordinator",
"operation": "cancel_virtual_process",
"whole_process_cancel_available": true,
"explicit_user_action": true,
},
}))
}
fn task_list_report(args: TaskListArgs) -> Result<Value> {
let events = list_task_events_if_available(
args.scope.coordinator.as_deref(),
&args.scope,
args.process.clone(),
)?;
let tasks = task_summaries(events.as_ref());
Ok(json!({
"command": "task list",
"process": args.process,
"tasks": tasks,
"events": events,
}))
}
fn task_restart_report(args: TaskRestartArgs) -> Result<Value> {
if !args.yes {
return Ok(confirmation_required_report(
"task restart",
"restart_selected_task",
json!({
"coordinator": args.scope.coordinator,
"tenant": args.scope.tenant,
"project": args.scope.project,
"process": args.process,
"task": args.task,
}),
format!(
"disasmer task restart {} --process {} --yes",
args.task, args.process
),
));
}
if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request_allow_error(json!({
"type": "restart_task",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"process": args.process,
"task": args.task,
}))?;
let restart_request = task_restart_request_summary(&response, !args.yes);
return Ok(json!({
"command": "task restart",
"coordinator": coordinator,
"process": args.process,
"task": args.task,
"requires_confirmation": !args.yes,
"restart_request": restart_request,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "task restart",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"process": args.process,
"task": args.task,
"restart_request": {
"status": "requires_coordinator",
"operation": "restart_selected_task",
"explicit_user_action": true,
"clean_boundary_required": true,
},
}))
}
fn logs_report(args: LogsArgs) -> Result<Value> {
let events = list_task_events_if_available(
args.scope.coordinator.as_deref(),
&args.scope,
args.process.clone(),
)?;
let log_entries = log_entries(events.as_ref(), args.task.as_deref());
Ok(json!({
"command": "logs",
"process": args.process,
"task": args.task,
"log_entries": log_entries,
"logs_are_capped": true,
"secret_redaction_policy": "configured-redaction-boundary",
"events": events,
}))
}
fn artifact_list_report(args: ArtifactListArgs) -> Result<Value> {
let events = list_task_events_if_available(
args.scope.coordinator.as_deref(),
&args.scope,
args.process.clone(),
)?;
let artifacts = artifact_summaries(events.as_ref());
Ok(json!({
"command": "artifact list",
"process": args.process,
"source": "task_events",
"artifacts": artifacts,
"default_durable_store_assumed": false,
"events": events,
}))
}
fn artifact_download_report(args: ArtifactDownloadArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let nonce = command_nonce("artifact-download");
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "create_artifact_download_link",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"artifact": args.artifact,
"max_bytes": args.max_bytes,
"token_nonce": nonce,
"now_epoch_seconds": 0,
}))?;
let download_session = artifact_download_session_summary(&response);
return Ok(json!({
"command": "artifact download",
"coordinator": coordinator,
"artifact": args.artifact,
"max_bytes": args.max_bytes,
"download_session": download_session,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "artifact download",
"status": "requires_coordinator",
"artifact": args.artifact,
"max_bytes": args.max_bytes,
"download_session": {
"status": "requires_coordinator",
"link_issued": false,
"explicit_user_action_required": true,
},
}))
}
fn artifact_export_report(args: ArtifactExportArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "export_artifact_to_node",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"artifact": args.artifact,
"receiver_node": args.receiver_node,
"direct_connectivity": true,
"failure_reason": "",
}))?;
let mut export_plan = artifact_export_plan_summary(&response, &args.to);
let local_export =
if response.get("type").and_then(Value::as_str) == Some("artifact_export_plan") {
artifact_export_local_write_followup(&mut session, &args, &response)?
} else {
json!({
"status": "skipped",
"explicit_user_action": true,
"local_path": &args.to,
"local_bytes_written_by_cli": false,
})
};
apply_local_export_summary(&mut export_plan, &local_export);
return Ok(json!({
"command": "artifact export",
"coordinator": coordinator,
"artifact": args.artifact,
"to": args.to,
"receiver_node": args.receiver_node,
"export_plan": export_plan,
"local_export": local_export,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "artifact export",
"status": "requires_coordinator",
"artifact": args.artifact,
"to": args.to,
"receiver_node": args.receiver_node,
"export_plan": {
"status": "requires_coordinator",
"explicit_user_action": true,
"local_bytes_written_by_cli": false,
"default_durable_store_assumed": false,
},
}))
}
fn artifact_export_local_write_followup(
session: &mut JsonLineSession,
args: &ArtifactExportArgs,
export_response: &Value,
) -> Result<Value> {
let artifact_size = export_response
.get("artifact_size_bytes")
.and_then(Value::as_u64)
.unwrap_or(DEFAULT_ARTIFACT_EXPORT_MAX_BYTES);
let nonce = command_nonce("artifact-export");
let link_response = session.request(json!({
"type": "create_artifact_download_link",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"artifact": args.artifact,
"max_bytes": DEFAULT_ARTIFACT_EXPORT_MAX_BYTES,
"token_nonce": nonce,
"now_epoch_seconds": 0,
}))?;
if link_response.get("type").and_then(Value::as_str) != Some("artifact_download_link") {
return Ok(json!({
"status": "download_link_failed",
"explicit_user_action": true,
"local_path": &args.to,
"local_bytes_written_by_cli": false,
"content_bytes_available": false,
"link_response": link_response,
}));
}
let token_digest = link_response
.pointer("/link/scoped_token_digest")
.cloned()
.context("artifact download link did not include a scoped token digest")?;
let stream_response = session.request(json!({
"type": "open_artifact_download_stream",
"tenant": args.scope.tenant,
"project": args.scope.project,
"actor_user": args.scope.user,
"artifact": args.artifact,
"max_bytes": DEFAULT_ARTIFACT_EXPORT_MAX_BYTES,
"token_nonce": nonce,
"token_digest": token_digest,
"now_epoch_seconds": 1,
"chunk_bytes": artifact_size,
}))?;
if stream_response.get("type").and_then(Value::as_str) != Some("artifact_download_stream") {
return Ok(json!({
"status": "download_stream_failed",
"explicit_user_action": true,
"local_path": &args.to,
"local_bytes_written_by_cli": false,
"content_bytes_available": false,
"stream": artifact_stream_summary(&stream_response),
}));
}
let Some(content_base64) = stream_response
.get("content_base64")
.and_then(Value::as_str)
else {
return Ok(json!({
"status": "download_stream_opened_without_content",
"explicit_user_action": true,
"local_path": &args.to,
"local_bytes_written_by_cli": false,
"content_bytes_available": false,
"stream": artifact_stream_summary(&stream_response),
}));
};
let bytes = BASE64_STANDARD
.decode(content_base64)
.context("artifact download stream returned invalid base64 content")?;
if let Some(parent) = args.to.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::write(&args.to, &bytes)
.with_context(|| format!("failed to write {}", args.to.display()))?;
Ok(json!({
"status": "local_bytes_written",
"explicit_user_action": true,
"local_path": &args.to,
"local_bytes_written_by_cli": true,
"bytes_written": bytes.len(),
"content_bytes_available": true,
"content_source": stream_response.get("content_source").cloned().unwrap_or(Value::Null),
"stream": artifact_stream_summary(&stream_response),
}))
}
fn artifact_stream_summary(response: &Value) -> Value {
json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"streamed_bytes": response.get("streamed_bytes").cloned().unwrap_or_else(|| json!(0)),
"charged_download_bytes": response.get("charged_download_bytes").cloned().unwrap_or_else(|| json!(0)),
"content_bytes_available": response.get("content_bytes_available").cloned().unwrap_or_else(|| json!(false)),
"content_source": response.get("content_source").cloned().unwrap_or(Value::Null),
"content_material_returned_in_report": false,
"error": response.get("message").cloned().unwrap_or(Value::Null),
})
}
fn apply_local_export_summary(export_plan: &mut Value, local_export: &Value) {
let Some(object) = export_plan.as_object_mut() else {
return;
};
object.insert(
"local_bytes_written_by_cli".to_owned(),
local_export
.get("local_bytes_written_by_cli")
.cloned()
.unwrap_or_else(|| json!(false)),
);
object.insert(
"local_export_status".to_owned(),
local_export
.get("status")
.cloned()
.unwrap_or_else(|| json!("unknown")),
);
object.insert(
"content_bytes_available".to_owned(),
local_export
.get("content_bytes_available")
.cloned()
.unwrap_or_else(|| json!(false)),
);
if let Some(bytes_written) = local_export.get("bytes_written") {
object.insert("bytes_written".to_owned(), bytes_written.clone());
object.insert(
"writes_require_data_plane_followup".to_owned(),
json!(false),
);
}
}
fn dap_plan(args: DapArgs) -> Result<Value> {
Ok(json!({
"command": "dap",
"adapter": dap_binary_path()?.display().to_string(),
"args": args.args,
"private_website_required": false,
}))
}
fn exec_dap(args: DapArgs) -> Result<()> {
let status = Command::new(dap_binary_path()?)
.args(args.args)
.status()
.context("failed to launch disasmer-debug-dap")?;
if !status.success() {
anyhow::bail!("disasmer-debug-dap exited with {status}");
}
Ok(())
}
fn debug_attach_report(args: DebugAttachArgs) -> Result<Value> {
let dap = dap_binary_path()?.display().to_string();
debug_attach_report_with_dap(args, dap)
}
fn debug_attach_report_with_dap(args: DebugAttachArgs, dap: String) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let project = args.scope.project.clone();
let user = args.scope.user.clone();
let process = args.process.clone();
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "debug_attach",
"tenant": tenant,
"project": project,
"actor_user": user,
"process": process,
}))?;
let authorization = response.get("authorization").cloned().unwrap_or_else(
|| json!({"allowed": false, "reason": "missing authorization response"}),
);
return Ok(json!({
"command": "debug attach",
"process": process,
"coordinator": coordinator,
"tenant": tenant,
"project": project,
"user": user,
"dap": dap,
"authorized": authorization
.get("allowed")
.cloned()
.unwrap_or_else(|| json!(false)),
"authorization": authorization,
"audit_event": response.get("audit_event").cloned().unwrap_or(Value::Null),
"charged_debug_read_bytes": response
.get("charged_debug_read_bytes")
.cloned()
.unwrap_or_else(|| json!(0)),
"used_debug_read_bytes": response
.get("used_debug_read_bytes")
.cloned()
.unwrap_or_else(|| json!(0)),
"debug_reads_quota_limited": true,
"private_website_required": false,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "debug attach",
"process": args.process,
"coordinator": args.scope.coordinator,
"tenant": args.scope.tenant,
"project": args.scope.project,
"dap": dap,
"authorized": "unknown_without_coordinator",
"debug_reads_quota_limited": "unknown_without_coordinator",
"private_website_required": false,
}))
}
fn quota_status_report(args: QuotaStatusArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?;
let effective_scope = effective_project_scope(&args.scope, config.as_ref());
let coordinator = effective_scope.coordinator.clone();
let attached_nodes =
list_attached_nodes_if_available(coordinator.as_deref(), &effective_scope)?;
let task_events =
list_task_events_if_available(coordinator.as_deref(), &effective_scope, None)?;
let current_usage = quota_current_usage(&attached_nodes, task_events.as_ref());
Ok(json!({
"command": "quota status",
"tenant": effective_scope.tenant,
"project": effective_scope.project,
"user": effective_scope.user,
"coordinator": coordinator,
"project_config": config,
"policy_surface": "generic public quota categories; hosted tuning remains private policy",
"limits": quota_limits_value(),
"current_usage": current_usage,
"attached_nodes": attached_nodes,
"task_events": task_events,
"next_blocked_action": quota_next_blocked_action(&current_usage),
"community_tier_language": true,
"private_abuse_heuristics_exposed": false,
}))
}
fn admin_status_report(args: AdminStatusArgs) -> Result<Value> {
if let Some(coordinator) = &args.scope.coordinator {
let tenant = args.scope.tenant.clone();
let user = args.scope.user.clone();
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "admin_status",
"tenant": tenant,
"actor_user": user,
}))?;
return Ok(json!({
"command": "admin status",
"coordinator": coordinator,
"tenant": tenant,
"user": user,
"suspended": response
.get("suspended")
.cloned()
.unwrap_or_else(|| json!(false)),
"response": response,
"safe_default": "read_only",
"private_website_required": false,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "admin status",
"mode": "self_hosted_local",
"safe_default": "read_only",
"private_website_required": false,
}))
}
fn admin_bootstrap_report(args: AdminBootstrapArgs, cwd: PathBuf) -> Result<Value> {
let scope = args.scope.clone();
let new_project = args.scope.project.clone();
let project_init = project_init_report(
ProjectInitArgs {
scope: args.scope,
new_project,
name: args.name,
yes: args.yes,
},
cwd,
)?;
let coordinator = scope
.coordinator
.clone()
.unwrap_or_else(|| "<local-coordinator>".to_owned());
let tenant = scope.tenant.clone();
let project = scope.project.clone();
let user = scope.user.clone();
Ok(json!({
"command": "admin bootstrap",
"mode": if scope.coordinator.is_some() { "public_coordinator_api" } else { "self_hosted_local" },
"tenant": tenant.clone(),
"project": project.clone(),
"user": user,
"coordinator": scope.coordinator,
"private_website_required": false,
"self_hosted_cli_only": true,
"project_config_written": project_init
.get("project_config_written")
.cloned()
.unwrap_or_else(|| json!(false)),
"project_init": project_init,
"admin_surfaces": {
"coordinator": "disasmer-coordinator",
"project": "disasmer project init/status/list/select",
"node": "disasmer node enroll/list/status/revoke",
"process": "disasmer run/process status/process restart/process cancel",
"logs": "disasmer logs",
"artifacts": "disasmer artifact list/download/export",
"quota": "disasmer quota status",
"policy": "disasmer admin status/suspend-tenant",
},
"bootstrap_sequence": [
{
"step": "start_self_hosted_coordinator",
"command": "disasmer-coordinator --listen 127.0.0.1:0",
"private_website_required": false,
},
{
"step": "create_or_link_project",
"command": "disasmer project init --yes",
"completed": true,
"private_website_required": false,
},
{
"step": "create_node_enrollment_grant",
"command": format!(
"disasmer node enroll --coordinator {coordinator} --tenant {} --project-id {}",
tenant, project
),
"private_website_required": false,
},
{
"step": "attach_worker_node",
"command": format!(
"disasmer node attach --coordinator {coordinator} --tenant {} --project-id {} --worker",
tenant, project
),
"private_website_required": false,
},
{
"step": "run_process",
"command": format!(
"disasmer run --coordinator {coordinator} --tenant {} --project-id {}",
tenant, project
),
"private_website_required": false,
},
{
"step": "inspect_status_logs_artifacts",
"commands": [
"disasmer process status",
"disasmer task list",
"disasmer logs",
"disasmer artifact list",
"disasmer quota status",
],
"private_website_required": false,
},
{
"step": "revoke_access",
"command": "disasmer admin revoke-node --node <node-id> --yes",
"private_website_required": false,
}
],
}))
}
fn admin_suspend_tenant_report(args: AdminSuspendTenantArgs) -> Result<Value> {
let tenant = args
.target_tenant
.unwrap_or_else(|| args.scope.tenant.clone());
if !args.yes {
return Ok(confirmation_required_report(
"admin suspend-tenant",
"suspend_tenant",
json!({
"coordinator": args.scope.coordinator,
"actor_tenant": args.scope.tenant,
"actor_user": args.scope.user,
"target_tenant": tenant,
}),
"disasmer admin suspend-tenant --yes".to_owned(),
));
}
if let Some(coordinator) = &args.scope.coordinator {
let actor_tenant = args.scope.tenant.clone();
let actor_user = args.scope.user.clone();
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "suspend_tenant",
"tenant": actor_tenant,
"actor_user": actor_user,
"target_tenant": tenant,
}))?;
return Ok(json!({
"command": "admin suspend-tenant",
"coordinator": coordinator,
"requires_confirmation": !args.yes,
"tenant": tenant,
"actor_tenant": actor_tenant,
"actor_user": actor_user,
"suspended": response.get("type").and_then(Value::as_str) == Some("tenant_suspended"),
"private_website_required": false,
"response": response,
"coordinator_session_requests": session.requests(),
}));
}
Ok(json!({
"command": "admin suspend-tenant",
"status": "requires_coordinator",
"requires_confirmation": !args.yes,
"tenant": tenant,
"private_website_required": false,
}))
}
fn emit_report<T: Serialize>(report: &T, json_output: bool) -> Result<()> {
let mut value = serde_json::to_value(report)?;
let exit_code = apply_command_report_exit_code(&mut value);
if json_output {
println!("{}", serde_json::to_string_pretty(&value)?);
} else {
println!("{}", human_report(&value));
}
if let Some(exit_code) = exit_code {
std::process::exit(exit_code);
}
Ok(())
}
fn human_report(value: &Value) -> String {
let mut lines = Vec::new();
let title = value
.get("command")
.and_then(Value::as_str)
.map(|command| format!("Disasmer {command}"))
.or_else(|| {
if value.get("human_flow").is_some() {
Some("Disasmer login".to_owned())
} else if value.get("metadata").is_some()
&& value.get("source_provider_manifest").is_some()
{
Some("Disasmer bundle inspect".to_owned())
} else if value.get("entry").is_some() && value.get("session").is_some() {
Some("Disasmer run".to_owned())
} else if value.get("capabilities").is_some() && value.get("node").is_some() {
Some("Disasmer node attach".to_owned())
} else if value.get("public_key_fingerprint").is_some() {
Some("Disasmer agent enroll".to_owned())
} else {
None
}
})
.unwrap_or_else(|| "Disasmer report".to_owned());
lines.push(title);
push_string_field(&mut lines, value, "status", "status");
push_string_field(&mut lines, value, "coordinator", "coordinator");
push_string_field(&mut lines, value, "active_coordinator", "coordinator");
push_string_field(&mut lines, value, "tenant", "tenant");
push_string_field(&mut lines, value, "project", "project");
push_string_field(&mut lines, value, "process", "process");
push_string_field(&mut lines, value, "active_process", "active process");
push_string_field(&mut lines, value, "node", "node");
push_string_field(&mut lines, value, "artifact", "artifact");
push_string_field(&mut lines, value, "entry", "entry");
push_string_field(&mut lines, value, "config_file", "config");
push_string_field(&mut lines, value, "adapter", "adapter");
if let Some(project) = value.get("project").and_then(Value::as_str) {
if !lines
.iter()
.any(|line| line == &format!("project: {project}"))
{
lines.push(format!("project: {project}"));
}
}
if let Some(project_config) = value.get("project_config").or_else(|| value.get("project")) {
if project_config.is_object() {
push_nested_string_field(&mut lines, project_config, "tenant", "tenant");
push_nested_string_field(&mut lines, project_config, "project", "project");
push_nested_string_field(&mut lines, project_config, "coordinator", "coordinator");
}
}
if let Some(metadata) = value.get("metadata") {
push_nested_string_field(&mut lines, metadata, "identity", "bundle");
if let Some(environments) = metadata.get("environments").and_then(Value::as_array) {
let names = environments
.iter()
.filter_map(|env| env.get("name").and_then(Value::as_str))
.collect::<Vec<_>>();
if !names.is_empty() {
lines.push(format!("environments: {}", names.join(", ")));
}
}
}
if let Some(bundle) = value.get("bundle") {
if let Some(metadata) = bundle.get("metadata") {
push_nested_string_field(&mut lines, metadata, "identity", "bundle");
}
}
if let Some(environments) = value
.get("discovered_environments")
.and_then(Value::as_array)
{
let names = environments
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !names.is_empty() {
lines.push(format!("environments: {}", names.join(", ")));
}
}
if let Some(attached_nodes) = value.get("attached_nodes") {
if let Some(count) = attached_nodes.get("count").and_then(Value::as_u64) {
let online = attached_nodes
.get("online")
.and_then(Value::as_u64)
.unwrap_or(0);
lines.push(format!("attached nodes: {count} ({online} online)"));
}
}
if let Some(current_usage) = value.pointer("/quota_posture/current_usage") {
lines.push(format!("quota usage: {}", compact_json(current_usage)));
}
if let Some(current_task_count) = value.get("current_task_count").and_then(Value::as_u64) {
lines.push(format!("tasks: {current_task_count}"));
}
if let Some(tasks) = value.get("current_tasks").and_then(Value::as_array) {
push_task_placement_reasons(&mut lines, tasks);
push_task_locality_failures(&mut lines, tasks);
}
if let Some(tasks) = value.get("tasks").and_then(Value::as_array) {
lines.push(format!("tasks: {}", tasks.len()));
push_task_placement_reasons(&mut lines, tasks);
push_task_locality_failures(&mut lines, tasks);
}
if let Some(log_entries) = value.get("log_entries").and_then(Value::as_array) {
lines.push(format!("log entries: {}", log_entries.len()));
}
if let Some(artifacts) = value.get("artifacts").and_then(Value::as_array) {
lines.push(format!("artifacts: {}", artifacts.len()));
}
if let Some(statuses) = value
.get("source_provider_statuses")
.and_then(Value::as_array)
{
push_source_provider_statuses(&mut lines, statuses);
}
if let Some(bundle_statuses) = value
.pointer("/bundle/source_provider_statuses")
.and_then(Value::as_array)
{
push_source_provider_statuses(&mut lines, bundle_statuses);
}
if let Some(diagnostics) = value
.get("pre_schedule_diagnostics")
.or_else(|| value.get("diagnostics"))
.and_then(Value::as_array)
{
push_cli_diagnostics(&mut lines, diagnostics);
}
if let Some(bundle_diagnostics) = value
.pointer("/bundle/pre_schedule_diagnostics")
.and_then(Value::as_array)
{
push_cli_diagnostics(&mut lines, bundle_diagnostics);
}
if let Some(current_usage) = value.get("current_usage") {
lines.push(format!("quota usage: {}", compact_json(current_usage)));
}
if let Some(next_blocked_action) = value.get("next_blocked_action") {
if !next_blocked_action.is_null() {
lines.push(format!(
"next blocked action: {}",
compact_json(next_blocked_action)
));
}
}
push_machine_error_line(&mut lines, value.get("machine_error"));
push_machine_error_line(&mut lines, value.pointer("/run_start/machine_error"));
if let Some(flow) = value.get("human_flow") {
if let Some(device) = flow.get("Device") {
lines.push("flow: device".to_owned());
push_nested_string_field(&mut lines, device, "verification_url", "verify");
push_nested_string_field(&mut lines, device, "user_code", "code");
} else if let Some(browser) = flow.get("Browser") {
lines.push("flow: browser".to_owned());
push_nested_string_field(&mut lines, browser, "authorization_url", "open");
push_nested_string_field(&mut lines, browser, "callback_path", "callback");
}
}
if let Some(session) = value.get("session") {
lines.push(format!("session: {}", compact_json(session)));
}
if let Some(coordinator_selection) = value.get("coordinator") {
if coordinator_selection.is_object() {
lines.push(format!(
"coordinator: {}",
compact_json(coordinator_selection)
));
}
}
if let Some(reachability) = value.get("coordinator_reachability") {
if let Some(status) = reachability.get("status").and_then(Value::as_str) {
lines.push(format!("coordinator reachability: {status}"));
}
if let Some(error) = reachability.get("error").and_then(Value::as_str) {
lines.push(format!("coordinator error: {error}"));
}
if let Some(response_type) = reachability
.pointer("/response/type")
.and_then(Value::as_str)
{
lines.push(format!("coordinator ping: {response_type}"));
}
}
if let Some(response) = value
.get("response")
.or_else(|| value.get("coordinator_response"))
{
if let Some(response_type) = response.get("type").and_then(Value::as_str) {
lines.push(format!("coordinator response: {response_type}"));
}
}
if let Some(events) = value
.pointer("/events/response/events")
.and_then(Value::as_array)
{
lines.push(format!("events: {}", events.len()));
}
if let Some(events) = value
.pointer("/coordinator_response/response/events")
.and_then(Value::as_array)
{
lines.push(format!("events: {}", events.len()));
}
if let Some(requires_confirmation) = value.get("requires_confirmation").and_then(Value::as_bool)
{
lines.push(format!(
"confirmation: {}",
if requires_confirmation {
"required"
} else {
"confirmed"
}
));
}
if let Some(flag) = value
.get("private_website_required")
.and_then(Value::as_bool)
{
lines.push(format!("private website required: {flag}"));
}
if let Some(next_actions) = value.get("next_actions").and_then(Value::as_array) {
let actions = next_actions
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !actions.is_empty() {
lines.push(format!("next: {}", actions.join("; ")));
}
}
if let Some(auth) = value.get("auth").or_else(|| value.get("session")) {
if let Some(kind) = auth.get("kind").and_then(Value::as_str) {
lines.push(format!("auth: {kind}"));
}
if let Some(authenticated) = auth.get("authenticated").and_then(Value::as_bool) {
lines.push(format!("authenticated: {authenticated}"));
}
if let Some(expires) = auth.get("expires_at").and_then(Value::as_str) {
lines.push(format!("token expiry: {expires}"));
} else if let Some(posture) = auth.get("token_expiry_posture").and_then(Value::as_str) {
lines.push(format!("token expiry: {posture}"));
} else if auth.get("authenticated").is_some() {
lines.push("token expiry: unavailable".to_owned());
}
}
if let Some(dependencies) = value.get("dependencies").and_then(Value::as_object) {
let mut entries = dependencies
.iter()
.map(|(name, available)| {
format!(
"{name}={}",
if available.as_bool().unwrap_or(false) {
"ok"
} else {
"missing"
}
)
})
.collect::<Vec<_>>();
entries.sort();
if !entries.is_empty() {
lines.push(format!("dependencies: {}", entries.join(", ")));
}
}
if let Some(readiness) = value.get("node_readiness") {
push_nested_string_field(&mut lines, readiness, "os", "node os");
push_nested_string_field(&mut lines, readiness, "arch", "node arch");
if let Some(capabilities) = readiness.get("capabilities").and_then(Value::as_array) {
let caps = capabilities
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !caps.is_empty() {
lines.push(format!("node capabilities: {}", caps.join(", ")));
}
}
}
if let Some(summary) = value.get("node_readiness_summary") {
push_nested_string_field(&mut lines, summary, "status", "node readiness");
if let Some(missing) = summary
.get("missing_local_dependencies")
.and_then(Value::as_array)
{
let missing = missing.iter().filter_map(Value::as_str).collect::<Vec<_>>();
if !missing.is_empty() {
lines.push(format!("node missing dependencies: {}", missing.join(", ")));
}
}
if let Some(actions) = summary.get("next_actions").and_then(Value::as_array) {
let actions = actions.iter().filter_map(Value::as_str).collect::<Vec<_>>();
if !actions.is_empty() {
lines.push(format!("node next: {}", actions.join("; ")));
}
}
}
if let Some(disclosures) = value
.get("grant_disclosures")
.and_then(Value::as_array)
.filter(|disclosures| !disclosures.is_empty())
{
for disclosure in disclosures {
let grant = disclosure
.get("grant")
.and_then(Value::as_str)
.unwrap_or("capability");
let description = disclosure
.get("description")
.and_then(Value::as_str)
.unwrap_or("capability grant");
let policy = if disclosure
.get("coordinator_policy_limited")
.and_then(Value::as_bool)
.unwrap_or(false)
{
"policy-limited"
} else {
"unbounded"
};
lines.push(format!("grant {grant}: {description} ({policy})"));
}
}
lines.dedup();
lines.join("\n")
}
fn push_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
if let Some(text) = value.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {text}"));
} else if let Some(path) = value.get(key).and_then(|value| {
value
.as_object()
.and_then(|object| object.get("display"))
.and_then(Value::as_str)
}) {
lines.push(format!("{label}: {path}"));
}
}
fn push_task_placement_reasons(lines: &mut Vec<String>, tasks: &[Value]) {
for task in tasks {
let Some(placement) = task.get("node_placement") else {
continue;
};
let Some(reasons) = placement.get("reasons").and_then(Value::as_array) else {
continue;
};
let reasons = reasons.iter().filter_map(Value::as_str).collect::<Vec<_>>();
if reasons.is_empty() {
continue;
}
let task_name = task
.get("task")
.and_then(Value::as_str)
.unwrap_or("unknown");
let node = placement
.get("node")
.and_then(Value::as_str)
.unwrap_or("unknown");
lines.push(format!(
"placement {task_name}: {node} ({})",
reasons.join(", ")
));
}
}
fn push_task_locality_failures(lines: &mut Vec<String>, tasks: &[Value]) {
for task in tasks {
let Some(locality) = task.get("locality_failure") else {
continue;
};
if locality.is_null() {
continue;
}
let task_name = task
.get("task")
.and_then(Value::as_str)
.unwrap_or("unknown");
let affected = locality
.get("affected_data")
.and_then(Value::as_str)
.unwrap_or("direct_transfer");
let reason = locality
.get("reason")
.and_then(Value::as_str)
.unwrap_or("direct transfer or locality failed");
lines.push(format!("locality {task_name}: {affected} ({reason})"));
let actions = locality
.get("safe_next_actions")
.and_then(Value::as_array)
.map(|actions| actions.iter().filter_map(Value::as_str).collect::<Vec<_>>())
.unwrap_or_default();
if !actions.is_empty() {
lines.push(format!("locality next {task_name}: {}", actions.join("; ")));
}
}
}
fn push_nested_string_field(lines: &mut Vec<String>, value: &Value, key: &str, label: &str) {
if let Some(text) = value.get(key).and_then(Value::as_str) {
lines.push(format!("{label}: {text}"));
}
}
fn push_machine_error_line(lines: &mut Vec<String>, machine_error: Option<&Value>) {
let Some(machine_error) = machine_error else {
return;
};
if machine_error.is_null() {
return;
}
let category = machine_error
.get("category")
.and_then(Value::as_str)
.unwrap_or("unknown");
let exit_code = machine_error
.get("stable_exit_code")
.and_then(Value::as_i64)
.unwrap_or(1);
lines.push(format!("error category: {category} (exit {exit_code})"));
if let Some(next_actions) = machine_error.get("next_actions").and_then(Value::as_array) {
let actions = next_actions
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
if !actions.is_empty() {
lines.push(format!("error next: {}", actions.join("; ")));
}
}
}
fn push_source_provider_statuses(lines: &mut Vec<String>, statuses: &[Value]) {
let entries = statuses
.iter()
.filter_map(|status| {
let provider = status.get("provider").and_then(Value::as_str)?;
let state = status.get("status").and_then(Value::as_str)?;
let active = status
.get("active")
.and_then(Value::as_bool)
.unwrap_or(false);
Some(format!(
"{provider}={state}{}",
if active { "(active)" } else { "" }
))
})
.collect::<Vec<_>>();
if !entries.is_empty() {
lines.push(format!("source providers: {}", entries.join(", ")));
}
}
fn push_cli_diagnostics(lines: &mut Vec<String>, diagnostics: &[Value]) {
if diagnostics.is_empty() {
return;
}
lines.push(format!("diagnostics: {}", diagnostics.len()));
for diagnostic in diagnostics.iter().take(3) {
let severity = diagnostic
.get("severity")
.and_then(Value::as_str)
.unwrap_or("info");
let message = diagnostic
.get("message")
.and_then(Value::as_str)
.unwrap_or("diagnostic");
lines.push(format!("diagnostic {severity}: {message}"));
}
}
fn compact_json(value: &Value) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "<unprintable>".to_owned())
}
fn apply_command_report_exit_code(value: &mut Value) -> Option<i32> {
for pointer in [
"/machine_error",
"/run_start/machine_error",
"/restart_request/machine_error",
"/cancel_request/machine_error",
"/task_restart/machine_error",
] {
let Some(machine_error) = value.pointer_mut(pointer) else {
continue;
};
let Some(exit_code) = machine_error
.get("stable_exit_code")
.and_then(Value::as_i64)
.and_then(|code| i32::try_from(code).ok())
.filter(|code| *code != 0)
else {
continue;
};
if let Some(object) = machine_error.as_object_mut() {
object.insert("process_exit_code_applied".to_owned(), json!(true));
}
return Some(exit_code);
}
None
}
fn cli_error_summary(message: &str) -> Value {
let category = classify_cli_error_message(message);
cli_error_summary_for_category(category, message)
}
fn cli_error_summary_with_default(message: &str, default_category: &'static str) -> Value {
let category = classify_cli_error_message(message);
let category = if category == "unknown" {
default_category
} else {
category
};
cli_error_summary_for_category(category, message)
}
fn cli_error_summary_for_category(category: &'static str, message: &str) -> Value {
let mut summary = json!({
"category": category,
"stable_exit_code": cli_error_exit_code(category),
"process_exit_code_applied": false,
"retryable_after_user_action": cli_error_retryable_after_user_action(category),
"message": message,
"safe_failure": true,
"next_actions": cli_error_next_actions(category),
});
if category == "quota" {
if let Some(object) = summary.as_object_mut() {
object.insert(
"resource_category".to_owned(),
json!(
quota_error_resource_category(message).unwrap_or_else(|| "unknown".to_owned())
),
);
object.insert("community_tier_language".to_owned(), json!(true));
object.insert("private_abuse_heuristics_exposed".to_owned(), json!(false));
}
}
summary
}
fn quota_error_resource_category(message: &str) -> Option<String> {
let lower = message.to_ascii_lowercase();
for marker in [
"resource limit exceeded for ",
"quota unavailable for ",
"quota exceeded for ",
"limit exceeded for ",
"metering limit exceeded for ",
"quota limit for ",
"resource category ",
"resource=",
"resource:",
] {
if let Some(index) = lower.find(marker) {
let start = index + marker.len();
let rest = &message[start..];
let value: String = rest
.chars()
.skip_while(|ch| ch.is_ascii_whitespace())
.take_while(|ch| {
ch.is_ascii_alphanumeric()
|| *ch == '_'
|| *ch == '-'
|| *ch == '.'
|| *ch == ':'
})
.collect();
let value = value.trim_matches(|ch: char| ch == '.' || ch == ':' || ch == ',');
if !value.is_empty() {
return Some(value.to_owned());
}
}
}
None
}
fn classify_cli_error_message(message: &str) -> &'static str {
let message = message.to_ascii_lowercase();
if message.contains("already has active virtual process") || message.contains("active process")
{
return "active_process";
}
if message_mentions_locality_failure(&message) {
return "connectivity";
}
if message.contains("no capable node")
|| message.contains("missing capability")
|| message.contains("capability")
|| message.contains("placement failed")
|| message.contains("cmd.run")
|| message.contains("node required")
{
return "capability";
}
if message.contains("quota")
|| message.contains("resource limit")
|| message.contains("limit exceeded")
|| message.contains("metering")
{
return "quota";
}
if message.contains("policy")
|| message.contains("disallowed")
|| message.contains("not allowed")
|| message.contains("suspended")
|| message.contains("blocked by")
|| message.contains("denied")
{
return "policy";
}
if message.contains("unauthenticated")
|| message.contains("not authenticated")
|| message.contains("login required")
|| message.contains("browser login required")
|| message.contains("missing session")
|| message.contains("no cli session")
|| message.contains("401")
{
return "authentication";
}
if message.contains("unauthorized")
|| message.contains("not authorized")
|| message.contains("forbidden")
|| message.contains("permission")
|| message.contains("tenant mismatch")
|| message.contains("project mismatch")
{
return "authorization";
}
if message.contains("failed to connect")
|| message.contains("connection refused")
|| message.contains("closed session")
|| message.contains("timed out")
|| message.contains("timeout")
|| message.contains("name resolution")
|| message.contains("network unreachable")
|| message.contains("dns")
{
return "connectivity";
}
if message.contains("missing environment")
|| message.contains("environment mismatch")
|| message.contains("incompatible environment")
|| message.contains("containerfile")
|| message.contains("dockerfile")
|| message.contains("envs/")
{
return "environment";
}
if message.contains("task exited")
|| message.contains("exit status")
|| message.contains("status code")
|| message.contains("stderr")
|| message.contains("panic")
|| message.contains("program error")
|| message.contains("command failed")
{
return "program";
}
"unknown"
}
fn message_mentions_locality_failure(message: &str) -> bool {
message.contains("direct connectivity unavailable")
|| message.contains("direct transfer")
|| message.contains("source snapshot unavailable")
|| (message.contains("required artifact")
&& message.contains("unavailable")
&& message.contains("direct connectivity"))
|| message.contains("locality assumption")
}
fn cli_error_exit_code(category: &str) -> i64 {
match category {
"authentication" => 20,
"authorization" => 21,
"quota" => 22,
"policy" => 23,
"capability" => 24,
"connectivity" => 25,
"environment" => 26,
"program" => 27,
"active_process" => 28,
_ => 1,
}
}
fn cli_error_retryable_after_user_action(category: &str) -> bool {
matches!(
category,
"authentication"
| "authorization"
| "quota"
| "policy"
| "capability"
| "connectivity"
| "environment"
| "program"
| "active_process"
)
}
fn cli_error_next_actions(category: &str) -> Vec<&'static str> {
match category {
"authentication" => vec!["disasmer login --browser", "disasmer auth status"],
"authorization" => vec![
"disasmer auth status",
"check tenant/project selection",
"ask an admin to grant access",
],
"quota" => vec![
"disasmer quota status",
"reduce concurrent work or wait for usage to fall",
],
"policy" => vec![
"disasmer doctor",
"check coordinator policy for this action",
],
"capability" => vec![
"disasmer node list",
"attach a node with the required capabilities",
"check tenant/project on the attached node",
],
"connectivity" => vec![
"disasmer doctor",
"check the coordinator endpoint and network reachability",
],
"environment" => vec![
"disasmer inspect",
"check envs/<name>/Containerfile or envs/<name>/Dockerfile",
],
"program" => vec!["disasmer logs", "fix the program or task command and rerun"],
"active_process" => vec![
"disasmer process status",
"disasmer logs",
"disasmer process restart --yes",
"disasmer process cancel --yes",
"wait for the active process to finish",
],
_ => vec![
"disasmer doctor",
"rerun with --json for machine-readable details",
],
}
}
fn main() {
if let Err(error) = run_cli() {
let message = error.to_string();
let machine_error = cli_error_summary(&message);
let category = machine_error
.get("category")
.and_then(Value::as_str)
.unwrap_or("unknown");
let exit_code = machine_error
.get("stable_exit_code")
.and_then(Value::as_i64)
.and_then(|code| i32::try_from(code).ok())
.unwrap_or(1);
eprintln!("Error ({category}, exit {exit_code}): {error:#}");
std::process::exit(exit_code);
}
}
fn run_cli() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Doctor(args) => {
let json_output = args.scope.json;
let report = doctor_report(args, std::env::current_dir()?)?;
emit_report(&report, json_output)?;
}
Commands::Login(args) => {
let json_output = args.json;
if args.non_interactive
&& args.browser
&& !args.plan
&& args.complete_browser_code.is_none()
{
let report = non_interactive_browser_login_report(&args);
emit_report(&report, json_output)?;
} else if args.complete_browser_code.is_some() {
let report = execute_browser_login_completion(args)?;
emit_report(&report, json_output)?;
} else if args.browser && !args.plan {
let report = execute_interactive_browser_login(args)?;
if json_output {
emit_report(&report, true)?;
} else {
print_browser_login_success(&report);
}
} else {
let plan = login_plan(args);
emit_report(&plan, json_output)?;
}
}
Commands::Logout(args) => {
let json_output = args.scope.json;
let report = logout_report(args, std::env::current_dir()?, "logout")?;
emit_report(&report, json_output)?;
}
Commands::Auth { command } => {
let (report, json_output) = match command {
AuthCommands::Status(args) => {
let json_output = args.scope.json;
(
auth_status_report(args, std::env::current_dir()?)?,
json_output,
)
}
AuthCommands::Logout(args) => {
let json_output = args.scope.json;
(
auth_logout_report(args, std::env::current_dir()?)?,
json_output,
)
}
};
emit_report(&report, json_output)?;
}
Commands::Agent {
command: AgentCommands::Enroll(args),
} => {
let json_output = args.json;
let plan = agent_enrollment_plan(args);
emit_report(&plan, json_output)?;
}
Commands::Key { command } => {
let (report, json_output) = match command {
KeyCommands::Add(args) => {
let json_output = args.scope.json;
(key_add_report(args)?, json_output)
}
KeyCommands::List(args) => {
let json_output = args.scope.json;
(key_list_report(args)?, json_output)
}
KeyCommands::Revoke(args) => {
let json_output = args.scope.json;
(key_revoke_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Project { command } => {
let cwd = std::env::current_dir()?;
let (report, json_output) = match command {
ProjectCommands::Init(args) => {
let json_output = args.scope.json;
(project_init_report(args, cwd)?, json_output)
}
ProjectCommands::Status(args) => {
let json_output = args.scope.json;
(project_status_report(args, cwd)?, json_output)
}
ProjectCommands::List(args) => {
let json_output = args.scope.json;
(project_list_report(args, cwd)?, json_output)
}
ProjectCommands::Select(args) => {
let json_output = args.scope.json;
(project_select_report(args, cwd)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Inspect(args) => {
let json_output = args.json;
let inspection = bundle_inspection(args, std::env::current_dir()?)?;
emit_report(&inspection, json_output)?;
}
Commands::Build(args) => {
let json_output = args.json;
let report = build_report(args, std::env::current_dir()?)?;
emit_report(&report, json_output)?;
}
Commands::Bundle {
command: BundleCommands::Inspect(args),
} => {
let json_output = args.json;
let inspection = bundle_inspection(args, std::env::current_dir()?)?;
emit_report(&inspection, json_output)?;
}
Commands::Run(args) => {
let json_output = args.json;
let cwd = std::env::current_dir()?;
let session_project = args.project.clone().unwrap_or_else(|| cwd.clone());
let session = session_from_sources(&session_project)?;
let report = run_report(args, cwd, session)?;
emit_report(&report, json_output)?;
}
Commands::Node {
command: NodeCommands::Attach(args),
} => {
let json_output = args.json;
if args.coordinator.is_some() {
let report = execute_node_attach(args)?;
emit_report(&report, json_output)?;
} else {
let plan = attach_plan(args);
emit_report(&plan, json_output)?;
}
}
Commands::Node { command } => {
let (report, json_output) = match command {
NodeCommands::Enroll(args) => {
let json_output = args.scope.json;
(node_enroll_report(args)?, json_output)
}
NodeCommands::List(args) => {
let json_output = args.scope.json;
(node_list_report(args)?, json_output)
}
NodeCommands::Status(args) => {
let json_output = args.scope.json;
(node_status_report(args)?, json_output)
}
NodeCommands::Revoke(args) => {
let json_output = args.scope.json;
(node_revoke_report(args)?, json_output)
}
NodeCommands::Attach(_) => unreachable!("node attach is handled above"),
};
emit_report(&report, json_output)?;
}
Commands::Process { command } => {
let (report, json_output) = match command {
ProcessCommands::Status(args) => {
let json_output = args.scope.json;
(process_status_report(args)?, json_output)
}
ProcessCommands::Restart(args) => {
let json_output = args.scope.json;
(process_restart_report(args)?, json_output)
}
ProcessCommands::Cancel(args) => {
let json_output = args.scope.json;
(process_cancel_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Task { command } => {
let (report, json_output) = match command {
TaskCommands::List(args) => {
let json_output = args.scope.json;
(task_list_report(args)?, json_output)
}
TaskCommands::Restart(args) => {
let json_output = args.scope.json;
(task_restart_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Logs(args) => {
let json_output = args.scope.json;
emit_report(&logs_report(args)?, json_output)?;
}
Commands::Artifact { command } => {
let (report, json_output) = match command {
ArtifactCommands::List(args) => {
let json_output = args.scope.json;
(artifact_list_report(args)?, json_output)
}
ArtifactCommands::Download(args) => {
let json_output = args.scope.json;
(artifact_download_report(args)?, json_output)
}
ArtifactCommands::Export(args) => {
let json_output = args.scope.json;
(artifact_export_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
Commands::Dap(args) => {
let json_output = args.json;
if args.plan {
emit_report(&dap_plan(args)?, json_output)?;
} else {
return exec_dap(args);
}
}
Commands::Debug {
command: DebugCommands::Attach(args),
} => {
let json_output = args.scope.json;
emit_report(&debug_attach_report(args)?, json_output)?;
}
Commands::Quota {
command: QuotaCommands::Status(args),
} => {
let json_output = args.scope.json;
emit_report(
&quota_status_report(args, std::env::current_dir()?)?,
json_output,
)?;
}
Commands::Admin { command } => {
let (report, json_output) = match command {
AdminCommands::Status(args) => {
let json_output = args.scope.json;
(admin_status_report(args)?, json_output)
}
AdminCommands::Bootstrap(args) => {
let json_output = args.scope.json;
(
admin_bootstrap_report(args, std::env::current_dir()?)?,
json_output,
)
}
AdminCommands::RevokeNode(args) => {
let json_output = args.scope.json;
(node_revoke_report(args)?, json_output)
}
AdminCommands::StopProcess(args) => {
let json_output = args.scope.json;
(process_cancel_report(args)?, json_output)
}
AdminCommands::SuspendTenant(args) => {
let json_output = args.scope.json;
(admin_suspend_tenant_report(args)?, json_output)
}
};
emit_report(&report, json_output)?;
}
}
Ok(())
}
fn project_config_file(project: &Path) -> PathBuf {
project.join(".disasmer").join("project.json")
}
fn session_config_file(project: &Path) -> PathBuf {
project.join(".disasmer").join("session.json")
}
fn read_project_config(project: &Path) -> Result<Option<ProjectConfig>> {
let file = project_config_file(project);
if !file.exists() {
return Ok(None);
}
let bytes =
std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
let config = serde_json::from_slice(&bytes)
.with_context(|| format!("failed to parse {}", file.display()))?;
Ok(Some(config))
}
fn write_project_config(project: &Path, config: &ProjectConfig) -> Result<()> {
let file = project_config_file(project);
if let Some(parent) = file.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::write(&file, serde_json::to_vec_pretty(config)?)
.with_context(|| format!("failed to write {}", file.display()))
}
fn read_cli_session(project: &Path) -> Result<Option<StoredCliSession>> {
let file = session_config_file(project);
if !file.exists() {
return Ok(None);
}
let bytes =
std::fs::read(&file).with_context(|| format!("failed to read {}", file.display()))?;
let session = serde_json::from_slice(&bytes)
.with_context(|| format!("failed to parse {}", file.display()))?;
Ok(Some(session))
}
fn write_cli_session(project: &Path, session: &StoredCliSession) -> Result<PathBuf> {
let file = session_config_file(project);
if let Some(parent) = file.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create {}", parent.display()))?;
}
std::fs::write(&file, serde_json::to_vec_pretty(session)?)
.with_context(|| format!("failed to write {}", file.display()))?;
Ok(file)
}
fn effective_project_scope(scope: &CliScopeArgs, config: Option<&ProjectConfig>) -> CliScopeArgs {
CliScopeArgs {
coordinator: scope
.coordinator
.clone()
.or_else(|| config.and_then(|config| config.coordinator.clone())),
tenant: effective_scope_value(
&scope.tenant,
config.map(|config| config.tenant.as_str()),
"tenant",
),
project: effective_scope_value(
&scope.project,
config.map(|config| config.project.as_str()),
"project",
),
user: effective_scope_value(
&scope.user,
config.map(|config| config.user.as_str()),
"user",
),
json: scope.json,
}
}
fn effective_scope_value(
cli_value: &str,
config_value: Option<&str>,
default_value: &str,
) -> String {
if cli_value == default_value {
config_value.unwrap_or(cli_value).to_owned()
} else {
cli_value.to_owned()
}
}
fn auth_state_value(cwd: &Path) -> Result<Value> {
match session_from_env() {
CliSession::Anonymous => {
if let Some(session) = read_cli_session(cwd)? {
return Ok(json!({
"kind": session.kind,
"authenticated": true,
"source": "session_file",
"coordinator": session.coordinator,
"tenant": session.tenant,
"project": session.project,
"principal": session.user,
"cli_session_credential_kind": session.cli_session_credential_kind,
"provider_tokens_exposed_to_cli": session.provider_tokens_exposed_to_cli,
"provider_tokens_exposed_to_nodes": session.provider_tokens_sent_to_nodes,
"expires_at": session.expires_at,
"token_expiry_posture": session.token_expiry_posture,
}));
}
Ok(json!({
"kind": "anonymous",
"authenticated": false,
"source": "environment",
"token_expiry_posture": "no_session",
}))
}
CliSession::HumanSession => {
let expires_at = std::env::var("DISASMER_TOKEN_EXPIRES_AT").ok();
Ok(json!({
"kind": "human",
"authenticated": true,
"source": "DISASMER_TOKEN",
"provider_tokens_exposed_to_nodes": false,
"expires_at": expires_at,
"token_expiry_posture": if expires_at.is_some() { "expires_at" } else { "unknown_env_token" },
}))
}
CliSession::AgentPublicKey {
agent,
public_key_fingerprint,
browser_interaction_required,
} => Ok(json!({
"kind": "agent_public_key",
"authenticated": true,
"agent": agent,
"source": "DISASMER_AGENT_PUBLIC_KEY",
"public_key_fingerprint": public_key_fingerprint,
"browser_interaction_required": browser_interaction_required,
"token_expiry_posture": "not_applicable_public_key",
})),
}
}
fn command_available(command: &str) -> bool {
Command::new(command)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok()
}
fn sibling_binary(name: &str) -> Option<PathBuf> {
let mut sibling = std::env::current_exe().ok()?;
sibling.set_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX));
sibling.is_file().then_some(sibling)
}
fn dap_binary_path() -> Result<PathBuf> {
if let Some(path) = std::env::var_os("DISASMER_DAP_BIN") {
return Ok(PathBuf::from(path));
}
if let Some(path) = sibling_binary("disasmer-debug-dap") {
return Ok(path);
}
let release = PathBuf::from("target/release").join(format!(
"disasmer-debug-dap{}",
std::env::consts::EXE_SUFFIX
));
if release.is_file() {
return Ok(release);
}
let debug = PathBuf::from("target/debug").join(format!(
"disasmer-debug-dap{}",
std::env::consts::EXE_SUFFIX
));
if debug.is_file() {
return Ok(debug);
}
anyhow::bail!("could not locate disasmer-debug-dap; set DISASMER_DAP_BIN")
}
fn command_nonce(prefix: &str) -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default();
format!("{prefix}-{now}-{}", std::process::id())
}
fn list_task_events_if_available(
coordinator: Option<&str>,
scope: &CliScopeArgs,
process: Option<String>,
) -> Result<Option<Value>> {
let Some(coordinator) = coordinator else {
return Ok(None);
};
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "list_task_events",
"tenant": scope.tenant,
"project": scope.project,
"actor_user": scope.user,
"process": process,
}))?;
Ok(Some(json!({
"coordinator": coordinator,
"response": response,
"coordinator_session_requests": session.requests(),
})))
}
fn list_attached_nodes_if_available(
coordinator: Option<&str>,
scope: &CliScopeArgs,
) -> Result<Value> {
let Some(coordinator) = coordinator else {
return Ok(json!({
"checked": false,
"source": "no_coordinator",
"count": 0,
"online": 0,
"response": null,
}));
};
let mut session = JsonLineSession::connect(coordinator)?;
let response = session.request(json!({
"type": "list_node_descriptors",
"tenant": scope.tenant,
"project": scope.project,
"actor_user": scope.user,
}))?;
let descriptors = response
.get("descriptors")
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let online = descriptors
.iter()
.filter(|descriptor| {
descriptor
.get("online")
.and_then(Value::as_bool)
.unwrap_or(false)
})
.count();
Ok(json!({
"checked": true,
"source": "coordinator",
"coordinator": coordinator,
"count": descriptors.len(),
"online": online,
"response": response,
"coordinator_session_requests": session.requests(),
}))
}
fn discovered_environment_names(inspection: Option<&BundleInspection>) -> Vec<String> {
inspection
.map(|inspection| {
inspection
.metadata
.environments
.iter()
.map(|environment| environment.name.clone())
.collect()
})
.unwrap_or_default()
}
fn active_process_from_task_events(task_events: Option<&Value>) -> Option<String> {
let events = task_events?
.pointer("/response/events")
.and_then(Value::as_array)?;
let processes = events
.iter()
.filter_map(|event| event.get("process").and_then(Value::as_str))
.map(str::to_owned)
.collect::<BTreeSet<_>>();
match processes.len() {
0 => None,
1 => processes.into_iter().next(),
_ => Some(format!(
"multiple_observed:{}",
processes.into_iter().collect::<Vec<_>>().join(",")
)),
}
}
fn task_event_values(task_events: Option<&Value>) -> Vec<&Value> {
task_events
.and_then(|task_events| task_events.pointer("/response/events"))
.and_then(Value::as_array)
.map(|events| events.iter().collect())
.unwrap_or_default()
}
fn event_string(event: &Value, field: &str) -> Option<String> {
event.get(field).and_then(Value::as_str).map(str::to_owned)
}
fn event_u64(event: &Value, field: &str) -> Option<u64> {
event.get(field).and_then(Value::as_u64)
}
fn task_summaries(task_events: Option<&Value>) -> Value {
Value::Array(
task_event_values(task_events)
.into_iter()
.map(|event| {
let task = event_string(event, "task").unwrap_or_else(|| "unknown".to_owned());
let terminal_state =
event_string(event, "terminal_state").unwrap_or_else(|| "unknown".to_owned());
let node = event_string(event, "node");
let placement = event.get("placement");
let placement_reasons = placement
.and_then(|placement| placement.get("reasons"))
.cloned()
.unwrap_or_else(|| json!([]));
let placement_score = placement
.and_then(|placement| placement.get("score"))
.cloned()
.unwrap_or(Value::Null);
let failure_reason = task_failure_reason(event);
let locality_failure = task_locality_failure_from_reason(&failure_reason);
let machine_error = task_failure_machine_error_from_reason(event, &failure_reason);
json!({
"process": event_string(event, "process"),
"task": task,
"state": terminal_state,
"environment": event.get("environment").cloned().unwrap_or_else(|| json!("unknown_from_task_event")),
"environment_digest": event.get("environment_digest").cloned().unwrap_or(Value::Null),
"node_placement": {
"node": node,
"source": "coordinator_task_event",
"score": placement_score,
"reasons": placement_reasons,
"explanation_available": placement.is_some(),
},
"failure_reason": failure_reason,
"locality_failure": locality_failure,
"machine_error": machine_error,
"stdout_bytes": event_u64(event, "stdout_bytes").unwrap_or(0),
"stderr_bytes": event_u64(event, "stderr_bytes").unwrap_or(0),
})
})
.collect(),
)
}
fn task_failure_reason(event: &Value) -> Value {
match event.get("terminal_state").and_then(Value::as_str) {
Some("failed") => {
if let Some(stderr) = event.get("stderr_tail").and_then(Value::as_str) {
if !stderr.is_empty() {
return json!(redact_secret_like_text(stderr).0);
}
}
if let Some(status_code) = event.get("status_code").and_then(Value::as_i64) {
return json!(format!("task exited with status {status_code}"));
}
json!("task failed")
}
Some("cancelled") => json!("task cancelled"),
_ => Value::Null,
}
}
fn task_failure_machine_error_from_reason(event: &Value, reason: &Value) -> Value {
match event.get("terminal_state").and_then(Value::as_str) {
Some("failed") => {
let reason = reason.as_str().unwrap_or("task failed").to_owned();
let mut summary = cli_error_summary_with_default(&reason, "program");
if let Some(object) = summary.as_object_mut() {
if message_mentions_locality_failure(&reason.to_ascii_lowercase()) {
object.insert("locality_failure".to_owned(), json!(true));
object.insert(
"next_actions".to_owned(),
json!(locality_failure_next_actions(&reason)),
);
}
}
summary
}
Some("cancelled") => cli_error_summary_for_category("program", "task cancelled"),
_ => Value::Null,
}
}
fn task_locality_failure_from_reason(reason: &Value) -> Value {
let Some(reason) = reason.as_str() else {
return Value::Null;
};
let lower = reason.to_ascii_lowercase();
if !message_mentions_locality_failure(&lower) {
return Value::Null;
}
let affected_data = if lower.contains("source snapshot") {
"source_snapshot"
} else if lower.contains("artifact") {
"artifact"
} else {
"direct_transfer"
};
json!({
"category": "connectivity",
"affected_data": affected_data,
"reason": reason,
"coordinator_bulk_relay_used": false,
"safe_failure": true,
"safe_next_actions": locality_failure_next_actions(reason),
})
}
fn locality_failure_next_actions(reason: &str) -> Vec<&'static str> {
let lower = reason.to_ascii_lowercase();
if lower.contains("source snapshot") {
return vec![
"attach or select a node that already has the required source snapshot",
"rerun source preparation on an attached node",
"restore direct node-to-node connectivity and retry",
"do not rely on coordinator bulk source relay",
];
}
if lower.contains("artifact") {
return vec![
"attach or select a node that already has the required artifact",
"explicitly export or download the artifact before retrying",
"restore direct node-to-node connectivity and retry",
"do not rely on coordinator bulk artifact relay",
];
}
vec![
"check node direct connectivity and NAT traversal",
"attach a node with the needed source/artifact locality",
"retry after node connectivity is restored",
"do not rely on coordinator bulk relay",
]
}
fn process_state_from_tasks(task_events: Option<&Value>) -> &'static str {
let events = task_event_values(task_events);
if events.is_empty() {
return "no_tasks_observed";
}
if events.iter().any(|event| {
event
.get("terminal_state")
.and_then(Value::as_str)
.is_some_and(|state| state == "failed")
}) {
return "has_failed_tasks";
}
if events.iter().any(|event| {
event
.get("terminal_state")
.and_then(Value::as_str)
.is_some_and(|state| state == "cancelled")
}) {
return "has_cancelled_tasks";
}
if events.iter().all(|event| {
event
.get("terminal_state")
.and_then(Value::as_str)
.is_some_and(|state| state == "completed")
}) {
return "completed_tasks_observed";
}
"tasks_observed"
}
fn log_entries(task_events: Option<&Value>, task_filter: Option<&str>) -> Value {
Value::Array(
task_event_values(task_events)
.into_iter()
.filter(|event| {
task_filter.map_or(true, |task_filter| {
event
.get("task")
.and_then(Value::as_str)
.is_some_and(|task| task == task_filter)
})
})
.map(|event| {
let stdout_tail = event
.get("stdout_tail")
.and_then(Value::as_str)
.unwrap_or("");
let stderr_tail = event
.get("stderr_tail")
.and_then(Value::as_str)
.unwrap_or("");
let (stdout_tail, stdout_tail_redacted) = redact_secret_like_text(stdout_tail);
let (stderr_tail, stderr_tail_redacted) = redact_secret_like_text(stderr_tail);
json!({
"process": event_string(event, "process"),
"task": event_string(event, "task"),
"node": event_string(event, "node"),
"stdout_bytes": event_u64(event, "stdout_bytes").unwrap_or(0),
"stderr_bytes": event_u64(event, "stderr_bytes").unwrap_or(0),
"stdout_tail": stdout_tail,
"stderr_tail": stderr_tail,
"stdout_truncated": event.get("stdout_truncated").and_then(Value::as_bool).unwrap_or(false),
"stderr_truncated": event.get("stderr_truncated").and_then(Value::as_bool).unwrap_or(false),
"capped": true,
"secret_like_values_redacted": stdout_tail_redacted || stderr_tail_redacted,
"redacted_fields": redacted_log_fields(stdout_tail_redacted, stderr_tail_redacted),
})
})
.collect(),
)
}
fn redacted_log_fields(
stdout_tail_redacted: bool,
stderr_tail_redacted: bool,
) -> Vec<&'static str> {
let mut fields = Vec::new();
if stdout_tail_redacted {
fields.push("stdout_tail");
}
if stderr_tail_redacted {
fields.push("stderr_tail");
}
fields
}
fn redact_secret_like_text(text: &str) -> (String, bool) {
let markers = [
"access_token=",
"access_token:",
"refresh_token=",
"refresh_token:",
"id_token=",
"id_token:",
"api_key=",
"api_key:",
"api-key=",
"api-key:",
"token=",
"token:",
"secret=",
"secret:",
"password=",
"password:",
"passwd=",
"passwd:",
"bearer ",
];
let mut output = text.to_owned();
let mut redacted = false;
for marker in markers {
let (updated, changed) = redact_marker_values(output, marker);
output = updated;
redacted |= changed;
}
(output, redacted)
}
fn redact_marker_values(mut text: String, marker: &str) -> (String, bool) {
let mut changed = false;
let mut search_start = 0;
loop {
let lower = text.to_ascii_lowercase();
let Some(relative) = lower[search_start..].find(marker) else {
break;
};
let value_start = search_start + relative + marker.len();
let value_end = text[value_start..]
.char_indices()
.find_map(|(offset, character)| {
(character.is_whitespace()
|| matches!(
character,
'&' | '"' | '\'' | '`' | '<' | '>' | ',' | ';' | ')' | ']'
))
.then_some(value_start + offset)
})
.unwrap_or_else(|| text.len());
if value_start == value_end {
search_start = value_end;
if search_start >= text.len() {
break;
}
continue;
}
if text[value_start..value_end].starts_with("[redacted") {
search_start = value_end;
if search_start >= text.len() {
break;
}
continue;
}
text.replace_range(value_start..value_end, "[redacted]");
changed = true;
search_start = value_start + "[redacted]".len();
if search_start >= text.len() {
break;
}
}
(text, changed)
}
fn artifact_summaries(task_events: Option<&Value>) -> Value {
Value::Array(
task_event_values(task_events)
.into_iter()
.filter_map(|event| {
let path = event.get("artifact_path").and_then(Value::as_str)?;
let node = event_string(event, "node");
Some(json!({
"artifact": artifact_name_from_path(path),
"path": path,
"producer_task": event_string(event, "task"),
"producer_node": node,
"process": event_string(event, "process"),
"digest": event.get("artifact_digest").cloned().unwrap_or(Value::Null),
"size_bytes": event.get("artifact_size_bytes").cloned().unwrap_or(Value::Null),
"state": if event.get("artifact_digest").is_some() { "metadata_flushed" } else { "metadata_without_digest" },
"known_locations": node.into_iter().collect::<Vec<_>>(),
"durable_storage": false,
}))
})
.collect(),
)
}
fn artifact_name_from_path(path: &str) -> String {
path.rsplit('/').next().unwrap_or(path).to_owned()
}
fn response_error_message(response: &Value, fallback: &str) -> String {
response
.get("message")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| fallback.to_owned())
}
fn process_restart_request_summary(response: &Value, requires_confirmation: bool) -> Value {
if response.get("type").and_then(Value::as_str) != Some("process_started") {
let message = response_error_message(response, "coordinator rejected process restart");
return json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"operation": "restart_virtual_process",
"accepted": false,
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"error": message,
"machine_error": cli_error_summary(&message),
});
}
json!({
"status": "process_started",
"operation": "restart_virtual_process",
"accepted": true,
"process": response.get("process").cloned().unwrap_or(Value::Null),
"coordinator_epoch": response.get("epoch").cloned().unwrap_or(Value::Null),
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"website_required": false,
"single_active_process_boundary": true,
})
}
fn process_cancel_request_summary(response: &Value, requires_confirmation: bool) -> Value {
if response.get("type").and_then(Value::as_str) != Some("process_cancellation_requested") {
let message = response_error_message(response, "coordinator rejected process cancel");
return json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"operation": "cancel_virtual_process",
"accepted": false,
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"whole_process_cancel_available": true,
"error": message,
"machine_error": cli_error_summary(&message),
});
}
let cancelled_tasks = response
.get("cancelled_tasks")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
json!({
"status": "process_cancellation_requested",
"operation": "cancel_virtual_process",
"accepted": true,
"process": response.get("process").cloned().unwrap_or(Value::Null),
"cancelled_task_count": cancelled_tasks,
"cancelled_tasks": response.get("cancelled_tasks").cloned().unwrap_or_else(|| json!([])),
"affected_nodes": response.get("affected_nodes").cloned().unwrap_or_else(|| json!([])),
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"website_required": false,
"whole_process_cancel_available": true,
"node_must_poll_task_control": true,
"new_task_launches_blocked": true,
"surviving_state_visibility": "task and artifact state remains visible after terminal task events are reported",
})
}
fn task_restart_request_summary(response: &Value, requires_confirmation: bool) -> Value {
if response.get("type").and_then(Value::as_str) != Some("task_restart") {
let message = response_error_message(response, "coordinator rejected task restart");
return json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"operation": "restart_selected_task",
"accepted": false,
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"clean_boundary_required": true,
"error": message,
"machine_error": cli_error_summary(&message),
});
}
json!({
"status": "task_restart",
"operation": "restart_selected_task",
"accepted": response.get("accepted").cloned().unwrap_or_else(|| json!(false)),
"process": response.get("process").cloned().unwrap_or(Value::Null),
"task": response.get("task").cloned().unwrap_or(Value::Null),
"requires_confirmation": requires_confirmation,
"explicit_user_action": true,
"clean_boundary_required": true,
"clean_boundary_available": response
.get("clean_boundary_available")
.cloned()
.unwrap_or_else(|| json!(false)),
"active_task": response
.get("active_task")
.cloned()
.unwrap_or_else(|| json!(false)),
"completed_event_observed": response
.get("completed_event_observed")
.cloned()
.unwrap_or_else(|| json!(false)),
"requires_whole_process_restart": response
.get("requires_whole_process_restart")
.cloned()
.unwrap_or_else(|| json!(true)),
"message": response.get("message").cloned().unwrap_or(Value::Null),
"audit_event": response.get("audit_event").cloned().unwrap_or(Value::Null),
"charged_debug_read_bytes": response
.get("charged_debug_read_bytes")
.cloned()
.unwrap_or_else(|| json!(0)),
"used_debug_read_bytes": response
.get("used_debug_read_bytes")
.cloned()
.unwrap_or_else(|| json!(0)),
"debug_reads_quota_limited": true,
"website_required": false,
})
}
fn artifact_download_session_summary(response: &Value) -> Value {
if response.get("type").and_then(Value::as_str) != Some("artifact_download_link") {
return json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"link_issued": false,
"explicit_user_action_required": true,
"error": response.get("message").cloned().unwrap_or(Value::Null),
});
}
let link = response.get("link").unwrap_or(&Value::Null);
json!({
"status": "download_link_issued",
"link_issued": true,
"explicit_user_action_required": true,
"coordinator_preflight": "completed_before_link_issued",
"tenant": link.get("tenant").cloned().unwrap_or(Value::Null),
"project": link.get("project").cloned().unwrap_or(Value::Null),
"process": link.get("process").cloned().unwrap_or(Value::Null),
"artifact": link.get("artifact").cloned().unwrap_or(Value::Null),
"actor": link.get("actor").cloned().unwrap_or(Value::Null),
"source": link.get("source").cloned().unwrap_or(Value::Null),
"url_path": link.get("url_path").cloned().unwrap_or(Value::Null),
"expires_at_epoch_seconds": link.get("expires_at_epoch_seconds").cloned().unwrap_or(Value::Null),
"max_bytes": link.get("max_bytes").cloned().unwrap_or(Value::Null),
"token_material_returned": false,
"scoped_token_digest_present": link.get("scoped_token_digest").is_some(),
"policy_context_digest_present": link.get("policy_context_digest").is_some(),
"cross_tenant_usable": false,
"default_durable_store_assumed": false,
})
}
fn artifact_export_plan_summary(response: &Value, to: &Path) -> Value {
if response.get("type").and_then(Value::as_str) != Some("artifact_export_plan") {
return json!({
"status": response.get("type").and_then(Value::as_str).unwrap_or("coordinator_response"),
"explicit_user_action": true,
"local_path": to,
"local_bytes_written_by_cli": false,
"default_durable_store_assumed": false,
"error": response.get("message").cloned().unwrap_or(Value::Null),
});
}
let plan = response.get("plan").unwrap_or(&Value::Null);
json!({
"status": "transfer_plan_created",
"explicit_user_action": true,
"local_path": to,
"local_bytes_written_by_cli": false,
"writes_require_data_plane_followup": true,
"default_durable_store_assumed": false,
"artifact_size_bytes": response.get("artifact_size_bytes").cloned().unwrap_or(Value::Null),
"source_node": response.get("source_node").cloned().unwrap_or(Value::Null),
"receiver_node": response.get("receiver_node").cloned().unwrap_or(Value::Null),
"transport": plan.get("transport").cloned().unwrap_or(Value::Null),
"artifact": plan.pointer("/scope/object/Artifact").cloned().unwrap_or(Value::Null),
"tenant": plan.pointer("/scope/tenant").cloned().unwrap_or(Value::Null),
"project": plan.pointer("/scope/project").cloned().unwrap_or(Value::Null),
"process": plan.pointer("/scope/process").cloned().unwrap_or(Value::Null),
"coordinator_assisted_rendezvous": plan.get("coordinator_assisted_rendezvous").cloned().unwrap_or(Value::Null),
"coordinator_bulk_relay_allowed": plan.get("coordinator_bulk_relay_allowed").cloned().unwrap_or(Value::Null),
"authorization_digest_present": plan.get("authorization_digest").is_some(),
})
}
fn task_event_count(task_events: Option<&Value>) -> usize {
task_events
.and_then(|task_events| task_events.pointer("/response/events"))
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0)
}
fn project_quota_posture(attached_nodes: &Value, task_events: Option<&Value>) -> Value {
let current_usage = quota_current_usage(attached_nodes, task_events);
let next_blocked_action = quota_next_blocked_action(&current_usage);
json!({
"source": "cli_project_status_summary",
"current_usage": current_usage,
"limits": quota_limits_value(),
"next_blocked_action": next_blocked_action,
"private_abuse_heuristics_exposed": false,
})
}
fn quota_current_usage(attached_nodes: &Value, task_events: Option<&Value>) -> Value {
let attached_node_count = attached_nodes
.get("count")
.and_then(Value::as_u64)
.unwrap_or(0);
let online_node_count = attached_nodes
.get("online")
.and_then(Value::as_u64)
.unwrap_or(0);
json!({
"attached_nodes": attached_node_count,
"online_nodes": online_node_count,
"observed_task_events": task_event_count(task_events),
"artifact_download_bytes": 0,
"rendezvous_attempts": 0,
"hosted_wasm_processes": 0,
})
}
fn quota_limits_value() -> Value {
let limits = ResourceLimits::community_tier_defaults();
json!({
"api_calls": limits.limit(&LimitKind::ApiCall),
"spawns": limits.limit(&LimitKind::Spawn),
"log_bytes": limits.limit(&LimitKind::LogBytes),
"metadata_bytes": limits.limit(&LimitKind::MetadataBytes),
"debug_read_bytes": limits.limit(&LimitKind::DebugReadBytes),
"ui_events": limits.limit(&LimitKind::UiEvent),
"rendezvous_attempts": limits.limit(&LimitKind::RendezvousAttempt),
"artifact_download_bytes": limits.limit(&LimitKind::ArtifactDownloadBytes),
"hosted_fuel": limits.limit(&LimitKind::HostedFuel),
"hosted_memory_bytes": limits.limit(&LimitKind::HostedMemoryBytes),
"hosted_wall_clock_ms": limits.limit(&LimitKind::HostedWallClockMs),
"hosted_state_bytes": limits.limit(&LimitKind::HostedStateBytes),
"hosted_tuning_private": true,
})
}
fn quota_next_blocked_action(current_usage: &Value) -> Value {
if current_usage
.get("online_nodes")
.and_then(Value::as_u64)
.unwrap_or(0)
== 0
{
return json!({
"action": "node_work_requires_online_attached_node",
"category": "capability",
"quota_related": false,
"message": "no online attached node is visible for work that requires a user node",
"machine_error": cli_error_summary_for_category(
"capability",
"no online attached node is visible for work that requires a user node"
)
});
}
Value::Null
}
fn login_plan(args: LoginArgs) -> LoginPlan {
login_plan_with_nonce(args, login_nonce())
}
fn default_operator_endpoint() -> String {
DEFAULT_OPERATOR_ENDPOINT.to_owned()
}
fn login_plan_with_nonce(args: LoginArgs, nonce: String) -> LoginPlan {
login_plan_with_nonce_and_callback(args, nonce, browser_callback_url())
}
fn login_plan_with_nonce_and_callback(
args: LoginArgs,
nonce: String,
callback_path: String,
) -> LoginPlan {
let challenge = Digest::from_parts([
b"cli-login-challenge:v1".as_slice(),
args.coordinator.as_bytes(),
nonce.as_bytes(),
]);
let human_flow = if args.browser {
let state = challenge.as_str().to_owned();
LoginFlowPlan::Browser(BrowserLoginFlow {
authorization_url: browser_authorization_url(&args, &state, &callback_path),
callback_path,
state,
})
} else {
LoginFlowPlan::Device(CliLoginFlow {
verification_url: format!("{}/auth/device", args.coordinator),
user_code: device_user_code(&challenge),
device_code: challenge.as_str().to_owned(),
expires_in_seconds: 900,
yields_long_lived_secret_directly: false,
})
};
LoginPlan {
coordinator: args.coordinator,
human_flow,
}
}
fn execute_browser_login_completion(args: LoginArgs) -> Result<LoginCompletionReport> {
let plan = login_plan_with_nonce(args.clone(), login_nonce());
execute_browser_login_completion_for_plan(args, plan)
}
fn execute_browser_login_completion_for_plan(
args: LoginArgs,
plan: LoginPlan,
) -> Result<LoginCompletionReport> {
if !args.browser {
anyhow::bail!("browser login completion requires --browser");
}
let authorization_code = args
.complete_browser_code
.clone()
.context("--complete-browser-code requires an authorization code")?;
let issuer_url = args
.oidc_issuer_url
.clone()
.unwrap_or_else(default_oidc_issuer_url);
let coordinator = args.coordinator.clone();
let tenant = args.tenant.clone();
let project = args.project.clone();
let user = args.user.clone();
let client_id = args.oidc_client_id.clone();
let LoginFlowPlan::Browser(flow) = &plan.human_flow else {
anyhow::bail!("browser login completion requires a browser flow");
};
let state = args.state.clone().unwrap_or_else(|| flow.state.clone());
let mut session = JsonLineSession::connect(&coordinator)?;
let coordinator_response = session.request(json!({
"type": "oidc_browser_login",
"tenant": tenant,
"project": project,
"user": user,
"issuer_url": issuer_url,
"client_id": client_id,
"redirect_path": flow.callback_path.clone(),
"state": state,
"authorization_code": authorization_code,
}))?;
let scoped_cli_session_received = coordinator_response
.pointer("/session/cli_session_credential_kind")
.and_then(Value::as_str)
== Some("CliDeviceSession");
let provider_tokens_sent_to_nodes = coordinator_response
.pointer("/session/provider_tokens_sent_to_nodes")
.and_then(Value::as_bool)
.unwrap_or(true);
let provider_tokens_exposed_to_cli = contains_provider_token_field(&coordinator_response);
let local_cli_session_file_written = if scoped_cli_session_received {
let cwd = std::env::current_dir()?;
let stored_session = stored_cli_session_from_login_response(
&args,
&coordinator,
&coordinator_response,
provider_tokens_exposed_to_cli,
provider_tokens_sent_to_nodes,
);
write_cli_session(&cwd, &stored_session)?;
true
} else {
false
};
Ok(LoginCompletionReport {
plan,
boundary: LoginCompletionBoundaryEvidence {
cli_contacted_coordinator: true,
coordinator_address: coordinator,
scoped_cli_session_received,
local_cli_session_file_written,
provider_tokens_persisted_locally: false,
provider_tokens_exposed_to_cli,
provider_tokens_sent_to_nodes,
coordinator_session_requests: session.requests(),
},
coordinator_response,
})
}
fn stored_cli_session_from_login_response(
args: &LoginArgs,
coordinator: &str,
coordinator_response: &Value,
provider_tokens_exposed_to_cli: bool,
provider_tokens_sent_to_nodes: bool,
) -> StoredCliSession {
let session = coordinator_response.get("session").unwrap_or(&Value::Null);
let expires_at = session
.get("expires_at")
.or_else(|| session.get("token_expires_at"))
.and_then(Value::as_str)
.map(str::to_owned);
StoredCliSession {
kind: "human".to_owned(),
coordinator: coordinator.to_owned(),
tenant: session
.get("tenant")
.and_then(Value::as_str)
.unwrap_or(&args.tenant)
.to_owned(),
project: session
.get("project")
.and_then(Value::as_str)
.unwrap_or(&args.project)
.to_owned(),
user: session
.get("user")
.and_then(Value::as_str)
.unwrap_or(&args.user)
.to_owned(),
cli_session_credential_kind: session
.get("cli_session_credential_kind")
.and_then(Value::as_str)
.unwrap_or("CliDeviceSession")
.to_owned(),
token_expiry_posture: if expires_at.is_some() {
"expires_at".to_owned()
} else {
"unknown_coordinator_session".to_owned()
},
expires_at,
provider_tokens_exposed_to_cli,
provider_tokens_sent_to_nodes,
created_at_unix_seconds: unix_timestamp_seconds(),
}
}
fn execute_interactive_browser_login(mut args: LoginArgs) -> Result<LoginCompletionReport> {
args.browser = true;
let callback_path = browser_callback_url();
let listener = TcpListener::bind(BROWSER_CALLBACK_ADDR).with_context(|| {
format!("failed to listen for browser login callback on {BROWSER_CALLBACK_ADDR}")
})?;
let plan = login_plan_with_nonce_and_callback(args.clone(), login_nonce(), callback_path);
let LoginFlowPlan::Browser(flow) = &plan.human_flow else {
anyhow::bail!("browser login requires a browser flow");
};
eprintln!("Opening Disasmer browser login: {}", flow.authorization_url);
eprintln!("Waiting for login callback on {}", flow.callback_path);
open_browser(&flow.authorization_url)?;
let callback = receive_browser_callback(&listener)?;
if callback.state != flow.state {
anyhow::bail!("browser login callback state did not match the requested login state");
}
args.complete_browser_code = Some(callback.authorization_code);
args.state = Some(callback.state);
execute_browser_login_completion_for_plan(args, plan)
}
fn print_browser_login_success(report: &LoginCompletionReport) {
let session = report.coordinator_response.get("session");
let tenant = session
.and_then(|value| value.get("tenant"))
.and_then(Value::as_str)
.unwrap_or("tenant");
let project = session
.and_then(|value| value.get("project"))
.and_then(Value::as_str)
.unwrap_or("project");
let user = session
.and_then(|value| value.get("user"))
.and_then(Value::as_str)
.unwrap_or("user");
println!(
"Signed in to {} as {user} for {tenant}/{project}.",
report.plan.coordinator
);
if report.boundary.scoped_cli_session_received {
println!("Received a scoped CLI session from the coordinator.");
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct BrowserCallback {
authorization_code: String,
state: String,
}
fn browser_callback_url() -> String {
format!("http://{BROWSER_CALLBACK_ADDR}{BROWSER_CALLBACK_PATH}")
}
fn default_oidc_issuer_url() -> String {
DEFAULT_OIDC_ISSUER_URL.to_owned()
}
fn browser_authorization_url(args: &LoginArgs, state: &str, callback_path: &str) -> String {
if let Some(issuer_url) = &args.oidc_issuer_url {
return oidc_authorization_url(issuer_url, &args.oidc_client_id, state, callback_path);
}
let start = if args.coordinator == DEFAULT_OPERATOR_ENDPOINT {
DEFAULT_BROWSER_LOGIN_START.to_owned()
} else {
format!(
"{}/auth/browser/start",
args.coordinator.trim_end_matches('/')
)
};
format!(
"{start}?client_id={}&state={}&redirect_uri={}",
percent_encode(&args.oidc_client_id),
percent_encode(state),
percent_encode(callback_path)
)
}
fn oidc_authorization_url(
issuer_url: &str,
client_id: &str,
state: &str,
callback_path: &str,
) -> String {
format!(
"{}/application/o/authorize/?client_id={}&response_type=code&scope=openid%20profile%20email&state={}&redirect_uri={}",
issuer_url.trim_end_matches('/'),
percent_encode(client_id),
percent_encode(state),
percent_encode(callback_path)
)
}
fn open_browser(url: &str) -> Result<()> {
let mut command = if let Some(command) = std::env::var_os("DISASMER_BROWSER_OPEN_COMMAND") {
Command::new(command)
} else {
platform_browser_command()
};
command.arg(url);
command
.spawn()
.with_context(|| format!("failed to start browser opener for {url}"))?;
Ok(())
}
#[cfg(target_os = "macos")]
fn platform_browser_command() -> Command {
Command::new("open")
}
#[cfg(target_os = "windows")]
fn platform_browser_command() -> Command {
let mut command = Command::new("cmd");
command.args(["/C", "start", ""]);
command
}
#[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
fn platform_browser_command() -> Command {
Command::new("xdg-open")
}
fn receive_browser_callback(listener: &TcpListener) -> Result<BrowserCallback> {
listener
.set_nonblocking(true)
.context("failed to configure browser callback listener")?;
let timeout = browser_callback_timeout();
let started = Instant::now();
let mut stream = loop {
match listener.accept() {
Ok((stream, _)) => break stream,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
if started.elapsed() >= timeout {
anyhow::bail!(
"timed out waiting for browser login callback on {BROWSER_CALLBACK_ADDR}"
);
}
std::thread::sleep(Duration::from_millis(50));
}
Err(error) => return Err(error).context("failed to accept browser login callback"),
}
};
let callback = read_browser_callback_request(&stream);
match &callback {
Ok(_) => write_browser_callback_response(
&mut stream,
"200 OK",
"Disasmer login callback received. You can return to the terminal.",
)?,
Err(error) => write_browser_callback_response(
&mut stream,
"400 Bad Request",
&format!("Disasmer login callback was rejected: {error}"),
)?,
}
callback
}
fn browser_callback_timeout() -> Duration {
let seconds = std::env::var("DISASMER_BROWSER_LOGIN_TIMEOUT_SECONDS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|seconds| *seconds > 0)
.unwrap_or(DEFAULT_BROWSER_LOGIN_CALLBACK_TIMEOUT_SECONDS);
Duration::from_secs(seconds)
}
fn unix_timestamp_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn read_browser_callback_request(stream: &TcpStream) -> Result<BrowserCallback> {
let mut reader = BufReader::new(
stream
.try_clone()
.context("failed to read browser callback request")?,
);
let mut request_line = String::new();
reader
.read_line(&mut request_line)
.context("failed to read browser callback request line")?;
parse_browser_callback_request_line(&request_line)
}
fn parse_browser_callback_request_line(line: &str) -> Result<BrowserCallback> {
let mut parts = line.split_whitespace();
let method = parts.next().context("browser callback omitted method")?;
let target = parts.next().context("browser callback omitted path")?;
if method != "GET" {
anyhow::bail!("browser callback must use GET");
}
let (path, query) = target
.split_once('?')
.context("browser callback omitted query string")?;
if path != BROWSER_CALLBACK_PATH {
anyhow::bail!("browser callback used unexpected path {path}");
}
let mut authorization_code = None;
let mut state = None;
for pair in query.split('&').filter(|item| !item.is_empty()) {
let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
match key {
"code" => authorization_code = Some(percent_decode(value)?),
"state" => state = Some(percent_decode(value)?),
_ => {}
}
}
Ok(BrowserCallback {
authorization_code: authorization_code.context("browser callback omitted code")?,
state: state.context("browser callback omitted state")?,
})
}
fn write_browser_callback_response(stream: &mut TcpStream, status: &str, body: &str) -> Result<()> {
let body = html_escape(body);
let html = format!(
"<!doctype html><html><head><meta charset=\"utf-8\"><title>Disasmer login</title></head><body><h1>Disasmer login</h1><p>{body}</p></body></html>"
);
write!(
stream,
"HTTP/1.1 {status}\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{html}",
html.len()
)
.context("failed to write browser callback response")
}
fn percent_encode(input: &str) -> String {
let mut encoded = String::new();
for byte in input.as_bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
encoded.push(*byte as char);
} else {
encoded.push_str(&format!("%{byte:02X}"));
}
}
encoded
}
fn percent_decode(input: &str) -> Result<String> {
let mut decoded = Vec::new();
let bytes = input.as_bytes();
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'+' => {
decoded.push(b' ');
index += 1;
}
b'%' => {
let high = bytes
.get(index + 1)
.copied()
.and_then(hex_digit)
.context("invalid percent-encoded callback value")?;
let low = bytes
.get(index + 2)
.copied()
.and_then(hex_digit)
.context("invalid percent-encoded callback value")?;
decoded.push((high << 4) | low);
index += 3;
}
byte => {
decoded.push(byte);
index += 1;
}
}
}
String::from_utf8(decoded).context("callback value was not UTF-8")
}
fn hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn html_escape(input: &str) -> String {
input
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
.replace('\'', "&#39;")
}
fn contains_provider_token_field(value: &Value) -> bool {
match value {
Value::Object(object) => object.iter().any(|(key, value)| {
matches!(
key.as_str(),
"access_token" | "refresh_token" | "id_token" | "provider_token" | "oauth_token"
) || contains_provider_token_field(value)
}),
Value::Array(items) => items.iter().any(contains_provider_token_field),
_ => false,
}
}
fn login_nonce() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_nanos())
.unwrap_or_default();
format!("{now}:{}", std::process::id())
}
fn device_user_code(challenge: &Digest) -> String {
let hex = challenge.as_str().trim_start_matches("sha256:");
let first = hex.get(0..4).unwrap_or(hex).to_ascii_uppercase();
let second = hex.get(4..8).unwrap_or(hex).to_ascii_uppercase();
format!("DISASMER-{first}-{second}")
}
fn agent_enrollment_plan(args: AgentEnrollArgs) -> AgentEnrollmentPlan {
AgentEnrollmentPlan {
public_key_fingerprint: Digest::sha256(args.public_key),
browser_interaction_required_each_run: false,
}
}
fn bundle_inspection(args: BundleInspectArgs, cwd: PathBuf) -> Result<BundleInspection> {
let project = args.project.unwrap_or(cwd);
let model = ProjectModel::discover_without_config(&project)?;
let selected_inputs = discover_selected_inputs(&project)?;
let provider_selection = source_provider_selection(
&project,
args.source_provider.as_deref(),
&args.disabled_source_providers,
);
let source_provider_manifest = source_provider_manifest(&provider_selection.active_provider);
source_provider_manifest.validate_public_mvp()?;
let environment_diagnostics =
environment_diagnostics_for_inputs(&project, &selected_inputs, &model.environments)?;
let identity_inputs = BundleIdentityInputs {
wasm_code: wasm_source_proxy_digest(&selected_inputs),
task_abi: task_abi_digest(&model),
entrypoints: model.entrypoints.keys().cloned().collect(),
default_entrypoint: model.default_entrypoint.clone(),
environments: model.environments.clone(),
source_provider_manifest: source_provider_manifest.digest.clone(),
source_transfer_policy: source_provider_manifest.transfer_policy.clone(),
selected_inputs,
};
let pre_schedule_diagnostics = pre_schedule_diagnostics(
&provider_selection.statuses,
&environment_diagnostics,
&model.environments,
);
Ok(BundleInspection {
project,
default_source_providers: vec![SourceProviderKind::Filesystem, SourceProviderKind::Git],
source_provider_manifest,
source_provider_statuses: provider_selection.statuses,
environment_diagnostics,
pre_schedule_diagnostics,
metadata: identity_inputs.inspectable_metadata(),
})
}
fn discover_selected_inputs(project: &Path) -> Result<Vec<SelectedInput>> {
let mut inputs = Vec::new();
for path in [
"Cargo.toml",
"Cargo.lock",
"src/main.rs",
"src/lib.rs",
"src/build.rs",
] {
let absolute = project.join(path);
if !absolute.is_file() {
continue;
}
let bytes = std::fs::read(&absolute)?;
inputs.push(SelectedInput {
path: path.to_owned(),
digest: Digest::from_parts([
b"bundle-selected-input:v1".as_slice(),
path.as_bytes(),
bytes.as_slice(),
]),
});
}
Ok(inputs)
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SourceProviderSelection {
active_provider: SourceProviderKind,
statuses: Vec<SourceProviderStatus>,
}
fn source_provider_selection(
project: &Path,
requested_provider: Option<&str>,
disabled_providers: &[String],
) -> SourceProviderSelection {
let has_git_checkout = project.join(".git").exists();
let disabled = disabled_providers
.iter()
.map(|provider| provider.trim().to_ascii_lowercase())
.collect::<BTreeSet<_>>();
let requested = requested_provider.map(source_provider_kind_from_id);
let active_provider = requested.clone().unwrap_or_else(|| {
if has_git_checkout && !disabled.contains("git") {
SourceProviderKind::Git
} else {
SourceProviderKind::Filesystem
}
});
let active_provider_id = active_provider.provider_id().to_owned();
let mut statuses = vec![
builtin_source_provider_status(
SourceProviderKind::Filesystem,
true,
disabled.contains("filesystem"),
active_provider_id == "filesystem",
),
builtin_source_provider_status(
SourceProviderKind::Git,
has_git_checkout,
disabled.contains("git"),
active_provider_id == "git",
),
SourceProviderStatus {
provider: "custom".to_owned(),
status: "disabled".to_owned(),
active: false,
reason: "no custom source-provider module was configured for this project".to_owned(),
coordinator_checkout_required: false,
coordinator_receives_source_bytes_by_default: false,
},
];
if let Some(SourceProviderKind::Custom(provider)) = requested {
statuses.push(SourceProviderStatus {
provider,
status: "unsupported".to_owned(),
active: true,
reason: "custom source-provider modules must be supplied by public plugin/API code before use".to_owned(),
coordinator_checkout_required: false,
coordinator_receives_source_bytes_by_default: false,
});
}
SourceProviderSelection {
active_provider,
statuses,
}
}
fn source_provider_kind_from_id(provider: &str) -> SourceProviderKind {
match provider.trim().to_ascii_lowercase().as_str() {
"filesystem" => SourceProviderKind::Filesystem,
"git" => SourceProviderKind::Git,
other => SourceProviderKind::Custom(other.to_owned()),
}
}
fn builtin_source_provider_status(
kind: SourceProviderKind,
available: bool,
disabled: bool,
active: bool,
) -> SourceProviderStatus {
let provider = kind.provider_id().to_owned();
let (status, reason) = if disabled {
(
"disabled",
format!("source provider `{provider}` was disabled by CLI override"),
)
} else if available {
(
"enabled",
format!("source provider `{provider}` is available in this checkout"),
)
} else {
(
"missing",
format!("source provider `{provider}` is not available for this checkout"),
)
};
SourceProviderStatus {
provider,
status: status.to_owned(),
active,
reason,
coordinator_checkout_required: false,
coordinator_receives_source_bytes_by_default: false,
}
}
fn source_provider_manifest(kind: &SourceProviderKind) -> SourceProviderManifest {
SourceProviderManifest::local_first(
kind.clone(),
"default source provider manifest; snapshot creation can be scheduled as a node task",
)
}
fn environment_diagnostics_for_inputs(
project: &Path,
selected_inputs: &[SelectedInput],
environments: &[disasmer_core::EnvironmentResource],
) -> Result<Vec<EnvironmentDiagnosticReport>> {
let mut diagnostics = Vec::new();
for input in selected_inputs {
if !input.path.ends_with(".rs") {
continue;
}
let source = std::fs::read_to_string(project.join(&input.path))
.with_context(|| format!("failed to read {}", project.join(&input.path).display()))?;
diagnostics.extend(
diagnose_environment_references(&source, environments)
.into_iter()
.map(|diagnostic| EnvironmentDiagnosticReport {
path: input.path.clone(),
reference: diagnostic.reference,
message: diagnostic.message,
}),
);
}
Ok(diagnostics)
}
fn pre_schedule_diagnostics(
source_provider_statuses: &[SourceProviderStatus],
environment_diagnostics: &[EnvironmentDiagnosticReport],
environments: &[disasmer_core::EnvironmentResource],
) -> Vec<CliDiagnostic> {
let mut diagnostics = Vec::new();
diagnostics.extend(
environment_diagnostics
.iter()
.map(|diagnostic| CliDiagnostic {
severity: "error".to_owned(),
category: "environment".to_owned(),
code: "missing_environment".to_owned(),
message: format!("{} at {}", diagnostic.message, diagnostic.path),
next_actions: vec![
"create the missing envs/<name>/Containerfile or envs/<name>/Dockerfile"
.to_owned(),
"rerun disasmer inspect".to_owned(),
],
}),
);
diagnostics.extend(
source_provider_statuses
.iter()
.filter(|status| {
status.active
&& matches!(
status.status.as_str(),
"missing" | "disabled" | "unsupported"
)
})
.map(|status| CliDiagnostic {
severity: "error".to_owned(),
category: "source_provider".to_owned(),
code: format!("source_provider_{}", status.status),
message: format!(
"active source provider `{}` is {}: {}",
status.provider, status.status, status.reason
),
next_actions: vec![
"choose an available source provider with --source-provider".to_owned(),
"rerun disasmer inspect --json".to_owned(),
],
}),
);
for environment in environments {
if environment.requirements.capabilities.is_empty() {
continue;
}
let mut capabilities = environment
.requirements
.capabilities
.iter()
.map(|capability| format!("{capability:?}"))
.collect::<Vec<_>>();
capabilities.sort();
diagnostics.push(CliDiagnostic {
severity: "info".to_owned(),
category: "capability".to_owned(),
code: "environment_capability_requirements".to_owned(),
message: format!(
"environment `{}` requires node capabilities: {}",
environment.name,
capabilities.join(", ")
),
next_actions: vec![
"attach a node that reports these capabilities before scheduling work".to_owned(),
],
});
}
diagnostics
}
fn wasm_source_proxy_digest(selected_inputs: &[SelectedInput]) -> Digest {
let mut parts = vec![b"wasm-source-proxy:v1".to_vec()];
for input in selected_inputs {
parts.push(input.path.as_bytes().to_vec());
parts.push(input.digest.as_str().as_bytes().to_vec());
}
Digest::from_parts(parts)
}
fn task_abi_digest(model: &ProjectModel) -> Digest {
let mut parts = vec![b"task-abi:v1".to_vec()];
for entrypoint in model.entrypoints.values() {
parts.push(entrypoint.name.as_bytes().to_vec());
parts.push(entrypoint.function.as_bytes().to_vec());
}
Digest::from_parts(parts)
}
fn run_plan(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<RunPlan> {
let (coordinator, operator_endpoint) = if let Some(url) = args.coordinator {
(CoordinatorSelection::LocalOverride(url), None)
} else if args.local {
(CoordinatorSelection::LocalOnly, None)
} else if session.is_authenticated() {
(
CoordinatorSelection::Hosted,
Some(default_operator_endpoint()),
)
} else {
(CoordinatorSelection::LocalOnly, None)
};
Ok(RunPlan {
project: args.project.unwrap_or(cwd),
entry: args.entry.unwrap_or_else(|| "build".to_owned()),
coordinator,
operator_endpoint,
session,
})
}
fn non_interactive_run_requires_auth_report(args: RunArgs, cwd: PathBuf) -> Value {
let project = args.project.unwrap_or(cwd);
let entry = args.entry.unwrap_or_else(|| "build".to_owned());
let message = "non-interactive run requires an authenticated human or agent session unless --local or --coordinator is explicit";
let next_actions = vec![
"disasmer login --browser",
"set DISASMER_AGENT_PUBLIC_KEY for automation",
"pass --local to run against local services",
"pass --coordinator for an explicit self-hosted coordinator",
];
json!({
"command": "run",
"status": "authentication_required",
"project_root": project,
"entry": entry,
"non_interactive": true,
"browser_opened": false,
"safe_failure": true,
"message": message,
"next_actions": next_actions,
"private_website_required": false,
"machine_error": non_interactive_auth_machine_error(message, next_actions),
})
}
fn run_report(args: RunArgs, cwd: PathBuf, session: CliSession) -> Result<Value> {
if args.non_interactive
&& !args.local
&& args.coordinator.is_none()
&& !session.is_authenticated()
{
return Ok(non_interactive_run_requires_auth_report(args, cwd));
}
let plan = run_plan(args, cwd, session)?;
if should_execute_local_node(&plan) {
return Ok(serde_json::to_value(execute_local_node_run(plan)?)?);
}
coordinator_run_report(plan)
}
fn coordinator_run_report(plan: RunPlan) -> Result<Value> {
let config = read_project_config(&plan.project)?;
let tenant = config
.as_ref()
.map(|config| config.tenant.clone())
.unwrap_or_else(|| "tenant".to_owned());
let project = config
.as_ref()
.map(|config| config.project.clone())
.unwrap_or_else(|| "project".to_owned());
let user = config
.as_ref()
.map(|config| config.user.clone())
.unwrap_or_else(|| "user".to_owned());
let coordinator = run_coordinator_endpoint(&plan)?;
let process = "vp-current".to_owned();
let mut session = JsonLineSession::connect(&coordinator)?;
let mut request = json!({
"type": "start_process",
"tenant": tenant.clone(),
"project": project.clone(),
"process": process.clone(),
"restart": false,
});
add_workflow_actor_fields(&mut request, &plan.session, &user);
let response = session.request_allow_error(request)?;
let run_start = run_start_summary(&response);
let status = run_start
.get("status")
.and_then(Value::as_str)
.unwrap_or("coordinator_response");
Ok(json!({
"command": "run",
"status": status,
"project_root": plan.project,
"entry": plan.entry,
"tenant": tenant,
"project": project,
"user": user,
"workflow_actor": response.get("actor").cloned().unwrap_or(Value::Null),
"coordinator": coordinator,
"process": process,
"run_start": run_start,
"coordinator_response": response,
"coordinator_session_requests": session.requests(),
"private_website_required": false,
}))
}
fn run_coordinator_endpoint(plan: &RunPlan) -> Result<String> {
match &plan.coordinator {
CoordinatorSelection::Hosted => Ok(plan
.operator_endpoint
.clone()
.unwrap_or_else(default_operator_endpoint)),
CoordinatorSelection::LocalOverride(coordinator) => Ok(coordinator.clone()),
CoordinatorSelection::LocalOnly => {
anyhow::bail!("local-only run should execute through local services")
}
}
}
fn run_start_summary(response: &Value) -> Value {
if response.get("type").and_then(Value::as_str) == Some("process_started") {
return json!({
"status": "started",
"accepted": true,
"process": response.get("process").cloned().unwrap_or(Value::Null),
"coordinator_epoch": response.get("epoch").cloned().unwrap_or(Value::Null),
"actor": response.get("actor").cloned().unwrap_or(Value::Null),
"restart": false,
"single_active_process_boundary": true,
"next_actions": [
"disasmer process status",
"disasmer logs",
"disasmer process cancel"
],
});
}
let message = response
.get("message")
.and_then(Value::as_str)
.unwrap_or("coordinator rejected run");
let active_conflict = message.contains("already has active virtual process");
let machine_error = if active_conflict {
cli_error_summary_for_category("active_process", message)
} else {
cli_error_summary(message)
};
let error_category = machine_error
.get("category")
.cloned()
.unwrap_or_else(|| json!("unknown"));
let stable_exit_code = machine_error
.get("stable_exit_code")
.cloned()
.unwrap_or_else(|| json!(1));
json!({
"status": if active_conflict { "blocked_active_process" } else { "coordinator_rejected" },
"accepted": false,
"category": if active_conflict { "active_process_already_running" } else { "coordinator" },
"error_category": error_category,
"stable_exit_code": stable_exit_code,
"machine_error": machine_error,
"message": message,
"restart": false,
"single_active_process_boundary": true,
"safe_failure": true,
"next_actions": if active_conflict {
json!([
"disasmer process status",
"disasmer logs",
"disasmer process restart --yes",
"disasmer process cancel --yes",
"wait for the active process to finish"
])
} else {
json!(["disasmer doctor", "check coordinator status"])
},
})
}
fn should_execute_local_node(plan: &RunPlan) -> bool {
match &plan.coordinator {
CoordinatorSelection::LocalOnly => true,
CoordinatorSelection::LocalOverride(coordinator) => !coordinator.contains("://"),
CoordinatorSelection::Hosted => false,
}
}
fn execute_local_node_run(plan: RunPlan) -> Result<RunExecutionReport> {
let local_coordinator = match &plan.coordinator {
CoordinatorSelection::LocalOverride(coordinator) => LocalCoordinator::external(coordinator),
CoordinatorSelection::LocalOnly => LocalCoordinator::start_ephemeral()?,
CoordinatorSelection::Hosted => anyhow::bail!("local node execution requires local mode"),
};
let coordinator_address = local_coordinator.address.clone();
let spawned = spawn_node_process(&plan, &coordinator_address)?;
if !spawned.output.status.success() {
anyhow::bail!(
"node process failed with status {}\n{}",
spawned.output.status,
String::from_utf8_lossy(&spawned.output.stderr)
);
}
let stdout = String::from_utf8(spawned.output.stdout).context("node stdout was not UTF-8")?;
let line = stdout
.lines()
.rev()
.find(|line| !line.trim().is_empty())
.context("node did not print a JSON report")?;
let node_report: serde_json::Value =
serde_json::from_str(line).context("node report was not JSON")?;
let node_session_requests = node_report
.get("session_requests")
.and_then(serde_json::Value::as_u64)
.unwrap_or_default();
Ok(RunExecutionReport {
plan,
boundary: RunBoundaryEvidence {
cli_process_started_node_process: true,
cli_process_started_coordinator_process: local_coordinator.process_id.is_some(),
coordinator_address,
coordinator_process_id: local_coordinator.process_id,
spawned_node_process_id: spawned.process_id,
node_session_requests,
},
node_report,
})
}
struct SpawnedNodeOutput {
process_id: u32,
output: std::process::Output,
}
fn spawn_node_process(plan: &RunPlan, coordinator: &str) -> Result<SpawnedNodeOutput> {
let mut command = node_command()?;
command.args([
"--coordinator",
coordinator,
"--tenant",
"tenant",
"--project-id",
"project",
"--node",
"node-cli-local",
"--process",
"vp-cli-local",
"--task",
"compile-linux",
"--project",
]);
command.arg(&plan.project);
command.args(["--artifact", "/vfs/artifacts/cli-run-output.txt"]);
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
let child = command.spawn().context("failed to spawn node process")?;
let process_id = child.id();
let output = child
.wait_with_output()
.context("failed to wait for node process")?;
Ok(SpawnedNodeOutput { process_id, output })
}
fn node_command() -> Result<Command> {
if let Some(path) = std::env::var_os("DISASMER_NODE_BIN") {
return Ok(Command::new(path));
}
let mut sibling = std::env::current_exe().context("cannot locate current executable")?;
sibling.set_file_name(format!("disasmer-node{}", std::env::consts::EXE_SUFFIX));
if sibling.is_file() {
return Ok(Command::new(sibling));
}
let mut command = Command::new("cargo");
command.args([
"run",
"-q",
"-p",
"disasmer-node",
"--bin",
"disasmer-node",
"--",
]);
Ok(command)
}
struct LocalCoordinator {
address: String,
process_id: Option<u32>,
child: Option<std::process::Child>,
}
impl LocalCoordinator {
fn external(address: &str) -> Self {
Self {
address: address.to_owned(),
process_id: None,
child: None,
}
}
fn start_ephemeral() -> Result<Self> {
let mut command = coordinator_command()?;
command.args(["--listen", "127.0.0.1:0"]);
command.stdout(Stdio::piped());
command.stderr(Stdio::inherit());
let mut child = command
.spawn()
.context("failed to spawn local coordinator process")?;
let process_id = child.id();
let address = match read_coordinator_ready_address(&mut child) {
Ok(address) => address,
Err(error) => {
let _ = child.kill();
let _ = child.wait();
return Err(error);
}
};
Ok(Self {
address,
process_id: Some(process_id),
child: Some(child),
})
}
}
fn read_coordinator_ready_address(child: &mut std::process::Child) -> Result<String> {
let stdout = child
.stdout
.take()
.context("local coordinator stdout was not captured")?;
let mut ready_line = String::new();
BufReader::new(stdout)
.read_line(&mut ready_line)
.context("failed to read local coordinator ready line")?;
if ready_line.trim().is_empty() {
anyhow::bail!("local coordinator exited before reporting its listen address");
}
let ready: Value =
serde_json::from_str(&ready_line).context("local coordinator ready line was not JSON")?;
ready
.get("listen")
.and_then(Value::as_str)
.map(str::to_owned)
.context("local coordinator did not report a listen address")
}
impl Drop for LocalCoordinator {
fn drop(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
fn coordinator_command() -> Result<Command> {
if let Some(path) = std::env::var_os("DISASMER_COORDINATOR_BIN") {
return Ok(Command::new(path));
}
let mut sibling = std::env::current_exe().context("cannot locate current executable")?;
sibling.set_file_name(format!(
"disasmer-coordinator{}",
std::env::consts::EXE_SUFFIX
));
if sibling.is_file() {
return Ok(Command::new(sibling));
}
let mut command = Command::new("cargo");
command.args([
"run",
"-q",
"-p",
"disasmer-coordinator",
"--bin",
"disasmer-coordinator",
"--",
]);
Ok(command)
}
fn session_from_env() -> CliSession {
if let Some(public_key) = std::env::var_os("DISASMER_AGENT_PUBLIC_KEY") {
let public_key = public_key.to_string_lossy();
return CliSession::AgentPublicKey {
agent: std::env::var("DISASMER_AGENT_ID").unwrap_or_else(|_| "agent".to_owned()),
public_key_fingerprint: Digest::sha256(public_key.as_bytes()),
browser_interaction_required: false,
};
}
if std::env::var_os("DISASMER_TOKEN").is_some() {
return CliSession::HumanSession;
}
CliSession::Anonymous
}
fn session_from_sources(project: &Path) -> Result<CliSession> {
let session = session_from_env();
if session.is_authenticated() {
return Ok(session);
}
if read_cli_session(project)?.is_some() {
return Ok(CliSession::HumanSession);
}
Ok(CliSession::Anonymous)
}
fn add_workflow_actor_fields(request: &mut Value, session: &CliSession, fallback_user: &str) {
let Value::Object(map) = request else {
return;
};
match session {
CliSession::AgentPublicKey {
agent,
public_key_fingerprint,
..
} => {
map.insert("actor_agent".to_owned(), json!(agent));
map.insert(
"agent_public_key_fingerprint".to_owned(),
json!(public_key_fingerprint),
);
}
CliSession::HumanSession | CliSession::Anonymous => {
map.insert("actor_user".to_owned(), json!(fallback_user));
}
}
}
fn attach_plan(args: AttachArgs) -> NodeAttachPlan {
let mut capabilities = NodeCapabilities::detect_current();
for cap in &args.caps {
if let Some(parsed) = parse_capability(&cap) {
capabilities.capabilities.insert(parsed);
}
}
let node = args.node.unwrap_or_else(default_node_id);
let public_key = args
.public_key
.unwrap_or_else(|| format!("{node}-public-key"));
let enrollment = args.enrollment_grant.map(|grant| NodeEnrollmentPlan {
grant,
public_key_fingerprint: Digest::sha256(public_key),
exchanges_short_lived_grant_for_long_lived_node_identity: true,
});
let grant_disclosures = capability_grant_disclosures(&capabilities);
NodeAttachPlan {
node,
coordinator: args.coordinator,
capabilities,
grant_disclosures,
enrollment,
}
}
fn capability_grant_disclosures(capabilities: &NodeCapabilities) -> Vec<CapabilityGrantDisclosure> {
let mut disclosures = Vec::new();
let mut push = |capability: Capability, grant: &str, description: &str, risk: &str| {
if capabilities.capabilities.contains(&capability) {
disclosures.push(CapabilityGrantDisclosure {
capability,
grant: grant.to_owned(),
description: description.to_owned(),
risk: risk.to_owned(),
coordinator_policy_limited: true,
});
}
};
push(
Capability::Command,
"native_command_execution",
"placed tasks may run native commands on this node",
"local process execution under the node account",
);
push(
Capability::WindowsCommandDev,
"native_command_execution",
"placed tasks may run Windows developer commands on this node",
"local process execution under the node account",
);
push(
Capability::SourceFilesystem,
"source_access",
"placed tasks may read the local project/source checkout exposed by this node",
"broad local source visibility",
);
push(
Capability::SourceGit,
"source_access",
"placed tasks may use Git-backed source access exposed by this node",
"source-provider visibility",
);
push(
Capability::Network,
"network_access",
"placed tasks may use outbound network access from this node",
"network egress from the node environment",
);
push(
Capability::HostFilesystem,
"host_filesystem_access",
"placed tasks may access configured host filesystem mounts",
"host file visibility outside the project checkout",
);
push(
Capability::Secrets,
"secret_access",
"placed tasks may receive configured secret material",
"secret exposure to authorized task code",
);
push(
Capability::InboundPorts,
"inbound_ports",
"placed tasks may expose inbound ports from this node",
"network service exposure from the node environment",
);
push(
Capability::ArbitrarySyscalls,
"arbitrary_syscalls",
"placed tasks may use broader host syscall surface",
"reduced host isolation",
);
disclosures.sort_by(|left, right| {
left.grant
.cmp(&right.grant)
.then_with(|| left.capability.cmp(&right.capability))
});
disclosures
}
fn execute_node_attach(args: AttachArgs) -> Result<NodeAttachReport> {
let coordinator = args
.coordinator
.clone()
.context("node attach execution requires --coordinator")?;
let tenant = args.tenant.clone();
let project = args.project.clone();
let node = args.node.clone().unwrap_or_else(default_node_id);
let public_key = args
.public_key
.clone()
.unwrap_or_else(|| format!("{node}-public-key"));
let plan = attach_plan(args);
let mut session = JsonLineSession::connect(&coordinator)?;
let used_enrollment_exchange = plan.enrollment.is_some();
let coordinator_response = if let Some(enrollment) = &plan.enrollment {
session.request(json!({
"type": "exchange_node_enrollment_grant",
"tenant": &tenant,
"project": &project,
"node": &node,
"public_key": &public_key,
"enrollment_grant": enrollment.grant,
"now_epoch_seconds": 0,
}))?
} else {
session.request(json!({
"type": "attach_node",
"tenant": &tenant,
"project": &project,
"node": &node,
"public_key": &public_key,
}))?
};
let heartbeat_response = session.request(json!({
"type": "node_heartbeat",
"node": &plan.node,
}))?;
let capability_response = session.request(json!({
"type": "report_node_capabilities",
"tenant": &tenant,
"project": &project,
"node": &plan.node,
"capabilities": &plan.capabilities,
"cached_environment_digests": [],
"dependency_cache_digests": [],
"source_snapshots": [],
"artifact_locations": [],
"direct_connectivity": true,
"online": false,
}))?;
Ok(NodeAttachReport {
command: "node attach".to_owned(),
node: plan.node.clone(),
grant_disclosures: plan.grant_disclosures.clone(),
plan,
boundary: NodeAttachBoundaryEvidence {
cli_contacted_coordinator: true,
coordinator_address: coordinator,
used_enrollment_exchange,
coordinator_session_requests: session.requests(),
},
coordinator_response,
heartbeat_response,
capability_response,
})
}
struct JsonLineSession {
writer: TcpStream,
reader: BufReader<TcpStream>,
requests: u64,
}
impl JsonLineSession {
fn connect(addr: &str) -> Result<Self> {
let transport_addr = json_line_transport_addr(addr);
let writer = TcpStream::connect(&transport_addr)
.with_context(|| format!("failed to connect to {addr} via {transport_addr}"))?;
Self::from_stream(writer)
}
fn from_stream(writer: TcpStream) -> Result<Self> {
let reader = BufReader::new(writer.try_clone()?);
Ok(Self {
writer,
reader,
requests: 0,
})
}
fn request(&mut self, value: Value) -> Result<Value> {
let response = self.request_allow_error(value)?;
if response.get("type").and_then(Value::as_str) == Some("error") {
let message = response
.get("message")
.and_then(Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| response.to_string());
let machine_error = cli_error_summary(&message);
let category = machine_error
.get("category")
.and_then(Value::as_str)
.unwrap_or("unknown");
let exit_code = machine_error
.get("stable_exit_code")
.and_then(Value::as_i64)
.unwrap_or(1);
anyhow::bail!("coordinator error ({category}, exit {exit_code}): {response}");
}
Ok(response)
}
fn request_allow_error(&mut self, value: Value) -> Result<Value> {
serde_json::to_writer(&mut self.writer, &value)?;
self.writer.write_all(b"\n")?;
self.writer.flush()?;
let mut line = String::new();
if self.reader.read_line(&mut line)? == 0 {
anyhow::bail!("coordinator closed session without a response");
}
self.requests += 1;
let response: Value = serde_json::from_str(&line)?;
Ok(response)
}
fn requests(&self) -> u64 {
self.requests
}
}
fn json_line_transport_addr(endpoint: &str) -> String {
let endpoint = endpoint.trim();
for (scheme, default_port) in [("https://", 443), ("http://", 80)] {
if let Some(rest) = endpoint.strip_prefix(scheme) {
let authority = rest.split('/').next().unwrap_or(rest);
if authority.contains(':') {
return authority.to_owned();
}
return format!("{authority}:{default_port}");
}
}
endpoint.to_owned()
}
fn default_node_id() -> String {
std::env::var("DISASMER_NODE_ID")
.or_else(|_| std::env::var("HOSTNAME"))
.or_else(|_| std::env::var("COMPUTERNAME"))
.unwrap_or_else(|_| "node-local".to_owned())
}
fn parse_capability(cap: &str) -> Option<Capability> {
match cap {
"command" => Some(Capability::Command),
"containers" => Some(Capability::Containers),
"rootless-podman" => Some(Capability::RootlessPodman),
"source-filesystem" => Some(Capability::SourceFilesystem),
"source-git" => Some(Capability::SourceGit),
"host-filesystem" => Some(Capability::HostFilesystem),
"network" => Some(Capability::Network),
"secrets" => Some(Capability::Secrets),
"inbound-ports" => Some(Capability::InboundPorts),
"arbitrary-syscalls" => Some(Capability::ArbitrarySyscalls),
"vfs-artifacts" => Some(Capability::VfsArtifacts),
"wasmtime" => Some(Capability::Wasmtime),
"windows-command-dev" => Some(Capability::WindowsCommandDev),
"quic-direct" => Some(Capability::QuicDirect),
_ => None,
}
}
#[cfg(test)]
mod tests {
use std::fs;
use clap::CommandFactory;
use super::*;
fn parse(args: &[&str]) -> Cli {
Cli::parse_from(args)
}
#[test]
fn cli_error_classifier_distinguishes_mvp_failure_categories() {
for (message, category, exit_code) in [
(
"login required before running this command",
"authentication",
20,
),
("unauthorized tenant action", "authorization", 21),
(
"quota unavailable: resource limit exceeded for api_calls",
"quota",
22,
),
("policy denied native command execution", "policy", 23),
(
"scheduler placement failed: no capable node for placement: project mismatch",
"capability",
24,
),
(
"scheduler placement failed: no capable node for placement: source snapshot unavailable and direct connectivity unavailable",
"connectivity",
25,
),
(
"failed to connect to coordinator: connection refused",
"connectivity",
25,
),
(
"missing environment envs/linux/Containerfile",
"environment",
26,
),
("task exited with status 101 after panic", "program", 27),
(
"project already has active virtual process vp-current",
"active_process",
28,
),
] {
let summary = cli_error_summary(message);
assert_eq!(summary["category"], category, "{message}");
assert_eq!(summary["stable_exit_code"], exit_code, "{message}");
assert_eq!(summary["safe_failure"], true);
assert_eq!(summary["process_exit_code_applied"], false);
assert!(summary["next_actions"].as_array().unwrap().len() >= 2);
if category == "quota" {
assert_eq!(summary["resource_category"], "api_calls");
assert_eq!(summary["community_tier_language"], true);
assert_eq!(summary["private_abuse_heuristics_exposed"], false);
}
}
}
#[test]
fn command_report_exit_code_marks_command_failures_only() {
let run_start = run_start_summary(&json!({
"type": "error",
"message": "quota unavailable: resource limit exceeded for api_calls",
}));
let mut report = json!({
"command": "run",
"status": run_start["status"].clone(),
"run_start": run_start,
});
assert_eq!(apply_command_report_exit_code(&mut report), Some(22));
assert_eq!(
report["run_start"]["machine_error"]["process_exit_code_applied"],
true
);
let mut task_list = json!({
"command": "task list",
"tasks": [{
"task": "compile",
"state": "failed",
"machine_error": cli_error_summary_for_category("program", "task exited with status 1"),
}],
});
assert_eq!(apply_command_report_exit_code(&mut task_list), None);
assert_eq!(
task_list["tasks"][0]["machine_error"]["process_exit_code_applied"],
false
);
}
#[test]
fn top_level_version_is_available() {
let command = Cli::command();
assert_eq!(command.get_name(), "disasmer");
assert!(command.get_version().is_some());
}
#[test]
fn run_defaults_to_current_project_and_build_entry() {
let Cli {
command: Commands::Run(args),
} = parse(&["disasmer", "run"])
else {
panic!("wrong command");
};
let plan = run_plan(args, PathBuf::from("/repo"), CliSession::Anonymous).unwrap();
assert_eq!(plan.project, PathBuf::from("/repo"));
assert_eq!(plan.entry, "build");
assert_eq!(plan.coordinator, CoordinatorSelection::LocalOnly);
assert_eq!(plan.operator_endpoint, None);
assert_eq!(plan.session, CliSession::Anonymous);
}
#[test]
fn non_interactive_run_without_session_requires_explicit_auth_or_local() {
let Cli {
command: Commands::Run(args),
} = parse(&["disasmer", "run", "--non-interactive", "--json"])
else {
panic!("wrong command");
};
let report = run_report(args, PathBuf::from("/repo"), CliSession::Anonymous).unwrap();
assert_eq!(report["command"], "run");
assert_eq!(report["status"], "authentication_required");
assert_eq!(report["non_interactive"], true);
assert_eq!(report["browser_opened"], false);
assert_eq!(report["private_website_required"], false);
assert_eq!(report["machine_error"]["category"], "authentication");
assert_eq!(report["machine_error"]["stable_exit_code"], 20);
assert_eq!(report["machine_error"]["browser_opened"], false);
let next_actions = report["machine_error"]["next_actions"]
.as_array()
.unwrap()
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
assert!(next_actions.contains(&"disasmer login --browser"));
assert!(next_actions.contains(&"set DISASMER_AGENT_PUBLIC_KEY for automation"));
assert!(next_actions.contains(&"pass --local to run against local services"));
}
#[test]
fn run_project_and_named_entry_are_respected() {
let Cli {
command: Commands::Run(args),
} = parse(&["disasmer", "run", "test", "--project", "/other"])
else {
panic!("wrong command");
};
let plan = run_plan(args, PathBuf::from("/repo"), CliSession::HumanSession).unwrap();
assert_eq!(plan.project, PathBuf::from("/other"));
assert_eq!(plan.entry, "test");
assert_eq!(plan.coordinator, CoordinatorSelection::Hosted);
assert_eq!(
plan.operator_endpoint.as_deref(),
Some(DEFAULT_OPERATOR_ENDPOINT)
);
assert_eq!(plan.session, CliSession::HumanSession);
}
#[test]
fn node_attach_detects_and_accepts_capability_overrides() {
let Cli {
command:
Commands::Node {
command: NodeCommands::Attach(args),
},
} = parse(&["disasmer", "node", "attach", "--cap", "quic-direct"])
else {
panic!("wrong command");
};
let plan = attach_plan(args);
assert!(plan
.capabilities
.capabilities
.contains(&Capability::QuicDirect));
assert!(!plan.capabilities.arch.is_empty());
}
#[test]
fn node_attach_discloses_dangerous_capability_grants() {
let Cli {
command:
Commands::Node {
command: NodeCommands::Attach(args),
},
} = parse(&[
"disasmer",
"node",
"attach",
"--cap",
"network",
"--cap",
"host-filesystem",
"--cap",
"secrets",
])
else {
panic!("wrong command");
};
let plan = attach_plan(args);
let grants = plan
.grant_disclosures
.iter()
.map(|disclosure| disclosure.grant.as_str())
.collect::<Vec<_>>();
assert!(grants.contains(&"native_command_execution"));
assert!(grants.contains(&"source_access"));
assert!(grants.contains(&"network_access"));
assert!(grants.contains(&"host_filesystem_access"));
assert!(grants.contains(&"secret_access"));
assert!(plan
.grant_disclosures
.iter()
.all(|disclosure| disclosure.coordinator_policy_limited));
let rendered = human_report(&json!({
"command": "node attach",
"node": plan.node,
"grant_disclosures": plan.grant_disclosures,
}));
assert!(rendered.contains("grant native_command_execution"));
assert!(rendered.contains("grant network_access"));
assert!(rendered.contains("policy-limited"));
}
#[test]
fn agents_can_select_hosted_with_public_key_identity() {
let args = RunArgs {
entry: None,
project: None,
coordinator: None,
local: false,
non_interactive: true,
json: false,
};
let plan = run_plan(
args,
PathBuf::from("/repo"),
CliSession::AgentPublicKey {
agent: "agent-ci".to_owned(),
public_key_fingerprint: Digest::sha256("agent-key"),
browser_interaction_required: false,
},
)
.unwrap();
assert_eq!(plan.coordinator, CoordinatorSelection::Hosted);
assert_eq!(
plan.operator_endpoint.as_deref(),
Some(DEFAULT_OPERATOR_ENDPOINT)
);
assert_eq!(
plan.session,
CliSession::AgentPublicKey {
agent: "agent-ci".to_owned(),
public_key_fingerprint: Digest::sha256("agent-key"),
browser_interaction_required: false,
}
);
}
#[test]
fn run_with_agent_public_key_sends_attributable_workflow_actor() {
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-live".to_owned(),
user: "user-live".to_owned(),
coordinator: None,
},
)
.unwrap();
let fingerprint = Digest::sha256("agent-key");
let expected_fingerprint = fingerprint.to_string();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"start_process""#));
assert!(line.contains(r#""tenant":"tenant-live""#));
assert!(line.contains(r#""project":"project-live""#));
assert!(line.contains(r#""actor_agent":"agent-ci""#));
assert!(line.contains(&format!(
r#""agent_public_key_fingerprint":"{expected_fingerprint}""#
)));
assert!(!line.contains(r#""actor_user""#));
stream
.write_all(
format!(
r#"{{"type":"process_started","process":"vp-current","epoch":7,"actor":{{"kind":"agent","user":"user-live","agent":"agent-ci","credential_kind":"public_key","public_key_fingerprint":"{expected_fingerprint}","authenticated_without_browser":true,"scopes":["project:read","project:run"]}}}}"#
)
.as_bytes(),
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = run_report(
RunArgs {
entry: Some("build".to_owned()),
project: Some(temp.path().to_path_buf()),
coordinator: Some(format!("http://{addr}")),
local: false,
non_interactive: true,
json: false,
},
PathBuf::from("/unused"),
CliSession::AgentPublicKey {
agent: "agent-ci".to_owned(),
public_key_fingerprint: fingerprint,
browser_interaction_required: false,
},
)
.unwrap();
server.join().unwrap();
assert_eq!(report["status"], "started");
assert_eq!(report["workflow_actor"]["kind"], "agent");
assert_eq!(report["workflow_actor"]["agent"], "agent-ci");
assert_eq!(
report["workflow_actor"]["authenticated_without_browser"],
true
);
assert_eq!(report["run_start"]["actor"]["kind"], "agent");
}
#[test]
fn run_local_flag_overrides_logged_in_hosted_selection() {
let Cli {
command: Commands::Run(args),
} = parse(&["disasmer", "run", "--local"])
else {
panic!("wrong command");
};
let plan = run_plan(args, PathBuf::from("/repo"), CliSession::HumanSession).unwrap();
assert_eq!(plan.coordinator, CoordinatorSelection::LocalOnly);
assert_eq!(plan.operator_endpoint, None);
}
#[test]
fn run_contacts_configured_coordinator_and_reports_active_process_conflicts() {
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-live".to_owned(),
user: "user-live".to_owned(),
coordinator: None,
},
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
for response in [
r#"{"type":"process_started","process":"vp-current","epoch":7}"#,
r#"{"type":"error","message":"coordinator request failed: unauthorized coordinator action: project already has active virtual process vp-current; inspect it, restart it, cancel it, or wait before starting another run"}"#,
] {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"start_process""#));
assert!(line.contains(r#""tenant":"tenant-live""#));
assert!(line.contains(r#""project":"project-live""#));
assert!(line.contains(r#""process":"vp-current""#));
assert!(line.contains(r#""restart":false"#));
stream.write_all(response.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
}
});
let started = run_report(
RunArgs {
entry: Some("build".to_owned()),
project: Some(temp.path().to_path_buf()),
coordinator: Some(format!("http://{addr}")),
local: false,
non_interactive: false,
json: false,
},
PathBuf::from("/unused"),
CliSession::Anonymous,
)
.unwrap();
let blocked = run_report(
RunArgs {
entry: Some("test".to_owned()),
project: Some(temp.path().to_path_buf()),
coordinator: Some(format!("http://{addr}")),
local: false,
non_interactive: false,
json: false,
},
PathBuf::from("/unused"),
CliSession::Anonymous,
)
.unwrap();
server.join().unwrap();
assert_eq!(started["status"], "started");
assert_eq!(started["entry"], "build");
assert_eq!(started["tenant"], "tenant-live");
assert_eq!(started["project"], "project-live");
assert_eq!(started["process"], "vp-current");
assert_eq!(started["run_start"]["restart"], false);
assert_eq!(started["run_start"]["single_active_process_boundary"], true);
assert_eq!(blocked["status"], "blocked_active_process");
assert_eq!(blocked["entry"], "test");
assert_eq!(
blocked["run_start"]["category"],
"active_process_already_running"
);
assert_eq!(blocked["run_start"]["error_category"], "active_process");
assert_eq!(blocked["run_start"]["stable_exit_code"], 28);
assert_eq!(
blocked["run_start"]["machine_error"]["category"],
"active_process"
);
assert_eq!(
blocked["run_start"]["machine_error"]["stable_exit_code"],
28
);
assert_eq!(blocked["run_start"]["safe_failure"], true);
assert!(blocked["run_start"]["next_actions"]
.as_array()
.unwrap()
.iter()
.any(|action| action == "disasmer process restart --yes"));
}
#[test]
fn run_rejection_reports_machine_readable_error_category() {
let rejected = run_start_summary(&json!({
"type": "error",
"message": "quota unavailable: resource limit exceeded for api_calls"
}));
assert_eq!(rejected["status"], "coordinator_rejected");
assert_eq!(rejected["error_category"], "quota");
assert_eq!(rejected["stable_exit_code"], 22);
assert_eq!(rejected["machine_error"]["category"], "quota");
assert_eq!(rejected["machine_error"]["resource_category"], "api_calls");
assert_eq!(rejected["machine_error"]["community_tier_language"], true);
assert_eq!(
rejected["machine_error"]["private_abuse_heuristics_exposed"],
false
);
assert!(rejected["machine_error"]["next_actions"]
.as_array()
.unwrap()
.iter()
.any(|action| action == "disasmer quota status"));
}
#[test]
fn local_only_run_executes_ephemeral_local_services() {
let Cli {
command: Commands::Run(args),
} = parse(&["disasmer", "run", "--local"])
else {
panic!("wrong command");
};
let plan = run_plan(args, PathBuf::from("/repo"), CliSession::Anonymous).unwrap();
assert!(should_execute_local_node(&plan));
}
#[test]
fn login_uses_device_flow_without_long_lived_secret_copying() {
let Cli {
command: Commands::Login(args),
} = parse(&["disasmer", "login"])
else {
panic!("wrong command");
};
let plan = login_plan(args);
assert_eq!(plan.coordinator, DEFAULT_OPERATOR_ENDPOINT);
match plan.human_flow {
LoginFlowPlan::Device(flow) => {
assert!(!flow.yields_long_lived_secret_directly);
assert!(flow.verification_url.contains("/auth/device"));
assert_ne!(flow.user_code, format!("DISASMER-{}", "DEMO"));
assert!(flow.user_code.starts_with("DISASMER-"));
assert!(flow.device_code.starts_with("sha256:"));
}
LoginFlowPlan::Browser(_) => panic!("expected device flow"),
}
}
#[test]
fn browser_login_flow_is_available_for_humans() {
let Cli {
command: Commands::Login(args),
} = parse(&["disasmer", "login", "--browser"])
else {
panic!("wrong command");
};
let plan = login_plan(args);
let LoginFlowPlan::Browser(flow) = plan.human_flow else {
panic!("expected browser flow");
};
assert!(flow.authorization_url.contains("/auth/browser/start"));
assert!(flow.state.starts_with("sha256:"));
}
#[test]
fn browser_login_non_interactive_fails_before_opening_browser() {
let Cli {
command: Commands::Login(args),
} = parse(&["disasmer", "login", "--browser", "--non-interactive"])
else {
panic!("wrong command");
};
let report = non_interactive_browser_login_report(&args);
assert_eq!(report["command"], "login");
assert_eq!(report["status"], "authentication_required");
assert_eq!(report["non_interactive"], true);
assert_eq!(report["browser_requested"], true);
assert_eq!(report["browser_opened"], false);
assert_eq!(report["machine_error"]["category"], "authentication");
assert_eq!(report["machine_error"]["stable_exit_code"], 20);
assert_eq!(report["machine_error"]["browser_opened"], false);
let next_actions = report["machine_error"]["next_actions"]
.as_array()
.unwrap()
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>();
assert!(next_actions.contains(&"rerun without --non-interactive to open the browser"));
assert!(next_actions.contains(&"use DISASMER_AGENT_PUBLIC_KEY for automation"));
}
#[test]
fn browser_login_completion_detects_raw_provider_token_fields() {
assert!(contains_provider_token_field(&json!({
"session": {
"access_token": "secret"
}
})));
assert!(!contains_provider_token_field(&json!({
"session": {
"cli_session_credential_kind": "CliDeviceSession",
"oidc_token_exchange": {
"received_access_token": true,
"received_id_token": true
}
}
})));
}
#[test]
fn stored_browser_login_session_omits_provider_token_values() {
let Cli {
command: Commands::Login(args),
} = parse(&[
"disasmer",
"login",
"--browser",
"--coordinator",
"https://coord.example.test",
"--tenant",
"tenant-cli",
"--project-id",
"project-cli",
"--user",
"user-cli",
])
else {
panic!("wrong command");
};
let stored = stored_cli_session_from_login_response(
&args,
"https://coord.example.test",
&json!({
"session": {
"tenant": "tenant-live",
"project": "project-live",
"user": "user-live",
"cli_session_credential_kind": "CliDeviceSession",
"expires_at": "2026-07-04T00:00:00Z",
"access_token": "provider-secret",
"id_token": "provider-id-token",
"provider_tokens_sent_to_nodes": false
}
}),
true,
false,
);
let serialized = serde_json::to_string(&stored).unwrap();
assert_eq!(stored.kind, "human");
assert_eq!(stored.coordinator, "https://coord.example.test");
assert_eq!(stored.tenant, "tenant-live");
assert_eq!(stored.project, "project-live");
assert_eq!(stored.user, "user-live");
assert_eq!(stored.token_expiry_posture, "expires_at");
assert!(stored.provider_tokens_exposed_to_cli);
assert!(!stored.provider_tokens_sent_to_nodes);
assert!(!serialized.contains("provider-secret"));
assert!(!serialized.contains("provider-id-token"));
assert!(!serialized.contains("access_token"));
assert!(!serialized.contains("id_token"));
}
#[test]
fn agent_enroll_uses_public_key_without_browser_each_run() {
let Cli {
command:
Commands::Agent {
command: AgentCommands::Enroll(args),
},
} = parse(&["disasmer", "agent", "enroll", "--public-key", "agent-key"])
else {
panic!("wrong command");
};
let plan = agent_enrollment_plan(args);
assert!(!plan.browser_interaction_required_each_run);
assert!(plan.public_key_fingerprint.as_str().starts_with("sha256:"));
}
#[test]
fn bundle_inspect_discovers_environments_selected_inputs_and_source_providers() {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join("envs/linux")).unwrap();
fs::create_dir_all(temp.path().join("envs/docker")).unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(
temp.path().join("envs/linux/Containerfile"),
"FROM alpine\n",
)
.unwrap();
fs::write(
temp.path().join("envs/docker/Dockerfile"),
"FROM debian:stable-slim\n",
)
.unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let Cli {
command:
Commands::Bundle {
command: BundleCommands::Inspect(args),
},
} = parse(&[
"disasmer",
"bundle",
"inspect",
"--project",
temp.path().to_str().unwrap(),
])
else {
panic!("wrong command");
};
let inspection = bundle_inspection(args, PathBuf::from("/unused")).unwrap();
assert_eq!(inspection.project, temp.path());
assert!(inspection
.default_source_providers
.contains(&SourceProviderKind::Git));
assert!(inspection.source_provider_statuses.iter().any(|status| {
status.provider == "filesystem" && status.status == "enabled" && status.active
}));
assert!(inspection
.source_provider_statuses
.iter()
.any(|status| status.provider == "git" && status.status == "missing"));
assert!(inspection
.source_provider_manifest
.description
.contains("node task"));
assert!(
!inspection
.source_provider_manifest
.coordinator_requires_checkout_access
);
assert!(
!inspection
.source_provider_manifest
.transfer_policy
.default_full_repo_tarball
);
assert!(inspection
.metadata
.environments
.iter()
.any(|environment| environment.name == "linux"));
assert!(inspection
.metadata
.environments
.iter()
.any(|environment| environment.name == "docker"));
assert_eq!(
inspection.metadata.source_metadata.source_provider_manifest,
inspection.source_provider_manifest.digest
);
assert!(
inspection
.metadata
.source_metadata
.transfer_policy
.local_source_bytes_remain_node_local
);
assert!(
!inspection
.metadata
.source_metadata
.transfer_policy
.coordinator_receives_source_bytes_by_default
);
assert!(
!inspection
.metadata
.source_metadata
.transfer_policy
.default_full_repo_tarball
);
assert!(inspection
.metadata
.task_metadata
.entrypoints
.contains(&"build".to_owned()));
assert_eq!(
inspection.metadata.task_metadata.default_entrypoint,
"build"
);
assert_eq!(
inspection.metadata.task_metadata.task_abi,
task_abi_digest(&ProjectModel::discover_without_config(temp.path()).unwrap())
);
assert!(inspection
.metadata
.wasm_code
.as_str()
.starts_with("sha256:"));
assert!(inspection.metadata.debug_metadata.available);
assert!(inspection.metadata.debug_metadata.source_level_breakpoints);
assert!(inspection.metadata.debug_metadata.variables_pane_supported);
assert!(
inspection
.metadata
.large_input_policy
.selected_inputs_are_content_digests
);
assert!(
!inspection
.metadata
.large_input_policy
.selected_input_bytes_included
);
assert!(
!inspection
.metadata
.large_input_policy
.full_repository_bytes_included
);
assert!(
!inspection
.metadata
.large_input_policy
.silent_task_argument_serialization
);
assert!(inspection
.metadata
.large_input_policy
.supported_handle_types
.contains(&"SourceSnapshot".to_owned()));
assert!(
inspection
.metadata
.restart_compatibility
.source_edits_can_restart_from_clean_task_boundary
);
assert!(
inspection
.metadata
.restart_compatibility
.requires_clean_checkpoint_boundary
);
assert_eq!(
inspection.metadata.restart_compatibility.compares_task_abi,
inspection.metadata.task_metadata.task_abi
);
assert!(
inspection
.metadata
.restart_compatibility
.incompatible_changes_require_whole_process_restart
);
assert!(!inspection.metadata.embeds_full_container_images);
assert!(inspection
.metadata
.selected_inputs
.iter()
.any(|input| input.path == "Cargo.toml"));
assert!(inspection
.metadata
.selected_inputs
.iter()
.any(|input| input.path == "src/main.rs"));
assert!(inspection.environment_diagnostics.is_empty());
assert!(inspection
.pre_schedule_diagnostics
.iter()
.any(|diagnostic| diagnostic.category == "capability"
&& diagnostic.message.contains("environment `linux`")));
}
#[test]
fn bundle_inspect_reports_missing_environment_references_before_schedule() {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
fs::write(
temp.path().join("src/main.rs"),
"fn main() { let _target = env!(\"linux\"); }\n",
)
.unwrap();
let inspection = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert_eq!(inspection.environment_diagnostics.len(), 1);
assert_eq!(inspection.environment_diagnostics[0].path, "src/main.rs");
assert_eq!(
inspection.environment_diagnostics[0].reference.name,
"linux"
);
assert!(inspection.environment_diagnostics[0]
.message
.contains("missing Disasmer environment `linux`"));
assert!(inspection
.pre_schedule_diagnostics
.iter()
.any(|diagnostic| {
diagnostic.severity == "error"
&& diagnostic.category == "environment"
&& diagnostic.code == "missing_environment"
}));
}
#[test]
fn bundle_inspect_reports_source_provider_overrides_before_schedule() {
let temp = tempfile::tempdir().unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
let missing_git = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
source_provider: Some("git".to_owned()),
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert!(missing_git.source_provider_statuses.iter().any(|status| {
status.provider == "git" && status.status == "missing" && status.active
}));
assert!(missing_git
.pre_schedule_diagnostics
.iter()
.any(|diagnostic| {
diagnostic.category == "source_provider"
&& diagnostic.code == "source_provider_missing"
}));
let unsupported = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
source_provider: Some("custom-lfs".to_owned()),
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert!(unsupported.source_provider_statuses.iter().any(|status| {
status.provider == "custom-lfs" && status.status == "unsupported" && status.active
}));
assert!(unsupported
.pre_schedule_diagnostics
.iter()
.any(|diagnostic| {
diagnostic.category == "source_provider"
&& diagnostic.code == "source_provider_unsupported"
}));
}
#[test]
fn bundle_identity_changes_when_selected_input_file_changes() {
let temp = tempfile::tempdir().unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
let first = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname='changed'\n",
)
.unwrap();
let second = bundle_inspection(
BundleInspectArgs {
project: Some(temp.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert_ne!(first.metadata.identity, second.metadata.identity);
}
#[test]
fn bundle_rebuild_after_source_edit_keeps_restart_compatibility_contract() {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
fs::write(
temp.path().join("src/main.rs"),
"fn main() { println!(\"first\"); }\n",
)
.unwrap();
let inspect = |project: &Path| {
bundle_inspection(
BundleInspectArgs {
project: Some(project.to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: true,
},
PathBuf::from("/unused"),
)
.unwrap()
};
let first = inspect(temp.path());
fs::write(
temp.path().join("src/main.rs"),
"fn main() { println!(\"second\"); }\n",
)
.unwrap();
let rebuilt = inspect(temp.path());
assert_ne!(first.metadata.identity, rebuilt.metadata.identity);
assert_ne!(
first
.metadata
.source_metadata
.selected_inputs
.iter()
.find(|input| input.path == "src/main.rs")
.unwrap()
.digest,
rebuilt
.metadata
.source_metadata
.selected_inputs
.iter()
.find(|input| input.path == "src/main.rs")
.unwrap()
.digest
);
assert_eq!(
first.metadata.task_metadata.task_abi,
rebuilt.metadata.task_metadata.task_abi
);
assert_eq!(
first.metadata.restart_compatibility.compares_task_abi,
rebuilt.metadata.restart_compatibility.compares_task_abi
);
assert!(
rebuilt
.metadata
.restart_compatibility
.source_edits_can_restart_from_clean_task_boundary
);
assert!(
rebuilt
.metadata
.restart_compatibility
.requires_clean_checkpoint_boundary
);
assert!(
rebuilt
.metadata
.restart_compatibility
.discards_unflushed_task_local_changes
);
assert!(
rebuilt
.metadata
.restart_compatibility
.incompatible_changes_require_whole_process_restart
);
}
#[test]
fn source_provider_manifest_digest_does_not_include_local_project_path() {
let first = tempfile::tempdir().unwrap();
let second = tempfile::tempdir().unwrap();
fs::write(first.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
fs::write(second.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
let first = bundle_inspection(
BundleInspectArgs {
project: Some(first.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
let second = bundle_inspection(
BundleInspectArgs {
project: Some(second.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert_eq!(
first.source_provider_manifest.digest,
second.source_provider_manifest.digest
);
}
#[test]
fn node_attach_can_exchange_enrollment_grant() {
let Cli {
command:
Commands::Node {
command: NodeCommands::Attach(args),
},
} = parse(&[
"disasmer",
"node",
"attach",
"--enrollment-grant",
"grant",
"--public-key",
"node-key",
])
else {
panic!("wrong command");
};
let plan = attach_plan(args);
assert!(
plan.enrollment
.unwrap()
.exchanges_short_lived_grant_for_long_lived_node_identity
);
}
#[test]
fn node_attach_enrollment_uses_default_public_key_when_not_explicit() {
let Cli {
command:
Commands::Node {
command: NodeCommands::Attach(args),
},
} = parse(&[
"disasmer",
"node",
"attach",
"--node",
"node-default-key",
"--enrollment-grant",
"grant",
])
else {
panic!("wrong command");
};
let plan = attach_plan(args);
let enrollment = plan.enrollment.unwrap();
assert_eq!(enrollment.grant, "grant");
assert_eq!(
enrollment.public_key_fingerprint,
Digest::sha256("node-default-key-public-key")
);
}
#[test]
fn hosted_operator_url_maps_to_json_line_transport_address() {
assert_eq!(
json_line_transport_addr(DEFAULT_OPERATOR_ENDPOINT),
"disasmer.michelpaulissen.com:9443"
);
assert_eq!(
json_line_transport_addr("https://disasmer.michelpaulissen.com:9443/auth/device"),
"disasmer.michelpaulissen.com:9443"
);
assert_eq!(
json_line_transport_addr("http://operator.example.test"),
"operator.example.test:80"
);
assert_eq!(json_line_transport_addr("127.0.0.1:7999"), "127.0.0.1:7999");
}
#[test]
fn doctor_reports_unchecked_coordinator_reachability_without_config() {
let temp = tempfile::tempdir().unwrap();
let report = doctor_report(
DoctorArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(report["command"], "doctor");
assert!(report["coordinator"].is_null());
assert_eq!(report["coordinator_reachability"]["checked"], false);
assert_eq!(
report["coordinator_reachability"]["status"],
"not_configured"
);
assert!(matches!(
report["node_readiness_summary"]["status"].as_str(),
Some("ready_to_attach")
| Some("local_dependencies_missing")
| Some("limited_capabilities")
));
assert_eq!(
report["node_readiness_summary"]["explicit_attach_required"],
true
);
assert_eq!(
report["node_readiness_summary"]["command_execution_capability"],
true
);
assert!(report["node_readiness_summary"]["missing_local_dependencies"].is_array());
assert!(
report["node_readiness_summary"]["next_actions"]
.as_array()
.unwrap()
.len()
>= 2
);
}
#[test]
fn doctor_pings_configured_coordinator() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains("\"type\":\"ping\""));
stream
.write_all(b"{\"type\":\"pong\",\"epoch\":42}\n")
.unwrap();
});
let temp = tempfile::tempdir().unwrap();
let report = doctor_report(
DoctorArgs {
scope: CliScopeArgs {
coordinator: Some(addr.clone()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
server.join().unwrap();
assert_eq!(report["coordinator"], addr);
assert_eq!(report["coordinator_reachability"]["checked"], true);
assert_eq!(report["coordinator_reachability"]["status"], "reachable");
assert_eq!(
report["coordinator_reachability"]["response"]["type"],
"pong"
);
assert_eq!(report["coordinator_reachability"]["response"]["epoch"], 42);
}
#[test]
fn auth_status_reads_stored_cli_session_without_provider_tokens() {
let temp = tempfile::tempdir().unwrap();
write_cli_session(
temp.path(),
&StoredCliSession {
kind: "human".to_owned(),
coordinator: "https://coord.example.test".to_owned(),
tenant: "tenant-session".to_owned(),
project: "project-session".to_owned(),
user: "user-session".to_owned(),
cli_session_credential_kind: "CliDeviceSession".to_owned(),
token_expiry_posture: "expires_at".to_owned(),
expires_at: Some("2026-07-04T00:00:00Z".to_owned()),
provider_tokens_exposed_to_cli: false,
provider_tokens_sent_to_nodes: false,
created_at_unix_seconds: 1,
},
)
.unwrap();
let report = auth_status_report(
AuthStatusArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(report["active_coordinator"], "https://coord.example.test");
assert_eq!(report["principal"], "user-session");
assert_eq!(report["tenant"], "tenant-session");
assert_eq!(report["project"], "project-session");
assert_eq!(report["session"]["kind"], "human");
assert_eq!(report["session"]["source"], "session_file");
assert_eq!(report["session"]["authenticated"], true);
assert_eq!(
report["session"]["cli_session_credential_kind"],
"CliDeviceSession"
);
assert_eq!(report["session"]["token_expiry_posture"], "expires_at");
assert_eq!(report["session"]["provider_tokens_exposed_to_cli"], false);
assert_eq!(report["session"]["provider_tokens_exposed_to_nodes"], false);
}
#[test]
fn cli_first_mvp_command_surface_parses() {
for args in [
&["disasmer", "doctor"][..],
&["disasmer", "auth", "status"],
&["disasmer", "logout", "--yes"],
&["disasmer", "auth", "logout", "--yes"],
&["disasmer", "login", "--browser", "--non-interactive"],
&[
"disasmer",
"key",
"add",
"--agent",
"agent",
"--public-key",
"key",
],
&["disasmer", "key", "list"],
&["disasmer", "key", "revoke", "--agent", "agent", "--yes"],
&["disasmer", "project", "init", "--yes"],
&["disasmer", "project", "status"],
&["disasmer", "project", "list"],
&["disasmer", "project", "select", "project"],
&["disasmer", "inspect"],
&["disasmer", "build"],
&["disasmer", "run", "--non-interactive"],
&["disasmer", "node", "enroll"],
&["disasmer", "node", "list"],
&["disasmer", "node", "status"],
&["disasmer", "node", "revoke", "--node", "node", "--yes"],
&["disasmer", "process", "status"],
&["disasmer", "process", "restart", "--yes"],
&["disasmer", "process", "cancel", "--yes"],
&["disasmer", "task", "list"],
&["disasmer", "task", "restart", "compile-linux", "--yes"],
&["disasmer", "logs"],
&["disasmer", "artifact", "list"],
&["disasmer", "artifact", "download", "artifact"],
&[
"disasmer", "artifact", "export", "artifact", "--to", "/tmp/out",
],
&["disasmer", "dap", "--plan"],
&["disasmer", "debug", "attach"],
&["disasmer", "quota", "status"],
&["disasmer", "admin", "status"],
&["disasmer", "admin", "bootstrap", "--yes"],
&[
"disasmer",
"admin",
"revoke-node",
"--node",
"node",
"--yes",
],
&["disasmer", "admin", "stop-process", "--yes"],
&["disasmer", "admin", "suspend-tenant", "--yes"],
] {
let _ = parse(args);
}
}
#[test]
fn admin_bootstrap_reports_self_hosted_cli_only_path() {
let temp = tempfile::tempdir().unwrap();
let report = admin_bootstrap_report(
AdminBootstrapArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "team".to_owned(),
project: "self-hosted".to_owned(),
user: "admin".to_owned(),
json: false,
},
name: "Self Hosted".to_owned(),
yes: true,
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(report["command"], "admin bootstrap");
assert_eq!(report["mode"], "self_hosted_local");
assert_eq!(report["private_website_required"], false);
assert_eq!(report["self_hosted_cli_only"], true);
assert_eq!(report["project_config_written"], true);
assert_eq!(report["project_init"]["command"], "project init");
assert_eq!(report["project_init"]["private_website_required"], false);
assert_eq!(
report["project_init"]["project_config"]["project"],
"self-hosted"
);
assert_eq!(
report["admin_surfaces"]["node"],
"disasmer node enroll/list/status/revoke"
);
let steps = report["bootstrap_sequence"].as_array().unwrap();
for expected in [
"start_self_hosted_coordinator",
"create_or_link_project",
"create_node_enrollment_grant",
"attach_worker_node",
"run_process",
"inspect_status_logs_artifacts",
"revoke_access",
] {
assert!(
steps.iter().any(|step| step["step"] == expected),
"missing bootstrap step {expected}"
);
}
assert!(steps.iter().all(|step| {
step.get("private_website_required")
.and_then(Value::as_bool)
.unwrap_or(false)
== false
}));
assert!(steps.iter().any(|step| step
.get("command")
.and_then(Value::as_str)
.unwrap_or("")
.contains("disasmer node enroll")));
assert!(steps.iter().any(|step| step
.get("command")
.and_then(Value::as_str)
.unwrap_or("")
.contains("disasmer admin revoke-node")));
}
#[test]
fn top_level_help_exposes_primary_workflow_without_auth() {
let mut command = Cli::command();
let help = command.render_help().to_string();
for expected in [
"Primary workflow:",
"disasmer login --browser",
"disasmer project init",
"disasmer node enroll",
"disasmer node attach --worker",
"disasmer run [entry] --project <path>",
"Disasmer: Launch Virtual Process",
"disasmer dap",
"disasmer process status",
"task list",
"logs",
"artifact list",
"--json",
"Hosted account creation happens in the browser login flow.",
] {
assert!(help.contains(expected), "help output missing {expected}");
}
}
#[test]
fn top_level_logout_alias_removes_only_cli_session_state() {
let temp = tempfile::tempdir().unwrap();
let session_file = session_config_file(temp.path());
fs::create_dir_all(session_file.parent().unwrap()).unwrap();
fs::write(&session_file, br#"{"kind":"human","token":"local"}"#).unwrap();
let unconfirmed = logout_report(
AuthLogoutArgs {
yes: false,
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
"logout",
)
.unwrap();
assert_eq!(unconfirmed["status"], "confirmation_required");
assert_eq!(unconfirmed["coordinator_request_sent"], false);
assert_eq!(unconfirmed["machine_error"]["category"], "policy");
assert!(session_file.exists());
let report = logout_report(
AuthLogoutArgs {
yes: true,
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
"logout",
)
.unwrap();
assert_eq!(report["command"], "logout");
assert_eq!(report["requires_confirmation"], false);
assert_eq!(report["removed_cli_session_file"], true);
assert_eq!(report["node_credentials_untouched"], true);
assert!(!session_file.exists());
}
#[test]
fn mutating_commands_require_yes_before_side_effects() {
let scope = CliScopeArgs {
coordinator: Some("127.0.0.1:9".to_owned()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
};
let reports = [
key_revoke_report(KeyRevokeArgs {
scope: scope.clone(),
agent: "agent-ci".to_owned(),
yes: false,
})
.unwrap(),
node_revoke_report(NodeRevokeArgs {
scope: scope.clone(),
node: "node-a".to_owned(),
yes: false,
})
.unwrap(),
process_restart_report(ProcessRestartArgs {
scope: scope.clone(),
process: "vp".to_owned(),
yes: false,
})
.unwrap(),
process_cancel_report(ProcessCancelArgs {
scope: scope.clone(),
process: "vp".to_owned(),
node: None,
task: None,
yes: false,
})
.unwrap(),
task_restart_report(TaskRestartArgs {
scope: scope.clone(),
task: "compile-linux".to_owned(),
process: "vp".to_owned(),
yes: false,
})
.unwrap(),
admin_suspend_tenant_report(AdminSuspendTenantArgs {
scope,
target_tenant: Some("tenant".to_owned()),
yes: false,
})
.unwrap(),
];
for report in reports {
assert_eq!(report["status"], "confirmation_required");
assert_eq!(report["requires_confirmation"], true);
assert_eq!(report["coordinator_request_sent"], false);
assert_eq!(report["safe_failure"], true);
assert_eq!(report["machine_error"]["category"], "policy");
assert_eq!(report["machine_error"]["confirmation_required"], true);
assert!(report["next_actions"]
.as_array()
.unwrap()
.iter()
.any(|action| action.as_str().unwrap_or_default().contains("--yes")));
}
}
#[test]
fn cli_first_json_mode_parses_for_primary_commands() {
for args in [
&["disasmer", "doctor", "--json"][..],
&["disasmer", "login", "--json"],
&[
"disasmer",
"login",
"--browser",
"--non-interactive",
"--json",
],
&["disasmer", "logout", "--yes", "--json"],
&["disasmer", "auth", "status", "--json"],
&[
"disasmer",
"agent",
"enroll",
"--public-key",
"key",
"--json",
],
&[
"disasmer",
"key",
"add",
"--agent",
"agent",
"--public-key",
"key",
"--json",
],
&["disasmer", "project", "init", "--yes", "--json"],
&["disasmer", "inspect", "--json"],
&["disasmer", "build", "--json"],
&["disasmer", "bundle", "inspect", "--json"],
&["disasmer", "run", "--json"],
&["disasmer", "run", "--non-interactive", "--json"],
&["disasmer", "node", "attach", "--json"],
&["disasmer", "node", "enroll", "--json"],
&["disasmer", "process", "status", "--json"],
&["disasmer", "task", "list", "--json"],
&["disasmer", "logs", "--json"],
&["disasmer", "artifact", "list", "--json"],
&["disasmer", "artifact", "download", "artifact", "--json"],
&[
"disasmer", "artifact", "export", "artifact", "--to", "/tmp/out", "--json",
],
&["disasmer", "dap", "--plan", "--json"],
&["disasmer", "debug", "attach", "--json"],
&["disasmer", "quota", "status", "--json"],
&["disasmer", "admin", "status", "--json"],
] {
let _ = parse(args);
}
}
#[test]
fn key_lifecycle_reports_project_scoped_agent_credentials() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let add_response = concat!(
r#"{"type":"agent_public_key","actor":"user","record":{"tenant":"tenant","project":"project","user":"user","agent":"agent-ci","public_key":"agent-key-v1","public_key_fingerprint":"sha256:agent-v1","version":1,"revoked":false,"scopes":["project:read","project:run"],"human_account_creation_privilege":false,"browser_interaction_required_each_run":false}}"#
);
let list_response = concat!(
r#"{"type":"agent_public_keys","actor":"user","records":[{"tenant":"tenant","project":"project","user":"user","agent":"agent-ci","public_key":"agent-key-v1","public_key_fingerprint":"sha256:agent-v1","version":1,"revoked":false,"scopes":["project:read","project:run"],"human_account_creation_privilege":false,"browser_interaction_required_each_run":false}]}"#
);
let revoke_response = concat!(
r#"{"type":"agent_public_key","actor":"user","record":{"tenant":"tenant","project":"project","user":"user","agent":"agent-ci","public_key":"agent-key-v1","public_key_fingerprint":"sha256:agent-v1","version":1,"revoked":true,"scopes":["project:read","project:run"],"human_account_creation_privilege":false,"browser_interaction_required_each_run":false}}"#
);
for (expected, response) in [
("register_agent_public_key", add_response),
("list_agent_public_keys", list_response),
("revoke_agent_public_key", revoke_response),
] {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(&format!(r#""type":"{expected}""#)));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""user":"user""#));
if expected != "list_agent_public_keys" {
assert!(line.contains(r#""agent":"agent-ci""#));
}
if expected == "register_agent_public_key" {
assert!(line.contains(r#""public_key":"agent-key-v1""#));
}
stream.write_all(response.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
}
});
let scope = CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
};
let added = key_add_report(KeyAddArgs {
scope: scope.clone(),
agent: "agent-ci".to_owned(),
public_key: "agent-key-v1".to_owned(),
})
.unwrap();
let listed = key_list_report(KeyListArgs {
scope: scope.clone(),
})
.unwrap();
let revoked = key_revoke_report(KeyRevokeArgs {
scope,
agent: "agent-ci".to_owned(),
yes: true,
})
.unwrap();
server.join().unwrap();
assert_eq!(added["command"], "key add");
assert_eq!(added["agent"], "agent-ci");
assert_eq!(added["credential_scope"]["actions"][0], "project:read");
assert_eq!(
added["credential_scope"]["human_account_creation_privilege"],
false
);
assert_eq!(added["browser_interaction_required_each_run"], false);
assert_eq!(added["attribution"]["registered_by_user"], "user");
assert_eq!(listed["records"].as_array().unwrap().len(), 1);
assert_eq!(listed["credential_scope"]["listed_for_user"], "user");
assert_eq!(revoked["revoked"], true);
assert_eq!(revoked["attribution"]["revoked_by_user"], "user");
}
#[test]
fn node_revoke_reports_scoped_credential_revocation() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"revoke_node_credential""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""actor_user":"user""#));
assert!(line.contains(r#""node":"node-a""#));
stream
.write_all(
br#"{"type":"node_credential_revoked","node":"node-a","tenant":"tenant","project":"project","actor":"user","descriptor_removed":true,"queued_assignments_removed":2}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let revoked = node_revoke_report(NodeRevokeArgs {
scope: CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
node: "node-a".to_owned(),
yes: true,
})
.unwrap();
server.join().unwrap();
assert_eq!(revoked["command"], "node revoke");
assert_eq!(revoked["node"], "node-a");
assert_eq!(revoked["credential_revoked"], true);
assert_eq!(revoked["descriptor_removed"], true);
assert_eq!(revoked["queued_assignments_removed"], 2);
assert_eq!(revoked["node_credentials_separate_from_user_session"], true);
}
#[test]
fn admin_status_and_suspend_use_public_coordinator_api() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
for (expected, response) in [
(
"admin_status",
r#"{"type":"admin_status","tenant":"tenant","actor":"admin","suspended":false,"safe_default":"read_only"}"#,
),
(
"suspend_tenant",
r#"{"type":"tenant_suspended","tenant":"tenant","actor":"admin","policy":{"tenant":"tenant","name":"tenant:suspended","digest":"sha256:suspension"}}"#,
),
] {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(&format!(r#""type":"{expected}""#)));
assert!(
line.contains(r#""tenant":"admin-tenant""#)
|| line.contains(r#""tenant":"tenant""#)
);
assert!(line.contains(r#""actor_user":"admin""#));
if expected == "suspend_tenant" {
assert!(line.contains(r#""target_tenant":"tenant""#));
}
stream.write_all(response.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
}
});
let scope = CliScopeArgs {
coordinator: Some(addr),
tenant: "admin-tenant".to_owned(),
project: "project".to_owned(),
user: "admin".to_owned(),
json: false,
};
let status = admin_status_report(AdminStatusArgs {
scope: scope.clone(),
})
.unwrap();
let suspended = admin_suspend_tenant_report(AdminSuspendTenantArgs {
scope,
target_tenant: Some("tenant".to_owned()),
yes: true,
})
.unwrap();
server.join().unwrap();
assert_eq!(status["command"], "admin status");
assert_eq!(status["safe_default"], "read_only");
assert_eq!(status["private_website_required"], false);
assert_eq!(status["suspended"], false);
assert_eq!(suspended["command"], "admin suspend-tenant");
assert_eq!(suspended["tenant"], "tenant");
assert_eq!(suspended["actor_tenant"], "admin-tenant");
assert_eq!(suspended["suspended"], true);
assert_eq!(suspended["private_website_required"], false);
}
#[test]
fn debug_attach_reports_public_authorization() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"debug_attach""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""actor_user":"user""#));
assert!(line.contains(r#""process":"vp""#));
stream
.write_all(
br#"{"type":"debug_attach","process":"vp","actor":"user","authorization":{"allowed":true,"reason":"debug attach authorized for project"},"audit_event":{"tenant":"tenant","project":"project","process":"vp","task":null,"actor":"user","operation":"debug_attach","allowed":true,"reason":"debug attach authorized for project","charged_debug_read_bytes":1024,"used_debug_read_bytes":1024},"charged_debug_read_bytes":1024,"used_debug_read_bytes":1024}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = debug_attach_report_with_dap(
DebugAttachArgs {
scope: CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
process: "vp".to_owned(),
},
"/tmp/disasmer-debug-dap-test".to_owned(),
)
.unwrap();
server.join().unwrap();
assert_eq!(report["command"], "debug attach");
assert_eq!(report["process"], "vp");
assert_eq!(report["authorized"], true);
assert_eq!(
report["authorization"]["reason"],
"debug attach authorized for project"
);
assert_eq!(report["audit_event"]["operation"], "debug_attach");
assert_eq!(report["charged_debug_read_bytes"], 1024);
assert_eq!(report["used_debug_read_bytes"], 1024);
assert_eq!(report["debug_reads_quota_limited"], true);
assert_eq!(report["private_website_required"], false);
}
#[test]
fn human_report_is_text_not_json() {
let report = json!({
"command": "doctor",
"status": "ok",
"coordinator": "127.0.0.1:9443",
"next_actions": ["disasmer login --browser", "disasmer project init"],
});
let human = human_report(&report);
assert!(!human.trim_start().starts_with('{'));
assert!(human.contains("Disasmer doctor"));
assert!(human.contains("status: ok"));
assert!(human.contains("coordinator: 127.0.0.1:9443"));
assert!(human.contains("disasmer login --browser"));
}
#[test]
fn project_init_select_and_status_use_local_project_config() {
let temp = tempfile::tempdir().unwrap();
let init = project_init_report(
ProjectInitArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "ignored".to_owned(),
user: "user".to_owned(),
json: false,
},
new_project: "project-a".to_owned(),
name: "Project A".to_owned(),
yes: true,
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(init["command"], "project init");
let config = read_project_config(temp.path()).unwrap().unwrap();
assert_eq!(config.project, "project-a");
assert_eq!(config.tenant, "tenant");
let selected = project_select_report(
ProjectSelectArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "ignored".to_owned(),
user: "user".to_owned(),
json: false,
},
selected_project: "project-b".to_owned(),
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(selected["command"], "project select");
assert_eq!(
read_project_config(temp.path()).unwrap().unwrap().project,
"project-b"
);
let status = project_status_report(
ProjectStatusArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(status["command"], "project status");
assert_eq!(status["project_identity"]["project"], "project-b");
assert_eq!(status["project_identity"]["tenant"], "tenant");
assert_eq!(status["active_process"], "unknown_without_coordinator");
assert_eq!(status["attached_nodes"]["checked"], false);
}
#[test]
fn project_list_and_select_use_public_api_without_website() {
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-original".to_owned(),
user: "user-live".to_owned(),
coordinator: None,
},
)
.unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
for index in 0..3 {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""tenant":"tenant-live""#));
assert!(line.contains(r#""actor_user":"user-live""#));
match index {
0 => {
assert!(line.contains(r#""type":"list_projects""#));
stream
.write_all(
br#"{"type":"projects","projects":[{"id":"project-a","tenant":"tenant-live","name":"Project A"}],"actor":"user-live"}"#,
)
.unwrap();
}
1 => {
assert!(line.contains(r#""type":"select_project""#));
assert!(line.contains(r#""project":"project-a""#));
stream
.write_all(
br#"{"type":"project_selected","project":{"id":"project-a","tenant":"tenant-live","name":"Project A"},"actor":"user-live"}"#,
)
.unwrap();
}
2 => {
assert!(line.contains(r#""type":"select_project""#));
assert!(line.contains(r#""project":"project-b""#));
stream
.write_all(
br#"{"type":"error","message":"project is outside the signed-in tenant scope"}"#,
)
.unwrap();
}
_ => unreachable!(),
}
stream.write_all(b"\n").unwrap();
}
});
let scope = CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant-live".to_owned(),
project: "project".to_owned(),
user: "user-live".to_owned(),
json: false,
};
let list = project_list_report(
ProjectListArgs {
scope: scope.clone(),
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(list["command"], "project list");
assert_eq!(list["source"], "public_coordinator_api");
assert_eq!(list["project_count"], 1);
assert_eq!(list["projects"][0]["id"], "project-a");
assert_eq!(list["private_website_required"], false);
assert_eq!(list["coordinator_session_requests"], 1);
let selected = project_select_report(
ProjectSelectArgs {
scope: scope.clone(),
selected_project: "project-a".to_owned(),
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(selected["command"], "project select");
assert_eq!(selected["source"], "public_coordinator_api");
assert_eq!(selected["selected_project"]["id"], "project-a");
assert_eq!(selected["project_config_written"], true);
assert_eq!(selected["private_website_required"], false);
assert_eq!(
read_project_config(temp.path()).unwrap().unwrap().project,
"project-a"
);
let rejected = project_select_report(
ProjectSelectArgs {
scope,
selected_project: "project-b".to_owned(),
},
temp.path().to_path_buf(),
)
.unwrap_err();
server.join().unwrap();
assert!(rejected.to_string().contains("tenant scope"));
assert_eq!(
read_project_config(temp.path()).unwrap().unwrap().project,
"project-a"
);
}
#[test]
fn project_status_queries_public_coordinator_state() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
for _ in 0..2 {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
if line.contains("\"type\":\"list_node_descriptors\"") {
assert!(line.contains("\"project\":\"project-live\""));
stream
.write_all(
br#"{"type":"node_descriptors","descriptors":[{"id":"node-a","online":true}],"actor":"user"}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
} else if line.contains("\"type\":\"list_task_events\"") {
assert!(line.contains("\"project\":\"project-live\""));
stream
.write_all(
br#"{"type":"task_events","events":[{"process":"vp-live","task":"task-a"}]}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
} else {
panic!("unexpected coordinator request: {line}");
}
}
});
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-live".to_owned(),
user: "user".to_owned(),
coordinator: Some(addr.clone()),
},
)
.unwrap();
let status = project_status_report(
ProjectStatusArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
server.join().unwrap();
assert_eq!(status["coordinator"], addr);
assert_eq!(status["project_identity"]["project"], "project-live");
assert_eq!(status["attached_nodes"]["checked"], true);
assert_eq!(status["attached_nodes"]["count"], 1);
assert_eq!(status["attached_nodes"]["online"], 1);
assert_eq!(status["active_process"], "vp-live");
assert_eq!(
status["quota_posture"]["current_usage"]["observed_task_events"],
1
);
}
#[test]
fn quota_status_uses_project_config_and_generic_public_limits() {
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-quota".to_owned(),
project: "project-quota".to_owned(),
user: "user".to_owned(),
coordinator: None,
},
)
.unwrap();
let status = quota_status_report(
QuotaStatusArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(status["command"], "quota status");
assert_eq!(status["tenant"], "tenant-quota");
assert_eq!(status["project"], "project-quota");
assert_eq!(status["current_usage"]["attached_nodes"], 0);
assert_eq!(status["limits"]["artifact_download_bytes"], 268_435_456);
assert_eq!(status["community_tier_language"], true);
assert_eq!(
status["next_blocked_action"]["action"],
"node_work_requires_online_attached_node"
);
assert_eq!(
status["next_blocked_action"]["machine_error"]["category"],
"capability"
);
assert_eq!(
status["next_blocked_action"]["machine_error"]["stable_exit_code"],
24
);
}
#[test]
fn quota_status_queries_public_coordinator_usage() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
for _ in 0..2 {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
if line.contains("\"type\":\"list_node_descriptors\"") {
assert!(line.contains("\"tenant\":\"tenant-live\""));
stream
.write_all(
br#"{"type":"node_descriptors","descriptors":[{"id":"node-a","online":true},{"id":"node-b","online":false}],"actor":"user"}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
} else if line.contains("\"type\":\"list_task_events\"") {
assert!(line.contains("\"project\":\"project-live\""));
stream
.write_all(
br#"{"type":"task_events","events":[{"process":"vp-live","task":"task-a"},{"process":"vp-live","task":"task-b"}]}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
} else {
panic!("unexpected coordinator request: {line}");
}
}
});
let temp = tempfile::tempdir().unwrap();
write_project_config(
temp.path(),
&ProjectConfig {
tenant: "tenant-live".to_owned(),
project: "project-live".to_owned(),
user: "user".to_owned(),
coordinator: Some(addr.clone()),
},
)
.unwrap();
let status = quota_status_report(
QuotaStatusArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
server.join().unwrap();
assert_eq!(status["coordinator"], addr);
assert_eq!(status["current_usage"]["attached_nodes"], 2);
assert_eq!(status["current_usage"]["online_nodes"], 1);
assert_eq!(status["current_usage"]["observed_task_events"], 2);
assert!(status["next_blocked_action"].is_null());
assert_eq!(status["task_events"]["response"]["type"], "task_events");
}
#[test]
fn process_task_log_and_artifact_reports_summarize_task_events() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let response = concat!(
r#"{"type":"task_events","events":["#,
r#"{"tenant":"tenant","project":"project","process":"vp","node":"node-a","task":"task-a","placement":{"node":"node-a","score":120,"reasons":["warm environment cache","source snapshot already local"]},"terminal_state":"completed","status_code":0,"stdout_bytes":12,"stderr_bytes":0,"stdout_tail":"ok","stderr_tail":"","stdout_truncated":false,"stderr_truncated":false,"artifact_path":"/vfs/artifacts/app.txt","artifact_digest":"sha256:artifact","artifact_size_bytes":12},"#,
r#"{"tenant":"tenant","project":"project","process":"vp","node":"node-b","task":"task-b","terminal_state":"failed","status_code":1,"stdout_bytes":0,"stderr_bytes":7,"stdout_tail":"","stderr_tail":"boom","stdout_truncated":false,"stderr_truncated":false,"artifact_path":null,"artifact_digest":null,"artifact_size_bytes":null},"#,
r#"{"tenant":"tenant","project":"project","process":"vp","node":"node-c","task":"task-c","terminal_state":"failed","status_code":1,"stdout_bytes":0,"stderr_bytes":71,"stdout_tail":"","stderr_tail":"source snapshot unavailable and direct connectivity unavailable","stdout_truncated":false,"stderr_truncated":false,"artifact_path":null,"artifact_digest":null,"artifact_size_bytes":null}"#,
r#"]}"#
);
for _ in 0..4 {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains("\"type\":\"list_task_events\""));
assert!(line.contains("\"process\":\"vp\""));
stream.write_all(response.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
}
});
let scope = CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
};
let process = process_status_report(ProcessStatusArgs {
scope: scope.clone(),
process: "vp".to_owned(),
})
.unwrap();
let tasks = task_list_report(TaskListArgs {
scope: scope.clone(),
process: Some("vp".to_owned()),
})
.unwrap();
let logs = logs_report(LogsArgs {
scope: scope.clone(),
process: Some("vp".to_owned()),
task: Some("task-a".to_owned()),
})
.unwrap();
let artifacts = artifact_list_report(ArtifactListArgs {
scope,
process: Some("vp".to_owned()),
})
.unwrap();
server.join().unwrap();
assert_eq!(process["state"], "has_failed_tasks");
assert_eq!(process["current_task_count"], 3);
assert_eq!(
process["current_tasks"][0]["node_placement"]["node"],
"node-a"
);
assert_eq!(
process["current_tasks"][0]["node_placement"]["reasons"][0],
"warm environment cache"
);
assert_eq!(tasks["tasks"][0]["node_placement"]["score"], 120);
let rendered_tasks = human_report(&tasks);
assert!(rendered_tasks.contains("placement task-a: node-a"));
assert!(rendered_tasks.contains("source snapshot already local"));
assert_eq!(tasks["tasks"][1]["failure_reason"], "boom");
assert_eq!(tasks["tasks"][1]["machine_error"]["category"], "program");
assert_eq!(tasks["tasks"][1]["machine_error"]["stable_exit_code"], 27);
assert_eq!(
tasks["tasks"][2]["locality_failure"]["affected_data"],
"source_snapshot"
);
assert_eq!(
tasks["tasks"][2]["locality_failure"]["coordinator_bulk_relay_used"],
false
);
assert_eq!(
tasks["tasks"][2]["machine_error"]["category"],
"connectivity"
);
assert_eq!(tasks["tasks"][2]["machine_error"]["stable_exit_code"], 25);
assert_eq!(tasks["tasks"][2]["machine_error"]["locality_failure"], true);
assert!(tasks["tasks"][2]["machine_error"]["next_actions"]
.as_array()
.unwrap()
.iter()
.any(|action| action == "rerun source preparation on an attached node"));
assert!(rendered_tasks.contains("locality task-c: source_snapshot"));
assert!(rendered_tasks.contains("do not rely on coordinator bulk source relay"));
assert_eq!(logs["log_entries"].as_array().unwrap().len(), 1);
assert_eq!(logs["log_entries"][0]["task"], "task-a");
assert_eq!(logs["log_entries"][0]["stdout_tail"], "ok");
assert_eq!(artifacts["artifacts"].as_array().unwrap().len(), 1);
assert_eq!(artifacts["artifacts"][0]["artifact"], "app.txt");
assert_eq!(artifacts["artifacts"][0]["size_bytes"], 12);
assert_eq!(artifacts["artifacts"][0]["known_locations"][0], "node-a");
assert_eq!(artifacts["default_durable_store_assumed"], false);
}
#[test]
fn log_and_task_reports_redact_secret_like_values() {
let events = json!({
"response": {
"type": "task_events",
"events": [{
"tenant": "tenant",
"project": "project",
"process": "vp",
"node": "node-a",
"task": "task-secret",
"terminal_state": "failed",
"status_code": 1,
"stdout_bytes": 128,
"stderr_bytes": 64,
"stdout_tail": "upload token=abc123 Authorization: Bearer bearer-secret",
"stderr_tail": "failed password=hunter2 access_token=provider-secret",
"stdout_truncated": true,
"stderr_truncated": false
}]
}
});
let entries = log_entries(Some(&events), Some("task-secret"));
let entry = &entries.as_array().unwrap()[0];
assert_eq!(
entry["stdout_tail"],
"upload token=[redacted] Authorization: Bearer [redacted]"
);
assert_eq!(
entry["stderr_tail"],
"failed password=[redacted] access_token=[redacted]"
);
assert_eq!(entry["stdout_bytes"], 128);
assert_eq!(entry["stdout_truncated"], true);
assert_eq!(entry["secret_like_values_redacted"], true);
assert_eq!(entry["redacted_fields"][0], "stdout_tail");
assert_eq!(entry["redacted_fields"][1], "stderr_tail");
let tasks = task_summaries(Some(&events));
let task = &tasks.as_array().unwrap()[0];
assert_eq!(
task["failure_reason"],
"failed password=[redacted] access_token=[redacted]"
);
assert_eq!(
task["machine_error"]["message"],
"failed password=[redacted] access_token=[redacted]"
);
assert!(!serde_json::to_string(&entries)
.unwrap()
.contains("provider-secret"));
assert!(!serde_json::to_string(&tasks).unwrap().contains("hunter2"));
}
#[test]
fn artifact_download_and_export_reports_expose_safe_session_boundaries() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let download_response = concat!(
r#"{"type":"artifact_download_link","link":{"artifact":"app.txt","source":{"RetainedNode":"node-a"},"url_path":"/artifacts/tenant/project/vp/app.txt","scoped_token_digest":"sha256:token","expires_at_epoch_seconds":60,"tenant":"tenant","project":"project","process":"vp","actor":{"User":"user"},"max_bytes":2048,"policy_context_digest":"sha256:policy"}}"#
);
let export_response = concat!(
r#"{"type":"artifact_export_plan","source_node":"node-a","receiver_node":"node-b","artifact_size_bytes":9,"plan":{"transport":"NativeQuic","scope":{"tenant":"tenant","project":"project","process":"vp","object":{"Artifact":"app.txt"},"authorization_subject":"artifact-export:node-a-to-node-b"},"source":{"node":"node-a","advertised_addr":"quic://node-a","public_key_fingerprint":"sha256:source"},"destination":{"node":"node-b","advertised_addr":"quic://node-b","public_key_fingerprint":"sha256:destination"},"authorization_digest":"sha256:auth","coordinator_assisted_rendezvous":true,"coordinator_bulk_relay_allowed":false}}"#
);
let export_download_response = concat!(
r#"{"type":"artifact_download_link","link":{"artifact":"app.txt","source":{"RetainedNode":"node-a"},"url_path":"/artifacts/tenant/project/vp/app.txt","scoped_token_digest":"sha256:export-token","expires_at_epoch_seconds":60,"tenant":"tenant","project":"project","process":"vp","actor":{"User":"user"},"max_bytes":67108864,"policy_context_digest":"sha256:policy"}}"#
);
let export_stream_response = concat!(
r#"{"type":"artifact_download_stream","link":{"artifact":"app.txt","source":{"RetainedNode":"node-a"},"url_path":"/artifacts/tenant/project/vp/app.txt","scoped_token_digest":"sha256:export-token","expires_at_epoch_seconds":60,"tenant":"tenant","project":"project","process":"vp","actor":{"User":"user"},"max_bytes":67108864,"policy_context_digest":"sha256:policy"},"streamed_bytes":9,"charged_download_bytes":9,"content_bytes_available":true,"content_base64":"YXBwLWJ5dGVz","content_source":"coordinator_complete_stdout_tail"}"#
);
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"create_artifact_download_link""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""artifact":"app.txt""#));
assert!(line.contains(r#""max_bytes":2048"#));
stream.write_all(download_response.as_bytes()).unwrap();
stream.write_all(b"\n").unwrap();
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
for (expected, phase) in [
("export_artifact_to_node", "export-plan"),
("create_artifact_download_link", "export-download"),
("open_artifact_download_stream", "export-stream"),
] {
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(&format!(r#""type":"{expected}""#)));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""artifact":"app.txt""#));
if expected == "create_artifact_download_link" {
assert_eq!(phase, "export-download");
assert!(line.contains(&format!(
r#""max_bytes":{}"#,
DEFAULT_ARTIFACT_EXPORT_MAX_BYTES
)));
stream
.write_all(export_download_response.as_bytes())
.unwrap();
} else if expected == "export_artifact_to_node" {
assert!(line.contains(r#""receiver_node":"node-b""#));
stream.write_all(export_response.as_bytes()).unwrap();
} else {
assert!(line.contains(r#""chunk_bytes":9"#));
assert!(line.contains(r#""token_digest":"sha256:export-token""#));
stream.write_all(export_stream_response.as_bytes()).unwrap();
}
stream.write_all(b"\n").unwrap();
}
});
let scope = CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
};
let temp = tempfile::tempdir().unwrap();
let export_path = temp.path().join("exports/app.txt");
let download = artifact_download_report(ArtifactDownloadArgs {
scope: scope.clone(),
artifact: "app.txt".to_owned(),
max_bytes: 2048,
})
.unwrap();
let export = artifact_export_report(ArtifactExportArgs {
scope,
artifact: "app.txt".to_owned(),
to: export_path.clone(),
receiver_node: "node-b".to_owned(),
})
.unwrap();
server.join().unwrap();
assert_eq!(
download["download_session"]["status"],
"download_link_issued"
);
assert_eq!(download["download_session"]["tenant"], "tenant");
assert_eq!(download["download_session"]["project"], "project");
assert_eq!(download["download_session"]["process"], "vp");
assert_eq!(
download["download_session"]["source"]["RetainedNode"],
"node-a"
);
assert_eq!(download["download_session"]["max_bytes"], 2048);
assert_eq!(
download["download_session"]["token_material_returned"],
false
);
assert_eq!(
download["download_session"]["scoped_token_digest_present"],
true
);
assert_eq!(download["download_session"]["cross_tenant_usable"], false);
assert_eq!(
download["download_session"]["default_durable_store_assumed"],
false
);
assert_eq!(export["export_plan"]["status"], "transfer_plan_created");
assert_eq!(export["export_plan"]["source_node"], "node-a");
assert_eq!(export["export_plan"]["receiver_node"], "node-b");
assert_eq!(export["export_plan"]["artifact"], "app.txt");
assert_eq!(
export["export_plan"]["local_path"].as_str().unwrap(),
export_path.to_string_lossy().as_ref()
);
assert_eq!(export["export_plan"]["artifact_size_bytes"], 9);
assert_eq!(export["export_plan"]["local_bytes_written_by_cli"], true);
assert_eq!(
export["export_plan"]["local_export_status"],
"local_bytes_written"
);
assert_eq!(export["export_plan"]["bytes_written"], 9);
assert_eq!(
export["local_export"]["stream"]["content_material_returned_in_report"],
false
);
assert_eq!(
std::fs::read(&export_path).unwrap().as_slice(),
b"app-bytes"
);
assert_eq!(
export["export_plan"]["default_durable_store_assumed"],
false
);
assert_eq!(
export["export_plan"]["coordinator_bulk_relay_allowed"],
false
);
assert_eq!(export["export_plan"]["authorization_digest_present"], true);
}
#[test]
fn process_restart_and_cancel_reports_expose_control_boundaries() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let restart_response = r#"{"type":"process_started","process":"vp","epoch":42}"#;
let cancel_response = r#"{"type":"process_cancellation_requested","process":"vp","cancelled_tasks":[{"process":"vp","task":"compile-linux","node":"node-a"},{"process":"vp","task":"link-linux","node":"node-b"}],"affected_nodes":["node-a","node-b"]}"#;
for expected in ["start_process", "cancel_process"] {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(&format!(r#""type":"{expected}""#)));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""process":"vp""#));
if expected == "start_process" {
stream.write_all(restart_response.as_bytes()).unwrap();
} else {
assert!(line.contains(r#""actor_user":"user""#));
stream.write_all(cancel_response.as_bytes()).unwrap();
}
stream.write_all(b"\n").unwrap();
}
});
let scope = CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
};
let restart = process_restart_report(ProcessRestartArgs {
scope: scope.clone(),
process: "vp".to_owned(),
yes: true,
})
.unwrap();
let cancel = process_cancel_report(ProcessCancelArgs {
scope,
process: "vp".to_owned(),
node: None,
task: None,
yes: true,
})
.unwrap();
server.join().unwrap();
assert_eq!(restart["restart_request"]["status"], "process_started");
assert_eq!(
restart["restart_request"]["operation"],
"restart_virtual_process"
);
assert_eq!(restart["restart_request"]["accepted"], true);
assert_eq!(restart["restart_request"]["process"], "vp");
assert_eq!(restart["restart_request"]["coordinator_epoch"], 42);
assert_eq!(restart["restart_request"]["requires_confirmation"], false);
assert_eq!(restart["restart_request"]["website_required"], false);
assert_eq!(
cancel["cancel_request"]["status"],
"process_cancellation_requested"
);
assert_eq!(
cancel["cancel_request"]["operation"],
"cancel_virtual_process"
);
assert_eq!(cancel["cancel_request"]["accepted"], true);
assert_eq!(cancel["cancel_request"]["process"], "vp");
assert_eq!(cancel["cancel_request"]["cancelled_task_count"], 2);
assert_eq!(
cancel["cancel_request"]["cancelled_tasks"][0]["task"],
"compile-linux"
);
assert_eq!(cancel["cancel_request"]["affected_nodes"][1], "node-b");
assert_eq!(cancel["cancel_request"]["requires_confirmation"], false);
assert_eq!(cancel["cancel_request"]["website_required"], false);
assert_eq!(
cancel["cancel_request"]["whole_process_cancel_available"],
true
);
assert_eq!(
cancel["cancel_request"]["node_must_poll_task_control"],
true
);
assert_eq!(cancel["cancel_request"]["new_task_launches_blocked"], true);
}
#[test]
fn task_restart_reports_clean_boundary_requirements() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"restart_task""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""actor_user":"user""#));
assert!(line.contains(r#""process":"vp""#));
assert!(line.contains(r#""task":"compile-linux""#));
stream
.write_all(
br#"{"type":"task_restart","process":"vp","task":"compile-linux","actor":"user","accepted":false,"clean_boundary_available":false,"active_task":true,"completed_event_observed":false,"requires_whole_process_restart":true,"message":"selected task is still active; clean task restart requires a captured checkpoint boundary","audit_event":{"tenant":"tenant","project":"project","process":"vp","task":"compile-linux","actor":"user","operation":"restart_task","allowed":true,"reason":"selected task is still active; clean task restart requires a captured checkpoint boundary","charged_debug_read_bytes":1024,"used_debug_read_bytes":1024},"charged_debug_read_bytes":1024,"used_debug_read_bytes":1024}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = task_restart_report(TaskRestartArgs {
scope: CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
task: "compile-linux".to_owned(),
process: "vp".to_owned(),
yes: true,
})
.unwrap();
server.join().unwrap();
assert_eq!(report["command"], "task restart");
assert_eq!(
report["restart_request"]["operation"],
"restart_selected_task"
);
assert_eq!(report["restart_request"]["accepted"], false);
assert_eq!(report["restart_request"]["clean_boundary_available"], false);
assert_eq!(
report["restart_request"]["requires_whole_process_restart"],
true
);
assert_eq!(report["restart_request"]["active_task"], true);
assert_eq!(
report["restart_request"]["audit_event"]["operation"],
"restart_task"
);
assert_eq!(report["restart_request"]["charged_debug_read_bytes"], 1024);
assert_eq!(report["restart_request"]["used_debug_read_bytes"], 1024);
assert_eq!(report["restart_request"]["debug_reads_quota_limited"], true);
assert_eq!(report["restart_request"]["website_required"], false);
assert_eq!(report["coordinator_session_requests"], 1);
}
#[test]
fn build_command_reuses_bundle_inspection_without_full_repo_upload() {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let report = build_report(
BuildArgs {
project: Some(temp.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
output: None,
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert_eq!(report["command"], "build");
assert_eq!(report["content_addressed"], true);
assert_eq!(report["contains_full_repository_upload"], false);
assert!(report["bundle"]["metadata"]["identity"]
.as_str()
.unwrap()
.starts_with("sha256:"));
assert!(report["bundle"]["metadata"]["wasm_code"]
.as_str()
.unwrap()
.starts_with("sha256:"));
assert_eq!(
report["bundle"]["metadata"]["task_metadata"]["default_entrypoint"],
"build"
);
assert!(report["bundle"]["metadata"]["task_metadata"]["entrypoints"]
.as_array()
.unwrap()
.iter()
.any(|entrypoint| entrypoint == "release"));
assert_eq!(
report["bundle"]["metadata"]["source_metadata"]["transfer_policy"]
["coordinator_receives_source_bytes_by_default"],
false
);
assert_eq!(
report["bundle"]["metadata"]["source_metadata"]["transfer_policy"]
["default_full_repo_tarball"],
false
);
assert_eq!(
report["bundle"]["metadata"]["debug_metadata"]["dap_virtual_process"],
true
);
assert_eq!(
report["bundle"]["metadata"]["large_input_policy"]
["selected_inputs_are_content_digests"],
true
);
assert_eq!(
report["bundle"]["metadata"]["large_input_policy"]["selected_input_bytes_included"],
false
);
assert_eq!(
report["bundle"]["metadata"]["large_input_policy"]["full_repository_bytes_included"],
false
);
assert_eq!(
report["bundle"]["metadata"]["large_input_policy"]
["silent_task_argument_serialization"],
false
);
assert!(
report["bundle"]["metadata"]["large_input_policy"]["supported_handle_types"]
.as_array()
.unwrap()
.iter()
.any(|handle| handle == "Artifact")
);
assert_eq!(
report["bundle"]["metadata"]["restart_compatibility"]
["source_edits_can_restart_from_clean_task_boundary"],
true
);
assert_eq!(
report["bundle"]["metadata"]["restart_compatibility"]
["requires_clean_checkpoint_boundary"],
true
);
assert_eq!(
report["bundle"]["metadata"]["restart_compatibility"]["compares_task_abi"],
report["bundle"]["metadata"]["task_metadata"]["task_abi"]
);
assert_eq!(
report["bundle"]["metadata"]["restart_compatibility"]
["incompatible_changes_require_whole_process_restart"],
true
);
assert_eq!(report["status"], "built");
assert_eq!(report["scheduled_work"], false);
}
#[test]
fn build_blocks_before_schedule_on_missing_environment_reference() {
let temp = tempfile::tempdir().unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("Cargo.toml"), "[package]\nname='demo'\n").unwrap();
fs::write(
temp.path().join("src/main.rs"),
"fn main() { let _target = env!(\"linux\"); }\n",
)
.unwrap();
let report = build_report(
BuildArgs {
project: Some(temp.path().to_path_buf()),
source_provider: None,
disabled_source_providers: Vec::new(),
output: None,
json: false,
},
PathBuf::from("/unused"),
)
.unwrap();
assert_eq!(report["status"], "blocked_before_schedule");
assert_eq!(report["scheduled_work"], false);
assert_eq!(report["machine_error"]["category"], "environment");
assert_eq!(report["diagnostics"][0]["code"], "missing_environment");
}
#[test]
fn node_enroll_reports_short_lived_public_api_grant() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains(r#""type":"create_node_enrollment_grant""#));
assert!(line.contains(r#""tenant":"tenant""#));
assert!(line.contains(r#""project":"project""#));
assert!(line.contains(r#""actor_user":"user""#));
assert!(line.contains(r#""grant":"grant-live""#));
assert!(line.contains(r#""ttl_seconds":300"#));
stream
.write_all(
br#"{"type":"node_enrollment_grant_created","tenant":"tenant","project":"project","grant":"grant-live","scope":"node:attach","expires_at_epoch_seconds":300}"#,
)
.unwrap();
stream.write_all(b"\n").unwrap();
});
let report = node_enroll_report(NodeEnrollArgs {
scope: CliScopeArgs {
coordinator: Some(addr),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
grant: "grant-live".to_owned(),
ttl_seconds: 300,
})
.unwrap();
server.join().unwrap();
assert_eq!(report["command"], "node enroll");
assert_eq!(report["status"], "created");
assert_eq!(report["private_website_required"], false);
assert_eq!(report["tenant"], "tenant");
assert_eq!(report["project"], "project");
assert_eq!(report["user"], "user");
assert_eq!(report["enrollment_grant"]["grant"], "grant-live");
assert_eq!(report["enrollment_grant"]["scope"], "node:attach");
assert_eq!(report["enrollment_grant"]["ttl_seconds"], 300);
assert_eq!(report["enrollment_grant"]["expires_at_epoch_seconds"], 300);
assert_eq!(report["enrollment_grant"]["short_lived"], true);
assert_eq!(
report["enrollment_grant"]["exchange_for_persistent_node_identity"],
true
);
assert_eq!(
report["enrollment_grant"]["node_credentials_separate_from_user_session"],
true
);
assert_eq!(report["coordinator_session_requests"], 1);
}
#[test]
fn node_enroll_and_process_commands_have_safe_plan_without_coordinator() {
let scope = CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
};
let enroll = node_enroll_report(NodeEnrollArgs {
scope: scope.clone(),
grant: "grant".to_owned(),
ttl_seconds: 60,
})
.unwrap();
assert_eq!(enroll["status"], "planned_without_coordinator");
assert_eq!(enroll["private_website_required"], false);
assert_eq!(enroll["enrollment_grant"]["grant"], "grant");
assert_eq!(enroll["enrollment_grant"]["scope"], "node:attach");
assert_eq!(enroll["enrollment_grant"]["ttl_seconds"], 60);
assert_eq!(enroll["enrollment_grant"]["short_lived"], true);
assert_eq!(
enroll["enrollment_grant"]["exchange_for_persistent_node_identity"],
true
);
let cancel = process_cancel_report(ProcessCancelArgs {
scope,
process: "vp".to_owned(),
node: None,
task: None,
yes: false,
})
.unwrap();
assert_eq!(cancel["status"], "confirmation_required");
assert_eq!(cancel["requires_confirmation"], true);
assert_eq!(cancel["coordinator_request_sent"], false);
}
}