@@ -18,6 +18,53 @@ import { BashArity } from "@/permission/arity"
1818import { Truncate } from "./truncation"
1919import { Plugin } from "@/plugin"
2020import { Global } from "@/global"
21+ // altimate_change start — upstream_fix: resolve literal shell paths for permission matching
22+ import type { Node } from "web-tree-sitter"
23+
24+ function shellWords ( node : Node ) {
25+ return node . namedChildren . filter (
26+ ( child ) : child is Node =>
27+ child !== null && [ "command_name" , "word" , "string" , "raw_string" , "concatenation" ] . includes ( child . type ) ,
28+ )
29+ }
30+
31+ function literalWord ( node : Node | null ) : string | undefined {
32+ if ( ! node ) return
33+ if ( node . type === "raw_string" ) return node . text . slice ( 1 , - 1 )
34+ if ( node . type === "word" ) {
35+ if ( / [ ~ $ * ? { \[ ] / . test ( node . text ) ) return
36+ return node . text . replace ( / \\ ( .) / gs, "$1" )
37+ }
38+ if ( node . type === "string" && node . namedChildren . every ( ( child ) => child ?. type === "string_content" ) ) {
39+ return node . text . slice ( 1 , - 1 ) . replace ( / \\ \n / g, "" ) . replace ( / \\ ( [ $ ` " \\ ] ) / g, "$1" )
40+ }
41+ if ( node . type === "command_name" || node . type === "concatenation" ) {
42+ const parts = node . namedChildren . map ( literalWord )
43+ if ( parts . every ( ( part ) => part !== undefined ) ) return parts . join ( "" )
44+ }
45+ }
46+
47+ // Track literal directory changes in straight-line sequences. Analyze branches
48+ // independently and keep subshell, substitution, and pipeline changes local.
49+ function * shellCommands ( node : Node | null , cwd : string ) : Generator < { node : Node ; cwd : string } , string > {
50+ if ( ! node ) return cwd
51+ if ( node . type === "command" ) {
52+ yield { node, cwd }
53+ for ( const child of node . namedChildren ) yield * shellCommands ( child , cwd )
54+ const [ name , arg ] = shellWords ( node ) . map ( literalWord )
55+ return name === "cd" && arg && ! arg . startsWith ( "-" ) ? path . resolve ( cwd , arg ) : cwd
56+ }
57+ const sequential =
58+ [ "program" , "list" , "subshell" , "command_substitution" , "compound_statement" , "redirected_statement" ] . includes ( node . type ) &&
59+ ! node . children . some ( ( child ) => child ?. type === "||" || child ?. type === "&" )
60+ let next = cwd
61+ for ( const child of node . namedChildren ) {
62+ const result = yield * shellCommands ( child , sequential ? next : cwd )
63+ if ( sequential ) next = result
64+ }
65+ return [ "program" , "list" , "compound_statement" , "redirected_statement" ] . includes ( node . type ) ? next : cwd
66+ }
67+ // altimate_change end
2168
2269// altimate_change start — run-mode markers must not reach bash child processes.
2370// `run` sets ALTIMATE_RUN_MODE on its own process to arm run-mode-only
@@ -132,7 +179,9 @@ export const BashTool = Tool.define("bash", async () => {
132179 ) ,
133180 } ) ,
134181 async execute ( params , ctx ) {
135- const cwd = params . workdir || Instance . directory
182+ // altimate_change start — upstream_fix: resolve workdir relative to the active instance
183+ const cwd = path . resolve ( Instance . directory , params . workdir || "." )
184+ // altimate_change end
136185 if ( params . timeout !== undefined && params . timeout < 0 ) {
137186 throw new Error ( `Invalid timeout value: ${ params . timeout } . Timeout must be a positive number.` )
138187 }
@@ -145,34 +194,41 @@ export const BashTool = Tool.define("bash", async () => {
145194 if ( ! Instance . containsPath ( cwd ) ) directories . add ( cwd )
146195 const patterns = new Set < string > ( )
147196 const always = new Set < string > ( )
197+ // altimate_change start — upstream_fix: retain raw command rules alongside absolute path rules
198+ const permissionAliases = new Map < string , Set < string > > ( )
148199
149- for ( const node of tree . rootNode . descendantsOfType ( "command" ) ) {
200+ for ( const { node, cwd : commandCwd } of shellCommands ( tree . rootNode , cwd ) ) {
201+ // altimate_change end
150202 if ( ! node ) continue
151203
152204 // Get full command text including redirects if present
153205 let commandText = node . parent ?. type === "redirected_statement" ? node . parent . text : node . text
154206
155- const command = [ ]
156- for ( let i = 0 ; i < node . childCount ; i ++ ) {
157- const child = node . child ( i )
158- if ( ! child ) continue
159- if (
160- child . type !== "command_name" &&
161- child . type !== "word" &&
162- child . type !== "string" &&
163- child . type !== "raw_string" &&
164- child . type !== "concatenation"
165- ) {
166- continue
167- }
168- command . push ( child . text )
169- }
207+ // altimate_change start — upstream_fix: normalize static path operands, including directory listings
208+ const words = shellWords ( node )
209+ const command = words . map ( ( word ) => word . text )
210+ const name = words [ 0 ] && literalWord ( words [ 0 ] )
211+ const source = node . parent ?. type === "redirected_statement" ? node . parent : node
212+ const replacements : { start : number ; end : number ; text : string } [ ] = [ ]
170213
171214 // not an exhaustive list, but covers most common cases
172- if ( [ "cd" , "rm" , "cp" , "mv" , "mkdir" , "touch" , "chmod" , "chown" , "cat" ] . includes ( command [ 0 ] ) ) {
173- for ( const arg of command . slice ( 1 ) ) {
174- if ( arg . startsWith ( "-" ) || ( command [ 0 ] === "chmod" && arg . startsWith ( "+" ) ) ) continue
175- const resolved = await fs . realpath ( path . resolve ( cwd , arg ) ) . catch ( ( ) => "" )
215+ if ( name && [ "cd" , "rm" , "cp" , "mv" , "mkdir" , "touch" , "chmod" , "chown" , "cat" , "ls" ] . includes ( name ) ) {
216+ const operands = words . slice ( 1 ) . filter ( ( word ) => ! word . text . startsWith ( "-" ) )
217+ const targets = operands . flatMap ( ( word ) => {
218+ const arg = literalWord ( word )
219+ if ( arg === undefined || ( name === "chmod" && arg . startsWith ( "+" ) ) ) return [ ]
220+ const target = path . resolve ( commandCwd , arg )
221+ if ( ! [ "chmod" , "chown" ] . includes ( name ) || word !== operands [ 0 ] ) {
222+ replacements . push ( { start : word . startIndex , end : word . endIndex , text : target } )
223+ }
224+ return [ target ]
225+ } )
226+ if ( name === "ls" && operands . length === 0 ) {
227+ targets . push ( commandCwd )
228+ replacements . push ( { start : node . endIndex , end : node . endIndex , text : ` ${ commandCwd } ` } )
229+ }
230+ for ( const arg of targets ) {
231+ const resolved = await fs . realpath ( arg ) . catch ( ( ) => "" )
176232 log . info ( "resolved path" , { arg, resolved } )
177233 if ( resolved ) {
178234 const normalized =
@@ -184,10 +240,22 @@ export const BashTool = Tool.define("bash", async () => {
184240 }
185241 }
186242 }
243+ const original = commandText
244+ for ( const replacement of replacements . reverse ( ) ) {
245+ const start = replacement . start - source . startIndex
246+ const end = replacement . end - source . startIndex
247+ commandText = commandText . slice ( 0 , start ) + replacement . text + commandText . slice ( end )
248+ }
249+ const aliases = permissionAliases . get ( original ) ?? new Set < string > ( )
250+ aliases . add ( commandText )
251+ permissionAliases . set ( original , aliases )
252+ // altimate_change end
187253
188254 // cd covered by above check
189255 if ( command . length && command [ 0 ] !== "cd" ) {
190- patterns . add ( commandText )
256+ // altimate_change start — upstream_fix: rules match the authored command text
257+ patterns . add ( original )
258+ // altimate_change end
191259 always . add ( BashArity . prefix ( command ) . join ( " " ) + " *" )
192260 }
193261 }
@@ -211,7 +279,13 @@ export const BashTool = Tool.define("bash", async () => {
211279 permission : "bash" ,
212280 patterns : Array . from ( patterns ) ,
213281 always : Array . from ( always ) ,
214- metadata : { } ,
282+ // altimate_change start — upstream_fix: evaluate raw and normalized commands as equivalent spellings
283+ metadata : {
284+ permissionAliases : Object . fromEntries (
285+ Array . from ( permissionAliases , ( [ key , values ] ) => [ key , [ ...values ] . map ( ( value ) => [ value ] ) ] ) ,
286+ ) ,
287+ } ,
288+ // altimate_change end
215289 } )
216290 }
217291
0 commit comments