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 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, #[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, #[arg(long = "oidc-client-id", default_value = "disasmer")] oidc_client_id: String, #[arg(long)] state: Option, } #[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, #[arg(long = "source-provider")] source_provider: Option, #[arg(long = "disable-source-provider")] disabled_source_providers: Vec, #[arg(long)] json: bool, } #[derive(Clone, Debug, Parser)] struct BuildArgs { #[arg(long)] project: Option, #[arg(long = "source-provider")] source_provider: Option, #[arg(long = "disable-source-provider")] disabled_source_providers: Vec, #[arg(long)] output: Option, #[arg(long)] json: bool, } #[derive(Clone, Debug, Parser)] struct RunArgs { entry: Option, #[arg(long)] project: Option, #[arg(long)] coordinator: Option, #[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, #[arg(long, default_value = "tenant")] tenant: String, #[arg(long = "project-id", default_value = "project")] project: String, #[arg(long)] node: Option, #[arg(long = "cap")] caps: Vec, #[arg(long = "enrollment-grant")] enrollment_grant: Option, #[arg(long = "public-key")] public_key: Option, #[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, } #[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, #[arg(long)] task: Option, #[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, } #[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, #[arg(long)] task: Option, } #[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, } #[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, } #[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, #[arg(long)] yes: bool, } #[derive(Clone, Debug, Args)] struct CliScopeArgs { #[arg(long)] coordinator: Option, #[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, 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, 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, source_provider_manifest: SourceProviderManifest, source_provider_statuses: Vec, environment_diagnostics: Vec, pre_schedule_diagnostics: Vec, 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, } #[derive(Clone, Debug, PartialEq, Eq, Serialize)] struct NodeAttachPlan { node: String, coordinator: Option, capabilities: NodeCapabilities, grant_disclosures: Vec, enrollment: Option, } #[derive(Clone, Debug, PartialEq, Serialize)] struct NodeAttachReport { command: String, node: String, plan: NodeAttachPlan, grant_disclosures: Vec, 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, } #[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, 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 { 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::>(), "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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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::>(); 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 { 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 { 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 { 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 { node_descriptors_report("node list", args.scope, None) } fn node_status_report(args: NodeStatusArgs) -> Result { node_descriptors_report("node status", args.scope, args.node) } fn node_descriptors_report( command: &str, scope: CliScopeArgs, node: Option, ) -> Result { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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(¤t_usage), "community_tier_language": true, "private_abuse_heuristics_exposed": false, })) } fn admin_status_report(args: AdminStatusArgs) -> Result { 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 { 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(|| "".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 --yes", "private_website_required": false, } ], })) } fn admin_suspend_tenant_report(args: AdminSuspendTenantArgs) -> Result { 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(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::>(); 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::>(); 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::>(); 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::>(); 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::>(); 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::>(); 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::>(); 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, 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, 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::>(); 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, 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::>()) .unwrap_or_default(); if !actions.is_empty() { lines.push(format!("locality next {task_name}: {}", actions.join("; "))); } } } fn push_nested_string_field(lines: &mut Vec, 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, 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::>(); if !actions.is_empty() { lines.push(format!("error next: {}", actions.join("; "))); } } } fn push_source_provider_statuses(lines: &mut Vec, 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::>(); if !entries.is_empty() { lines.push(format!("source providers: {}", entries.join(", "))); } } fn push_cli_diagnostics(lines: &mut Vec, 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(|_| "".to_owned()) } fn apply_command_report_exit_code(value: &mut Value) -> Option { 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 { 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//Containerfile or envs//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( "a_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> { 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> { 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 { 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 { 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 { 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 { 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, ) -> Result> { 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 { 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 { 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 { 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::>(); match processes.len() { 0 => None, 1 => processes.into_iter().next(), _ => Some(format!( "multiple_observed:{}", processes.into_iter().collect::>().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 { event.get(field).and_then(Value::as_str).map(str::to_owned) } fn event_u64(event: &Value, field: &str) -> Option { 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::>(), "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(¤t_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 { 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 { 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 { 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 { 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::().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 { 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 { 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!( "Disasmer login

Disasmer login

{body}

" ); 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 { 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 { 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('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } 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 { 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> { 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, } 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::>(); 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> { 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 { 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//Containerfile or envs//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::>(); 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, child: Option, } impl LocalCoordinator { fn external(address: &str) -> Self { Self { address: address.to_owned(), process_id: None, child: None, } } fn start_ephemeral() -> Result { 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 { 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 { 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 { 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 { 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 { 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, requests: u64, } impl JsonLineSession { fn connect(addr: &str) -> Result { 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 { let reader = BufReader::new(writer.try_clone()?); Ok(Self { writer, reader, requests: 0, }) } fn request(&mut self, value: Value) -> Result { 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 { 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 { 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::>(); 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::>(); 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::>(); 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 ", "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); } }