@ingeniomaps/cauce 0.79.0 → 0.81.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 +132 -0
- package/README.md +6 -3
- package/automatization/hooks/README.md +48 -2
- package/automatization/hooks/guard-chat.sh +5 -0
- package/automatization/hooks/guard-secrets-read.sh +3 -0
- package/automatization/hooks/guard-secrets-shell.sh +3 -0
- package/automatization/runners/claude/README.md +5 -2
- package/automatization/runners/claude/manifest.json +26 -1
- package/automatization/runners/claude/settings.json +13 -0
- package/automatization/runners/codex/README.md +4 -0
- package/automatization/runners/codex/hooks.json +7 -0
- package/automatization/runners/gemini/README.md +6 -2
- package/automatization/runners/gemini/settings.json +19 -0
- package/engine/automation/index.js +9 -1
- package/engine/cli/args.js +1 -0
- package/engine/cli/bootstrap.js +1 -1
- package/engine/cli/ops.js +9 -5
- package/engine/cli/wiring.js +21 -3
- package/engine/config/paths.js +5 -3
- package/engine/hooks/approval.js +42 -8
- package/engine/hooks/chat.js +122 -0
- package/engine/hooks/files.js +94 -35
- package/engine/hooks/input.js +7 -1
- package/engine/hooks/run.js +30 -3
- package/engine/hooks/secrets-shell.js +62 -0
- package/engine/hooks/self-approval.js +31 -0
- package/engine/hooks/shell.js +49 -28
- package/engine/integrations/providers/jira.js +1 -1
- package/engine/integrations/registry.js +28 -5
- package/engine/secrets/index.js +201 -0
- package/package.json +1 -1
- package/template/AGENTS.md +14 -3
- package/template/integrations/README.md +21 -0
- package/template/organization/README.md +52 -0
- package/template/planning/adr/README.md +1 -0
- package/template/planning/adr/system/OPS-007-contrato-de-secretos-compartido.md +62 -0
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,138 @@ 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.81.0] - 2026-09-11
|
|
18
|
+
|
|
19
|
+
### Cambiado
|
|
20
|
+
|
|
21
|
+
- **Lo que pedís en el chat ya no te frena.** Los guards veían la herramienta y nada de la conversación,
|
|
22
|
+
así que frenaban igual lo que pediste con todas las letras y lo que el agente decidía solo. Ahora, en
|
|
23
|
+
Claude Code, Codex y Gemini, un hook registra tu mensaje y los guards lo leen: si nombraste lo que se
|
|
24
|
+
iba a frenar —«borrá la prueba de altas», «reescribí la migración 004»—, pasa; si no, el agente te dice
|
|
25
|
+
qué se frenó y un «dale» aprueba exactamente eso. `plan-first` deja de pedir un plan cuando el cambio lo
|
|
26
|
+
pediste vos. Lo que el agente hace por su cuenta, dentro de un recorrido o en un subagente, se sigue
|
|
27
|
+
frenando igual.
|
|
28
|
+
|
|
29
|
+
**Qué cambia para vos**: corré `automation install` para que tu runner registre el hook nuevo
|
|
30
|
+
(`UserPromptSubmit` en Claude y Codex, `BeforeAgent` en Gemini); en Codex, confialo con `/hooks`. Tu
|
|
31
|
+
mensaje se guarda en el temporal del sistema, no en el repositorio, y sólo el último de cada sesión.
|
|
32
|
+
Antigravity sigue con el archivo de aprobación.
|
|
33
|
+
|
|
34
|
+
- **Leer una credencial por shell se frena en los cuatro runners, y en Claude ya no te frena a vos.** Un
|
|
35
|
+
guard nuevo, `secrets-shell`, frena el comando que muestra una credencial —`cat .env`, `head`, `grep`,
|
|
36
|
+
`source`, una redirección `<`, un `node -e`— en Claude, Codex, Gemini y Antigravity. Hasta ahora sólo
|
|
37
|
+
Claude lo frenaba, con reglas nativas `permissions.deny` que tampoco dejaban pasar tu pedido; esas reglas
|
|
38
|
+
se retiran, y como con el resto de los guards, si lo pedís en el chat, pasa.
|
|
39
|
+
|
|
40
|
+
**Qué cambia para vos**: corré `automation install`; en Claude quita las reglas que Cauce había puesto y
|
|
41
|
+
conserva las tuyas. Si querés un bloqueo nativo total, escribilo como regla propia.
|
|
42
|
+
|
|
43
|
+
### Corregido
|
|
44
|
+
|
|
45
|
+
- **El bloqueo de `verify` cita la prueba que falló también con jest, vitest, mocha y pytest.** Con esas
|
|
46
|
+
herramientas citaba el archivo, el resumen o —con `pytest -v`— una prueba verde cuyo nombre decía «error».
|
|
47
|
+
Ahora reconoce cómo marca cada una la prueba que falla, comprobado contra la salida real de cada una.
|
|
48
|
+
|
|
49
|
+
**Qué cambia para vos**: el mensaje apunta a la prueba que hay que mirar.
|
|
50
|
+
|
|
51
|
+
- **El agente ya no puede escribirse la aprobación.** `planning/.ops-approval` se escribía con la misma
|
|
52
|
+
herramienta que usa el agente y ningún guard lo miraba, así que una aprobación suya destrababa igual que
|
|
53
|
+
una tuya. Los guards de límites lo frenan ahora —por la herramienta de escritura y por el destino evidente
|
|
54
|
+
de un comando—, salvo que se lo hayas pedido nombrándolo en el chat.
|
|
55
|
+
|
|
56
|
+
**Qué cambia para vos**: nada si el archivo lo editás vos. Si el agente lo escribía por pedido tuyo,
|
|
57
|
+
ahora se lo pedís en el chat.
|
|
58
|
+
|
|
59
|
+
- **En sidecar, el bloqueo dice en qué archivo aprobar.** Mandaba a `planning/.ops-approval`, y desde la
|
|
60
|
+
carpeta del workspace, donde se abre la sesión, ése no es el `planning/` de la instancia: pegar donde
|
|
61
|
+
decía no destrababa nada. Ahora nombra la ruta que el guard lee —`acme-ops/planning/.ops-approval`—; en
|
|
62
|
+
modo embebido sigue diciendo `planning/.ops-approval`.
|
|
63
|
+
|
|
64
|
+
**Qué cambia para vos**: nada que hacer; el mensaje dice dónde.
|
|
65
|
+
|
|
66
|
+
## [0.80.0] - 2026-09-10
|
|
67
|
+
|
|
68
|
+
### Agregado
|
|
69
|
+
|
|
70
|
+
- **Leer una credencial también se frena, y las identidades declaradas cuentan como credencial.** Un guard
|
|
71
|
+
nuevo, `secrets-read`, corre en la herramienta de lectura de Claude (`Read`) y de Gemini (`read_file`) y
|
|
72
|
+
frena los mismos archivos que `secrets` frena al escribir. Esos archivos ahora incluyen las identidades
|
|
73
|
+
`source: file` de `organization/secrets.json`, que antes pasaban por no tener nombre de credencial. En
|
|
74
|
+
Claude, además, la instalación agrega reglas `permissions.deny` `Read(...)` que el propio Claude aplica
|
|
75
|
+
también a `cat`, `head`, `tail`, `sed` y redirecciones.
|
|
76
|
+
|
|
77
|
+
**Qué cambia para vos**: el agente deja de poder leer `.env`, claves y tokens conocidos; `.env.example`
|
|
78
|
+
sigue legible. Si una lectura hace falta, aprobá la ruta en `planning/.ops-approval`. Corré
|
|
79
|
+
`automation install` para que tu `.claude/settings.json` reciba las reglas: se suman a las tuyas. No es un
|
|
80
|
+
límite de seguridad —un `grep -r` o un script propio siguen leyendo—, y en Codex y Antigravity no hay guard
|
|
81
|
+
de lectura.
|
|
82
|
+
|
|
83
|
+
- **Un proveedor de integraciones propio, sin tocar Cauce.** El registro de `integrations/config.json`
|
|
84
|
+
ya tenía un campo `adapter` que nadie leía: ahora `"adapter": "./adapter.js"` carga el adaptador de la
|
|
85
|
+
empresa desde `integrations/<nombre>/`, e `integration enable` lo conecta sin exigir un molde de Cauce.
|
|
86
|
+
El adaptador declara `contract: 1` y las tres funciones del contrato, y `check` rechaza el que no
|
|
87
|
+
cumpla.
|
|
88
|
+
|
|
89
|
+
**Qué cambia para vos**: nada si sólo usás Jira. Para otra herramienta, `integrations/README.md` de tu
|
|
90
|
+
instancia trae el recorrido.
|
|
91
|
+
|
|
92
|
+
- **Un contrato de secretos compartido entre repositorios, y un chequeo sin red.** Si tus servicios
|
|
93
|
+
usan un gestor de secretos, los scripts y workflows que lo conectan terminan copiados en cada
|
|
94
|
+
repositorio, y el arreglo que alguien hizo en uno no llega a los demás. `organization/secrets.json`
|
|
95
|
+
declara qué cuenta, qué identidad y qué archivos comparte cada servicio, y
|
|
96
|
+
`node tools/ops.js secrets check .` compara cada copia contra la canónica que guarda la instancia:
|
|
97
|
+
dice cuál quedó atrás y el `cp` que la pone al día. También falla si una credencial vive dentro de un
|
|
98
|
+
repositorio o si la declaración guarda un valor en vez de una referencia.
|
|
99
|
+
|
|
100
|
+
**Qué cambia para vos**: nada si no lo usás. Para adoptarlo, `organization/README.md` trae el formato
|
|
101
|
+
y el recorrido. Cauce no se conecta a ningún gestor ni trae adaptadores: lo que habla con el gestor
|
|
102
|
+
sigue siendo tuyo. Si tus proyectos tienen instancias separadas, el chequeo sólo compara dentro de
|
|
103
|
+
cada una: para que un esqueleto y sus derivados se midan contra lo mismo, van como raíces de la misma
|
|
104
|
+
instancia.
|
|
105
|
+
|
|
106
|
+
### Corregido
|
|
107
|
+
|
|
108
|
+
- **`init` rechaza un runner mal escrito sin crear la instancia.** Con `--runner none` —el valor es
|
|
109
|
+
`ninguno`— o una `--integration` que no existe, `init` escribía la instancia entera y recién después
|
|
110
|
+
salía con error: el código de salida decía que no había pasado nada y el segundo intento encontraba la
|
|
111
|
+
carpeta creada. Ahora valida los dos valores antes de escribir.
|
|
112
|
+
|
|
113
|
+
**Qué cambia para vos**: nada con un valor correcto. Con uno mal escrito, el destino queda como estaba.
|
|
114
|
+
|
|
115
|
+
- **El bloqueo de `verify` cita la prueba que falló.** Mostraba la primera línea de la salida que dijera
|
|
116
|
+
«error», y en un reporte de pruebas ésa puede ser una verde cuyo nombre lo dice. Ahora, con la salida de
|
|
117
|
+
`node --test` —spec y TAP— y de `go test`, cita la primera prueba en rojo; con otras herramientas sigue
|
|
118
|
+
buscando por palabra, pero ya no elige una línea marcada como verde.
|
|
119
|
+
|
|
120
|
+
**Qué cambia para vos**: el mensaje apunta a la prueba que hay que mirar.
|
|
121
|
+
|
|
122
|
+
- **Cada bloqueo con salida angosta dice qué líneas pegar.** Aprobar en `planning/.ops-approval` sólo
|
|
123
|
+
funciona si la ruta está escrita en la forma que ese guard coteja —absoluta la de un `Write`, relativa
|
|
124
|
+
al repositorio la de un commit—, y el mensaje decía «escribí esa(s) ruta(s)» sin nombrarlas. `verify`
|
|
125
|
+
y `dependencies` ni siquiera mostraban la línea. Ahora cada bloqueo imprime las líneas exactas, y
|
|
126
|
+
pegarlas tal cual destraba ese mismo bloqueo.
|
|
127
|
+
|
|
128
|
+
**Qué cambia para vos**: si una aprobación no pegaba y terminabas exportando la variable del guard,
|
|
129
|
+
pegá lo que dice el mensaje.
|
|
130
|
+
|
|
131
|
+
- **`plan-first` deja de frenar la configuración y los archivos de la instancia.** Frenaba
|
|
132
|
+
`ops.config.json` —justo el archivo que el límite de raíces manda a editar para declarar una ruta— y,
|
|
133
|
+
en sidecar, también `AGENTS.md`, `CLAUDE.md`, `package.json` y `.gitignore` de la instancia, como si
|
|
134
|
+
fueran producto. Ahora producto es el código de una raíz declarada: la instancia sidecar no lo es
|
|
135
|
+
aunque viva dentro de su raíz (`..`), y lo que queda fuera de toda raíz —lo declarado en
|
|
136
|
+
`writableOutsideRoots`— tampoco. En embedded, el `package.json` de la raíz sigue siendo producto.
|
|
137
|
+
|
|
138
|
+
**Qué cambia para vos**: si aprobabas esas rutas a mano en `.ops-approval` o exportabas
|
|
139
|
+
`OPS_PLAN_FIRST_OVERRIDE` para poder tocarlas, ya no hace falta.
|
|
140
|
+
|
|
141
|
+
- **`verify` deja afuera del índice de su copia lo que enlaza.** Cuando el árbol difiere del índice, el
|
|
142
|
+
guard corre los gates sobre una copia del índice con lo ignorado enlazado —`node_modules`, `.env`—. Si
|
|
143
|
+
tu `.gitignore` escribe ese directorio con barra final (`node_modules/`), el enlace entraba al índice de
|
|
144
|
+
la copia, y un gate que recorre lo trackeado —`git ls-files`— lo recibía como si fuera parte del commit.
|
|
145
|
+
|
|
146
|
+
**Qué cambia para vos**: si una suite que pasa a mano fallaba bajo `verify` sólo cuando tenías archivos
|
|
147
|
+
sin trackear, podía ser esto.
|
|
148
|
+
|
|
17
149
|
## [0.79.0] - 2026-09-10
|
|
18
150
|
|
|
19
151
|
### Corregido
|
package/README.md
CHANGED
|
@@ -216,6 +216,7 @@ Lee [template/planning/PROTOCOL.md](template/planning/PROTOCOL.md) para el contr
|
|
|
216
216
|
| `ops integration rebase <ops-root> jira KEY` | Recalcula el borrador canónico sin avanzar la base remota. |
|
|
217
217
|
| `ops integration reconcile <ops-root> jira KEY` | Conserva curación sobre la nueva base remota. |
|
|
218
218
|
| `ops integration writeback-plan <ops-root> jira` | Muestra escrituras posibles sin ejecutarlas. |
|
|
219
|
+
| `ops secrets check <ops-root>` | Compara el contrato de secretos compartido contra cada servicio, sin conectarse. |
|
|
219
220
|
| `ops automation list <ops-root>` | Lista adaptadores y su instalación. |
|
|
220
221
|
| `ops automation list-hooks <ops-root>` | Describe los guards portables disponibles. |
|
|
221
222
|
| `ops automation check <ops-root>` | Valida guards, permisos y configuraciones. |
|
|
@@ -314,9 +315,11 @@ La sincronización solo lee Jira. `writeback-plan` calcula intención local, no
|
|
|
314
315
|
`writeBack` permanece en `false`. Consulta el [recorrido de Jira](template/integrations/jira/README.md) antes
|
|
315
316
|
de conectar una instancia real.
|
|
316
317
|
|
|
317
|
-
Para añadir otra herramienta
|
|
318
|
-
`
|
|
319
|
-
promoción y validación no se reimplementan.
|
|
318
|
+
Para añadir otra herramienta no hace falta tocar Cauce: el adaptador se escribe en la instancia, se
|
|
319
|
+
registra con una ruta en el campo `adapter` y cumple el contrato —`contract: 1`, `validateConfig`,
|
|
320
|
+
`fetchItems` y `normalizeFixture`—. Staging, revisión, promoción y validación no se reimplementan. El
|
|
321
|
+
recorrido está en [template/integrations/README.md](template/integrations/README.md) y el contrato en
|
|
322
|
+
[integrations/README.md](integrations/README.md).
|
|
320
323
|
|
|
321
324
|
## Hooks y runners
|
|
322
325
|
|
|
@@ -4,6 +4,7 @@ Los hooks convierten invariantes comprobables en gates mecánicos. La base recom
|
|
|
4
4
|
|
|
5
5
|
- comandos destructivos, force, amend y stage indiscriminado;
|
|
6
6
|
- escritura de secretos o credenciales;
|
|
7
|
+
- lectura de credenciales conocidas o declaradas, en los runners que tienen una herramienta de lectura;
|
|
7
8
|
- edición manual de código generado y drift respecto a OpenAPI/SQL;
|
|
8
9
|
- commits sin Verify aplicable;
|
|
9
10
|
- apagado o borrado de la prueba que juzga el cambio;
|
|
@@ -44,6 +45,46 @@ publica— por la sensación de que ya está cubierto.
|
|
|
44
45
|
Regla práctica: si algo **tiene** que ser imposible, no lo pongas acá. Ponelo donde no dependa de leer
|
|
45
46
|
una cadena — permisos del runner, alcance del token, aprobación de un PR.
|
|
46
47
|
|
|
48
|
+
### Leer una credencial
|
|
49
|
+
|
|
50
|
+
Dos guards frenan leer un archivo que `secrets` frenaría al escribir —los nombres conocidos y las
|
|
51
|
+
identidades que declara `organization/secrets.json`—: `secrets-read` en las herramientas de lectura y de
|
|
52
|
+
búsqueda del runner (`Read` y `Grep` en Claude, `read_file` y `grep_search` en Gemini) y `secrets-shell` en
|
|
53
|
+
el shell de los cuatro runners, cuando un comando la muestra —`cat`, `head`, `grep`, `sed`, `source`, una
|
|
54
|
+
redirección `<`, un intérprete en línea—. Un comodín que la nombra —`rg -g '.env*'`, `--include='*.env'`—
|
|
55
|
+
cuenta igual. Lo que sólo la nombra —`ls`, `test -f`, `rm`, `cp .env.example .env`— pasa, y lo que la
|
|
56
|
+
persona pidió en el chat también.
|
|
57
|
+
|
|
58
|
+
Lo que no ve ninguno, comprobado en sesiones reales: una búsqueda sobre la carpeta que no nombra el archivo
|
|
59
|
+
—un `rg` de todo el árbol—, un nombre armado en una variable, y en Gemini el propio entorno del runner,
|
|
60
|
+
que carga el `.env` de la carpeta al arrancar (documentado en geminicli.com/docs/reference/configuration):
|
|
61
|
+
un `env` muestra sus valores sin leer ningún archivo.
|
|
62
|
+
|
|
63
|
+
Hasta 0.80.0 Claude traía además reglas nativas `permissions.deny` `Read(...)`. Las aplica Claude mismo, sin
|
|
64
|
+
pasar por ningún hook, así que frenaban también lo que la persona pedía, mientras en los otros runners el
|
|
65
|
+
shell no tenía freno (caso 104). `automation install` las retira de una instalación anterior y conserva las
|
|
66
|
+
que escribió la empresa: quien quiera un bloqueo nativo total lo escribe como regla propia.
|
|
67
|
+
|
|
68
|
+
### Lo que pide la persona
|
|
69
|
+
|
|
70
|
+
Un guard ve la llamada a la herramienta y nada de la conversación, así que frenaba igual lo que la persona
|
|
71
|
+
pidió con todas las letras y lo que el agente decidió solo. `chat` corre sobre el mensaje de la persona —el
|
|
72
|
+
runner lo dispara cuando ella manda algo, nunca por el resultado de una herramienta— y lo deja donde los
|
|
73
|
+
guards lo leen:
|
|
74
|
+
|
|
75
|
+
- lo que la persona **nombró** en su mensaje pasa: «leé el `.env`» autoriza leer el `.env`, y «no toques
|
|
76
|
+
el `.env`» no;
|
|
77
|
+
- lo que se frenó sin que lo nombrara queda anotado, y un «dale» en el mensaje siguiente aprueba
|
|
78
|
+
exactamente eso;
|
|
79
|
+
- `plan-first` no aplica: el plan es del trabajo que va por tareas.
|
|
80
|
+
|
|
81
|
+
No cuenta cuando no hay persona —CI, o un aviso del runner como el de un subagente que terminó—, cuando lo
|
|
82
|
+
que pidió es un recorrido de Cauce (`/autobuild`, `$flow`…), ni en la llamada de un subagente, que Claude
|
|
83
|
+
marca con `agent_id`. En Claude y Codex cada llamada trae el identificador del mensaje que la originó;
|
|
84
|
+
Gemini no lo manda, y ahí vale el último mensaje. El registro vive en el temporal del sistema, uno por
|
|
85
|
+
sesión, y los guards de límites lo cuidan junto con `planning/.ops-approval`: el agente no puede
|
|
86
|
+
escribirse ninguno de los dos. Como todo lo de esta página, frena la forma habitual y no un script decidido.
|
|
87
|
+
|
|
47
88
|
## Cómo se ejecutan
|
|
48
89
|
|
|
49
90
|
```text
|
|
@@ -67,10 +108,15 @@ runner, mientras la lógica se prueba y mantiene una sola vez en `engine/hooks/r
|
|
|
67
108
|
|
|
68
109
|
| Grupo | Guards | Wrapper |
|
|
69
110
|
|---|---|---|
|
|
70
|
-
| `pre-shell` | destructive, git-add, dependencies, governance, verify | `guard-shell.sh` |
|
|
71
|
-
| `pre-files` | secrets, generated, workspace-boundary, engine, migrations, integration-snapshot, test-evidence | `guard-files.sh` |
|
|
111
|
+
| `pre-shell` | destructive, git-add, dependencies, governance, verify, shell-boundary | `guard-shell.sh` |
|
|
112
|
+
| `pre-files` | secrets, generated, workspace-boundary, engine, migrations, integration-snapshot, test-evidence, plan-first | `guard-files.sh` |
|
|
113
|
+
| `pre-read` | secrets-read | `guard-secrets-read.sh` |
|
|
114
|
+
| `prompt` | chat | `guard-chat.sh` |
|
|
72
115
|
| `stop` | planning-drift | `guard-planning-drift.sh` |
|
|
73
116
|
|
|
117
|
+
`prompt` corre sobre el mensaje de la persona —`UserPromptSubmit` en Claude y Codex, `BeforeAgent` en
|
|
118
|
+
Gemini— y no sobre una herramienta, y su shim sale siempre con 0.
|
|
119
|
+
|
|
74
120
|
Registrar el grupo gasta un proceso por herramienta en lugar de cinco, con el mismo orden y la misma
|
|
75
121
|
semántica: el primer guard que bloquea corta la ejecución. Un runner que necesite granularidad fina puede
|
|
76
122
|
seguir registrando los wrappers individuales.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shim: qué registra está en engine/hooks/run.js → guards['chat']. Sale siempre con 0: el runner lo corre
|
|
3
|
+
# sobre el mensaje de la persona, y un 2 ahí no frena una herramienta sino lo que la persona escribió.
|
|
4
|
+
"$(dirname "$0")/run-hook.sh" chat >/dev/null 2>&1
|
|
5
|
+
exit 0
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# Claude Code
|
|
2
2
|
|
|
3
|
-
Adaptador nativo mediante `PreToolUse` y `Stop`. Instalar con:
|
|
3
|
+
Adaptador nativo mediante `PreToolUse`, `UserPromptSubmit` y `Stop`. Instalar con:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
node tools/ops.js automation install . claude
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves.
|
|
9
|
+
El instalador fusiona la sección `hooks` en `.claude/settings.json` y conserva otras claves. Las reglas
|
|
10
|
+
`permissions.deny` `Read(...)` que Cauce sumaba hasta 0.80.0 las retira al reinstalar —las declara
|
|
11
|
+
`config.retired` en `manifest.json`— y deja las que el proyecto haya escrito: leer una credencial lo frenan
|
|
12
|
+
ahora los guards, y lo que la persona pide en el chat pasa (caso 104). Si ya
|
|
10
13
|
existe una versión distinta de un archivo, se detiene sin sobrescribirla para no destruir
|
|
11
14
|
personalizaciones del proyecto. También crea `CLAUDE.md` cuando no existe y conserva uno existente.
|
|
12
15
|
|
|
@@ -4,7 +4,32 @@
|
|
|
4
4
|
"command": "claude",
|
|
5
5
|
"config": {
|
|
6
6
|
"source": "settings.json",
|
|
7
|
-
"target": ".claude/settings.json"
|
|
7
|
+
"target": ".claude/settings.json",
|
|
8
|
+
"retired": {
|
|
9
|
+
"permissions": {
|
|
10
|
+
"deny": [
|
|
11
|
+
"Read(.env)",
|
|
12
|
+
"Read(.env.local)",
|
|
13
|
+
"Read(.env.*.local)",
|
|
14
|
+
"Read(.npmrc)",
|
|
15
|
+
"Read(.netrc)",
|
|
16
|
+
"Read(_netrc)",
|
|
17
|
+
"Read(.pypirc)",
|
|
18
|
+
"Read(.dockercfg)",
|
|
19
|
+
"Read(id_rsa)",
|
|
20
|
+
"Read(id_dsa)",
|
|
21
|
+
"Read(id_ecdsa)",
|
|
22
|
+
"Read(id_ed25519)",
|
|
23
|
+
"Read(credentials)",
|
|
24
|
+
"Read(credentials*.json)",
|
|
25
|
+
"Read(*service-account*.json)",
|
|
26
|
+
"Read(*.pem)",
|
|
27
|
+
"Read(*.key)",
|
|
28
|
+
"Read(accesos.md)",
|
|
29
|
+
"Read(credenciales*)"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
8
33
|
},
|
|
9
34
|
"instructions": [
|
|
10
35
|
{
|
|
@@ -12,6 +12,19 @@
|
|
|
12
12
|
"hooks": [
|
|
13
13
|
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh" }
|
|
14
14
|
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Read|Grep",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-secrets-read.sh" }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"UserPromptSubmit": [
|
|
24
|
+
{
|
|
25
|
+
"hooks": [
|
|
26
|
+
{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-chat.sh" }
|
|
27
|
+
]
|
|
15
28
|
}
|
|
16
29
|
],
|
|
17
30
|
"Stop": [
|
|
@@ -18,6 +18,10 @@ porque cambia el hash. No se usa `--dangerously-bypass-hook-trust`.
|
|
|
18
18
|
Los `matcher` filtran el **nombre de la herramienta**: los comandos de shell llegan como `Bash` y las
|
|
19
19
|
ediciones como `apply_patch`, `Edit` o `Write`. No son los nombres internos del protocolo.
|
|
20
20
|
|
|
21
|
+
`UserPromptSubmit` registra el mensaje de la persona, para que lo que pidió en el chat pase sin aprobarlo
|
|
22
|
+
a mano. Codex le pasa a cada herramienta el `turn_id` del mensaje que la originó, que es lo que ata la
|
|
23
|
+
llamada al pedido.
|
|
24
|
+
|
|
21
25
|
Si actualizás una instalación anterior a este cambio, `.codex/hooks/hooks.json` queda huérfano —Codex
|
|
22
26
|
nunca lo leyó— y se borra a mano.
|
|
23
27
|
|
|
@@ -17,8 +17,12 @@ Antes vivían bajo `/ops:` y eran tres: el arranque y el recorrido de equipo le
|
|
|
17
17
|
así que alguien que venía de otro runner los buscaba en la lista y no estaban. Si actualizás una
|
|
18
18
|
instalación vieja, `.gemini/commands/ops/` queda huérfano y se borra a mano.
|
|
19
19
|
|
|
20
|
-
Gemini CLI tiene hooks nativos y el adaptador los usa: `BeforeTool` y `AfterAgent` en
|
|
20
|
+
Gemini CLI tiene hooks nativos y el adaptador los usa: `BeforeTool`, `BeforeAgent` y `AfterAgent` en
|
|
21
21
|
`.gemini/settings.json`, declarados en `manifest.json`. Sólo corren si la carpeta está marcada como
|
|
22
|
-
confiable —`GEMINI.md` explica qué avisa Gemini cuando no lo está—.
|
|
22
|
+
confiable —`GEMINI.md` explica qué avisa Gemini cuando no lo está—. `read_file` pasa por
|
|
23
|
+
`guard-secrets-read.sh`, que frena leer una credencial; un `cat` por `run_shell_command` lo frena
|
|
24
|
+
`secrets-shell`, en el grupo de shell.
|
|
25
|
+
`BeforeAgent` registra el mensaje de la persona, para que lo que pidió en el chat pase sin aprobarlo a
|
|
26
|
+
mano; Gemini no le pasa a cada herramienta de qué mensaje viene, así que ahí vale el último.
|
|
23
27
|
|
|
24
28
|
Comprueba la instalación con `node tools/ops.js automation doctor . gemini`.
|
|
@@ -27,6 +27,25 @@
|
|
|
27
27
|
"command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-files.sh"
|
|
28
28
|
}
|
|
29
29
|
]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"matcher": "read_file|grep_search",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-secrets-read.sh"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"BeforeAgent": [
|
|
42
|
+
{
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "$GEMINI_PROJECT_DIR/{{OPS_DIR}}automatization/hooks/guard-chat.sh"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
30
49
|
}
|
|
31
50
|
],
|
|
32
51
|
"AfterAgent": [
|
|
@@ -322,7 +322,7 @@ function uninstall(root, name, output = console) {
|
|
|
322
322
|
|
|
323
323
|
if (fs.existsSync(paths.configTarget)) {
|
|
324
324
|
const current = JSON.parse(fs.readFileSync(paths.configTarget, 'utf8'))
|
|
325
|
-
const clean = unmergeConfig(current, runnerConfig(paths, root))
|
|
325
|
+
const clean = unmergeConfig(unmergeConfig(current, runnerConfig(paths, root)), runner.config.retired || {})
|
|
326
326
|
if (clean && Object.keys(clean).length) F.atomicWriteJson(paths.configTarget, clean)
|
|
327
327
|
else { removeFile(paths.configTarget, paths.install); removed += 1 }
|
|
328
328
|
output.log(`✓ ${name}: ${runner.config.target} sin las entradas de Cauce`)
|
|
@@ -389,6 +389,14 @@ function install(root, name, output = console, options = {}) {
|
|
|
389
389
|
? { config: {}, dropped: [] }
|
|
390
390
|
: withoutDeliveredHooks(current, live, previous)
|
|
391
391
|
reportRemoved(name, clean.dropped, live, output)
|
|
392
|
+
// Lo que Cauce entregó en una versión y retiró en otra sin ser un hook, que el merge no saca: las reglas
|
|
393
|
+
// `permissions.deny` que puso el 092 y sacó el 104. Se va exactamente lo que el adaptador declara como
|
|
394
|
+
// retirado; una regla de la empresa que no coincide letra por letra se queda.
|
|
395
|
+
if (runner.config.retired) {
|
|
396
|
+
const before = JSON.stringify(clean.config)
|
|
397
|
+
clean.config = unmergeConfig(clean.config, runner.config.retired) || {}
|
|
398
|
+
if (JSON.stringify(clean.config) !== before) output.log(`− ${name}: quitadas las reglas que Cauce ya no entrega`)
|
|
399
|
+
}
|
|
392
400
|
F.atomicWriteJson(paths.configTarget, mergeConfig(clean.config, incoming))
|
|
393
401
|
// Dónde aterrizó, no sólo qué archivo: en sidecar el destino no es el repo desde el que se corrió
|
|
394
402
|
// el comando, y descubrirlo por sorpresa es la diferencia entre confiar y adivinar.
|
package/engine/cli/args.js
CHANGED
|
@@ -32,6 +32,7 @@ const FLAGS = {
|
|
|
32
32
|
adopt: [],
|
|
33
33
|
agents: ['--json', '--own', '--system'],
|
|
34
34
|
integration: ['--fixture'],
|
|
35
|
+
secrets: [],
|
|
35
36
|
automation: ['--force'],
|
|
36
37
|
learn: ['--flow', '--proposal', '--applied', '--archived', '--period'],
|
|
37
38
|
evaluate: ['--cases', '--json', '--bench', '--force', '--record', '--flow'],
|
package/engine/cli/bootstrap.js
CHANGED
package/engine/cli/ops.js
CHANGED
|
@@ -95,11 +95,6 @@ async function init(target, cli) {
|
|
|
95
95
|
if (existing.length && !force) {
|
|
96
96
|
fail(`El destino no está vacío: ${root}. Usa --force para agregar solo archivos faltantes.`)
|
|
97
97
|
}
|
|
98
|
-
IN.scaffold(root, { name, mode, force })
|
|
99
|
-
const relative = path.relative(process.cwd(), root)
|
|
100
|
-
const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
|
|
101
|
-
console.log(`\n✓ ${name}: sistema ops creado en ${root} (modo ${mode})`)
|
|
102
|
-
|
|
103
98
|
// Preguntar exige una terminal, e instalar baja un paquete y escribe `node_modules`: las dos cosas
|
|
104
99
|
// pasan cuando hay alguien mirando. Una corrida automatizada —CI, un contenedor, estas pruebas—
|
|
105
100
|
// recibe la instancia materializada y decide por bandera, sin descargas ni preguntas implícitas.
|
|
@@ -112,6 +107,13 @@ async function init(target, cli) {
|
|
|
112
107
|
interactive,
|
|
113
108
|
install: cli.has('--install') || (interactive && !cli.has('--no-install')),
|
|
114
109
|
}
|
|
110
|
+
// Se valida antes de escribir: un runner o una integración que no existen no pueden dejar una
|
|
111
|
+
// instancia hecha con el comando en error (caso 096). `BOOT.run` vuelve a validar con la misma función.
|
|
112
|
+
try { BOOT.validate(options) } catch (error) { fail(error.message, 2) }
|
|
113
|
+
IN.scaffold(root, { name, mode, force })
|
|
114
|
+
const relative = path.relative(process.cwd(), root)
|
|
115
|
+
const enter = relative && relative !== '.' ? `cd ${relative} && ` : ''
|
|
116
|
+
console.log(`\n✓ ${name}: sistema ops creado en ${root} (modo ${mode})`)
|
|
115
117
|
let result
|
|
116
118
|
try {
|
|
117
119
|
result = await BOOT.run(root, options, {
|
|
@@ -166,6 +168,7 @@ function usage() {
|
|
|
166
168
|
ops integration rebase <ops-root> <provider> <remote-key>
|
|
167
169
|
ops integration reconcile <ops-root> <provider> <remote-key>
|
|
168
170
|
ops integration writeback-plan <ops-root> <provider>
|
|
171
|
+
ops secrets check <ops-root>
|
|
169
172
|
ops automation list <ops-root>
|
|
170
173
|
ops automation list-hooks <ops-root>
|
|
171
174
|
ops automation check <ops-root>
|
|
@@ -218,6 +221,7 @@ async function run(cli) {
|
|
|
218
221
|
else if (command === 'integration') {
|
|
219
222
|
await W.integration(arg[1], arg[2], arg[3], arg[4], cli)
|
|
220
223
|
}
|
|
224
|
+
else if (command === 'secrets') W.secrets(arg[1], arg[2])
|
|
221
225
|
else if (command === 'automation') W.automation(arg[1], arg[2], arg[3], cli)
|
|
222
226
|
else if (command === 'learn') CAT.learn(arg[1], cli)
|
|
223
227
|
else if (command === 'evaluate') CAT.evaluate(arg[1], arg[2], cli)
|
package/engine/cli/wiring.js
CHANGED
|
@@ -8,6 +8,7 @@ const fs = require('node:fs')
|
|
|
8
8
|
const path = require('node:path')
|
|
9
9
|
const F = require('../core/files')
|
|
10
10
|
const I = require('../integrations/registry')
|
|
11
|
+
const SE = require('../secrets')
|
|
11
12
|
const A = require('../automation')
|
|
12
13
|
const SC = require('../core/scan')
|
|
13
14
|
const OB = require('../core/onboarding')
|
|
@@ -38,11 +39,18 @@ const INTEGRATION = {
|
|
|
38
39
|
missing: 'Falta <provider>.',
|
|
39
40
|
run: (root, provider) => {
|
|
40
41
|
const source = path.join(IN.PROJECT_ROOT, 'template', 'integrations', provider)
|
|
41
|
-
|
|
42
|
+
const fromCauce = fs.existsSync(source)
|
|
43
|
+
// Sin molde en Cauce no hay andamiaje que reponer: habilitar un proveedor propio es sólo el interruptor.
|
|
44
|
+
const own = !fromCauce && (providerRegistry(root).config.providers || {})[provider]
|
|
45
|
+
&& fs.existsSync(path.join(root, 'integrations', provider))
|
|
46
|
+
if (!fromCauce && !own) {
|
|
47
|
+
fail(`Cauce no trae un adaptador para ${provider}. Uno propio vive en integrations/${provider}/ y se `
|
|
48
|
+
+ `registra en integrations/config.json con "adapter": "./adapter.js"; con eso, enable lo conecta.`, 2)
|
|
49
|
+
}
|
|
42
50
|
// Habilitar no es inicializar: repone lo que falte y conserva lo que ya esté. Una instancia que
|
|
43
51
|
// trae el andamiaje de una versión anterior —o que ya tiene snapshots— sólo quiere el interruptor.
|
|
44
52
|
providerRegistry(root)
|
|
45
|
-
IN.copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
|
|
53
|
+
if (fromCauce) IN.copyTemplate(source, path.join(root, 'integrations', provider), {}, true)
|
|
46
54
|
switchProvider(root, provider, true)
|
|
47
55
|
console.log(`✓ ${provider}: conectado al proyecto y andamiaje en integrations/${provider}/.`)
|
|
48
56
|
// Sólo se pide lo que falta: reencender un proveedor ya configurado no debería mandar a
|
|
@@ -277,8 +285,18 @@ function automation(action, rootArg, runnerName, cli) {
|
|
|
277
285
|
fail(`Acción de automatización desconocida: ${action || '(vacía)'}`, 2)
|
|
278
286
|
}
|
|
279
287
|
|
|
288
|
+
function secrets(action, rootArg) {
|
|
289
|
+
if (action !== 'check') fail(`Acción de secretos desconocida: ${action || '(vacía)'}`, 2)
|
|
290
|
+
const result = SE.check(opsRoot(rootArg))
|
|
291
|
+
if (!result.declared) return console.log(`Sin ${SE.DECLARATION}: no hay contrato de secretos que comprobar.`)
|
|
292
|
+
for (const warning of result.warnings) console.log(`⚠ ${warning}`)
|
|
293
|
+
for (const error of result.errors) console.error(`✗ ${error}`)
|
|
294
|
+
if (result.errors.length) fail(`${result.errors.length} error(es) en el contrato de secretos`)
|
|
295
|
+
console.log(`✓ contrato de secretos: ${result.current} servicio(s) al día`)
|
|
296
|
+
}
|
|
297
|
+
|
|
280
298
|
// `init` enciende un proveedor en la misma corrida en que crea la instancia, y ésta es la operación
|
|
281
299
|
// que lo hace: se expone para que la composición no tenga que conocer la tabla entera.
|
|
282
300
|
const enableProvider = (root, provider) => INTEGRATION.enable.run(root, provider)
|
|
283
301
|
|
|
284
|
-
module.exports = { scan, onboard, integration, automation, enableProvider }
|
|
302
|
+
module.exports = { scan, onboard, integration, automation, secrets, enableProvider }
|
package/engine/config/paths.js
CHANGED
|
@@ -10,7 +10,9 @@ const path = require('node:path')
|
|
|
10
10
|
|
|
11
11
|
// `~` se expande sólo cuando es el prefijo entero. `~datos` es un nombre de directorio válido y no la
|
|
12
12
|
// casa de nadie; expandirlo ahí convertiría una ruta relativa en una absoluta que el autor no escribió.
|
|
13
|
-
|
|
13
|
+
// La usa también el contrato de secretos para las rutas de sus identidades, por la misma razón que
|
|
14
|
+
// arriba: una ruta declarada en un archivo del proyecto se resuelve igual la lea quien la lea.
|
|
15
|
+
function resolvePath(root, entry) {
|
|
14
16
|
return path.resolve(root, String(entry).replace(/^~(?=$|[/\\])/, os.homedir()))
|
|
15
17
|
}
|
|
16
18
|
|
|
@@ -21,7 +23,7 @@ function resolve(root, entry) {
|
|
|
21
23
|
function writableOutsideRoots(root, config) {
|
|
22
24
|
const declared = config && Array.isArray(config.writableOutsideRoots) ? config.writableOutsideRoots : []
|
|
23
25
|
return declared.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
24
|
-
.map((entry) => ({ declared: entry, path:
|
|
26
|
+
.map((entry) => ({ declared: entry, path: resolvePath(root, entry) }))
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
module.exports = { writableOutsideRoots }
|
|
29
|
+
module.exports = { writableOutsideRoots, resolvePath }
|
package/engine/hooks/approval.js
CHANGED
|
@@ -20,9 +20,14 @@
|
|
|
20
20
|
//
|
|
21
21
|
// Queda a la vista porque `check` avisa mientras exista. Sin eso, un archivo olvidado sigue autorizando
|
|
22
22
|
// esas mismas rutas la próxima vez que alguien las stagee, que es la puerta abierta que esto evitaba.
|
|
23
|
+
//
|
|
24
|
+
// El archivo es la vía de cuando no hay chat. Con una persona hablando, lo que ella pidió ya está
|
|
25
|
+
// aprobado —cómo se sabe, en `chat.js`—, y escribir el archivo deja de ser necesario (caso 098).
|
|
23
26
|
|
|
24
27
|
const path = require('node:path')
|
|
25
28
|
const fs = require('node:fs')
|
|
29
|
+
const { opsRoot, cwdOf } = require('./input')
|
|
30
|
+
const CHAT = require('./chat')
|
|
26
31
|
|
|
27
32
|
const APPROVAL = '.ops-approval'
|
|
28
33
|
|
|
@@ -35,16 +40,45 @@ function read(root) {
|
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
// Qué queda sin aprobar de lo que un guard está por bloquear. Se reporta sólo eso: mandar a revisar lo
|
|
38
|
-
// que ya se aprobó es lo que hace que la próxima vez nadie lea el mensaje.
|
|
39
|
-
|
|
43
|
+
// que ya se aprobó es lo que hace que la próxima vez nadie lea el mensaje. Cuenta también lo que la
|
|
44
|
+
// persona pidió en el chat.
|
|
45
|
+
function pending(root, files, input) {
|
|
40
46
|
const approved = new Set(root ? read(root) : [])
|
|
41
|
-
return files.filter((file) => !approved.has(file))
|
|
47
|
+
return CHAT.unauthorized(input, files.filter((file) => !approved.has(file)))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// El archivo que el guard va a leer, nombrado desde la carpeta en la que está la sesión. En sidecar la
|
|
51
|
+
// sesión se abre en el workspace y la instancia es una subcarpeta, así que `planning/` a secas nombraba
|
|
52
|
+
// otro directorio y pegar ahí no destrababa nada (caso 097).
|
|
53
|
+
function where(input) {
|
|
54
|
+
const root = opsRoot(input)
|
|
55
|
+
if (!root) return `planning/${APPROVAL}`
|
|
56
|
+
const file = path.join(root, 'planning', APPROVAL)
|
|
57
|
+
const session = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || cwdOf(input)
|
|
58
|
+
const relative = path.relative(session, file)
|
|
59
|
+
return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
// Cómo se toma la salida angosta, dicho una vez porque
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
62
|
+
// Cómo se toma la salida angosta, dicho una vez porque lo dicen todos los bloqueos que la tienen. Lleva
|
|
63
|
+
// las líneas exactas porque cada guard coteja la ruta en la forma que tiene a mano —absoluta la que llega
|
|
64
|
+
// de un Write, relativa al repositorio la que sale del índice— y una línea en la otra forma no pega: sin
|
|
65
|
+
// decirla, lo que quedaba a mano era la variable (caso 089). Nombra también la variable: sigue
|
|
66
|
+
// existiendo, y esconderla haría que quien la necesite la descubra sin saber su alcance.
|
|
67
|
+
//
|
|
68
|
+
// Con una persona en el chat, además, deja anotado lo que se frenó —para eso llama a `hold`— y lo dice
|
|
69
|
+
// primero: contestar es más corto que editar un archivo, y es lo que la persona ya está haciendo. El
|
|
70
|
+
// archivo queda como cosa de ella: dicho en imperativo, el agente leía «aprobalo» como una orden para él
|
|
71
|
+
// e intentaba escribírselo en vez de reintentar, medido en una sesión real de Claude Code.
|
|
72
|
+
function HOW(variable, lines, input) {
|
|
73
|
+
const chat = CHAT.hold(input, lines)
|
|
74
|
+
return (chat
|
|
75
|
+
? 'Decile a la persona qué se frenó y por qué, y esperá: si contesta «dale», reintentá el mismo cambio y '
|
|
76
|
+
+ 'pasa. Si prefiere aprobarlo a mano, que pegue ella tal cual en'
|
|
77
|
+
: 'Aprobalo pegando tal cual en')
|
|
78
|
+
+ ` ${where(input)} estas líneas:\n`
|
|
79
|
+
+ lines.map((line) => ` ${line}\n`).join('')
|
|
80
|
+
+ `Valen para ese conjunto y dejan de valer en cuanto cambie. La variable ${variable}=1 sigue existiendo `
|
|
81
|
+
+ 'y apaga el guard para toda la sesión, que es por lo que no es la vía recomendada.'
|
|
82
|
+
}
|
|
49
83
|
|
|
50
84
|
module.exports = { APPROVAL, read, pending, HOW }
|