Skip to main content

supercode_harness/
interop_settings.rs

1//! Harness-owned controls that materially affect Supercode interoperability.
2//!
3//! This is deliberately not a generic editor for every harness preference.
4//! Providers expose only controls that affect discovery, translation, peer
5//! delivery, or continuation, behind one stable report/change contract.
6
7use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{
12    read_claude_peer_settings, update_claude_peer_settings, ClaudeCrossSessionInbound,
13    ClaudePeerSettingsError, HarnessHomes, HarnessId,
14};
15
16/// Stable report schema shared by Rust, JSON-RPC, SDKs, and UIs.
17pub const HARNESS_INTEROP_SETTINGS_SCHEMA: &str = "supercode.harness-interop-settings.v1";
18/// Stable key for Claude Code's native `crossSessionInbound` preference.
19pub const CLAUDE_CROSS_SESSION_INBOUND_KEY: &str = "cross_session_inbound";
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23/// Native configuration layer from which a control was read.
24pub enum HarnessSettingScope {
25    /// User-wide harness preferences.
26    User,
27    /// Repository or workspace preferences.
28    Project,
29    /// Organization-managed policy.
30    Managed,
31    /// A command-line override on the running process.
32    CommandLine,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36/// One allowed value for an interoperability control.
37pub struct HarnessSettingChoice {
38    /// Native serialized value.
39    pub value: String,
40    /// Short user-facing label.
41    pub label: String,
42    /// Behavioral meaning of the value.
43    pub description: String,
44    /// Security or interoperability consequence, when notable.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub risk: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50/// One harness-owned setting that Supercode can inspect or configure.
51pub struct HarnessInteropControl {
52    /// Stable Supercode control key.
53    pub key: String,
54    /// Harness-native setting key.
55    pub native_key: String,
56    /// Short user-facing label.
57    pub label: String,
58    /// Explanation of the interoperability behavior controlled.
59    pub description: String,
60    /// Native configuration layer inspected.
61    pub scope: HarnessSettingScope,
62    /// Exact native file inspected or changed.
63    pub source_path: PathBuf,
64    /// Value explicitly configured in that file.
65    pub configured_value: Option<String>,
66    /// Effective value, when precedence can be proven.
67    pub effective_value: Option<String>,
68    /// Whether `effective_value` is authoritative for the running process.
69    pub effective_known: bool,
70    /// Why the effective value is or is not known.
71    pub effective_note: String,
72    /// Finite values accepted by this control.
73    pub choices: Vec<HarnessSettingChoice>,
74    /// Whether the host may change this control.
75    pub writable: bool,
76    /// Whether the explicit value can be removed.
77    pub resettable: bool,
78    /// Whether a harness restart is required after changing it.
79    pub requires_restart: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83/// Requested change to one stable interoperability control.
84pub struct HarnessSettingChange {
85    /// Stable Supercode control key.
86    pub key: String,
87    /// `None` removes the user override and restores the harness default.
88    pub value: Option<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92/// Concrete, reviewable remediation for an advisory.
93pub struct HarnessSettingRecommendation {
94    /// Short action label.
95    pub label: String,
96    /// Description of the native change.
97    pub description: String,
98    /// Security or behavior consequence the user should review.
99    pub consequence: String,
100    /// Machine-readable change for trusted hosts.
101    pub change: HarnessSettingChange,
102    /// Equivalent CLI command for transparent manual use.
103    pub command: String,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108/// Importance of an interoperability advisory.
109pub enum HarnessAdvisorySeverity {
110    /// Informational only.
111    Info,
112    /// The current configuration can impair an intended workflow.
113    Warning,
114    /// Configuration prevents a supported workflow.
115    Error,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119/// Actionable finding derived from native harness configuration.
120pub struct HarnessInteropAdvisory {
121    /// Stable machine-readable advisory code.
122    pub code: String,
123    /// User-facing importance.
124    pub severity: HarnessAdvisorySeverity,
125    /// Short summary.
126    pub title: String,
127    /// Explanation of the observed interoperability impact.
128    pub message: String,
129    /// Stable control key responsible for the finding.
130    pub setting: String,
131    /// Explicit remediation with its consequence.
132    pub recommendation: HarnessSettingRecommendation,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136/// Canonical, revisioned snapshot shared across CLI, RPC, SDK, and UI.
137pub struct HarnessInteropSettingsReport {
138    /// Stable schema identifier.
139    pub schema: String,
140    /// Harness identifier.
141    pub harness: String,
142    /// Hash of the exact native settings bytes used to build this report.
143    pub revision: String,
144    /// Exposed interoperability controls.
145    pub controls: Vec<HarnessInteropControl>,
146    /// Findings for the current values.
147    pub advisories: Vec<HarnessInteropAdvisory>,
148}
149
150#[derive(Debug, thiserror::Error)]
151/// Failure while inspecting or changing harness interoperability controls.
152pub enum HarnessInteropSettingsError {
153    /// The harness does not currently expose an interoperability-control provider.
154    #[error("`{0}` exposes no configurable Volter Harness interoperability controls")]
155    UnsupportedHarness(String),
156    /// The requested stable key is not exposed by this harness provider.
157    #[error("unsupported interoperability setting `{0}`")]
158    UnsupportedSetting(String),
159    /// The requested value is not one of the provider's declared choices.
160    #[error("invalid value `{value}` for `{key}`")]
161    InvalidValue {
162        /// Stable control key.
163        key: String,
164        /// Rejected native value.
165        value: String,
166    },
167    /// Native Claude Code settings could not be read or safely changed.
168    #[error(transparent)]
169    Claude(#[from] ClaudePeerSettingsError),
170}
171
172/// Inspect the interoperability controls exposed by one harness adapter.
173pub fn inspect_harness_interop_settings(
174    homes: &HarnessHomes,
175    harness: &str,
176) -> Result<HarnessInteropSettingsReport, HarnessInteropSettingsError> {
177    // A harness with no interoperability controls has an answer to "what are they?": none. Only changing one is
178    // refused (`configure_harness_interop_settings`).
179    if harness != HarnessId::CLAUDE_CODE {
180        return Ok(HarnessInteropSettingsReport {
181            schema: HARNESS_INTEROP_SETTINGS_SCHEMA.into(),
182            harness: harness.to_string(),
183            revision: String::new(),
184            controls: Vec::new(),
185            advisories: Vec::new(),
186        });
187    }
188    let settings = read_claude_peer_settings(homes)?;
189    let configured = settings
190        .cross_session_inbound
191        .map(|value| value.as_str().to_string());
192    let choices = [
193        (
194            "accept",
195            "Allow automatically",
196            "Deliver messages from the user's other Claude Code sessions without a separate inbound approval.",
197            Some("A trusted peer session can introduce instructions into this session; the receiver's configured permission mode still governs subsequent tool use."),
198        ),
199        (
200            "hold",
201            "Ask before delivery",
202            "Hold messages from sessions in a different permission-mode class for review.",
203            None,
204        ),
205        (
206            "refuse",
207            "Refuse automatically",
208            "Do not deliver messages from sessions in a different permission-mode class.",
209            None,
210        ),
211    ]
212    .into_iter()
213    .map(|(value, label, description, risk)| HarnessSettingChoice {
214        value: value.into(),
215        label: label.into(),
216        description: description.into(),
217        risk: risk.map(str::to_string),
218    })
219    .collect();
220    let controls = vec![HarnessInteropControl {
221        key: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
222        native_key: "crossSessionInbound".into(),
223        label: "Messages from other sessions".into(),
224        description: "How Claude Code handles messages arriving from another live Claude Code session.".into(),
225        scope: HarnessSettingScope::User,
226        source_path: settings.path.clone(),
227        configured_value: configured.clone(),
228        // Supercode can inspect the user file but cannot prove the effective
229        // value of a running process after managed/project/CLI precedence.
230        effective_value: None,
231        effective_known: false,
232        effective_note: "This is the user-level value. Managed, project, or command-line policy may override it for a particular process.".into(),
233        choices,
234        writable: true,
235        resettable: configured.is_some(),
236        requires_restart: false,
237    }];
238    let advisories = if settings.user_allows_automatic_delivery() {
239        Vec::new()
240    } else {
241        vec![HarnessInteropAdvisory {
242            code: "claude_cross_session_inbound_accept".into(),
243            severity: HarnessAdvisorySeverity::Warning,
244            title: "Claude may hold messages from other sessions".into(),
245            message: "Volter Harness can hand a message to Claude's inbox, but Claude may hold or refuse it under the current user-level inbound policy.".into(),
246            setting: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
247            recommendation: HarnessSettingRecommendation {
248                label: "Allow messages from my other Claude sessions".into(),
249                description: "Set Claude Code's user-level cross-session inbound policy to accept.".into(),
250                consequence: "Other Claude Code sessions owned by this user can introduce instructions without a separate inbound approval. The receiving session's configured permission mode still applies.".into(),
251                change: HarnessSettingChange {
252                    key: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
253                    value: Some("accept".into()),
254                },
255                command: "supercode harness configure claude-code --cross-session-inbound accept".into(),
256            },
257        }]
258    };
259    Ok(HarnessInteropSettingsReport {
260        schema: HARNESS_INTEROP_SETTINGS_SCHEMA.into(),
261        harness: HarnessId::CLAUDE_CODE.into(),
262        revision: settings.revision,
263        controls,
264        advisories,
265    })
266}
267
268/// Apply a revision-checked set of changes and return the resulting snapshot.
269pub fn configure_harness_interop_settings(
270    homes: &HarnessHomes,
271    harness: &str,
272    changes: &[HarnessSettingChange],
273    expected_revision: Option<&str>,
274) -> Result<HarnessInteropSettingsReport, HarnessInteropSettingsError> {
275    if harness != HarnessId::CLAUDE_CODE {
276        return Err(HarnessInteropSettingsError::UnsupportedHarness(
277            harness.to_string(),
278        ));
279    }
280    if changes.len() != 1 || changes[0].key != CLAUDE_CROSS_SESSION_INBOUND_KEY {
281        let key = changes
282            .first()
283            .map(|change| change.key.clone())
284            .unwrap_or_else(|| "<missing>".into());
285        return Err(HarnessInteropSettingsError::UnsupportedSetting(key));
286    }
287    let change = &changes[0];
288    let value = match change.value.as_deref() {
289        None => None,
290        Some("accept") => Some(ClaudeCrossSessionInbound::Accept),
291        Some("hold") => Some(ClaudeCrossSessionInbound::Hold),
292        Some("refuse") => Some(ClaudeCrossSessionInbound::Refuse),
293        Some(value) => {
294            return Err(HarnessInteropSettingsError::InvalidValue {
295                key: change.key.clone(),
296                value: value.into(),
297            })
298        }
299    };
300    update_claude_peer_settings(homes, value, expected_revision)?;
301    inspect_harness_interop_settings(homes, harness)
302}