Skip to main content

aft/bash_rewrite/
mod.rs

1//! Bash command rewriter for hoisted bash.
2//!
3//! Rewriting is an optimization of native bash, not a second command
4//! language. The dispatch layer makes a pre-execution decision and records it
5//! for differential tests. Once a request is accepted, its handler response is
6//! returned directly; native bash is never run as a recovery path.
7
8pub mod catalog;
9pub mod differential;
10pub mod dispatch;
11pub mod footer;
12pub mod observation;
13pub mod parser;
14pub mod rules;
15
16use serde_json::Value;
17
18use crate::context::AppContext;
19use crate::protocol::Response;
20use crate::sandbox_spawn::{native_sandbox_enforced, AuthenticatedPrincipal};
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct RewriteRequest {
24    pub request_id: String,
25    pub command: String,
26    pub session_id: Option<String>,
27    pub rule_id: &'static str,
28    pub branch_id: &'static str,
29    pub decision_class_id: &'static str,
30    pub params: Value,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct DeclineReason {
35    pub rule_id: Option<&'static str>,
36    pub branch_id: &'static str,
37    pub decision_class_id: &'static str,
38    pub reason: String,
39}
40
41#[derive(Debug, Clone, PartialEq)]
42pub enum RewriteDecision {
43    Accept(RewriteRequest),
44    Decline(DeclineReason),
45}
46
47/// A rewrite rule owns both the pre-execution shape decision and the typed
48/// request it emits. Implementations keep parsing in the decision phase so an
49/// accepted request has one stable request ID and one decision class.
50pub trait RewriteRule: Send + Sync {
51    fn name(&self) -> &'static str;
52    fn decide(
53        &self,
54        command: &str,
55        request_id: &str,
56        session_id: Option<&str>,
57        ctx: &AppContext,
58    ) -> RewriteDecision;
59    fn execute(&self, request: &RewriteRequest, ctx: &AppContext) -> Response;
60}
61
62/// Try to rewrite a synthetic request. Production bash calls
63/// [`try_rewrite_for_request`] with the caller's request ID; this compatibility
64/// wrapper is retained for direct library users and existing unit tests.
65pub fn try_rewrite(
66    command: &str,
67    session_id: Option<&str>,
68    ctx: &AppContext,
69    principal: &AuthenticatedPrincipal,
70) -> Option<Response> {
71    try_rewrite_for_request(command, "bash_rewrite", session_id, ctx, principal)
72}
73
74/// Make the rewrite decision for one public bash request. A native route is
75/// recorded when the sandbox owns execution or when no rule accepts the
76/// command. The record is test-visible through `dispatch::route_record` and an
77/// opt-in JSONL sidecar, without changing the agent-facing response.
78pub fn try_rewrite_for_request(
79    command: &str,
80    request_id: &str,
81    session_id: Option<&str>,
82    ctx: &AppContext,
83    principal: &AuthenticatedPrincipal,
84) -> Option<Response> {
85    if native_sandbox_enforced(ctx, principal) {
86        dispatch::record_native(
87            request_id,
88            catalog::ControlRole::Sandbox,
89            "dispatch.native.sandbox",
90            "native sandbox is enforced",
91        );
92        return None;
93    }
94    dispatch::dispatch_for_request(command, request_id, session_id, ctx)
95}