From 2a888da955299fe59ce13ed88f5dd2a6cf53f2e6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 14:46:47 +0200 Subject: [PATCH] Verse command: a lane that cannot open names the flag or fault kind, never the endpoint open_inference's error was wrapped with .context and printed through the anyhow chain, so a refused non-loopback --base-url put the operator's address on stderr. lane_fault maps each ControllerOpenError variant to one fixed message; ControllerOpenError is exported for it. Fixes local-endpoint-echoed. Builds; the workspace is unchanged elsewhere. Co-Authored-By: Claude Opus 5.5 --- crates/ghostlight-verse/src/main.rs | 30 +++++++++++++++++-- crates/ghostlight-verse/tests/verse_binary.rs | 29 +++++++++++++++++- crates/ghostlight/src/lib.rs | 1 + 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/crates/ghostlight-verse/src/main.rs b/crates/ghostlight-verse/src/main.rs index 52f0c50..43c97b2 100644 --- a/crates/ghostlight-verse/src/main.rs +++ b/crates/ghostlight-verse/src/main.rs @@ -9,10 +9,10 @@ //! `calls.cc`. The provider key is read from a file at run time and appears //! nowhere else. -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result, anyhow, bail}; use chrono::{Duration, Utc}; use ghostlight::{ - CommandBody, CommandId, ControllerModels, ControllerRunner, DEFAULT_HOSTED_MODEL_PREFIX, + CommandBody, CommandId, ControllerModels, ControllerOpenError, ControllerRunner, DEFAULT_HOSTED_MODEL_PREFIX, DEFAULT_LOCAL_MODEL_PREFIX, CallRecordStore, CoverBudget, CreateVerseWorldIntent, HostedBinding, Lens, LensWeights, LocalBinding, PrincipalCommandIntent, RecordingInferencePort, RunStart, RunStatus, TickMinutes, VerifiedPrincipalEvidence, VerseRunConfig, VerseRunStore, @@ -290,5 +290,29 @@ fn open_lane( bail!("--model names no inference lane (expected a hosted/ or local/ prefix)"); } open_inference(None, None, local, hosted, &models.each()) - .context("the inference lane cannot open") + .map_err(|error| anyhow!(lane_fault(&error))) } + +/// What a lane that cannot open says: the flag or the kind of fault, never a +/// value the operator passed. +fn lane_fault(error: &ControllerOpenError) -> &'static str { + match error { + ControllerOpenError::InvalidModels => "--model is not an exact nonempty identifier", + ControllerOpenError::Connector(_) => "the connector lane cannot open", + ControllerOpenError::WorkStore(_) => "the controller work journal cannot open", + ControllerOpenError::UnroutableModel { .. } => "--model is claimed by no inference lane", + ControllerOpenError::SdkSidecarMissing { .. } => "the SDK sidecar entry is not a file", + ControllerOpenError::LocalEndpointNotLoopback { .. } => { + "--base-url for a local model must be a loopback address" + } + ControllerOpenError::HostedEndpointNotHttps => { + "--base-url for a hosted model must be an https URL" + } + ControllerOpenError::HostedKeyUnreadable => { + "--key-file is unreadable, empty, or not one run of visible ASCII" + } + ControllerOpenError::SharedModelPrefix { .. } => "two inference lanes claim one model prefix", + ControllerOpenError::EmptyModelPrefix { .. } => "an inference lane's model prefix is empty", + } +} + diff --git a/crates/ghostlight-verse/tests/verse_binary.rs b/crates/ghostlight-verse/tests/verse_binary.rs index eda0cc6..e224c21 100644 --- a/crates/ghostlight-verse/tests/verse_binary.rs +++ b/crates/ghostlight-verse/tests/verse_binary.rs @@ -312,7 +312,7 @@ fn the_command_refuses_what_it_cannot_run_and_never_echoes_a_value() { "--token-cap", "1", "--tick-span-minutes", "60", "--model", "hosted/x", "--base-url", "https://api.example.test", "--key-file", key.to_str().unwrap(), ], - "the inference lane cannot open", + "--key-file is unreadable", ), ( vec![ @@ -332,3 +332,30 @@ fn the_command_refuses_what_it_cannot_run_and_never_echoes_a_value() { assert!(!error.contains("many"), "{arguments:?} echoed a value: {error}"); } } + +#[test] +fn a_non_loopback_local_endpoint_is_refused_without_echoing_it() { + let state = tempfile::tempdir().unwrap(); + assert!(genesis(state.path()).status.success()); + let output = verse(&[ + "run", + "--state", + state.path().to_str().unwrap(), + "--ticks", + "1", + "--token-cap", + "1000", + "--tick-span-minutes", + "60", + "--model", + "local/x", + "--base-url", + "http://10.77.0.99:4242", + ]); + assert!(!output.status.success()); + let error = stderr(&output); + assert!(error.contains("--base-url"), "{error}"); + assert!(!error.contains("10.77.0.99"), "the endpoint was echoed: {error}"); + // The lane failed before any run was claimed. + assert!(!stdout(&output).contains("run 1"), "{}", stdout(&output)); +} diff --git a/crates/ghostlight/src/lib.rs b/crates/ghostlight/src/lib.rs index be64414..d59b1c5 100644 --- a/crates/ghostlight/src/lib.rs +++ b/crates/ghostlight/src/lib.rs @@ -85,6 +85,7 @@ pub use consumer::{ }; pub use controllers::{ CONTROLLER_WORK_SCHEMA, CellRun, ConnectorBinding, ControllerError, ControllerModels, + ControllerOpenError, ControllerNeed, ControllerPendingReason, ControllerRunner, ControllerWork, ControllerWorkCustody, ControllerWorkLookup, ControllerWorkStore, ControllerWorkStoreError, ControllerWorkWrite,