use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::subagents::{QueuedApproval, QueuedApprovalOutcome};
use crate::{HarnessEvent, HarnessId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalKind {
Live,
Stored,
Proposal,
}
impl ApprovalKind {
pub const fn as_str(self) -> &'static str {
match self {
Self::Live => "live",
Self::Stored => "stored",
Self::Proposal => "proposal",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalStatus {
Pending,
Allowed,
Denied,
Expired,
Cancelled,
}
impl ApprovalStatus {
pub const fn as_str(self) -> &'static str {
match self {
Self::Pending => "pending",
Self::Allowed => "allowed",
Self::Denied => "denied",
Self::Expired => "expired",
Self::Cancelled => "cancelled",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovalOption {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
}
impl ApprovalOption {
fn bare(id: &str) -> Self {
Self {
id: id.to_string(),
label: None,
kind: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApprovalRow {
pub id: String,
pub harness: HarnessId,
pub kind: ApprovalKind,
pub status: ApprovalStatus,
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_id: Option<String>,
pub requested_at_ms: i64,
pub age_ms: i64,
#[serde(default)]
pub options: Vec<ApprovalOption>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ApprovalsQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub harness: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub session: Option<String>,
}
impl ApprovalsQuery {
pub fn matches(&self, row: &ApprovalRow) -> bool {
if let Some(harness) = self.harness.as_deref() {
if row.harness.as_str() != harness {
return false;
}
}
if let Some(session) = self.session.as_deref() {
let hit = row.session_id.as_deref() == Some(session)
|| row.runtime_id.as_deref() == Some(session);
if !hit {
return false;
}
}
true
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalDecision {
AllowOnce,
AllowAlways,
Deny,
}
impl ApprovalDecision {
pub const ALL: [Self; 3] = [Self::AllowOnce, Self::AllowAlways, Self::Deny];
pub const fn as_str(self) -> &'static str {
match self {
Self::AllowOnce => "allow_once",
Self::AllowAlways => "allow_always",
Self::Deny => "deny",
}
}
pub fn parse(text: &str) -> Option<Self> {
let normalized = text.trim().to_ascii_lowercase().replace('-', "_");
Self::ALL
.into_iter()
.find(|decision| decision.as_str() == normalized)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct ApprovalsResolveParams {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub decision: Option<ApprovalDecision>,
#[serde(skip_serializing_if = "Option::is_none")]
pub option_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApprovalChoice {
Decision(ApprovalDecision),
Option(String),
}
impl ApprovalChoice {
pub fn asked(&self) -> &str {
match self {
Self::Decision(decision) => decision.as_str(),
Self::Option(option) => option.as_str(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApprovalResolution {
pub connection: String,
pub request_id: Value,
pub option_id: String,
pub response: Value,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApprovalResolveError {
UnknownId(String),
QueuedSubagentRow(String),
NotOffered {
asked: String,
offered: Vec<String>,
},
NoOptions {
door: &'static str,
},
}
impl std::fmt::Display for ApprovalResolveError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownId(id) => write!(
formatter,
"no approval request `{id}` is waiting on this service — a live request \
exists only inside the process driving the runtime whose turn it blocks, \
and only until it is answered. Answer it there: the SDK client or TUI that \
started the runtime, or `harness.v1.approvals.resolve` over that same \
`supercode harness serve` stdio session (SUP-62: no cross-process relay)"
),
Self::QueuedSubagentRow(id) => write!(
formatter,
"`{id}` is a queued subagent record — supercode's own audit trail of a \
request the parent's own handler answers (the terminal's modal, or the \
frontend request broker). Answer it on the door that raised it: the \
`request` envelope row of the joined supercode runtime"
),
Self::NotOffered { asked, offered } => write!(
formatter,
"this request does not offer `{asked}` — it offers: {}",
if offered.is_empty() {
"(nothing)".to_string()
} else {
offered.join(", ")
}
),
Self::NoOptions { door } => write!(
formatter,
"this `{door}` request enumerates no answers, so there is no option to \
select — answer it with `harness.v1.runtimes.respond` and that door's own \
reply body"
),
}
}
}
impl std::error::Error for ApprovalResolveError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalDoor {
Acp,
Opencode,
Codex,
ClaudeCode,
SupercodeFrontend,
}
impl ApprovalDoor {
pub const fn as_str(self) -> &'static str {
match self {
Self::Acp => "acp",
Self::Opencode => "opencode",
Self::Codex => "codex",
Self::ClaudeCode => "claude-code",
Self::SupercodeFrontend => "supercode-frontend",
}
}
const fn spellings(self, decision: ApprovalDecision) -> &'static [&'static str] {
match (self, decision) {
(Self::Acp, ApprovalDecision::AllowOnce) => &["allow_once"],
(Self::Acp, ApprovalDecision::AllowAlways) => &["allow_always"],
(Self::Acp, ApprovalDecision::Deny) => &["reject_once", "reject_always"],
(Self::Opencode, ApprovalDecision::AllowOnce) => &["once"],
(Self::Opencode, ApprovalDecision::AllowAlways) => &["always"],
(Self::Opencode, ApprovalDecision::Deny) => &["reject"],
(Self::ClaudeCode, ApprovalDecision::AllowOnce) => &["allow"],
(Self::ClaudeCode, ApprovalDecision::AllowAlways) => &[],
(Self::ClaudeCode, ApprovalDecision::Deny) => &["deny"],
(Self::SupercodeFrontend, ApprovalDecision::AllowOnce) => &["allow"],
(Self::SupercodeFrontend, ApprovalDecision::AllowAlways) => &["allow_for_session"],
(Self::SupercodeFrontend, ApprovalDecision::Deny) => &["deny"],
(Self::Codex, _) => &[],
}
}
fn reply(self, option_id: &str) -> Option<Value> {
match self {
Self::Acp => Some(serde_json::json!({
"outcome": {"outcome": "selected", "optionId": option_id},
})),
Self::Opencode => Some(serde_json::json!({"response": option_id})),
Self::SupercodeFrontend => Some(serde_json::json!({"decision": option_id})),
Self::ClaudeCode if option_id == "deny" => Some(serde_json::json!({
"behavior": "deny",
"message": "denied through supercode approvals",
})),
Self::ClaudeCode => Some(serde_json::json!({"behavior": option_id})),
Self::Codex => None,
}
}
}
pub fn plan_reply(
door: ApprovalDoor,
options: &[ApprovalOption],
choice: &ApprovalChoice,
) -> Result<(String, Value), ApprovalResolveError> {
if options.is_empty() {
return Err(ApprovalResolveError::NoOptions {
door: door.as_str(),
});
}
let offered = || {
options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>()
};
let chosen = match choice {
ApprovalChoice::Option(option_id) => options
.iter()
.find(|option| &option.id == option_id)
.ok_or_else(|| ApprovalResolveError::NotOffered {
asked: option_id.clone(),
offered: offered(),
})?,
ApprovalChoice::Decision(decision) => door
.spellings(*decision)
.iter()
.find_map(|spelling| {
options.iter().find(|option| {
option.kind.as_deref() == Some(*spelling) || option.id == *spelling
})
})
.ok_or_else(|| ApprovalResolveError::NotOffered {
asked: decision.as_str().to_string(),
offered: offered(),
})?,
};
let response = door
.reply(&chosen.id)
.ok_or(ApprovalResolveError::NoOptions {
door: door.as_str(),
})?;
Ok((chosen.id.clone(), response))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiveRequest {
pub request_id: Value,
pub door: ApprovalDoor,
pub session_id: Option<String>,
pub subject: String,
pub options: Vec<ApprovalOption>,
}
pub fn classify_live_request(kind: &str, payload: &Value) -> Option<LiveRequest> {
match kind {
"session/request_permission" => acp_request(payload),
"permission.asked" => opencode_request(payload),
"request" => supercode_request(payload),
"control_request" => claude_code_request(payload),
_ if kind.ends_with("Approval") => codex_request(payload),
_ => None,
}
}
fn request_id(payload: &Value) -> Option<Value> {
payload
.get("id")
.filter(|id| !id.is_null())
.filter(|id| id.is_string() || id.is_number())
.cloned()
}
fn acp_request(payload: &Value) -> Option<LiveRequest> {
let request_id = request_id(payload)?;
let params = payload.get("params").unwrap_or(&Value::Null);
let tool_call = params.get("toolCall");
let subject = tool_call
.and_then(|call| call.get("title"))
.and_then(Value::as_str)
.map(str::to_string)
.or_else(|| {
tool_call
.and_then(|call| call.get("rawInput"))
.and_then(command_line)
})
.or_else(|| {
tool_call
.and_then(|call| call.get("kind"))
.and_then(Value::as_str)
.map(str::to_string)
})
.unwrap_or_else(|| "permission request".to_string());
let options = params
.get("options")
.and_then(Value::as_array)
.map(|options| {
options
.iter()
.filter_map(|option| {
Some(ApprovalOption {
id: option.get("optionId").and_then(Value::as_str)?.to_string(),
label: option
.get("name")
.and_then(Value::as_str)
.map(str::to_string),
kind: option
.get("kind")
.and_then(Value::as_str)
.map(str::to_string),
})
})
.collect()
})
.unwrap_or_default();
Some(LiveRequest {
request_id,
door: ApprovalDoor::Acp,
session_id: params
.get("sessionId")
.and_then(Value::as_str)
.map(str::to_string),
subject: one_line(&subject),
options,
})
}
fn opencode_request(payload: &Value) -> Option<LiveRequest> {
let properties = payload.get("properties").unwrap_or(payload);
let permission = properties
.get("permission")
.filter(|value| value.is_object())
.unwrap_or(properties);
let id = permission.get("id").and_then(Value::as_str)?;
let subject = permission
.get("title")
.and_then(Value::as_str)
.or_else(|| permission.get("pattern").and_then(Value::as_str))
.or_else(|| permission.get("type").and_then(Value::as_str))
.unwrap_or("permission request");
Some(LiveRequest {
request_id: Value::String(id.to_string()),
door: ApprovalDoor::Opencode,
session_id: permission
.get("sessionID")
.and_then(Value::as_str)
.map(str::to_string),
subject: one_line(subject),
options: ["once", "always", "reject"]
.into_iter()
.map(ApprovalOption::bare)
.collect(),
})
}
fn supercode_request(payload: &Value) -> Option<LiveRequest> {
let request = payload.get("request")?;
if request.get("kind").and_then(Value::as_str) != Some("approval") {
return None;
}
let request_id = request
.get("id")
.filter(|id| id.is_number())
.cloned()
.filter(|id| !id.is_null())?;
let inner = request.get("payload").unwrap_or(&Value::Null);
let subject = inner
.get("subject")
.and_then(Value::as_str)
.filter(|subject| !subject.is_empty())
.or_else(|| inner.get("tool").and_then(Value::as_str))
.unwrap_or("permission request");
Some(LiveRequest {
request_id,
door: ApprovalDoor::SupercodeFrontend,
session_id: inner
.get("child_agent_id")
.and_then(Value::as_str)
.map(str::to_string),
subject: one_line(subject),
options: FRONTEND_DECISIONS
.into_iter()
.map(ApprovalOption::bare)
.collect(),
})
}
fn claude_code_request(payload: &Value) -> Option<LiveRequest> {
let request = payload.get("request")?;
if request.get("subtype").and_then(Value::as_str) != Some("can_use_tool") {
return None;
}
let request_id = payload
.get("request_id")
.filter(|id| id.is_string())
.cloned()?;
let tool = request
.get("tool_name")
.and_then(Value::as_str)
.or_else(|| request.get("display_name").and_then(Value::as_str))
.unwrap_or("tool");
let detail = request
.get("input")
.and_then(command_line)
.or_else(|| {
request
.get("description")
.and_then(Value::as_str)
.map(str::to_string)
})
.or_else(|| {
request
.get("blocked_path")
.and_then(Value::as_str)
.map(str::to_string)
});
let subject = match detail {
Some(detail) if !detail.is_empty() => format!("{tool} {detail}"),
_ => tool.to_string(),
};
Some(LiveRequest {
request_id,
door: ApprovalDoor::ClaudeCode,
session_id: None,
subject: one_line(&subject),
options: CLAUDE_CODE_BEHAVIORS
.into_iter()
.map(ApprovalOption::bare)
.collect(),
})
}
fn codex_request(payload: &Value) -> Option<LiveRequest> {
let request_id = request_id(payload)?;
let params = payload.get("params").unwrap_or(&Value::Null);
let subject = params
.get("command")
.and_then(command_line)
.or_else(|| {
params
.get("fileChanges")
.and_then(Value::as_object)
.map(|changes| {
let files = changes.keys().cloned().collect::<Vec<_>>().join(", ");
if files.is_empty() {
"apply patch".to_string()
} else {
format!("apply patch: {files}")
}
})
})
.or_else(|| {
params
.get("reason")
.and_then(Value::as_str)
.map(str::to_string)
})
.unwrap_or_else(|| "approval request".to_string());
Some(LiveRequest {
request_id,
door: ApprovalDoor::Codex,
session_id: ["threadId", "conversationId", "sessionId"]
.into_iter()
.find_map(|key| params.get(key).and_then(Value::as_str))
.map(str::to_string),
subject: one_line(&subject),
options: Vec::new(),
})
}
fn command_line(value: &Value) -> Option<String> {
match value {
Value::String(text) => Some(text.clone()),
Value::Array(parts) => {
let joined = parts
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(" ");
(!joined.is_empty()).then_some(joined)
}
Value::Object(object) => object
.get("command")
.or_else(|| object.get("cmd"))
.and_then(command_line),
_ => None,
}
}
fn one_line(text: &str) -> String {
let flattened = text.split_whitespace().collect::<Vec<_>>().join(" ");
if flattened.is_empty() {
"permission request".to_string()
} else {
flattened
}
}
fn id_segment(request_id: &Value) -> String {
match request_id {
Value::String(text) => text.clone(),
other => other.to_string(),
}
}
#[derive(Debug, Clone)]
struct LiveEntry {
request_id: Value,
door: ApprovalDoor,
harness: HarnessId,
runtime_id: String,
session_id: Option<String>,
subject: String,
options: Vec<ApprovalOption>,
requested_at_ms: i64,
}
#[derive(Debug, Default)]
pub struct ApprovalRegistry {
entries: BTreeMap<String, Vec<LiveEntry>>,
}
impl ApprovalRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn observe(
&mut self,
connection: &str,
harness: &HarnessId,
runtime_id: &str,
event: &HarnessEvent,
now_ms: i64,
) -> bool {
let Some(request) = classify_live_request(&event.kind, &event.payload) else {
return false;
};
let entries = self.entries.entry(connection.to_string()).or_default();
if entries
.iter()
.any(|entry| entry.request_id == request.request_id)
{
return false;
}
entries.push(LiveEntry {
request_id: request.request_id,
door: request.door,
harness: harness.clone(),
runtime_id: runtime_id.to_string(),
session_id: request.session_id,
subject: request.subject,
options: request.options,
requested_at_ms: now_ms,
});
true
}
pub fn answered(&mut self, connection: &str, request_id: &Value) -> bool {
let Some(entries) = self.entries.get_mut(connection) else {
return false;
};
let before = entries.len();
entries.retain(|entry| &entry.request_id != request_id);
let removed = entries.len() < before;
if entries.is_empty() {
self.entries.remove(connection);
}
removed
}
pub fn forget(&mut self, connection: &str) {
self.entries.remove(connection);
}
pub fn is_empty(&self) -> bool {
self.entries.values().all(Vec::is_empty)
}
pub fn resolution(
&self,
row_id: &str,
choice: &ApprovalChoice,
) -> Result<ApprovalResolution, ApprovalResolveError> {
if row_id.starts_with(SUBAGENT_ROW_PREFIX) {
return Err(ApprovalResolveError::QueuedSubagentRow(row_id.to_string()));
}
let (connection, entry) = self
.entries
.iter()
.flat_map(|(connection, entries)| entries.iter().map(move |entry| (connection, entry)))
.find(|(connection, entry)| {
format!("{connection}/{}", id_segment(&entry.request_id)) == row_id
})
.ok_or_else(|| ApprovalResolveError::UnknownId(row_id.to_string()))?;
let (option_id, response) = plan_reply(entry.door, &entry.options, choice)?;
Ok(ApprovalResolution {
connection: connection.clone(),
request_id: entry.request_id.clone(),
option_id,
response,
})
}
pub fn rows(&self, now_ms: i64) -> Vec<ApprovalRow> {
self.entries
.iter()
.flat_map(|(connection, entries)| {
entries.iter().map(move |entry| ApprovalRow {
id: format!("{connection}/{}", id_segment(&entry.request_id)),
harness: entry.harness.clone(),
kind: ApprovalKind::Live,
status: ApprovalStatus::Pending,
subject: entry.subject.clone(),
session_id: entry.session_id.clone(),
runtime_id: Some(entry.runtime_id.clone()),
requested_at_ms: entry.requested_at_ms,
age_ms: now_ms.saturating_sub(entry.requested_at_ms).max(0),
options: entry.options.clone(),
})
})
.collect()
}
}
const FRONTEND_DECISIONS: [&str; 3] = ["allow", "allow_for_session", "deny"];
const CLAUDE_CODE_BEHAVIORS: [&str; 2] = ["allow", "deny"];
const SUBAGENT_ROW_PREFIX: &str = "supercode/subagent/";
pub fn subagent_rows(queued: &[QueuedApproval], now_ms: i64) -> Vec<ApprovalRow> {
queued
.iter()
.enumerate()
.map(|(index, record)| {
let status = match record.outcome {
None => ApprovalStatus::Pending,
Some(QueuedApprovalOutcome::Allowed) => ApprovalStatus::Allowed,
Some(QueuedApprovalOutcome::Denied) => ApprovalStatus::Denied,
};
let subject = match record.subject.as_deref() {
Some(subject) if !subject.is_empty() => {
format!("{} {}", record.tool, subject)
}
_ => record.tool.clone(),
};
ApprovalRow {
id: format!(
"{SUBAGENT_ROW_PREFIX}{}/{}/{index}",
record.child_agent_id, record.queued_at_ms
),
harness: HarnessId::from(HarnessId::SUPERCODE),
kind: ApprovalKind::Live,
status,
subject: one_line(&subject),
session_id: Some(record.child_agent_id.clone()),
runtime_id: None,
requested_at_ms: record.queued_at_ms,
age_ms: now_ms.saturating_sub(record.queued_at_ms).max(0),
options: if status == ApprovalStatus::Pending {
FRONTEND_DECISIONS
.into_iter()
.map(ApprovalOption::bare)
.collect()
} else {
Vec::new()
},
}
})
.collect()
}
pub fn lists_approvals(harness: &str) -> bool {
crate::support::harness_support(harness)
.is_some_and(|descriptor| descriptor.runtime.capabilities.respond_to_requests)
}
pub fn approval_harnesses() -> Vec<String> {
crate::support::harness_support_registry()
.harnesses
.into_iter()
.filter(|descriptor| descriptor.runtime.capabilities.respond_to_requests)
.map(|descriptor| descriptor.id.as_str().to_string())
.collect()
}
pub fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_millis() as i64)
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn event(kind: &str, payload: Value) -> HarnessEvent {
HarnessEvent {
sequence: None,
kind: kind.to_string(),
payload,
}
}
fn acp_permission(id: u64, title: &str) -> HarnessEvent {
event(
"session/request_permission",
json!({
"jsonrpc": "2.0",
"id": id,
"method": "session/request_permission",
"params": {
"sessionId": "acp-session",
"toolCall": {"toolCallId": "call-1", "title": title, "kind": "execute"},
"options": [
{"optionId": "allow_once", "name": "Allow once", "kind": "allow_once"},
{"optionId": "deny", "name": "Deny", "kind": "reject_once"},
],
},
}),
)
}
#[test]
fn acp_permission_requests_carry_subject_session_and_option_ids() {
let request = classify_live_request(
"session/request_permission",
&acp_permission(7, "rm -rf build").payload,
)
.expect("an ACP permission request is recognized");
assert_eq!(request.request_id, json!(7));
assert_eq!(request.session_id.as_deref(), Some("acp-session"));
assert_eq!(request.subject, "rm -rf build");
assert_eq!(
request
.options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<_>>(),
vec!["allow_once", "deny"],
);
}
#[test]
fn opencode_and_codex_requests_use_their_own_protocol_spellings() {
let opencode = classify_live_request(
"permission.asked",
&json!({
"type": "permission.asked",
"properties": {
"id": "perm-9",
"sessionID": "oc-session",
"title": "git push origin main",
},
}),
)
.expect("an opencode permission ask is recognized");
assert_eq!(opencode.request_id, json!("perm-9"));
assert_eq!(opencode.session_id.as_deref(), Some("oc-session"));
assert_eq!(opencode.subject, "git push origin main");
assert_eq!(
opencode
.options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<_>>(),
vec!["once", "always", "reject"],
);
let codex = classify_live_request(
"execCommandApproval",
&json!({
"jsonrpc": "2.0",
"id": "req-3",
"method": "execCommandApproval",
"params": {"threadId": "cx-thread", "command": ["cargo", "test"]},
}),
)
.expect("a Codex approval reverse request is recognized");
assert_eq!(codex.subject, "cargo test");
assert_eq!(codex.session_id.as_deref(), Some("cx-thread"));
assert!(codex.options.is_empty());
}
#[test]
fn a_joined_supercode_runtime_publishes_its_own_request_envelope() {
let request = classify_live_request(
"request",
&json!({
"type": "request",
"request": {
"id": 4,
"kind": "approval",
"payload": {
"tool": "shell",
"subject": "cargo publish --dry-run",
"child_agent_id": "child-2",
},
},
}),
)
.expect("supercode's own frontend request is recognized");
assert_eq!(request.request_id, json!(4));
assert_eq!(request.subject, "cargo publish --dry-run");
assert_eq!(request.session_id.as_deref(), Some("child-2"));
assert_eq!(
request
.options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<_>>(),
vec!["allow", "allow_for_session", "deny"],
);
assert!(classify_live_request(
"request",
&json!({"type": "request", "request": {"id": 5, "kind": "elicitation", "payload": {}}})
)
.is_none());
}
fn claude_can_use_tool() -> Value {
json!({
"type": "control_request",
"request_id": "053f8a2d-3445-4011-a259-4261b31c7326",
"request": {
"subtype": "can_use_tool",
"tool_name": "Bash",
"display_name": "Bash",
"input": {"command": "touch probe-artifact.txt", "description": "probe"},
"description": "probe",
"permission_suggestions": [{
"type": "addRules",
"rules": [{"toolName": "Bash", "ruleContent": "touch probe-artifact.txt"}],
"behavior": "allow",
"destination": "localSettings",
}],
"blocked_path": "/tmp/work/probe-artifact.txt",
"tool_use_id": "toolu_mock_1",
},
})
}
#[test]
fn claude_code_can_use_tool_is_a_live_request_with_the_protocols_two_behaviors() {
let request = classify_live_request("control_request", &claude_can_use_tool())
.expect("a can_use_tool control request is a permission request");
assert_eq!(request.door, ApprovalDoor::ClaudeCode);
assert_eq!(
request.request_id,
json!("053f8a2d-3445-4011-a259-4261b31c7326")
);
assert_eq!(request.subject, "Bash touch probe-artifact.txt");
assert_eq!(
request
.options
.iter()
.map(|option| option.id.as_str())
.collect::<Vec<_>>(),
vec!["allow", "deny"],
);
assert!(classify_live_request(
"control_request",
&json!({"type":"control_request","request_id":"x","request":{"subtype":"hook_callback"}})
)
.is_none());
assert!(classify_live_request(
"control_response",
&json!({"type":"control_response","response":{"subtype":"success"}})
)
.is_none());
}
#[test]
fn claude_code_decisions_translate_onto_the_permission_result_the_cli_accepts() {
let request = classify_live_request("control_request", &claude_can_use_tool()).unwrap();
let (option, reply) = plan_reply(
request.door,
&request.options,
&ApprovalChoice::Decision(ApprovalDecision::AllowOnce),
)
.unwrap();
assert_eq!(option, "allow");
assert_eq!(reply, json!({"behavior": "allow"}));
let (option, reply) = plan_reply(
request.door,
&request.options,
&ApprovalChoice::Decision(ApprovalDecision::Deny),
)
.unwrap();
assert_eq!(option, "deny");
assert_eq!(reply["behavior"], "deny");
assert!(reply["message"].as_str().is_some_and(|m| !m.is_empty()));
let error = plan_reply(
request.door,
&request.options,
&ApprovalChoice::Decision(ApprovalDecision::AllowAlways),
)
.unwrap_err();
assert_eq!(
error,
ApprovalResolveError::NotOffered {
asked: "allow_always".into(),
offered: vec!["allow".into(), "deny".into()],
}
);
}
#[test]
fn ordinary_events_and_id_less_notifications_are_not_approvals() {
assert!(classify_live_request(
"session/update",
&json!({"method": "session/update", "params": {}})
)
.is_none());
assert!(classify_live_request(
"execCommandApproval",
&json!({"method": "execCommandApproval", "params": {}})
)
.is_none());
}
#[test]
fn a_recorded_request_lists_once_and_leaves_when_answered() {
let mut registry = ApprovalRegistry::new();
let harness = HarnessId::from(HarnessId::HERMES);
let event = acp_permission(7, "rm -rf build");
assert!(registry.observe("runtime-1", &harness, "acp-session", &event, 1_000));
assert!(!registry.observe("runtime-1", &harness, "acp-session", &event, 2_000));
let rows = registry.rows(1_500);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].id, "runtime-1/7");
assert_eq!(rows[0].harness.as_str(), HarnessId::HERMES);
assert_eq!(rows[0].kind, ApprovalKind::Live);
assert_eq!(rows[0].status, ApprovalStatus::Pending);
assert_eq!(rows[0].age_ms, 500);
assert!(registry.answered("runtime-1", &json!(7)));
assert!(registry.is_empty());
assert!(!registry.answered("runtime-1", &json!(7)));
}
#[test]
fn a_closed_connection_takes_its_requests_with_it() {
let mut registry = ApprovalRegistry::new();
let harness = HarnessId::from(HarnessId::OPENCLAW);
registry.observe(
"runtime-2",
&harness,
"acp-session",
&acp_permission(1, "write src/main.rs"),
10,
);
registry.forget("runtime-2");
assert!(registry.rows(20).is_empty());
}
#[test]
fn queued_subagent_rows_report_the_outcome_the_record_holds() {
let queued = vec![
QueuedApproval {
child_agent_id: "child-1".into(),
tool: "shell".into(),
subject: Some("git push".into()),
queued_at_ms: 100,
outcome: None,
},
QueuedApproval {
child_agent_id: "child-2".into(),
tool: "write_file".into(),
subject: None,
queued_at_ms: 200,
outcome: Some(QueuedApprovalOutcome::Denied),
},
];
let rows = subagent_rows(&queued, 500);
assert_eq!(rows[0].id, "supercode/subagent/child-1/100/0");
assert_eq!(rows[0].harness.as_str(), HarnessId::SUPERCODE);
assert_eq!(rows[0].status, ApprovalStatus::Pending);
assert_eq!(rows[0].subject, "shell git push");
assert_eq!(rows[0].age_ms, 400);
assert_eq!(rows[0].options.len(), 3);
assert_eq!(rows[1].status, ApprovalStatus::Denied);
assert_eq!(rows[1].subject, "write_file");
assert!(rows[1].options.is_empty());
}
#[test]
fn only_harnesses_whose_runtime_can_answer_are_listed() {
assert!(lists_approvals(HarnessId::HERMES));
assert!(lists_approvals(HarnessId::OPENCLAW));
assert!(lists_approvals(HarnessId::CODEX));
assert!(lists_approvals(HarnessId::OPENCODE));
assert!(lists_approvals(HarnessId::CLAUDE_CODE));
assert!(!lists_approvals("notaharness"));
let harnesses = approval_harnesses();
assert!(harnesses.iter().any(|id| id == HarnessId::HERMES));
assert!(harnesses.iter().any(|id| id == HarnessId::CLAUDE_CODE));
}
#[test]
fn each_door_spells_a_uniform_decision_in_its_own_vocabulary() {
let acp = vec![
ApprovalOption {
id: "proceed-once".into(),
label: Some("Allow once".into()),
kind: Some("allow_once".into()),
},
ApprovalOption {
id: "refuse".into(),
label: Some("Deny".into()),
kind: Some("reject_once".into()),
},
];
let (option, response) = plan_reply(
ApprovalDoor::Acp,
&acp,
&ApprovalChoice::Decision(ApprovalDecision::AllowOnce),
)
.expect("the request offers an allow-once option");
assert_eq!(option, "proceed-once");
assert_eq!(
response,
json!({"outcome": {"outcome": "selected", "optionId": "proceed-once"}}),
);
let (option, _) = plan_reply(
ApprovalDoor::Acp,
&acp,
&ApprovalChoice::Decision(ApprovalDecision::Deny),
)
.expect("`reject_once` is how ACP spells deny");
assert_eq!(option, "refuse");
let opencode = ["once", "always", "reject"]
.map(ApprovalOption::bare)
.to_vec();
let (option, response) = plan_reply(
ApprovalDoor::Opencode,
&opencode,
&ApprovalChoice::Decision(ApprovalDecision::AllowAlways),
)
.expect("opencode offers `always`");
assert_eq!(option, "always");
assert_eq!(response, json!({"response": "always"}));
let frontend = FRONTEND_DECISIONS.map(ApprovalOption::bare).to_vec();
let (option, response) = plan_reply(
ApprovalDoor::SupercodeFrontend,
&frontend,
&ApprovalChoice::Decision(ApprovalDecision::AllowAlways),
)
.expect("the frontend offers `allow_for_session`");
assert_eq!(option, "allow_for_session");
assert_eq!(response, json!({"decision": "allow_for_session"}));
}
#[test]
fn a_decision_the_request_does_not_offer_names_the_ones_it_does() {
let options = vec![
ApprovalOption {
id: "allow_once".into(),
label: None,
kind: Some("allow_once".into()),
},
ApprovalOption {
id: "deny".into(),
label: None,
kind: Some("reject_once".into()),
},
];
let error = plan_reply(
ApprovalDoor::Acp,
&options,
&ApprovalChoice::Decision(ApprovalDecision::AllowAlways),
)
.expect_err("this request has no allow-always option");
assert_eq!(
error,
ApprovalResolveError::NotOffered {
asked: "allow_always".into(),
offered: vec!["allow_once".into(), "deny".into()],
},
);
let message = error.to_string();
assert!(message.contains("allow_always"), "{message}");
assert!(message.contains("allow_once, deny"), "{message}");
let error = plan_reply(
ApprovalDoor::Acp,
&options,
&ApprovalChoice::Option("allow_always".into()),
)
.expect_err("an unoffered token is not passed through");
assert!(matches!(error, ApprovalResolveError::NotOffered { .. }));
let (option, _) = plan_reply(
ApprovalDoor::Acp,
&options,
&ApprovalChoice::Option("deny".into()),
)
.expect("`deny` is offered");
assert_eq!(option, "deny");
}
#[test]
fn a_door_that_enumerates_nothing_is_refused_rather_than_guessed_at() {
let error = plan_reply(
ApprovalDoor::Codex,
&[],
&ApprovalChoice::Decision(ApprovalDecision::AllowOnce),
)
.expect_err("nothing to select");
assert_eq!(error, ApprovalResolveError::NoOptions { door: "codex" });
assert!(
error.to_string().contains("harness.v1.runtimes.respond"),
"{error}"
);
}
#[test]
fn the_registry_plans_an_answer_for_the_row_id_it_published() {
let mut registry = ApprovalRegistry::new();
let harness = HarnessId::from(HarnessId::HERMES);
registry.observe(
"runtime-1",
&harness,
"acp-session",
&acp_permission(7, "rm -rf build"),
1_000,
);
let row_id = registry.rows(1_000)[0].id.clone();
assert_eq!(row_id, "runtime-1/7");
let resolution = registry
.resolution(
&row_id,
&ApprovalChoice::Decision(ApprovalDecision::AllowOnce),
)
.expect("the listed row plans an answer");
assert_eq!(resolution.connection, "runtime-1");
assert_eq!(resolution.request_id, json!(7));
assert_eq!(resolution.option_id, "allow_once");
assert_eq!(
resolution.response,
json!({"outcome": {"outcome": "selected", "optionId": "allow_once"}}),
);
assert_eq!(registry.rows(1_000).len(), 1);
let error = registry
.resolution(
"runtime-1/999",
&ApprovalChoice::Decision(ApprovalDecision::Deny),
)
.expect_err("no such row");
assert_eq!(
error,
ApprovalResolveError::UnknownId("runtime-1/999".into())
);
let error = registry
.resolution(
"supercode/subagent/child-1/100/0",
&ApprovalChoice::Decision(ApprovalDecision::AllowOnce),
)
.expect_err("an audit record is not a door");
assert!(matches!(
error,
ApprovalResolveError::QueuedSubagentRow(ref id)
if id == "supercode/subagent/child-1/100/0"
));
assert!(error.to_string().contains("audit trail"), "{error}");
}
#[test]
fn a_decision_parses_from_both_the_wire_and_the_cli_spelling() {
assert_eq!(
ApprovalDecision::parse("allow-once"),
Some(ApprovalDecision::AllowOnce)
);
assert_eq!(
ApprovalDecision::parse("ALLOW_ALWAYS"),
Some(ApprovalDecision::AllowAlways)
);
assert_eq!(
ApprovalDecision::parse("deny"),
Some(ApprovalDecision::Deny)
);
assert_eq!(ApprovalDecision::parse("maybe"), None);
assert_eq!(
serde_json::to_value(ApprovalDecision::AllowAlways).unwrap(),
json!("allow_always"),
);
}
#[test]
fn a_query_filters_by_harness_and_by_session() {
let rows = subagent_rows(
&[QueuedApproval {
child_agent_id: "child-1".into(),
tool: "shell".into(),
subject: None,
queued_at_ms: 1,
outcome: None,
}],
2,
);
let query = ApprovalsQuery {
harness: Some(HarnessId::SUPERCODE.into()),
session: Some("child-1".into()),
};
assert!(query.matches(&rows[0]));
let other = ApprovalsQuery {
harness: Some(HarnessId::HERMES.into()),
session: None,
};
assert!(!other.matches(&rows[0]));
}
}