Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 71 additions & 15 deletions crates/webcodex-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,27 @@ use webcodex_cli::{
client_profile_state_dir, client_profile_user_token_file,
client_profile_user_token_file_for_scope, connect_usage, controller_usage, current_user_home,
default_device_name, default_server_paths, disconnect_usage, discover_internal_binary,
is_effective_root, login_usage, logout_usage, ops_projects_usage, ops_runner_usage,
ops_runners_usage, ops_smoke_preflight_usage, ops_status_usage, ops_usage, ops_windows_usage,
pairing_create_usage, pairing_usage, parse_plugin_command, parse_plugin_init,
plugin_check_usage, plugin_describe_usage, plugin_init_usage, plugin_list_usage,
plugin_reload_usage, plugin_usage, project_activate_usage, project_register_usage,
read_env_file_value, render_token_generate, run_connect, run_controller_command,
run_disconnect, run_hosted_log_writer, run_internal_binary, run_login, run_logout,
run_ops_command, run_pairing_create, run_plugin_command, run_plugin_init, run_project_activate,
run_project_register, run_runner_install_service, run_runner_service, run_runner_status,
run_runner_token_create_local, run_server_init, run_server_install_service, run_server_service,
run_server_status, run_server_tunnel, run_status, run_token_create_local,
is_effective_root, login_usage, logout_usage, ops_diagnose_provider_usage, ops_projects_usage,
ops_runner_usage, ops_runners_usage, ops_smoke_preflight_usage, ops_status_usage, ops_usage,
ops_windows_usage, pairing_create_usage, pairing_usage, parse_plugin_command,
parse_plugin_init, plugin_check_usage, plugin_describe_usage, plugin_init_usage,
plugin_list_usage, plugin_reload_usage, plugin_usage, project_activate_usage,
project_register_usage, read_env_file_value, render_token_generate, run_connect,
run_controller_command, run_disconnect, run_hosted_log_writer, run_internal_binary, run_login,
run_logout, run_ops_command, run_pairing_create, run_plugin_command, run_plugin_init,
run_project_activate, run_project_register, run_runner_install_service, run_runner_service,
run_runner_status, run_runner_token_create_local, run_server_init, run_server_install_service,
run_server_service, run_server_status, run_server_tunnel, run_status, run_token_create_local,
runner_config_for_scope, runner_init_usage, runner_install_service_usage,
runner_service_file_for_scope, runner_status_usage, runner_usage, server_init_usage,
server_install_service_usage, server_status_usage, server_tunnel_usage, server_usage,
service_unit_name, status_usage, system_user_home, system_user_is_root, usage,
validate_client_profile, validate_service_file_scope, write_connect_result, ConnectAuth,
ConnectOptions, ControllerCommand, DisconnectOptions, LoginOptions, LogoutOptions, OpsCommand,
OpsCommonOptions, OpsRunnerOptions, OpsSmokePreflightOptions, OpsWindowsOptions, PluginCommand,
PluginInitOptions, ProjectActivateOptions, ProjectRegisterOptions, ServerStatusOptions,
ServiceControl, StatusOptions, DEFAULT_LOG_LINES, RUNNER_SERVICE_UNIT, SERVER_SERVICE_FILE,
SERVER_SERVICE_UNIT,
OpsCommonOptions, OpsDiagnoseProviderOptions, OpsRunnerOptions, OpsSmokePreflightOptions,
OpsWindowsOptions, PluginCommand, PluginInitOptions, ProjectActivateOptions,
ProjectRegisterOptions, ServerStatusOptions, ServiceControl, StatusOptions, DEFAULT_LOG_LINES,
RUNNER_SERVICE_UNIT, SERVER_SERVICE_FILE, SERVER_SERVICE_UNIT,
};
const SETUP_GPT_SCOPES: &[&str] = &[
"runtime:read",
Expand Down Expand Up @@ -1626,6 +1626,23 @@ fn parse_ops_subcommand(args: &[String]) -> CliAction {
},
}
}
"diagnose-provider" => {
if args.get(1).is_some_and(|a| a == "--help" || a == "-h") {
return CliAction::Exit {
code: 0,
stdout: ops_diagnose_provider_usage().to_string(),
stderr: String::new(),
};
}
match parse_ops_diagnose_provider(&args[1..]) {
Ok(opts) => CliAction::Ops(OpsCommand::DiagnoseProvider(opts)),
Err(e) => CliAction::Exit {
code: 2,
stdout: String::new(),
stderr: format!("{}\n", e),
},
}
}
"smoke-preflight" => {
if args.get(1).is_some_and(|a| a == "--help" || a == "-h") {
return CliAction::Exit {
Expand Down Expand Up @@ -1778,6 +1795,45 @@ fn parse_ops_windows(args: &[String]) -> Result<OpsWindowsOptions, String> {
})
}

fn parse_ops_diagnose_provider(args: &[String]) -> Result<OpsDiagnoseProviderOptions, String> {
let mut common = default_ops_common_options();
let mut project = String::new();
let mut provider = String::new();
let mut include_live_acp = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--project" => project = next_value(&mut iter, arg)?,
"--provider" => provider = next_value(&mut iter, arg)?,
"--include-live-acp" => include_live_acp = true,
"--server-url" => common.server_url = next_value(&mut iter, arg)?,
"--proxy" => common.server_http.proxy = Some(next_value(&mut iter, arg)?),
"--no-system-proxy" => common.server_http.no_system_proxy = true,
"--env-file" => common.env_file = Some(PathBuf::from(next_value(&mut iter, arg)?)),
"--token-file" => common.token_file = Some(PathBuf::from(next_value(&mut iter, arg)?)),
"--token" => common.token = Some(next_value(&mut iter, arg)?),
"--json" => common.json = true,
"--strict" => common.strict = true,
other => return Err(format!("unknown ops diagnose-provider flag: {other}")),
}
}
common = validate_ops_common(&common)?;
let project = project.trim().to_string();
if project.is_empty() {
return Err("--project is required".to_string());
}
let provider = provider.trim().to_string();
if provider.is_empty() {
return Err("--provider is required".to_string());
}
Ok(OpsDiagnoseProviderOptions {
common,
project,
provider,
include_live_acp,
})
}

