Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .repository-projection.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
"projection": "deixic-code",
"projectionSchemaVersion": 1,
"sourceRepository": "dx-corp/mono",
"sourceSha": "201e106555d8af4ac94c597d49f3d78fd73afbd0",
"sourceSha": "6477558fb453744e10ae520a48f32bac3396e262",
"destinationRepository": "dx-corp/code",
"priorProjectedBase": "a97cfc3b262fa2430cd1d04becfea2642973bc0d",
"priorProjectedBase": "0d3003357ee3e926c2ec525e099c7179465f3081",
"definitionDigest": "93eb3fb9be07d3644538c5519cf38973b4246cdcf2f8d7b60ccd593d7ca73033",
"toolDigest": "fdfeaca00676828c28baab97981d7b47d418788d43c4396350b1998d6a8eb3b4",
"contentDigest": "ee915b5c9f059ace7994fbdb5772d7e2216d68928f6cd364655a33e9d4d62519",
"contentDigest": "b59bb63f13dece4416ac3cce13f223fd656e144034f84f93dd3311d8b6fefe3d",
"publicationEligible": true
}
67 changes: 65 additions & 2 deletions packages/tui-rs/src/entrypoint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ struct Args {

#[derive(Debug, Default, PartialEq, Eq)]
struct NativeExecOptions {
help: bool,
specialist: Option<String>,
json: bool,
model: Option<String>,
Expand Down Expand Up @@ -561,6 +562,8 @@ fn parse_native_exec_options(raw_args: &[std::ffi::OsString]) -> NativeExecOptio
prompt_parts.push(arg.into_owned());
} else if arg == "--" {
positional_only = true;
} else if arg == "--help" || arg == "-h" {
options.help = true;
} else if arg == "--json" || arg == "--mode=json" {
options.json = true;
} else if arg == "--list-tools" {
Expand Down Expand Up @@ -1124,8 +1127,62 @@ async fn run_agent(raw_args: Vec<std::ffi::OsString>) -> Result<i32> {
// maestro print|exec [--json] [--model X] [--output-last-message P]
// [--output-schema S] <prompt...>
let mut options = parse_native_exec_options(&raw_args[2..]);
if options.help {
println!(
r#"Usage: deixic-code exec [options] <prompt...>
deixic-code print [options] <prompt...>

Run one coding task without the interactive terminal interface.
Prompt arguments are joined with spaces; use -- before literal flag-like text.

Options:
-h, --help Show this help and exit without inference
-m, --model <id> Select a model, optionally as provider/model
--provider <id> Select the provider for the model
--specialist <name> Apply a named specialist profile
--prompt-stdin Read the prompt from standard input
--json Emit newline-delimited JSON events
--output-last-message <path>
Write the final answer to a file on success
--output-schema <path|json>
Check final JSON types and required properties
--list-tools List native tools and exit without inference
--only-tools <names> Restrict tools (comma- or space-separated)
--add-tools <names> Add tools within the configured tool ceiling
--remove-tools <names> Remove tools from the selection
--approval-mode fail Reject calls requiring approval
--sandbox <mode> Subprocess sandbox: read-only, workspace-write,
native, or danger-full-access
--plugin <path> Load a plugin for this process; repeatable
--tag <name|json> Attach run metadata; repeatable
--log-group-id <id> Correlate related runs
-w, --worktree <name> Run in an isolated Git worktree

Safety:
Tool selection cannot widen MAESTRO_PRINT_ALLOWED_TOOLS.
Exec has no interactive approval dialog. Use --approval-mode fail to reject
calls requiring approval; subprocess sandbox bypass requests are rejected.

Examples:
deixic-code exec --only-tools read,grep,glob "Review this repository"
deixic-code exec --approval-mode fail --json "Fix the failing test"
cat task.txt | deixic-code exec --prompt-stdin
deixic-code exec --output-last-message answer.txt "Explain the parser"

Exit status:
0 on success; nonzero for execution or validation failures.
Missing prompts and invalid sandbox modes exit with status 2."#
);
return Ok(0);
}
if options.list_tools {
crate::print_mode::print_available_tools(options.json)?;
crate::print_mode::print_available_tools(
options.json,
&options.only_tools,
&options.add_tools,
&options.remove_tools,
options.specialist.as_deref(),
)?;
return Ok(0);
}
let env_sandbox = std::env::var("MAESTRO_SANDBOX_MODE").ok();
Expand Down Expand Up @@ -1303,7 +1360,13 @@ async fn run_agent(raw_args: Vec<std::ffi::OsString>) -> Result<i32> {
}

if args.list_tools {
crate::print_mode::print_available_tools(args.json)?;
crate::print_mode::print_available_tools(
args.json,
&args.only_tools,
&args.add_tools,
&args.remove_tools,
args.specialist.as_deref(),
)?;
return Ok(0);
}

Expand Down
25 changes: 23 additions & 2 deletions packages/tui-rs/src/print_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,32 @@ fn effective_run_tools(
Ok(Some(effective))
}

pub(crate) fn print_available_tools(json: bool) -> Result<()> {
/// Report native run selection, not authorization for individual tool calls.
pub(crate) fn print_available_tools(
json: bool,
only: &[String],
add: &[String],
remove: &[String],
specialist: Option<&str>,
) -> Result<()> {
let mut selected = effective_run_tools(allowed_tools_from_env()?, only, add, remove)?;
if let Some(name) = specialist {
let workspace = dunce::canonicalize(std::env::current_dir()?)?;
let profile = crate::agents_cli::resolve_specialist(name, &workspace)?;
if let Some(tools) = profile.tools.as_ref() {
selected = Some(specialist_tool_ceiling(selected, tools));
}
}
let registry = crate::tools::ToolRegistry::new();
let mut tools = registry
.tools()
.map(|definition| {
serde_json::json!({
"name": definition.tool.name,
"requiresApproval": definition.requires_approval,
"selected": selected.as_ref().is_none_or(|tools| {
tools.contains(&definition.tool.name.to_ascii_lowercase())
}),
})
})
.collect::<Vec<_>>();
Expand All @@ -251,11 +269,14 @@ pub(crate) fn print_available_tools(json: bool) -> Result<()> {
serde_json::to_string_pretty(&serde_json::json!({"tools": tools}))?
);
} else {
println!("Native run selection (approval and sandbox checks still apply):");
for tool in tools {
println!(
"{:<28} {}",
tool["name"].as_str().unwrap_or_default(),
if tool["requiresApproval"].as_bool().unwrap_or(false) {
if tool["selected"] == false {
"not selected"
} else if tool["requiresApproval"].as_bool().unwrap_or(false) {
"approval may be required"
} else {
"available"
Expand Down
150 changes: 150 additions & 0 deletions packages/tui-rs/tests/direct_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ fn direct_binary_help_remains_a_successful_early_exit() {
fn direct_binary_supported_noninteractive_commands_exit_successfully() {
let cases: &[(&[&str], &str)] = &[
(&["a2a", "--help"], "a2a"),
(&["exec", "--help"], "deixic-code exec"),
(&["print", "-h"], "deixic-code print"),
(&["hosted-runner", "--help"], "hosted-runner"),
(&["init", "--help"], "init"),
(&["remote", "--help"], "remote"),
Expand All @@ -43,6 +45,36 @@ fn direct_binary_supported_noninteractive_commands_exit_successfully() {
}
}

#[test]
fn exec_help_precedes_execution_options_but_respects_literal_prompts() {
let output = Command::new(maestro_tui_binary())
.args(["exec", "--help", "--prompt-stdin"])
.env("MAESTRO_SANDBOX_MODE", "invalid-help-probe")
.env("OPENAI_API_KEY", "help-must-not-print-this-value")
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
let stdout = String::from_utf8_lossy(&output.stdout);
for marker in [
"--only-tools",
"--approval-mode fail",
"Examples:",
"Exit status:",
] {
assert!(stdout.contains(marker), "missing {marker}: {stdout}");
}
assert!(!stdout.contains("help-must-not-print-this-value"));
assert!(output.stderr.is_empty(), "{output:?}");

let literal = Command::new(maestro_tui_binary())
.args(["exec", "--sandbox", "invalid-help-probe", "--", "--help"])
.output()
.unwrap();
assert_eq!(literal.status.code(), Some(2), "{literal:?}");
assert!(String::from_utf8_lossy(&literal.stderr).contains("Unsupported exec sandbox"));
assert!(!String::from_utf8_lossy(&literal.stdout).contains("Examples:"));
}

#[test]
fn specialists_are_discoverable_and_unknown_selection_fails_before_inference() {
let home = tempfile::tempdir().unwrap();
Expand Down Expand Up @@ -73,6 +105,124 @@ fn specialists_are_discoverable_and_unknown_selection_fails_before_inference() {
assert!(String::from_utf8_lossy(&output.stderr).contains("authorized scope"));
}

#[test]
fn tool_listing_reports_run_selection_without_widening_policy() {
let home = tempfile::tempdir().unwrap();
let run = |args: &[&str]| {
Command::new(maestro_tui_binary())
.args(args)
.env("MAESTRO_HOME", home.path())
.env("MAESTRO_PRINT_ALLOWED_TOOLS", "read,grep,write")
.current_dir(home.path())
.output()
.unwrap()
};
for entry in ["exec", "--print"] {
let output = run(&[
entry,
"--list-tools",
"--json",
"--only-tools",
"READ,write",
"--add-tools",
"grep,bash",
"--remove-tools",
"write",
]);
assert!(output.status.success(), "{entry}: {output:?}");
let catalog: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let tools = catalog["tools"].as_array().unwrap();
let selected: Vec<_> = tools
.iter()
.filter(|tool| tool["selected"] == true)
.map(|tool| tool["name"].as_str().unwrap())
.collect();
assert_eq!(selected, ["grep", "read"], "{entry}: {catalog}");
assert!(
tools
.iter()
.any(|tool| tool["name"] == "bash" && tool["selected"] == false)
);
assert!(
tools
.iter()
.all(|tool| tool["requiresApproval"].is_boolean())
);
}

let output = run(&[
"exec",
"--list-tools",
"--json",
"--only-tools",
"read",
"--remove-tools",
"read",
]);
assert!(output.status.success(), "{output:?}");
let catalog: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert!(
catalog["tools"]
.as_array()
.unwrap()
.iter()
.all(|tool| tool["selected"] == false)
);

let output = run(&["exec", "--list-tools", "--only-tools", "read"]);
assert!(output.status.success(), "{output:?}");
let text = String::from_utf8_lossy(&output.stdout);
assert!(text.contains("approval and sandbox checks still apply"));
assert!(
text.lines()
.any(|line| line.starts_with("write ") && line.ends_with("not selected"))
);

let output = run(&[
"exec",
"--list-tools",
"--json",
"--only-tools",
"missing-tool-9f64",
]);
assert!(!output.status.success(), "{output:?}");
assert!(String::from_utf8_lossy(&output.stderr).contains("missing-tool-9f64"));
}

#[test]
fn tool_listing_honors_specialist_limits_and_rejects_unknown_profiles() {
let home = tempfile::tempdir().unwrap();
std::fs::create_dir(home.path().join("agent-profiles")).unwrap();
std::fs::write(
home.path().join("agent-profiles/listing-probe.md"),
"---\nname: listing-probe\ntools: [read,write]\n---\nInspect the repository.\n",
)
.unwrap();
for (profile, success) in [("listing-probe", true), ("missing-profile-9f64", false)] {
let output = Command::new(maestro_tui_binary())
.args(["exec", "--list-tools", "--json", "--specialist", profile])
.env("MAESTRO_HOME", home.path())
.env("MAESTRO_PRINT_ALLOWED_TOOLS", "read,grep")
.current_dir(home.path())
.output()
.unwrap();
assert_eq!(output.status.success(), success, "{output:?}");
if success {
let catalog: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
let selected: Vec<_> = catalog["tools"]
.as_array()
.unwrap()
.iter()
.filter(|tool| tool["selected"] == true)
.map(|tool| tool["name"].as_str().unwrap())
.collect();
assert_eq!(selected, ["read"]);
} else {
assert!(String::from_utf8_lossy(&output.stderr).contains("authorized scope"));
}
}
}

#[test]
fn experiments_cli_persists_consent_and_never_exports_the_installation_seed() {
let home = tempfile::tempdir().unwrap();
Expand Down
Loading