use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::{
read_claude_peer_settings, update_claude_peer_settings, ClaudeCrossSessionInbound,
ClaudePeerSettingsError, HarnessHomes, HarnessId,
};
pub const HARNESS_INTEROP_SETTINGS_SCHEMA: &str = "supercode.harness-interop-settings.v1";
pub const CLAUDE_CROSS_SESSION_INBOUND_KEY: &str = "cross_session_inbound";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnessSettingScope {
User,
Project,
Managed,
CommandLine,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessSettingChoice {
pub value: String,
pub label: String,
pub description: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub risk: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessInteropControl {
pub key: String,
pub native_key: String,
pub label: String,
pub description: String,
pub scope: HarnessSettingScope,
pub source_path: PathBuf,
pub configured_value: Option<String>,
pub effective_value: Option<String>,
pub effective_known: bool,
pub effective_note: String,
pub choices: Vec<HarnessSettingChoice>,
pub writable: bool,
pub resettable: bool,
pub requires_restart: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessSettingChange {
pub key: String,
pub value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessSettingRecommendation {
pub label: String,
pub description: String,
pub consequence: String,
pub change: HarnessSettingChange,
pub command: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnessAdvisorySeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessInteropAdvisory {
pub code: String,
pub severity: HarnessAdvisorySeverity,
pub title: String,
pub message: String,
pub setting: String,
pub recommendation: HarnessSettingRecommendation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnessInteropSettingsReport {
pub schema: String,
pub harness: String,
pub revision: String,
pub controls: Vec<HarnessInteropControl>,
pub advisories: Vec<HarnessInteropAdvisory>,
}
#[derive(Debug, thiserror::Error)]
pub enum HarnessInteropSettingsError {
#[error("`{0}` exposes no configurable Supercode interoperability controls")]
UnsupportedHarness(String),
#[error("unsupported interoperability setting `{0}`")]
UnsupportedSetting(String),
#[error("invalid value `{value}` for `{key}`")]
InvalidValue {
key: String,
value: String,
},
#[error(transparent)]
Claude(#[from] ClaudePeerSettingsError),
}
pub fn inspect_harness_interop_settings(
homes: &HarnessHomes,
harness: &str,
) -> Result<HarnessInteropSettingsReport, HarnessInteropSettingsError> {
if harness != HarnessId::CLAUDE_CODE {
return Err(HarnessInteropSettingsError::UnsupportedHarness(
harness.to_string(),
));
}
let settings = read_claude_peer_settings(homes)?;
let configured = settings
.cross_session_inbound
.map(|value| value.as_str().to_string());
let choices = [
(
"accept",
"Allow automatically",
"Deliver messages from the user's other Claude Code sessions without a separate inbound approval.",
Some("A trusted peer session can introduce instructions into this session; the receiver's configured permission mode still governs subsequent tool use."),
),
(
"hold",
"Ask before delivery",
"Hold messages from sessions in a different permission-mode class for review.",
None,
),
(
"refuse",
"Refuse automatically",
"Do not deliver messages from sessions in a different permission-mode class.",
None,
),
]
.into_iter()
.map(|(value, label, description, risk)| HarnessSettingChoice {
value: value.into(),
label: label.into(),
description: description.into(),
risk: risk.map(str::to_string),
})
.collect();
let controls = vec![HarnessInteropControl {
key: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
native_key: "crossSessionInbound".into(),
label: "Messages from other sessions".into(),
description: "How Claude Code handles messages arriving from another live Claude Code session.".into(),
scope: HarnessSettingScope::User,
source_path: settings.path.clone(),
configured_value: configured.clone(),
effective_value: None,
effective_known: false,
effective_note: "This is the user-level value. Managed, project, or command-line policy may override it for a particular process.".into(),
choices,
writable: true,
resettable: configured.is_some(),
requires_restart: false,
}];
let advisories = if settings.user_allows_automatic_delivery() {
Vec::new()
} else {
vec![HarnessInteropAdvisory {
code: "claude_cross_session_inbound_accept".into(),
severity: HarnessAdvisorySeverity::Warning,
title: "Claude may hold messages from other sessions".into(),
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(),
setting: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
recommendation: HarnessSettingRecommendation {
label: "Allow messages from my other Claude sessions".into(),
description: "Set Claude Code's user-level cross-session inbound policy to accept.".into(),
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(),
change: HarnessSettingChange {
key: CLAUDE_CROSS_SESSION_INBOUND_KEY.into(),
value: Some("accept".into()),
},
command: "supercode harness configure claude-code --cross-session-inbound accept".into(),
},
}]
};
Ok(HarnessInteropSettingsReport {
schema: HARNESS_INTEROP_SETTINGS_SCHEMA.into(),
harness: HarnessId::CLAUDE_CODE.into(),
revision: settings.revision,
controls,
advisories,
})
}
pub fn configure_harness_interop_settings(
homes: &HarnessHomes,
harness: &str,
changes: &[HarnessSettingChange],
expected_revision: Option<&str>,
) -> Result<HarnessInteropSettingsReport, HarnessInteropSettingsError> {
if harness != HarnessId::CLAUDE_CODE {
return Err(HarnessInteropSettingsError::UnsupportedHarness(
harness.to_string(),
));
}
if changes.len() != 1 || changes[0].key != CLAUDE_CROSS_SESSION_INBOUND_KEY {
let key = changes
.first()
.map(|change| change.key.clone())
.unwrap_or_else(|| "<missing>".into());
return Err(HarnessInteropSettingsError::UnsupportedSetting(key));
}
let change = &changes[0];
let value = match change.value.as_deref() {
None => None,
Some("accept") => Some(ClaudeCrossSessionInbound::Accept),
Some("hold") => Some(ClaudeCrossSessionInbound::Hold),
Some("refuse") => Some(ClaudeCrossSessionInbound::Refuse),
Some(value) => {
return Err(HarnessInteropSettingsError::InvalidValue {
key: change.key.clone(),
value: value.into(),
})
}
};
update_claude_peer_settings(homes, value, expected_revision)?;
inspect_harness_interop_settings(homes, harness)
}