fn parse_ops_smoke_preflight(args: &[String]) -> Result<OpsSmokePreflightOptions, String> {
let mut common = default_ops_common_options();
let mut project = String::new();
Expand Down
19 changes: 10 additions & 9 deletions crates/webcodex-cli/src/webcodex_cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ pub(crate) use login::{
LogoutOptions, StatusOptions,
};
pub(crate) use ops::{
run_ops_command, OpsCommand, OpsCommonOptions, OpsRunnerOptions, OpsSmokePreflightOptions,
OpsWindowsOptions,
run_ops_command, OpsCommand, OpsCommonOptions, OpsDiagnoseProviderOptions, OpsRunnerOptions,
OpsSmokePreflightOptions, OpsWindowsOptions,
};
#[cfg(test)]
pub(crate) use output::RevisionComparison;
Expand Down Expand Up @@ -134,13 +134,14 @@ pub(crate) use tokens::{
};
pub(crate) use usage::{
connect_usage, controller_usage, disconnect_usage, login_usage, logout_usage,
ops_projects_usage, ops_runner_usage, ops_runners_usage, ops_smoke_preflight_usage,
ops_status_usage, ops_usage, ops_windows_usage, pairing_create_usage, pairing_usage,
plugin_check_usage, plugin_describe_usage, plugin_init_usage, plugin_list_usage,
plugin_reload_usage, plugin_usage, project_activate_usage, project_register_usage,
runner_init_usage, runner_install_service_usage, runner_status_usage, runner_usage,
server_init_usage, server_install_service_usage, server_status_usage, server_tunnel_usage,
server_usage, status_usage, usage,
ops_diagnose_provider_usage, ops_projects_usage, ops_runner_usage, ops_runners_usage,
ops_smoke_preflight_usage, ops_status_usage, ops_usage, ops_windows_usage,
pairing_create_usage, pairing_usage, plugin_check_usage, plugin_describe_usage,
plugin_init_usage, plugin_list_usage, plugin_reload_usage, plugin_usage,
project_activate_usage, project_register_usage, runner_init_usage,
runner_install_service_usage, runner_status_usage, runner_usage, server_init_usage,
server_install_service_usage, server_status_usage, server_tunnel_usage, server_usage,
status_usage, usage,
};

#[cfg(test)]
Expand Down
193 changes: 193 additions & 0 deletions crates/webcodex-cli/src/webcodex_cli/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ pub(crate) struct OpsSmokePreflightOptions {
pub(crate) project: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OpsDiagnoseProviderOptions {
pub(crate) common: OpsCommonOptions,
pub(crate) project: String,
pub(crate) provider: String,
pub(crate) include_live_acp: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct OpsWindowsOptions {
pub(crate) common: OpsCommonOptions,
Expand All @@ -46,6 +54,7 @@ pub(crate) enum OpsCommand {
Projects(OpsCommonOptions),
Windows(OpsWindowsOptions),
SmokePreflight(OpsSmokePreflightOptions),
DiagnoseProvider(OpsDiagnoseProviderOptions),
}

impl OpsCommand {
Expand All @@ -57,6 +66,7 @@ impl OpsCommand {
OpsCommand::Runner(opts) => opts.common.strict,
OpsCommand::Windows(opts) => opts.common.strict,
OpsCommand::SmokePreflight(opts) => opts.common.strict,
OpsCommand::DiagnoseProvider(opts) => opts.common.strict,
}
}
}
Expand Down Expand Up @@ -337,6 +347,37 @@ pub(crate) async fn run_ops_command(command: OpsCommand) -> Result<OpsCommandOut
);
render_ops_command_output(report, opts.common.json, render_ops_smoke_preflight)
}
OpsCommand::DiagnoseProvider(opts) => {
let token = resolve_ops_token(&opts.common)?;
let report = match call_runtime_tool(
&opts.common.server_url,
&opts.common.server_http,
token.as_deref(),
"diagnose_coding_agent",
json!({
"project": opts.project,
"provider_id": opts.provider,
"include_live_acp": opts.include_live_acp,
}),
)
.await
{
Ok(output) => ops_diagnose_provider_report(
&opts.common.server_url,
&opts.project,
&opts.provider,
opts.include_live_acp,
output.as_ref(),
),
Err(failure) => ops_http_failure_report(
&opts.common.server_url,
"diagnose_coding_agent",
failure,
token.is_some(),
),
};
render_ops_command_output(report, opts.common.json, render_ops_diagnose_provider)
}
}
}

