@ingeniomaps/cauce 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/engine/cli/catalog.js +9 -1
- package/engine/hooks/input.js +63 -6
- package/engine/hooks/shell.js +13 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,42 @@ desde este repositorio no va, porque el que lee no puede actuar sobre eso. Cuand
|
|
|
14
14
|
unas pocas líneas casi siempre es porque cuenta cómo se descubrió el problema o por qué se eligió el
|
|
15
15
|
diseño — eso vive en el commit y en el código.
|
|
16
16
|
|
|
17
|
+
## [0.63.0] - 2026-09-06
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- **El salto de línea termina la lista de argumentos de un comando.** `shell-boundary` leía los
|
|
22
|
+
argumentos de un `cp`, un `tee` o un `sed -i` cruzando a la línea siguiente, así que el destino que
|
|
23
|
+
acusaba podía ser el comando de abajo: un bloqueo que hablaba de una escritura en `…/python3`, una
|
|
24
|
+
ruta que no aparecía en el comando. Falla hacia el lado seguro —frena de más— pero señala algo que no
|
|
25
|
+
existe, y un guard que señala mal es el que se termina apagando. **Qué cambia para vos**: si escribís
|
|
26
|
+
scripts de varias líneas en un solo comando, dejás de ver bloqueos por rutas inventadas; lo que sí
|
|
27
|
+
escribe fuera de las raíces se sigue frenando igual.
|
|
28
|
+
|
|
29
|
+
- **El cuerpo de un heredoc es texto, no un comando.** Escribir un archivo con
|
|
30
|
+
`cat > nota.md <<'FIN' … FIN` juzgaba cada línea del documento como si fuera a ejecutarse, así que no
|
|
31
|
+
se podía documentar lo que los guards vigilan: un párrafo que explica por qué no se borra la raíz se
|
|
32
|
+
bloqueaba por nombrarlo, y la salida era cambiar de herramienta para escribir un archivo. Un heredoc
|
|
33
|
+
es entrada estándar y no se ejecuta nunca. **Qué cambia para vos**: el cuerpo deja de juzgarse y la
|
|
34
|
+
línea que lo abre se sigue juzgando entera, con su redirección —también si la escribís después del
|
|
35
|
+
delimitador, como en `cat <<FIN > salida`—. Lo que se pierde a cambio: un cuerpo que después alguien
|
|
36
|
+
ejecuta; al escribirse no ejecuta nada, y cuando se corra el guard verá el comando de verdad.
|
|
37
|
+
|
|
38
|
+
- **Una variable delante de `git commit` ya no apaga tres guards.** `dependencies`, `governance` y el
|
|
39
|
+
control de generados de `verify` sólo corren sobre un commit, y decidían si lo era con un ancla que
|
|
40
|
+
no contempla lo que un shell admite antes del verbo. Con `VAR=1 git commit` dejaban de correr **sin
|
|
41
|
+
decir nada**, y con la ironía de que el prefijo que se escribe para un commit de gobernanza es una
|
|
42
|
+
asignación: `OPS_GOVERNANCE_OVERRIDE=1 git commit` no leía el override, hacía que el guard no se
|
|
43
|
+
ejecutara. **Qué cambia para vos**: si venías escribiendo ese prefijo, ahora el guard corre y te va a
|
|
44
|
+
frenar. La variable se lee del entorno del guard, no del comando, y el mensaje ahora lo dice.
|
|
45
|
+
|
|
46
|
+
- **Un índice que no se puede leer deja de autorizar el commit.** `stagedFiles` devolvía una lista vacía
|
|
47
|
+
tanto si el índice estaba vacío como si no se pudo leer, y los tres guards de arriba leen esa
|
|
48
|
+
respuesta: una lectura fallida se les presentaba como «no hay nada que revisar». Llegar a una es
|
|
49
|
+
fácil, porque el guard no expande variables: `git -C $OPS commit` resuelve la ruta literal `$OPS`.
|
|
50
|
+
**Qué cambia para vos**: ese comando ahora se frena con un mensaje que nombra la causa. Escribí la
|
|
51
|
+
ruta literal en `git -C`.
|
|
52
|
+
|
|
17
53
|
## [0.62.0] - 2026-09-06
|
|
18
54
|
|
|
19
55
|
### Corregido
|
package/engine/cli/catalog.js
CHANGED
|
@@ -102,7 +102,15 @@ function evaluationBench(root, agent, caso, force, kind) {
|
|
|
102
102
|
// reintentos, rehacer un banco es una operación que falla de vez en cuando y deja la corrida sin
|
|
103
103
|
// empezar.
|
|
104
104
|
fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 })
|
|
105
|
-
|
|
105
|
+
// Con `force`: el banco es desechable y se acaba de borrar, así que lo que sobreviva al `rmSync` se
|
|
106
|
+
// pisa en vez de cortar la corrida. Sin esto, `copyTemplate` se niega ante cualquier archivo que
|
|
107
|
+
// quede —«El destino contiene …/AGENTS.md»— y el mismo test falló así tres veces en un día, en las
|
|
108
|
+
// dos patas de la matriz. Por qué algo sobrevive a un borrado que no lanzó no está establecido.
|
|
109
|
+
//
|
|
110
|
+
// No ablanda ninguna protección: la pregunta «¿acá alguien trabajó?» la contesta el `git status` de
|
|
111
|
+
// arriba, que exige `--force` explícito para seguir. Esta segunda puerta no la eligió nadie y sólo
|
|
112
|
+
// se cerraba a veces, que es la clase de freno que enseña a re-correr sin leer.
|
|
113
|
+
IN.scaffold(dir, { name: 'Banco de evaluación', mode: 'sidecar', quiet: true, force: true })
|
|
106
114
|
// El motor por symlink: la misma resolución que en una instancia real —`node_modules/@ingeniomaps`—
|
|
107
115
|
// sin pagar un `npm install` por corrida. El cargo llega a un banco donde el CLI funciona.
|
|
108
116
|
const scope = path.join(dir, 'node_modules', '@ingeniomaps')
|
package/engine/hooks/input.js
CHANGED
|
@@ -22,10 +22,27 @@ function readInput() {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// El cuerpo de un heredoc es entrada estándar: no se ejecuta, se escribe. Juzgarlo como comando frenaba
|
|
26
|
+
// documentar lo que los guards vigilan — escribir un archivo que explica por qué borrar la raíz es
|
|
27
|
+
// catastrófico se bloqueaba por nombrarlo—, y la salida era cambiar de herramienta, que es el rodeo que
|
|
28
|
+
// un guard no debería enseñar.
|
|
29
|
+
//
|
|
30
|
+
// La línea de apertura se conserva **entera**, porque sí es comando y `shell-boundary` tiene que seguir
|
|
31
|
+
// viendo dónde escribe. Entera incluye lo que va después del delimitador: `cat <<FIN > salida` es una
|
|
32
|
+
// forma válida y su destino está ahí. El cuerpo empieza en el salto de línea, no en el delimitador —
|
|
33
|
+
// recortar desde el delimitador se llevaba esa redirección, y nada lo notaba porque en la forma común
|
|
34
|
+
// el destino va antes del `<<`.
|
|
35
|
+
//
|
|
36
|
+
// Lo que se pierde: un cuerpo que después alguien ejecuta. `cat > script.sh <<EOF` no ejecuta nada al
|
|
37
|
+
// escribirse y el guard verá el comando de verdad cuando alguien corra el script; el borde filoso es
|
|
38
|
+
// `$(cat <<EOF …)`, donde el cuerpo sí corre y ya no se mira. Es el mismo trato que con el mensaje de un
|
|
39
|
+
// commit: se frena la forma habitual, no al que quiere pasar.
|
|
40
|
+
const HEREDOC = /<<(-?)\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2([^\n]*)\n[\s\S]*?^\s*\3\s*$/gm
|
|
41
|
+
|
|
25
42
|
function commandOf(input) {
|
|
26
43
|
const value = input.tool_input && (input.tool_input.command || input.tool_input.cmd)
|
|
27
44
|
|| input.command || input.input && input.input.command || process.env.OPS_HOOK_COMMAND || ''
|
|
28
|
-
return Array.isArray(value) ? value.join(' ') :
|
|
45
|
+
return String(Array.isArray(value) ? value.join(' ') : value).replace(HEREDOC, '<<$1$2$3$2$4')
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
function fileOf(input) {
|
|
@@ -86,19 +103,59 @@ function configOf(root) {
|
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
|
|
106
|
+
// Vacía lo que va entre comillas, dejando una marca que ningún patrón confunde con una ruta ni con un
|
|
107
|
+
// comando. Vive acá porque la usan tres lugares por razones distintas, y cada uno explica la suya donde
|
|
108
|
+
// la llama.
|
|
109
|
+
const unquoted = (command) => String(command).replace(/'[^']*'|"[^"]*"/g, '\u0000')
|
|
110
|
+
|
|
111
|
+
// Sobre qué repositorio se lee el índice. En un commit se mira el comando con el mensaje vaciado, por
|
|
112
|
+
// la misma razón por la que `destructive` lo hace: un mensaje que menciona `git -C $VAR` no está
|
|
113
|
+
// eligiendo un repositorio, lo está citando. Sin esto, el commit que explica este arreglo se bloquea a
|
|
114
|
+
// sí mismo — pasó al escribirlo.
|
|
115
|
+
//
|
|
116
|
+
// El precio es una ruta entrecomillada en el propio `-C` de un commit —`git -C "mi carpeta" commit`—,
|
|
117
|
+
// que se pierde y cae al cwd. Es más raro que un mensaje que cita un comando, y el cwd de un commit
|
|
118
|
+
// suele ser el repositorio correcto; el caso contrario deja al guard leyendo un índice ajeno.
|
|
89
119
|
function gitDirectory(command, cwd) {
|
|
90
|
-
const
|
|
91
|
-
const
|
|
120
|
+
const text = isCommit(command) ? unquoted(command) : command
|
|
121
|
+
const flag = text.match(/(?:^|\s)git\s+-C\s+(['"]?)([^\s'";&|]+)\1/)
|
|
122
|
+
const cd = text.match(/(?:^|[;&|]\s*)cd\s+(['"]?)([^\s'";&|]+)\1/)
|
|
92
123
|
return path.resolve(cwd, flag ? flag[2] : cd ? cd[2] : '.')
|
|
93
124
|
}
|
|
94
125
|
|
|
126
|
+
// Lo que un shell admite delante del verbo: asignaciones de entorno, `env` y `sudo`. `VAR=1 git commit`
|
|
127
|
+
// empieza por la asignación, así que un ancla que sólo acepta el principio del comando o un separador
|
|
128
|
+
// no ve el `git` que viene después.
|
|
129
|
+
//
|
|
130
|
+
// Falla en los dos sentidos y uno no avisa. Del lado ruidoso, el mensaje del commit vuelve a juzgarse
|
|
131
|
+
// como comando. Del silencioso —el que importa— los tres guards que sólo corren sobre un commit dejan
|
|
132
|
+
// de correr: gobernanza, dependencias y generados. Cualquier variable delante alcanza, y la ironía es
|
|
133
|
+
// que el prefijo que el procedimiento manda escribir para un commit de gobernanza es
|
|
134
|
+
// `OPS_GOVERNANCE_OVERRIDE=1`: escrito ahí, el guard no lee el override, directamente no se ejecuta.
|
|
135
|
+
const PREFIX = String.raw`(?:^|[;&|]\s*)(?:(?:env|sudo)\s+)*`
|
|
136
|
+
+ String.raw`(?:[A-Za-z_][A-Za-z0-9_]*=(?:'[^']*'|"[^"]*"|\S*)\s+)*`
|
|
137
|
+
const COMMIT = new RegExp(PREFIX + String.raw`git(?:\s+-C\s+\S+)?\s+commit(?:\s|$)`)
|
|
138
|
+
|
|
95
139
|
function isCommit(command) {
|
|
96
|
-
return
|
|
140
|
+
return COMMIT.test(command)
|
|
97
141
|
}
|
|
98
142
|
|
|
143
|
+
// Un índice vacío y un índice ilegible no son la misma respuesta: la primera autoriza a seguir, la
|
|
144
|
+
// segunda no autoriza nada. Devolviendo `[]` en los dos casos, los tres guards que preguntan acá se
|
|
145
|
+
// apagaban en silencio ante cualquier lectura fallida — y llegar a una es fácil, porque `gitDirectory`
|
|
146
|
+
// no expande variables: `git -C $OPS commit` resuelve la ruta literal `$OPS`, que no existe.
|
|
147
|
+
//
|
|
148
|
+
// Es la regla que el propio shim ya tiene escrita —«Un guard que no encuentra su motor bloquea, nunca
|
|
149
|
+
// permite»—, aplicada donde faltaba. Bloquear desde acá es seguro: los tres llamadores son guards, así
|
|
150
|
+
// que no hay ningún consumidor que sólo quiera consultar el índice.
|
|
99
151
|
function stagedFiles(dir) {
|
|
100
152
|
const result = spawnSync('git', ['-C', dir, 'diff', '--cached', '--name-only'], { encoding: 'utf8' })
|
|
101
|
-
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
const why = (result.stderr || '').trim() || (result.error && result.error.message) || 'git falló'
|
|
155
|
+
block(`no se pudo leer el índice de ${dir} (${why}). Un guard que no puede verificar no autoriza. `
|
|
156
|
+
+ 'Si usaste una variable en `git -C`, escribí la ruta literal.')
|
|
157
|
+
}
|
|
158
|
+
return result.stdout.trim().split('\n').filter(Boolean)
|
|
102
159
|
}
|
|
103
160
|
|
|
104
161
|
// R10 pide «la autorización configurada para el proyecto» y `runner.allowPush` es esa configuración:
|
|
@@ -157,5 +214,5 @@ const DECLARE_IT = 'Si el proyecto necesita escribir ahí, declaralo en writable
|
|
|
157
214
|
module.exports = {
|
|
158
215
|
readInput, commandOf, patchOf, filesOf, contentOf, cwdOf, block, configOf,
|
|
159
216
|
gitDirectory, isCommit, stagedFiles, pushAllowed, findOpsRoot,
|
|
160
|
-
writableRoots, outsideRoots, DECLARE_IT,
|
|
217
|
+
writableRoots, outsideRoots, DECLARE_IT, unquoted,
|
|
161
218
|
}
|
package/engine/hooks/shell.js
CHANGED
|
@@ -10,7 +10,7 @@ const path = require('node:path')
|
|
|
10
10
|
const { spawnSync } = require('node:child_process')
|
|
11
11
|
const {
|
|
12
12
|
commandOf, cwdOf, block, gitDirectory, isCommit, stagedFiles, pushAllowed,
|
|
13
|
-
writableRoots, outsideRoots, DECLARE_IT,
|
|
13
|
+
writableRoots, outsideRoots, DECLARE_IT, unquoted,
|
|
14
14
|
} = require('./input')
|
|
15
15
|
|
|
16
16
|
// Dónde empieza y dónde termina una palabra dentro de un comando. Tres reglas de la tabla de abajo lo
|
|
@@ -169,10 +169,17 @@ function dependencies(input) {
|
|
|
169
169
|
// Tres familias, porque los comandos no nombran su destino igual: `tee` y `truncate` escriben en cada
|
|
170
170
|
// argumento, `cp` y sus hermanos en el último, y `sed` sólo escribe con `-i` —sin él lee y manda a
|
|
171
171
|
// stdout, y esa redirección la ve REDIRECT—.
|
|
172
|
+
//
|
|
173
|
+
// El salto de línea termina una lista de argumentos igual que `;`. Sin excluirlo, la de un `cp` seguía
|
|
174
|
+
// leyendo la línea de abajo y el destino terminaba siendo el comando siguiente: un bloqueo que nombraba
|
|
175
|
+
// `…/python3`, una ruta que no aparecía en el comando. Se veía con un heredoc debajo, pero el heredoc no
|
|
176
|
+
// era la causa —sólo hacía que la lectura frenara en un token que sobrevive—: sin él la lista cruzaba
|
|
177
|
+
// igual y el último token era la marca de lo entrecomillado, que el filtro final descarta. O sea que
|
|
178
|
+
// pasaba de casualidad, y aserciar que pasa no fijaba nada.
|
|
172
179
|
const REDIRECT = /(?:^|[\s(])&?\d*>>?\s*(?![&(])([^\s;|&<>()]+)/g
|
|
173
|
-
const EVERY_ARG = /(?:^|[\s;|&(])(tee|truncate)\s+([^;|&<>()]+)/g
|
|
174
|
-
const LAST_ARG = /(?:^|[\s;|&(])(cp|mv|install|rsync)\s+([^;|&<>()]+)/g
|
|
175
|
-
const SED = /(?:^|[\s;|&(])sed\s+([^;|&<>()]+)/g
|
|
180
|
+
const EVERY_ARG = /(?:^|[\s;|&(])(tee|truncate)\s+([^;|&<>()\n]+)/g
|
|
181
|
+
const LAST_ARG = /(?:^|[\s;|&(])(cp|mv|install|rsync)\s+([^;|&<>()\n]+)/g
|
|
182
|
+
const SED = /(?:^|[\s;|&(])sed\s+([^;|&<>()\n]+)/g
|
|
176
183
|
const IN_PLACE = /(?:^|\s)-{1,2}i/
|
|
177
184
|
|
|
178
185
|
// Los argumentos que no son flags. El valor de un flag se cuela —`truncate -s 0 log` trae el `0`— y no
|
|
@@ -180,10 +187,6 @@ const IN_PLACE = /(?:^|\s)-{1,2}i/
|
|
|
180
187
|
// decide un bloqueo. Filtrarlo sería una rama que ninguna prueba puede ver caer.
|
|
181
188
|
const positional = (text) => text.trim().split(/\s+/).filter((one) => one && !one.startsWith('-'))
|
|
182
189
|
|
|
183
|
-
// Vacía lo que va entre comillas, dejando una marca que ningún patrón confunde con una ruta ni con un
|
|
184
|
-
// comando. Lo usan dos guards por razones distintas, y cada uno explica la suya donde lo llama.
|
|
185
|
-
const unquoted = (command) => String(command).replace(/'[^']*'|"[^"]*"/g, '\u0000')
|
|
186
|
-
|
|
187
190
|
// Un `>` adentro de una cadena no redirige nada. Pierde el destino entrecomillado, que es un falso
|
|
188
191
|
// negativo — el error barato en un guard que ya es incompleto, porque el caro es frenar un comando
|
|
189
192
|
// legítimo y que alguien apague el guard entero.
|
|
@@ -250,7 +253,8 @@ function governance(input) {
|
|
|
250
253
|
if (governed.length) {
|
|
251
254
|
const files = governed.map((file) => ` - ${file}`).join('\n')
|
|
252
255
|
block(`El commit toca gobernanza protegida:\n${files}\n` +
|
|
253
|
-
'Usa OPS_GOVERNANCE_OVERRIDE=1 solo con aprobación
|
|
256
|
+
'Usa OPS_GOVERNANCE_OVERRIDE=1 solo con aprobación, en el entorno del guard: escrita delante '
|
|
257
|
+
+ 'del comando no llega hasta acá.')
|
|
254
258
|
}
|
|
255
259
|
}
|
|
256
260
|
|