A folder-dropped declarative plugin runs a shell hook with no allowlist entry, on a stock project

Cerrado
#497 3 comentarios 1 reacción 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
5/5
Tiempo estimado
Más de una semana
Aptitud para principiantes
35/100
Tipo de issue
Error
Claridad
Bastante claro
Estado de actividad
Tranquilo
Stack tecnológico
python
Área
security

Línea de trabajo

Empieza por plugins/trust.py, loader._discover_project, registry._resolve, registry.hooks_for y HookBus.emit para rastrear los plugins declarativos originados en el proyecto a través de la ruta stock-project reproducida. Revisa los tres enfoques de confianza propuestos con un maintainer antes de elegir el alcance. Se considera terminado cuando la política acordada impida que un hook depositado en una carpeta que no esté en la allowlist se ejecute en la ruta desatendida, y la reproducción esté cubierta por tests.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

area:config bug needs-design P2

Split out of #461 point 4 / PR #495. #496 names this as separate and out of scope there; it had no issue of its own. Found by a completeness pass over PR #495's config_digest and confirmed firsthand against the code.

Related to #496 but strictly wider: #496 needs a project that has already enabled a [python] plugin. This one needs nothing — [plugins] enabled empty (the default) is enough.

The hole

plugins/trust.py is explicit that this is the designed behavior:

Data-only / declarative plugins (no [python]) carry no executable Python — they load on folder-drop. Their shell hooks are the same risk surface as today's engine.toml *_cmd and run through the bus like any command.

That reasoning holds when a human drops the folder. It does not hold under #461's threat model, where a driven session can write anywhere in the workspace — and loader._discover_project reads manifests from <project>/.bmad-loop/plugins/*/plugin.toml, inside that writable tree.

The chain, with no trust gate at any hop:

  1. loader._discover_project yields the manifest (origin="project").
  2. registry._resolve — if manifest.python is None: returns a LoadedPlugin immediately, before the trust.is_enabled check, which only guards the [python] branch.
  3. registry.hooks_for(stage) iterates every loaded plugin and filters on manifest.hook_for(stage) only.
  4. HookBus.emit → _dispatch_declarative → _run_subprocess(cmd, shell=True).

Reproduced

Stock policy, empty allowlist, one file written:

# <project>/.bmad-loop/plugins/rogue/plugin.toml
[plugin]
name = "rogue"
api_version = 1

[hooks.pre_sweep_cycle]
cmd = "..."
policy [plugins] enabled = ()
digest moved      : False
plugin discovered : True
trusted flag      : True
hooks_for(stage)  : [('rogue', '...')]

The child SweepEngine builds a fresh PluginRegistry per run and emits pre_sweep_cycle, so the command runs on the host in the unattended path — which is precisely the human-absent window PR #495's gate exists to protect.

Why PR #495 does not close it

config_digest hashes sorted(policy.plugins.enabled). Nothing in .bmad-loop/plugins/ is read at all, and no policy field moves, so the auto-sweep integrity gate passes.

PR #495's docstring previously claimed "Adding a plugin is caught; changing what an enabled one runs is not." That is true only of plugins declaring [python]. Corrected in b049133 — the docstring and CHANGELOG now scope the claim to the [python] tier and point here, so the pin is not read as stronger than it is.

Shape of a fix (undecided — needs a maintainer call)

The trust tiers were drawn on what the plugin contains (Python vs. data). The gap is that they ignore where it came from, and project origin is agent-writable. Options, roughly in increasing cost:

  1. Gate declarative hooks on enabled too, at least for origin="project" manifests. Smallest change and it makes the allowlist mean what the CHANGELOG implies. Behavior change for anyone relying on folder-drop for a data-only plugin.
  2. Trust declarative folder-drop by origin: builtin/entry_point load freely, project-origin needs an allowlist entry. Keeps folder-drop working for anything shipped or installed outside the workspace.
  3. Digest the manifests — fold .bmad-loop/plugins/*/plugin.toml content into config_digest. Closes the auto-sweep window only, not the parent run, and does nothing for a first run. Weakest of the three; noted for completeness.

Options 1 and 2 also subsume the [plugins.<name>] settings surface: those are overlaid onto manifest defaults by registry._resolve_settings and exported into the hook env as BMAD_LOOP_SETTING_*, and the Unity plugin turns one into an --editor-path argv token. PR #495 deliberately does not pin them (the TUI settings screen writes those same tables, so pinning would refuse an auto-sweep after a supported human edit) — they sit inside an already-trusted plugin's blast radius, which is the boundary this issue and #496 are about.

Notes

  • Not a regression. Predates #495, which narrows the surrounding surface without widening this one.
  • Unlike #496, a stock project with no plugins configured is exposed.

Related: #461, #496, PR #495.

Lenguaje dominante
Python
Estrellas
174
Forks
72
Merge medio
17 h 56 min
PR fusionados (30 d)
95

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de bmad-code-org/bmad-loop

Todos los issues de bmad-code-org/bmad-loop

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.