Expand Down Expand Up @@ -1229,6 +1270,158 @@ pub(crate) fn render_ops_runner(report: &OpsReport, json_output: bool) -> Result
Ok(out)
}

/// Operator report for one `diagnose_coding_agent` call. Everything shown is
/// already the bounded Server projection: presence booleans, safe provider
/// inventory, and the optional live ACP diagnostic. No environment values,
/// provider stderr, or credentials ever appear here.
pub(crate) fn ops_diagnose_provider_report(
server_url: &str,
project: &str,
provider: &str,
include_live_acp: bool,
output: Option<&Value>,
) -> OpsReport {
let mut verdict = OpsVerdict::pass();
let output = output.cloned().unwrap_or(Value::Null);
let summary = json!({
"server_url": server_url,
"project": project,
"provider_id": provider,
"include_live_acp": include_live_acp,
"env_presence": output["env_presence"],
"live_acp": output["live_acp"],
"error_kind": output.get("error_kind").cloned().unwrap_or(Value::Null),
});
if let Some(error_kind) = output["error_kind"].as_str() {
verdict.fail_reason(
error_kind,
"fix the reported input/authority problem and re-run; the Server never retried the diagnostic",
);
return OpsReport {
verdict,
summary,
source: json!({
"server_url": server_url,
"tool": "diagnose_coding_agent",
"provider_id": provider,
}),
};
}
let mut absent: Vec<String> = Vec::new();
if let Some(map) = output["env_presence"].as_object() {
for (name, present) in map {
if present.as_bool() != Some(true) {
absent.push(name.clone());
}
}
}
if !absent.is_empty() {
absent.sort();
verdict.warn_reason(
"env_source_absent",
&format!(
"declared env_from_env sources absent from the running Runner process: {} (names only; values are never printed)",
absent.join(", ")
),
);
}
if include_live_acp {
let live = &output["live_acp"];
let outcome = live["outcome"].as_str().unwrap_or("missing");
if outcome != "ok" {
let phase = live["phase"].as_str().unwrap_or("unknown");
verdict.fail_reason(
"live_acp_probe_failed",
&format!(
"live ACP probe stopped at outcome={outcome} phase={phase}; re-observe the Runner provider inventory and fix the provider configuration"
),
);
} else if live["options_truncated"] == true {
verdict.warn_reason(
"live_acp_options_truncated",
"the live option projection hit its bounded limit; unlisted options exist on the live session",
);
}
}
OpsReport {
verdict,
summary,
source: json!({
"server_url": server_url,
"tool": "diagnose_coding_agent",
"provider_id": provider,
}),
}
}

