use crate::core::docs::{DocFragment, Mandate};
use serde::{Deserialize, Serialize};
use sha2::Digest;
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RpcRequest {
pub op: String,
#[serde(default)]
pub params: serde_json::Value,
#[serde(default = "default_request_id")]
pub id: String,
#[serde(default)]
pub session: Option<String>,
}
pub fn default_request_id() -> String {
ulid::Ulid::new().to_string()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RpcResponse {
pub id: String,
pub success: bool,
pub receipt: Receipt,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub mandates: Vec<Mandate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context_capsule: Option<ContextCapsule>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<serde_json::Value>,
pub allowed_next_ops: Vec<AllowedOp>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub blocked_by: Vec<Blocker>,
#[serde(skip_serializing_if = "Option::is_none")]
pub interlock: Option<Interlock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub advisory: Option<Advisory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attestation: Option<Attestation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RpcError>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Receipt {
pub op: String,
pub timestamp: String,
pub inputs_hash: String,
pub outputs_hash: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub touched_paths: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub governing_anchors: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ContextCapsule {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub fragments: Vec<DocFragment>,
#[serde(skip_serializing_if = "Option::is_none")]
pub spec: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub architecture: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub security: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub standards: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AllowedOp {
pub op: String,
pub reason: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub required_params: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Blocker {
pub kind: BlockerKind,
pub message: String,
pub resolve_hint: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BlockerKind {
MissingAnswer,
MissingProof,
Unauthorized,
Conflict,
ValidationFailed,
WorkspaceRequired,
ProtectedBranch,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RpcError {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CapabilitiesReport {
pub version: String,
pub capabilities: Vec<Capability>,
pub subsystems: Vec<SubsystemInfo>,
pub workspace: WorkspaceCapabilities,
pub interview: InterviewCapabilities,
pub interlock_codes: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Capability {
pub name: String,
pub description: String,
pub stability: String,
pub cost: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SubsystemInfo {
pub name: String,
pub status: String,
pub ops: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WorkspaceCapabilities {
pub enforcement_available: bool,
pub docker_available: bool,
pub protected_patterns: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InterviewCapabilities {
pub available: bool,
pub artifact_types: Vec<String>,
pub standards_resolution: bool,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Interlock {
pub code: String,
pub message: String,
pub unblock_ops: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvidenceRef {
pub source: String,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub hash: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ReconciliationPointer {
pub kind: String,
pub r#ref: String,
pub title: String,
pub why_short: String,
pub evidence: EvidenceRef,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ReconciliationSets {
pub must: Vec<ReconciliationPointer>,
pub recommended: Vec<ReconciliationPointer>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct VerificationPlan {
pub required: Vec<String>,
pub checklist: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct LoopSignal {
pub code: String,
pub message: String,
pub suggested_redirect_ops: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Advisory {
pub reconciliations: ReconciliationSets,
pub verification_plan: VerificationPlan,
#[serde(skip_serializing_if = "Option::is_none")]
pub loop_signal: Option<LoopSignal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Attestation {
pub id: String,
pub op: String,
pub timestamp: String,
pub input_hash: String,
pub touched_paths: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub interlock_code: Option<String>,
pub outcome: String,
pub trace_path: String,
}
pub fn generate_capabilities() -> CapabilitiesReport {
CapabilitiesReport {
version: env!("CARGO_PKG_VERSION").to_string(),
capabilities: vec![
Capability {
name: "daemonless".to_string(),
description: "Decapod never runs in the background; it is invoked by agents"
.to_string(),
stability: "stable".to_string(),
cost: "none".to_string(),
},
Capability {
name: "deterministic".to_string(),
description: "Same inputs produce identical outputs given fixed repo state"
.to_string(),
stability: "stable".to_string(),
cost: "none".to_string(),
},
Capability {
name: "context.resolve".to_string(),
description: "Resolve relevant constitution/authority fragments for an operation"
.to_string(),
stability: "stable".to_string(),
cost: "low".to_string(),
},
Capability {
name: "context.scope".to_string(),
description:
"Return scoped, query-matched constitution fragments for just-in-time context"
.to_string(),
stability: "stable".to_string(),
cost: "low".to_string(),
},
Capability {
name: "context.capsule.query".to_string(),
description:
"Return deterministic context capsules scoped to core/interfaces/plugins docs"
.to_string(),
stability: "stable".to_string(),
cost: "low".to_string(),
},
Capability {
name: "schema.get".to_string(),
description: "Get authoritative JSON schemas for entities".to_string(),
stability: "stable".to_string(),
cost: "low".to_string(),
},
Capability {
name: "store.upsert".to_string(),
description: "Deterministic storage for decisions/knowledge/todos".to_string(),
stability: "stable".to_string(),
cost: "medium".to_string(),
},
Capability {
name: "store.query".to_string(),
description: "Retrieve canonical entities deterministically".to_string(),
stability: "stable".to_string(),
cost: "medium".to_string(),
},
Capability {
name: "validate.run".to_string(),
description: "Run deterministic validation gates".to_string(),
stability: "stable".to_string(),
cost: "medium".to_string(),
},
Capability {
name: "workspace.ensure".to_string(),
description: "Create or enter an isolated agent workspace".to_string(),
stability: "stable".to_string(),
cost: "low".to_string(),
},
Capability {
name: "workspace.status".to_string(),
description: "Check current workspace and branch status".to_string(),
stability: "stable".to_string(),
cost: "low".to_string(),
},
Capability {
name: "eval.gate".to_string(),
description: "Run variance-aware statistical promotion gate over eval aggregates"
.to_string(),
stability: "beta".to_string(),
cost: "medium".to_string(),
},
],
subsystems: vec![
SubsystemInfo {
name: "todo".to_string(),
status: "active".to_string(),
ops: vec![
"add".to_string(),
"claim".to_string(),
"done".to_string(),
"list".to_string(),
],
},
SubsystemInfo {
name: "knowledge".to_string(),
status: "active".to_string(),
ops: vec!["add".to_string(), "search".to_string()],
},
SubsystemInfo {
name: "federation".to_string(),
status: "active".to_string(),
ops: vec!["add".to_string(), "get".to_string(), "graph".to_string()],
},
SubsystemInfo {
name: "lcm".to_string(),
status: "active".to_string(),
ops: vec![
"ingest".to_string(),
"list".to_string(),
"show".to_string(),
"summarize".to_string(),
"summary".to_string(),
"schema".to_string(),
"rebuild".to_string(),
],
},
SubsystemInfo {
name: "map".to_string(),
status: "active".to_string(),
ops: vec![
"llm".to_string(),
"agentic".to_string(),
"schema".to_string(),
],
},
SubsystemInfo {
name: "eval".to_string(),
status: "active".to_string(),
ops: vec![
"plan".to_string(),
"ingest-run".to_string(),
"judge".to_string(),
"aggregate".to_string(),
"gate".to_string(),
"bucket-failures".to_string(),
],
},
],
workspace: WorkspaceCapabilities {
enforcement_available: true,
docker_available: std::env::var("DECAPOD_CONTAINER_RUNTIME_DISABLED").is_err(),
protected_patterns: vec![
"main".to_string(),
"master".to_string(),
"production".to_string(),
"release/*".to_string(),
],
},
interview: InterviewCapabilities {
available: true,
artifact_types: vec![
"spec".to_string(),
"architecture".to_string(),
"security".to_string(),
"ops".to_string(),
"adr".to_string(),
],
standards_resolution: true,
},
interlock_codes: vec![
"workspace_required".to_string(),
"verification_required".to_string(),
"store_boundary_violation".to_string(),
"decision_required".to_string(),
],
}
}
#[allow(clippy::too_many_arguments)]
pub fn success_response(
request_id: String,
op: String,
params: serde_json::Value,
result: Option<serde_json::Value>,
touched_paths: Vec<String>,
context_capsule: Option<ContextCapsule>,
allowed_next_ops: Vec<AllowedOp>,
mandates: Vec<Mandate>,
) -> RpcResponse {
let timestamp = crate::core::time::now_epoch_z();
let inputs_hash = format!(
"{:x}",
sha2::Sha256::digest(serde_json::to_string(¶ms).unwrap_or_default())
);
let outputs_hash = format!(
"{:x}",
sha2::Sha256::digest(serde_json::to_string(&result).unwrap_or_default())
);
RpcResponse {
id: request_id,
success: true,
receipt: Receipt {
op,
timestamp,
inputs_hash,
outputs_hash,
touched_paths,
governing_anchors: mandates.iter().map(|m| m.fragment.r#ref.clone()).collect(),
},
mandates,
context_capsule,
result,
allowed_next_ops,
blocked_by: vec![],
interlock: None,
advisory: None,
attestation: None,
error: None,
}
}
pub fn error_response(
request_id: String,
op: String,
params: serde_json::Value,
code: String,
message: String,
blocker: Option<Blocker>,
mandates: Vec<Mandate>,
) -> RpcResponse {
let timestamp = crate::core::time::now_epoch_z();
let inputs_hash = format!(
"{:x}",
sha2::Sha256::digest(serde_json::to_string(¶ms).unwrap_or_default())
);
let outputs_hash = format!("{:x}", sha2::Sha256::digest("error"));
let blocked_by = if let Some(b) = blocker {
vec![b]
} else {
vec![]
};
RpcResponse {
id: request_id,
success: false,
receipt: Receipt {
op,
timestamp,
inputs_hash,
outputs_hash,
touched_paths: vec![],
governing_anchors: mandates.iter().map(|m| m.fragment.r#ref.clone()).collect(),
},
mandates,
context_capsule: None,
result: None,
allowed_next_ops: vec![AllowedOp {
op: "agent.init".to_string(),
reason: "Session may be invalid or expired".to_string(),
required_params: vec![],
}],
blocked_by,
interlock: None,
advisory: None,
attestation: None,
error: Some(RpcError {
code,
message,
details: None,
}),
}
}