mod worktree;
use worktree::execute as execute_tool_inner;
use super::artifact_read::{execute_artifact_read_silent, ArtifactReadContext};
#[cfg(test)]
use super::content_spill::{self, SpillStore};
#[cfg(test)]
use super::display::ToolDisplay;
use super::display::ToolPresentation;
use super::mcp::{leash_mcp_call, McpGrant, McpTools};
use super::memory_fetch::{execute_memory_fetch, memory_fetch_tool_definition};
use super::note_sink::{execute_save_note, save_note_tool_definition};
use super::permissions::{
DenialKind, HumanQuestionOutcome, PermissionDecision, PermissionGate, PermissionRequest,
BOUND_REASON_PREFIX,
};
use super::prompt_intake::PromptDisposition;
use super::prompt_read::execute_prompt_read_silent;
#[cfg(test)]
use super::prompt_read::PromptReadContext;
use super::recall::{execute_recall, recall_tool_definition};
use super::report::{execute_render_report, render_report_tool_definition};
use crate::caveats::CaveatsExt as _;
use crate::PermissionAction;
#[cfg(test)]
use output_budget::paginate_read;
#[cfg(test)]
use output_budget::DEFAULT_MAX_OUTPUT_TOKENS;
#[cfg(test)]
use output_budget::DEFAULT_OUTPUT_CAP_CHARS_PER_TOKEN;
#[cfg(test)]
use output_budget::{cap_model_output, cap_model_output_with_handle};
use output_budget::{paginate_unspillable, read_file_page};
use super::tool_args::nonnegative_usize as arg_usize;
pub use output_budget::{
set_max_output_tokens, set_output_cap_chars_per_token, set_output_head_tokens,
};
mod build_shell;
mod catalog;
mod dependency_fetch;
mod dispatch;
pub(super) mod file_capture;
mod file_change;
#[cfg(feature = "ast")]
mod move_from;
mod navigation;
mod path_suggest;
#[cfg(test)]
use dispatch::execute_tool_with_display_cancellable;
pub use dispatch::{
execute_tool, execute_tool_with_offload, execute_tool_with_offload_and_prompt_and_artifacts,
};
pub(crate) use dispatch::{execute_tool_with_collaborators, ToolCollaborators};
use path_suggest::FileIoError;
pub(crate) mod exposure;
mod grep_tool;
mod live_output;
mod native_git;
pub(crate) mod output_budget;
mod read_history;
pub(crate) use read_history::ReadHistory;
mod shell;
pub use shell::{run_command_dialect_sentence, shell_dialect_sentence};
#[cfg(all(test, unix))]
mod tool_spinner_pty_test;
use live_output::LiveOutputSession;
#[cfg(test)]
pub(crate) use shell::absent_binary_refusal;
pub use shell::venv_cmd_prefix;
#[cfg(test)]
use shell::{
confined_dispatch_args, decode_shell_stream, denial_recovery_hints, denied_run_command_result,
envelope_denial_reason, envelope_denied, exec_denial_requests, exec_floor_permits,
net_denial_requests, pr_creation_url, same_file_redirect_refusal, shadow_records, shell_engine,
shell_envelope_output, venv_env_map,
};
use shell::{
declared_filesystem_requests, dispatch_caveats_for_git_shell, exec_confined_command,
permits_filesystem_request, resolve_exec_cwd, split_leading_cd,
};
#[cfg(all(test, not(windows)))]
use shell::{
host_shell_command, host_shell_output, host_shell_output_with_timeout,
CHILD_STRIPPED_AUTHORITY_ENV,
};
pub(crate) use shell::{ABSENT_BINARY_MARKER, NOT_ON_HOST_MARKER};
#[cfg(test)]
use catalog::lifecycle_tool_definition;
pub(crate) use catalog::{
classify_gated_off_reach, classify_phantom_reach, is_context_remaining_call, is_hallucination,
known_builtin_tool_name, merged_tool_definitions, resolve_tool_alias, AliasOutcome,
};
use catalog::{
disposition_tool_denied_message, is_mcp_tool_name, run_command_creates_shell_git_commit,
run_command_redirect, unknown_tool_message,
};
pub use catalog::{
filter_advertised_tools, filter_tools_for_disposition, persona_tool_allowed, tool_allowed,
tool_definitions,
};
#[cfg(test)]
use catalog::{
levenshtein, nearest_tool_name, ALL_TOOL_NAMES, BASE_TOOL_NAMES, EXTENDED_TOOL_REGISTRY,
};
pub use exposure::ExposureSettings;
pub(crate) use exposure::{select_exposed, select_openai_compatible_tools, HiddenTools};
pub(crate) fn validate_tool_call(
name: Option<&str>,
raw_args: &serde_json::Value,
) -> Result<(String, serde_json::Value), String> {
let name = name
.map(str::trim)
.filter(|n| !n.is_empty())
.ok_or_else(|| "tool call is missing a name".to_string())?;
let args = match raw_args {
serde_json::Value::Null => serde_json::json!({}),
serde_json::Value::Object(_) => raw_args.clone(),
serde_json::Value::String(s) => {
let trimmed = s.trim();
if trimmed.is_empty() {
serde_json::json!({})
} else {
match serde_json::from_str::<serde_json::Value>(trimmed) {
Ok(v @ serde_json::Value::Object(_)) => v,
Ok(_) => {
return Err(format!(
"tool '{name}' arguments must be a JSON object, but the model sent a non-object JSON value"
))
}
Err(e) => {
return Err(format!(
"tool '{name}' arguments are not valid JSON (call truncated or malformed): {e}. \
If you were writing a large file, write it in smaller pieces."
))
}
}
}
}
other => {
return Err(format!(
"tool '{name}' arguments must be a JSON object, got {other}"
))
}
};
Ok((name.to_string(), args))
}
pub(crate) struct ValidatedCall {
pub call_id: String,
pub name: String,
pub args: serde_json::Value,
}
#[derive(Debug)]
pub(crate) enum BatchRejection {
CorrelationImpossible(String),
ContentInvalid(String),
}
pub(crate) fn uncorrelatable_tool_calls(reason: &str) -> anyhow::Error {
super::observability::DispatchError::new(
super::observability::ErrorClass::Model,
format!("malformed provider output: {reason}"),
)
.into()
}
pub(crate) const MAX_UNCORRELATABLE_BATCHES: u32 = 3;
pub(crate) fn reask_uncorrelatable(
strikes: &mut u32,
reason: &str,
smart_harness: Option<&super::smart_harness::SmartHarness>,
tool_events: Option<&mut Vec<crate::ToolEvent>>,
) -> anyhow::Result<String> {
*strikes += 1;
let recoverable = *strikes < MAX_UNCORRELATABLE_BATCHES;
if let Some(harness) = smart_harness {
harness.reject_tools(reason, recoverable)?;
}
if !recoverable {
return Err(uncorrelatable_tool_calls(reason));
}
if let Some(rec) = tool_events {
rec.push(crate::ToolEvent::from_call(
"(rejected tool-call batch)",
&serde_json::Value::Null,
false,
Some(0),
));
}
let reason: String = reason.chars().take(200).collect();
Ok(format!(
"Your last reply's tool calls could not be correlated ({reason}), so none of them \
were run. Re-emit the calls you intended as native tool calls (not as text in your \
reply), each with its own unique, non-empty call id."
))
}
pub(crate) fn withdraw_tool_calls(assistant_turn: &mut serde_json::Value) {
if assistant_turn["role"] != "assistant" {
return;
}
if let Some(turn) = assistant_turn.as_object_mut() {
turn.remove("tool_calls");
if turn
.get("content")
.is_none_or(|c| c.is_null() || c.as_str().is_some_and(|t| t.trim().is_empty()))
{
turn.insert(
"content".into(),
"(tool calls without ids were discarded)".into(),
);
}
}
}
impl BatchRejection {
pub(crate) fn reason(&self) -> &str {
match self {
Self::CorrelationImpossible(r) | Self::ContentInvalid(r) => r,
}
}
}
pub(crate) fn validate_tool_call_batch(
calls: &[(Option<&str>, Option<&str>, &serde_json::Value)],
require_call_id: bool,
) -> Result<Vec<ValidatedCall>, BatchRejection> {
if require_call_id {
let mut seen_ids: std::collections::HashSet<&str> = std::collections::HashSet::new();
for (i, &(call_id, _, _)) in calls.iter().enumerate() {
let n = i + 1;
let id = call_id
.map(str::trim)
.filter(|s| !s.is_empty())
.ok_or_else(|| {
BatchRejection::CorrelationImpossible(format!(
"tool call #{n} is missing a call id — its result cannot be correlated"
))
})?;
if !seen_ids.insert(id) {
return Err(BatchRejection::CorrelationImpossible(format!(
"tool call #{n} repeats call id {id:?} — ambiguous result routing"
)));
}
}
}
let mut validated = Vec::with_capacity(calls.len());
for (i, &(call_id, name, raw_args)) in calls.iter().enumerate() {
let n = i + 1;
let (name, args) = validate_tool_call(name, raw_args)
.map_err(|e| BatchRejection::ContentInvalid(format!("tool call #{n}: {e}")))?;
let call_id = call_id
.map(str::trim)
.filter(|s| !s.is_empty())
.unwrap_or("")
.to_string();
validated.push(ValidatedCall {
call_id,
name,
args,
});
}
Ok(validated)
}
pub fn ocap_disabled() -> bool {
crate::launch_authority::current().ocap_disabled()
}
pub fn full_access_requested() -> bool {
crate::launch_authority::current().full_access()
}
pub fn plan_phase_clamp() -> crate::caveats::Caveats {
use crate::caveats::{CountBound, Scope};
crate::caveats::Caveats {
fs_read: Scope::All,
fs_write: Scope::none(),
exec: Scope::none(),
net: Scope::none(),
max_calls: CountBound::Unlimited,
valid_for_generation: Scope::All,
}
}
pub fn routing_disabled() -> bool {
std::env::var("NEWT_NO_ROUTE").is_ok_and(|v| v == "1")
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::caveats::lexically_normalize;
pub(crate) fn tui_permits_path(scope: &crate::caveats::Scope<String>, full_path: &str) -> bool {
crate::caveats::permits_path(scope, full_path)
}
fn append_routed_note(text: String, note: impl std::fmt::Display) -> String {
format!("{text}\n{note}")
}
fn justfile_findable_from(dir: &std::path::Path) -> bool {
dir.ancestors()
.any(|d| d.join("justfile").exists() || d.join("Justfile").exists())
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn authorizing_root<'a>(
scope: &'a crate::caveats::Scope<String>,
full_path: &str,
) -> Option<Option<&'a str>> {
match scope {
crate::caveats::Scope::All => Some(None),
crate::caveats::Scope::Only(set) if set.is_empty() => None,
crate::caveats::Scope::Only(set) => {
let candidate = lexically_normalize(full_path);
set.iter()
.find(|root| candidate.starts_with(lexically_normalize(root)))
.map(|r| Some(r.as_str()))
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn contained_relative(full_path: &str, root: &str) -> std::path::PathBuf {
let cand = lexically_normalize(full_path);
let nroot = lexically_normalize(root);
let rel = cand.strip_prefix(&nroot).unwrap_or(&cand);
if rel.as_os_str().is_empty() {
std::path::PathBuf::from(".")
} else {
rel.to_path_buf()
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn is_fs_containment_denied(e: &std::io::Error) -> bool {
matches!(e.raw_os_error(), Some(libc::EXDEV) | Some(libc::ELOOP))
|| (cfg!(target_os = "macos")
&& matches!(e.raw_os_error(), Some(libc::ENOTDIR) | Some(libc::EMLINK)))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn object_bound_target<'a>(
scope: &'a crate::caveats::Scope<String>,
full_str: &str,
) -> Option<Option<(&'a str, std::path::PathBuf)>> {
authorizing_root(scope, full_str)
.map(|opt| opt.map(|root| (root, contained_relative(full_str, root))))
}
fn std_list_dir(full: &std::path::Path) -> Result<Vec<String>, FileIoError> {
match std::fs::read_dir(full) {
Ok(entries) => Ok(entries
.flatten()
.map(|e| e.file_name().to_string_lossy().into_owned())
.collect()),
Err(e) => Err(FileIoError::io(&e, full, format!("error: {e}"))),
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn object_bound_read(
scope: &crate::caveats::Scope<String>,
axis: &str,
path: &str,
full: &std::path::Path,
full_str: &str,
) -> Result<String, FileIoError> {
use std::io::Read;
match object_bound_target(scope, full_str) {
None => Err(denied_fs_result(axis, full_str).into()),
Some(None) => std::fs::read_to_string(full)
.map_err(|e| FileIoError::io(&e, full, format!("error: reading {path}: {e}"))),
Some(Some((root, rel))) => {
let read = crate::fs_cap::WorkspaceDir::open_granted_file(
std::path::Path::new(root),
&rel,
false,
)
.and_then(|mut f| {
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
});
match read {
Ok(s) => Ok(s),
Err(e) if is_fs_containment_denied(&e) => {
Err(denied_fs_result(axis, full_str).into())
}
Err(e) => Err(FileIoError::io(
&e,
full,
format!("error: reading {path}: {e}"),
)),
}
}
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn object_bound_list(
scope: &crate::caveats::Scope<String>,
full: &std::path::Path,
full_str: &str,
) -> Result<Vec<String>, FileIoError> {
match object_bound_target(scope, full_str) {
None => Err(denied_fs_result("fs_read", full_str).into()),
Some(None) => std_list_dir(full),
Some(Some((root, rel))) => {
match crate::fs_cap::WorkspaceDir::open_root(std::path::Path::new(root))
.and_then(|dir| dir.read_dir(&rel))
{
Ok(names) => Ok(names
.into_iter()
.map(|n| n.to_string_lossy().into_owned())
.collect()),
Err(e) if is_fs_containment_denied(&e) => {
Err(denied_fs_result("fs_read", full_str).into())
}
Err(e) => Err(FileIoError::io(&e, full, format!("error: {e}"))),
}
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn object_bound_read(
_scope: &crate::caveats::Scope<String>,
_axis: &str,
path: &str,
full: &std::path::Path,
_full_str: &str,
) -> Result<String, FileIoError> {
std::fs::read_to_string(full)
.map_err(|e| FileIoError::io(&e, full, format!("error: reading {path}: {e}")))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn object_bound_list(
_scope: &crate::caveats::Scope<String>,
full: &std::path::Path,
_full_str: &str,
) -> Result<Vec<String>, FileIoError> {
std_list_dir(full)
}
fn std_write(full: &std::path::Path, path: &str, content: &str) -> Result<(), FileIoError> {
if let Some(parent) = full.parent() {
let _ = std::fs::create_dir_all(parent);
}
std::fs::write(full, content)
.map_err(|e| FileIoError::io(&e, full, format!("error: writing {path}: {e}")))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn object_bound_write(
scope: &crate::caveats::Scope<String>,
axis: &str,
path: &str,
full: &std::path::Path,
full_str: &str,
content: &str,
) -> Result<(), FileIoError> {
use std::io::Write;
match object_bound_target(scope, full_str) {
None => Err(denied_fs_result(axis, full_str).into()),
Some(None) => std_write(full, path, content),
Some(Some((root, rel))) => {
let write =
crate::fs_cap::WorkspaceDir::create_granted_file(std::path::Path::new(root), &rel)
.and_then(|mut f| {
f.write_all(content.as_bytes())?;
Ok(())
});
match write {
Ok(()) => Ok(()),
Err(e) if is_fs_containment_denied(&e) => {
Err(denied_fs_result(axis, full_str).into())
}
Err(e) => Err(FileIoError::io(
&e,
full,
format!("error: writing {path}: {e}"),
)),
}
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn object_bound_write(
_scope: &crate::caveats::Scope<String>,
_axis: &str,
path: &str,
full: &std::path::Path,
_full_str: &str,
content: &str,
) -> Result<(), FileIoError> {
std_write(full, path, content)
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn object_bound_delete(
scope: &crate::caveats::Scope<String>,
path: &str,
full: &std::path::Path,
full_str: &str,
) -> Result<(), FileIoError> {
match object_bound_target(scope, full_str) {
None => Err(denied_fs_result("fs_write", full_str).into()),
Some(None) => std::fs::remove_file(full)
.map_err(|e| FileIoError::io(&e, full, format!("error: deleting {path}: {e}"))),
Some(Some((root, rel))) => {
match crate::fs_cap::WorkspaceDir::open_root(std::path::Path::new(root))
.and_then(|dir| dir.unlink(&rel))
{
Ok(()) => Ok(()),
Err(e) if is_fs_containment_denied(&e) => {
Err(denied_fs_result("fs_write", full_str).into())
}
Err(e) => Err(FileIoError::io(
&e,
full,
format!("error: deleting {path}: {e}"),
)),
}
}
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
fn object_bound_delete(
_scope: &crate::caveats::Scope<String>,
path: &str,
full: &std::path::Path,
_full_str: &str,
) -> Result<(), FileIoError> {
std::fs::remove_file(full)
.map_err(|e| FileIoError::io(&e, full, format!("error: deleting {path}: {e}")))
}
#[cfg(target_os = "linux")]
fn find_root_contained(
_scope: &crate::caveats::Scope<String>,
workspace: &str,
full: &std::path::Path,
_full_str: &str,
) -> bool {
let rel = match full.strip_prefix(workspace) {
Ok(r) if !r.as_os_str().is_empty() => r.to_path_buf(),
Ok(_) => std::path::PathBuf::from("."), Err(_) => return false, };
match crate::fs_cap::WorkspaceDir::open_root(std::path::Path::new(workspace))
.and_then(|dir| dir.open_dir(&rel))
{
Ok(_) => true,
Err(e) if is_fs_containment_denied(&e) => false,
Err(_) => true,
}
}
#[cfg(not(target_os = "linux"))]
fn find_root_contained(
_scope: &crate::caveats::Scope<String>,
workspace: &str,
full: &std::path::Path,
_full_str: &str,
) -> bool {
match (
std::path::Path::new(workspace).canonicalize(),
full.canonicalize(),
) {
(Ok(ws), Ok(root)) => root.starts_with(&ws),
_ => true,
}
}
fn confirm_unrestricted_fs_mutation(
caveats: &crate::caveats::Caveats,
gate: &mut Option<&mut dyn PermissionGate>,
question: &str,
) -> bool {
if !matches!(caveats.fs_write, crate::caveats::Scope::All) {
return true;
}
if ocap_disabled() {
return true;
}
let definition = mutation_confirm_definition(question);
let rendered = crate::markup::plain::render(&definition);
match gate {
Some(g) => match g.ask_question(&rendered) {
HumanQuestionOutcome::Answer(answer) => {
confirm_choice_options(&definition)
.and_then(|options| newt_interaction::binding::resolve_typed(options, &answer))
.and_then(|option| {
crate::interaction_adapter::action_for_option(option.as_str())
})
== Some(PermissionAction::AllowOnce)
}
_ => false,
},
None => false,
}
}
fn confirm_choice_options(
definition: &newt_interaction::InteractionDefinition,
) -> Option<&Vec<newt_interaction::ChoiceOption>> {
definition.controls.iter().find_map(|c| match &c.kind {
newt_interaction::ControlKind::Choice { options } => Some(options),
_ => None,
})
}
fn mutation_confirm_definition(question: &str) -> newt_interaction::InteractionDefinition {
use newt_interaction::{
ChoiceOption, Control, ControlId, ControlKind, InteractionDefinition, InteractionKind,
OptionId, Requirement, SemanticRole,
};
let option = |wire: &str, role, key: &str, label: &str, alias: &str| {
ChoiceOption {
id: OptionId::new(wire).expect(
"the confirm wire names are consts drawn from [A-Za-z0-9_-]; this cannot vary at runtime",
),
role,
label: label.to_string(),
key: key.to_string(),
aliases: vec![alias.to_string()],
}
};
InteractionDefinition::new(
InteractionKind::Confirm,
question.to_string(),
vec![Control {
id: ControlId::new(crate::interaction_adapter::DECISION_CONTROL)
.expect("`decision` is a valid control id; it is a const"),
kind: ControlKind::Choice {
options: vec![
option(
PermissionAction::AllowOnce.as_str(),
SemanticRole::Allow,
"y",
"y to confirm",
"Y",
),
option(
PermissionAction::Deny.as_str(),
SemanticRole::Deny,
"n",
"n to skip",
"N",
),
],
},
label: String::new(),
requirement: Requirement::Required,
}],
)
}
pub(crate) fn run_build_check(
cmd: &str,
workspace: &str,
network: &crate::Scope<String>,
) -> String {
use crate::confined_exec::{build_tool_request, ConstrainedExecutor};
let (program, args) = build_check_argv(cmd);
let workspace = std::path::Path::new(workspace);
let req = build_tool_request(workspace, workspace, program, args, network);
match ConstrainedExecutor::run(&req) {
Ok(out) if out.success => " ✓ build check passed".to_string(),
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
let combined = format!("{stdout}{stderr}");
let excerpt: String = combined.lines().take(8).collect::<Vec<_>>().join("\n");
format!(" ✗ build check failed:\n{excerpt}")
}
Err(e) => format!(" ⚠ build check could not run: {e}"),
}
}
fn literal_newline_escape_warning(old_string: &str, new_string: &str) -> Option<&'static str> {
(!new_string.contains('\n')
&& new_string.matches("\\n").count() > old_string.matches("\\n").count())
.then_some(
"warning: the edit was applied, but new_string contains literal `\\n` sequences \
and no real newlines. If you meant line breaks, re-send with real newlines.",
)
}
fn lifecycle_run_result(
args: &serde_json::Value,
mut result: (String, crate::ExecOutcome),
) -> (String, crate::ExecOutcome) {
let suggestion = build_call_suggestion(args, args["phase"].as_str().unwrap_or(""));
match result.1 {
crate::ExecOutcome::Unavailable => {
result.0.push_str(&format!(
"\nThis was lifecycle action=run. For compiler/test validation, action=build \
requests explicit approval for toolchain/cache reads and workspace writes, \
with network denied unless covered by an existing operator grant. Existing permission requirements and denials remain binding; \
do not retry a declined grant.\nSuggested lifecycle call: {suggestion}"
));
}
crate::ExecOutcome::TimedOut => {
result.0 = format!("Suggested lifecycle call: {suggestion}\n{}", result.0);
}
_ => {}
}
result
}
fn escalated_after_timeout(
mut result: (String, crate::ExecOutcome),
wall: std::time::Duration,
) -> (String, crate::ExecOutcome) {
result.0 = format!(
"This was lifecycle action=run; it hit the {}s wall, so it was re-run once in the \
action=build lane (same permission-gate re-check, existing network grants only).\n{}",
wall.as_secs(),
result.0
);
result
}
fn format_wall(wall: std::time::Duration) -> String {
let secs = wall.as_secs();
if secs > 0 && secs.is_multiple_of(60) {
format!("{} min", secs / 60)
} else {
format!("{secs}s")
}
}
fn escalates(outcome: crate::ExecOutcome, wall: std::time::Duration) -> bool {
outcome == crate::ExecOutcome::TimedOut && wall != shell::LIFECYCLE_BUILD_TIMEOUT
}
fn escalation_result(
first: (String, crate::ExecOutcome),
escalated: (String, crate::ExecOutcome),
wall: std::time::Duration,
command_budget: crate::RunCommandBudget,
) -> (String, crate::ExecOutcome) {
if matches!(
escalated.1,
crate::ExecOutcome::Denied | crate::ExecOutcome::Unavailable
) {
let first_text = first
.0
.strip_suffix(&shell::timed_out_note(wall, command_budget))
.unwrap_or(&first.0);
return (
format!(
"{first_text}\n\nThe action=build escalation did not run:\n{}\n\nBuild \
authority was declined for this call — do not retry it; narrow the \
command instead (one crate, one test filter).",
escalated.0
),
escalated.1,
);
}
escalated_after_timeout(escalated, wall)
}
#[allow(clippy::too_many_arguments)]
async fn lifecycle_run_with_escalation(
args: &serde_json::Value,
joined: &str,
effective_dir: &str,
effective_path: &std::path::Path,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
exec_floor: Option<&crate::caveats::Scope<String>>,
mut permission_gate: Option<&mut dyn PermissionGate>,
tool_offload: bool,
spill_store: Option<&dyn super::content_spill::SpillStore>,
live_tool_output: Option<std::sync::Arc<dyn crate::agentic::LiveToolOutput>>,
smart_harness: Option<&super::smart_harness::SmartHarness>,
presentation: &mut dyn ToolPresentation,
command_budget: crate::RunCommandBudget,
) -> (String, crate::ExecOutcome) {
let first = exec_confined_command(
joined,
effective_dir,
workspace,
color,
tool_output_lines,
caveats,
&[],
exec_floor,
&mut permission_gate,
tool_offload,
spill_store,
live_tool_output,
presentation,
command_budget,
)
.await;
let wall = shell::dispatch_wall(joined, command_budget);
if !escalates(first.1, wall) {
return lifecycle_run_result(args, first);
}
let (program, argv) = build_check_argv(joined);
let escalated = run_confined_build_lane(
workspace,
effective_path,
program,
argv,
joined,
smart_harness,
caveats,
&mut permission_gate,
tool_output_lines,
color,
tool_offload,
spill_store,
Some(&format!(
"lifecycle action=run hit the confined shell's {}s wall",
wall.as_secs()
)),
None,
shell::LIFECYCLE_BUILD_TIMEOUT,
presentation,
)
.await;
escalation_result(first, escalated, wall, command_budget)
}
fn lifecycle_run_routes_to_build_lane(cmd: &str) -> bool {
shell::leading_program(cmd).is_some_and(crate::confined_exec::is_build_tool_exec)
}
fn build_call_suggestion(args: &serde_json::Value, phase: &str) -> serde_json::Value {
let mut suggestion = serde_json::json!({"phase": phase, "action": "build"});
if let Some(dir) = args.get("dir").and_then(serde_json::Value::as_str) {
suggestion["dir"] = dir.into();
}
suggestion
}
fn lifecycle_build_request(
workspace: &str,
command: &str,
build: &crate::Caveats,
escalation_note: Option<&str>,
) -> PermissionRequest {
let reads = match &build.fs_read {
crate::Scope::Only(roots) => roots.iter().cloned().collect::<Vec<_>>().join("\n"),
crate::Scope::All => unreachable!("build reads are calibrated"),
};
let escalation = escalation_note
.map(|note| format!("\nThis is an escalation: {note}."))
.unwrap_or_default();
let network = build_network_description(&build.net);
PermissionRequest {
tool: "lifecycle".into(),
kind: DenialKind::Build,
target: workspace.into(),
reason: format!("Run this resolved lifecycle command: {command}\nRead roots (including any credentials stored within them):\n{reads}\nWrites within the workspace and its build scratch directory; {network}. Compiler, build-script and test subprocesses inherit the same filesystem fence.{escalation}"),
harness_bound: false,
}
}
fn build_network_description(network: &crate::Scope<String>) -> &'static str {
match network {
crate::Scope::All => "network allowed by the existing operator grant",
crate::Scope::Only(hosts) if hosts.is_empty() => "network denied",
crate::Scope::Only(_) => "network limited to the existing operator grant",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OutputTrim {
Tail(u32),
Head(u32),
}
impl OutputTrim {
fn from_json(value: Option<&serde_json::Value>) -> Option<Self> {
let value = value?;
let n = u32::try_from(value.get("n")?.as_u64()?).ok()?;
if n == 0 {
return None;
}
match value.get("mode")?.as_str()? {
"tail" => Some(Self::Tail(n)),
"head" => Some(Self::Head(n)),
_ => None,
}
}
fn apply(self, stdout: &str, stderr: &str) -> String {
let mut combined = String::with_capacity(stdout.len() + stderr.len());
combined.push_str(stdout);
combined.push_str(stderr);
let lines: Vec<&str> = combined.lines().collect();
let kept = match self {
Self::Tail(n) => &lines[lines.len().saturating_sub(n as usize)..],
Self::Head(n) => &lines[..(n as usize).min(lines.len())],
};
kept.join("\n")
}
fn note_clause(self) -> String {
match self {
Self::Tail(n) => {
format!("output trimmed to the last {n} lines, as `| tail -{n}` asked")
}
Self::Head(n) => {
format!("output trimmed to the first {n} lines, as `| head -{n}` asked")
}
}
}
}
#[allow(clippy::too_many_arguments)]
async fn run_confined_build_lane(
workspace: &str,
cwd: &std::path::Path,
program: &str,
argv: Vec<String>,
display: &str,
smart_harness: Option<&super::smart_harness::SmartHarness>,
caveats: &crate::caveats::Caveats,
permission_gate: &mut Option<&mut dyn PermissionGate>,
tool_output_lines: usize,
color: bool,
tool_offload: bool,
spill_store: Option<&dyn super::content_spill::SpillStore>,
escalation_note: Option<&str>,
trim: Option<OutputTrim>,
wall: std::time::Duration,
presentation: &mut dyn ToolPresentation,
) -> (String, crate::ExecOutcome) {
presentation.execution_command(display, &cwd.to_string_lossy());
use crate::confined_exec::{build_tool_request, ConstrainedExecutor};
let (root, cwd) = match build_shell::build_directory(workspace, cwd, caveats) {
Ok(directory) => directory,
Err(refusal) => return refusal,
};
let request = build_tool_request(&root, &cwd, program, argv, &caveats.net).timeout(wall);
let build = request.caveats();
if let Some(harness) = smart_harness {
if let Err(error) = harness.validate_tool_authority(build, &root) {
return (
format!("Error: frame isolation: {error}"),
crate::ExecOutcome::Denied,
);
}
}
if !build.leq(caveats) {
let permission =
lifecycle_build_request(&root.to_string_lossy(), display, build, escalation_note);
let allowed = permission_gate.as_deref_mut().is_some_and(|gate| {
matches!(gate.ask_with_caveats(build, &[permission]), PermissionDecision::Allow(allowed) if build.leq(&allowed))
});
if !allowed {
return (
"capability denied: lifecycle action=build requires explicit confined build authority; no command ran".into(),
crate::ExecOutcome::Denied,
);
}
}
let mut run = ConstrainedExecutor::run_async(request.clone()).await;
let mut fetch_note = None;
if let Ok(out) = &run {
if !out.success && dependency_fetch::needs_dependency_fetch(&out.stderr) {
let read = |path: &std::path::Path| std::fs::read_to_string(path).ok();
match dependency_fetch::fetch_locked_dependencies(
&root,
&cwd,
caveats,
permission_gate,
read,
)
.await
{
Ok(()) => {
run = ConstrainedExecutor::run_async(request).await;
fetch_note = Some(dependency_fetch::FETCHED_NOTE.to_owned());
}
Err(reason) => fetch_note = Some(dependency_fetch::blocked_note(&reason, &cwd)),
}
}
}
render_confined_build_result(
run,
fetch_note.as_deref(),
trim,
tool_output_lines,
color,
tool_offload,
spill_store,
presentation,
)
}
#[allow(clippy::too_many_arguments)]
fn render_confined_build_result(
run: Result<crate::confined_exec::ConfinedOutput, crate::confined_exec::ExecRefused>,
fetch_note: Option<&str>,
trim: Option<OutputTrim>,
tool_output_lines: usize,
color: bool,
tool_offload: bool,
spill_store: Option<&dyn super::content_spill::SpillStore>,
presentation: &mut dyn ToolPresentation,
) -> (String, crate::ExecOutcome) {
let (mut text, outcome) = match run {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let view = trim.map(|trim| trim.apply(&stdout, &stderr));
let recovery_view = fetch_note.map(|note| {
let selected = view.clone().unwrap_or_else(|| format!("{stdout}{stderr}"));
format!("{selected}\n{note}")
});
let envelope = serde_json::json!({
"exit_code": out.code,
"stdout": stdout,
"stderr": stderr,
"timed_out": out.timed_out,
});
(
shell::shell_envelope_output_with_view(
&envelope,
recovery_view.as_deref().or(view.as_deref()),
tool_output_lines,
color,
tool_offload,
spill_store,
Some(presentation),
),
shell::envelope_outcome(&envelope),
)
}
Err(error) => (format!("error: {error}"), crate::ExecOutcome::Unavailable),
};
if let Some(note) = fetch_note.filter(|note| !text.contains(note)) {
text.push('\n');
text.push_str(note);
}
(text, outcome)
}
#[cfg(windows)]
fn build_check_argv(cmd: &str) -> (&'static str, Vec<String>) {
("cmd", vec!["/C".to_string(), cmd.to_string()])
}
#[cfg(not(windows))]
fn build_check_argv(cmd: &str) -> (&'static str, Vec<String>) {
("sh", vec!["-c".to_string(), cmd.to_string()])
}
#[cfg(all(test, windows))]
fn passing_build_check_cmd() -> &'static str {
"exit /B 0"
}
#[cfg(all(test, not(windows)))]
fn passing_build_check_cmd() -> &'static str {
"true"
}
#[cfg(all(test, windows))]
fn failing_build_check_cmd(message: &str) -> String {
format!("echo {message} 1>&2 & exit /B 1")
}
#[cfg(all(test, not(windows)))]
fn failing_build_check_cmd(message: &str) -> String {
format!("echo {message} >&2; exit 1")
}
fn denial_recovery_hint(capability: &str, target: &str) -> String {
let capability = serde_json::json!(capability);
let target = serde_json::json!(target);
format!(
"This is outside your granted authority — to ask the operator, call \
request_permissions(capability={capability}, target={target}, \
reason=\"<why you need it>\"), or take a different approach that stays \
within your current authority."
)
}
fn denial_context(
workspace: &str,
requested_cwd: Option<&str>,
caveats: Option<&crate::caveats::Caveats>,
) -> String {
fn scope(scope: &crate::caveats::Scope<String>) -> String {
match scope {
crate::caveats::Scope::All => "all".into(),
crate::caveats::Scope::Only(roots) => {
const DISPLAY_ROOTS: usize = 4;
let shown: Vec<_> = roots.iter().take(DISPLAY_ROOTS).collect();
let mut text = serde_json::json!(shown).to_string();
let omitted = roots.len().saturating_sub(shown.len());
if omitted > 0 {
text.push_str(&format!(" ({omitted} more roots)"));
}
text
}
}
}
let mut text = format!(
"Workspace root: {}\nDefault tool directory: workspace root",
serde_json::json!(workspace)
);
if let Some(cwd) = requested_cwd {
text.push_str(&format!(
"\nRequested command directory: {}",
serde_json::json!(cwd)
));
}
if let Some(caveats) = caveats {
text.push_str(&format!(
"\nKnown filesystem grants: fs_read={}, fs_write={}",
scope(&caveats.fs_read),
scope(&caveats.fs_write)
));
}
text
}
const CREW_OFF_RECOVERY_HINT: &str =
"the crew/team surface is not enabled this session (the operator launches it \
with NEWT_TEAM). Accomplish this yourself with the available tools \
(read_file/write_file/edit_file/run_command/...), or ask the operator to \
enable a crew.";
fn crew_off_recovery_result(name: &str) -> String {
format!("'{name}' is unavailable: {CREW_OFF_RECOVERY_HINT}")
}
fn denied_fs_result(kind: &str, path: &str) -> String {
format!(
"capability denied: {kind} does not permit '{path}'. {}",
denial_recovery_hint(kind, path)
)
}
pub(super) fn authorized_read(
tool: &str,
path: &str,
workspace: &str,
caveats: &crate::caveats::Caveats,
permission_gate: Option<&mut (dyn PermissionGate + '_)>,
) -> Result<String, String> {
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
let scope_permits = tui_permits_path(&caveats.fs_read, &full_str);
if !scope_permits {
let allowed = permission_gate.is_some_and(|gate| {
fs_gate_allows(gate, tool, DenialKind::FsRead, &full_str, |c| &c.fs_read)
});
if !allowed {
return Err(denied_fs_result("fs_read", &full_str));
}
}
if full_access_requested() {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::FsRead,
&full_str,
tool,
);
}
if scope_permits {
object_bound_read(&caveats.fs_read, "fs_read", path, &full, &full_str)
} else {
std::fs::read_to_string(&full)
.map_err(|e| FileIoError::io(&e, &full, format!("error: reading {path}: {e}")))
}
.map_err(|error| error.render(workspace, &caveats.fs_read))
}
fn fs_gate_allows(
gate: &mut dyn PermissionGate,
tool: &str,
kind: DenialKind,
full_path: &str,
axis: impl Fn(&crate::caveats::Caveats) -> &crate::caveats::Scope<String>,
) -> bool {
let request = PermissionRequest {
tool: tool.to_string(),
kind,
target: full_path.to_string(),
reason: format!("{} does not permit '{full_path}'", kind.as_str()),
harness_bound: false,
};
match gate.ask(std::slice::from_ref(&request)) {
PermissionDecision::Allow(widened) => tui_permits_path(axis(&widened), full_path),
PermissionDecision::Deny => false,
}
}
fn is_git_write_denial(out: &str) -> bool {
out.contains("capability denied: git ") && out.contains("not permitted")
}
fn is_git_data_loss_op(op: &str) -> bool {
matches!(op, "stash-drop" | "branch-delete")
}
fn git_data_loss_confirmed(gate: &mut dyn PermissionGate, op: &str) -> bool {
let request = PermissionRequest {
tool: "git".to_string(),
kind: DenialKind::GitWrite,
target: op.to_string(),
reason: format!(
"git {op} DESTROYS work irrecoverably (a dropped stash / deleted branch cannot be recovered) — confirm before proceeding"
),
harness_bound: false,
};
matches!(
gate.ask(std::slice::from_ref(&request)),
PermissionDecision::Allow(_)
)
}
fn git_gate_allows(gate: &mut dyn PermissionGate, op: &str) -> bool {
let request = PermissionRequest {
tool: "git".to_string(),
kind: DenialKind::GitWrite,
target: op.to_string(),
reason: format!("git {op} is outside the granted git-write authority"),
harness_bound: false,
};
matches!(
gate.ask(std::slice::from_ref(&request)),
PermissionDecision::Allow(_)
)
}
fn parse_capability(s: &str) -> Option<DenialKind> {
match s.trim().to_ascii_lowercase().as_str() {
"exec" | "run" | "run_command" | "command" | "shell" => Some(DenialKind::Exec),
"fs_read" | "fs-read" | "read" | "read_file" => Some(DenialKind::FsRead),
"fs_write" | "fs-write" | "write" | "write_file" => Some(DenialKind::FsWrite),
"net" | "network" | "web" | "web_fetch" => Some(DenialKind::Net),
_ => None,
}
}
fn permission_granted_result(capability: &str, target: &str) -> String {
let mut result = format!(
"granted: the operator allowed {capability} for '{target}'. \
Retry the original operation now."
);
let field = match parse_capability(capability) {
Some(DenialKind::FsRead) => Some("fs_read"),
Some(DenialKind::FsWrite) => Some("fs_write"),
_ => None,
};
if let Some(field) =
field.filter(|_| std::path::Path::new(target).is_absolute() && !target.contains('\0'))
{
result.push_str(&format!(
" For a native run_command retry, keep the command and add {field}={}; \
one-shot filesystem grants apply only to the declared invocation.",
serde_json::json!([target])
));
}
result
}
pub(super) fn permission_grant_succeeded(
name: &str,
args: &serde_json::Value,
ok: bool,
result: &str,
execution: Option<crate::ExecOutcome>,
) -> bool {
let capability = args["capability"].as_str().unwrap_or("").trim();
let target = args["target"].as_str().unwrap_or("").trim();
if name != "request_permissions"
|| !ok
|| parse_capability(capability).is_none()
|| target.is_empty()
{
return false;
}
result == permission_granted_result(capability, target)
|| execution == Some(crate::ExecOutcome::Passed)
}
pub(crate) struct PendingRerun {
cmd: String,
cwd: String,
declared: Vec<PermissionRequest>,
missing: Vec<PermissionRequest>,
}
pub(super) struct EligibleReplay(());
impl EligibleReplay {
fn new(covers_missing: bool) -> Option<Self> {
covers_missing.then_some(Self(()))
}
}
fn single_grant_covers_missing(
kind: DenialKind,
target: &str,
missing: &[PermissionRequest],
) -> bool {
if missing.is_empty() {
return false;
}
missing.iter().all(|request| {
if request.kind != kind {
return false;
}
match kind {
DenialKind::FsRead | DenialKind::FsWrite => {
let scope = crate::caveats::Scope::only([target.to_string()]);
crate::caveats::permits_path(&scope, &request.target)
}
DenialKind::Exec => request.target == target,
_ => false,
}
})
}
fn exec_denial_is_replay_safe(target: &str, cmd: &str) -> bool {
let Ok(inspection) = agent_bridle::inspect_shell(cmd) else {
return false;
};
if !inspection.warnings.is_empty() || !inspection.constructs.is_empty() {
return false;
}
let [only] = inspection.commands.as_slice() else {
return false;
};
let Some(program) = only.program.as_deref() else {
return false;
};
let names_target = program == target
|| (!program.contains(['/', '\\'])
&& std::path::Path::new(target).is_absolute()
&& std::path::Path::new(target)
.file_name()
.and_then(|name| name.to_str())
== Some(program));
names_target && !only.redirects.iter().any(redirect_has_effect)
}
fn requests_are_replay_safe(requests: &[PermissionRequest], cmd: &str) -> bool {
requests.iter().all(|request| {
request.kind == DenialKind::Exec && exec_denial_is_replay_safe(&request.target, cmd)
})
}
fn redirect_has_effect(redirect: &agent_bridle::InspectedRedirect) -> bool {
matches!(
redirect.operation,
agent_bridle::RedirectOperation::Write
| agent_bridle::RedirectOperation::Append
| agent_bridle::RedirectOperation::ReadWrite
| agent_bridle::RedirectOperation::Clobber
| agent_bridle::RedirectOperation::OutputAndError
| agent_bridle::RedirectOperation::AppendOutputAndError
)
}
fn execute_request_permissions(
caveats: &crate::Caveats,
args: &serde_json::Value,
gate: Option<&mut dyn PermissionGate>,
_color: bool,
_tool_output_lines: usize,
workspace: &str,
bound: Option<&PendingRerun>,
) -> (
Option<crate::caveats::Caveats>,
Option<EligibleReplay>,
String,
) {
let capability = args["capability"].as_str().unwrap_or("").trim();
let target = args["target"].as_str().unwrap_or("").trim();
let reason = args["reason"].as_str().unwrap_or("").trim();
if target == "worktree-lift" || target == "/permissions worktree-lift" {
return (
None,
None,
"request_permissions: worktree-lift is an operator command, not a capability target. The operator must use /permissions worktree-lift; no permission was granted and no worktree protection was lifted.".into(),
);
}
let Some(kind) = parse_capability(capability) else {
return (
None,
None,
format!(
"request_permissions: unknown capability '{capability}'. Use one of: \
exec, fs_read, fs_write, net."
),
);
};
if target.is_empty() {
return (
None,
None,
"request_permissions: 'target' is required — the executable path or command name (exec), \
the path (fs_read/fs_write), or the host (net)."
.to_string(),
);
}
let eligible = bound.filter(|pending| {
single_grant_covers_missing(kind, target, &pending.missing)
&& (kind != DenialKind::Exec || exec_denial_is_replay_safe(target, &pending.cmd))
});
let request = PermissionRequest {
tool: "request_permissions".to_string(),
kind,
target: target.to_string(),
reason: match eligible {
Some(pending) => format!(
"{BOUND_REASON_PREFIX}approving this widens {capability} for '{target}' and \
IMMEDIATELY re-runs the command it was denied for, ONCE, under exactly that \
grant: `{}` in `{}`. It will not run again without a fresh approval.",
pending.cmd, pending.cwd
),
None if bound.is_some() => format!(
"model requested {capability} for '{target}'. This grant alone does not \
cover everything the previously denied command needs, so it will NOT be \
automatically re-run — retry it yourself with a fresh run_command call."
),
None if reason.is_empty() => format!("model requested {capability} for '{target}'"),
None => reason.to_string(),
},
harness_bound: eligible.is_some(),
};
let quoted_target = serde_json::json!(target);
let mut out = match gate {
Some(g) => match g.ask(std::slice::from_ref(&request)) {
PermissionDecision::Allow(widened) => {
let covers_missing = eligible.is_some_and(|pending| {
pending.missing.iter().all(|r| match r.kind {
DenialKind::Exec => widened.permits_exec(&r.target),
_ => permits_filesystem_request(&widened, r),
})
});
let replay_auth = EligibleReplay::new(covers_missing);
if replay_auth.is_some() {
g.consume_pending_once(kind, target);
}
return (Some(widened), replay_auth, permission_granted_result(capability, target));
}
PermissionDecision::Deny => (
None,
None,
format!(
"denied: the operator declined {capability} for {quoted_target}. \
Do not retry it — take a different approach."
),
),
},
None if bound.is_none() && match kind {
DenialKind::Exec => caveats.permits_exec(target),
DenialKind::Net => caveats.permits_net(target),
_ => permits_filesystem_request(caveats, &request),
} => (
None,
None,
format!("already granted: {capability} for {quoted_target}. Retry the original operation now."),
),
None => (
None,
None,
format!(
"no operator available to grant {capability} for {quoted_target} — this session \
has no interactive permission gate (headless / eval / piped), so authority \
cannot be widened mid-run and re-calling request_permissions will not help. \
Proceed within the authority you already have and the tools available to you; \
if {quoted_target} is genuinely essential and outside your current scope, say so in \
your final answer rather than retrying it."
),
),
};
if matches!(kind, DenialKind::FsRead | DenialKind::FsWrite) {
out.2.push('\n');
out.2.push_str(&denial_context(workspace, None, None));
}
out
}
const HEADLESS_NO_HUMAN: &str = "no human available this session (running headless) \
— proceed with your best judgment or state your assumption explicitly.";
const NO_OPERATOR_AVAILABLE: &str = "no operator is available to answer this session \
— proceed with your best judgment or state your assumption explicitly.";
const OPERATOR_CANCELLED: &str = "the operator cancelled this question; no answer was provided.";
const OPERATOR_EXIT_REQUESTED: &str = "the operator requested exit; stop the current interaction.";
const OPERATOR_INPUT_CLOSED: &str =
"the operator input stream closed before an answer was provided.";
const OPERATOR_INPUT_FAILED: &str = "operator input failed; no answer was provided.";
fn execute_request_user_input(
args: &serde_json::Value,
gate: Option<&mut dyn PermissionGate>,
_color: bool,
_tool_output_lines: usize,
) -> String {
let question = args["question"].as_str().unwrap_or("").trim();
if question.is_empty() {
return "request_user_input: 'question' is required — the free-text \
question to ask the human."
.to_string();
}
let Some(gate) = gate else {
return HEADLESS_NO_HUMAN.to_string();
};
match gate.ask_question(question) {
HumanQuestionOutcome::Answer(answer) => answer,
HumanQuestionOutcome::Unavailable => NO_OPERATOR_AVAILABLE.to_string(),
HumanQuestionOutcome::Cancelled => OPERATOR_CANCELLED.to_string(),
HumanQuestionOutcome::ExitRequested => OPERATOR_EXIT_REQUESTED.to_string(),
HumanQuestionOutcome::InputClosed => OPERATOR_INPUT_CLOSED.to_string(),
HumanQuestionOutcome::InputFailed => OPERATOR_INPUT_FAILED.to_string(),
}
}
pub(crate) fn host_of_url(url: &str) -> Option<String> {
let rest = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))?;
let authority = rest.split(['/', '?', '#']).next()?;
let host_port = authority.rsplit('@').next()?;
let host = if let Some(stripped) = host_port.strip_prefix('[') {
stripped.split(']').next()?
} else {
host_port.split(':').next()?
};
if host.is_empty() {
None
} else {
Some(host.to_ascii_lowercase())
}
}
fn parse_net_denial_host(reason: &str) -> Option<String> {
let host = reason
.strip_prefix("network access to \"")?
.strip_suffix("\" is not within the granted authority")?;
(!host.is_empty() && !host.contains('"')).then(|| host.to_string())
}
fn web_fetch_denial_host(_url: &str, err: &agent_bridle::ToolError) -> Option<String> {
match err {
agent_bridle::ToolError::Denied { reason } => parse_net_denial_host(reason),
_ => None,
}
}
pub const MCP_RESOURCE_URL_PREFIXES_META_KEY: &str = "newt/resourceUrlPrefixes";
pub fn preserve_mcp_resource_url_affinity(
definition: &mut serde_json::Value,
mcp_tool_meta: Option<&serde_json::Value>,
) {
let Some(mcp_tool_meta) = mcp_tool_meta else {
return;
};
if validated_resource_url_prefixes(mcp_tool_meta).is_none() {
return;
}
let prefixes = mcp_tool_meta[MCP_RESOURCE_URL_PREFIXES_META_KEY].clone();
let Some(definition) = definition.as_object_mut() else {
return;
};
let meta = definition
.entry("_meta")
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
let Some(meta) = meta.as_object_mut() else {
return;
};
meta.insert(MCP_RESOURCE_URL_PREFIXES_META_KEY.to_string(), prefixes);
}
pub(super) fn strip_mcp_catalog_metadata(definition: &mut serde_json::Value) {
if let Some(definition) = definition.as_object_mut() {
definition.remove("_meta");
}
}
fn resource_url_prefix(prefix: &str) -> Option<reqwest::Url> {
if prefix.trim() != prefix {
return None;
}
let parsed = reqwest::Url::parse(prefix).ok()?;
if !matches!(parsed.scheme(), "http" | "https")
|| parsed.host_str().is_none()
|| !parsed.username().is_empty()
|| parsed.password().is_some()
|| parsed.query().is_some()
|| parsed.fragment().is_some()
{
return None;
}
Some(parsed)
}
fn validated_resource_url_prefixes(meta: &serde_json::Value) -> Option<Vec<reqwest::Url>> {
let prefixes = meta.get(MCP_RESOURCE_URL_PREFIXES_META_KEY)?.as_array()?;
if prefixes.is_empty() {
return None;
}
prefixes
.iter()
.map(|prefix| prefix.as_str().and_then(resource_url_prefix))
.collect()
}
fn resource_url_has_prefix(url: &reqwest::Url, prefix: &reqwest::Url) -> bool {
if url.scheme() != prefix.scheme()
|| url.host_str() != prefix.host_str()
|| url.port_or_known_default() != prefix.port_or_known_default()
{
return false;
}
let target_path = url.path();
let prefix_path = prefix.path();
if prefix_path == "/" {
return true;
}
let prefix_base = prefix_path.strip_suffix('/').unwrap_or(prefix_path);
target_path == prefix_base || target_path.starts_with(&format!("{prefix_base}/"))
}
fn tool_declares_resource_url(tool: &serde_json::Value, url: &reqwest::Url) -> bool {
tool.get("_meta")
.and_then(validated_resource_url_prefixes)
.is_some_and(|prefixes| {
prefixes
.iter()
.any(|prefix| resource_url_has_prefix(url, prefix))
})
}
fn resource_url_discovery_query(url: &reqwest::Url) -> String {
const MAX_TERMS: usize = 8;
const MAX_TERM_CHARS: usize = 32;
const GENERIC_TERMS: &[&str] = &["com", "net", "org", "www"];
let mut terms = Vec::new();
let mut seen = std::collections::BTreeSet::new();
let sources = std::iter::once(url.path()).chain(url.host_str());
for source in sources {
for raw in source.split(|ch: char| !ch.is_ascii_alphanumeric()) {
let term = raw.to_ascii_lowercase();
if term.is_empty()
|| term.chars().count() > MAX_TERM_CHARS
|| GENERIC_TERMS.contains(&term.as_str())
{
continue;
}
for candidate in [
Some(term.as_str()),
term.strip_suffix('s').filter(|stem| stem.len() >= 3),
]
.into_iter()
.flatten()
{
if seen.insert(candidate.to_string()) {
terms.push(candidate.to_string());
if terms.len() == MAX_TERMS {
return terms.join(" ");
}
}
}
}
}
terms.join(" ")
}
fn callable_mcp_catalog(
mcp: &dyn McpTools,
persona_tools: Option<&[String]>,
disposition: PromptDisposition,
) -> serde_json::Value {
let defs = serde_json::Value::Array(mcp.tool_defs());
let defs = filter_advertised_tools(defs, persona_tools);
filter_tools_for_disposition(defs, disposition)
}
fn authenticated_url_recovery(
failure: String,
url: &str,
mcp: &dyn McpTools,
persona_tools: Option<&[String]>,
disposition: PromptDisposition,
) -> String {
let Some(url) = reqwest::Url::parse(url)
.ok()
.filter(|url| matches!(url.scheme(), "http" | "https"))
.filter(|url| url.host_str().is_some())
.filter(|url| url.username().is_empty() && url.password().is_none())
else {
return failure;
};
let catalog = callable_mcp_catalog(mcp, persona_tools, disposition);
let matching = catalog
.as_array()
.into_iter()
.flatten()
.filter(|tool| {
tool.pointer("/function/name")
.and_then(serde_json::Value::as_str)
.is_some_and(|name| !name.is_empty())
})
.filter(|tool| tool_declares_resource_url(tool, &url))
.cloned()
.collect::<Vec<_>>();
if catalog.as_array().is_none_or(Vec::is_empty) {
return failure;
}
if matching.is_empty() {
let query = resource_url_discovery_query(&url);
let candidates = super::tool_search::execute_tool_search(&query, &catalog);
return format!(
"{failure}\n\nAuthenticated-source recovery (non-authoritative discovery): raw HTTP \
cannot access this resource and no connected MCP tool explicitly declares access to \
its URL. The connected catalog may still contain an imported authenticated source. \
Next call `tool_search` with the URL-derived query `{query}` and inspect the returned \
tool contracts. This is discovery only: do not assume that a candidate can read or \
authenticate to the URL, and do not call one unless its description and parameters \
fit the resource. Do not fall back to `run_command`/curl or `request_user_input` for \
unrelated local shell/client configuration until connected-source discovery has \
been tried.\n{candidates}"
);
}
let matching = serde_json::Value::Array(matching);
let candidates = super::tool_search::execute_tool_search("", &matching);
format!(
"{failure}\n\nAuthenticated-source recovery: raw HTTP cannot access this resource, but \
the connected MCP catalog explicitly declares one or more URL-affine tools. Next call \
`tool_search` with an exact candidate name from the authoritative list below, inspect \
its contract, then call the matching namespaced MCP tool for the resource. Do not fall \
back to `run_command`/curl or `request_user_input` for local shell/client configuration \
until the connected MCP routes have been tried.\n{candidates}"
)
}
fn is_private_address_fetch_error(error: &str) -> bool {
let lower = error.to_ascii_lowercase();
lower.contains("ssrf block")
&& (lower.contains("private/loopback address") || lower.contains("private address"))
}
fn is_authentication_fetch_error(error: &str) -> bool {
let lower = error.to_ascii_lowercase();
[
"http 401",
"http status 401",
"401 unauthorized",
"http 403",
"http status 403",
"403 forbidden",
]
.iter()
.any(|marker| lower.contains(marker))
}
fn render_web_fetch_result(
url: &str,
result: &serde_json::Value,
mcp: &dyn McpTools,
persona_tools: Option<&[String]>,
disposition: PromptDisposition,
) -> String {
let status = result.get("status").and_then(serde_json::Value::as_u64);
if matches!(status, Some(401 | 403)) {
return authenticated_url_recovery(
format!(
"error: web_fetch returned HTTP {}",
status.unwrap_or_default()
),
url,
mcp,
persona_tools,
disposition,
);
}
let markdown = result
.get("markdown")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let title = result
.get("title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let final_url = result
.get("final_url")
.and_then(serde_json::Value::as_str)
.unwrap_or(url);
if title.is_empty() {
format!("{final_url}\n\n{markdown}")
} else {
format!("# {title}\n{final_url}\n\n{markdown}")
}
}
fn render_web_fetch_error(
url: &str,
error: &str,
mcp: &dyn McpTools,
persona_tools: Option<&[String]>,
disposition: PromptDisposition,
) -> String {
let failure = format!("error: {error}");
if is_private_address_fetch_error(error) || is_authentication_fetch_error(error) {
authenticated_url_recovery(failure, url, mcp, persona_tools, disposition)
} else {
failure
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum FindType {
Files,
Dirs,
Any,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum FindCategory {
Any,
Source,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum FindSort {
Name,
Size,
Lines,
}
struct FindOpts<'a> {
name: Option<&'a str>,
type_filter: FindType,
category: FindCategory,
language: Option<&'a str>,
max_depth: Option<usize>,
max_results: usize,
respect_gitignore: bool,
case_sensitive: bool,
show_size: bool,
show_lines: bool,
sort: FindSort,
}
fn find_detail(path: &str, opts: &FindOpts) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(name) = opts.name {
parts.push(format!("name={name}"));
}
match opts.type_filter {
FindType::Files => parts.push("type=f".to_string()),
FindType::Dirs => parts.push("type=d".to_string()),
FindType::Any => {}
}
if opts.category == FindCategory::Source {
parts.push("category=source".to_string());
}
if let Some(language) = opts.language {
parts.push(format!("language={language}"));
}
if let Some(d) = opts.max_depth {
parts.push(format!("depth={d}"));
}
if opts.max_results != 1000 {
parts.push(format!("max={}", opts.max_results));
}
if !opts.respect_gitignore {
parts.push("no-gitignore".to_string());
}
if !opts.case_sensitive {
parts.push("icase".to_string());
}
match opts.sort {
FindSort::Size => parts.push("sort=size".to_string()),
FindSort::Lines => parts.push("sort=lines".to_string()),
FindSort::Name => {}
}
if opts.show_size {
parts.push("size".to_string());
}
if opts.show_lines {
parts.push("lines".to_string());
}
if parts.is_empty() {
path.to_string()
} else {
format!("{path} ({})", parts.join(", "))
}
}
fn find_opts_from_args(args: &serde_json::Value) -> FindOpts<'_> {
FindOpts {
name: args["name"].as_str(),
type_filter: match args["type"].as_str() {
Some("f") => FindType::Files,
Some("d") => FindType::Dirs,
_ => FindType::Any,
},
category: if args["category"].as_str() == Some("source")
|| args["language"].as_str().is_some()
|| args["code"].as_bool() == Some(true)
{
FindCategory::Source
} else {
FindCategory::Any
},
language: args["language"].as_str(),
max_depth: args["max_depth"].as_u64().map(|d| d as usize),
max_results: args["max_results"]
.as_u64()
.map(|m| m as usize)
.unwrap_or(1000),
respect_gitignore: args["respect_gitignore"].as_bool().unwrap_or(true),
case_sensitive: args["case_sensitive"].as_bool().unwrap_or(true),
show_size: args["show_size"].as_bool().unwrap_or(false),
show_lines: args["show_lines"].as_bool().unwrap_or(false),
sort: match args["sort"].as_str() {
Some("size") => FindSort::Size,
Some("lines") => FindSort::Lines,
_ => FindSort::Name,
},
}
}
fn find_source_extensions(
workspace: &std::path::Path,
opts: &FindOpts<'_>,
) -> Result<Option<Vec<String>>, String> {
if opts.category == FindCategory::Any {
return Ok(None);
}
let api_cfg = super::display::with_migration_notices(crate::Config::resolve_unpublished)
.ok()
.and_then(|cfg| cfg.context.map(|context| context.api_surface))
.unwrap_or_default();
let packs = crate::api_surface::resolve_language_packs(workspace, &api_cfg);
crate::api_surface::source_extensions_for(&packs, opts.language).map(Some)
}
fn finalize_find(mut entries: Vec<(u64, String)>, opts: &FindOpts<'_>) -> (Vec<String>, bool) {
entries.sort_by(|a, b| a.1.cmp(&b.1));
entries.dedup_by(|a, b| a.1 == b.1);
if matches!(opts.sort, FindSort::Size | FindSort::Lines) {
entries.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
}
let truncated = entries.len() > opts.max_results;
entries.truncate(opts.max_results);
let show_metric = opts.show_size || opts.show_lines;
let lines = entries
.into_iter()
.map(|(metric, path)| {
if show_metric {
format!("{metric}\t{path}")
} else {
path
}
})
.collect();
(lines, truncated)
}
fn tool_call_detail(name: &str, args: &serde_json::Value, workspace: &std::path::Path) -> String {
let string = |key: &str, default: &str| {
args.get(key)
.and_then(serde_json::Value::as_str)
.unwrap_or(default)
.to_string()
};
match name {
"run_command" => string("command", ""),
"write_file" => {
let path = string("path", "");
let bytes = args["content"].as_str().unwrap_or("").len();
if let Some(source) = args["move_from"]["path"].as_str() {
return format!(
"{} (move functions from {})",
file_capture::display_text(&path),
file_capture::display_text(source)
);
}
let copy = &args["copy_from"];
match copy["path"].as_str() {
Some(source) => format!(
"{} ({bytes} bytes + {}:{}-{})",
file_capture::display_text(&path),
file_capture::display_text(source),
copy["start_line"],
copy["end_line"],
),
None => format!("{} ({bytes} bytes)", file_capture::display_text(&path)),
}
}
"edit_file" | "delete_file" => file_capture::display_text(&string("path", "")).into_owned(),
"read_file" => string("path", ""),
"list_dir" => string("path", "."),
"find" => {
let path = args["path"].as_str().unwrap_or(".");
find_detail(path, &find_opts_from_args(args))
}
"use_skill" => string("name", ""),
"web_fetch" => string("url", ""),
"request_permissions" => string("capability", ""),
"request_user_input" => string("question", ""),
"select_operating_mode" => string("mode", ""),
"prompt_read" | "artifact_read" => string("address", "current"),
"tool_search" | "recall" | "code_search" | "experience_recall" => string("query", ""),
"where_is" => string("symbol", ""),
"memory_fetch" => string("address", ""),
"save_note" => string("action", ""),
"git" => string("op", ""),
"state_set" | "state_get" => string("key", ""),
"experience_record" => string("task", ""),
"lifecycle" => {
let phase = string("phase", "");
let action = string("action", "run");
let resolved = crate::tooling::Phase::from_key(&phase)
.map(|phase| crate::tooling::resolved_phase_commands(workspace, phase))
.unwrap_or_default();
if resolved.is_empty() {
format!("{phase} ({action})")
} else {
format!("{phase} ({action}) → {}", resolved.join(" && "))
}
}
"render_report" => string("title", ""),
"resume_context"
| "state_clear"
| "update_plan"
| "plan_get"
| "get_context_remaining"
| "enter_plan_mode"
| "exit_plan_mode" => String::new(),
_ => args.to_string(),
}
}
fn correction_alias_detail(args: &serde_json::Value) -> String {
if let Some(path) = args.get("path").and_then(serde_json::Value::as_str) {
if let Some(content) = args.get("content").and_then(serde_json::Value::as_str) {
return format!("{path} ({} bytes)", content.len());
}
let mut sizes = Vec::new();
for key in ["old_string", "new_string"] {
if let Some(value) = args.get(key).and_then(serde_json::Value::as_str) {
sizes.push(format!("{key}={} bytes", value.len()));
}
}
if sizes.is_empty() {
return path.to_string();
}
return format!("{path} ({})", sizes.join(", "));
}
let keys = args
.as_object()
.map(|object| object.keys().cloned().collect::<Vec<_>>())
.unwrap_or_default();
if keys.is_empty() {
"{}".to_string()
} else {
format!("arguments: {}", keys.join(", "))
}
}
pub(crate) fn tool_presentation(
raw_name: &str,
raw_args: &serde_json::Value,
workspace: &std::path::Path,
read_scope: &crate::caveats::Scope<String>,
) -> (String, String) {
let (name, correction) = match resolve_tool_alias(raw_name) {
Some(AliasOutcome::Rewrite(canonical)) => (canonical, false),
Some(AliasOutcome::Correct(_)) => (raw_name, true),
None => (raw_name, false),
};
if correction {
return (name.to_string(), correction_alias_detail(raw_args));
}
if name == "run_command" && !routing_disabled() {
if let super::routing::RouteDecision::Route { tool, args } =
super::routing::RouteTable::builtin().classify_call(raw_args, workspace, read_scope)
{
let detail = tool_call_detail(tool, &args, workspace);
return (tool.to_string(), detail);
}
}
(
name.to_string(),
tool_call_detail(name, raw_args, workspace),
)
}
fn glob_to_regex(glob: &str, case_sensitive: bool) -> Result<regex::Regex, String> {
let mut re = String::with_capacity(glob.len() + 8);
if !case_sensitive {
re.push_str("(?i)");
}
re.push('^');
for ch in glob.chars() {
match ch {
'*' => re.push_str(".*"),
'?' => re.push('.'),
'.' | '+' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' | '\\' => {
re.push('\\');
re.push(ch);
}
other => re.push(other),
}
}
re.push('$');
regex::Regex::new(&re).map_err(|e| format!("invalid name pattern: {e}"))
}
pub(super) fn workspace_walker(
root: &std::path::Path,
respect_gitignore: bool,
max_depth: Option<usize>,
) -> ignore::WalkBuilder {
let mut builder = ignore::WalkBuilder::new(root);
builder
.hidden(respect_gitignore)
.ignore(respect_gitignore)
.git_ignore(respect_gitignore)
.git_global(respect_gitignore)
.git_exclude(respect_gitignore)
.parents(respect_gitignore)
.require_git(false)
.follow_links(false);
if let Some(d) = max_depth {
builder.max_depth(Some(d));
}
if respect_gitignore {
let mut ob = ignore::overrides::OverrideBuilder::new(root);
if ob.add("!target/").is_ok() && ob.add("!node_modules/").is_ok() {
if let Ok(ov) = ob.build() {
builder.overrides(ov);
}
}
}
builder
}
fn find_walk(
root: &std::path::Path,
workspace_root: &std::path::Path,
opts: &FindOpts<'_>,
source_extensions: Option<&[String]>,
mut on_hit: impl FnMut(&str),
) -> Result<(Vec<String>, bool), String> {
let pattern = match opts.name {
Some(g) if !g.is_empty() => Some(glob_to_regex(g, opts.case_sensitive)?),
_ => None,
};
let builder = workspace_walker(root, opts.respect_gitignore, opts.max_depth);
let mut entries: Vec<(u64, String)> = Vec::new();
for result in builder.build() {
let entry = match result {
Ok(e) => e,
Err(_) => continue,
};
if entry.depth() == 0 {
continue;
}
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
if let Some(extensions) = source_extensions {
if is_dir
|| !entry
.path()
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extensions
.iter()
.any(|known| known.eq_ignore_ascii_case(extension))
})
{
continue;
}
}
match opts.type_filter {
FindType::Files if is_dir => continue,
FindType::Dirs if !is_dir => continue,
_ => {}
}
if let Some(re) = &pattern {
let base = entry.file_name().to_string_lossy();
if !re.is_match(&base) {
continue;
}
}
let rel = entry
.path()
.strip_prefix(workspace_root)
.unwrap_or_else(|_| entry.path());
let rel_display = rel.to_string_lossy().replace('\\', "/");
let want_lines = opts.show_lines || matches!(opts.sort, FindSort::Lines);
let want_size = opts.show_size || matches!(opts.sort, FindSort::Size);
let metric = if want_lines {
if is_dir {
0
} else {
count_lines(entry.path())
}
} else if want_size {
entry.metadata().map(|m| m.len()).unwrap_or(0)
} else {
0
};
on_hit(&rel_display);
entries.push((metric, rel_display));
}
Ok(finalize_find(entries, opts))
}
fn count_lines(path: &std::path::Path) -> u64 {
match std::fs::read(path) {
Ok(bytes) => count_newlines(&bytes),
Err(_) => 0,
}
}
fn count_newlines(bytes: &[u8]) -> u64 {
bytes.iter().filter(|&&b| b == b'\n').count() as u64
}
#[cfg(unix)]
fn artifact_open_regular_file(path: &std::path::Path) -> std::io::Result<std::fs::File> {
open_regular_file(path, true)
}
#[cfg(unix)]
fn open_regular_file(path: &std::path::Path, nofollow: bool) -> std::io::Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt as _;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NONBLOCK | if nofollow { libc::O_NOFOLLOW } else { 0 })
.open(path)?;
if !file.metadata()?.file_type().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"artifact path is not a regular file",
));
}
Ok(file)
}
#[cfg(windows)]
fn artifact_open_regular_file(path: &std::path::Path) -> std::io::Result<std::fs::File> {
open_regular_file(path, true)
}
#[cfg(windows)]
fn open_regular_file(path: &std::path::Path, nofollow: bool) -> std::io::Result<std::fs::File> {
use std::os::windows::fs::OpenOptionsExt as _;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(if nofollow {
FILE_FLAG_OPEN_REPARSE_POINT
} else {
0
})
.open(path)?;
if !file.metadata()?.file_type().is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"artifact path is not a regular file",
));
}
Ok(file)
}
#[cfg(not(any(unix, windows)))]
fn artifact_open_regular_file(_path: &std::path::Path) -> std::io::Result<std::fs::File> {
open_regular_file(_path, true)
}
#[cfg(not(any(unix, windows)))]
fn open_regular_file(_path: &std::path::Path, _nofollow: bool) -> std::io::Result<std::fs::File> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"race-safe artifact file capture is unavailable on this platform",
))
}
fn artifact_open_scoped_regular_file(
scope: &crate::caveats::Scope<String>,
path: &std::path::Path,
) -> std::io::Result<std::fs::File> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
if let crate::caveats::Scope::Only(_) = scope {
let Some(Some((root, relative))) = object_bound_target(scope, &path.to_string_lossy())
else {
return Err(std::io::Error::from_raw_os_error(libc::EACCES));
};
return crate::fs_cap::WorkspaceDir::open_granted_file(
std::path::Path::new(root),
&relative,
true,
);
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let _ = scope;
artifact_open_regular_file(path)
}
fn artifact_preimage_state(
scope: &crate::caveats::Scope<String>,
path: &std::path::Path,
read_authorized: bool,
) -> super::artifact_hooks::ArtifactFileState {
use std::io::Read as _;
if !read_authorized {
return super::artifact_hooks::ArtifactFileState::unavailable("fs_read_not_granted");
}
match std::fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() => {
super::artifact_hooks::ArtifactFileState::unavailable("symlink_preimage_not_hashed")
}
Ok(metadata) if !metadata.file_type().is_file() => {
super::artifact_hooks::ArtifactFileState::unavailable("preimage_not_regular_file")
}
Ok(_) => {
let mut file = match artifact_open_scoped_regular_file(scope, path) {
Ok(file) => file,
Err(_) => {
return super::artifact_hooks::ArtifactFileState::unavailable(
"preimage_read_failed",
)
}
};
let mut hasher = blake3::Hasher::new();
let mut bytes = 0_u64;
let mut buffer = [0_u8; 64 * 1024];
loop {
match file.read(&mut buffer) {
Ok(0) => break,
Ok(read) => {
hasher.update(&buffer[..read]);
bytes = bytes.saturating_add(u64::try_from(read).unwrap_or(u64::MAX));
}
Err(_) => {
return super::artifact_hooks::ArtifactFileState::unavailable(
"preimage_read_failed",
)
}
}
}
super::artifact_hooks::ArtifactFileState::from_digest(
hasher.finalize().to_hex().to_string(),
bytes,
)
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
super::artifact_hooks::ArtifactFileState::absent()
}
Err(_) => super::artifact_hooks::ArtifactFileState::unavailable("preimage_read_failed"),
}
}
fn artifact_file_matches(
scope: &crate::caveats::Scope<String>,
path: &std::path::Path,
expected: &[u8],
) -> std::io::Result<bool> {
file_contents_match(artifact_open_scoped_regular_file(scope, path)?, expected)
}
fn file_contents_match(mut file: std::fs::File, expected: &[u8]) -> std::io::Result<bool> {
use std::io::Read as _;
let mut offset = 0_usize;
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
return Ok(offset == expected.len());
}
let Some(end) = offset.checked_add(read) else {
return Ok(false);
};
if expected.get(offset..end) != Some(&buffer[..read]) {
return Ok(false);
}
offset = end;
}
}
fn artifact_path_is_physically_within_workspace(
workspace: &std::path::Path,
target: &std::path::Path,
) -> bool {
let Ok(workspace) = workspace.canonicalize() else {
return false;
};
let mut probe = target;
loop {
match std::fs::symlink_metadata(probe) {
Ok(_) => {
return probe
.canonicalize()
.is_ok_and(|resolved| resolved.starts_with(&workspace));
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let Some(parent) = probe.parent() else {
return false;
};
probe = parent;
}
Err(_) => return false,
}
}
}
#[allow(clippy::too_many_arguments)]
fn record_governed_file_change(
sink: Option<&dyn super::artifact_read::PromptArtifactSink>,
context: Option<ArtifactReadContext<'_>>,
path: &str,
operation: &'static str,
before: Option<super::artifact_hooks::ArtifactFileState>,
after: super::artifact_hooks::ArtifactFileState,
_color: bool,
_tool_output_lines: usize,
) -> String {
let (Some(sink), Some(context), Some(before)) = (sink, context, before) else {
return String::new();
};
match super::artifact_hooks::record_file_change(sink, context, path, operation, before, after) {
Ok(_) => String::new(),
Err(error) => {
let warning = format!("warning: failed to record file-change artifact: {error}");
format!("\n{warning}")
}
}
}
const EXIT_PLAN_MODE_REQUESTED: &str = "exit requested. Awaiting operator approval — tool calls \
remain clamped to Plan reads and the plan ledger until approved. If asked, continue drafting \
with update_plan / render_report; do not assume approval.";
const PLAN_APPROVAL_REQUESTED: &str = "plan recorded — awaiting operator approval before \
implementation. Tool calls are limited to reads and the plan ledger until approved; do not \
assume approval. If asked, refine the plan with update_plan.";
pub fn exit_plan_mode_result(initiative: crate::initiative::Initiative) -> String {
let base = "exited the model-entered PLAN PHASE. Subsequent tool calls return to this turn's validated disposition and underlying session permissions; the next outer turn returns to the human-selected operating mode. `/mode plan` and other clamps still remain read-only.";
if initiative.exit_plan_requires_edit() {
format!(
"{base}\n\nThe plan is set — now EXECUTE it. Your NEXT action must be a concrete \
change (edit_file or write_file) that begins the first step — not another \
read, search, or plan. If a prerequisite is missing, make the smallest edit \
that unblocks it."
)
} else {
base.to_string()
}
}
fn artifact_postcondition_warning(
path: &str,
detail: &str,
_color: bool,
_tool_output_lines: usize,
) -> String {
let warning =
format!("warning: {path} changed, but no file-change artifact was recorded: {detail}");
format!("\n{warning}")
}
#[allow(clippy::too_many_arguments)]
async fn execute_tool_unadopted(
presentation: &mut dyn ToolPresentation,
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
mut collab: ToolCollaborators<'_, '_>,
tool_offload: bool,
disposition: PromptDisposition,
) -> String {
let Some(invocation) = collab.invocation else {
return execute_authorized_tool(
presentation,
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
collab,
tool_offload,
disposition,
)
.await;
};
let harness = invocation.harness();
let execution = collab.execution;
if let Err(error) = harness.validate_tool_authority(caveats, std::path::Path::new(workspace)) {
invocation.host();
if let Some(slot) = execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
return format!("Error: frame isolation: {error}");
}
let mut gate =
collab
.permission_gate
.take()
.map(|inner| super::smart_harness::FramePermissionGate {
harness,
workspace: std::path::Path::new(workspace),
inner,
refusal: None,
});
let result = execute_authorized_tool(
presentation,
name,
args,
workspace,
color,
tool_output_lines,
caveats,
mcp,
ToolCollaborators {
permission_gate: gate.as_mut().map(|gate| gate as &mut dyn PermissionGate),
..collab
},
tool_offload,
disposition,
)
.await;
match gate.and_then(|gate| gate.refusal) {
Some(error) => {
invocation.host();
if let Some(slot) = execution {
let _ = slot.set(crate::ExecOutcome::Denied);
}
format!("Error: frame isolation: permission denied: {error}")
}
None => result,
}
}
#[allow(clippy::too_many_arguments)]
async fn execute_authorized_tool(
presentation: &mut dyn ToolPresentation,
name: &str,
args: &serde_json::Value,
workspace: &str,
color: bool,
tool_output_lines: usize,
caveats: &crate::caveats::Caveats,
mcp: &mut dyn McpTools,
collab: ToolCollaborators<'_, '_>,
tool_offload: bool,
disposition: PromptDisposition,
) -> String {
let ToolCollaborators {
command_budget,
shell_dialect,
read_history,
default_command_cwd,
worktree_session,
invocation,
build_check_cmd,
tool_evidence,
note_sink,
recall_source,
memory_source,
prompt_context,
artifact_context,
artifact_sink,
mut permission_gate,
exec_floor,
git_tool,
crew_runner,
scratchpad_store,
code_search,
where_is,
nav,
experience_store,
step_ledger,
operating_mode_control,
plan_mode_control,
plan_draft_sink,
spill_store,
persona_tools,
hidden_tools,
live_tool_output,
completed_spill_renderer: _,
execution,
report_capture,
governed_pr,
command_directory,
routed_to: routed_to_slot,
pending_rerun,
retry_authorized,
} = collab;
let mut report_presentation = super::observed_report::Presentation {
inner: presentation,
capture: report_capture,
};
let presentation: &mut dyn ToolPresentation = &mut report_presentation;
let pending_rerun = pending_rerun.map(|slot| {
if name != "request_permissions" {
*slot = None;
}
slot
});
let adopted = worktree_session.and_then(crate::worktree_adoption::WorktreeSession::snapshot);
let build_workspace = adopted.as_ref().map_or(workspace, |policy| {
policy.worktree.to_str().unwrap_or(workspace)
});
let smart_harness = invocation.map(|call| call.harness());
let executed = |(text, outcome): (String, crate::ExecOutcome)| {
if let Some(slot) = execution {
let _ = slot.set(outcome);
}
text
};
let host_return = |text: String| {
if let Some(invocation) = invocation {
invocation.host();
}
text
};
let disposition = if plan_mode_control.is_some_and(super::PlanModeControl::is_plan_mode)
&& disposition != PromptDisposition::Ask
{
PromptDisposition::Plan
} else {
disposition
};
let (raw_name, raw_args) = (name, args);
let remote_call = mcp.handles(name);
let evidence_turn = matches!(
disposition,
PromptDisposition::Explain | PromptDisposition::Research
);
if !tool_allowed(disposition, name)
|| (evidence_turn && is_mcp_tool_name(name) && !remote_call)
|| (name == "git"
&& disposition == PromptDisposition::Plan
&& !args
.get("op")
.and_then(|op| op.as_str())
.is_some_and(super::git_tool::is_scoped_read_op))
{
return host_return(executed((
disposition_tool_denied_message(disposition, name),
crate::ExecOutcome::Denied,
)));
}
if disposition == PromptDisposition::Plan && name != "request_user_input" && !remote_call {
permission_gate = None;
}
if let Some(denied) = super::deny::deny_check(raw_name, raw_args) {
return host_return(denied.reason);
}
if let Some(message) = hidden_tools.and_then(|hidden| hidden.promote(name).message(name)) {
return host_return(message);
}
if remote_call {
let Some(gate) = permission_gate else {
return host_return(executed((
format!(
"MCP tool `{name}` requires OCAP permission, but no interactive permission gate is available. \
Enable permission prompts in an interactive session, then retry."
),
crate::ExecOutcome::Denied,
)));
};
let request = PermissionRequest {
tool: name.to_string(),
kind: DenialKind::RemoteTool,
target: name.to_string(),
reason: "Approve this remote MCP operation. Persona preferences and server tool hints do not grant permission.".into(),
harness_bound: false,
};
let grant = matches!(gate.ask(&[request]), PermissionDecision::Allow(_))
.then_some(McpGrant::HumanApproved);
return match leash_mcp_call(name, args, grant) {
Ok(leased) => mcp.call(&leased).await,
Err(_) => host_return(executed((
format!(
"MCP tool `{name}` was not run: OCAP permission was denied or cancelled. \
Respect that decision; do not retry it through another tool."
),
crate::ExecOutcome::Denied,
))),
};
}
let name = match resolve_tool_alias(name) {
Some(AliasOutcome::Rewrite(canonical)) => canonical,
Some(AliasOutcome::Correct(msg)) => return host_return(msg),
None => name,
};
let command_args = match shell::command_args_with_default_cwd(
name,
args,
workspace,
default_command_cwd,
worktree_session,
) {
Ok(args) => args,
Err(error) => return host_return(error.to_owned()),
};
let args = command_args.as_ref();
let routed: Option<(&'static str, serde_json::Value)> =
if name == "run_command" && !routing_disabled() {
let command = args.get("command").and_then(|v| v.as_str()).unwrap_or("");
let decision = super::routing::RouteTable::builtin().classify_call(
args,
std::path::Path::new(workspace),
&caveats.fs_read,
);
let decision = build_shell::route_for_ocap(decision, ocap_disabled());
let decision = match decision {
super::routing::RouteDecision::Route { tool: "find", .. }
if smart_harness.is_some() =>
{
super::routing::RouteDecision::Exec
}
decision => decision,
};
if let Some(line) = super::routing::audit_line(command, &decision) {
tracing::debug!(target: "newt::routing", "{line}");
}
match decision {
super::routing::RouteDecision::Route { tool, args } => Some((tool, args)),
super::routing::RouteDecision::Exec => None,
}
} else {
None
};
if let (Some(slot), Some((tool, routed_args))) = (routed_to_slot, &routed) {
let _ = slot.set((*tool, routed_args.clone()));
}
let (name, args): (&str, &serde_json::Value) = match &routed {
Some((tool, routed_args)) => (*tool, routed_args),
None => (name, args),
};
if adopted.is_some() && worktree::administration::refuses(name, args) {
let notice = match (name, &adopted, args["command"].as_str()) {
("run_command", Some(policy), Some(source)) => {
worktree::branch::wrapper_refusal(policy, source)
}
_ => None,
}
.unwrap_or_else(|| worktree::administration::NOTICE.into());
return host_return(executed((notice, crate::ExecOutcome::Denied)));
}
match name {
"prompt_read" => match prompt_context {
Some(context) => {
let output = execute_prompt_read_silent(args, context);
presentation.override_result(output.display);
output.model
}
None => "prompt_read error: no active prompt context in this session".to_string(),
},
"artifact_read" => match artifact_context {
Some(context) => {
let output = execute_artifact_read_silent(args, context);
presentation.override_result(output.display);
output.model
}
None => "error: artifact_read: no active artifact context in this session".to_string(),
},
"save_note" => match note_sink {
Some(sink) => execute_save_note(args, sink, color, tool_output_lines),
None => "unknown tool: save_note (no note store in this session)".to_string(),
},
"recall" => match recall_source {
Some(source) => execute_recall(args, source, color, tool_output_lines),
None => "unknown tool: recall (no conversation store in this session)".to_string(),
},
"propose_context" => match smart_harness {
Some(harness) => {
invocation.expect("smart dispatch has a witness").host();
match harness.propose_context(args) {
Ok(text) => text,
Err(error) => executed((format!("Error: propose_context refused: {error}"), crate::ExecOutcome::Denied)),
}
},
None => executed(("Error: propose_context is unavailable outside an enabled composition session".into(), crate::ExecOutcome::Unavailable)),
},
"re_read" => match smart_harness {
Some(harness) => match harness.read(args) {
Ok(text) => { invocation.expect("smart dispatch has a witness").retrieval(); text }
Err(error) => {
invocation.expect("smart dispatch has a witness").host();
executed((format!("Error: re_read refused: {error}"), crate::ExecOutcome::Denied))
}
},
None => executed((
"Error: re_read is unavailable outside a smart harness session".to_string(),
crate::ExecOutcome::Unavailable,
)),
},
"memory_fetch" => match memory_source {
Some(source) => execute_memory_fetch(args, source, color, tool_output_lines),
None => "unknown tool: memory_fetch (no memory source in this session)".to_string(),
},
"state_set" => match scratchpad_store {
Some(s) => super::scratchpad::execute_state_set(args, s, color, tool_output_lines),
None => "unknown tool: state_set (no scratchpad in this session)".to_string(),
},
"state_get" => match scratchpad_store {
Some(s) => super::scratchpad::execute_state_get(args, s, color, tool_output_lines),
None => "unknown tool: state_get (no scratchpad in this session)".to_string(),
},
"state_clear" => match scratchpad_store {
Some(s) => super::scratchpad::execute_state_clear(s, color, tool_output_lines),
None => "unknown tool: state_clear (no scratchpad in this session)".to_string(),
},
"code_search" => match code_search {
Some(search) => {
super::semantic::execute_code_search(args, search, color, tool_output_lines).await
}
None => {
"unknown tool: code_search (semantic retrieval is off this session)".to_string()
}
},
"where_is" => match where_is {
Some(index) => crate::where_is::execute_where_is(args, index, tool_output_lines),
None => "unknown tool: where_is (no symbol index built for this session)".to_string(),
},
name if crate::navigator::NAV_TOOL_NAMES.contains(&name) => {
let ctx = nav.unwrap_or(crate::navigator::NavToolCtx {
workspace,
where_is,
usage: None,
graph: None,
project: None,
files: None,
status: None,
});
navigation::execute(name, args, workspace, caveats, &ctx)
}
"experience_record" => match experience_store {
Some(s) => {
super::experiential::execute_experience_record(args, s, color, tool_output_lines)
}
None => "unknown tool: experience_record (experiential memory is off)".to_string(),
},
"experience_recall" => match experience_store {
Some(s) => super::experiential::execute_experience_recall(
args,
s,
super::experiential::EXPERIENCE_TOP_K,
color,
tool_output_lines,
),
None => "unknown tool: experience_recall (experiential memory is off)".to_string(),
},
"update_plan" => match step_ledger {
Some(ledger) => {
let before = ledger.snapshot();
let mut out =
super::scheduled::execute_update_plan(args, ledger, color, tool_output_lines);
if tool_result_ok(&out) {
let mut after = ledger.snapshot();
if after.target != before.target {
if artifact_sink.is_none() || artifact_context.is_none() {
ledger.restore(&before);
return "error: target revision requires prompt-artifact recording; previous selection retained".into();
}
if let Some(target) = &mut after.target {
let root = super::selected_target::root(workspace, worktree_session);
if let Err(error) = target.bind_baseline(super::selected_target::baseline(&root, &caveats.fs_read)) {
ledger.restore(&before);
return format!("error: {error}");
}
}
ledger.restore(&after);
out = super::scheduled::plan_block(ledger).unwrap_or(out);
}
let mut artifact_warning = false;
if let (Some(sink), Some(context)) = (artifact_sink, artifact_context) {
let plan = ledger.snapshot();
if !plan.is_empty() {
if let Err(error) =
super::artifact_hooks::record_plan_revision(sink, context, &plan)
{
if before.target != after.target {
ledger.restore(&before);
return format!("error: target revision was not recorded: {error}; previous selection retained");
}
let warning =
format!("warning: failed to record plan artifact: {error}");
out.push('\n');
out.push_str(&warning);
artifact_warning = true;
}
}
}
if super::scheduled::fresh_multi_step_plan(&before, &after)
&& disposition == PromptDisposition::Act
&& crate::initiative::effective_initiative().asks_before_acting()
{
if let Some(control) = plan_mode_control
.filter(|control| !control.implementing_approved_plan())
{
match control
.set_plan_mode(true)
.and_then(|()| control.request_exit())
{
Ok(()) => {
out.push_str("\n\n");
out.push_str(PLAN_APPROVAL_REQUESTED);
}
Err(error) => {
out.push_str(&format!(
"\nwarning: plan approval unavailable: {error}"
));
}
}
}
}
if !artifact_warning {
if let Some(line) = super::scheduled::step_change_line(&before, &after) {
presentation.override_result(line);
}
}
}
out
}
None => "unknown tool: update_plan (scheduled planning is off)".to_string(),
},
"enter_plan_mode" => match (plan_mode_control, step_ledger) {
(Some(control), Some(_)) => match control.set_plan_mode(true) {
Ok(()) => "entered PLAN MODE (read-only): subsequent tool calls are immediately limited to Plan reads and the plan ledger until you call exit_plan_mode. Read/search the relevant code, draft the ordered steps with update_plan, then exit_plan_mode to request approval — the operator decides whether execution proceeds.".to_string(),
Err(error) => format!("error: enter_plan_mode: {error}"),
},
_ => {
"unknown tool: enter_plan_mode (scheduled planning and a session Plan-mode control are both required)".to_string()
}
},
"exit_plan_mode" => match plan_mode_control {
Some(control) => match control.request_exit() {
Ok(()) => EXIT_PLAN_MODE_REQUESTED.to_string(),
Err(error) => format!("error: exit_plan_mode: {error}"),
},
None => {
"unknown tool: exit_plan_mode (no session Plan-mode control is available)"
.to_string()
}
},
"select_operating_mode" => {
let mode = args
.get("mode")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
match operating_mode_control {
Some(control) => control
.select_operating_mode(mode)
.unwrap_or_else(|error| format!("error: select_operating_mode: {error}")),
None => {
"unknown tool: select_operating_mode (available only while /mode auto is active)"
.to_string()
}
}
}
"plan_get" => match step_ledger {
Some(l) => super::scheduled::execute_plan_get(l, color, tool_output_lines),
None => "unknown tool: plan_get (scheduled planning is off)".to_string(),
},
"resume_context" => super::resume::execute_resume_context(
recall_source,
step_ledger,
scratchpad_store,
prompt_context,
color,
tool_output_lines,
),
"request_permissions" => {
let rerun = pending_rerun.and_then(|slot| slot.take());
let (granted, replay_auth, msg) = execute_request_permissions(
caveats,
args,
permission_gate,
color,
tool_output_lines,
workspace,
rerun.as_ref(),
);
match (granted, replay_auth, rerun) {
(Some(widened), Some(_auth), Some(pending))
if pending.missing.iter().all(|request| match request.kind {
DenialKind::Exec => widened.permits_exec(&request.target),
_ => permits_filesystem_request(&widened, request),
}) =>
{
executed(
exec_confined_command(
&pending.cmd,
&pending.cwd,
workspace,
color,
tool_output_lines,
&widened,
&pending.declared,
exec_floor,
&mut None, tool_offload,
spill_store,
live_tool_output.clone(),
presentation,
command_budget,
)
.await,
)
}
(_, _, _) => msg,
}
}
"request_user_input" => {
execute_request_user_input(args, permission_gate, color, tool_output_lines)
}
"tool_search" => {
let query = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
if let Some(message) = hidden_tools
.filter(|_| tool_allowed(disposition, query.trim()))
.and_then(|hidden| hidden.promote(query.trim()).message(query.trim()))
{
return host_return(message);
}
let catalog = filter_advertised_tools(
merged_tool_definitions(
&*mcp,
note_sink.is_some(),
recall_source.is_some(),
memory_source.is_some(),
None, crew_runner.is_some(),
scratchpad_store.is_some(),
code_search.is_some(),
experience_store.is_some(),
step_ledger.is_some(),
operating_mode_control.is_some(),
plan_mode_control.is_some(),
plan_mode_control.is_some_and(super::PlanModeControl::is_plan_mode),
command_budget,
shell_dialect,
),
persona_tools,
);
super::tool_search::execute_tool_search_for_disposition(
query,
&catalog,
disposition,
hidden_tools,
)
}
"git" => match git_tool {
Some(tool) => {
let op = args.get("op").and_then(|v| v.as_str()).unwrap_or("");
if let Err(error) = super::git_tool::check_git_read_scope(op, &caveats.fs_read) {
return host_return(format!("error: {error}"));
}
let gc = crate::git_caveats::GitCaveats::from_session(caveats);
if is_git_data_loss_op(op) {
let confirmed = permission_gate
.as_deref_mut()
.is_some_and(|gate| git_data_loss_confirmed(gate, op));
if !confirmed {
let refusal = format!(
"refused: git {op} destroys work irrecoverably and was not \
confirmed by the operator. If you stashed changes you still \
need, restore them with stash-pop/stash-apply instead of \
dropping; do NOT delete a branch that holds unmerged work. \
The operator must confirm any data-loss git op."
);
return host_return(refusal);
}
}
let mut out = match tool.dispatch(op, args, &gc, caveats) {
Ok(rendered) => rendered,
Err(e) => format!("error: {e}"),
};
if is_git_write_denial(&out) {
let granted = permission_gate
.as_deref_mut()
.is_some_and(|gate| git_gate_allows(gate, op));
if granted {
out = match tool.dispatch(op, args, &crate::git_caveats::GitCaveats::top(), caveats)
{
Ok(rendered) => rendered,
Err(e) => format!("error: {e}"),
};
}
}
out
}
None => "unknown tool: git (no git surface in this session)".to_string(),
},
"crew" if smart_harness.is_some() =>
{
invocation.expect("smart dispatch has a witness").host();
executed((
"Error: frame isolation: crew execution is unavailable until its file operations enforce the session filesystem boundary; use the confined local tools".into(),
crate::ExecOutcome::Unavailable,
))
},
"compose_roster" | "crew" => match crew_runner {
Some(runner) => {
let out = match runner.dispatch(name, args, caveats).await {
Ok(rendered) => rendered,
Err(e) => format!("error: {e}"),
};
out
}
None => crew_off_recovery_result(name),
},
"run_command" => {
let raw_cmd = args["command"].as_str().unwrap_or("");
let (cd_path, cmd_owned) = split_leading_cd(raw_cmd);
let cmd = cmd_owned.as_str();
if cd_path.is_some() && cmd.trim().is_empty() {
let path = cd_path.as_deref().unwrap_or("");
return host_return(format!(
"note: a bare `cd` has no effect — each command runs \
independently, so there is no persistent shell to change. \
Prefix the command instead (`cd {path} && <command>`, which \
newt runs in `{path}`) or pass `cwd`."
));
}
if let Some(tool) = run_command_redirect(cmd)
.filter(|tool| smart_harness.is_none() || !matches!(*tool, "find" | "grep"))
{
return host_return(format!(
"error: '{tool}' is a tool, not a shell command. \
Call it as a separate tool invocation — \
do not pass '{tool}' as a command argument to run_command."
));
}
let publication_bypass = ocap_disabled() && shell::exec_floor_permits(exec_floor, cmd);
let governed_git = native_git::publication::governed(cmd, publication_bypass, presentation);
let commit_requested = governed_git && native_git::needs_commit_broker(cmd);
let commit_broker = if commit_requested {
match git_tool.and_then(|tool| tool.native_commit_policy()) {
Some(policy) => match crate::native_git_broker::NativeGitBroker::new(policy) {
Ok(broker) => Some(broker as std::sync::Arc<dyn agent_bridle_tool_shell::CommandBroker>),
Err(error) => return host_return(format!("error: native Git commit broker: {error}")),
},
None => None,
}
} else {
None
};
if governed_git && run_command_creates_shell_git_commit(cmd) && commit_broker.is_none() {
let reason = match agent_bridle::inspect_shell(cmd) {
Err(error) => format!("shell inspection failed: {error}"),
Ok(_) if commit_requested => "native commit attribution/signing policy is unavailable in this session".to_owned(),
Ok(_) => "this commit-producing Git operation is not supported by the native broker".to_owned(),
};
return host_return(format!(
"error: refusing Git commit publication for this invocation: {reason}. \
Supported `git commit` commands use harness-managed attribution and signing; \
other commit-producing operations (`merge`, `cherry-pick`, `revert`, `rebase`) \
still require native lifecycle support. No command ran."
));
}
let command_cwd = resolve_exec_cwd(workspace, args["cwd"].as_str());
let run_cwd = resolve_exec_cwd(&command_cwd, cd_path.as_deref());
if let Err((error, outcome)) = shell::existing_exec_cwd(std::path::Path::new(&run_cwd)) {
let task_root = worktree_session.and_then(|session| session.task_root(std::path::Path::new(workspace)));
let previous = task_root.as_deref().or(default_command_cwd).unwrap_or_else(|| std::path::Path::new(workspace));
return host_return(executed((
format!("{error}; default working directory remains {}", crate::worktree_adoption::task_path_literal(previous)),
outcome,
)));
}
if let Some(slot) = command_directory {
let _ = slot.set(std::path::PathBuf::from(&run_cwd));
}
if governed_git && native_git::needs_push_broker(cmd) {
return host_return(native_git::execute_governed_push(
cmd,
std::path::Path::new(&run_cwd),
caveats,
&mut permission_gate,
governed_pr,
));
}
if governed_git && native_git::needs_pr_create_broker(cmd) {
return host_return(native_git::execute_governed_pr_create(
cmd,
std::path::Path::new(&run_cwd),
caveats,
&mut permission_gate,
governed_pr,
));
}
if governed_git {
if let Err(reason) = native_git::preflight(
cmd,
std::path::Path::new(&run_cwd),
caveats,
&mut permission_gate,
commit_broker.is_some(),
) {
if let Some(notice) = adopted.as_ref().and_then(|policy| worktree::branch::wrapper_refusal(policy, cmd)) {
return host_return(executed((notice, crate::ExecOutcome::Denied)));
}
return host_return(reason);
}
}
let filesystem_requests = match declared_filesystem_requests(args, cmd, &run_cwd) {
Ok(requests) => requests,
Err(error) => return host_return(error),
};
if let Some(policy) = &adopted {
if let Some(result) = worktree::branch::execute(policy, cmd, std::path::Path::new(&run_cwd), caveats, &filesystem_requests) {
if let Some(invocation) = invocation { invocation.host(); }
return executed(result);
}
}
if let Some(program) = build_shell::confined_build_program(cmd, ocap_disabled()) {
return executed(
build_shell::execute(
cmd,
&program,
&run_cwd,
build_workspace,
caveats,
&filesystem_requests,
&mut permission_gate,
smart_harness,
tool_output_lines,
color,
tool_offload,
spill_store,
live_tool_output.clone(),
presentation,
commit_broker,
args.get("timeout_secs").and_then(serde_json::Value::as_u64),
command_budget,
)
.await,
);
}
let adopted_git_workspace = adopted.as_ref().filter(|policy| {
std::path::Path::new(&run_cwd).canonicalize().ok()
.is_some_and(|cwd| cwd.starts_with(&policy.worktree))
});
let commit_workspace = if let Some(policy) = adopted_git_workspace {
policy.worktree.to_str().unwrap_or(workspace)
} else if commit_broker.is_some()
&& matches!(caveats.fs_write, crate::Scope::All)
{
run_cwd.as_str()
} else {
workspace
};
let invocation_caveats = if commit_broker.is_some() {
match crate::native_git_broker::NativeGitBroker::invocation_caveats(
caveats, std::path::Path::new(commit_workspace),
) {
Ok(invocation) => invocation,
Err(error) => return host_return(format!("error: native Git commit broker: {error}")),
}
} else {
caveats.clone()
};
let git_shell_caveats = dispatch_caveats_for_git_shell(cmd, commit_workspace, &invocation_caveats);
let commit_broker_used = commit_broker.is_some();
let ref_move = commit_requested
.then(|| {
crate::git_hardening::own_branch_for_commit_ref_move(std::path::Path::new(
commit_workspace,
))
})
.flatten();
let mut detach_guard = None;
if let Some(move_info) = &ref_move {
if adopted.as_ref().is_some_and(|policy| policy.protects_branch(&move_info.branch)) {
if let Some(invocation) = invocation { invocation.host(); }
return executed(("capability denied: the original checkout's branch remains read-only".into(), crate::ExecOutcome::Denied));
}
if let Err(error) =
crate::git_hardening::detach_own_head(&move_info.identity, &move_info.old_tip)
{
return host_return(format!(
"error: could not prepare branch '{}' for a governed commit: {error}",
move_info.branch
));
}
detach_guard = Some(std::sync::Arc::new(
crate::git_hardening::DetachedHeadGuard::new(
move_info.identity.clone(),
move_info.branch.clone(),
move_info.old_tip.clone(),
),
));
}
let mut fs_pre_exec_missing: Option<Vec<PermissionRequest>> = None;
let (mut exec_text, mut exec_outcome) = shell::exec_confined_command_with_broker(
cmd,
&run_cwd,
workspace,
color,
tool_output_lines,
&git_shell_caveats,
&filesystem_requests,
exec_floor,
&mut permission_gate,
tool_offload,
spill_store,
live_tool_output.clone(),
presentation,
commit_broker,
detach_guard
.as_ref()
.map(|guard| guard.clone() as agent_bridle_tool_shell::ExecutionLease),
&mut fs_pre_exec_missing,
retry_authorized,
args.get("timeout_secs").and_then(serde_json::Value::as_u64),
adopted.as_ref(),
command_budget,
)
.await;
if let Some(move_info) = &ref_move {
let branch = &move_info.branch;
if exec_outcome == crate::ExecOutcome::Passed {
match crate::git_hardening::advance_own_branch_ref(
&move_info.identity,
branch,
&move_info.old_tip,
) {
Ok(_new_oid) => {
exec_text = exec_text.replace("detached HEAD ", &format!("{branch} "));
}
Err(refusal) => {
exec_outcome = crate::ExecOutcome::Failed;
let candidate = refusal
.candidate_oid
.as_deref()
.map(|oid| format!(" The commit exists, unpublished, as {oid}."))
.unwrap_or_default();
exec_text = format!(
"{exec_text}\nerror: the commit was created but refused publication to \
branch '{branch}': {refusal}.{candidate} No branch ref moved; \
retry the commit."
);
}
}
} else {
match crate::git_hardening::detached_commit_candidate(
&move_info.identity,
&move_info.old_tip,
) {
Ok(Some(candidate)) => {
exec_text = format!(
"{exec_text}\nnote: a commit was created on detached HEAD but \
left unpublished because the dispatch did not succeed: \
{candidate}. It is reachable only by this SHA until a retried \
commit publishes it."
);
}
Ok(None) => {}
Err(error) => {
exec_text = format!(
"{exec_text}\nerror: could not check whether a commit landed \
on detached HEAD before reattaching: {error}"
);
}
}
}
if let Err(error) =
crate::git_hardening::reattach_own_head(&move_info.identity, branch)
{
exec_outcome = crate::ExecOutcome::Failed;
exec_text = format!(
"{exec_text}\nerror: could not restore branch '{branch}' as HEAD: {error}"
);
}
if let Some(guard) = detach_guard.as_mut() {
guard.resolve();
}
}
let result = executed((exec_text, exec_outcome));
if let (Some(slot), Some(missing)) = (pending_rerun, fs_pre_exec_missing) {
if !commit_broker_used {
*slot = Some(PendingRerun {
cmd: cmd.to_string(),
cwd: run_cwd.clone(),
declared: filesystem_requests.clone(),
missing,
});
}
}
result
}
"render_report" => {
let plan_draft_sink = (disposition == PromptDisposition::Plan)
.then_some(())
.and(plan_draft_sink);
let (result, document) =
execute_render_report(args, color, tool_evidence, plan_draft_sink);
if let Some(document) = document {
presentation.document(&document);
}
result
}
"lifecycle" => {
let phase_key = args.get("phase").and_then(|v| v.as_str()).unwrap_or("");
if phase_key.is_empty()
&& ["event", "status", "state"]
.iter()
.any(|k| args.get(k).is_some())
{
return "error: lifecycle runs a project build phase (setup, format, \
lint, test, check, clean); it does not record task state. To \
report progress, call update_plan with \
{\"plan\":[{\"step\",\"status\"}]}. If you are blocked on the \
operator, call request_user_input (or request_permissions for \
a grant). When the work is done, reply without a tool call — \
that final answer ends the turn."
.to_string();
}
let Some(phase) = crate::tooling::Phase::from_key(phase_key) else {
let valid = crate::tooling::Phase::ALL
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>()
.join(", ");
if phase_key == "build" {
let suggestion = build_call_suggestion(args, "test");
return format!(
"error: unknown lifecycle phase '{phase_key}'. Valid phases: {valid}.\n\
Suggested lifecycle call: {suggestion}"
);
}
return format!(
"error: unknown lifecycle phase '{phase_key}'. Valid phases: {valid}."
);
};
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("run");
let effective_dir = resolve_exec_cwd(workspace, args.get("dir").and_then(|v| v.as_str()));
let effective_path = std::path::Path::new(&effective_dir);
let cmds = crate::tooling::resolved_phase_commands(effective_path, phase);
if cmds.is_empty() {
let nested =
crate::tooling::nested_projects_with_configured_phase(effective_path, phase);
return crate::tooling::unconfigured_phase_message(phase, &nested);
}
let joined = cmds.join(" && ");
if action != "list"
&& adopted.is_some()
&& worktree::administration::destructive(&joined)
{
return host_return(executed((
worktree::administration::NOTICE.into(),
crate::ExecOutcome::Denied,
)));
}
let action = if action == "run" && lifecycle_run_routes_to_build_lane(&joined) {
"build"
} else {
action
};
match action {
"list" => format!("lifecycle {} → {joined}", phase.as_str()),
"build" => {
let (program, argv) = build_check_argv(&joined);
executed(
run_confined_build_lane(
build_workspace,
effective_path,
program,
argv,
&joined,
smart_harness,
caveats,
&mut permission_gate,
tool_output_lines,
color,
tool_offload,
spill_store,
None,
None,
shell::LIFECYCLE_BUILD_TIMEOUT,
presentation,
)
.await,
)
}
"run" => executed(
lifecycle_run_with_escalation(
args,
&joined,
&effective_dir,
effective_path,
workspace,
color,
tool_output_lines,
caveats,
exec_floor,
permission_gate,
tool_offload,
spill_store,
live_tool_output.clone(),
smart_harness,
presentation,
command_budget,
)
.await,
),
other => format!(
"error: unknown lifecycle action '{other}'. Use 'run' (default), 'list', or 'build'."
),
}
}
"build_exec" => {
let argv: Vec<String> = args
.get("argv")
.and_then(|v| v.as_array())
.into_iter()
.flatten()
.filter_map(|tok| tok.as_str().map(str::to_string))
.collect();
let Some((program, rest)) = argv.split_first() else {
return host_return("error: routed build command had no argv".into());
};
let cwd_field = args.get("cwd").and_then(serde_json::Value::as_str);
let effective_dir = match cwd_field {
Some(cwd) => std::path::Path::new(workspace).join(cwd),
None => std::path::Path::new(workspace).to_path_buf(),
};
let display = argv.join(" ");
if program == "just" && !justfile_findable_from(&effective_dir) {
let ran_in = effective_dir.to_string_lossy().into_owned();
let filesystem_requests =
match declared_filesystem_requests(args, &display, &ran_in) {
Ok(requests) => requests,
Err(error) => return host_return(error),
};
return executed(
exec_confined_command(
&display,
&ran_in,
workspace,
color,
tool_output_lines,
caveats,
&filesystem_requests,
exec_floor,
&mut permission_gate,
tool_offload,
spill_store,
live_tool_output.clone(),
presentation,
command_budget,
)
.await,
);
}
let trim = OutputTrim::from_json(args.get("trim"));
let timeout_secs = args.get("timeout_secs").and_then(serde_json::Value::as_u64);
let wall = match timeout_secs {
Some(0) | None => shell::LIFECYCLE_BUILD_TIMEOUT,
Some(secs) => std::cmp::min(
std::time::Duration::from_secs(secs),
shell::LIFECYCLE_BUILD_TIMEOUT,
),
};
let (text, outcome) = run_confined_build_lane(
build_workspace,
&effective_dir,
program,
rest.to_vec(),
&display,
smart_harness,
caveats,
&mut permission_gate,
tool_output_lines,
color,
tool_offload,
spill_store,
None,
trim,
wall,
presentation,
)
.await;
let cd_clause = match cwd_field {
Some(".") | None => String::new(),
Some(cwd) => format!("; the leading `cd`/`cwd` was folded into cwd={cwd}"),
};
let echo_clause = if args.get("echo_dropped").and_then(serde_json::Value::as_bool)
== Some(true)
{
"; a trailing exit-code `echo` was dropped — this lane's own exit code, above, \
is the real one"
} else {
""
};
let timeout_clause = match timeout_secs {
None | Some(0) => String::new(),
Some(secs) => {
if wall.as_secs() == secs {
format!(
"; `timeout {secs}` honoured as this lane's wall ({})",
format_wall(wall)
)
} else {
format!(
"; `timeout {secs}` was capped at the lane's {} limit",
format_wall(wall)
)
}
}
};
let wall_desc = format_wall(wall);
let network = build_network_description(&caveats.net);
let note = match trim {
Some(trim) => format!(
"[routed `{display}` to the confined build lane — {wall_desc} limit, \
{network}; Cargo offline; {}{cd_clause}{echo_clause}{timeout_clause}]",
trim.note_clause()
),
None => format!(
"[routed `{display}` to the confined build lane — {wall_desc} limit, \
{network}; Cargo offline{cd_clause}{echo_clause}{timeout_clause}]"
),
};
executed((append_routed_note(text, note), outcome))
}
"read_file" if super::memory_fetch::is_memory_address(args["path"].as_str().unwrap_or("")) => {
let address = args["path"].as_str().unwrap_or("").trim();
match memory_source {
Some(source) => match super::memory_fetch::resolve_memory_address(address, source) {
Ok(body) => match (arg_usize(args, "offset"), arg_usize(args, "limit"),
arg_usize(args, "char_offset"))
{
(Ok(offset), Ok(limit), Ok(char_offset)) => paginate_unspillable(
address,
&body,
offset,
limit,
char_offset,
),
(Err(e), _, _) | (_, Err(e), _) | (_, _, Err(e)) => format!("error: read_file: {e}"),
},
Err(refusal) => refusal,
},
None => format!(
"`{address}` is a memory address, not a file, and this session has \
no memory store to read it from."
),
}
}
"read_file" => {
let path = args["path"].as_str().unwrap_or("");
match authorized_read("read_file", path, workspace, caveats, permission_gate) {
Ok(contents) => {
let offset = match arg_usize(args, "offset") {
Ok(v) => v,
Err(e) => return format!("error: read_file: {e}"),
};
let limit = match arg_usize(args, "limit") {
Ok(v) => v,
Err(e) => return format!("error: read_file: {e}"),
};
let char_offset = match arg_usize(args, "char_offset") {
Ok(v) => v,
Err(e) => return format!("error: read_file: {e}"),
};
match read_history {
Some(history) => history.read(path, &contents, offset, limit, char_offset, tool_offload),
None => read_file_page(path, &contents, offset, limit, char_offset, tool_offload),
}
}
Err(tool_output) => tool_output,
}
}
"write_file" => {
let path = args["path"].as_str().unwrap_or("");
let Some(content) = args.get("content").and_then(serde_json::Value::as_str) else {
return "error: write_file needs `content` as a string — nothing was written"
.to_string();
};
if path.is_empty() {
return "error: write_file needs `path` — nothing was written".to_string();
}
if args.get("move_from").is_some() {
#[cfg(not(feature = "ast"))]
return "error: move_from requires a build with the ast feature; nothing changed".into();
#[cfg(feature = "ast")]
{
let moved = match move_from::execute(args, workspace, caveats, smart_harness, &mut permission_gate) {
Ok(moved) => moved,
Err(error) => return format!("error: move_from: {error}"),
};
let mut artifacts = super::publish_early::bounded_move_hint(moved.child.lines().count()).to_owned();
for (label, before, after) in [
(moved.source.as_str(), super::artifact_hooks::ArtifactFileState::from_bytes(moved.before.as_bytes()), moved.parent.as_str()),
(path, super::artifact_hooks::ArtifactFileState::absent(), moved.child.as_str()),
] {
artifacts.push_str(&record_governed_file_change(artifact_sink, artifact_context, label, "write_file",
Some(before), super::artifact_hooks::ArtifactFileState::from_bytes(after.as_bytes()), color, tool_output_lines));
}
return format!("Moved functions from {} into {}; confined cargo check --lib exited 0. Parent: {} lines; child: {} lines. Displaced entries retained as .newt-move-*.saved beside changed files; inspect before removing.{}",
file_capture::display_text(&moved.source), file_capture::display_text(path),
moved.parent.lines().count(), moved.child.lines().count(), artifacts);
}
}
let copied;
let content = match args.get("copy_from") {
Some(spec) => {
let (Some(source), Some(start), Some(end)) = (
spec["path"].as_str(),
spec["start_line"].as_u64(),
spec["end_line"].as_u64(),
) else {
return "error: copy_from needs path, start_line and end_line".to_string();
};
let text = match authorized_read(
"write_file",
source,
workspace,
caveats,
permission_gate.as_deref_mut(),
) {
Ok(text) => text,
Err(refusal) => return refusal,
};
copied = match file_capture::copy_line_range(
content,
&text,
start as usize,
end as usize,
) {
Ok(text) => text,
Err(refusal) => return refusal,
};
copied.as_str()
}
None => content,
};
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
let scope_permits = tui_permits_path(&caveats.fs_write, &full_str);
if !scope_permits {
let allowed = permission_gate.as_deref_mut().is_some_and(|gate| {
fs_gate_allows(gate, "write_file", DenialKind::FsWrite, &full_str, |c| {
&c.fs_write
})
});
if !allowed {
return denied_fs_result("fs_write", &full_str);
}
}
if full_access_requested() {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::FsWrite,
&full_str,
"write_file",
);
}
let artifact_tracking = artifact_sink.is_some() && artifact_context.is_some();
let artifact_path_within = artifact_tracking
&& artifact_path_is_physically_within_workspace(
std::path::Path::new(workspace),
&full,
);
let mutation_scope = if scope_permits { &caveats.fs_write } else { &crate::caveats::Scope::All };
if !file_capture::regular_target(&full) {
return file_capture::present(
format!("error: write_file refuses a nonregular target: {path}"),
presentation,
);
}
if let Ok(existing) = file_capture::read_for_edit(mutation_scope, &full, path) {
let orig_lines = existing.lines().count();
let new_lines = content.lines().count();
let removed = orig_lines.saturating_sub(new_lines);
if removed > 30 && new_lines < orig_lines * 7 / 10 {
let pct = removed * 100 / orig_lines.max(1);
let msg = format!(
"error: write_file would shrink {path} from {orig_lines} → {new_lines} lines \
(-{pct}%). This is likely unintentional. Use edit_file to make targeted \
changes, or ensure your content includes the full file."
);
return msg;
}
}
let preview: String = content.lines().take(20).collect::<Vec<_>>().join("\n");
let has_more = content.lines().count() > 20;
let visible_preview = file_capture::display_text(&preview);
let preview = if visible_preview != preview {
format!("Preview escapes control characters:\n{visible_preview}")
} else {
preview
};
presentation.preview(
&format!("{preview}{}", if has_more { "\n…" } else { "" }),
tool_output_lines,
);
let confirmed = confirm_unrestricted_fs_mutation(
caveats,
&mut permission_gate,
"Write this file? [y/N]",
);
if confirmed {
let receipt_before = file_capture::capture(&caveats.fs_read, &full);
let artifact_before = artifact_path_within.then(|| {
artifact_preimage_state(&caveats.fs_read, &full, tui_permits_path(&caveats.fs_read, &full_str))
});
let write_result = if scope_permits {
object_bound_write(&caveats.fs_write, "fs_write", path, &full, &full_str, content)
} else {
std_write(&full, path, content)
};
let receipt_after = file_capture::capture(&caveats.fs_read, &full);
let receipt = file_capture::receipt(path, &receipt_before, &receipt_after);
match write_result {
Ok(()) => {
if !file_capture::verified_after(&receipt_after, mutation_scope, &full, Some(content.as_bytes())) {
return receipt.present(
format!("error: write_file returned success for {path}, but the submitted bytes could not be verified"),
"",
presentation,
);
}
let line_count = content.lines().count();
let artifact = if !artifact_tracking {
String::new()
} else if !artifact_path_within
|| !artifact_path_is_physically_within_workspace(
std::path::Path::new(workspace),
&full,
)
{
artifact_postcondition_warning(
path,
"the physical path could not be proven inside the workspace",
color,
tool_output_lines,
)
} else {
match artifact_file_matches(&caveats.fs_write, &full, content.as_bytes()) {
Ok(true) => record_governed_file_change(
artifact_sink,
artifact_context,
path,
"write_file",
artifact_before,
super::artifact_hooks::ArtifactFileState::from_bytes(
content.as_bytes(),
),
color,
tool_output_lines,
),
Ok(false) => artifact_postcondition_warning(
path,
"post-write bytes did not match the submitted content",
color,
tool_output_lines,
),
Err(_) => artifact_postcondition_warning(
path,
"post-write bytes could not be verified",
color,
tool_output_lines,
),
}
};
let check = build_check_cmd
.map(|cmd| run_build_check(cmd, build_workspace, &caveats.net))
.unwrap_or_default();
receipt.present_success(format!("wrote {path} ({line_count} lines)"), &format!("{artifact}{check}{}", super::publish_early::bounded_move_hint(line_count)), presentation)
}
Err(tool_output) => receipt.present(file_capture::failure(tool_output.render(workspace, &caveats.fs_read), ""), "", presentation),
}
} else {
format!("user declined to write {path}")
}
}
"delete_file" => {
let path = args["path"].as_str().unwrap_or("");
if path.trim().is_empty() {
return "error: path is required".to_string();
}
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
let scope_permits = tui_permits_path(&caveats.fs_write, &full_str);
if !scope_permits {
let allowed = permission_gate.as_deref_mut().is_some_and(|gate| {
fs_gate_allows(gate, "delete_file", DenialKind::FsWrite, &full_str, |c| {
&c.fs_write
})
});
if !allowed {
return denied_fs_result("fs_write", &full_str);
}
}
let meta = match std::fs::symlink_metadata(&full) {
Ok(meta) => meta,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return FileIoError::io(&e, &full, format!("error: deleting {path}: file does not exist")).render(workspace, &caveats.fs_read);
}
Err(e) => return format!("error: deleting {path}: {e}"),
};
if meta.file_type().is_dir() {
return format!("error: deleting {path}: delete_file refuses directories");
}
let artifact_tracking = artifact_sink.is_some() && artifact_context.is_some();
let artifact_path_within = artifact_tracking
&& artifact_path_is_physically_within_workspace(
std::path::Path::new(workspace),
&full,
);
let confirmed = confirm_unrestricted_fs_mutation(
caveats,
&mut permission_gate,
"Delete this file? [y/N]",
);
if !confirmed {
return format!("user declined to delete {path}");
}
let receipt_before = file_capture::capture(&caveats.fs_read, &full);
let artifact_before = artifact_path_within.then(|| {
artifact_preimage_state(&caveats.fs_read, &full, tui_permits_path(&caveats.fs_read, &full_str))
});
let mutation_scope = if scope_permits { &caveats.fs_write } else { &crate::caveats::Scope::All };
let delete_result = if scope_permits {
object_bound_delete(&caveats.fs_write, path, &full, &full_str)
} else {
std::fs::remove_file(&full).map_err(|e| FileIoError::io(&e, &full, format!("error: deleting {path}: {e}")))
};
let receipt_after = file_capture::capture(&caveats.fs_read, &full);
let receipt = file_capture::receipt(path, &receipt_before, &receipt_after);
match delete_result {
Ok(()) => {
if !file_capture::verified_after(&receipt_after, mutation_scope, &full, None) {
return receipt.present(
format!("error: delete_file returned success for {path}, but absence could not be verified"),
"",
presentation,
);
}
let artifact = if !artifact_tracking {
String::new()
} else if !artifact_path_within
|| !artifact_path_is_physically_within_workspace(
std::path::Path::new(workspace),
&full,
)
{
artifact_postcondition_warning(
path,
"the physical path could not be proven inside the workspace",
color,
tool_output_lines,
)
} else {
match std::fs::symlink_metadata(&full) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
record_governed_file_change(
artifact_sink,
artifact_context,
path,
"delete_file",
artifact_before,
super::artifact_hooks::ArtifactFileState::absent(),
color,
tool_output_lines,
)
}
_ => artifact_postcondition_warning(
path,
"the path still existed after delete_file returned success",
color,
tool_output_lines,
),
}
};
let check = build_check_cmd
.map(|cmd| run_build_check(cmd, build_workspace, &caveats.net))
.unwrap_or_default();
receipt.present_success(format!("deleted {path}"), &format!("{artifact}{check}"), presentation)
}
Err(tool_output) => receipt.present(file_capture::failure(tool_output.render(workspace, &caveats.fs_read), ""), "", presentation),
}
}
"edit_file" => {
let path = args["path"].as_str().unwrap_or("");
let old_string = args["old_string"].as_str().unwrap_or("");
let Some(new_string) = args.get("new_string").and_then(serde_json::Value::as_str)
else {
return "error: edit_file needs `new_string` as a string (use \"\" to delete \
the matched text) — nothing was changed"
.to_string();
};
if args.get("start_line").is_some() || args.get("end_line").is_some() {
return "error: edit_file does not take a line range (start_line/end_line) — \
give old_string, the exact text to replace; nothing was changed"
.to_string();
}
if path.is_empty() {
return "error: edit_file needs `path` — nothing was changed".to_string();
}
if old_string.is_empty() {
return "error: old_string must not be empty — use write_file to create new files"
.to_string();
}
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
let scope_permits = tui_permits_path(&caveats.fs_write, &full_str);
if !scope_permits {
let allowed = permission_gate.is_some_and(|gate| {
fs_gate_allows(gate, "edit_file", DenialKind::FsWrite, &full_str, |c| {
&c.fs_write
})
});
if !allowed {
return denied_fs_result("fs_write", &full_str);
}
}
if full_access_requested() {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::FsWrite,
&full_str,
"edit_file",
);
}
if !file_capture::regular_target(&full) {
return file_capture::present(
format!("error: edit_file refuses a nonregular target: {path}"),
presentation,
);
}
let mutation_scope = if scope_permits { &caveats.fs_write } else { &crate::caveats::Scope::All };
let read = file_capture::read_for_edit(mutation_scope, &full, path);
let existing = match read {
Ok(s) => s,
Err(tool_output) => return tool_output.render(workspace, &caveats.fs_read),
};
let count = existing.matches(old_string).count();
if count == 0 {
const HEAD: usize = 40;
let total = existing.lines().count();
let head: String = existing
.lines()
.take(HEAD)
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n");
let more = if total > HEAD {
format!("\n … ({} more line(s))", total - HEAD)
} else {
String::new()
};
return format!(
"error: old_string not found in {path} — do not guess again. Copy the \
EXACT text (including leading whitespace) from the contents below, then \
retry. To add a header/first line, set old_string to the shown first \
line and put your header + that line in new_string; to create a new \
file use write_file.\n--- {path} (first {shown} of {total} line(s)) ---\n{head}{more}",
shown = total.min(HEAD),
);
}
if count > 1 {
return format!(
"error: old_string matches {count} locations in {path}. \
Add more surrounding context to make it unique."
);
}
let artifact_tracking = artifact_sink.is_some() && artifact_context.is_some();
let artifact_path_within = artifact_tracking
&& artifact_path_is_physically_within_workspace(
std::path::Path::new(workspace),
&full,
);
let artifact_before = artifact_path_within.then(|| {
if !tui_permits_path(&caveats.fs_read, &full_str) {
super::artifact_hooks::ArtifactFileState::unavailable("fs_read_not_granted")
} else if std::fs::symlink_metadata(&full)
.is_ok_and(|metadata| metadata.file_type().is_symlink())
{
super::artifact_hooks::ArtifactFileState::unavailable(
"symlink_preimage_not_hashed",
)
} else {
super::artifact_hooks::ArtifactFileState::from_bytes(existing.as_bytes())
}
});
let updated = existing.replacen(old_string, new_string, 1);
let old_lines = existing.lines().count();
let new_lines = updated.lines().count();
let delta = new_lines as i64 - old_lines as i64;
let delta_str = if delta >= 0 {
format!("+{delta}")
} else {
format!("{delta}")
};
let receipt_before = file_capture::capture(&caveats.fs_read, &full);
if !file_capture::current_preimage(&receipt_before, mutation_scope, &full, existing.as_bytes()) {
return file_capture::present(
format!("error: edit_file refused a stale preimage for {path}; reread the file and retry"),
presentation,
);
}
let write_result = if scope_permits {
object_bound_write(&caveats.fs_write, "fs_write", path, &full, &full_str, &updated)
} else {
std_write(&full, path, &updated)
};
let receipt_after = file_capture::capture(&caveats.fs_read, &full);
let receipt = file_capture::receipt(path, &receipt_before, &receipt_after);
match write_result {
Ok(()) => {
if !file_capture::verified_after(&receipt_after, mutation_scope, &full, Some(updated.as_bytes())) {
return receipt.present(
format!("error: edit_file returned success for {path}, but the replacement bytes could not be verified"),
"",
presentation,
);
}
let artifact = if !artifact_tracking {
String::new()
} else if !artifact_path_within
|| !artifact_path_is_physically_within_workspace(
std::path::Path::new(workspace),
&full,
)
{
artifact_postcondition_warning(
path,
"the physical path could not be proven inside the workspace",
color,
tool_output_lines,
)
} else {
match artifact_file_matches(&caveats.fs_write, &full, updated.as_bytes()) {
Ok(true) => record_governed_file_change(
artifact_sink,
artifact_context,
path,
"edit_file",
artifact_before,
super::artifact_hooks::ArtifactFileState::from_bytes(
updated.as_bytes(),
),
color,
tool_output_lines,
),
Ok(false) => artifact_postcondition_warning(
path,
"post-edit bytes did not match the computed replacement",
color,
tool_output_lines,
),
Err(_) => artifact_postcondition_warning(
path,
"post-edit bytes could not be verified",
color,
tool_output_lines,
),
}
};
let check = build_check_cmd
.map(|cmd| run_build_check(cmd, build_workspace, &caveats.net))
.unwrap_or_default();
let escape_warning = literal_newline_escape_warning(old_string, new_string)
.map(|w| format!("\n{w}"))
.unwrap_or_default();
receipt.present_success(format!("edited {path} ({delta_str} lines, now {new_lines} total){escape_warning}"), &format!("{artifact}{check}{}", super::publish_early::bounded_move_hint(old_string.lines().count().max(new_string.lines().count()))), presentation)
}
Err(tool_output) => receipt.present(file_capture::failure(tool_output.render(workspace, &caveats.fs_read), ""), "", presentation),
}
}
"list_dir" => {
let path = args["path"].as_str().unwrap_or(".");
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
let scope_permits = tui_permits_path(&caveats.fs_read, &full_str);
if !scope_permits {
let allowed = permission_gate.is_some_and(|gate| {
fs_gate_allows(gate, "list_dir", DenialKind::FsRead, &full_str, |c| {
&c.fs_read
})
});
if !allowed {
return denied_fs_result("fs_read", &full_str);
}
}
if full_access_requested() {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::FsRead,
&full_str,
"list_dir",
);
}
let listing = if scope_permits {
object_bound_list(&caveats.fs_read, &full, &full_str)
} else {
std_list_dir(&full)
};
match listing {
Ok(mut names) => {
names.sort();
names.join("\n")
}
Err(tool_output) => tool_output.render(workspace, &caveats.fs_read),
}
}
"find" | "grep" if smart_harness.is_some() =>
{
invocation.expect("smart dispatch has a witness").host();
executed((
format!("Error: frame isolation: native {name} is unavailable until its recursive walker retains the directory capability; use run_command for confined shell search"),
crate::ExecOutcome::Unavailable,
))
},
"find" => {
let path = args["path"].as_str().unwrap_or(".");
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
const WORKSPACE_ONLY: &str = "capability denied: find is workspace-only; an fs_read grant cannot enable an external search root. Use list_dir for an authorized directory, or run_command within your granted authority.";
if !find_root_contained(&caveats.fs_read, workspace, &full, &full_str) {
return WORKSPACE_ONLY.to_string();
}
if !tui_permits_path(&caveats.fs_read, &full_str) {
let allowed = permission_gate.is_some_and(|gate| {
fs_gate_allows(gate, "find", DenialKind::FsRead, &full_str, |c| &c.fs_read)
});
if !allowed {
return denied_fs_result("fs_read", &full_str);
}
}
if full_access_requested() {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::FsRead,
&full_str,
"find",
);
}
let opts = find_opts_from_args(args);
let source_extensions =
match find_source_extensions(std::path::Path::new(workspace), &opts) {
Ok(extensions) => extensions,
Err(error) => return format!("error: {error}"),
};
if let Err(error) = std::fs::metadata(&full) {
return FileIoError::io(&error, &full, format!("error: no such path '{path}'")).render(workspace, &caveats.fs_read);
}
if !find_root_contained(&caveats.fs_read, workspace, &full, &full_str) {
return WORKSPACE_ONLY.to_string();
}
let mut live = LiveOutputSession::start(live_tool_output.clone());
let relay = live.as_ref().map(LiveOutputSession::relay);
let on_hit = |line: &str| {
if let Some(relay) = &relay {
let mut chunk = line.as_bytes().to_vec();
chunk.push(b'\n');
relay.write(crate::agentic::ToolOutputStream::Stdout, &chunk);
}
};
let walked = find_walk(
&full,
std::path::Path::new(workspace),
&opts,
source_extensions.as_deref(),
on_hit,
);
if let Some(live) = live.as_mut() {
live.finish();
}
match walked {
Ok((hits, truncated)) => {
let mut listing = if hits.is_empty() {
"no matches".to_string()
} else {
hits.join("\n")
};
if truncated {
listing
.push_str(&format!("\n… (truncated at {} matches)", opts.max_results));
}
listing
}
Err(e) => format!("error: {e}"),
}
}
"grep" => {
let path = args["path"].as_str().unwrap_or(".");
let full = std::path::Path::new(workspace).join(path);
let full_str = full.to_string_lossy();
const WORKSPACE_ONLY: &str = "capability denied: grep is workspace-only; an fs_read grant cannot enable an external search root. Use list_dir for an authorized directory, or run_command within your granted authority.";
if !find_root_contained(&caveats.fs_read, workspace, &full, &full_str) {
return WORKSPACE_ONLY.to_string();
}
if !tui_permits_path(&caveats.fs_read, &full_str) {
let allowed = permission_gate.is_some_and(|gate| {
fs_gate_allows(gate, "grep", DenialKind::FsRead, &full_str, |c| &c.fs_read)
});
if !allowed {
return denied_fs_result("fs_read", &full_str);
}
}
if full_access_requested() {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::FsRead,
&full_str,
"grep",
);
}
let glob = args["glob"].as_str();
let ignore_case = args["ignore_case"].as_bool().unwrap_or(false);
let context = match arg_usize(args, "context") {
Ok(v) => v.unwrap_or(0),
Err(e) => return format!("error: grep: {e}"),
};
let max_results = match arg_usize(args, "max_results") {
Ok(Some(0)) => return "error: grep: `max_results` must be at least 1".to_string(),
Ok(v) => v.unwrap_or(grep_tool::DEFAULT_MAX_RESULTS),
Err(e) => return format!("error: grep: {e}"),
};
if let Err(error) = std::fs::metadata(&full) {
return FileIoError::io(&error, &full, format!("error: no such path '{path}'")).render(workspace, &caveats.fs_read);
}
if !find_root_contained(&caveats.fs_read, workspace, &full, &full_str) {
return WORKSPACE_ONLY.to_string();
}
let opts = grep_tool::GrepOpts {
pattern: args["pattern"].as_str().unwrap_or(""),
glob,
ignore_case,
context,
max_results,
};
match grep_tool::grep_search(&full, std::path::Path::new(workspace), &opts) {
Ok(found) => {
let mut out = if found.lines.is_empty() {
format!("no matches for {} under {path}", opts.pattern)
} else {
found.lines.join("\n")
};
if found.truncated {
out.push_str(&format!(
"\n[stopped at {max_results} results; narrow pattern/path/glob or raise max_results]"
));
}
if found.skipped > 0 {
out.push_str(&format!(
"\n[{} file(s) not searched: binary or unreadable]",
found.skipped
));
}
out
}
Err(e) => format!("error: grep: {e}"),
}
}
"use_skill" => {
let skill_name = args["name"].as_str().unwrap_or("");
let dirs = super::display::with_migration_notices(crate::Config::resolve_unpublished)
.map(|c| c.skill_search_dirs())
.unwrap_or_default();
match newt_skills::load_body_from(&dirs, skill_name) {
Ok(body) => body,
Err(e) => format!("error: {e}"),
}
}
"web_fetch" => {
let url = args["url"].as_str().unwrap_or("");
let mut fetch_args = serde_json::json!({ "url": url });
if let Some(max_bytes) = args.get("max_bytes").and_then(serde_json::Value::as_u64) {
fetch_args["max_bytes"] = serde_json::json!(max_bytes);
}
let widened_for_net = match (permission_gate, host_of_url(url)) {
(Some(gate), Some(host)) if !caveats.permits_net(&host) => {
let request = PermissionRequest {
tool: "web_fetch".to_string(),
kind: DenialKind::Net,
target: host.clone(),
reason: format!("net does not permit '{host}'"),
harness_bound: false,
};
match gate.ask(std::slice::from_ref(&request)) {
PermissionDecision::Allow(widened) => Some(widened),
PermissionDecision::Deny => None,
}
}
_ => None,
};
let effective_caveats = widened_for_net.as_ref().unwrap_or(caveats);
if full_access_requested() {
if let Some(host) = host_of_url(url) {
crate::flight_recorder::log_observed(
crate::flight_recorder::ShadowAxis::Net,
&host,
url,
);
}
}
let registry = agent_bridle::registry();
let grant = registry.mint_grant(effective_caveats.clone());
match registry.dispatch("web_fetch", fetch_args, &grant).await {
Ok(result) =>
render_web_fetch_result(url, &result, &*mcp, persona_tools, disposition),
Err(e) => {
let reason = e.to_string();
if let Some(host) = web_fetch_denial_host(url, &e) {
crate::denial_journal::record_net_denial("web_fetch", &host, &reason);
}
render_web_fetch_error(url, &reason, &*mcp, persona_tools, disposition)
}
}
}
other => unknown_tool_message(other),
}
}
pub(crate) fn tool_result_ok(result: &str) -> bool {
let r = result.trim_start();
!(r.starts_with("error:")
|| r.starts_with("capability denied:")
|| r.starts_with("unknown tool")
|| r.starts_with("no command configured"))
}
pub(crate) fn tool_ok(result: &str, execution: Option<crate::ExecOutcome>) -> bool {
match execution {
Some(crate::ExecOutcome::Passed) => true,
Some(
crate::ExecOutcome::Failed
| crate::ExecOutcome::Denied
| crate::ExecOutcome::TimedOut
| crate::ExecOutcome::Unavailable,
) => false,
None => tool_result_ok(result),
}
}
#[cfg(test)]
#[path = "tools_tests/private_url_mcp_bat.rs"]
mod private_url_mcp_bat_tests;
#[cfg(test)]
#[path = "tools_tests/tests.rs"]
mod tests;
#[cfg(test)]
#[path = "tools_tests/execute_tool_branch_tests.rs"]
mod execute_tool_branch_tests;
#[cfg(test)]
#[path = "tools_tests/exit_code_ok_tests.rs"]
mod exit_code_ok_tests;
#[cfg(test)]
#[path = "tools_tests/build_wall_tests.rs"]
mod build_wall_tests;
#[cfg(test)]
#[path = "tools_tests/disable_ocap_tests.rs"]
pub(crate) mod disable_ocap_tests;
#[cfg(test)]
#[path = "tools_tests/redirect_guard.rs"]
mod redirect_guard_tests;
#[cfg(test)]
#[path = "tools_tests/smart_frame_isolation.rs"]
mod smart_frame_isolation_tests;
#[cfg(all(test, target_os = "linux"))]
#[path = "tools_tests/smart_tool_completion.rs"]
mod smart_tool_completion_tests;
#[cfg(test)]
#[path = "tools_tests/sibling_worktree_build.rs"]
mod sibling_worktree_build_tests;
#[cfg(all(windows, feature = "test-util"))]
pub(crate) use shell::test_windows_ambient_dispatch;
#[cfg(test)]
#[path = "tools_tests/git_fixture.rs"]
pub(super) mod git_fixture;
#[cfg(test)]
#[path = "tools_tests/command_shape_contract.rs"]
mod command_shape_contract;
pub(crate) fn dispatch_exec_path() -> Option<std::ffi::OsString> {
shell::venv_env_map()
.get("PATH")
.map(std::ffi::OsString::from)
.or_else(|| std::env::var_os("PATH"))
}
#[cfg(all(test, target_os = "macos"))]
#[path = "tools_tests/path_aliases.rs"]
mod path_aliases;
#[cfg(all(test, unix))]
#[path = "tools_tests/path_alias_attacks.rs"]
mod path_alias_attacks;