agent-file-tools 0.49.4

Agent File Tools — tree-sitter powered code analysis for AI agents
Documentation
//! Bash command rewriter for hoisted bash.
//!
//! Rewriting is an optimization of native bash, not a second command
//! language. The dispatch layer makes a pre-execution decision and records it
//! for differential tests. Once a request is accepted, its handler response is
//! returned directly; native bash is never run as a recovery path.

pub mod catalog;
pub mod differential;
pub mod dispatch;
pub mod footer;
pub mod observation;
pub mod parser;
pub mod rules;

use serde_json::Value;

use crate::context::AppContext;
use crate::protocol::Response;
use crate::sandbox_spawn::{native_sandbox_enforced, AuthenticatedPrincipal};

#[derive(Debug, Clone, PartialEq)]
pub struct RewriteRequest {
    pub request_id: String,
    pub command: String,
    pub session_id: Option<String>,
    pub rule_id: &'static str,
    pub branch_id: &'static str,
    pub decision_class_id: &'static str,
    pub params: Value,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclineReason {
    pub rule_id: Option<&'static str>,
    pub branch_id: &'static str,
    pub decision_class_id: &'static str,
    pub reason: String,
}

#[derive(Debug, Clone, PartialEq)]
pub enum RewriteDecision {
    Accept(RewriteRequest),
    Decline(DeclineReason),
}

/// A rewrite rule owns both the pre-execution shape decision and the typed
/// request it emits. Implementations keep parsing in the decision phase so an
/// accepted request has one stable request ID and one decision class.
pub trait RewriteRule: Send + Sync {
    fn name(&self) -> &'static str;
    fn decide(
        &self,
        command: &str,
        request_id: &str,
        session_id: Option<&str>,
        ctx: &AppContext,
    ) -> RewriteDecision;
    fn execute(&self, request: &RewriteRequest, ctx: &AppContext) -> Response;
}

/// Try to rewrite a synthetic request. Production bash calls
/// [`try_rewrite_for_request`] with the caller's request ID; this compatibility
/// wrapper is retained for direct library users and existing unit tests.
pub fn try_rewrite(
    command: &str,
    session_id: Option<&str>,
    ctx: &AppContext,
    principal: &AuthenticatedPrincipal,
) -> Option<Response> {
    try_rewrite_for_request(command, "bash_rewrite", session_id, ctx, principal)
}

/// Make the rewrite decision for one public bash request. A native route is
/// recorded when the sandbox owns execution or when no rule accepts the
/// command. The record is test-visible through `dispatch::route_record` and an
/// opt-in JSONL sidecar, without changing the agent-facing response.
pub fn try_rewrite_for_request(
    command: &str,
    request_id: &str,
    session_id: Option<&str>,
    ctx: &AppContext,
    principal: &AuthenticatedPrincipal,
) -> Option<Response> {
    if native_sandbox_enforced(ctx, principal) {
        dispatch::record_native(
            request_id,
            catalog::ControlRole::Sandbox,
            "dispatch.native.sandbox",
            "native sandbox is enforced",
        );
        return None;
    }
    dispatch::dispatch_for_request(command, request_id, session_id, ctx)
}