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 Supercode 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    if harness != HarnessId::CLAUDE_CODE {
178        return Err(HarnessInteropSettingsError::UnsupportedHarness(
179            harness.to_string(),
180        ));
181    }
182    let settings = read_claude_peer_settings(homes)?;
183    let configured = settings
184        .cross_session_inbound
185        .map(|value| value.as_str().to_string());
186    let choices = [
187        (
188            "accept",
189            "Allow automatically",
190            "Deliver messages from the user's other Claude Code sessions without a separate inbound approval.",
191            Some("A trusted peer session can introduce instructions into this session; the receiver's configured permission mode still governs subsequent tool use."),
192        ),
193        (
194            "hold",
195            "Ask before delivery",
196            "Hold messages from sessions in a different permission-mode class for review.",
197            None,
198        ),
199        (
200            "refuse",
201            "Refuse automatically",
202            "Do not deliver messages from sessions in a different permission-mode class.",
203            None,
204        ),
205    ]
206    .into_iter()
207    .map(|(value, label, description, risk)| HarnessSettingChoice {
208        value: value.into(),
209        label: label.into(),
210        description: description.into(),
211        risk: risk.map(str::to_string),
212    })
213    .collect();
214    let controls = vec![HarnessInteropControl {
215        key: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
216        native_key: "crossSessionInbound".into(),
217        label: "Messages from other sessions".into(),
218        description: "How Claude Code handles messages arriving from another live Claude Code session.".into(),
219        scope: HarnessSettingScope::User,
220        source_path: settings.path.clone(),
221        configured_value: configured.clone(),
222        // Supercode can inspect the user file but cannot prove the effective
223        // value of a running process after managed/project/CLI precedence.
224        effective_value: None,
225        effective_known: false,
226        effective_note: "This is the user-level value. Managed, project, or command-line policy may override it for a particular process.".into(),
227        choices,
228        writable: true,
229        resettable: configured.is_some(),
230        requires_restart: false,
231    }];
232    let advisories = if settings.user_allows_automatic_delivery() {
233        Vec::new()
234    } else {
235        vec![HarnessInteropAdvisory {
236            code: "claude_cross_session_inbound_accept".into(),
237            severity: HarnessAdvisorySeverity::Warning,
238            title: "Claude may hold messages from other sessions".into(),
239            message: "Supercode can hand a message to Claude's inbox, but Claude may hold or refuse it under the current user-level inbound policy.".into(),
240            setting: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
241            recommendation: HarnessSettingRecommendation {
242                label: "Allow messages from my other Claude sessions".into(),
243                description: "Set Claude Code's user-level cross-session inbound policy to accept.".into(),
244                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(),
245                change: HarnessSettingChange {
246                    key: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
247                    value: Some("accept".into()),
248                },
249                command: "supercode harness configure claude-code --cross-session-inbound accept".into(),
250            },
251        }]
252    };
253    Ok(HarnessInteropSettingsReport {
254        schema: HARNESS_INTEROP_SETTINGS_SCHEMA.into(),
255        harness: HarnessId::CLAUDE_CODE.into(),
256        revision: settings.revision,
257        controls,
258        advisories,
259    })
260}
261
262/// Apply a revision-checked set of changes and return the resulting snapshot.
263pub fn configure_harness_interop_settings(
264    homes: &HarnessHomes,
265    harness: &str,
266    changes: &[HarnessSettingChange],
267    expected_revision: Option<&str>,
268) -> Result<HarnessInteropSettingsReport, HarnessInteropSettingsError> {
269    if harness != HarnessId::CLAUDE_CODE {
270        return Err(HarnessInteropSettingsError::UnsupportedHarness(
271            harness.to_string(),
272        ));
273    }
274    if changes.len() != 1 || changes[0].key != CLAUDE_CROSS_SESSION_INBOUND_KEY {
275        let key = changes
276            .first()
277            .map(|change| change.key.clone())
278            .unwrap_or_else(|| "<missing>".into());
279        return Err(HarnessInteropSettingsError::UnsupportedSetting(key));
280    }
281    let change = &changes[0];
282    let value = match change.value.as_deref() {
283        None => None,
284        Some("accept") => Some(ClaudeCrossSessionInbound::Accept),
285        Some("hold") => Some(ClaudeCrossSessionInbound::Hold),
286        Some("refuse") => Some(ClaudeCrossSessionInbound::Refuse),
287        Some(value) => {
288            return Err(HarnessInteropSettingsError::InvalidValue {
289                key: change.key.clone(),
290                value: value.into(),
291            })
292        }
293    };
294    update_claude_peer_settings(homes, value, expected_revision)?;
295    inspect_harness_interop_settings(homes, harness)
296}