pub(crate) fn render_ops_diagnose_provider(
report: &OpsReport,
json_output: bool,
) -> Result<String, String> {
if json_output {
return render_ops_json(report);
}
let summary = &report.summary;
let mut out = render_overall_header(report);
out.push_str(&render_http_failure(report));
out.push_str(&format!(
"Provider diagnose:\n project={} provider_id={} include_live_acp={}\n",
display_value(&summary["project"]),
display_value(&summary["provider_id"]),
display_value(&summary["include_live_acp"]),
));
out.push_str("Environment mappings:\n");
match summary["env_presence"].as_object() {
Some(map) if !map.is_empty() => {
for (name, present) in map {
out.push_str(&format!(
" {} {}\n",
name,
if present.as_bool() == Some(true) {
"present"
} else {
"ABSENT"
}
));
}
}
_ => out.push_str(" (none declared)\n"),
}
let live = &summary["live_acp"];
if summary["include_live_acp"] == true {
out.push_str("Live ACP:\n");
if live.is_null() {
out.push_str(" (not reported)\n");
} else {
out.push_str(&format!(
" outcome={} phase={} protocol_version={} options={} options_truncated={}\n",
display_value(&live["outcome"]),
display_value(&live["phase"]),
display_value(&live["protocol_version"]),
display_value(&json!(live["options"]
.as_array()
.map(Vec::len)
.unwrap_or(0))),
display_value(&live["options_truncated"]),
));
for option in live["options"].as_array().into_iter().flatten() {
out.push_str(&format!(
" {} kind={} choices={} current={}\n",
display_value(&option["id"]),
display_value(&option["kind"]),
display_value(&json!(option["choices"]
.as_array()
.map(Vec::len)
.unwrap_or(0))),
display_value(&option["current"]),
));
}
}
}
out.push_str(&render_reasons(report));
Ok(out)
}

pub(crate) fn render_ops_projects(report: &OpsReport, json_output: bool) -> Result<String, String> {
if json_output {
return render_ops_json(report);
Expand Down
1 change: 1 addition & 0 deletions crates/webcodex-cli/src/webcodex_cli/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod support;

mod connect;
mod ops;
mod ops_diagnose;
mod platform;
mod plugin;
mod profiles;
Expand Down
4 changes: 4 additions & 0 deletions crates/webcodex-cli/src/webcodex_cli/tests/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,10 @@ async fn run_ops_with_routes(
opts.common.server_url = server_url;
OpsCommand::SmokePreflight(opts)
}
OpsCommand::DiagnoseProvider(mut opts) => {
opts.common.server_url = server_url;
OpsCommand::DiagnoseProvider(opts)
}
};
let output = run_ops_command(command).await.unwrap().stdout;
stop_tx.send(()).unwrap();
Expand Down
Loading
Loading