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
-863
View File
@@ -1,6 +1,5 @@
//! Workspace and session configuration types.
use crate::capability::CapabilityMode;
use crate::hub::HubConfig;
use kigi_tools::registry::types::{SessionContext, ToolRegistryBuilder, ToolServerConfig};
use std::collections::HashMap;
use std::path::PathBuf;
@@ -97,605 +96,6 @@ pub trait SessionContextFactory: Send + Sync {
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct MemoryConfig {}
/// Per-session toolset/capability selection from the `session.bind`
/// metadata. Absent fields fall back to the workspace default and `CapabilityMode::All`.
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct WorkspaceBindConfig {
/// Named toolset preset from the wire. **Never resolved** (see
/// [`Self::resolve`]); parsed only so it can be logged.
pub preset: Option<String>,
/// Capability mode applied to the session's toolset.
pub capability_mode: Option<CapabilityMode>,
/// Fully-specified toolset in the runtime serde shape. Takes precedence
/// over `tools`.
pub tool_config: Option<ToolServerConfig>,
/// Per-user feature-flag bag. `None` on legacy payloads → tools
/// fall back to their safe defaults.
pub viewer_ctx: Option<kigi_tool_runtime::WorkspaceViewerContext>,
/// Initial auto-approve (YOLO) state. `None` on legacy payloads →
/// fail-closed (false).
pub yolo_mode: Option<bool>,
/// Plane-configured toolset in the gRPC wire shape. An empty list is
/// treated as unset (proto3 repeated default).
pub tools: Option<Vec<kigi_tools_api::ToolConfigEntry>>,
pub manifest_version: Option<String>,
pub manifest_hash: Option<String>,
/// Opt-in: forward `BackgroundTaskCompleted` system notifications for this session.
pub system_notifications: bool,
pub rpc_only: bool,
}
/// Outcome of resolving a [`WorkspaceBindConfig`]; lets callers fail closed
/// instead of widening to the default toolset. Deliberately has **no preset
/// arm** (see [`WorkspaceBindConfig::resolve`]).
#[derive(Debug)]
pub enum ResolvedToolset {
/// An explicit toolset (`tool_config` or `tools`).
Toolset(ResolvedTools),
/// No explicit toolset was specified and the workspace allows falling
/// back to its default catalog (local/CLI embedders only).
UseDefault,
/// No explicit toolset was specified and the workspace requires one
/// (sandbox-launched standalone servers) — fail closed.
MissingToolConfig,
/// `tools` entries were specified but at least one failed to convert.
InvalidToolConfig(kigi_tools::registry::proto_convert::ToolConfigEntryError),
}
/// A resolved toolset plus the pinned entries this binary could not serve.
#[derive(Debug)]
pub struct ResolvedTools {
pub toolset: ToolServerConfig,
/// Pinned `tools` ids unknown to this binary's registry, sorted. Always
/// empty for `tool_config` resolutions.
pub unserved_tool_ids: Vec<String>,
}
impl ResolvedTools {
/// A fully-served toolset (no divergence).
fn full(toolset: ToolServerConfig) -> Self {
Self {
toolset,
unserved_tool_ids: Vec::new(),
}
}
}
impl WorkspaceBindConfig {
/// Parse hub `session.bind` metadata. The envelope is the shared
/// [`kigi_tool_runtime::WorkspaceBindMetadata`] (same type the emitter
/// serializes); `tool_config` is a consumer-only raw escape hatch read
/// separately.
pub fn from_metadata(metadata: &serde_json::Value) -> Self {
let wire: kigi_tool_runtime::WorkspaceBindMetadata =
serde_json::from_value(metadata.clone()).unwrap_or_default();
Self {
preset: wire.preset,
capability_mode: wire
.capability_mode
.and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()),
tool_config: metadata
.as_object()
.and_then(|obj| obj.get("tool_config"))
.and_then(|v| parse_field("tool_config", v)),
viewer_ctx: wire.viewer_ctx,
yolo_mode: wire.yolo_mode,
tools: Some(wire.tools).filter(|tools| !tools.is_empty()),
manifest_version: wire.manifest_version,
manifest_hash: wire.manifest_hash,
system_notifications: wire.system_notifications.unwrap_or(false),
rpc_only: wire.rpc_only,
}
}
/// Resolve the selected toolset.
///
/// Precedence: `tool_config` > `tools` (wire entries) > default/fail-closed.
/// Pinned `tools` are served per entry: ids `known_id` rejects are dropped
/// and reported in [`ResolvedTools::unserved_tool_ids`] instead of
/// silently falling back to a different toolset.
///
/// **Presets are never resolved** — a `preset` on the wire is logged and
/// ignored; only explicit `tools`/`tool_config` may select a toolset.
///
/// With `require_explicit_toolset` (sandbox standalone servers) a bind
/// without an explicit toolset fails closed instead of widening to the
/// binary's default catalog.
pub fn resolve(
&self,
known_id: &dyn Fn(&str) -> bool,
require_explicit_toolset: bool,
) -> ResolvedToolset {
if let Some(cfg) = &self.tool_config {
for (idx, tool) in cfg.tools.iter().enumerate() {
if let Err(err) = kigi_tools_api::config_validation::validate_name_override(
idx,
&tool.id,
tool.name_override.as_deref(),
) {
return ResolvedToolset::InvalidToolConfig(err);
}
}
return ResolvedToolset::Toolset(ResolvedTools::full(cfg.clone()));
}
if let Some(tools) = &self.tools {
let mut unserved_tool_ids: Vec<String> = Vec::new();
let mut served = Vec::with_capacity(tools.len());
for (idx, entry) in tools.iter().enumerate() {
if !known_id(&entry.id) {
unserved_tool_ids.push(entry.id.clone());
continue;
}
match kigi_tools::registry::proto_convert::tool_config_from_entry(
idx,
entry.clone(),
) {
Ok(tc) => served.push(tc),
Err(err) => return ResolvedToolset::InvalidToolConfig(err),
}
}
unserved_tool_ids.sort_unstable();
if !unserved_tool_ids.is_empty() {
tracing::warn!(
unserved = ? unserved_tool_ids, config_manifest_version = ? self
.manifest_version, running_version = kigi_version::VERSION,
"session.bind: serving known subset of pinned tools"
);
}
return ResolvedToolset::Toolset(ResolvedTools {
toolset: ToolServerConfig {
tools: served,
behavior_preset: None,
},
unserved_tool_ids,
});
}
if let Some(preset) = self.preset.as_deref() {
tracing::warn!(
preset,
"session.bind: toolset presets are not resolved by the workspace \
server; pass an explicit `tools` config"
);
}
if require_explicit_toolset {
ResolvedToolset::MissingToolConfig
} else {
ResolvedToolset::UseDefault
}
}
}
/// Parse a single bind-metadata field, ignoring (and logging) a malformed value.
fn parse_field<T: serde::de::DeserializeOwned>(name: &str, value: &serde_json::Value) -> Option<T> {
match serde_json::from_value(value.clone()) {
Ok(parsed) => Some(parsed),
Err(e) => {
tracing::warn!(
field = name, error = % e,
"session.bind metadata: ignoring malformed field"
);
None
}
}
}
#[cfg(test)]
mod bind_config_tests {
use super::*;
/// Predicate for tests where every pinned id is known to the binary.
fn all_known(_: &str) -> bool {
true
}
/// Predicate for tests simulating a binary that knows none of the ids.
fn none_known(_: &str) -> bool {
false
}
#[test]
fn parses_preset_and_capability() {
let v = serde_json::json!(
{ "preset" : "explore", "capability_mode" : "read_only" }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert_eq!(cfg.preset.as_deref(), Some("explore"));
assert_eq!(
cfg.capability_mode,
Some(crate::capability::CapabilityMode::ReadOnly)
);
}
#[test]
fn defaults_on_empty_or_mismatched_metadata() {
let empty = WorkspaceBindConfig::from_metadata(&serde_json::json!({}));
assert!(empty.preset.is_none());
assert!(empty.capability_mode.is_none());
assert!(matches!(
empty.resolve(&all_known, false),
ResolvedToolset::UseDefault
));
let weird = WorkspaceBindConfig::from_metadata(&serde_json::json!("hello"));
assert!(matches!(
weird.resolve(&all_known, false),
ResolvedToolset::UseDefault
));
}
/// Presets are banned: any preset (known or not) is ignored — never
/// resolved to a toolset, and never widened to the default in strict mode.
#[test]
fn presets_are_never_resolved() {
for preset in ["explore", "grok-computer", "bogus"] {
let cfg = WorkspaceBindConfig::from_metadata(&serde_json::json!({ "preset" : preset }));
assert!(
matches!(cfg.resolve(&all_known, false), ResolvedToolset::UseDefault),
"lax mode must fall through to the default, preset={preset}"
);
assert!(
matches!(
cfg.resolve(&all_known, true),
ResolvedToolset::MissingToolConfig
),
"strict mode must fail closed, preset={preset}"
);
}
}
/// Strict mode (sandbox standalone server): no explicit toolset on the
/// bind ⇒ fail closed instead of widening to the default catalog.
#[test]
fn strict_mode_requires_explicit_toolset() {
let empty = WorkspaceBindConfig::from_metadata(&serde_json::json!({}));
assert!(matches!(
empty.resolve(&all_known, true),
ResolvedToolset::MissingToolConfig
));
}
#[test]
fn malformed_field_does_not_discard_valid_siblings() {
let v = serde_json::json!(
{ "preset" : "explore", "capability_mode" : "raed_only" }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert_eq!(cfg.preset.as_deref(), Some("explore"));
assert!(cfg.capability_mode.is_none());
}
#[test]
fn workspace_bind_config_from_metadata_extracts_viewer_ctx() {
let v = serde_json::json!(
{ "preset" : "explore", "viewer_ctx" : { "stream_tool_progress" : true }, }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert_eq!(cfg.preset.as_deref(), Some("explore"));
let viewer = cfg.viewer_ctx.expect("viewer_ctx parsed");
assert!(viewer.stream_tool_progress);
}
/// Legacy payload without `viewer_ctx` still parses (mixed-version
/// proxy/workspace deploys).
#[test]
fn workspace_bind_config_from_metadata_legacy_omitted_viewer_ctx() {
let v = serde_json::json!({ "preset" : "explore" });
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert!(cfg.viewer_ctx.is_none());
}
#[test]
fn workspace_bind_config_from_metadata_extracts_yolo_mode() {
let v = serde_json::json!({ "preset" : "explore", "yolo_mode" : true });
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert_eq!(cfg.yolo_mode, Some(true));
}
#[test]
fn workspace_bind_config_yolo_mode_omitted_or_malformed_fails_closed() {
let omitted =
WorkspaceBindConfig::from_metadata(&serde_json::json!({ "preset" : "explore" }));
assert!(omitted.yolo_mode.is_none());
let malformed = WorkspaceBindConfig::from_metadata(
&serde_json::json!({ "preset" : "explore", "yolo_mode" : "yes" }),
);
assert!(malformed.yolo_mode.is_none());
assert_eq!(malformed.preset.as_deref(), Some("explore"));
}
#[test]
fn workspace_bind_config_extracts_system_notifications_flag() {
let on = WorkspaceBindConfig::from_metadata(
&serde_json::json!({ "system_notifications" : true }),
);
assert!(on.system_notifications);
let off = WorkspaceBindConfig::from_metadata(&serde_json::json!({ "preset" : "explore" }));
assert!(!off.system_notifications);
let explicit_off = WorkspaceBindConfig::from_metadata(
&serde_json::json!({ "system_notifications" : false }),
);
assert!(!explicit_off.system_notifications);
}
#[test]
fn workspace_bind_config_extracts_rpc_only_flag() {
let on = WorkspaceBindConfig::from_metadata(&serde_json::json!({ "rpc_only" : true }));
assert!(on.rpc_only);
let off = WorkspaceBindConfig::from_metadata(&serde_json::json!({ "preset" : "explore" }));
assert!(!off.rpc_only);
let explicit_off =
WorkspaceBindConfig::from_metadata(&serde_json::json!({ "rpc_only" : false }));
assert!(!explicit_off.rpc_only);
}
#[test]
fn workspace_bind_config_from_metadata_extracts_manifest_fields() {
let v = serde_json::json!(
{ "preset" : "explore", "manifest_version" : "v1", "manifest_hash" :
"abc123", }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert_eq!(cfg.manifest_version.as_deref(), Some("v1"));
assert_eq!(cfg.manifest_hash.as_deref(), Some("abc123"));
}
#[test]
fn workspace_bind_config_manifest_fields_default_to_none_when_absent() {
let cfg = WorkspaceBindConfig::from_metadata(&serde_json::json!({ "preset" : "explore" }));
assert!(cfg.manifest_version.is_none());
assert!(cfg.manifest_hash.is_none());
}
/// Consumer-side parity test for the bind-metadata `tools` contract;
/// pairs with the producer-side pin test in agentic-sampler's
/// `configs::plane` tests.
#[test]
fn tools_entries_resolve_to_tool_server_config() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "GrokBuild:grep", "params_json" :
"{\"max_results\":50}", "name_override" : "search", "params_name_overrides" :
{ "pattern" : "query" }, "behavior_version" : "legacy-0.4.10",
"description_override" : "Search the codebase", }, { "id" :
"GrokBuild:read_file" },], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, false) else {
panic!("tools entries must resolve to an explicit toolset");
};
assert!(resolved.unserved_tool_ids.is_empty());
let toolset = resolved.toolset;
assert_eq!(
toolset.behavior_preset, None,
"always the 'current' default"
);
assert_eq!(toolset.tools.len(), 2);
let grep = &toolset.tools[0];
assert_eq!(grep.id, "GrokBuild:grep");
assert_eq!(
grep.params,
serde_json::json!({ "max_results" : 50 })
.as_object()
.cloned()
);
assert_eq!(grep.name_override.as_deref(), Some("search"));
assert_eq!(
grep.params_name_overrides.as_ref().unwrap()["pattern"],
"query"
);
assert_eq!(grep.behavior_version.as_deref(), Some("legacy-0.4.10"));
assert_eq!(
grep.description_override.as_deref(),
Some("Search the codebase")
);
assert_eq!(grep.kind, None);
assert_eq!(toolset.tools[1].id, "GrokBuild:read_file");
}
#[test]
fn explicit_tool_config_wins_over_tools_entries() {
let v = serde_json::json!(
{ "tool_config" : { "tools" : [{ "id" : "raw:tool" }] }, "tools" : [{ "id" :
"wire:tool" }], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, false) else {
panic!("must resolve to a toolset");
};
assert_eq!(resolved.toolset.tools.len(), 1);
assert_eq!(resolved.toolset.tools[0].id, "raw:tool");
}
#[test]
fn tools_entries_win_even_with_preset_present() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "wire:tool" }], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, false) else {
panic!("must resolve to a toolset");
};
assert_eq!(resolved.toolset.tools.len(), 1);
assert_eq!(resolved.toolset.tools[0].id, "wire:tool");
}
#[test]
fn empty_tools_array_is_treated_as_unset() {
let v = serde_json::json!({ "preset" : "explore", "tools" : [] });
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert!(cfg.tools.is_none());
assert!(matches!(
cfg.resolve(&all_known, false),
ResolvedToolset::UseDefault
));
assert!(matches!(
cfg.resolve(&all_known, true),
ResolvedToolset::MissingToolConfig
));
let no_preset = serde_json::json!({ "tools" : [] });
let cfg = WorkspaceBindConfig::from_metadata(&no_preset);
assert!(matches!(
cfg.resolve(&all_known, false),
ResolvedToolset::UseDefault
));
}
#[test]
fn invalid_tools_entry_fails_closed() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "bad:tool", "params_json" :
"{not json" }], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
match cfg.resolve(&all_known, false) {
ResolvedToolset::InvalidToolConfig(err) => {
assert_eq!(err.tool_id, "bad:tool");
assert_eq!(err.index, 0);
}
other => panic!("expected InvalidToolConfig, got {other:?}"),
}
}
#[test]
fn invalid_name_override_fails_closed() {
let v = serde_json::json!(
{ "tools" : [{ "id" : "wire:ok", "name_override" : "fine_name" }, { "id" :
"wire:bad", "name_override" : "not a tool id!" },], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
match cfg.resolve(&all_known, true) {
ResolvedToolset::InvalidToolConfig(err) => {
assert_eq!(err.tool_id, "wire:bad");
assert_eq!(err.field_path(), "tools[1].name_override");
}
other => panic!("expected InvalidToolConfig, got {other:?}"),
}
}
#[test]
fn tool_config_escape_hatch_invalid_name_override_fails_closed() {
let v = serde_json::json!(
{ "tool_config" : { "tools" : [{ "id" : "raw:ok", "name_override" :
"fine_name" }, { "id" : "raw:bad", "name_override" : "not a tool id!" },] },
}
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
match cfg.resolve(&all_known, true) {
ResolvedToolset::InvalidToolConfig(err) => {
assert_eq!(err.tool_id, "raw:bad");
assert_eq!(err.field_path(), "tools[1].name_override");
}
other => panic!("expected InvalidToolConfig, got {other:?}"),
}
let v = serde_json::json!(
{ "tool_config" : { "tools" : [{ "id" : "raw:ok", "name_override" :
"fine_name" }] }, }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, true) else {
panic!("valid escape-hatch config must resolve");
};
assert_eq!(resolved.toolset.tools.len(), 1);
}
#[test]
fn invalid_entry_error_reports_wire_index_after_unknown_drop() {
let v = serde_json::json!(
{ "tools" : [{ "id" : "wire:unknown" }, { "id" : "wire:bad", "params_json" :
"{not json" },], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let known = |id: &str| id != "wire:unknown";
match cfg.resolve(&known, false) {
ResolvedToolset::InvalidToolConfig(err) => {
assert_eq!(err.tool_id, "wire:bad");
assert_eq!(
err.index, 1,
"index must be the wire position, not the known-subset position"
);
}
other => panic!("expected InvalidToolConfig, got {other:?}"),
}
}
#[test]
fn valid_name_overrides_resolve_intact() {
let v = serde_json::json!(
{ "tools" : [{ "id" : "wire:a", "name_override" : "renamed_a" }, { "id" :
"wire:b" },], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, true) else {
panic!("well-formed overrides must resolve to a toolset");
};
assert_eq!(resolved.toolset.tools.len(), 2);
assert_eq!(
resolved.toolset.tools[0].name_override.as_deref(),
Some("renamed_a")
);
assert_eq!(resolved.toolset.tools[1].name_override, None);
}
#[test]
fn pinned_tools_all_known_serves_full_expansion() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "wire:tool" }],
"manifest_version" : "9.9.9-any", }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, false) else {
panic!("known pinned tools must use the tools expansion");
};
assert!(resolved.unserved_tool_ids.is_empty());
assert_eq!(resolved.toolset.tools.len(), 1);
assert_eq!(resolved.toolset.tools[0].id, "wire:tool");
}
/// Unknown ids must be partitioned and reported, never silently replaced
/// by live preset resolution.
#[test]
fn pinned_tools_unknown_ids_are_partitioned_and_reported() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "wire:known" }, { "id" :
"wire:zz_unknown" }, { "id" : "wire:aa_unknown" },], "manifest_version" :
"0.0.0-stale", }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let known = |id: &str| id == "wire:known";
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&known, false) else {
panic!("partial coverage must still resolve to the known subset");
};
assert_eq!(resolved.toolset.tools.len(), 1);
assert_eq!(resolved.toolset.tools[0].id, "wire:known");
assert_eq!(
resolved.unserved_tool_ids,
vec!["wire:aa_unknown".to_owned(), "wire:zz_unknown".to_owned()],
"unserved ids are reported sorted"
);
}
/// A fully-unknown expansion serves empty and reports every id — it never
/// widens to preset/default.
#[test]
fn pinned_tools_all_unknown_serves_empty_and_reports_all() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "wire:tool" }],
"manifest_version" : "0.0.0-stale", }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&none_known, false) else {
panic!("all-unknown expansion must resolve (empty), not fall back");
};
assert!(resolved.toolset.tools.is_empty());
assert_eq!(resolved.unserved_tool_ids, vec!["wire:tool".to_owned()]);
}
#[test]
fn legacy_tools_without_manifest_version_are_not_gated() {
let v = serde_json::json!(
{ "preset" : "explore", "tools" : [{ "id" : "wire:tool" }], }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert!(cfg.manifest_version.is_none());
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&all_known, false) else {
panic!("legacy unpinned tools must resolve without gating");
};
assert_eq!(resolved.toolset.tools.len(), 1);
assert_eq!(resolved.toolset.tools[0].id, "wire:tool");
}
#[test]
fn tool_config_wins_regardless_of_stale_manifest_version() {
let v = serde_json::json!(
{ "tool_config" : { "tools" : [{ "id" : "raw:tool" }] }, "tools" : [{ "id" :
"wire:tool" }], "manifest_version" : "0.0.0-stale", }
);
let cfg = WorkspaceBindConfig::from_metadata(&v);
let ResolvedToolset::Toolset(resolved) = cfg.resolve(&none_known, false) else {
panic!("tool_config must always win");
};
assert!(resolved.unserved_tool_ids.is_empty());
assert_eq!(resolved.toolset.tools.len(), 1);
assert_eq!(resolved.toolset.tools[0].id, "raw:tool");
}
#[test]
fn malformed_tools_field_is_dropped_keeping_siblings() {
let v = serde_json::json!({ "preset" : "explore", "tools" : "not-a-list" });
let cfg = WorkspaceBindConfig::from_metadata(&v);
assert!(cfg.tools.is_none());
assert!(matches!(
cfg.resolve(&all_known, false),
ResolvedToolset::UseDefault
));
assert!(matches!(
cfg.resolve(&all_known, true),
ResolvedToolset::MissingToolConfig
));
}
}
/// Top-level config required to construct a [`crate::handle::WorkspaceHandle`].
///
/// `#[non_exhaustive]` so future fields are non-breaking.
@@ -726,18 +126,6 @@ pub struct WorkspaceConfig {
/// and disabled/enabled lists. Stored on `WorkspaceShared` for
/// `discover_plugins` calls. Defaults to empty.
pub plugin_discovery_config: crate::discovery::PluginDiscoveryConfig,
/// Optional server configuration. When `Some`, the workspace
/// can connect to the server after construction via
/// [`WorkspaceHandle::connect_hub`](crate::handle::WorkspaceHandle::connect_hub).
pub hub_config: Option<HubConfig>,
/// Auth provider for xAI service calls made from workspace-scoped code.
/// `None` for workspaces that do not configure service auth.
pub auth_provider: Option<kigi_computer_hub_sdk::SharedAuthProvider>,
/// Metadata attached to the tool server registration.
/// Propagated through the server to `ServerInfo.metadata` in
/// `servers.list` responses so harness clients can identify the
/// sandbox that started the tool server.
pub server_metadata: Option<serde_json::Value>,
/// Runtime-tunable timing/threshold config for the tool server.
pub status_config: crate::status_config::StatusConfig,
/// Folder-trust verdict for repo-local (project-scoped) LSP servers from
@@ -745,108 +133,12 @@ pub struct WorkspaceConfig {
/// shell caller resolves the verdict and threads it in; callers without a
/// folder-trust decision pass `true`.
pub project_lsp_trusted: bool,
/// Fail `session.bind`s without an explicit toolset closed instead of
/// widening to `default_tool_config`. Set by sandbox-launched standalone
/// servers; local/CLI embedders keep the default-catalog fallback.
pub require_explicit_toolset: bool,
/// Confine `x.ai/fs/*` / `workspace.fs_*` resolution to the workspace root
/// (reject `..`, absolute-outside-root, symlink escapes). Default `false`
/// (unconfined) — set to `true` only by the workspace server on a remote
/// sandbox, where the root is a real tenant boundary.
pub confine_fs_to_workspace_root: bool,
}
/// Metadata a tool server announces so hub consumers can identify and route
/// to it. Every field is optional and independently sourced; a local process
/// announces none.
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct WorkspaceServerMetadata {
/// Sandbox that provisioned this server. Absent for local servers.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sandbox_id: Option<String>,
/// Logical sandbox-service session UUID, from the `KIGI_SESSION_ID` env
/// var. Present whenever that var is set (every sandbox container, start
/// and restore), absent otherwise.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
/// Provider that provisioned this server. Populated on the start path
/// only (no container-side source on restore); absent for local servers.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_id: Option<String>,
/// Per-spawn launch nonce minted by the sandbox orchestrator and echoed
/// verbatim on the diagnostics `/ready` endpoint. Absent for local/legacy
/// launches.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub launch_id: Option<String>,
}
impl WorkspaceServerMetadata {
/// Merge an env-sourced logical session id into caller-supplied
/// tool-server metadata (`None` on the restore/local path).
///
/// `env_session_id` is the raw `KIGI_SESSION_ID`; empty is normalized to
/// absent. An explicit `session_id` already in `metadata` is never
/// clobbered. A non-object `metadata` value is returned unchanged (a
/// defensive no-op — the sole caller always sends an object).
pub fn merge_session_metadata(
metadata: Option<serde_json::Value>,
env_session_id: Option<String>,
) -> Option<serde_json::Value> {
let env_session_id = env_session_id.filter(|s| !s.is_empty());
match metadata {
Some(mut value) => {
if let Some(session_id) = env_session_id
&& let Some(obj) = value.as_object_mut()
&& !obj.contains_key("session_id")
{
obj.insert(
"session_id".to_owned(),
serde_json::Value::String(session_id),
);
}
Some(value)
}
None => serde_json::to_value(WorkspaceServerMetadata {
sandbox_id: None,
session_id: env_session_id,
provider_id: None,
launch_id: None,
})
.ok(),
}
}
}
impl WorkspaceConfig {
/// Construct a minimal config suitable for proxy-mode workspaces
/// where the workspace is used primarily as a ToolServer host.
pub fn new_for_proxy(
root_cwd: PathBuf,
session_factory: Arc<dyn SessionContextFactory>,
hub_config: HubConfig,
auth_provider: kigi_computer_hub_sdk::SharedAuthProvider,
server_metadata: Option<serde_json::Value>,
status_config: crate::status_config::StatusConfig,
tool_config: ToolServerConfig,
) -> Self {
Self {
root_cwd,
default_tool_config: tool_config,
respect_gitignore: false,
memory_config: None,
event_buffer_capacity: crate::config::DEFAULT_EVENT_BUFFER_CAPACITY,
session_factory,
hook_global_sources: vec![],
hook_project_sources: vec![],
skills_config: Default::default(),
plugin_discovery_config: Default::default(),
auth_provider: Some(auth_provider),
hub_config: Some(hub_config),
server_metadata,
project_lsp_trusted: true,
require_explicit_toolset: false,
confine_fs_to_workspace_root: false,
status_config,
}
}
}
/// Configuration for spawning a subagent session within a workspace.
#[derive(Clone)]
#[non_exhaustive]
@@ -928,158 +220,3 @@ pub enum IsolationMode {
/// Run the subagent inside a sandbox/container.
Sandbox,
}
#[cfg(test)]
mod tests {
use super::WorkspaceServerMetadata;
#[test]
fn workspace_server_metadata_serializes_all_present_fields() {
let meta = WorkspaceServerMetadata {
sandbox_id: Some("sb-123".to_owned()),
session_id: Some("11111111-1111-1111-1111-111111111111".to_owned()),
provider_id: Some("test-provider".to_owned()),
launch_id: Some("33333333-3333-3333-3333-333333333333".to_owned()),
};
let value = serde_json::to_value(&meta).unwrap();
assert_eq!(
value,
serde_json::json!({ "sandbox_id" : "sb-123", "session_id" :
"11111111-1111-1111-1111-111111111111", "provider_id" : "test-provider",
"launch_id" : "33333333-3333-3333-3333-333333333333", })
);
}
#[test]
fn workspace_server_metadata_omits_none_fields() {
let meta = WorkspaceServerMetadata {
sandbox_id: Some("sb-123".to_owned()),
session_id: None,
provider_id: None,
launch_id: None,
};
let value = serde_json::to_value(&meta).unwrap();
assert_eq!(value, serde_json::json!({ "sandbox_id" : "sb-123" }));
let empty = serde_json::to_value(WorkspaceServerMetadata::default()).unwrap();
assert_eq!(empty, serde_json::json!({}));
}
#[test]
fn workspace_server_metadata_deserializes_legacy_payload_without_new_fields() {
let legacy = serde_json::json!(
{ "sandbox_id" : "sb-legacy", "cwd" : "/workspace", "mode" : "remote", }
);
let meta: WorkspaceServerMetadata = serde_json::from_value(legacy).unwrap();
assert_eq!(meta.sandbox_id.as_deref(), Some("sb-legacy"));
assert_eq!(meta.session_id, None);
assert_eq!(meta.provider_id, None);
}
#[test]
fn workspace_server_metadata_round_trips_with_new_fields() {
let meta = WorkspaceServerMetadata {
sandbox_id: Some("sb-123".to_owned()),
session_id: Some("22222222-2222-2222-2222-222222222222".to_owned()),
provider_id: Some("test-provider".to_owned()),
launch_id: None,
};
let json = serde_json::to_string(&meta).unwrap();
let back: WorkspaceServerMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(back.sandbox_id, meta.sandbox_id);
assert_eq!(back.session_id, meta.session_id);
assert_eq!(back.provider_id, meta.provider_id);
}
#[test]
fn workspace_server_metadata_deserializes_partial_new_fields() {
let only_session = serde_json::json!(
{ "sandbox_id" : "sb-1", "session_id" :
"33333333-3333-3333-3333-333333333333", }
);
let meta: WorkspaceServerMetadata = serde_json::from_value(only_session).unwrap();
assert_eq!(
meta.session_id.as_deref(),
Some("33333333-3333-3333-3333-333333333333")
);
assert_eq!(meta.provider_id, None);
let only_provider = serde_json::json!(
{ "sandbox_id" : "sb-1", "provider_id" : "test-provider", }
);
let meta: WorkspaceServerMetadata = serde_json::from_value(only_provider).unwrap();
assert_eq!(meta.provider_id.as_deref(), Some("test-provider"));
assert_eq!(meta.session_id, None);
}
#[test]
fn workspace_server_metadata_reads_start_path_shaped_payload() {
let start_path = serde_json::json!(
{ "cwd" : "/workspace", "mode" : "remote", "sandbox_id" : "sb-start",
"session_id" : "44444444-4444-4444-4444-444444444444", "provider_id" :
"test-provider", }
);
let meta: WorkspaceServerMetadata = serde_json::from_value(start_path).unwrap();
assert_eq!(meta.sandbox_id.as_deref(), Some("sb-start"));
assert_eq!(
meta.session_id.as_deref(),
Some("44444444-4444-4444-4444-444444444444")
);
assert_eq!(meta.provider_id.as_deref(), Some("test-provider"));
}
#[test]
fn merge_session_metadata_builds_struct_from_env_on_none_branch() {
let merged =
WorkspaceServerMetadata::merge_session_metadata(None, Some("sess-1".to_owned()))
.unwrap();
assert_eq!(merged, serde_json::json!({ "session_id" : "sess-1" }));
let empty = WorkspaceServerMetadata::merge_session_metadata(None, None).unwrap();
assert_eq!(empty, serde_json::json!({}));
}
#[test]
fn merge_session_metadata_overlays_into_object_without_clobbering() {
let base = serde_json::json!({ "sandbox_id" : "sb-9", "mode" : "remote" });
let merged =
WorkspaceServerMetadata::merge_session_metadata(Some(base), Some("env-id".to_owned()))
.unwrap();
assert_eq!(
merged,
serde_json::json!({ "sandbox_id" : "sb-9", "mode" : "remote",
"session_id" : "env-id", })
);
let explicit = serde_json::json!({ "session_id" : "explicit" });
let merged = WorkspaceServerMetadata::merge_session_metadata(
Some(explicit),
Some("env-id".to_owned()),
)
.unwrap();
assert_eq!(merged, serde_json::json!({ "session_id" : "explicit" }));
}
#[test]
fn merge_session_metadata_leaves_object_untouched_when_no_env_id() {
let base = serde_json::json!({ "sandbox_id" : "sb-9" });
let merged =
WorkspaceServerMetadata::merge_session_metadata(Some(base.clone()), None).unwrap();
assert_eq!(merged, base);
}
#[test]
fn merge_session_metadata_non_object_is_returned_unchanged() {
let scalar = serde_json::json!("just-a-string");
let merged = WorkspaceServerMetadata::merge_session_metadata(
Some(scalar.clone()),
Some("env-id".to_owned()),
)
.unwrap();
assert_eq!(merged, scalar);
}
#[test]
fn merge_session_metadata_treats_empty_env_id_as_absent() {
let none_branch =
WorkspaceServerMetadata::merge_session_metadata(None, Some(String::new())).unwrap();
assert_eq!(none_branch, serde_json::json!({}));
let base = serde_json::json!({ "sandbox_id" : "sb-9" });
let overlay = WorkspaceServerMetadata::merge_session_metadata(
Some(base.clone()),
Some(String::new()),
)
.unwrap();
assert_eq!(overlay, base);
}
#[test]
fn workspace_server_metadata_rejects_wrong_typed_field() {
let bad = serde_json::json!({ "sandbox_id" : "sb-1", "session_id" : 42 });
let result: Result<WorkspaceServerMetadata, _> = serde_json::from_value(bad);
assert!(result.is_err());
}
}