use std::collections::BTreeMap;
use crate::dialect::Dialect;
use crate::env::{EnvPolicy, EnvSource};
use crate::model::{
AgentSpec, BindingReceiptShape, CliLaneSpec, CommandSpec, LaneSpec, PromptChannel, RpcCallSpec,
RpcCompletionSpec, RpcEnvelopeSpec, RpcLaneSpec, RpcMcpConfig, RpcMcpToolApproval,
RpcTransport, SessionScope, StreamMode,
};
pub struct AgentCard {
pub id: &'static str,
pub label: &'static str,
pub dialect: &'static Dialect,
pub surface: AgentSurface,
pub alternates: &'static [AlternateLane],
pub detect: DetectSurface,
pub features: FeatureSurface,
pub usage: UsageSurface,
pub history: &'static [HistoryRoot],
}
impl AgentCard {
pub fn spec(&self) -> AgentSpec {
self.spec_for(&self.surface, self.detect.binaries.first().copied(), false)
}
pub fn specs(&self) -> Vec<AgentSpec> {
std::iter::once(self.spec())
.chain(self.alternates.iter().map(|alternate| {
self.spec_for(
&alternate.surface,
alternate
.binary
.or_else(|| self.detect.binaries.first().copied()),
false,
)
}))
.collect()
}
pub fn spec_trusting_workspace(&self) -> AgentSpec {
self.spec_for(&self.surface, self.detect.binaries.first().copied(), true)
}
pub fn specs_trusting_workspace(&self) -> Vec<AgentSpec> {
std::iter::once(self.spec_trusting_workspace())
.chain(self.alternates.iter().map(|alternate| {
self.spec_for(
&alternate.surface,
alternate
.binary
.or_else(|| self.detect.binaries.first().copied()),
true,
)
}))
.collect()
}
pub fn spec_inheriting_host_env(&self) -> AgentSpec {
let mut spec = self.spec();
spec.command.env = EnvPolicy::InheritAll {
set: BTreeMap::new(),
};
spec
}
pub fn specs_inheriting_host_env(&self) -> Vec<AgentSpec> {
self.specs()
.into_iter()
.map(|mut spec| {
spec.command.env = EnvPolicy::InheritAll {
set: BTreeMap::new(),
};
spec
})
.collect()
}
fn spec_for(
&self,
surface: &AgentSurface,
binary: Option<&'static str>,
trust_workspace: bool,
) -> AgentSpec {
let lane = match surface {
AgentSurface::Acp {
prefer_load,
auth_key,
..
} => LaneSpec::Acp {
mcp_servers: Vec::new(),
prefer_load: *prefer_load,
auth_key: auth_key.map(AcpAuthKey::source),
enforced_permission: None,
},
AgentSurface::Cli(cli) => LaneSpec::Cli(Box::new(CliLaneSpec {
args: {
let mut args: Vec<String> =
cli.args.iter().map(|arg| (*arg).to_owned()).collect();
if trust_workspace {
args.extend(
cli.trusted_workspace_args
.iter()
.map(|arg| (*arg).to_owned()),
);
}
args
},
prompt_channel: cli.prompt_channel,
stream: cli.stream,
resume_args: cli
.resume_args
.iter()
.map(|arg| (*arg).to_owned())
.collect(),
resume_spawn_args: cli
.resume_spawn_args
.iter()
.map(|arg| (*arg).to_owned())
.collect(),
stdin_template: cli.stdin_template.map(|text| {
serde_json::from_str(text).expect("a4aa card stdin template is json")
}),
json_lines: cli.json_lines,
session_id_path: cli.session_id_path.map(str::to_owned),
usage_path: cli
.usage_path
.map(str::to_owned)
.or_else(|| self.usage.live.as_ref().map(|usage| usage.path.to_owned())),
open_args: cli.open_args.iter().map(|arg| (*arg).to_owned()).collect(),
binding_file_env: cli.binding_file_env.map(str::to_owned),
binding_receipt_shape: cli.binding_receipt_shape,
result_selector: cli.result_selector.map(str::to_owned),
result_selector_field: cli.result_selector_field.map(str::to_owned),
result_path: cli.result_path.map(str::to_owned),
execution_modes: self
.features
.execution_modes
.iter()
.map(|mode| (*mode).to_owned())
.collect(),
permission_modes: self
.features
.permission_modes
.iter()
.map(|mode| (*mode).to_owned())
.collect(),
effort_model_suffixes: cli
.effort_model_suffixes
.iter()
.map(|level| (*level).to_owned())
.collect(),
mcp_servers: Vec::new(),
})),
AgentSurface::Rpc(rpc) => LaneSpec::Rpc(Box::new(RpcLaneSpec {
initialize: rpc.initialize.map(RpcCallDecl::call),
initialized: rpc.initialized.map(str::to_owned),
session_open: rpc.session_open.map(RpcCallDecl::call),
session_resume: rpc.session_resume.map(RpcCallDecl::call),
prompt: rpc.prompt.call(),
completion: rpc.completion.map(|done| RpcCompletionSpec {
method: done.method.to_owned(),
turn_id_path: done.turn_id_path.map(str::to_owned),
status_path: done.status_path.map(str::to_owned),
status_value: done.status_value.map(str::to_owned),
}),
cancel: rpc.cancel.map(RpcCallDecl::call),
session_close: rpc.session_close.map(RpcCallDecl::call),
text_event_method: rpc.text_event_method.map(str::to_owned),
text_path: rpc.text_path.map(str::to_owned),
result_text_path: rpc.result_text_path.map(str::to_owned),
pre_turn: rpc.pre_turn.iter().map(|decl| decl.call()).collect(),
session_scope: rpc.session_scope,
kill_on_cancel: rpc.kill_on_cancel,
envelope: rpc.envelope.map(|envelope| RpcEnvelopeSpec {
method_field: envelope.method_field.to_owned(),
string_ids: envelope.string_ids,
params_inline: envelope.params_inline,
response_method: envelope.response_method.map(str::to_owned),
data_field: envelope.data_field.map(str::to_owned),
result_field: envelope.result_field.map(str::to_owned),
error_field: envelope.error_field.map(str::to_owned),
success_field: envelope.success_field.map(str::to_owned),
command_field: envelope.command_field.map(str::to_owned),
discriminant_field: envelope.discriminant_field.map(str::to_owned),
request_marker: envelope.request_marker.map(str::to_owned),
notification_marker: envelope.notification_marker.map(str::to_owned),
const_fields: envelope
.const_fields
.iter()
.map(|(name, value)| ((*name).to_owned(), (*value).to_owned()))
.collect(),
}),
passthrough: rpc
.passthrough
.iter()
.map(|method| (*method).to_owned())
.collect(),
transport: match rpc.transport {
RpcTransportDecl::Stdio => RpcTransport::Stdio,
RpcTransportDecl::WebSocket(announce) => RpcTransport::WebSocket {
announce: announce.to_owned(),
},
},
max_frame_bytes: rpc.max_frame_bytes,
usage_path: rpc.usage_path.map(str::to_owned),
failure_path: rpc.failure_path.map(str::to_owned),
failure_value: rpc.failure_value.map(str::to_owned),
failure_message_path: rpc.failure_message_path.map(str::to_owned),
mcp_servers: Vec::new(),
mcp_config: rpc.mcp.map(|mcp| RpcMcpConfig {
flag: mcp.flag.to_owned(),
root: mcp.root.to_owned(),
env_vars_key: mcp.env_vars_key.to_owned(),
tools_approval: match (mcp.tools_approval_key, mcp.tools_approval_value) {
(Some(key), Some(value)) => Some(RpcMcpToolApproval {
key: key.to_owned(),
value: value.to_owned(),
}),
_ => None,
},
tools_approval_servers: Vec::new(),
}),
})),
AgentSurface::Serve { .. } => LaneSpec::Serve {
mcp_servers: Vec::new(),
},
};
AgentSpec {
id: self.id.to_owned(),
client_name: None,
lane,
command: CommandSpec {
program: binary.unwrap_or(self.id).to_owned(),
args: Self::surface_spawn_args(surface)
.iter()
.map(|arg| (*arg).to_owned())
.collect(),
cwd: None,
env: EnvPolicy::default(),
},
max_output_bytes: None,
seatbelted_permission: self.features.seatbelted_permission.map(str::to_owned),
}
}
fn surface_spawn_args(surface: &AgentSurface) -> &'static [&'static str] {
match surface {
AgentSurface::Acp { spawn_args, .. }
| AgentSurface::Cli(CliSurface { spawn_args, .. })
| AgentSurface::Serve { spawn_args } => spawn_args,
AgentSurface::Rpc(rpc) => rpc.spawn_args,
}
}
}
pub enum AgentSurface {
Acp {
prefer_load: bool,
spawn_args: &'static [&'static str],
auth_key: Option<AcpAuthKey>,
},
Cli(CliSurface),
Rpc(&'static RpcSurface),
Serve { spawn_args: &'static [&'static str] },
}
pub struct AlternateLane {
pub surface: AgentSurface,
pub binary: Option<&'static str>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AcpAuthKey {
HostEnv(&'static str),
Secret(&'static str),
}
impl AcpAuthKey {
fn source(self) -> EnvSource {
match self {
Self::HostEnv(name) => EnvSource::HostEnv {
host_env: name.to_owned(),
},
Self::Secret(reference) => EnvSource::Secret {
secret: reference.to_owned(),
},
}
}
}
pub struct CliSurface {
pub args: &'static [&'static str],
pub trusted_workspace_args: &'static [&'static str],
pub prompt_channel: PromptChannel,
pub stream: StreamMode,
pub resume_args: &'static [&'static str],
pub resume_spawn_args: &'static [&'static str],
pub stdin_template: Option<&'static str>,
pub json_lines: bool,
pub session_id_path: Option<&'static str>,
pub open_args: &'static [&'static str],
pub binding_file_env: Option<&'static str>,
pub binding_receipt_shape: BindingReceiptShape,
pub result_selector: Option<&'static str>,
pub result_selector_field: Option<&'static str>,
pub result_path: Option<&'static str>,
pub usage_path: Option<&'static str>,
pub spawn_args: &'static [&'static str],
pub effort_model_suffixes: &'static [&'static str],
}
pub struct RpcSurface {
pub initialize: Option<RpcCallDecl>,
pub initialized: Option<&'static str>,
pub session_open: Option<RpcCallDecl>,
pub session_resume: Option<RpcCallDecl>,
pub prompt: RpcCallDecl,
pub cancel: Option<RpcCallDecl>,
pub session_close: Option<RpcCallDecl>,
pub text_event_method: Option<&'static str>,
pub text_path: Option<&'static str>,
pub completion: Option<CompletionDecl>,
pub result_text_path: Option<&'static str>,
pub pre_turn: &'static [RpcCallDecl],
pub session_scope: SessionScope,
pub kill_on_cancel: bool,
pub envelope: Option<&'static EnvelopeDecl>,
pub passthrough: &'static [&'static str],
pub spawn_args: &'static [&'static str],
pub transport: RpcTransportDecl,
pub max_frame_bytes: Option<usize>,
pub usage_path: Option<&'static str>,
pub failure_path: Option<&'static str>,
pub failure_value: Option<&'static str>,
pub failure_message_path: Option<&'static str>,
pub mcp: Option<&'static RpcMcpDecl>,
}
#[derive(Clone, Copy)]
pub struct RpcMcpDecl {
pub flag: &'static str,
pub root: &'static str,
pub env_vars_key: &'static str,
pub tools_approval_key: Option<&'static str>,
pub tools_approval_value: Option<&'static str>,
}
#[derive(Clone, Copy)]
pub enum RpcTransportDecl {
Stdio,
WebSocket(&'static str),
}
#[derive(Clone, Copy)]
pub struct EnvelopeDecl {
pub method_field: &'static str,
pub string_ids: bool,
pub params_inline: bool,
pub response_method: Option<&'static str>,
pub data_field: Option<&'static str>,
pub result_field: Option<&'static str>,
pub error_field: Option<&'static str>,
pub success_field: Option<&'static str>,
pub command_field: Option<&'static str>,
pub discriminant_field: Option<&'static str>,
pub request_marker: Option<&'static str>,
pub notification_marker: Option<&'static str>,
pub const_fields: &'static [(&'static str, &'static str)],
}
#[derive(Clone, Copy)]
pub struct RpcCallDecl {
pub method: &'static str,
pub params: &'static str,
}
impl RpcCallDecl {
fn call(self) -> RpcCallSpec {
RpcCallSpec {
method: self.method.to_owned(),
params: serde_json::from_str(self.params).expect("a4aa card rpc params are json"),
}
}
}
#[derive(Clone, Copy)]
pub struct CompletionDecl {
pub method: &'static str,
pub turn_id_path: Option<&'static str>,
pub status_path: Option<&'static str>,
pub status_value: Option<&'static str>,
}
pub struct DetectSurface {
pub binaries: &'static [&'static str],
pub processes: &'static [&'static str],
pub binary_dirs: &'static [OsPath],
pub config: &'static [&'static str],
pub detection: &'static [&'static str],
pub apps: &'static [OsPath],
}
pub struct OsPath {
pub path: &'static str,
pub oses: &'static [&'static str],
}
pub use acop::store::{HistoryRoot, SqliteUsageSpec, StoreKind, TranscriptUsage, UsageTemporality};
pub struct UsageSurface {
pub live: Option<LiveUsage>,
pub transcript: Option<TranscriptUsage>,
pub hosted: Option<&'static str>,
}
pub struct LiveUsage {
pub event: &'static str,
pub path: &'static str,
}
pub struct FeatureSurface {
pub instructions: InstructionChannel,
pub execution_modes: &'static [&'static str],
pub permission_modes: &'static [&'static str],
pub permission_default: Option<&'static str>,
pub seatbelted_permission: Option<&'static str>,
pub attachments: AttachmentSupport,
pub effort: bool,
pub steer: bool,
pub models: ModelCatalog,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InstructionChannel {
Compose,
Param(&'static str),
Flag(&'static str),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AttachmentSupport {
None,
LocalImages,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ModelCatalog {
None,
RpcMethod(&'static str),
CliArgs(&'static [&'static str]),
}