1use 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
16pub const HARNESS_INTEROP_SETTINGS_SCHEMA: &str = "supercode.harness-interop-settings.v1";
18pub 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")]
23pub enum HarnessSettingScope {
25 User,
27 Project,
29 Managed,
31 CommandLine,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct HarnessSettingChoice {
38 pub value: String,
40 pub label: String,
42 pub description: String,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub risk: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50pub struct HarnessInteropControl {
52 pub key: String,
54 pub native_key: String,
56 pub label: String,
58 pub description: String,
60 pub scope: HarnessSettingScope,
62 pub source_path: PathBuf,
64 pub configured_value: Option<String>,
66 pub effective_value: Option<String>,
68 pub effective_known: bool,
70 pub effective_note: String,
72 pub choices: Vec<HarnessSettingChoice>,
74 pub writable: bool,
76 pub resettable: bool,
78 pub requires_restart: bool,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83pub struct HarnessSettingChange {
85 pub key: String,
87 pub value: Option<String>,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct HarnessSettingRecommendation {
94 pub label: String,
96 pub description: String,
98 pub consequence: String,
100 pub change: HarnessSettingChange,
102 pub command: String,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
107#[serde(rename_all = "snake_case")]
108pub enum HarnessAdvisorySeverity {
110 Info,
112 Warning,
114 Error,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct HarnessInteropAdvisory {
121 pub code: String,
123 pub severity: HarnessAdvisorySeverity,
125 pub title: String,
127 pub message: String,
129 pub setting: String,
131 pub recommendation: HarnessSettingRecommendation,
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
136pub struct HarnessInteropSettingsReport {
138 pub schema: String,
140 pub harness: String,
142 pub revision: String,
144 pub controls: Vec<HarnessInteropControl>,
146 pub advisories: Vec<HarnessInteropAdvisory>,
148}
149
150#[derive(Debug, thiserror::Error)]
151pub enum HarnessInteropSettingsError {
153 #[error("`{0}` exposes no configurable Supercode interoperability controls")]
155 UnsupportedHarness(String),
156 #[error("unsupported interoperability setting `{0}`")]
158 UnsupportedSetting(String),
159 #[error("invalid value `{value}` for `{key}`")]
161 InvalidValue {
162 key: String,
164 value: String,
166 },
167 #[error(transparent)]
169 Claude(#[from] ClaudePeerSettingsError),
170}
171
172pub 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 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
262pub 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}