M2 audit: excise the Computer Hub stack — Kigi's last remote-cloud surface

Removed root-and-branch for the zero-egress guarantee (the hub was xAI's
remote-workspace/cloud-sandbox service):

- Crates deleted: kigi-computer-hub-core, kigi-computer-hub-sdk,
  kigi-computer-hub-mcp-adapter, kigi-workspace-client (hub-proxied
  workspace RPC client), and kigi-tracing (its sole network path was the
  OTLP gRPC exporter; zero consumers remained). kigi-tracing-macros
  (purely local) stays.
- kigi-workspace: every hub surface deleted — hub server/channel/auth,
  HITL-over-hub permissions, donation/metrics pumps, file upload RPCs,
  hub tool-snapshot merge (resolve pipeline is MCP-only now),
  WorkspaceOps::Proxy. Local worktrees, sessions, leader IPC, MCP, and
  the ACP permission prompt path are untouched; LocalRegistry re-homed
  into kigi-tool-runtime on the existing ToolDyn types so in-process
  tool dispatch is unchanged.
- kigi-shell: leader workspace-exposure control surface (incl. the
  wss://computer-hub... URL), [hub] config, ObservabilityBridge, hub
  WebSocket proxy, dead OTLP config knobs. ClientMode::Headless (never
  constructed) removed.
- kigi-tui/bin: hidden `kigi workspace` command removed (`kigi
  worktree` stays).
- Renames: --xai-api-base-url → --api-base-url / KIGI_API_BASE_URL /
  [endpoints] api_base_url (serde alias keeps old configs working; the
  flag feeds BYOK/custom-endpoint routing, not main inference);
  grok_version → kigi_version in inspect/models-cache/trace metadata
  (old caches self-heal via version-mismatch refetch).
- Dependency tree: dropped fastrace*, opentelemetry-otlp/http/proto,
  tokio-tungstenite from the workspace; fixed the 4 real useless_format
  violations the fastrace lint allowance was masking and removed the
  allowance.
- marketplaceAllowlist kept: it gates the LOCAL plugin-marketplace
  feature, not an xAI service.

Known §9 leftover (deliberate, for the M3 sweep): the BYOK default base
URL string. Gates: workspace check/clippy 0/0, fmt, deny ok; suites
green (workspace 1042, shell 4918, tui 6634, tools 2608, tool-runtime
47, mcp 154).
This commit is contained in:
2026-07-17 22:34:10 -04:00
parent 5919526e91
commit fa75eb139a
90 changed files with 452 additions and 9702 deletions
+1 -3
View File
@@ -79,7 +79,6 @@ kigi-acp-lib = { workspace = true }
axum = { workspace = true, features = ["ws", "multipart"] }
backon = { workspace = true }
webbrowser = { workspace = true }
tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] }
tokio-rustls = { version = "0.26", default-features = false, features = [
"ring",
"logging",
@@ -161,8 +160,7 @@ parking_lot.workspace = true
dashmap.workspace = true
# Used by acp_session.rs and mcp_servers.rs for the unified
# kigi_tool_runtime::Tool dispatch model and the computer-hub MCP adapter.
kigi-computer-hub-sdk = { workspace = true }
# kigi_tool_runtime::Tool dispatch model.
kigi-tool-runtime = { workspace = true }
kigi-tool-protocol = { workspace = true }
kigi-tool-types = { workspace = true }
+1 -7
View File
@@ -518,11 +518,7 @@ pub async fn run_leader(
lock_path: lock.lock_path().clone(),
socket_suffix: socket_suffix_from_paths(lock.lock_path(), &socket_path).unwrap_or_default(),
leader_binary_version: kigi_version::VERSION.to_string(),
})
.with_default_hub_url(agent_config.hub.url.clone());
// Cloned before control_state moves into the IPC server; auth wired below.
let workspace_control = control_state.workspace.clone();
});
// ── Phase 3: Bind socket and start IPC server (BEFORE auth/prefetch) ──────
//
@@ -654,8 +650,6 @@ pub async fn run_leader(
// process so a refresh can't straddle a suspend.
shared_auth_manager.start_system_power_listener();
// Same manager as the leader, so the exposure never writes auth.json itself.
workspace_control.set_auth_manager(shared_auth_manager.clone());
let auth_manager_for_agent = shared_auth_manager.clone();
let auth_manager_for_config = shared_auth_manager;
+19 -588
View File
@@ -43,7 +43,7 @@ pub fn default_agent_type() -> String {
DEFAULT_AGENT_TYPE.to_owned()
}
/// Default base URL for the public xAI API.
pub const XAI_API_BASE_URL_DEFAULT: &str = "https://api.x.ai/v1";
pub const API_BASE_URL_DEFAULT: &str = "https://api.x.ai/v1";
/// One or more environment variable names that may hold a model API key.
///
/// Serde `untagged`: accepts a string or an array in TOML/JSON.
@@ -142,8 +142,10 @@ pub struct EndpointsConfig {
/// default value) lets an org pin the proxy to the default on purpose.
#[serde(skip_serializing_if = "Option::is_none")]
pub coding_api_base_url: Option<String>,
/// Base URL for the public xAI API.
pub xai_api_base_url: String,
/// Base URL for direct (BYOK / external-API-key) API calls.
/// Accepts the legacy `xai_api_base_url` config key.
#[serde(alias = "xai_api_base_url")]
pub api_base_url: String,
/// Optional extra access-header value (applied only with the optional
/// non-production feature, and only for matching first-party hosts).
#[serde(skip_serializing_if = "Option::is_none")]
@@ -166,54 +168,6 @@ pub struct EndpointsConfig {
/// Defaults to `{proxy_url()}/deployment/config`.
#[serde(skip_serializing_if = "Option::is_none")]
pub managed_config_url: Option<String>,
/// Env: `OTEL_EXPORTER_OTLP_ENDPOINT`. OTLP collector base; `/v1/traces` is
/// appended. Legacy repoint of the INTERNAL trace pipeline — deprecated in
/// favor of `KIGI_INTERNAL_OTLP_TRACES_ENDPOINT`, and ignored by the internal
/// pipeline when `KIGI_EXTERNAL_OTEL` is set (the standard `OTEL_*` vars then
/// route the external stream only).
#[serde(skip_serializing_if = "Option::is_none")]
pub otel_exporter_otlp_endpoint: Option<String>,
/// Env: `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. Full traces endpoint, used
/// verbatim; overrides `otel_exporter_otlp_endpoint`. Same legacy/deprecation
/// semantics as `otel_exporter_otlp_endpoint`.
#[serde(skip_serializing_if = "Option::is_none")]
pub otel_exporter_otlp_traces_endpoint: Option<String>,
/// Env: `OTEL_EXPORTER_OTLP_HEADERS`. `k=v,k2=v2`; merged onto export headers.
/// Same legacy/deprecation semantics as `otel_exporter_otlp_endpoint`.
#[serde(skip_serializing_if = "Option::is_none")]
pub otel_exporter_otlp_headers: Option<String>,
/// Env: `KIGI_INTERNAL_OTLP_TRACES_ENDPOINT`. Full INTERNAL traces endpoint,
/// used verbatim. Dev/debug repoint of the internal span firehose (replaces
/// the legacy `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` behavior; used by
/// local-ic-testing / internal dev flows). Wins over the legacy `OTEL_*` vars.
#[serde(skip_serializing_if = "Option::is_none")]
pub grok_internal_otlp_traces_endpoint: Option<String>,
/// Env: `KIGI_INTERNAL_OTLP_HEADERS`. `k=v,k2=v2` extra headers for the
/// internal export (debug). Wins over the legacy `OTEL_EXPORTER_OTLP_HEADERS`.
#[serde(skip_serializing_if = "Option::is_none")]
pub grok_internal_otlp_headers: Option<String>,
/// External-OTEL master switch, captured at construction via
/// [`external_otel_master_switch_resolved`] — the same layered resolution
/// (requirement pin > `KIGI_EXTERNAL_OTEL` env > `[telemetry].otel_enabled`
/// config, managed layers included) that activates the external stream.
/// When set, the standard `OTEL_EXPORTER_OTLP_*` vars are reserved for the
/// external OTEL stream and the internal trace pipeline ignores them
/// entirely — an admin who opts in (by *any* layer, including an org
/// enable distributed via managed config with no env var) never receives
/// the internally-authed firehose. Held as a field (not re-read in the
/// resolvers) so the resolvers stay pure and testable without env races.
#[serde(skip)]
pub external_otel_master_switch: bool,
/// Env: `OTEL_TRACES_EXPORTER`. `otlp` (default) or `none` to disable spans.
#[serde(skip_serializing_if = "Option::is_none")]
pub otel_traces_exporter: Option<String>,
/// Env: `OTEL_BSP_SCHEDULE_DELAY` (OTel) or `OTEL_TRACES_EXPORT_INTERVAL`
/// (Claude alias). Batch flush interval (ms).
#[serde(skip_serializing_if = "Option::is_none")]
pub otel_traces_export_interval: Option<u64>,
/// Env: `OTEL_EXPORTER_OTLP_TIMEOUT`. Export HTTP timeout (ms).
#[serde(skip_serializing_if = "Option::is_none")]
pub otel_exporter_otlp_timeout: Option<u64>,
/// Read by `load_management_api_key_sync()`. Declared for `serde_ignored`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub management_api_key: Option<String>,
@@ -228,18 +182,6 @@ fn blank_as_unset(opt: &Option<String>) -> Option<String> {
.filter(|s| !s.trim().is_empty())
.map(str::to_owned)
}
/// Parse a `k=v,k2=v2` OTLP header list (the `OTEL_EXPORTER_OTLP_HEADERS`
/// format, shared with `KIGI_INTERNAL_OTLP_HEADERS`): split on `,`,
/// `split_once('=')`, trim key/value, skip blank keys, keep empty values.
fn parse_otlp_header_list(raw: &str) -> Vec<(String, String)> {
raw.split(',')
.filter_map(|kv| {
let (k, v) = kv.split_once('=')?;
let k = k.trim();
(!k.is_empty()).then(|| (k.to_string(), v.trim().to_string()))
})
.collect()
}
impl EndpointsConfig {
pub fn has_custom_endpoint(&self) -> bool {
self.models_base_url.is_some() || self.models_list_url.is_some()
@@ -257,22 +199,18 @@ impl EndpointsConfig {
/// Layer the `[endpoints]` table from `config` over the env/default base.
/// No field is derived from another — defaulting is done by the resolvers.
pub(crate) fn from_config_value(config: &toml::Value) -> Self {
let default = Self::default();
let external_otel_master_switch = default.external_otel_master_switch;
let mut base = match toml::Value::try_from(default) {
let mut base = match toml::Value::try_from(Self::default()) {
Ok(v) => v,
Err(_) => return Self::default(),
};
if let Some(endpoints) = config.get("endpoints") {
crate::config::deep_merge_toml(&mut base, endpoints);
}
let mut resolved: Self = base.try_into().unwrap_or_default();
resolved.external_otel_master_switch = external_otel_master_switch;
resolved
base.try_into().unwrap_or_default()
}
/// The subscription proxy base URL through which all auxiliary services (and
/// OAuth/session inference) resolve: explicit `coding_api_base_url`, else
/// [`kigi_env::coding_api_base_url`]. NEVER falls back to `xai_api_base_url` —
/// [`kigi_env::coding_api_base_url`]. NEVER falls back to `api_base_url` —
/// that is the inference endpoint (API-key auth) only.
pub fn proxy_url(&self) -> String {
blank_as_unset(&self.coding_api_base_url).unwrap_or_else(kigi_env::coding_api_base_url)
@@ -283,12 +221,12 @@ impl EndpointsConfig {
.unwrap_or_else(|| self.proxy_url())
}
/// Feedback endpoint — an auxiliary service, so it defaults to the
/// cli-chat-proxy, never `xai_api_base_url`.
/// cli-chat-proxy, never `api_base_url`.
pub fn resolve_feedback_base_url(&self) -> String {
blank_as_unset(&self.feedback_base_url).unwrap_or_else(|| self.proxy_url())
}
/// Managed deployment-config URL (`grok setup`): explicit `managed_config_url`,
/// else `proxy_url` + `/deployment/config`. Never `xai_api_base_url`, so the
/// else `proxy_url` + `/deployment/config`. Never `api_base_url`, so the
/// deployment key reaches the proxy, not the inference host.
pub fn resolve_managed_config_url(&self) -> String {
blank_as_unset(&self.managed_config_url).unwrap_or_else(|| {
@@ -298,99 +236,6 @@ impl EndpointsConfig {
)
})
}
/// INTERNAL OTLP traces endpoint. Precedence:
/// 1. `grok_internal_otlp_traces_endpoint` (verbatim)
/// 2. legacy `otel_exporter_otlp_traces_endpoint` (verbatim) >
/// `otel_exporter_otlp_endpoint` + `/v1/traces` — ONLY when the
/// external-OTEL master switch is unset (back-compat; deprecated)
/// 3. `proxy_url` + `/traces`.
/// Uses the proxy default (not the `xai_api_base_url` fallback) so
/// telemetry reports to xAI even when inference is overridden. When the
/// master switch IS set, the standard `OTEL_EXPORTER_OTLP_*` values are
/// completely ignored here so the internally-authed firehose never lands
/// at an external collector.
pub fn resolve_otlp_traces_endpoint(&self) -> String {
if let Some(full) = blank_as_unset(&self.grok_internal_otlp_traces_endpoint) {
return full.trim_end_matches('/').to_string();
}
if !self.external_otel_master_switch
&& let Some(legacy) = self.legacy_internal_otlp_traces_endpoint()
{
tracing::warn!(
"Repointing the internal trace pipeline via OTEL_EXPORTER_OTLP_ENDPOINT / \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is deprecated; use \
KIGI_INTERNAL_OTLP_TRACES_ENDPOINT instead the standard OTEL_* vars will \
route the external OTEL stream only in a future release"
);
return legacy;
}
format!("{}/traces", self.proxy_url().trim_end_matches('/'))
}
/// Legacy (standard-OTEL-var) internal traces endpoint, if any:
/// `otel_exporter_otlp_traces_endpoint` verbatim, else
/// `otel_exporter_otlp_endpoint` + `/v1/traces`. Ignores the master switch.
fn legacy_internal_otlp_traces_endpoint(&self) -> Option<String> {
if let Some(full) = blank_as_unset(&self.otel_exporter_otlp_traces_endpoint) {
return Some(full.trim_end_matches('/').to_string());
}
blank_as_unset(&self.otel_exporter_otlp_endpoint)
.map(|base| format!("{}/v1/traces", base.trim_end_matches('/')))
}
/// Extra headers for the INTERNAL export: `grok_internal_otlp_headers`
/// first; legacy fallback to `otel_exporter_otlp_headers` ONLY when the
/// external-OTEL master switch is unset (back-compat for existing users).
pub fn resolve_otlp_headers(&self) -> Vec<(String, String)> {
if let Some(headers) = blank_as_unset(&self.grok_internal_otlp_headers) {
return parse_otlp_header_list(&headers);
}
if !self.external_otel_master_switch {
return parse_otlp_header_list(
self.otel_exporter_otlp_headers.as_deref().unwrap_or(""),
);
}
Vec::new()
}
/// Whether the legacy fallback actually supplied the internal endpoint OR
/// internal headers from the standard `OTEL_EXPORTER_OTLP_*` vars — i.e.
/// the master switch is unset AND (`otel_exporter_otlp_traces_endpoint` /
/// `otel_exporter_otlp_endpoint` is non-blank for the endpoint, or
/// `otel_exporter_otlp_headers` is non-blank for headers) AND no
/// `grok_internal_otlp_*` override shadowed that half.
///
/// CONTRACT: this flag is passed to the external OTEL stream's init, which
/// MUST refuse to activate when it is true — the same standard vars cannot
/// feed both pipelines (no-double-send invariant, enforced in code).
pub fn internal_otlp_consumed_standard_vars(&self) -> bool {
if self.external_otel_master_switch {
return false;
}
let endpoint_consumed = blank_as_unset(&self.grok_internal_otlp_traces_endpoint).is_none()
&& self.legacy_internal_otlp_traces_endpoint().is_some();
let headers_consumed = blank_as_unset(&self.grok_internal_otlp_headers).is_none()
&& blank_as_unset(&self.otel_exporter_otlp_headers).is_some();
endpoint_consumed || headers_consumed
}
/// Trace export enabled unless `OTEL_TRACES_EXPORTER=none`. Deliberately
/// still honored by the internal pipeline even with `KIGI_EXTERNAL_OTEL`
/// set: disabling internal span export is the safe direction.
pub fn resolve_traces_export_enabled(&self) -> bool {
!matches!(
self.otel_traces_exporter.as_deref().map(str::trim),
Some("none")
)
}
/// `OTEL_BSP_SCHEDULE_DELAY` / `OTEL_TRACES_EXPORT_INTERVAL` — tuning-only,
/// deliberately shared between the internal and external pipelines.
pub fn resolve_otlp_export_interval(&self) -> Option<std::time::Duration> {
self.otel_traces_export_interval
.map(std::time::Duration::from_millis)
}
/// `OTEL_EXPORTER_OTLP_TIMEOUT` — tuning-only, deliberately shared between
/// the internal and external pipelines.
pub fn resolve_otlp_timeout(&self) -> Option<std::time::Duration> {
self.otel_exporter_otlp_timeout
.map(std::time::Duration::from_millis)
}
/// `models_list_url` > `{models_base_url}/models` > `{proxy_base_url}/models`.
pub fn resolve_models_list_url(&self) -> String {
if let Some(ref url) = self.models_list_url {
@@ -407,26 +252,14 @@ impl Default for EndpointsConfig {
fn default() -> Self {
Self {
coding_api_base_url: std::env::var("KIGI_CODE_BASE_URL").ok(),
xai_api_base_url: std::env::var("KIGI_XAI_API_BASE_URL")
.unwrap_or_else(|_| XAI_API_BASE_URL_DEFAULT.to_owned()),
api_base_url: std::env::var("KIGI_API_BASE_URL")
.unwrap_or_else(|_| API_BASE_URL_DEFAULT.to_owned()),
alpha_test_key: None,
models_base_url: env_string("KIGI_MODELS_BASE_URL"),
models_list_url: env_string("KIGI_MODELS_LIST_URL"),
feedback_base_url: env_string("KIGI_FEEDBACK_BASE_URL"),
deployment_key: env_string("KIGI_DEPLOYMENT_KEY"),
managed_config_url: env_string("KIGI_MANAGED_CONFIG_URL"),
otel_exporter_otlp_endpoint: env_string("OTEL_EXPORTER_OTLP_ENDPOINT"),
otel_exporter_otlp_traces_endpoint: env_string("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"),
otel_exporter_otlp_headers: env_string("OTEL_EXPORTER_OTLP_HEADERS"),
grok_internal_otlp_traces_endpoint: env_string("KIGI_INTERNAL_OTLP_TRACES_ENDPOINT"),
grok_internal_otlp_headers: env_string("KIGI_INTERNAL_OTLP_HEADERS"),
external_otel_master_switch: external_otel_master_switch_resolved(),
otel_traces_exporter: env_string("OTEL_TRACES_EXPORTER"),
otel_traces_export_interval: env_string("OTEL_BSP_SCHEDULE_DELAY")
.or_else(|| env_string("OTEL_TRACES_EXPORT_INTERVAL"))
.and_then(|s| s.parse().ok()),
otel_exporter_otlp_timeout: env_string("OTEL_EXPORTER_OTLP_TIMEOUT")
.and_then(|s| s.parse().ok()),
management_api_key: None,
gcs_service_account_key: None,
}
@@ -1005,30 +838,6 @@ pub struct RemoteConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub secret: Option<String>,
}
/// `[hub]` section from config.toml.
///
/// Optional default Computer Hub URL for **workspace provider** exposure
/// (`grok workspace` / leader `with_default_hub_url`). Does **not** enable
/// agent-side harness/client connections or alter local session behavior.
///
/// ```toml
/// [hub]
/// url = "wss://hub.x.ai/ws"
/// ```
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct HubConfig {
/// Hub WebSocket URL (`ws://` or `wss://`) used as the leader default for
/// `grok workspace start` when the CLI does not pass `--hub-url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
impl HubConfig {
/// Whether a non-empty hub URL is configured (workspace default only).
pub fn is_enabled(&self) -> bool {
self.url.as_ref().is_some_and(|u| !u.trim().is_empty())
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct WorktreePoolConfig {
@@ -1250,9 +1059,6 @@ pub struct Config {
pub harness: HarnessConfig,
#[serde(default, skip_serializing)]
pub remote: RemoteConfig,
/// Computer Hub configuration (`[hub]` in config.toml).
#[serde(default, skip_serializing)]
pub hub: HubConfig,
#[serde(default, skip_serializing)]
pub worktree_pool: WorktreePoolConfig,
#[serde(default, skip_serializing)]
@@ -1632,7 +1438,6 @@ impl Default for Config {
platforms: PlatformsConfig::default(),
harness: HarnessConfig::default(),
remote: RemoteConfig::default(),
hub: HubConfig::default(),
worktree_pool: WorktreePoolConfig::default(),
sandbox: SandboxSettingsConfig::default(),
mcp_servers: std::collections::HashMap::new(),
@@ -2703,41 +2508,6 @@ pub(crate) fn read_requirements_toml() -> Option<toml::Value> {
pub fn deployment_id_from_key(key: &str) -> String {
uuid::Uuid::new_v5(&uuid::Uuid::NAMESPACE_OID, key.as_bytes()).to_string()
}
/// Resolve the external-OTEL master switch exactly the way the external
/// stream's activation does: **requirement pin > `KIGI_EXTERNAL_OTEL` env >
/// `[telemetry].otel_enabled` config layer (managed config included) > off**.
///
/// The internal trace pipeline keys its "ignore `OTEL_EXPORTER_OTLP_*`"
/// behavior off this value ([`EndpointsConfig::external_otel_master_switch`]),
/// so an org enable distributed via managed config / requirements (no env
/// var) flips **both** sides together. A desync here would leave the
/// internally-authed firehose honoring legacy `OTEL_*` repointing while
/// `internal_pipeline_consumed_otel_vars` simultaneously blocks the external
/// stream — exactly the split this design forbids.
pub(crate) fn external_otel_master_switch_resolved() -> bool {
external_otel_master_switch_from(
kigi_config::load_merged_requirements().as_ref(),
env_bool("KIGI_EXTERNAL_OTEL"),
crate::config::load_effective_config().ok().as_ref(),
)
}
/// Testable core of [`external_otel_master_switch_resolved`].
pub(crate) fn external_otel_master_switch_from(
requirements: Option<&toml::Value>,
env_switch: Option<bool>,
effective_config: Option<&toml::Value>,
) -> bool {
let table_enabled = |v: Option<&toml::Value>| -> Option<bool> {
v?.get("telemetry")?.get("otel_enabled")?.as_bool()
};
if let Some(pinned) = table_enabled(requirements) {
return pinned;
}
if let Some(env) = env_switch {
return env;
}
table_enabled(effective_config).unwrap_or(false)
}
/// Seed free-function remote caches after writing `Config.remote_settings`.
pub fn apply_remote_settings_side_effects(settings: Option<&crate::util::config::RemoteSettings>) {
crate::util::config::cache_remote_mcp_startup_timeout_secs(
@@ -4983,7 +4753,7 @@ reasoning_effort = "low"
auth_scheme: AuthScheme::Bearer,
};
assert_eq!(
api_key_creds.base_url, endpoints.xai_api_base_url,
api_key_creds.base_url, endpoints.api_base_url,
"{model_id}: ExternalApiKey must route to api.x.ai"
);
}
@@ -6890,24 +6660,18 @@ reasoning_effort = "low"
for k in [
"KIGI_CODE_BASE_URL",
kigi_env::CODE_BASE_URL_ENV,
"KIGI_XAI_API_BASE_URL",
"KIGI_API_BASE_URL",
"KIGI_FEEDBACK_BASE_URL",
"KIGI_TRACE_UPLOAD_URL",
"KIGI_MANAGED_CONFIG_URL",
"KIGI_MODELS_BASE_URL",
"KIGI_MODELS_LIST_URL",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"KIGI_INTERNAL_OTLP_TRACES_ENDPOINT",
"KIGI_INTERNAL_OTLP_HEADERS",
"KIGI_EXTERNAL_OTEL",
] {
unsafe { std::env::remove_var(k) };
}
}
/// INVARIANT: auxiliary-service resolvers resolve to the cli-chat-proxy, never
/// `xai_api_base_url` — overriding ONLY inference keeps every aux endpoint on
/// `api_base_url` — overriding ONLY inference keeps every aux endpoint on
/// the proxy; explicit per-service overrides win verbatim.
#[test]
#[serial]
@@ -6915,7 +6679,7 @@ reasoning_effort = "low"
unset_endpoint_env_vars();
let inference = "https://inference.acme-corp.example/xai/v1";
let cfg = EndpointsConfig {
xai_api_base_url: inference.to_string(),
api_base_url: inference.to_string(),
coding_api_base_url: None,
..Default::default()
};
@@ -6928,11 +6692,7 @@ reasoning_effort = "low"
format!("{proxy}/deployment/config")
);
assert_eq!(cfg.resolve_feedback_base_url(), proxy);
assert_eq!(
cfg.resolve_otlp_traces_endpoint(),
format!("{proxy}/traces")
);
assert_eq!(cfg.xai_api_base_url, inference);
assert_eq!(cfg.api_base_url, inference);
let overridden = EndpointsConfig {
coding_api_base_url: Some("https://proxy.enterprise.example/v1".to_string()),
managed_config_url: Some(
@@ -6945,10 +6705,6 @@ reasoning_effort = "low"
overridden.proxy_url(),
"https://proxy.enterprise.example/v1"
);
assert_eq!(
overridden.resolve_otlp_traces_endpoint(),
"https://proxy.enterprise.example/v1/traces"
);
assert_eq!(
overridden.resolve_managed_config_url(),
"https://control.enterprise.example/deployment/config"
@@ -6958,7 +6714,7 @@ reasoning_effort = "low"
"https://feedback.enterprise.example"
);
}
/// REGRESSION: the managed-config URL never follows `xai_api_base_url`
/// REGRESSION: the managed-config URL never follows `api_base_url`
/// through the full loader `Config::new_from_toml_cfg` — a distinct construction
/// path from `from_config_value`, so the deployment key never reaches the
/// inference host on either.
@@ -6969,7 +6725,7 @@ reasoning_effort = "low"
let cfg = Config::new_from_toml_cfg(
&toml::from_str(
r#"[endpoints]
xai_api_base_url = "https://inference.acme-corp.example/xai/v1""#,
api_base_url = "https://inference.acme-corp.example/xai/v1""#,
)
.unwrap(),
)
@@ -8467,311 +8223,6 @@ agent_type = "cursor"
"exactly the typo'd key must be flagged"
);
}
#[test]
fn otlp_traces_endpoint_precedence() {
let proxy = "https://inference.acme.com/v1".to_string();
let derived = EndpointsConfig {
coding_api_base_url: Some(proxy.clone()),
..Default::default()
};
assert_eq!(
derived.resolve_otlp_traces_endpoint(),
"https://inference.acme.com/v1/traces"
);
let base = EndpointsConfig {
coding_api_base_url: Some(proxy.clone()),
otel_exporter_otlp_endpoint: Some("https://otel.acme.com".to_string()),
..Default::default()
};
assert_eq!(
base.resolve_otlp_traces_endpoint(),
"https://otel.acme.com/v1/traces"
);
let full = EndpointsConfig {
coding_api_base_url: Some(proxy),
otel_exporter_otlp_endpoint: Some("https://ignored.example".to_string()),
otel_exporter_otlp_traces_endpoint: Some("https://otel.acme.com/v1/traces".to_string()),
..Default::default()
};
assert_eq!(
full.resolve_otlp_traces_endpoint(),
"https://otel.acme.com/v1/traces"
);
}
#[test]
fn otlp_headers_parse() {
let cfg = EndpointsConfig {
otel_exporter_otlp_headers: Some("a=1, b = 2 ,=skip,c=".to_string()),
..Default::default()
};
assert_eq!(
cfg.resolve_otlp_headers(),
vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
("c".to_string(), String::new()),
]
);
}
/// Base config for the internal-OTLP tests: pinned proxy, every OTLP knob
/// explicitly unset so ambient env (via `Default`) can't leak in.
fn internal_otlp_test_config() -> EndpointsConfig {
EndpointsConfig {
coding_api_base_url: Some("https://proxy.example/v1".to_string()),
otel_exporter_otlp_endpoint: None,
otel_exporter_otlp_traces_endpoint: None,
otel_exporter_otlp_headers: None,
grok_internal_otlp_traces_endpoint: None,
grok_internal_otlp_headers: None,
external_otel_master_switch: false,
..Default::default()
}
}
/// `grok_internal_otlp_traces_endpoint` wins over the legacy `OTEL_*`
/// fields regardless of the master switch.
#[test]
fn internal_otlp_endpoint_grok_internal_wins_regardless_of_switch() {
for switch in [false, true] {
let cfg = EndpointsConfig {
grok_internal_otlp_traces_endpoint: Some(
"https://internal.example/traces/".to_string(),
),
otel_exporter_otlp_traces_endpoint: Some(
"https://legacy.example/v1/traces".to_string(),
),
otel_exporter_otlp_endpoint: Some("https://legacy-base.example".to_string()),
external_otel_master_switch: switch,
..internal_otlp_test_config()
};
assert_eq!(
cfg.resolve_otlp_traces_endpoint(),
"https://internal.example/traces",
"switch={switch}: KIGI_INTERNAL_OTLP_TRACES_ENDPOINT must win verbatim (trailing / trimmed)"
);
}
}
/// Master switch unset → legacy fallback preserved (back-compat).
#[test]
fn internal_otlp_endpoint_legacy_fallback_when_switch_unset() {
let traces = EndpointsConfig {
otel_exporter_otlp_traces_endpoint: Some(
"https://legacy.example/v1/traces".to_string(),
),
..internal_otlp_test_config()
};
assert_eq!(
traces.resolve_otlp_traces_endpoint(),
"https://legacy.example/v1/traces"
);
let base = EndpointsConfig {
otel_exporter_otlp_endpoint: Some("https://legacy-base.example/".to_string()),
..internal_otlp_test_config()
};
assert_eq!(
base.resolve_otlp_traces_endpoint(),
"https://legacy-base.example/v1/traces"
);
}
/// Master switch SET → legacy `OTEL_*` endpoint/headers are completely
/// ignored by the internal pipeline (the external stream owns them); the
/// internal pipeline falls back to the proxy default and
/// `internal_otlp_consumed_standard_vars()` is false.
#[test]
fn internal_otlp_ignores_legacy_vars_when_switch_set() {
let cfg = EndpointsConfig {
otel_exporter_otlp_traces_endpoint: Some(
"https://admin-collector.example/v1/traces".to_string(),
),
otel_exporter_otlp_endpoint: Some("https://admin-collector.example".to_string()),
otel_exporter_otlp_headers: Some("authorization=Bearer admin".to_string()),
external_otel_master_switch: true,
..internal_otlp_test_config()
};
assert_eq!(
cfg.resolve_otlp_traces_endpoint(),
"https://proxy.example/v1/traces",
"internal firehose must never follow OTEL_* to the external collector"
);
assert_eq!(cfg.resolve_otlp_headers(), Vec::<(String, String)>::new());
assert!(!cfg.internal_otlp_consumed_standard_vars());
}
/// `internal_otlp_consumed_standard_vars()` truth table.
#[test]
fn internal_otlp_consumed_standard_vars_cases() {
struct Case {
switch: bool,
legacy_traces_ep: bool,
legacy_base_ep: bool,
legacy_headers: bool,
internal_ep: bool,
internal_headers: bool,
expected: bool,
why: &'static str,
}
let unset = Case {
switch: false,
legacy_traces_ep: false,
legacy_base_ep: false,
legacy_headers: false,
internal_ep: false,
internal_headers: false,
expected: false,
why: "nothing set",
};
let cases = [
Case { ..unset },
Case {
legacy_traces_ep: true,
expected: true,
why: "legacy traces endpoint consumed",
..unset
},
Case {
legacy_base_ep: true,
expected: true,
why: "legacy base endpoint consumed",
..unset
},
Case {
legacy_headers: true,
expected: true,
why: "legacy headers consumed",
..unset
},
Case {
legacy_traces_ep: true,
internal_ep: true,
expected: false,
why: "internal endpoint shadows legacy",
..unset
},
Case {
legacy_headers: true,
internal_headers: true,
expected: false,
why: "internal headers shadow legacy",
..unset
},
Case {
legacy_traces_ep: true,
legacy_headers: true,
internal_ep: true,
expected: true,
why: "endpoint shadowed but legacy headers still consumed (headers half)",
..unset
},
Case {
switch: true,
legacy_traces_ep: true,
legacy_base_ep: true,
legacy_headers: true,
expected: false,
why: "switch set: legacy vars ignored",
..unset
},
];
for case in cases {
let cfg = EndpointsConfig {
external_otel_master_switch: case.switch,
otel_exporter_otlp_traces_endpoint: case
.legacy_traces_ep
.then(|| "https://legacy.example/v1/traces".to_string()),
otel_exporter_otlp_endpoint: case
.legacy_base_ep
.then(|| "https://legacy-base.example".to_string()),
otel_exporter_otlp_headers: case.legacy_headers.then(|| "k=v".to_string()),
grok_internal_otlp_traces_endpoint: case
.internal_ep
.then(|| "https://internal.example/traces".to_string()),
grok_internal_otlp_headers: case.internal_headers.then(|| "ik=iv".to_string()),
..internal_otlp_test_config()
};
assert_eq!(
cfg.internal_otlp_consumed_standard_vars(),
case.expected,
"case: {}",
case.why
);
}
}
/// Headers precedence: `grok_internal_otlp_headers` wins; legacy
/// `otel_exporter_otlp_headers` only when the master switch is unset.
#[test]
fn internal_otlp_headers_precedence() {
for switch in [false, true] {
let cfg = EndpointsConfig {
grok_internal_otlp_headers: Some("x-debug=1".to_string()),
otel_exporter_otlp_headers: Some("legacy=1".to_string()),
external_otel_master_switch: switch,
..internal_otlp_test_config()
};
assert_eq!(
cfg.resolve_otlp_headers(),
vec![("x-debug".to_string(), "1".to_string())],
"switch={switch}"
);
}
let legacy = EndpointsConfig {
otel_exporter_otlp_headers: Some("legacy=1".to_string()),
..internal_otlp_test_config()
};
assert_eq!(
legacy.resolve_otlp_headers(),
vec![("legacy".to_string(), "1".to_string())]
);
}
/// Regression: an org enable via `[telemetry].otel_enabled`
/// (managed config / requirements — no `KIGI_EXTERNAL_OTEL` env var) must
/// flip the master switch the *internal* pipeline keys off, so legacy
/// `OTEL_EXPORTER_OTLP_*` repointing shuts off in lockstep with the
/// external stream activating. A desync would point the internally-authed
/// firehose at the customer collector while
/// `internal_pipeline_consumed_otel_vars` blocks the external stream.
#[test]
fn external_otel_master_switch_resolves_from_all_layers() {
let enabled_table: toml::Value =
toml::from_str("[telemetry]\notel_enabled = true").unwrap();
let disabled_table: toml::Value =
toml::from_str("[telemetry]\notel_enabled = false").unwrap();
assert!(external_otel_master_switch_from(
None,
None,
Some(&enabled_table)
));
assert!(!external_otel_master_switch_from(None, None, None));
assert!(!external_otel_master_switch_from(
None,
Some(false),
Some(&enabled_table)
));
assert!(external_otel_master_switch_from(
None,
Some(true),
Some(&disabled_table)
));
assert!(!external_otel_master_switch_from(
Some(&disabled_table),
Some(true),
Some(&enabled_table)
));
assert!(external_otel_master_switch_from(
Some(&enabled_table),
Some(false),
None
));
let cfg = EndpointsConfig {
otel_exporter_otlp_traces_endpoint: Some(
"https://collector.corp:4318/v1/traces".into(),
),
external_otel_master_switch: true,
..internal_otlp_test_config()
};
assert!(!cfg.internal_otlp_consumed_standard_vars());
assert!(
!cfg.resolve_otlp_traces_endpoint()
.contains("collector.corp")
);
}
fn empty_config() -> toml::Value {
toml::Value::Table(toml::map::Map::new())
}
@@ -9820,26 +9271,6 @@ default = "grok-4.5"
}
}
#[test]
fn hub_config_default_has_no_url() {
assert!(HubConfig::default().url.is_none());
assert!(!HubConfig::default().is_enabled());
}
#[test]
fn hub_config_is_enabled_only_for_nonempty_url() {
assert!(
HubConfig {
url: Some("wss://hub.example/ws".into()),
}
.is_enabled()
);
assert!(
!HubConfig {
url: Some(" ".into()),
}
.is_enabled()
);
}
#[test]
fn resolve_model_list_prunes_bundled_entries_not_in_prefetch() {
let cfg = Config::default();
let mut defs = default_model_entries(&EndpointsConfig::default());
@@ -12,7 +12,6 @@ pub mod init;
pub mod models;
pub(crate) mod models_fetch;
pub mod mvp_agent;
pub(crate) mod proxy;
pub(crate) mod restore_code;
pub mod roster;
pub mod server;
@@ -1323,7 +1323,7 @@ const CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(300);
struct ModelsCache {
fetched_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
grok_version: Option<String>,
kigi_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
auth_method: Option<CacheAuthMethod>,
/// Models-list URL this catalog was fetched from
@@ -1418,7 +1418,7 @@ impl ModelsCacheManager {
) -> Option<ModelsCache> {
let data = std::fs::read(&self.path).ok()?;
let cache: ModelsCache = serde_json::from_slice(&data).ok()?;
if cache.grok_version.as_deref() != Some(kigi_version::VERSION) {
if cache.kigi_version.as_deref() != Some(kigi_version::VERSION) {
tracing::debug!("models cache version mismatch");
return None;
}
@@ -1447,7 +1447,7 @@ impl ModelsCacheManager {
) {
let cache = ModelsCache {
fetched_at: Utc::now(),
grok_version: Some(kigi_version::VERSION.to_string()),
kigi_version: Some(kigi_version::VERSION.to_string()),
auth_method: Some(auth_method),
origin: Some(origin.to_string()),
etag: etag.map(|s| s.to_string()),
@@ -3064,7 +3064,7 @@ mod tests {
let auth_method = mgr.inner.fetch_auth.read().cache_auth_method();
let stale = ModelsCache {
fetched_at: Utc::now() - ChronoDuration::seconds(3600),
grok_version: Some(kigi_version::VERSION.to_string()),
kigi_version: Some(kigi_version::VERSION.to_string()),
auth_method: Some(auth_method),
origin: Some(mgr.cache_origin()),
etag: Some("etag-stale".into()),
@@ -3140,7 +3140,7 @@ mod tests {
let auth_method = mgr.inner.fetch_auth.read().cache_auth_method();
let legacy = ModelsCache {
fetched_at: Utc::now(),
grok_version: Some(kigi_version::VERSION.to_string()),
kigi_version: Some(kigi_version::VERSION.to_string()),
auth_method: Some(auth_method),
origin: None,
etag: Some("etag-legacy".into()),
@@ -4100,7 +4100,7 @@ mod tests {
let cache = ModelsCacheManager::new();
let stale = ModelsCache {
fetched_at: Utc::now() - ChronoDuration::seconds(86_400),
grok_version: Some(kigi_version::VERSION.to_string()),
kigi_version: Some(kigi_version::VERSION.to_string()),
auth_method: Some(CacheAuthMethod::Platforms),
origin: Some(origin),
etag: None,
@@ -4187,7 +4187,7 @@ mod tests {
let cache = ModelsCacheManager::new();
cache.atomic_write(&ModelsCache {
fetched_at: Utc::now() - ChronoDuration::seconds(86_400),
grok_version: Some(kigi_version::VERSION.to_string()),
kigi_version: Some(kigi_version::VERSION.to_string()),
auth_method: Some(CacheAuthMethod::Platforms),
origin: Some(with_key_origin),
etag: None,
@@ -762,7 +762,7 @@ impl MvpAgent {
}
/// Build image generation config.
///
/// Both BYOK and session (OAuth) users go direct to `xai_api_base_url`.
/// Both BYOK and session (OAuth) users go direct to `api_base_url`.
/// `sampling_config.api_key` carries the OAuth bearer for session users (the
/// `api_key_provider` refreshes it per request), so IC authenticates and
/// meters Imagine usage per-user.
@@ -775,7 +775,7 @@ impl MvpAgent {
return ImageGenConfig::Disabled;
};
let cfg = self.cfg.borrow();
let base_url = cfg.endpoints.xai_api_base_url.clone();
let base_url = cfg.endpoints.api_base_url.clone();
let version = cfg
.client_version
.clone()
@@ -818,7 +818,7 @@ impl MvpAgent {
tracing::info!("video_gen disabled by tools.disable_zdr_incompatible_tools");
return VideoGenConfig::Disabled;
}
let base_url = cfg.endpoints.xai_api_base_url.clone();
let base_url = cfg.endpoints.api_base_url.clone();
let version = cfg
.client_version
.clone()
@@ -664,8 +664,6 @@ pub struct MvpAgent {
plugin_registry_initialized: std::cell::Cell<bool>,
persona_io_summaries: Vec<String>,
/// Local workspace ops, built lazily via [`Self::ensure_local_workspace_ops`].
/// The agent never opens Computer Hub as a harness/client; remote cloud
/// sandboxes are gateway-owned (`gateway_bridge` / `computer_sessions`).
workspace_ops: RefCell<Option<kigi_workspace::WorkspaceOps>>,
/// Sessions opened with `require_gateway` / chat light-frontend (K13).
/// Prompt-time guard consults this when the bridge map entry is missing,
@@ -652,7 +652,6 @@ async fn file_toolset_override_e2e_to_finalized_toolset() {
video_gen_config: kigi_tools::implementations::grok_build::video_gen::VideoGenConfig::default(),
app_builder_deployer_config: kigi_tools::implementations::grok_build::deploy_app::AppBuilderDeployerConfig::default(),
api_key_provider: None,
auth_provider: None,
attribution_callback: None,
system_reminder_tag: kigi_tools::reminders::DEFAULT_REMINDER_TAG,
};
@@ -3183,7 +3182,6 @@ fn interactive_trust_prompt_reprompts_after_untrust() {
}
mod direct_hub_cloud_removed {
use super::super::{DIRECT_HUB_CLOUD_REMOVED_MSG, reject_direct_hub_cloud_meta};
use crate::agent::config::HubConfig;
fn assert_direct_hub_error(err: agent_client_protocol::Error) {
assert_eq!(
err.data.as_ref(),
@@ -3238,38 +3236,6 @@ mod direct_hub_cloud_removed {
.is_ok()
);
}
#[test]
fn hub_url_gating_matrix() {
let with_url = HubConfig {
url: Some("wss://hub.example/ws".into()),
};
let without_url = HubConfig { url: None };
let blank = HubConfig {
url: Some(" ".into()),
};
assert!(with_url.is_enabled());
assert!(!without_url.is_enabled());
assert!(!blank.is_enabled());
}
#[test]
fn hub_config_is_url_only_workspace_default() {
let json = serde_json::to_value(HubConfig {
url: Some("wss://hub.example/ws".into()),
})
.expect("serialize");
let obj = json.as_object().expect("object");
assert_eq!(
obj.keys().collect::<Vec<_>>(),
vec!["url"],
"HubConfig must only serialize url (no proxy-mode fields)"
);
let from_legacy: HubConfig = serde_json::from_value(serde_json::json!(
{ "url" : "wss://hub.example/ws", "workspace_mode" : "remote",
"send_turn_hooks" : false, }
))
.expect("ignore unknown fields");
assert_eq!(from_legacy.url.as_deref(), Some("wss://hub.example/ws"));
}
}
mod soft_default_settings_emit {
use super::*;
@@ -39,6 +39,7 @@ async fn subagent_spawn_context_inherits_parent_permission_handle() {
Vec::new(),
false,
None,
true,
);
let mut handle = make_test_handle("test-model", false, None);
@@ -1,619 +0,0 @@
//! HTTP CONNECT proxy support for WebSocket connections.
//!
//! When running behind a corporate egress proxy,
//! `tokio-tungstenite`'s `connect_async` cannot reach external
//! hosts directly because it does not read the standard `HTTPS_PROXY` /
//! `HTTP_PROXY` environment variables.
//!
//! This module provides:
//! - [`resolve_proxy_for_host`]: reads proxy env vars and `NO_PROXY`, returning
//! the proxy URL to use for a given target host (or `None` for direct).
//! - [`connect_via_proxy`]: opens a TCP connection to the proxy, sends an HTTP
//! CONNECT request to create a tunnel, wraps the result in TLS, and returns a
//! stream suitable for `tokio_tungstenite::client_async`.
use std::sync::{Arc, OnceLock};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio_tungstenite::MaybeTlsStream;
use tracing::debug;
// ---------------------------------------------------------------------------
// Environment-variable resolution
// ---------------------------------------------------------------------------
/// Read proxy configuration from the environment and decide whether `target_host`
/// should be connected through a proxy.
///
/// Resolution order (matches `curl` / `reqwest` behaviour):
/// 1. If `NO_PROXY` contains `target_host` (or a matching domain suffix / CIDR),
/// return `None`.
/// 2. If `HTTPS_PROXY` (or `https_proxy`) is set, return its value.
/// 3. If `HTTP_PROXY` (or `http_proxy`) is set, return its value.
/// 4. Otherwise return `None`.
pub fn resolve_proxy_for_host(target_host: &str) -> Option<String> {
resolve_proxy_for_host_with(target_host, |key| std::env::var(key))
}
/// Testable inner implementation that accepts a custom env-var reader.
fn resolve_proxy_for_host_with<F>(target_host: &str, env: F) -> Option<String>
where
F: for<'a> Fn(&'a str) -> Result<String, std::env::VarError>,
{
// Check NO_PROXY / no_proxy.
let no_proxy = env("NO_PROXY")
.or_else(|_| env("no_proxy"))
.unwrap_or_default();
if is_host_bypassed(target_host, &no_proxy) {
return None;
}
// HTTPS_PROXY takes precedence (our target is always wss://).
if let Ok(url) = env("HTTPS_PROXY").or_else(|_| env("https_proxy")) {
let url = url.trim().to_string();
if !url.is_empty() {
return Some(url);
}
}
// Fall back to HTTP_PROXY.
if let Ok(url) = env("HTTP_PROXY").or_else(|_| env("http_proxy")) {
let url = url.trim().to_string();
if !url.is_empty() {
return Some(url);
}
}
None
}
/// Check whether `host` is in the `no_proxy` list.
///
/// The `no_proxy` value is a comma-separated list of hostnames, domain
/// suffixes (with or without a leading dot), IP addresses, or CIDR ranges.
/// The special value `*` matches everything.
fn is_host_bypassed(host: &str, no_proxy: &str) -> bool {
let host_lower = host.to_ascii_lowercase();
for entry in no_proxy.split(',') {
let entry = entry.trim().to_ascii_lowercase();
if entry.is_empty() {
continue;
}
// Wildcard — bypass all hosts.
if entry == "*" {
return true;
}
// Exact match.
if host_lower == entry {
return true;
}
// Domain suffix match: ".example.com" matches "foo.example.com".
// Also handle the common convention of omitting the leading dot:
// "example.com" in NO_PROXY should match "sub.example.com".
let matches_suffix = if entry.starts_with('.') {
host_lower.ends_with(entry.as_str())
} else {
host_lower.len() > entry.len()
&& host_lower.ends_with(entry.as_str())
&& host_lower.as_bytes()[host_lower.len() - entry.len() - 1] == b'.'
};
if matches_suffix {
return true;
}
// CIDR / IP matching is intentionally omitted here — our target host
// is always a DNS name, not an IP literal. Keeping this simple avoids
// pulling in a CIDR parsing dependency.
}
false
}
// ---------------------------------------------------------------------------
// HTTP CONNECT tunnel
// ---------------------------------------------------------------------------
/// Establish a TLS-wrapped TCP stream through an HTTP CONNECT proxy.
///
/// Steps:
/// 1. Parse the proxy URL to get host + port.
/// 2. Open a TCP connection to the proxy and perform the CONNECT handshake.
/// 3. Wrap the tunnel in TLS (using rustls with native root certificates).
/// 4. Return the stream as `MaybeTlsStream<TcpStream>` so it is compatible
/// with `tokio_tungstenite::client_async`.
pub async fn connect_via_proxy(
proxy_url: &str,
target_host: &str,
target_port: u16,
) -> anyhow::Result<MaybeTlsStream<TcpStream>> {
let stream = open_connect_tunnel(proxy_url, target_host, target_port).await?;
let tls_stream = tls_wrap(stream, target_host).await?;
Ok(MaybeTlsStream::Rustls(tls_stream))
}
/// Open a raw TCP tunnel through an HTTP CONNECT proxy (no TLS).
///
/// 1. Parse the proxy URL to get host + port.
/// 2. Open a plain TCP connection to the proxy.
/// 3. Send `CONNECT target_host:target_port HTTP/1.1\r\n\r\n`.
/// 4. Read the proxy's response; expect `HTTP/1.x 200 …`.
/// 5. Return the raw `TcpStream` positioned after the CONNECT response.
async fn open_connect_tunnel(
proxy_url: &str,
target_host: &str,
target_port: u16,
) -> anyhow::Result<TcpStream> {
// 1. Parse proxy URL.
let (proxy_host, proxy_port) = parse_proxy_url(proxy_url)?;
// 2. TCP connect to proxy.
let proxy_addr = format!("{proxy_host}:{proxy_port}");
debug!(proxy_addr = %proxy_addr, "Opening TCP to proxy");
let stream = TcpStream::connect(&proxy_addr)
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to proxy at {proxy_addr}: {e}"))?;
// 3. Send HTTP CONNECT.
let connect_req = format!(
"CONNECT {target_host}:{target_port} HTTP/1.1\r\n\
Host: {target_host}:{target_port}\r\n\
\r\n"
);
let (reader_half, mut writer_half) = stream.into_split();
writer_half.write_all(connect_req.as_bytes()).await?;
writer_half.flush().await?;
// 4. Read the status line from the proxy.
let mut reader = BufReader::new(reader_half);
let mut status_line = String::new();
reader.read_line(&mut status_line).await?;
debug!(status_line = %status_line.trim(), "Proxy CONNECT response");
if !status_line.starts_with("HTTP/1.1 200") && !status_line.starts_with("HTTP/1.0 200") {
anyhow::bail!("Proxy CONNECT failed: {}", status_line.trim());
}
// Consume remaining response headers (until empty line).
loop {
let mut line = String::new();
reader.read_line(&mut line).await?;
if line.trim().is_empty() {
break;
}
}
// 5. Assert the BufReader's internal buffer is empty before reuniting.
// BufReader::read_line may have read ahead into its buffer. If extra
// bytes were consumed beyond the HTTP headers (e.g., from a proxy that
// eagerly forwards data or coalesced TCP segments), dropping them would
// corrupt the subsequent TLS handshake.
let remaining = reader.buffer();
if !remaining.is_empty() {
anyhow::bail!(
"Proxy sent {} unexpected byte(s) after CONNECT response headers",
remaining.len()
);
}
// 6. Reunite the split halves back into a TcpStream.
let stream = reader.into_inner().reunite(writer_half)?;
Ok(stream)
}
/// Lazily-initialized TLS client configuration.
///
/// Loading native root certificates involves syscalls (reading `/etc/ssl/certs/`
/// or the macOS Keychain) and the cert store never changes at runtime. We build
/// the `ClientConfig` once and reuse it across all proxy connections / reconnects.
///
/// Stores `Ok(config)` on success or `Err(message)` if cert loading fails.
static TLS_CONFIG: OnceLock<Result<Arc<rustls::ClientConfig>, String>> = OnceLock::new();
/// Build (or return the cached) TLS client configuration.
fn get_tls_config() -> anyhow::Result<Arc<rustls::ClientConfig>> {
let result = TLS_CONFIG.get_or_init(|| {
let mut root_store = rustls::RootCertStore::empty();
let cert_result = rustls_native_certs::load_native_certs();
if cert_result.certs.is_empty() {
let errors: Vec<_> = cert_result.errors.iter().map(|e| e.to_string()).collect();
return Err(format!(
"No native root certificates found. Errors: {}",
if errors.is_empty() {
"(none)".to_string()
} else {
errors.join("; ")
}
));
}
for cert in cert_result.certs {
if let Err(e) = root_store.add(cert) {
tracing::warn!(error = %e, "Skipping unparseable native root certificate");
}
}
let config = rustls::ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
Ok(Arc::new(config))
});
match result {
Ok(config) => Ok(config.clone()),
Err(msg) => anyhow::bail!("{msg}"),
}
}
/// Perform a TLS handshake over an existing TCP stream using rustls with
/// native root certificates (cached via [`TLS_CONFIG`]).
async fn tls_wrap(
stream: TcpStream,
server_name: &str,
) -> anyhow::Result<tokio_rustls::client::TlsStream<TcpStream>> {
let tls_config = get_tls_config()?;
let connector = tokio_rustls::TlsConnector::from(tls_config);
let dns_name = rustls::pki_types::ServerName::try_from(server_name.to_string())
.map_err(|e| anyhow::anyhow!("Invalid TLS server name '{server_name}': {e}"))?;
let tls_stream = connector
.connect(dns_name, stream)
.await
.map_err(|e| anyhow::anyhow!("TLS handshake through proxy failed: {e}"))?;
Ok(tls_stream)
}
/// Parse a proxy URL into (host, port).
///
/// Accepted formats:
/// - `http://host:port`
/// - `http://host` (defaults to port 80)
/// - `host:port`
fn parse_proxy_url(url: &str) -> anyhow::Result<(String, u16)> {
// Strip scheme if present.
let without_scheme = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url);
// Strip trailing path/slash.
let authority = without_scheme.split('/').next().unwrap_or(without_scheme);
if let Some((host, port_str)) = authority.rsplit_once(':') {
let port: u16 = port_str
.parse()
.map_err(|_| anyhow::anyhow!("Invalid proxy port in '{url}'"))?;
Ok((host.to_string(), port))
} else {
// No port — default to 80 for HTTP proxies.
Ok((authority.to_string(), 80))
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
// ===== parse_proxy_url =====
#[test]
fn test_parse_proxy_url_with_scheme_and_port() {
let (host, port) = parse_proxy_url("http://proxy.example.com:3140").unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 3140);
}
#[test]
fn test_parse_proxy_url_without_scheme() {
let (host, port) = parse_proxy_url("proxy.example.com:8080").unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 8080);
}
#[test]
fn test_parse_proxy_url_without_port() {
let (host, port) = parse_proxy_url("http://proxy.example.com").unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 80);
}
#[test]
fn test_parse_proxy_url_with_trailing_slash() {
let (host, port) = parse_proxy_url("http://proxy.example.com:3140/").unwrap();
assert_eq!(host, "proxy.example.com");
assert_eq!(port, 3140);
}
#[test]
fn test_parse_proxy_url_https_scheme() {
let (host, port) = parse_proxy_url("https://secure-proxy:443").unwrap();
assert_eq!(host, "secure-proxy");
assert_eq!(port, 443);
}
#[test]
fn test_parse_proxy_url_multi_label_host() {
let (host, port) =
parse_proxy_url("http://http-proxy.services.internal.example:3128").unwrap();
assert_eq!(host, "http-proxy.services.internal.example");
assert_eq!(port, 3128);
}
#[test]
fn test_parse_proxy_url_invalid_port() {
assert!(parse_proxy_url("http://proxy:notaport").is_err());
}
// ===== is_host_bypassed =====
#[test]
fn test_bypass_exact_match() {
assert!(is_host_bypassed("localhost", "localhost,127.0.0.1"));
}
#[test]
fn test_bypass_domain_suffix_with_dot() {
assert!(is_host_bypassed(
"api.corp.example",
"localhost,.corp.example"
));
}
#[test]
fn test_bypass_domain_suffix_without_dot() {
// Common convention: "example.com" in NO_PROXY matches "api.example.com".
assert!(is_host_bypassed("api.example.com", "localhost,example.com"));
}
#[test]
fn test_bypass_wildcard() {
assert!(is_host_bypassed("anything.example.com", "*"));
}
#[test]
fn test_no_bypass_when_not_listed() {
assert!(!is_host_bypassed(
"api.external.example",
"localhost,127.0.0.1,.corp.example,.internal.example"
));
}
#[test]
fn test_bypass_case_insensitive() {
assert!(is_host_bypassed("API.Corp.EXAMPLE", ".corp.example"));
}
#[test]
fn test_bypass_empty_no_proxy() {
assert!(!is_host_bypassed("api.external.example", ""));
}
#[test]
fn test_bypass_spaces_in_entries() {
assert!(is_host_bypassed(
"foo.example.com",
" localhost , .example.com , .other.com "
));
}
#[test]
fn test_bypass_cidr_not_matched_for_dns_names() {
// CIDR entries like 10.0.0.0/8 should not match DNS names.
assert!(!is_host_bypassed("api.external.example", "10.0.0.0/8"));
}
#[test]
fn test_bypass_combined_no_proxy_list() {
// A typical corporate NO_PROXY mixes loopback, private CIDRs, and domain suffixes.
let no_proxy = "localhost,127.0.0.1,10.0.0.0/8,.internal.example,.corp.example";
assert!(!is_host_bypassed("api.external.example", no_proxy));
assert!(is_host_bypassed("db.internal.example", no_proxy));
assert!(is_host_bypassed("git.corp.example", no_proxy));
assert!(is_host_bypassed("localhost", no_proxy));
}
// ===== resolve_proxy_for_host_with =====
#[test]
fn test_resolve_no_proxy_vars_set() {
let result = resolve_proxy_for_host_with("api.external.example", |_| {
Err(std::env::VarError::NotPresent)
});
assert_eq!(result, None);
}
#[test]
fn test_resolve_https_proxy_used() {
let result = resolve_proxy_for_host_with("api.external.example", |key| match key {
"HTTPS_PROXY" => Ok("http://proxy.example.com:3128".to_string()),
"NO_PROXY" => Err(std::env::VarError::NotPresent),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(result, Some("http://proxy.example.com:3128".to_string()));
}
#[test]
fn test_resolve_http_proxy_fallback() {
let result = resolve_proxy_for_host_with("api.external.example", |key| match key {
"HTTP_PROXY" => Ok("http://proxy.example.com:8080".to_string()),
"NO_PROXY" => Err(std::env::VarError::NotPresent),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(result, Some("http://proxy.example.com:8080".to_string()));
}
#[test]
fn test_resolve_no_proxy_bypasses() {
let result = resolve_proxy_for_host_with("api.corp.example", |key| match key {
"HTTPS_PROXY" => Ok("http://proxy.example.com:3128".to_string()),
"NO_PROXY" => Ok("localhost,.corp.example".to_string()),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(result, None);
}
#[test]
fn test_resolve_https_proxy_takes_precedence() {
let result = resolve_proxy_for_host_with("api.external.example", |key| match key {
"HTTPS_PROXY" => Ok("http://https-proxy.example.com:443".to_string()),
"HTTP_PROXY" => Ok("http://http-proxy.example.com:80".to_string()),
"NO_PROXY" => Err(std::env::VarError::NotPresent),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(
result,
Some("http://https-proxy.example.com:443".to_string())
);
}
#[test]
fn test_resolve_lowercase_env_vars() {
let result = resolve_proxy_for_host_with("api.external.example", |key| match key {
"https_proxy" => Ok("http://proxy.example.com:3128".to_string()),
"no_proxy" => Err(std::env::VarError::NotPresent),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(result, Some("http://proxy.example.com:3128".to_string()));
}
#[test]
fn test_resolve_empty_proxy_ignored() {
let result = resolve_proxy_for_host_with("api.external.example", |key| match key {
"HTTPS_PROXY" => Ok(" ".to_string()),
"HTTP_PROXY" => Ok("http://proxy.example.com:8080".to_string()),
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(result, Some("http://proxy.example.com:8080".to_string()));
}
#[test]
fn test_resolve_respects_no_proxy_when_proxy_set() {
let result = resolve_proxy_for_host_with("api.external.example", |key| match key {
"HTTPS_PROXY" | "HTTP_PROXY" => Ok("http://proxy.example.com:3128".to_string()),
"NO_PROXY" => {
Ok("localhost,127.0.0.1,10.0.0.0/8,.internal.example,.corp.example".to_string())
}
_ => Err(std::env::VarError::NotPresent),
});
assert_eq!(result, Some("http://proxy.example.com:3128".to_string()));
}
// ===== HTTP CONNECT tunnel (integration-style) =====
/// Helper: spawn a mock HTTP CONNECT proxy that accepts one connection.
///
/// On receiving a CONNECT request, it validates the request format,
/// replies with `status_line`, and then echoes data (simulating a tunnel).
/// Returns the proxy's listen address.
async fn spawn_mock_proxy(status_line: &'static str) -> std::net::SocketAddr {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut stream, _) = listener.accept().await.unwrap();
// Read CONNECT request (read until \r\n\r\n).
let mut buf = vec![0u8; 4096];
let mut total = 0;
loop {
let n = stream.read(&mut buf[total..]).await.unwrap();
if n == 0 {
return;
}
total += n;
let so_far = std::str::from_utf8(&buf[..total]).unwrap_or("");
if so_far.contains("\r\n\r\n") {
break;
}
}
let request = std::str::from_utf8(&buf[..total]).unwrap().to_string();
assert!(
request.contains("CONNECT ") && request.contains(" HTTP/1.1"),
"Expected CONNECT request, got: {request}"
);
// Reply with the provided status line.
stream.write_all(status_line.as_bytes()).await.unwrap();
// Echo loop (simulates the transparent tunnel).
let mut echo_buf = [0u8; 1024];
loop {
let n = match stream.read(&mut echo_buf).await {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
if stream.write_all(&echo_buf[..n]).await.is_err() {
break;
}
}
});
addr
}
/// Tests that `open_connect_tunnel` sends a correct CONNECT request,
/// parses the proxy's 200 response, and returns a usable tunnel stream.
#[tokio::test]
async fn test_open_connect_tunnel_success() {
let addr =
spawn_mock_proxy("HTTP/1.1 200 Connection Established\r\nServer: mock\r\n\r\n").await;
let proxy_url = format!("http://{addr}");
// Call the real function under test.
let mut stream = open_connect_tunnel(&proxy_url, "example.com", 443)
.await
.expect("tunnel should succeed");
// Verify the tunnel works by echoing data through it.
stream.write_all(b"hello tunnel").await.unwrap();
stream.flush().await.unwrap();
let mut response = vec![0u8; 12];
stream.read_exact(&mut response).await.unwrap();
assert_eq!(&response, b"hello tunnel");
}
/// Tests that `open_connect_tunnel` with a non-default port sends the
/// correct CONNECT target.
#[tokio::test]
async fn test_open_connect_tunnel_custom_port() {
let addr = spawn_mock_proxy("HTTP/1.1 200 OK\r\n\r\n").await;
let proxy_url = format!("http://{addr}");
let stream = open_connect_tunnel(&proxy_url, "internal.example.com", 8443).await;
assert!(stream.is_ok(), "tunnel should succeed for custom port");
}
/// Tests that `open_connect_tunnel` returns an error when the proxy
/// rejects the CONNECT request with a non-200 status.
#[tokio::test]
async fn test_open_connect_tunnel_proxy_rejects() {
let addr = spawn_mock_proxy("HTTP/1.1 403 Forbidden\r\n\r\n").await;
let proxy_url = format!("http://{addr}");
let result = open_connect_tunnel(&proxy_url, "blocked.example.com", 443).await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("403"),
"Error should mention 403: {err_msg}"
);
}
/// Tests that `open_connect_tunnel` returns an error when connecting
/// to a proxy that isn't listening.
#[tokio::test]
async fn test_open_connect_tunnel_proxy_unreachable() {
let result = open_connect_tunnel("http://127.0.0.1:1", "example.com", 443).await;
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Failed to connect to proxy"),
"Error should mention proxy connection failure: {err_msg}"
);
}
}
+4 -4
View File
@@ -1059,11 +1059,11 @@ fn apply_requirements_inner(
enforce_str!("models", "default", config.models.default);
enforce_str!("cli", "channel", config.cli.channel);
enforce_str!("cli", "minimum_version", config.cli.minimum_version);
if let Some(val) = req_str(req, "endpoints", "xai_api_base_url")
&& config.endpoints.xai_api_base_url != val
if let Some(val) = req_str(req, "endpoints", "api_base_url")
&& config.endpoints.api_base_url != val
{
config.endpoints.xai_api_base_url = val.to_owned();
push("endpoints.xai_api_base_url", val.to_owned());
config.endpoints.api_base_url = val.to_owned();
push("endpoints.api_base_url", val.to_owned());
}
if let Some(val) = req_str(req, "endpoints", "coding_api_base_url")
&& config.endpoints.coding_api_base_url.as_deref() != Some(val)
@@ -2490,7 +2490,7 @@ fn enterprise_two_file_merge_routes_deployment_key_to_proxy() {
let managed = toml::from_str(
r#"
[endpoints]
xai_api_base_url = "https://inference.acme-corp.example/xai/v1"
api_base_url = "https://inference.acme-corp.example/xai/v1"
coding_api_base_url = "https://cli-chat-proxy.kigi.com/v1"
[model.kigi-build]
@@ -2511,7 +2511,7 @@ telemetry = false
[endpoints]
deployment_key = "xai-token-ENTERPRISE"
xai_api_base_url = "https://inference.acme-corp.example/xai/v1"
api_base_url = "https://inference.acme-corp.example/xai/v1"
"#,
)
.unwrap();
@@ -2586,13 +2586,13 @@ fn config_layers_system_managed_lowest_priority() {
#[test]
fn apply_requirements_value_overrides_user_settings() {
let raw_config: toml::Value = toml::from_str(
"[cli]\nauto_update = true\nchannel = \"beta\"\n\n[features]\nfeedback = true\nlsp_tools = true\nweb_fetch = true\nwrite_file = true\n\n[ui]\nyolo = true\n\n[models]\ndefault = \"user-model\"\n\n[endpoints]\ncoding_api_base_url = \"https://user-proxy.example/v1\"\nxai_api_base_url = \"https://user-api.example/v1\"\nmodels_base_url = \"https://user-models.example/v1\"\nmodels_list_url = \"https://user-models.example/v1/models\"\n",
"[cli]\nauto_update = true\nchannel = \"beta\"\n\n[features]\nfeedback = true\nlsp_tools = true\nweb_fetch = true\nwrite_file = true\n\n[ui]\nyolo = true\n\n[models]\ndefault = \"user-model\"\n\n[endpoints]\ncoding_api_base_url = \"https://user-proxy.example/v1\"\napi_base_url = \"https://user-api.example/v1\"\nmodels_base_url = \"https://user-models.example/v1\"\nmodels_list_url = \"https://user-models.example/v1/models\"\n",
)
.unwrap();
let mut cfg = crate::agent::config::Config::new_from_toml_cfg(&raw_config).unwrap();
cfg.default_yolo_mode = true;
let requirements: toml::Value = toml::from_str(
"[cli]\nauto_update = false\nchannel = \"stable\"\n\n[features]\nfeedback = false\nlsp_tools = false\nweb_fetch = false\nwrite_file = false\nremote_fetch = false\n\n[ui]\nyolo = false\n\n[models]\ndefault = \"managed-model\"\n\n[endpoints]\ncoding_api_base_url = \"https://managed-proxy.example/v1\"\nxai_api_base_url = \"https://managed-api.example/v1\"\nmodels_base_url = \"https://managed-models.example/v1\"\nmodels_list_url = \"https://managed-models.example/v1/models\"\ndeployment_key = \"enterprise-deploy-key-should-not-log\"\n",
"[cli]\nauto_update = false\nchannel = \"stable\"\n\n[features]\nfeedback = false\nlsp_tools = false\nweb_fetch = false\nwrite_file = false\nremote_fetch = false\n\n[ui]\nyolo = false\n\n[models]\ndefault = \"managed-model\"\n\n[endpoints]\ncoding_api_base_url = \"https://managed-proxy.example/v1\"\napi_base_url = \"https://managed-api.example/v1\"\nmodels_base_url = \"https://managed-models.example/v1\"\nmodels_list_url = \"https://managed-models.example/v1/models\"\ndeployment_key = \"enterprise-deploy-key-should-not-log\"\n",
)
.unwrap();
let source = RequirementSource::Requirements {
@@ -2617,7 +2617,7 @@ fn apply_requirements_value_overrides_user_settings() {
Some("https://managed-proxy.example/v1"), cfg.endpoints.coding_api_base_url
.as_deref()
);
assert_eq!("https://managed-api.example/v1", cfg.endpoints.xai_api_base_url);
assert_eq!("https://managed-api.example/v1", cfg.endpoints.api_base_url);
assert_eq!(
Some("https://managed-models.example/v1"), cfg.endpoints.models_base_url
.as_deref()
+3 -3
View File
@@ -53,7 +53,7 @@ impl std::fmt::Display for Scope {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InspectReport {
pub grok_version: String,
pub kigi_version: String,
pub channel: String,
pub cwd: String,
pub project_root: Option<String>,
@@ -370,7 +370,7 @@ async fn build_report(cwd: &Path) -> InspectReport {
.unwrap_or_default();
InspectReport {
grok_version: kigi_version::VERSION.to_string(),
kigi_version: kigi_version::VERSION.to_string(),
channel: crate::util::config::channel_name_from_cache()
.unwrap_or("unknown")
.to_string(),
@@ -1224,7 +1224,7 @@ fn render_harness_compatibility(report: &ExternalCompatReport) -> String {
fn print_human(r: &InspectReport) {
println!();
println!(" Environment");
println!(" {TREE} Version: {} [{}]", r.grok_version, r.channel);
println!(" {TREE} Version: {} [{}]", r.kigi_version, r.channel);
println!(" {TREE} CWD: {}", r.cwd);
if let Some(ref root) = r.project_root {
println!(" {TREE} Git root: {}", root);
+1 -1
View File
@@ -1113,7 +1113,7 @@ async fn evict_leader(conn: LeaderConnection, lock: &LeaderLock) {
/// # Arguments
///
/// * `client_type` - Identifier for the client type (e.g., "grok-tui", "vscode")
/// * `mode` - Communication mode (Stdio or Headless)
/// * `mode` - Communication mode (Stdio)
/// * `capabilities` - Client capabilities (e.g., yolo_mode) to register with the leader
pub async fn connect_or_spawn(
client_type: &str,
@@ -108,9 +108,6 @@ impl Default for ClientId {
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientMode {
/// Headless mode (grok agent, grok agent headless) - uses websocket relay.
/// Leader connects to websocket relay once and forwards messages.
Headless,
/// Stdio mode (grok agent stdio, grok -p) - uses local IPC.
/// Client sends/receives ACP messages directly via IPC.
Stdio,
@@ -178,8 +175,6 @@ pub struct LeaderCapabilities {
pub runtime_cpu_profile: bool,
#[serde(default)]
pub profile_formats: Vec<ProfileArtifactFormat>,
#[serde(default)]
pub workspace_exposure: bool,
/// Whether the leader supports [`ControlCommand::RelaunchForUpdate`] — a
/// disruptive, bounded-grace relaunch onto a freshly-installed binary
/// (driven by `grok update`). Old leaders default to `false`, so a new
@@ -200,15 +195,6 @@ pub enum ControlCommand {
frequency_hz: Option<i32>,
},
StopCpuProfile,
WorkspaceStart {
#[serde(default)]
hub_url: Option<String>,
cwd: String,
},
WorkspacePause,
WorkspaceResume,
WorkspaceStop,
WorkspaceStatus,
/// Ask the leader to relaunch onto a freshly-installed binary (driven by
/// `grok update`). The leader stops admitting new turns, waits a bounded
/// grace period for in-flight turns to finish, flushes session state, then
@@ -260,18 +246,6 @@ pub enum ControlPayload {
started_at: String,
stopped_at: String,
},
WorkspaceStatus {
state: String,
#[serde(default)]
hub_url: Option<String>,
#[serde(default)]
cwd: Option<String>,
uptime_ms: u64,
active_tool_calls: u32,
#[serde(default)]
sessions: Vec<String>,
pid: u32,
},
/// Ack for [`ControlCommand::RelaunchForUpdate`]: the leader accepted the
/// request and will exit after a bounded grace period of `grace_ms`.
Relaunching {
@@ -531,7 +505,6 @@ mod tests {
control_v1: true,
runtime_cpu_profile: true,
profile_formats: vec![ProfileArtifactFormat::Svg],
workspace_exposure: true,
relaunch_v1: true,
}),
};
@@ -549,7 +522,6 @@ mod tests {
control_v1: true,
runtime_cpu_profile: true,
profile_formats,
workspace_exposure: true,
relaunch_v1: true,
}),
} if profile_formats == vec![ProfileArtifactFormat::Svg]
@@ -622,71 +594,6 @@ mod tests {
));
}
#[tokio::test]
async fn workspace_control_command_roundtrip() {
let (mut client, mut server) = duplex(1024);
let msg = ClientMessage::Control {
request_id: "ws-1".into(),
command: ControlCommand::WorkspaceStart {
hub_url: Some("wss://hub.example/v1/tools".into()),
cwd: "/home/u/proj".into(),
},
};
write_message(&mut client, &msg).await.unwrap();
let received: ClientMessage = read_message(&mut server).await.unwrap();
assert!(matches!(
received,
ClientMessage::Control {
request_id,
command: ControlCommand::WorkspaceStart { hub_url: Some(url), cwd },
} if request_id == "ws-1"
&& url == "wss://hub.example/v1/tools"
&& cwd == "/home/u/proj"
));
}
#[test]
fn workspace_status_payload_roundtrip() {
let payload = ControlPayload::WorkspaceStatus {
state: "running".into(),
hub_url: Some("wss://hub.example/v1/tools".into()),
cwd: Some("/home/u/proj".into()),
uptime_ms: 4200,
active_tool_calls: 2,
sessions: vec!["grok-a".into(), "grok-b".into()],
pid: 4242,
};
let json = serde_json::to_string(&payload).unwrap();
let decoded: ControlPayload = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, payload);
assert!(json.contains("\"type\":\"workspace_status\""));
}
#[test]
fn workspace_status_payload_defaults_optional_fields() {
let json = r#"{"type":"workspace_status","state":"none","uptime_ms":0,"active_tool_calls":0,"pid":1}"#;
let decoded: ControlPayload = serde_json::from_str(json).unwrap();
assert!(matches!(
decoded,
ControlPayload::WorkspaceStatus {
state,
hub_url: None,
cwd: None,
sessions,
..
} if state == "none" && sessions.is_empty()
));
}
#[test]
fn workspace_exposure_capability_defaults_false() {
let json = r#"{"control_v1":true,"runtime_cpu_profile":false,"profile_formats":[]}"#;
let caps: LeaderCapabilities = serde_json::from_str(json).unwrap();
assert!(!caps.workspace_exposure);
}
#[test]
fn client_id_is_unique() {
let ids: Vec<_> = (0..100).map(|_| ClientId::new()).collect();
@@ -27,8 +27,6 @@ use crate::cpu_profile::{
};
use agent_client_protocol::AGENT_METHOD_NAMES;
use kanal::{AsyncReceiver, AsyncSender};
use kigi_computer_hub_sdk::{AuthCredential, AuthIdentity, AuthProvider};
use kigi_workspace::WorkspaceHandle;
use parking_lot::Mutex;
use tokio::sync::{mpsc, watch};
use tokio_util::sync::CancellationToken;
@@ -129,105 +127,24 @@ pub struct LeaderServerMetadata {
pub struct LeaderServerControlState {
pub metadata: LeaderServerMetadata,
pub cpu_profile: Arc<Mutex<CpuProfileManager>>,
pub workspace: Arc<WorkspaceControl>,
}
impl LeaderServerControlState {
pub fn new(metadata: LeaderServerMetadata) -> Self {
Self {
metadata,
cpu_profile: Arc::new(Mutex::new(CpuProfileManager::new())),
workspace: Arc::new(WorkspaceControl::new(None)),
}
}
pub fn with_default_hub_url(mut self, default_hub_url: Option<String>) -> Self {
self.workspace = Arc::new(WorkspaceControl::new(default_hub_url));
self
}
fn leader_capabilities(&self) -> LeaderCapabilities {
let manager = self.cpu_profile.lock();
LeaderCapabilities {
control_v1: true,
runtime_cpu_profile: manager.runtime_cpu_profile(),
profile_formats: manager.profile_formats().to_vec(),
workspace_exposure: true,
relaunch_v1: true,
}
}
}
pub struct WorkspaceControl {
default_hub_url: Option<String>,
/// Hub credential, wired to the leader's `AuthManager` once auth is ready.
/// A `watch` so a starting leader (socket up, auth pending) can be awaited
/// instead of failing the command.
auth: tokio::sync::watch::Sender<Option<Arc<dyn AuthProvider>>>,
/// Serializes mutating commands (start/pause/resume/stop) so their long
/// awaits (drain, reconnect) never interleave.
lock: tokio::sync::Mutex<()>,
/// Current exposure, published for lock-free reads so `status` never
/// blocks behind an in-flight drain/reconnect.
exposure: arc_swap::ArcSwapOption<WorkspaceExposure>,
}
impl WorkspaceControl {
fn new(default_hub_url: Option<String>) -> Self {
Self {
default_hub_url,
auth: tokio::sync::watch::channel(None).0,
lock: tokio::sync::Mutex::new(()),
exposure: arc_swap::ArcSwapOption::empty(),
}
}
/// Wire the hub credential to the leader's shared `AuthManager` (sole
/// owner of refresh + persistence).
pub fn set_auth_manager(&self, auth_manager: Arc<AuthManager>) {
self.auth
.send_replace(Some(Arc::new(LeaderAuthProvider { auth_manager })));
}
}
impl std::fmt::Debug for WorkspaceControl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WorkspaceControl")
.field("default_hub_url", &self.default_hub_url)
.finish_non_exhaustive()
}
}
/// Hub [`AuthProvider`] backed by the leader's `AuthManager`: returns the
/// current token at each connect/reconnect; never writes auth.json.
struct LeaderAuthProvider {
auth_manager: Arc<AuthManager>,
}
impl std::fmt::Debug for LeaderAuthProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LeaderAuthProvider").finish_non_exhaustive()
}
}
impl AuthProvider for LeaderAuthProvider {
fn current(&self) -> AuthCredential {
let token = self
.auth_manager
.current_or_expired()
.map(|a| a.key)
.unwrap_or_default();
AuthCredential::bearer(token)
}
/// Owner identity from the leader's `AuthManager`, surfaced on the auth
/// provider instead of a separate auth.json read. The Kimi credential
/// carries no principal metadata; only the (possibly empty) user id.
fn identity(&self) -> Option<AuthIdentity> {
let a = self.auth_manager.current_or_expired()?;
Some(AuthIdentity {
user_id: a.user_id,
principal_type: None,
principal_id: None,
})
}
}
struct WorkspaceExposure {
handle: WorkspaceHandle,
hub_url: String,
cwd: PathBuf,
started_at: Instant,
paused: std::sync::atomic::AtomicBool,
}
/// Rewrite JSON-RPC request ID **in place** by prefixing with client ID to
/// avoid collisions.
///
@@ -934,233 +851,6 @@ fn leader_info_payload(control_state: &LeaderServerControlState) -> ControlPaylo
profile_formats: manager.profile_formats().to_vec(),
}
}
const PROD_COMPUTER_HUB_URL: &str = "wss://computer-hub.kigi.com/v1/tools";
const WORKSPACE_DRAIN_TIMEOUT: Duration = Duration::from_secs(10);
fn workspace_err(message: impl Into<String>) -> ControlError {
ControlError {
code: ControlErrorCode::InternalError,
message: message.into(),
details: None,
}
}
/// Resolve the hub credential, waiting if the leader is still wiring auth
/// (the IPC socket comes up first). Resolves the instant auth is wired or the
/// leader cancels — event-driven, no timeout.
async fn wait_for_leader_auth(
ws: &WorkspaceControl,
cancel: &CancellationToken,
) -> Result<Arc<dyn AuthProvider>, ControlError> {
let mut rx = ws.auth.subscribe();
tokio::select! {
result = rx.wait_for(| v | v.is_some()) => match result { Ok(guard) => Ok(guard
.clone().expect("waited for Some")), Err(_) =>
Err(workspace_err("leader is shutting down; cannot expose workspace to the hub",)),
}, _ = cancel.cancelled() =>
Err(workspace_err("leader is shutting down; cannot expose workspace to the hub",)),
}
}
fn workspace_server_id() -> String {
let raw = gethostname::gethostname()
.to_string_lossy()
.to_ascii_lowercase();
let sanitized: String = raw
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect();
let name = sanitized.trim_matches('-');
if name.is_empty() {
"grok-workspace".to_string()
} else {
name.to_string()
}
}
async fn drain_and_disconnect(handle: &WorkspaceHandle) {
let tracker = handle.activity_tracker().clone();
tracker.set_draining();
if tokio::time::timeout(WORKSPACE_DRAIN_TIMEOUT, tracker.wait_until_drained())
.await
.is_err()
{
warn!(
active = tracker.total_active(),
"workspace drain timed out; disconnecting hub anyway"
);
}
handle.shutdown_hub().await;
}
fn build_workspace_status(
metadata: &LeaderServerMetadata,
exposure: Option<&WorkspaceExposure>,
) -> ControlPayload {
match exposure {
None => ControlPayload::WorkspaceStatus {
state: "none".to_string(),
hub_url: None,
cwd: None,
uptime_ms: 0,
active_tool_calls: 0,
sessions: Vec::new(),
pid: metadata.pid,
},
Some(exp) => {
let snapshot = exp.handle.activity_tracker().snapshot();
let mut sessions = exp.handle.session_ids();
sessions.sort();
ControlPayload::WorkspaceStatus {
state: if exp.paused.load(std::sync::atomic::Ordering::Relaxed) {
"paused"
} else {
"running"
}
.to_string(),
hub_url: Some(exp.hub_url.clone()),
cwd: Some(exp.cwd.display().to_string()),
uptime_ms: exp.started_at.elapsed().as_millis() as u64,
active_tool_calls: snapshot.active_tool_calls,
sessions,
pid: metadata.pid,
}
}
}
}
async fn handle_workspace_start(
control_state: LeaderServerControlState,
hub_url: Option<String>,
cwd: String,
cancel: CancellationToken,
) -> Result<ControlPayload, ControlError> {
let ws = &control_state.workspace;
let url_str = hub_url
.filter(|u| !u.trim().is_empty())
.or_else(|| ws.default_hub_url.clone())
.unwrap_or_else(|| PROD_COMPUTER_HUB_URL.to_string());
let url = url::Url::parse(&url_str)
.map_err(|e| workspace_err(format!("invalid hub url {url_str}: {e}")))?;
let cwd_path = PathBuf::from(&cwd);
let _serialize = ws.lock.lock().await;
if let Some(existing) = ws.exposure.load_full()
&& !existing.paused.load(Ordering::Relaxed)
&& existing.cwd == cwd_path
&& existing.hub_url == url_str
{
return Ok(build_workspace_status(
&control_state.metadata,
Some(existing.as_ref()),
));
}
let allow_insecure_ws =
url.scheme() == "ws" && matches!(url.host_str(), Some("localhost" | "127.0.0.1" | "::1"));
let status_config = kigi_workspace::StatusConfig::from_env();
let alpha_test_key = None;
let auth = wait_for_leader_auth(ws, &cancel).await?;
let server_id = workspace_server_id();
let metadata = serde_json::json!(
{ "source" : "grok-workspace", "hostname" : gethostname::gethostname()
.to_string_lossy(), "cwd" : cwd_path.display().to_string(), }
);
crate::agent::folder_trust::resolve_and_record(&cwd_path, None, false);
let project_lsp_trusted = crate::agent::folder_trust::project_scope_allowed(&cwd_path);
let handle = kigi_workspace::connect_local_workspace(
cwd_path.clone(),
url,
auth,
Some(metadata),
Some(server_id),
alpha_test_key,
allow_insecure_ws,
status_config,
project_lsp_trusted,
None,
None,
false,
false,
)
.await
.map_err(|e| workspace_err(format!("failed to connect workspace to hub: {e}")))?;
let exposure = Arc::new(WorkspaceExposure {
handle,
hub_url: url_str,
cwd: cwd_path,
started_at: Instant::now(),
paused: AtomicBool::new(false),
});
let payload = build_workspace_status(&control_state.metadata, Some(exposure.as_ref()));
if let Some(old) = ws.exposure.swap(Some(exposure)) {
drain_and_disconnect(&old.handle).await;
}
Ok(payload)
}
async fn handle_workspace_pause(
control_state: LeaderServerControlState,
) -> Result<ControlPayload, ControlError> {
let ws = &control_state.workspace;
let _serialize = ws.lock.lock().await;
let Some(exp) = ws.exposure.load_full() else {
return Err(workspace_err("no workspace exposure is running"));
};
if !exp.paused.load(Ordering::Relaxed) {
drain_and_disconnect(&exp.handle).await;
exp.paused.store(true, Ordering::Relaxed);
}
Ok(build_workspace_status(
&control_state.metadata,
Some(exp.as_ref()),
))
}
async fn handle_workspace_resume(
control_state: LeaderServerControlState,
) -> Result<ControlPayload, ControlError> {
let ws = &control_state.workspace;
let _serialize = ws.lock.lock().await;
let Some(exp) = ws.exposure.load_full() else {
return Err(workspace_err("no workspace exposure is running"));
};
if exp.paused.load(Ordering::Relaxed) {
exp.handle.activity_tracker().set_active();
if let Err(e) = exp.handle.connect_hub().await {
exp.handle.activity_tracker().set_draining();
return Err(workspace_err(format!("failed to reconnect to hub: {e}")));
}
exp.paused.store(false, Ordering::Relaxed);
}
Ok(build_workspace_status(
&control_state.metadata,
Some(exp.as_ref()),
))
}
async fn handle_workspace_stop(
control_state: LeaderServerControlState,
) -> Result<ControlPayload, ControlError> {
let ws = &control_state.workspace;
let _serialize = ws.lock.lock().await;
if let Some(exp) = ws.exposure.swap(None) {
drain_and_disconnect(&exp.handle).await;
}
Ok(build_workspace_status(&control_state.metadata, None))
}
async fn handle_workspace_status(
control_state: LeaderServerControlState,
) -> Result<ControlPayload, ControlError> {
let exposure = control_state.workspace.exposure.load_full();
Ok(build_workspace_status(
&control_state.metadata,
exposure.as_deref(),
))
}
async fn finalize_workspace_on_shutdown(control_state: LeaderServerControlState) {
let ws = &control_state.workspace;
let _serialize = ws.lock.lock().await;
if let Some(exp) = ws.exposure.swap(None) {
info!("Draining workspace exposure on leader shutdown");
drain_and_disconnect(&exp.handle).await;
}
}
fn handle_control_command(
control_state: &LeaderServerControlState,
command: ControlCommand,
@@ -1208,13 +898,6 @@ fn handle_control_command(
ControlCommand::StopCpuProfile => {
unreachable!("StopCpuProfile must be handled asynchronously")
}
ControlCommand::WorkspaceStart { .. }
| ControlCommand::WorkspacePause
| ControlCommand::WorkspaceResume
| ControlCommand::WorkspaceStop
| ControlCommand::WorkspaceStatus => {
unreachable!("workspace control commands are handled asynchronously")
}
ControlCommand::RelaunchForUpdate { .. } => {
unreachable!("RelaunchForUpdate must be handled asynchronously")
}
@@ -1564,13 +1247,6 @@ pub async fn run_leader_server(
agent_activity = agent_activity.clone(); let relaunching = relaunching
.clone(); tokio::spawn(async move { let result = match command {
ControlCommand::StopCpuProfile => { handle_stop_cpu_profile(control_state).
await } ControlCommand::WorkspaceStart { hub_url, cwd } => {
handle_workspace_start(control_state, hub_url, cwd, cancel.clone(),). await }
ControlCommand::WorkspacePause => { handle_workspace_pause(control_state).
await } ControlCommand::WorkspaceResume => {
handle_workspace_resume(control_state). await } ControlCommand::WorkspaceStop
=> { handle_workspace_stop(control_state). await }
ControlCommand::WorkspaceStatus => { handle_workspace_status(control_state).
await } ControlCommand::RelaunchForUpdate { to_version } => {
decide_relaunch_for_update(& control_state, to_version, & relaunching,) }
other => handle_control_command(& control_state, other), }; let arm_relaunch
@@ -1806,7 +1482,6 @@ pub async fn run_leader_server(
debug!("No client available for notification routing, message dropped"); } }
}
}
finalize_workspace_on_shutdown(control_state.clone()).await;
finalize_cpu_profile_on_shutdown(control_state).await;
let _ = std::fs::remove_file(&socket_path);
Ok(())
@@ -2214,48 +1889,6 @@ mod tests {
Ok(ControlPayload::RelaunchDeclined { .. })
));
}
#[derive(Debug)]
struct TestAuth;
impl AuthProvider for TestAuth {
fn current(&self) -> AuthCredential {
AuthCredential::bearer("test-token")
}
}
#[tokio::test]
async fn wait_for_leader_auth_returns_when_already_wired() {
let ws = WorkspaceControl::new(None);
ws.auth.send_replace(Some(Arc::new(TestAuth)));
let cancel = CancellationToken::new();
let auth = wait_for_leader_auth(&ws, &cancel).await.expect("wired");
assert!(matches!(auth.current(), AuthCredential::Bearer { .. }));
}
#[tokio::test]
async fn wait_for_leader_auth_resolves_when_wired_late() {
let ws = Arc::new(WorkspaceControl::new(None));
let cancel = CancellationToken::new();
let waiter = {
let ws = ws.clone();
let cancel = cancel.clone();
tokio::spawn(async move { wait_for_leader_auth(&ws, &cancel).await.is_ok() })
};
tokio::task::yield_now().await;
ws.auth.send_replace(Some(Arc::new(TestAuth)));
assert!(waiter.await.unwrap(), "auth wired late should resolve Ok");
}
#[tokio::test]
async fn workspace_start_errors_when_cancelled_before_auth() {
let state = default_test_control_state(Path::new("/tmp/grok-ws-auth-test.sock"));
let cancel = CancellationToken::new();
cancel.cancel();
let err = handle_workspace_start(state, None, "/tmp".to_string(), cancel)
.await
.unwrap_err();
assert!(
err.message.contains("shutting down"),
"unexpected error: {}",
err.message
);
}
async fn setup_test_server(
temp: &TempDir,
) -> (PathBuf, CancellationToken, mpsc::UnboundedReceiver<String>) {
@@ -35,7 +35,6 @@ pub(crate) fn fake_caps(control_v1: bool, relaunch_v1: bool) -> LeaderCapabiliti
control_v1,
runtime_cpu_profile: false,
profile_formats: Vec::new(),
workspace_exposure: false,
relaunch_v1,
}
}
@@ -928,9 +928,6 @@ pub(crate) struct SessionActor {
/// Centralized event tracking: event log, turn-end guard, active tool,
/// doom loop terminate flag. All event-related state lives here.
pub(crate) events: crate::session::events::EventTracker,
/// Optional hub-side session event emitter (always constructed without a
/// harness client in the agent; methods no-op with `None` transport).
pub(crate) observability_bridge: kigi_computer_hub_sdk::ObservabilityBridge,
/// Turn number captured at the start of each turn (before prompt index
/// increment). Used by `ToolCallStarted` bridge emissions so they
/// report the same turn number as `TurnStarted` / `TurnEnded`.
@@ -1522,8 +1519,6 @@ mod fs_injection_regression_tests;
#[path = "acp_session_tests/interjection_actor_tests.rs"]
mod interjection_actor_tests;
#[cfg(test)]
#[path = "acp_session_tests/observability_bridge_mapping_tests.rs"]
mod observability_bridge_mapping_tests;
#[cfg(test)]
#[path = "acp_session_tests/permission_auto_mode_tests.rs"]
mod permission_auto_mode_tests;
@@ -251,34 +251,8 @@ pub(crate) async fn spawn_session_actor(
.as_ref()
.map(kigi_workspace::permission::resolution::deny_read_globs_from_config)
.unwrap_or_default();
let hub_permission = if kigi_workspace::permission::hitl_permission_live_enabled() {
let server = match workspace_ops.workspace_handle() {
Some(handle) => handle.hub_server_blocking().await,
None => None,
};
let transport = server
.and_then(|server| {
kigi_workspace::permission::ToolServerPermissionTransport::from_session_id(
server,
session_info.id.0.as_ref(),
)
})
.map(|t| {
std::sync::Arc::new(t)
as std::sync::Arc<dyn kigi_workspace::permission::PermissionHookTransport>
});
if transport.is_none() {
tracing::debug!(
session_id = % session_info.id.0,
"hitl permission live enabled but no remote transport available; using local prompt"
);
}
transport
} else {
None
};
let (permissions, _permission_events_rx) =
kigi_workspace::permission::spawn_permission_manager_with_hub(
kigi_workspace::permission::spawn_permission_manager(
session_info.id.clone(),
gateway.clone(),
tool_context.cwd.clone(),
@@ -289,7 +263,6 @@ pub(crate) async fn spawn_session_actor(
session_yolo_mode,
session_client_identifier.clone(),
crate::util::config::remember_tool_approvals_from_disk(),
hub_permission,
);
if crate::util::config::auto_mode_session_active(
crate::util::config::auto_permission_mode_enabled_from_disk(),
@@ -975,11 +948,6 @@ pub(crate) async fn spawn_session_actor(
let (goal_update_tx, goal_update_rx) = tokio::sync::mpsc::unbounded_channel::<
kigi_tools::implementations::grok_build::update_goal::UpdateGoalEnvelope,
>();
let obs_bridge = {
let sid = kigi_tool_protocol::SessionId::new(&*session_info.id.0)
.unwrap_or_else(|_| kigi_tool_protocol::SessionId::new("unknown").expect("valid"));
kigi_computer_hub_sdk::ObservabilityBridge::new(None, sid)
};
let mut effective_config = crate::config::load_effective_config()
.ok()
.and_then(|raw| crate::agent::config::Config::new_from_toml_cfg(&raw).ok())
@@ -1199,7 +1167,6 @@ pub(crate) async fn spawn_session_actor(
events: crate::session::events::EventTracker::new(
&crate::session::persistence::session_dir(&session_info),
),
observability_bridge: obs_bridge,
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -323,15 +323,6 @@ impl SessionActor {
self.emit_event(crate::session::events::Event::ToolStarted {
tool_name: call.function.name.clone(),
});
self.observability_bridge
.emit(
kigi_tool_protocol::session_event::SessionEvent::ToolCallStarted {
tool_call_id: call.id.clone(),
tool_name: call.function.name.clone(),
turn_number: self.current_turn_number.get(),
},
)
.await;
let call_name = call.function.name.clone();
match self
.prepare_tool_call(call, &mut deferred_followups)
@@ -653,16 +644,6 @@ impl SessionActor {
duration_ms,
outcome: tool_outcome,
});
self.observability_bridge
.emit(
kigi_tool_protocol::session_event::SessionEvent::ToolCallCompleted {
tool_call_id: prepared.call_id.clone(),
tool_name: prepared.tool_name.clone(),
duration_ms,
outcome: map_tool_outcome(tool_outcome),
},
)
.await;
tracing::info_span!(
"tool.execution", tool_name = % prepared.tool_name, tool_use_id = %
prepared.call_id, tool_input_size_bytes = prepared.raw_arguments.len() as
@@ -394,15 +394,6 @@ impl SessionActor {
schema_version: crate::session::events::EVENT_SCHEMA_VERSION.into(),
redirect_kind,
});
self.observability_bridge
.emit(
kigi_tool_protocol::session_event::SessionEvent::TurnStarted {
turn_number,
model_id: model_id.clone(),
yolo_mode,
},
)
.await;
self.send_before_turn_event(kigi_tool_protocol::turn_hook::BeforeTurnPayload {
turn_number: self.chat_state_handle.get_prompt_index().await as u64,
model_id: model_id.clone(),
@@ -733,15 +724,6 @@ impl SessionActor {
);
let turn_tool_count = self.events.tool_count_this_turn();
let bridge_outcome = turn_result_to_hook_outcome(&result);
self.observability_bridge
.emit(kigi_tool_protocol::session_event::SessionEvent::TurnEnded {
turn_number: current_prompt_index as u64,
outcome: bridge_outcome,
duration_ms: turn_duration_ms,
tool_call_count: turn_tool_count,
model_id: turn_model_id.clone(),
})
.await;
match &result {
Ok(TurnOutcome::Completed { .. }) => {
self.emit_turn_ended(
@@ -1678,13 +1660,6 @@ impl SessionActor {
self.emit_event(crate::session::events::Event::PhaseChanged {
phase: crate::session::events::Phase::WaitingForModel,
});
self.observability_bridge
.emit(
kigi_tool_protocol::session_event::SessionEvent::PhaseChanged {
phase: kigi_tool_protocol::session_event::SessionPhase::Sampling,
},
)
.await;
kigi_log::unified_log::info(
"shell.turn.inference_start",
Some(self.session_info.id.0.as_ref()),
@@ -1989,13 +1964,6 @@ impl SessionActor {
self.emit_event(crate::session::events::Event::PhaseChanged {
phase: crate::session::events::Phase::ToolExecution,
});
self.observability_bridge
.emit(
kigi_tool_protocol::session_event::SessionEvent::PhaseChanged {
phase: kigi_tool_protocol::session_event::SessionPhase::ToolExecution,
},
)
.await;
let execute_tool_calls_result = self.execute_tool_calls(tool_call_responses).await;
match execute_tool_calls_result {
Ok(ToolLoop::PermissionReject { tool_name, reason }) => {
@@ -256,7 +256,6 @@ async fn persist_ack_waits_for_disk_flush_before_success() {
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -695,7 +694,6 @@ async fn first_turn_memory_injection_disabled_does_not_persist_to_chat_history()
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -943,7 +941,6 @@ async fn cancel_running_task_teardown_clears_running_and_pending_work() {
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -1924,7 +1921,6 @@ async fn cancel_propagates_to_sampler_handle_so_no_further_emission() {
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -62,7 +62,6 @@ async fn tool_bridge_routes_writes_through_injected_fs() {
video_gen_config: Default::default(),
app_builder_deployer_config: Default::default(),
api_key_provider: None,
auth_provider: None,
attribution_callback: None,
system_reminder_tag: kigi_tools::reminders::DEFAULT_REMINDER_TAG,
};
@@ -285,7 +285,6 @@ async fn test_e2e_idle_resume_refreshes_model_metadata() {
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -214,7 +214,6 @@ async fn create_test_actor(
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -655,7 +654,6 @@ async fn create_test_actor_with_memory(
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -1410,7 +1408,6 @@ async fn test_e2e_idle_resume_refreshes_model_metadata() {
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -276,7 +276,6 @@ async fn create_test_actor_with_memory(
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -33,6 +33,7 @@ fn install_real_permissions(actor: &mut SessionActor) {
vec![],
false,
None,
true,
);
actor.permissions = handle;
}
@@ -222,7 +222,6 @@ pub(super) async fn make_replay_send_update_fixture() -> ReplaySendUpdateFixture
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -12,12 +12,6 @@ pub(crate) const HARNESS_VERIFIES_SENTENCE: &str =
pub(crate) const PLAN_SEED_TODOS_PHRASE: &str =
"Seed todos from the plan's acceptance criteria via";
#[cfg(test)]
pub(crate) fn noop_observability_bridge() -> kigi_computer_hub_sdk::ObservabilityBridge {
kigi_computer_hub_sdk::ObservabilityBridge::new(
None,
kigi_tool_protocol::SessionId::new("test").expect("valid"),
)
}
#[cfg(test)]
pub(crate) async fn test_agent_default() -> kigi_agent::Agent {
test_agent_with_tools(vec![]).await
@@ -99,7 +93,6 @@ async fn test_agent_from_config(
video_gen_config: Default::default(),
app_builder_deployer_config: Default::default(),
api_key_provider: None,
auth_provider: None,
attribution_callback: None,
system_reminder_tag: kigi_tools::reminders::DEFAULT_REMINDER_TAG,
};
@@ -336,7 +329,6 @@ pub(crate) async fn create_test_actor_ex(
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -41,7 +41,6 @@ async fn web_search_errors_when_disabled() {
video_gen_config: Default::default(),
app_builder_deployer_config: Default::default(),
api_key_provider: None,
auth_provider: None,
attribution_callback: None,
system_reminder_tag: kigi_tools::reminders::DEFAULT_REMINDER_TAG,
};
@@ -2312,7 +2312,6 @@ mod inline_auto_compact_flow_tests {
plugin_registry: std::cell::RefCell::new(None),
plugin_registry_handle: None,
events: crate::session::events::EventTracker::new(std::path::Path::new("/tmp")),
observability_bridge: noop_observability_bridge(),
current_turn_number: std::cell::Cell::new(0),
last_recap_main_turn: std::cell::Cell::new(0),
recap_in_flight: std::cell::Cell::new(false),
@@ -1052,7 +1052,7 @@ async fn test_load_prompts_only_large_session() {
info.id.clone(),
acp::SessionUpdate::UserMessageChunk(
acp::ContentChunk::new(
acp::ContentBlock::Text(acp::TextContent::new(format!("part2"))),
acp::ContentBlock::Text(acp::TextContent::new("part2".to_string())),
),
),
);
@@ -432,7 +432,7 @@ fn git_rebase_refresh_storm_e2e() {
unsafe {
std::env::set_var("KIGI_SHARE_DIR", kigi_home.path());
std::env::set_var("KIGI_CODE_BASE_URL", server.url());
std::env::set_var("KIGI_XAI_API_BASE_URL", server.url());
std::env::set_var("KIGI_API_BASE_URL", server.url());
std::env::set_var("XAI_API_KEY", "test-key-for-ci");
std::env::set_var("KIGI_TELEMETRY_ENABLED", "false");
std::env::set_var("KIGI_FEEDBACK_ENABLED", "false");
@@ -626,7 +626,7 @@ async fn full_session_load_e2e() {
std::env::set_var("KIGI_INSTRUMENTATION", "log");
std::env::set_var("KIGI_INSTRUMENTATION_LOG", &instr_log);
std::env::set_var("KIGI_CODE_BASE_URL", server.url());
std::env::set_var("KIGI_XAI_API_BASE_URL", server.url());
std::env::set_var("KIGI_API_BASE_URL", server.url());
std::env::set_var("XAI_API_KEY", "test-key-for-ci");
std::env::set_var("KIGI_TELEMETRY_ENABLED", "false");
std::env::set_var("KIGI_FEEDBACK_ENABLED", "false");
@@ -1330,7 +1330,7 @@ async fn test_headless_managed_config_byok_sends_authorized_requests() {
r#"
[endpoints]
deployment_key = "test-deployment-key"
xai_api_base_url = "{url}"
api_base_url = "{url}"
[model.kigi-build]
api_backend = "responses"
@@ -137,7 +137,7 @@ async fn leader_soak_churning_clients_no_leaks_no_zombies() {
unsafe {
std::env::set_var("KIGI_SHARE_DIR", kigi_home.path());
std::env::set_var("KIGI_CODE_BASE_URL", server.url());
std::env::set_var("KIGI_XAI_API_BASE_URL", server.url());
std::env::set_var("KIGI_API_BASE_URL", server.url());
std::env::set_var("XAI_API_KEY", "test-key-for-ci");
std::env::set_var("KIGI_TELEMETRY_ENABLED", "false");
std::env::set_var("KIGI_FEEDBACK_ENABLED", "false");
@@ -16,7 +16,7 @@ use kigi_workspace::permission::types::{
};
use kigi_workspace::permission::{
AccessKind, ClientType, Decision, PermissionCommand, PermissionHandle, PermissionState,
spawn_permission_manager, spawn_permission_manager_with_hub,
spawn_permission_manager,
};
use serial_test::serial;
use tokio::sync::{mpsc, oneshot};
@@ -221,6 +221,7 @@ async fn run_actor_test_full<F, Fut>(
vec![],
initial_yolo,
None,
true,
);
body(handle, gw, cwd).await;
})
@@ -357,7 +358,7 @@ async fn policy_ask_suppresses_mcp_tool_allowlist() {
let (gw, _gw_task) = fake_gateway();
// Gate OFF so the `ask` rule stays a hard floor over the grant.
let (handle, _events) = spawn_permission_manager_with_hub(
let (handle, _events) = spawn_permission_manager(
make_session_id(),
gw.sender.clone(),
cwd.clone(),
@@ -368,7 +369,6 @@ async fn policy_ask_suppresses_mcp_tool_allowlist() {
false,
None,
false, // remember_tool_approvals
None,
);
// Script an outright reject so we can confirm the prompt fires.
@@ -408,7 +408,7 @@ async fn policy_ask_suppresses_mcp_server_allowlist() {
let (gw, _gw_task) = fake_gateway();
// Gate OFF so the `ask` rule stays a hard floor over the grant.
let (handle, _events) = spawn_permission_manager_with_hub(
let (handle, _events) = spawn_permission_manager(
make_session_id(),
gw.sender.clone(),
cwd.clone(),
@@ -419,7 +419,6 @@ async fn policy_ask_suppresses_mcp_server_allowlist() {
false,
None,
false, // remember_tool_approvals
None,
);
gw.expected.send(("reject-once".to_string(), None)).unwrap();
@@ -463,6 +462,7 @@ async fn policy_deny_takes_precedence_over_mcp_allowlist() {
vec![],
false,
None,
true,
);
// Do NOT script a response: a policy Deny must short-circuit