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 Volter Harness 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 {
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 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
268pub 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}