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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
context-JSON modes stay read-only and conflict with `--fix` (#5552, v1).
- Added `/context` reporting of the real provider prompt-cache hit rate (C3)
alongside token pressure.
- Optional per-session control socket (`[control_socket]` config table,
Unix, off by default): when enabled, the interactive TUI binds a
newline-framed JSON-RPC socket at `<sessions-dir>/<session-id>/control.sock`
(0600) with the verbs `message`, `interrupt`, `relaunch`, and `status`,
typed error codes, bounded request sizes and dispatch timeouts, and
socket lifecycle riding the session lifecycle (stale-file takeover,
live-bind refusal with retry backoff). Windows parses the key but refuses
to bind with a clear error (#5533).

### Changed

Expand Down
20 changes: 20 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,11 @@ pub struct ConfigToml {
/// lifecycle `[hooks]` table so config rewrites preserve existing hooks.
#[serde(default)]
pub hook_sinks: Option<HookSinksToml>,
/// Per-session control socket (`[control_socket]`). Opt-in: an absent
/// table or `enabled = false` (the default) leaves the feature off and
/// behavior unchanged.
#[serde(default)]
pub control_socket: Option<ControlSocketToml>,
/// Agent Fleet trust and security policy (#3165). When absent, fleet
/// workers inherit conservative Sandbox defaults.
#[serde(default)]
Expand Down Expand Up @@ -1553,6 +1558,21 @@ pub struct HookSinksToml {
pub unix_socket_path: Option<PathBuf>,
}

/// On-disk schema for the `[control_socket]` table.
///
/// Opt-in per-session control surface: when `enabled`, the interactive TUI
/// binds a unix domain socket at `<sessions-dir>/<session-id>/control.sock`
/// for the running session. The socket speaks newline-framed JSON-RPC with
/// the verbs `message`, `interrupt`, `relaunch`, and `status`. An absent
/// table, or `enabled = false` (the default), disables the feature entirely —
/// behavior is unchanged from a release without the table.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ControlSocketToml {
/// Bind the per-session control socket. Default: false (OFF).
#[serde(default)]
pub enabled: bool,
}

/// On-disk schema for the `[skills]` table (#140). See `config.example.toml`
/// for documentation.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
Expand Down
42 changes: 42 additions & 0 deletions crates/config/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,48 @@ fn verifier_config_rejects_unknown_verdict_policy() {
);
}

#[test]
fn control_socket_toml_is_off_by_default_and_parses_when_enabled() {
// Unset = feature OFF: the table is absent and the field is None.
let absent: ConfigToml = toml::from_str("model = \"demo\"\n").expect("minimal config");
assert!(
absent.control_socket.is_none(),
"unset [control_socket] must leave the feature off"
);

// An empty table is also off: enabled defaults to false.
let empty: ConfigToml =
toml::from_str("[control_socket]\n").expect("empty control_socket table");
let socket = empty.control_socket.expect("table should parse");
assert!(!socket.enabled, "empty table must leave the socket off");

// Explicit enable.
let enabled: ConfigToml = toml::from_str(
r#"
[control_socket]
enabled = true
"#,
)
.expect("enabled control_socket table");
assert!(
enabled.control_socket.expect("table should parse").enabled,
"enabled = true must turn the socket on"
);

// Explicit disable stays off.
let disabled: ConfigToml = toml::from_str(
r#"
[control_socket]
enabled = false
"#,
)
.expect("disabled control_socket table");
assert!(
!disabled.control_socket.expect("table should parse").enabled,
"enabled = false must keep the socket off"
);
}

#[test]
fn permissions_toml_deserializes_typed_ask_rules() {
let permissions: PermissionsToml = toml::from_str(
Expand Down
8 changes: 8 additions & 0 deletions crates/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
context-JSON modes stay read-only and conflict with `--fix` (#5552, v1).
- Added `/context` reporting of the real provider prompt-cache hit rate (C3)
alongside token pressure.
- Optional per-session control socket (`[control_socket]` config table,
Unix, off by default): when enabled, the interactive TUI binds a
newline-framed JSON-RPC socket at `<sessions-dir>/<session-id>/control.sock`
(0600) with the verbs `message`, `interrupt`, `relaunch`, and `status`,
typed error codes, bounded request sizes and dispatch timeouts, and
socket lifecycle riding the session lifecycle (stale-file takeover,
live-bind refusal with retry backoff). Windows parses the key but refuses
to bind with a clear error (#5533).

### Changed

Expand Down
8 changes: 8 additions & 0 deletions crates/tui/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2987,6 +2987,13 @@ pub struct Config {
#[serde(default)]
pub hooks: Option<HooksConfig>,

/// Per-session control socket (`[control_socket]`). Opt-in: an absent
/// table or `enabled = false` (the default) leaves the feature off.
/// When enabled, the interactive TUI binds a unix socket per running
/// session (see `crate::tui::control_socket`).
#[serde(default)]
pub control_socket: Option<codewhale_config::ControlSocketToml>,

/// Provider-specific credentials and defaults shared with the `codewhale` facade.
#[serde(default)]
pub providers: Option<ProvidersConfig>,
Expand Down Expand Up @@ -10155,6 +10162,7 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
tui: override_cfg.tui.or(base.tui),
transcript: override_cfg.transcript.or(base.transcript),
hooks: override_cfg.hooks.or(base.hooks),
control_socket: override_cfg.control_socket.or(base.control_socket),
providers: merge_providers(base.providers, override_cfg.providers),
features: merge_features(base.features, override_cfg.features),
notifications: override_cfg.notifications.or(base.notifications),
Expand Down
32 changes: 32 additions & 0 deletions crates/tui/src/config/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,38 @@ web_search = true
Ok(())
}

#[test]
fn tui_config_parses_control_socket_table() {
let raw = r#"
[control_socket]
enabled = true
"#;
let parsed: ConfigFile = toml::from_str(raw).expect("parse control_socket config");

let socket = parsed
.base
.control_socket
.expect("control_socket table should parse");
assert!(socket.enabled);

// Off by default: a config without the table leaves the feature off.
let absent: ConfigFile =
toml::from_str("model = \"demo\"").expect("parse config without control_socket table");
assert!(absent.base.control_socket.is_none());

// An empty table stays off.
let empty: ConfigFile =
toml::from_str("[control_socket]").expect("parse empty control_socket table");
assert!(
!empty
.base
.control_socket
.expect("table should parse")
.enabled,
"empty table must leave the socket off"
);
}

#[test]
fn tui_config_parses_hotbar_bindings() {
let raw = r#"
Expand Down
20 changes: 20 additions & 0 deletions crates/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5900,6 +5900,10 @@ fn print_doctor_setup_report(
" · runtime posture: {}",
doctor_runtime_posture_line(config, workspace)
);
println!(
" · control socket: {}",
doctor_control_socket_posture_line(config)
);
let consistency = doctor_setup_consistency(state, source);
if consistency["status"] == "inconsistent" {
let issues = consistency["issues"]
Expand Down Expand Up @@ -6117,6 +6121,22 @@ fn doctor_runtime_posture_line(config: &Config, workspace: &Path) -> String {
)
}

/// Doctor posture for the per-session control socket, enabled via
/// `[control_socket].enabled` (false = off, the default). Report the
/// resolved state and, when enabled, where the socket appears for the
/// running session.
fn doctor_control_socket_posture_line(config: &Config) -> String {
let enabled = config
.control_socket
.as_ref()
.is_some_and(|socket| socket.enabled);
if enabled {
"control_socket=on (sessions/<id>/control.sock per running session)".to_string()
} else {
"control_socket=off (default)".to_string()
}
}

/// Resolved telemetry consent and where it came from (#5441).
///
/// Telemetry ships ON by default, and no posture surface reported that — a
Expand Down
Loading
Loading