use std::{collections::BTreeSet, ffi::OsString};
use clap::{Arg, ArgAction, Command, CommandFactory, FromArgMatches, error::ErrorKind};
use crate::ToolSpec;
pub const AGENT_TOKEN_ENV: &str = "TFTIO_AGENT_TOKEN";
pub const AGENT_TOKEN_EXPECTED_ENV: &str = "TFTIO_AGENT_TOKEN_EXPECTED";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AgentModeContext {
pub active: bool,
}
impl AgentModeContext {
#[must_use]
pub fn from_tokens(presented: Option<String>, expected: Option<String>) -> Self {
Self {
active: matches!((presented, expected), (Some(presented), Some(expected)) if presented == expected),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProcessEnv {
pub agent: AgentModeContext,
pub home: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgentSurfaceSpec {
pub(crate) capabilities: &'static [AgentCapability],
}
impl AgentSurfaceSpec {
#[must_use]
pub const fn new(capabilities: &'static [AgentCapability]) -> Self {
Self { capabilities }
}
#[must_use]
pub const fn capabilities(&self) -> &'static [AgentCapability] {
self.capabilities
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AgentCapability {
pub(crate) name: &'static str,
pub(crate) summary: Option<&'static str>,
pub(crate) commands: &'static [CommandSelector],
pub(crate) flags: &'static [FlagSelector],
pub(crate) examples: Option<&'static [&'static str]>,
pub(crate) output: Option<&'static str>,
pub(crate) constraints: Option<&'static str>,
pub(crate) when_to_use: Option<&'static str>,
pub(crate) when_not_to_use: Option<&'static str>,
}
impl AgentCapability {
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn summary(&self) -> Option<&'static str> {
self.summary
}
#[must_use]
pub const fn commands(&self) -> &'static [CommandSelector] {
self.commands
}
#[must_use]
pub const fn flags(&self) -> &'static [FlagSelector] {
self.flags
}
#[must_use]
pub const fn examples(&self) -> Option<&'static [&'static str]> {
self.examples
}
#[must_use]
pub const fn output(&self) -> Option<&'static str> {
self.output
}
#[must_use]
pub const fn constraints(&self) -> Option<&'static str> {
self.constraints
}
#[must_use]
pub const fn when_to_use(&self) -> Option<&'static str> {
self.when_to_use
}
#[must_use]
pub const fn when_not_to_use(&self) -> Option<&'static str> {
self.when_not_to_use
}
#[must_use]
pub const fn new(
name: &'static str,
summary: &'static str,
commands: &'static [CommandSelector],
flags: &'static [FlagSelector],
) -> Self {
Self {
name,
summary: Some(summary),
commands,
flags,
examples: None,
output: None,
constraints: None,
when_to_use: None,
when_not_to_use: None,
}
}
#[must_use]
pub const fn minimal(
name: &'static str,
commands: &'static [CommandSelector],
flags: &'static [FlagSelector],
) -> Self {
Self {
name,
summary: None,
commands,
flags,
examples: None,
output: None,
constraints: None,
when_to_use: None,
when_not_to_use: None,
}
}
#[must_use]
pub const fn with_examples(self, examples: &'static [&'static str]) -> Self {
Self {
examples: Some(examples),
..self
}
}
#[must_use]
pub const fn with_output(self, output: &'static str) -> Self {
Self {
output: Some(output),
..self
}
}
#[must_use]
pub const fn with_constraints(self, constraints: &'static str) -> Self {
Self {
constraints: Some(constraints),
..self
}
}
#[must_use]
pub const fn with_when_to_use(self, when_to_use: &'static str) -> Self {
Self {
when_to_use: Some(when_to_use),
..self
}
}
#[must_use]
pub const fn with_when_not_to_use(self, when_not_to_use: &'static str) -> Self {
Self {
when_not_to_use: Some(when_not_to_use),
..self
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandSelector {
pub(crate) path: &'static [&'static str],
}
impl CommandSelector {
#[must_use]
pub const fn new(path: &'static [&'static str]) -> Self {
Self { path }
}
#[must_use]
pub const fn path(&self) -> &'static [&'static str] {
self.path
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FlagSelector {
pub(crate) command_path: &'static [&'static str],
pub(crate) long: &'static str,
}
impl FlagSelector {
#[must_use]
pub const fn new(command_path: &'static [&'static str], long: &'static str) -> Self {
Self { command_path, long }
}
#[must_use]
pub const fn command_path(&self) -> &'static [&'static str] {
self.command_path
}
#[must_use]
pub const fn long(&self) -> &'static str {
self.long
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AgentDispatch<T> {
Cli(T),
Printed(i32),
}
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum AgentSkillError {
#[error("unknown agent capability: {0}")]
UnknownCapability(String),
}
#[must_use]
pub fn visible_capabilities<'a>(
spec: &'a ToolSpec,
ctx: &AgentModeContext,
) -> &'a [AgentCapability] {
if ctx.active {
spec.agent_surface
.map_or(&[], |surface| surface.capabilities)
} else {
&[]
}
}
pub fn apply_agent_surface(command: &mut Command, spec: &ToolSpec, ctx: &AgentModeContext) {
if !ctx.active {
return;
}
ensure_agent_inspection_args(command);
let filtered = filter_command(command, spec.version, visible_capabilities(spec, ctx), &[]);
*command = filtered;
}
fn filter_command(
command: &Command,
version: &'static str,
capabilities: &[AgentCapability],
current_path: &[&str],
) -> Command {
let keep_full_subtree =
is_within_explicit_command_subtree(capabilities, current_path, command.has_subcommands());
let allowed_flags = allowed_flags(capabilities, current_path);
let mut filtered = clone_command_metadata(command, version, current_path.is_empty());
for arg in command
.get_arguments()
.filter(|arg| {
should_keep_arg(
arg,
capabilities,
current_path,
&allowed_flags,
keep_full_subtree,
)
})
.cloned()
{
filtered = filtered.arg(arg);
}
if keep_full_subtree {
for subcommand in command.get_subcommands() {
let subcommand_name = subcommand.get_name();
let next_path = extend_path_owned(current_path, subcommand_name);
let next_path_refs = next_path.iter().map(String::as_str).collect::<Vec<_>>();
filtered = filtered.subcommand(filter_command(
subcommand,
version,
capabilities,
&next_path_refs,
));
}
return filtered;
}
for subcommand_name in allowed_subcommands(capabilities, current_path) {
if let Some(subcommand) = command.find_subcommand(subcommand_name) {
let next_path = extend_path_owned(current_path, subcommand_name);
let next_path_refs = next_path.iter().map(String::as_str).collect::<Vec<_>>();
filtered = filtered.subcommand(filter_command(
subcommand,
version,
capabilities,
&next_path_refs,
));
}
}
filtered
}
fn clone_command_metadata(
command: &Command,
version: &'static str,
include_version: bool,
) -> Command {
let mut filtered = Command::new(command.get_name().to_owned());
if let Some(display_name) = command.get_display_name() {
filtered = filtered.display_name(display_name.to_owned());
}
if include_version {
filtered = filtered.version(version);
}
if let Some(about) = command.get_about() {
filtered = filtered.about(about.clone());
}
if let Some(long_about) = command.get_long_about() {
filtered = filtered.long_about(long_about.clone());
}
if let Some(before_help) = command.get_before_help() {
filtered = filtered.before_help(before_help.clone());
}
if let Some(after_help) = command.get_after_help() {
filtered = filtered.after_help(after_help.clone());
}
if command.is_disable_help_flag_set() {
filtered = filtered.disable_help_flag(true);
}
if command.is_disable_help_subcommand_set() {
filtered = filtered.disable_help_subcommand(true);
}
if command.is_disable_colored_help_set() {
filtered = filtered.disable_colored_help(true);
}
if command.is_flatten_help_set() {
filtered = filtered.flatten_help(true);
}
if let Some(bin_name) = command.get_bin_name() {
filtered.set_bin_name(bin_name.to_owned());
}
filtered
}
fn allowed_flags(
capabilities: &[AgentCapability],
current_path: &[&str],
) -> BTreeSet<&'static str> {
let mut flags = BTreeSet::new();
for capability in capabilities {
for selector in capability.flags {
if selector.command_path == current_path {
flags.insert(selector.long);
}
}
}
flags
}
fn allowed_subcommands(
capabilities: &[AgentCapability],
current_path: &[&str],
) -> BTreeSet<&'static str> {
let mut subcommands = BTreeSet::new();
for capability in capabilities {
for selector in capability.commands {
if selector.path.starts_with(current_path) {
if let Some(&segment) = selector.path.get(current_path.len()) {
subcommands.insert(segment);
}
}
}
}
subcommands
}
fn is_within_explicit_command_subtree(
capabilities: &[AgentCapability],
current_path: &[&str],
command_has_subcommands: bool,
) -> bool {
capabilities.iter().any(|capability| {
capability.commands.iter().any(|selector| {
!selector.path.is_empty()
&& current_path.starts_with(selector.path)
&& (selector.path.len() < current_path.len()
|| (selector.path.len() == current_path.len() && command_has_subcommands))
})
})
}
fn should_keep_arg(
arg: &Arg,
capabilities: &[AgentCapability],
current_path: &[&str],
allowed_flags: &BTreeSet<&str>,
keep_full_subtree: bool,
) -> bool {
if is_shared_agent_flag(arg) {
return true;
}
if arg.is_positional() {
return capability_includes_command_path(capabilities, current_path);
}
if keep_full_subtree {
return true;
}
arg.get_long()
.is_some_and(|long| allowed_flags.contains(long))
}
fn capability_includes_command_path(
capabilities: &[AgentCapability],
current_path: &[&str],
) -> bool {
capabilities
.iter()
.flat_map(|capability| capability.commands.iter())
.any(|selector| selector.path == current_path)
}
fn is_shared_agent_flag(arg: &Arg) -> bool {
matches!(arg.get_long(), Some("agent-help" | "agent-skill"))
}
fn ensure_agent_inspection_args(command: &mut Command) {
if !command
.get_arguments()
.any(|arg| arg.get_long() == Some("agent-help"))
{
*command = command.clone().arg(
Arg::new("agent-help")
.long("agent-help")
.help("Print the visible agent command surface")
.hide(true)
.global(true)
.action(ArgAction::SetTrue),
);
}
if !command
.get_arguments()
.any(|arg| arg.get_long() == Some("agent-skill"))
{
*command = command.clone().arg(
Arg::new("agent-skill")
.long("agent-skill")
.help("Print the visible agent capability contract")
.hide(true)
.global(true)
.value_name("NAME"),
);
}
}
fn extend_path_owned(current_path: &[&str], segment: &str) -> Vec<String> {
let mut next_path = current_path
.iter()
.map(|part| (*part).to_owned())
.collect::<Vec<_>>();
next_path.push(segment.to_owned());
next_path
}
pub fn parse_with_agent_surface_from<T, I>(
spec: &ToolSpec,
ctx: &AgentModeContext,
argv: I,
) -> Result<AgentDispatch<T>, clap::Error>
where
T: CommandFactory + FromArgMatches,
I: IntoIterator,
I::Item: Into<OsString> + Clone,
{
let argv = rewrite_trailing_help_subcommand(argv.into_iter().map(Into::into).collect());
let mut command = T::command();
ensure_agent_inspection_args(&mut command);
if ctx.active {
apply_agent_surface(&mut command, spec, ctx);
}
match command.try_get_matches_from_mut(argv) {
Ok(mut matches) => {
if matches.get_flag("agent-help") {
println!("{}", render_agent_help(spec, ctx));
return Ok(AgentDispatch::Printed(0));
}
if let Some(name) = matches.get_one::<String>("agent-skill") {
let text = render_agent_skill(spec, ctx, name)
.map_err(|error| command.error(ErrorKind::InvalidValue, error.to_string()))?;
println!("{text}");
return Ok(AgentDispatch::Printed(0));
}
T::from_arg_matches_mut(&mut matches).map(AgentDispatch::Cli)
}
Err(error) => Err(sanitize_agent_parse_error(error)),
}
}
fn rewrite_trailing_help_subcommand(mut argv: Vec<OsString>) -> Vec<OsString> {
if argv.last().is_some_and(|arg| arg == "help") {
argv.pop();
argv.push(OsString::from("--help"));
}
argv
}
fn sanitize_agent_parse_error(mut error: clap::Error) -> clap::Error {
let rendered = error.to_string();
if rendered.contains("Did you mean") {
error = clap::Error::raw(error.kind(), strip_suggestion_lines(&rendered));
}
error
}
fn strip_suggestion_lines(rendered: &str) -> String {
rendered
.lines()
.filter(|line| !line.contains("Did you mean"))
.collect::<Vec<_>>()
.join("\n")
}
#[must_use]
pub fn render_agent_help(spec: &ToolSpec, ctx: &AgentModeContext) -> String {
let capabilities = visible_capabilities(spec, ctx);
let capability_lines = if capabilities.is_empty() {
String::from("- none")
} else {
capabilities
.iter()
.map(|capability| format!("- {}: {}", capability.name, capability_summary(capability)))
.collect::<Vec<_>>()
.join("\n")
};
let argument_lines = render_surface_arguments(capabilities);
format!(
"tool:\n- {}\nmode:\n- {}\ncapabilities:\n{}\narguments:\n{}\noutput:\n- structured plain text for the visible command surface\nconstraints:\n- output is limited to the currently visible surface",
spec.bin_name,
if ctx.active { "agent" } else { "human" },
capability_lines,
argument_lines,
)
}
pub fn render_agent_skill(
spec: &ToolSpec,
ctx: &AgentModeContext,
name: &str,
) -> Result<String, AgentSkillError> {
let capability = visible_capabilities(spec, ctx)
.iter()
.find(|capability| capability.name == name)
.ok_or_else(|| AgentSkillError::UnknownCapability(name.to_owned()))?;
Ok(format!(
"tool:\n- {}\ncapability:\n- {}\nsummary:\n- {}\ncommands:\n{}\nflags:\n{}\nexamples:\n{}\noutput:\n- {}\nconstraints:\n- {}",
spec.bin_name,
capability.name,
capability_summary(capability),
render_command_lines(capability),
render_flag_lines(capability),
render_example_lines(capability),
capability_output(capability),
capability_constraints(capability),
))
}
fn capability_summary(capability: &AgentCapability) -> String {
if let Some(summary) = capability.summary {
return String::from(summary);
}
if let Some(primary_command) = capability.commands.first() {
return format!(
"Use {} via {}",
capability.name.replace('-', " "),
primary_command.path.join(" ")
);
}
format!("Use {}", capability.name.replace('-', " "))
}
fn capability_output(capability: &AgentCapability) -> String {
capability.output.map_or_else(
|| {
capability.commands.first().map_or_else(
|| String::from("output follows the existing CLI contract"),
|primary_command| {
format!(
"output follows the existing CLI contract for {}",
primary_command.path.join(" ")
)
},
)
},
String::from,
)
}
fn capability_constraints(capability: &AgentCapability) -> String {
capability.constraints.map_or_else(
|| String::from("existing command validation and auth rules still apply"),
String::from,
)
}
fn render_example_lines(capability: &AgentCapability) -> String {
capability.examples.map_or_else(
|| String::from("- none declared"),
|examples| {
if examples.is_empty() {
String::from("- none declared")
} else {
examples
.iter()
.map(|example| format!("- {example}"))
.collect::<Vec<_>>()
.join("\n")
}
},
)
}
fn render_surface_arguments(capabilities: &[AgentCapability]) -> String {
let mut lines = vec![
String::from("- --agent-help"),
String::from("- --agent-skill <NAME>"),
];
for capability in capabilities {
for command in capability.commands {
lines.push(format!("- command {}", command.path.join(" ")));
}
for flag in capability.flags {
let prefix = if flag.command_path.is_empty() {
String::new()
} else {
format!("{} ", flag.command_path.join(" "))
};
lines.push(format!("- {prefix}--{}", flag.long));
}
}
lines.join("\n")
}
fn render_command_lines(capability: &AgentCapability) -> String {
if capability.commands.is_empty() {
String::from("- none declared")
} else {
capability
.commands
.iter()
.map(|selector| format!("- {}", selector.path.join(" ")))
.collect::<Vec<_>>()
.join("\n")
}
}
fn render_flag_lines(capability: &AgentCapability) -> String {
if capability.flags.is_empty() {
String::from("- none declared")
} else {
capability
.flags
.iter()
.map(|selector| {
if selector.command_path.is_empty() {
format!("- --{}", selector.long)
} else {
format!("- {} --{}", selector.command_path.join(" "), selector.long)
}
})
.collect::<Vec<_>>()
.join("\n")
}
}
use clap::{Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum DescribeFormat {
Text,
Json,
SkillMd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum ListFormat {
Text,
Json,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum EmitTarget {
Claude,
Codex,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum EmitScope {
User,
Project,
}
#[derive(Debug, Clone, PartialEq, Eq, Subcommand)]
pub enum AgentSubcommand {
List {
#[arg(long, value_enum, default_value_t = ListFormat::Text)]
format: ListFormat,
},
Describe {
name: String,
#[arg(long, value_enum, default_value_t = DescribeFormat::Text)]
format: DescribeFormat,
},
EmitSkills {
#[arg(long, value_enum)]
target: EmitTarget,
#[arg(long, value_enum, default_value_t = EmitScope::User)]
scope: EmitScope,
#[arg(long, value_name = "DIR")]
out: Option<PathBuf>,
#[arg(long)]
install: bool,
},
}
#[cfg(test)]
mod tests {
use clap::{Arg, Args, Command, Parser, Subcommand};
use super::*;
use crate::{LicenseType, RepoInfo, ToolSpec, test_support::env_lock};
const QUERY_COMMAND: CommandSelector = CommandSelector::new(&["query"]);
const STATUS_COMMAND: CommandSelector = CommandSelector::new(&["status"]);
const QUERY_LIMIT_FLAG: FlagSelector = FlagSelector::new(&["query"], "limit");
const QUERY_OFFSET_FLAG: FlagSelector = FlagSelector::new(&["query"], "offset");
const QUERY_CAPABILITY: AgentCapability = AgentCapability::new(
"query-posts",
"Read paginated post records",
&[QUERY_COMMAND],
&[QUERY_LIMIT_FLAG, QUERY_OFFSET_FLAG],
);
const STATUS_CAPABILITY: AgentCapability = AgentCapability::new(
"inspect-status",
"Inspect current status",
&[STATUS_COMMAND],
&[],
);
const QUERY_MINIMAL_CAPABILITY: AgentCapability =
AgentCapability::minimal("query-minimal", &[QUERY_COMMAND], &[QUERY_LIMIT_FLAG]);
const AGENT_SURFACE: AgentSurfaceSpec =
AgentSurfaceSpec::new(&[QUERY_CAPABILITY, STATUS_CAPABILITY]);
const MINIMAL_AGENT_SURFACE: AgentSurfaceSpec =
AgentSurfaceSpec::new(&[QUERY_MINIMAL_CAPABILITY]);
fn spec() -> ToolSpec {
ToolSpec::new(
"tool",
"Tool",
"1.2.3",
LicenseType::MIT,
RepoInfo::new("owner", "repo"),
true,
false,
)
.with_agent_surface(&AGENT_SURFACE)
}
fn minimal_spec() -> ToolSpec {
ToolSpec::new(
"tool",
"Tool",
"1.2.3",
LicenseType::MIT,
RepoInfo::new("owner", "repo"),
true,
false,
)
.with_agent_surface(&MINIMAL_AGENT_SURFACE)
}
fn detect_from_env() -> AgentModeContext {
AgentModeContext::from_tokens(
std::env::var(AGENT_TOKEN_ENV).ok(),
std::env::var(AGENT_TOKEN_EXPECTED_ENV).ok(),
)
}
#[allow(unsafe_code)]
fn set_tokens(presented: Option<&str>, expected: Option<&str>) {
unsafe {
std::env::remove_var(AGENT_TOKEN_ENV);
std::env::remove_var(AGENT_TOKEN_EXPECTED_ENV);
if let Some(presented) = presented {
std::env::set_var(AGENT_TOKEN_ENV, presented);
}
if let Some(expected) = expected {
std::env::set_var(AGENT_TOKEN_EXPECTED_ENV, expected);
}
}
}
#[test]
fn agent_mode_activation_is_inactive_without_presented_token() {
let ctx = AgentModeContext::from_tokens(None, Some("expected".into()));
assert!(!ctx.active);
}
#[test]
fn agent_mode_activation_is_inactive_without_expected_token() {
let ctx = AgentModeContext::from_tokens(Some("presented".into()), None);
assert!(!ctx.active);
}
#[test]
fn agent_mode_activation_is_inactive_on_exact_string_mismatch() {
let ctx = AgentModeContext::from_tokens(Some("presented".into()), Some("expected".into()));
assert!(!ctx.active);
}
#[test]
fn agent_mode_activation_is_active_on_exact_string_match() {
let ctx =
AgentModeContext::from_tokens(Some("shared-token".into()), Some("shared-token".into()));
assert!(ctx.active);
}
#[test]
fn agent_mode_activation_preserves_capability_declarations() {
let spec = spec();
let capability = spec
.agent_surface
.expect("agent surface present")
.capabilities
.first()
.expect("capability present");
assert_eq!(capability.name, "query-posts");
assert_eq!(capability.commands[0].path, ["query"]);
assert_eq!(capability.flags[0].command_path, ["query"]);
assert_eq!(capability.flags[0].long, "limit");
assert_eq!(capability.flags[1].long, "offset");
}
#[test]
fn capability_policy_removes_undeclared_subcommand() {
let mut command = sample_command();
apply_agent_surface(&mut command, &spec(), &AgentModeContext { active: true });
assert!(command.find_subcommand("query").is_some());
assert!(command.find_subcommand("status").is_some());
assert!(command.find_subcommand("admin").is_none());
}
#[test]
fn capability_policy_removes_undeclared_flag() {
let mut command = sample_command();
apply_agent_surface(&mut command, &spec(), &AgentModeContext { active: true });
let query = command.find_subcommand("query").expect("query present");
assert!(
query
.get_arguments()
.any(|arg| arg.get_long() == Some("limit"))
);
assert!(
query
.get_arguments()
.any(|arg| arg.get_long() == Some("offset"))
);
assert!(
!query
.get_arguments()
.any(|arg| arg.get_long() == Some("secret"))
);
}
#[test]
fn capability_policy_returns_empty_surface_without_declared_capabilities() {
let spec = ToolSpec::new(
"tool",
"Tool",
"1.2.3",
LicenseType::MIT,
RepoInfo::new("owner", "repo"),
true,
false,
);
let mut command = sample_command();
apply_agent_surface(&mut command, &spec, &AgentModeContext { active: true });
assert!(visible_capabilities(&spec, &AgentModeContext { active: true }).is_empty());
assert!(command.find_subcommand("query").is_none());
assert!(command.find_subcommand("status").is_none());
assert!(command.find_subcommand("admin").is_none());
assert!(
command
.get_arguments()
.any(|arg| arg.get_long() == Some("agent-help"))
);
assert!(
command
.get_arguments()
.any(|arg| arg.get_long() == Some("agent-skill"))
);
}
fn sample_command() -> Command {
Command::new("tool")
.arg(Arg::new("agent-help").long("agent-help"))
.arg(
Arg::new("agent-skill")
.long("agent-skill")
.value_name("NAME"),
)
.subcommand(
Command::new("query")
.arg(Arg::new("limit").long("limit"))
.arg(Arg::new("offset").long("offset"))
.arg(Arg::new("secret").long("secret")),
)
.subcommand(Command::new("status"))
.subcommand(Command::new("admin").arg(Arg::new("danger").long("danger")))
}
#[derive(Debug, Parser, PartialEq, Eq)]
#[command(name = "tool")]
struct AgentTestCli {
#[command(subcommand)]
command: Option<AgentTestCommand>,
}
#[derive(Debug, Subcommand, PartialEq, Eq)]
enum AgentTestCommand {
Query(QueryArgs),
Status,
Admin(AdminArgs),
}
#[derive(Debug, Args, PartialEq, Eq)]
struct QueryArgs {
#[arg(long)]
limit: Option<u32>,
#[arg(long)]
offset: Option<u32>,
#[arg(long)]
secret: bool,
}
#[derive(Debug, Args, PartialEq, Eq)]
struct AdminArgs {
#[arg(long)]
danger: bool,
}
#[test]
fn agent_surface_redaction_rejects_hidden_command_and_flag() {
let _guard = env_lock();
set_tokens(Some("shared-token"), Some("shared-token"));
let ctx = detect_from_env();
let spec = spec();
let hidden_command_error =
parse_with_agent_surface_from::<AgentTestCli, _>(&spec, &ctx, ["tool", "admin"])
.expect_err("hidden subcommand should be rejected")
.to_string();
assert!(hidden_command_error.contains("unrecognized subcommand"));
let hidden_command_typo_error =
parse_with_agent_surface_from::<AgentTestCli, _>(&spec, &ctx, ["tool", "admni"])
.expect_err("hidden subcommand typo should not leak suggestions")
.to_string();
assert!(hidden_command_typo_error.contains("unrecognized subcommand"));
assert!(!hidden_command_typo_error.contains("Did you mean"));
let hidden_flag_error = parse_with_agent_surface_from::<AgentTestCli, _>(
&spec,
&ctx,
["tool", "query", "--secre"],
)
.expect_err("hidden flag typo should be rejected")
.to_string();
assert!(hidden_flag_error.contains("unexpected argument"));
assert!(!hidden_flag_error.contains("--secret"));
assert!(!hidden_flag_error.contains("Did you mean"));
}
#[test]
fn agent_surface_redaction_help_omits_hidden_entries() {
let _guard = env_lock();
set_tokens(Some("shared-token"), Some("shared-token"));
let ctx = detect_from_env();
let spec = spec();
let long_help =
parse_with_agent_surface_from::<AgentTestCli, _>(&spec, &ctx, ["tool", "--help"])
.expect_err("help should short-circuit through clap")
.to_string();
assert!(long_help.contains("query"));
assert!(long_help.contains("status"));
assert!(!long_help.contains("admin"));
assert!(!long_help.contains("--secret"));
let help_subcommand =
parse_with_agent_surface_from::<AgentTestCli, _>(&spec, &ctx, ["tool", "help"])
.expect_err("help subcommand should short-circuit through clap")
.to_string();
assert!(help_subcommand.contains("query"));
assert!(help_subcommand.contains("status"));
assert!(!help_subcommand.contains("admin"));
assert!(!help_subcommand.contains("--secret"));
}
#[test]
fn agent_surface_redaction_preserves_human_mode_surface() {
let _guard = env_lock();
set_tokens(None, None);
let ctx = detect_from_env();
let spec = spec();
let admin = parse_with_agent_surface_from::<AgentTestCli, _>(
&spec,
&ctx,
["tool", "admin", "--danger"],
)
.expect("human mode should keep the full command tree");
assert_eq!(
admin,
AgentDispatch::Cli(AgentTestCli {
command: Some(AgentTestCommand::Admin(AdminArgs { danger: true })),
})
);
let query = parse_with_agent_surface_from::<AgentTestCli, _>(
&spec,
&ctx,
["tool", "query", "--secret"],
)
.expect("human mode should keep hidden flags available");
assert_eq!(
query,
AgentDispatch::Cli(AgentTestCli {
command: Some(AgentTestCommand::Query(QueryArgs {
limit: None,
offset: None,
secret: true,
})),
})
);
}
#[test]
fn agent_surface_redaction_agent_flags_short_circuit() {
let _guard = env_lock();
set_tokens(Some("shared-token"), Some("shared-token"));
let ctx = detect_from_env();
let spec = spec();
let help =
parse_with_agent_surface_from::<AgentTestCli, _>(&spec, &ctx, ["tool", "--agent-help"])
.expect("agent help should print and exit");
assert_eq!(help, AgentDispatch::Printed(0));
let skill = parse_with_agent_surface_from::<AgentTestCli, _>(
&spec,
&ctx,
["tool", "--agent-skill", "query-posts"],
)
.expect("agent skill should print and exit");
assert_eq!(skill, AgentDispatch::Printed(0));
}
#[test]
fn agent_help_render_sections_are_structured_and_redacted() {
let rendered = render_agent_help(&spec(), &AgentModeContext { active: true });
let section_positions = [
rendered.find("tool:\n").expect("tool section"),
rendered.find("mode:\n").expect("mode section"),
rendered
.find("capabilities:\n")
.expect("capabilities section"),
rendered.find("arguments:\n").expect("arguments section"),
rendered.find("output:\n").expect("output section"),
rendered
.find("constraints:\n")
.expect("constraints section"),
];
assert!(section_positions.windows(2).all(|pair| pair[0] < pair[1]));
assert!(rendered.contains("query-posts"));
assert!(rendered.contains("inspect-status"));
assert!(!rendered.contains("admin"));
assert!(!rendered.contains("--secret"));
}
#[test]
fn agent_help_render_skill_output_is_single_capability_only() {
let rendered =
render_agent_skill(&spec(), &AgentModeContext { active: true }, "query-posts")
.expect("capability should render");
let section_positions = [
rendered.find("tool:\n").expect("tool section"),
rendered.find("capability:\n").expect("capability section"),
rendered.find("summary:\n").expect("summary section"),
rendered.find("commands:\n").expect("commands section"),
rendered.find("flags:\n").expect("flags section"),
rendered.find("examples:\n").expect("examples section"),
rendered.find("output:\n").expect("output section"),
rendered
.find("constraints:\n")
.expect("constraints section"),
];
assert!(section_positions.windows(2).all(|pair| pair[0] < pair[1]));
assert!(rendered.contains("query-posts"));
assert!(!rendered.contains("inspect-status"));
assert!(rendered.contains("query"));
assert!(rendered.contains("--limit"));
assert!(rendered.contains("--offset"));
}
#[test]
fn agent_help_render_unknown_skill_is_bounded() {
let error = render_agent_skill(&spec(), &AgentModeContext { active: true }, "missing")
.expect_err("unknown capability should fail");
assert_eq!(error.to_string(), "unknown agent capability: missing");
assert!(!error.to_string().contains("query-posts"));
assert!(!error.to_string().contains("inspect-status"));
}
#[test]
fn agent_help_render_fills_missing_prose_metadata() {
let rendered = render_agent_skill(
&minimal_spec(),
&AgentModeContext { active: true },
"query-minimal",
)
.expect("minimal capability should render");
assert!(rendered.contains("capability:\n- query-minimal"));
assert!(rendered.contains("summary:\n-"));
assert!(rendered.contains("commands:\n- query"));
assert!(rendered.contains("flags:\n- query --limit"));
assert!(rendered.contains("examples:\n- none declared"));
assert!(rendered.contains("output:\n-"));
assert!(rendered.contains("constraints:\n-"));
}
}