agentic-dev-loop 1.0.0a9__tar.gz → 1.0.0a11__tar.gz

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.

Potentially problematic release.


This version of agentic-dev-loop might be problematic. Click here for more details.

Files changed (71) hide show
  1. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/PKG-INFO +152 -10
  2. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/README.md +150 -9
  3. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/pyproject.toml +2 -1
  4. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/config.py +41 -6
  5. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/actions.py +24 -3
  6. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/cli.py +2 -1
  7. agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/deps.py +77 -0
  8. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/dryrun.py +17 -5
  9. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/graph.py +26 -8
  10. agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/hostboundary.py +137 -0
  11. agentic_dev_loop-1.0.0a11/src/agentic_dev_loop/engine/registry.py +291 -0
  12. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/stages.py +96 -8
  13. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/PKG-INFO +152 -10
  14. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/SOURCES.txt +5 -0
  15. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/requires.txt +1 -0
  16. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_actions.py +39 -1
  17. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_cli.py +6 -0
  18. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_config.py +41 -5
  19. agentic_dev_loop-1.0.0a11/tests/test_deps.py +76 -0
  20. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_dry_run.py +40 -0
  21. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_graph.py +60 -2
  22. agentic_dev_loop-1.0.0a11/tests/test_hostboundary.py +137 -0
  23. agentic_dev_loop-1.0.0a11/tests/test_hostboundary_integration.py +92 -0
  24. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_isolation.py +2 -0
  25. agentic_dev_loop-1.0.0a11/tests/test_registry.py +243 -0
  26. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_secrets.py +1 -0
  27. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_secrets_pipeline.py +2 -0
  28. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_stages.py +102 -2
  29. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_viz.py +1 -0
  30. agentic_dev_loop-1.0.0a9/src/agentic_dev_loop/engine/registry.py +0 -133
  31. agentic_dev_loop-1.0.0a9/tests/test_registry.py +0 -114
  32. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/LICENSE +0 -0
  33. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/setup.cfg +0 -0
  34. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/__init__.py +0 -0
  35. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/__init__.py +0 -0
  36. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/results.py +0 -0
  37. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/core/verdict.py +0 -0
  38. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/__init__.py +0 -0
  39. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/agents.py +0 -0
  40. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  41. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  42. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/isolation.py +0 -0
  43. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/llm.py +0 -0
  44. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/loaders.py +0 -0
  45. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/outputs.py +0 -0
  46. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/proc.py +0 -0
  47. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/engine/secrets.py +0 -0
  48. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/py.typed +0 -0
  49. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__init__.py +0 -0
  50. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/__main__.py +0 -0
  51. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop/viz/server.py +0 -0
  52. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  53. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  54. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  55. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_agents.py +0 -0
  56. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_aidlc.py +0 -0
  57. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_e2e_fixtures.py +0 -0
  58. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_isolation_integration.py +0 -0
  59. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_isolation_pbt.py +0 -0
  60. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_llm.py +0 -0
  61. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_loaders.py +0 -0
  62. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_module_contract.py +0 -0
  63. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_outputs.py +0 -0
  64. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_outputs_pbt.py +0 -0
  65. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_proc.py +0 -0
  66. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_publish.py +0 -0
  67. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_results.py +0 -0
  68. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_sdk_compat.py +0 -0
  69. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_secrets_integration.py +0 -0
  70. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_source_resolution.py +0 -0
  71. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a11}/tests/test_verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a9
3
+ Version: 1.0.0a11
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -35,6 +35,7 @@ Requires-Dist: langgraph-checkpoint-sqlite>=3.0
35
35
  Provides-Extra: dev
36
36
  Requires-Dist: hypothesis>=6.0; extra == "dev"
37
37
  Requires-Dist: pytest>=8.0; extra == "dev"
38
+ Requires-Dist: pip-audit>=2.0; extra == "dev"
38
39
  Dynamic: license-file
39
40
 
40
41
  # agentic-dev-loop
@@ -90,10 +91,27 @@ LLM timeout (agent runs are long).
90
91
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
91
92
 
92
93
  ```bash
93
- pip install "agentic-dev-loop==1.0.0a8"
94
+ pip install "agentic-dev-loop==1.0.0a9"
94
95
  agentic-dev-loop --version
95
96
  ```
96
97
 
98
+ The canonical consumer install is **pipx**, which puts the engine's venv
99
+ *outside* the workspace — the sandbox mount set is exactly `{workspace}`, so an
100
+ out-of-workspace install is unmounted and never agent-writable:
101
+
102
+ ```bash
103
+ pipx install "agentic-dev-loop==1.0.0a9"
104
+ ```
105
+
106
+ If you install with `uv` shortly after a release, pass `--refresh` — PyPI
107
+ serves the package index with `Cache-Control: max-age=600`, and uv otherwise
108
+ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
109
+ "no version of agentic-dev-loop==X.Y.Z" for a version that is already live:
110
+
111
+ ```bash
112
+ uv pip install --refresh "agentic-dev-loop==1.0.0a9"
113
+ ```
114
+
97
115
  On the first run the engine pulls the **digest-pinned** dev image
98
116
  (`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
99
117
  supply-chain check refuses to continue on a mismatch. If you're running
@@ -188,13 +206,16 @@ steps:
188
206
 
189
207
  | Key | Notes |
190
208
  | --- | --- |
191
- | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call); actions are registered host-side functions. |
209
+ | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
192
210
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
193
211
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
194
212
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` grants egress to a command or agent step (default off) — see [Per-stage networking](#per-stage-networking). |
195
213
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only — never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
196
214
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
197
215
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
216
+ | `extensions_dir` | Where consumer `extensions:` modules resolve — defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
217
+ | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
218
+ | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` — a `locked` pipeline with no lockfiles fails fast at load. |
198
219
 
199
220
  ### Secrets (`secrets:`) — refs only, never values
200
221
 
@@ -253,28 +274,149 @@ steps:
253
274
  sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
254
275
  on the Docker bridge network — the default network-none container is always
255
276
  created too, so no infrastructure work happens mid-run.
256
- - **Command and agent steps** may declare it. Action steps are host-side and
257
- never take it (a load-time error).
277
+ - **Command, agent, and sandbox-action steps** may declare it. The host-side
278
+ built-in `commit` action never takes it (refused once the registry is known).
258
279
  - **Author opt-in is the control.** A networked step gives its agent raw
259
280
  egress for that stage; the default stays network-off everywhere. Reserve it
260
281
  for stages that need it (QA, integration). The residual risk is documented
261
282
  in [docs/setup.md](docs/setup.md).
262
283
 
284
+ ## Dependency policy (locked by default)
285
+
286
+ The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
287
+ default**. The engine injects the policy into every agent step's context —
288
+ the rule lives in the engine, not in hand-authored `dev.agent.md` prose (the
289
+ class of rule that deadlocked the NuGet cache).
290
+
291
+ ```yaml
292
+ deps_policy: locked # or: open (default: locked)
293
+ lockfiles: [uv.lock] # or: [packages.lock.json, package-lock.json]
294
+ ```
295
+
296
+ - **`locked`** — the human-owned lockfile is the source of truth; the agent
297
+ may not add dependencies. The injected rule says *"You may only use
298
+ dependencies already present in `<lockfile>`. If the design requires a
299
+ dependency that is not present, declare it in your final response under the
300
+ fixed header `## Missing dependencies` (one `name==version` per line) and
301
+ finish what you can. Do not add it to the manifest."* A non-empty
302
+ `## Missing dependencies` declaration **fails the run fast** with
303
+ `Extend <lockfile> with these pins and re-run: …` — one human lockfile edit,
304
+ not a retry loop. The declaration is advisory to the human; the engine never
305
+ auto-fetches the declared packages.
306
+ - **`open`** — the bootstrap protocol (018): the agent may add exact-pinned
307
+ manifest entries directly (never a package-manager `add` command); the
308
+ network-on restore step fetches them. The engine injects the same rule
309
+ canonically. Nothing about `open` is default.
310
+
311
+ The **restore step** runs the toolchain's locked mode and fails when the
312
+ manifest and lockfile disagree:
313
+
314
+ | Toolchain | Locked restore | Lockfile |
315
+ | --- | --- | --- |
316
+ | .NET | `dotnet restore --locked-mode` | `packages.lock.json` (set `RestorePackagesWithLockFile=true`) |
317
+ | node | `npm ci` | `package-lock.json` |
318
+ | python | `uv sync --locked` (or pip-tools `pip-sync requirements.lock`) | `uv.lock` — pip has no native lockfile; `uv` is the canonical locked-install tool |
319
+
320
+ ### Audit escalation in the restore step
321
+
322
+ The restore step also fails on known-CVE findings, at a per-toolchain
323
+ threshold:
324
+
325
+ | Toolchain | Audit (fails the step) |
326
+ | --- | --- |
327
+ | .NET | `NuGetAudit` at `error` — any audit finding fails the restore |
328
+ | node | `npm audit --audit-level=high` |
329
+ | python | `pip-audit` — any finding fails |
330
+
331
+ Under `locked`, a failing audit means the lockfile pins a vulnerable version:
332
+ the run fails and the human regenerates the lockfile at a patched version.
333
+ Under `open`, the failure routes to `dev` like any restore failure — the dev
334
+ agent can bump the pin, but that pin is audited again on the next pass, so
335
+ the loop self-corrects.
336
+
337
+ ### Generating a lockfile
338
+
339
+ A human generates the lockfile once, on a network machine, and commits it —
340
+ the engine never fetches on the agent's behalf:
341
+
342
+ ```bash
343
+ uv lock # python — writes uv.lock from pyproject.toml
344
+ uv sync --locked # installs exactly uv.lock; fails on any drift
345
+ npm install --package-lock-only # node — writes package-lock.json
346
+ npm ci # installs exactly package-lock.json
347
+ dotnet restore --use-lock-file # .NET — writes packages.lock.json
348
+ dotnet restore --locked-mode # installs exactly packages.lock.json
349
+ ```
350
+
351
+ Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
352
+ to prove it holds from a clean cache before setting `deps_policy: locked`.
353
+
354
+ ## Host boundary (security model)
355
+
356
+ The orchestrator, tool layer, and the `commit` action run in the **host
357
+ process** — they hold your credentials and your full user account, so the
358
+ engine keeps anything agent-writable out of that process:
359
+
360
+ - **Consumer actions are sandbox-executed.** An action you register via an
361
+ extension is a command-shaped script (`script + params`), run through the
362
+ same sandbox path as command steps — the exit code is the verdict, and the
363
+ code never runs in the host. Only the engine's trusted built-ins (`commit`,
364
+ document loaders) run host-side.
365
+ - **Extensions load from outside the workspace.** `extensions_dir` defaults to
366
+ `~/.agentic-dev-loop/extensions`; at load time any extension or tool module
367
+ whose file resolves under the workspace is refused *before import*, naming
368
+ the file.
369
+ - **The engine installs outside the workspace.** Install with pipx so the venv
370
+ is unmounted from the sandbox; a startup self-check refuses to run if
371
+ `agentic_dev_loop` resolves under the workspace (an editable install bypasses
372
+ everything).
373
+ - **Tooling is read-only by the loop.** At run start the pipeline file,
374
+ `agents_dir`, `additional_files`, `aidlc-docs/`, and `tools/` must be
375
+ git-clean relative to HEAD — otherwise the run refuses, naming the dirty
376
+ files. The `commit` action excludes those paths from its sweep, so the loop
377
+ never commits its own tooling.
378
+ - **`commit` stays host-side, deliberately.** It holds push credentials; moving
379
+ it into the sandbox would trade host-exec-of-trusted-code for
380
+ credential-in-sandbox. It runs argv-style with a scrubbed child env and
381
+ `-c core.hooksPath=~/.agentic-dev-loop/state/hooks` so repo hooks never
382
+ execute.
383
+
384
+ Net: the host executes nothing from the workspace, reads only git-clean
385
+ control files, and the only agent-writable host-consumed thing left is the code
386
+ the loop is building.
387
+
388
+ ### Consumer migration notes
389
+
390
+ - **Actions** — replace a host-side `fn` registration with
391
+ `registry.add_sandbox_action(name, script)`. `script` is a command string
392
+ rendered with the step's `params:` plus `{subject}`; the exit code is the
393
+ verdict. `add_action` (host-side) is now engine-built-in-only and is refused
394
+ from extensions.
395
+ - **Extensions** — move workspace-resident extension modules (e.g.
396
+ `extensions/*.py`) into `~/.agentic-dev-loop/extensions/` (or whatever
397
+ `extensions_dir:` names), and reinstall the engine via pipx so it is not
398
+ editable/in-workspace.
399
+ - **Tooling** — commit the pipeline, agent defs, shared `additional_files`,
400
+ `aidlc-docs/`, and `tools/` before running: the run refuses dirty tooling,
401
+ and `commit` never commits it.
402
+
263
403
  ## Development environment
264
404
 
265
405
  ```bash
266
406
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
267
407
  cd agentic-dev-loop
268
- ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
408
+ pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
269
409
  export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
270
- python -m pytest tests/ -q # fast suite
410
+ python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
271
411
  python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
272
- ./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
412
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
273
413
  ./run-demo.sh # viz demo — scripted run replay, no pipeline needed
274
414
  ```
275
415
 
276
- - `setup-venv.sh` is safe to re-run and picks the right interpreter even on
277
- machines with exotic Python installations.
416
+ - `setup-venv.sh` builds the throwaway `.venv` for local `pytest` runs (the
417
+ pipeline's `test` step provisions its own `.venv-sandbox` inside the
418
+ sandbox). The loop itself must run from the pipx install — never an
419
+ in-workspace `.venv`, which the startup self-check refuses.
278
420
  - Docker-marked integration tests need a local daemon; CI runs them on Linux.
279
421
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
280
422
  its own repo** — dev → test → review → qa → commit, with the engine's own
@@ -51,10 +51,27 @@ LLM timeout (agent runs are long).
51
51
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
52
52
 
53
53
  ```bash
54
- pip install "agentic-dev-loop==1.0.0a8"
54
+ pip install "agentic-dev-loop==1.0.0a9"
55
55
  agentic-dev-loop --version
56
56
  ```
57
57
 
58
+ The canonical consumer install is **pipx**, which puts the engine's venv
59
+ *outside* the workspace — the sandbox mount set is exactly `{workspace}`, so an
60
+ out-of-workspace install is unmounted and never agent-writable:
61
+
62
+ ```bash
63
+ pipx install "agentic-dev-loop==1.0.0a9"
64
+ ```
65
+
66
+ If you install with `uv` shortly after a release, pass `--refresh` — PyPI
67
+ serves the package index with `Cache-Control: max-age=600`, and uv otherwise
68
+ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
69
+ "no version of agentic-dev-loop==X.Y.Z" for a version that is already live:
70
+
71
+ ```bash
72
+ uv pip install --refresh "agentic-dev-loop==1.0.0a9"
73
+ ```
74
+
58
75
  On the first run the engine pulls the **digest-pinned** dev image
59
76
  (`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
60
77
  supply-chain check refuses to continue on a mismatch. If you're running
@@ -149,13 +166,16 @@ steps:
149
166
 
150
167
  | Key | Notes |
151
168
  | --- | --- |
152
- | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call); actions are registered host-side functions. |
169
+ | `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
153
170
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
154
171
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
155
172
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` grants egress to a command or agent step (default off) — see [Per-stage networking](#per-stage-networking). |
156
173
  | `secrets:` (top-level) / step `secrets:` | Declares secret **refs only — never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
157
174
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
158
175
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
176
+ | `extensions_dir` | Where consumer `extensions:` modules resolve — defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
177
+ | `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
178
+ | `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` — a `locked` pipeline with no lockfiles fails fast at load. |
159
179
 
160
180
  ### Secrets (`secrets:`) — refs only, never values
161
181
 
@@ -214,28 +234,149 @@ steps:
214
234
  sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
215
235
  on the Docker bridge network — the default network-none container is always
216
236
  created too, so no infrastructure work happens mid-run.
217
- - **Command and agent steps** may declare it. Action steps are host-side and
218
- never take it (a load-time error).
237
+ - **Command, agent, and sandbox-action steps** may declare it. The host-side
238
+ built-in `commit` action never takes it (refused once the registry is known).
219
239
  - **Author opt-in is the control.** A networked step gives its agent raw
220
240
  egress for that stage; the default stays network-off everywhere. Reserve it
221
241
  for stages that need it (QA, integration). The residual risk is documented
222
242
  in [docs/setup.md](docs/setup.md).
223
243
 
244
+ ## Dependency policy (locked by default)
245
+
246
+ The supply-chain dial is a pipeline-level `deps_policy:` field, **locked by
247
+ default**. The engine injects the policy into every agent step's context —
248
+ the rule lives in the engine, not in hand-authored `dev.agent.md` prose (the
249
+ class of rule that deadlocked the NuGet cache).
250
+
251
+ ```yaml
252
+ deps_policy: locked # or: open (default: locked)
253
+ lockfiles: [uv.lock] # or: [packages.lock.json, package-lock.json]
254
+ ```
255
+
256
+ - **`locked`** — the human-owned lockfile is the source of truth; the agent
257
+ may not add dependencies. The injected rule says *"You may only use
258
+ dependencies already present in `<lockfile>`. If the design requires a
259
+ dependency that is not present, declare it in your final response under the
260
+ fixed header `## Missing dependencies` (one `name==version` per line) and
261
+ finish what you can. Do not add it to the manifest."* A non-empty
262
+ `## Missing dependencies` declaration **fails the run fast** with
263
+ `Extend <lockfile> with these pins and re-run: …` — one human lockfile edit,
264
+ not a retry loop. The declaration is advisory to the human; the engine never
265
+ auto-fetches the declared packages.
266
+ - **`open`** — the bootstrap protocol (018): the agent may add exact-pinned
267
+ manifest entries directly (never a package-manager `add` command); the
268
+ network-on restore step fetches them. The engine injects the same rule
269
+ canonically. Nothing about `open` is default.
270
+
271
+ The **restore step** runs the toolchain's locked mode and fails when the
272
+ manifest and lockfile disagree:
273
+
274
+ | Toolchain | Locked restore | Lockfile |
275
+ | --- | --- | --- |
276
+ | .NET | `dotnet restore --locked-mode` | `packages.lock.json` (set `RestorePackagesWithLockFile=true`) |
277
+ | node | `npm ci` | `package-lock.json` |
278
+ | python | `uv sync --locked` (or pip-tools `pip-sync requirements.lock`) | `uv.lock` — pip has no native lockfile; `uv` is the canonical locked-install tool |
279
+
280
+ ### Audit escalation in the restore step
281
+
282
+ The restore step also fails on known-CVE findings, at a per-toolchain
283
+ threshold:
284
+
285
+ | Toolchain | Audit (fails the step) |
286
+ | --- | --- |
287
+ | .NET | `NuGetAudit` at `error` — any audit finding fails the restore |
288
+ | node | `npm audit --audit-level=high` |
289
+ | python | `pip-audit` — any finding fails |
290
+
291
+ Under `locked`, a failing audit means the lockfile pins a vulnerable version:
292
+ the run fails and the human regenerates the lockfile at a patched version.
293
+ Under `open`, the failure routes to `dev` like any restore failure — the dev
294
+ agent can bump the pin, but that pin is audited again on the next pass, so
295
+ the loop self-corrects.
296
+
297
+ ### Generating a lockfile
298
+
299
+ A human generates the lockfile once, on a network machine, and commits it —
300
+ the engine never fetches on the agent's behalf:
301
+
302
+ ```bash
303
+ uv lock # python — writes uv.lock from pyproject.toml
304
+ uv sync --locked # installs exactly uv.lock; fails on any drift
305
+ npm install --package-lock-only # node — writes package-lock.json
306
+ npm ci # installs exactly package-lock.json
307
+ dotnet restore --use-lock-file # .NET — writes packages.lock.json
308
+ dotnet restore --locked-mode # installs exactly packages.lock.json
309
+ ```
310
+
311
+ Keep the lockfile tracked (do not gitignore it) and re-run the locked restore
312
+ to prove it holds from a clean cache before setting `deps_policy: locked`.
313
+
314
+ ## Host boundary (security model)
315
+
316
+ The orchestrator, tool layer, and the `commit` action run in the **host
317
+ process** — they hold your credentials and your full user account, so the
318
+ engine keeps anything agent-writable out of that process:
319
+
320
+ - **Consumer actions are sandbox-executed.** An action you register via an
321
+ extension is a command-shaped script (`script + params`), run through the
322
+ same sandbox path as command steps — the exit code is the verdict, and the
323
+ code never runs in the host. Only the engine's trusted built-ins (`commit`,
324
+ document loaders) run host-side.
325
+ - **Extensions load from outside the workspace.** `extensions_dir` defaults to
326
+ `~/.agentic-dev-loop/extensions`; at load time any extension or tool module
327
+ whose file resolves under the workspace is refused *before import*, naming
328
+ the file.
329
+ - **The engine installs outside the workspace.** Install with pipx so the venv
330
+ is unmounted from the sandbox; a startup self-check refuses to run if
331
+ `agentic_dev_loop` resolves under the workspace (an editable install bypasses
332
+ everything).
333
+ - **Tooling is read-only by the loop.** At run start the pipeline file,
334
+ `agents_dir`, `additional_files`, `aidlc-docs/`, and `tools/` must be
335
+ git-clean relative to HEAD — otherwise the run refuses, naming the dirty
336
+ files. The `commit` action excludes those paths from its sweep, so the loop
337
+ never commits its own tooling.
338
+ - **`commit` stays host-side, deliberately.** It holds push credentials; moving
339
+ it into the sandbox would trade host-exec-of-trusted-code for
340
+ credential-in-sandbox. It runs argv-style with a scrubbed child env and
341
+ `-c core.hooksPath=~/.agentic-dev-loop/state/hooks` so repo hooks never
342
+ execute.
343
+
344
+ Net: the host executes nothing from the workspace, reads only git-clean
345
+ control files, and the only agent-writable host-consumed thing left is the code
346
+ the loop is building.
347
+
348
+ ### Consumer migration notes
349
+
350
+ - **Actions** — replace a host-side `fn` registration with
351
+ `registry.add_sandbox_action(name, script)`. `script` is a command string
352
+ rendered with the step's `params:` plus `{subject}`; the exit code is the
353
+ verdict. `add_action` (host-side) is now engine-built-in-only and is refused
354
+ from extensions.
355
+ - **Extensions** — move workspace-resident extension modules (e.g.
356
+ `extensions/*.py`) into `~/.agentic-dev-loop/extensions/` (or whatever
357
+ `extensions_dir:` names), and reinstall the engine via pipx so it is not
358
+ editable/in-workspace.
359
+ - **Tooling** — commit the pipeline, agent defs, shared `additional_files`,
360
+ `aidlc-docs/`, and `tools/` before running: the run refuses dirty tooling,
361
+ and `commit` never commits it.
362
+
224
363
  ## Development environment
225
364
 
226
365
  ```bash
227
366
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
228
367
  cd agentic-dev-loop
229
- ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
368
+ pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
230
369
  export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
231
- python -m pytest tests/ -q # fast suite
370
+ python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
232
371
  python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
233
- ./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
372
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
234
373
  ./run-demo.sh # viz demo — scripted run replay, no pipeline needed
235
374
  ```
236
375
 
237
- - `setup-venv.sh` is safe to re-run and picks the right interpreter even on
238
- machines with exotic Python installations.
376
+ - `setup-venv.sh` builds the throwaway `.venv` for local `pytest` runs (the
377
+ pipeline's `test` step provisions its own `.venv-sandbox` inside the
378
+ sandbox). The loop itself must run from the pipx install — never an
379
+ in-workspace `.venv`, which the startup self-check refuses.
239
380
  - Docker-marked integration tests need a local daemon; CI runs them on Linux.
240
381
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
241
382
  its own repo** — dev → test → review → qa → commit, with the engine's own
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a9"
7
+ version = "1.0.0a11"
8
8
  description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -55,6 +55,7 @@ agentic-dev-loop = "agentic_dev_loop.engine.cli:main"
55
55
  dev = [
56
56
  "hypothesis>=6.0",
57
57
  "pytest>=8.0",
58
+ "pip-audit>=2.0",
58
59
  ]
59
60
 
60
61
  [tool.setuptools.packages.find]
@@ -114,7 +114,7 @@ class StepConfig(BaseModel):
114
114
  timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
115
115
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
116
116
  setup: str | None = None # BR-13: command steps only
117
- network: bool = False # per-stage egress (command + agent steps; action steps reject)
117
+ network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
118
118
  secrets: list[str] = [] # declared ref names; action steps reject
119
119
  on_pass: RouteTarget | None = None
120
120
  on_fail: RouteTarget | None = None
@@ -173,11 +173,11 @@ class StepConfig(BaseModel):
173
173
  raise ValueError(
174
174
  f"step {self.name!r}: 'setup' is only allowed on 'command' steps"
175
175
  )
176
- if self.network and self.action is not None:
177
- raise ValueError(
178
- f"step {self.name!r}: 'network' is only allowed on 'agent' or "
179
- "'command' steps"
180
- )
176
+ # `network` is NOT rejected here: the schema can't tell a host-side
177
+ # built-in action (e.g. `commit`, which must never take egress) from
178
+ # a sandbox-executed consumer action (which may, like a command step).
179
+ # That distinction is enforced once the registry is known — see
180
+ # `validate_action_network` (020).
181
181
  if self.secrets and self.action is not None:
182
182
  raise ValueError(
183
183
  f"step {self.name!r}: 'secrets' is only allowed on 'agent' or "
@@ -201,6 +201,11 @@ class PipelineConfig(BaseModel):
201
201
  name: str
202
202
  extensions: list[str] = []
203
203
  additional_files: list[str] = []
204
+ # Where consumer `extensions:` modules resolve (work item 020). Must live
205
+ # OUTSIDE the workspace — the host refuses workspace-resident extension and
206
+ # tool-handler modules, so agent-writable files are never imported by the
207
+ # host process.
208
+ extensions_dir: str = "~/.agentic-dev-loop/extensions"
204
209
  env_allowlist: list[str] = []
205
210
  llm: LlmConfig
206
211
  isolation: IsolationConfig | None = None # None = always-on defaults
@@ -209,6 +214,27 @@ class PipelineConfig(BaseModel):
209
214
  max_attempts: int = 3
210
215
  agents_dir: str
211
216
  steps: list[StepConfig] = Field(min_length=1)
217
+ # 021 supply-chain dependency dial: `locked` (default) = the human-owned
218
+ # lockfile is the source of truth and the agent may not add dependencies;
219
+ # `open` = the bootstrap protocol (the agent may add exact-pinned manifest
220
+ # entries for the restore step to fetch). `lockfiles` names the pipeline's
221
+ # declared lockfile(s) — injected into agent context and named by the
222
+ # missing-dependency fail-fast.
223
+ deps_policy: str = "locked"
224
+ lockfiles: list[str] = []
225
+
226
+ @field_validator("deps_policy")
227
+ @classmethod
228
+ def _known_deps_policy(cls, v: str) -> str:
229
+ if v not in ("locked", "open"):
230
+ raise ValueError(
231
+ f"deps_policy must be 'locked' or 'open', got {v!r}")
232
+ return v
233
+
234
+ @field_validator("lockfiles", mode="before")
235
+ @classmethod
236
+ def _coerce_lockfiles(cls, v: Any) -> Any:
237
+ return [v] if isinstance(v, str) else v
212
238
 
213
239
  @field_validator("secrets", mode="before")
214
240
  @classmethod
@@ -227,6 +253,15 @@ class PipelineConfig(BaseModel):
227
253
  "values never belong in the pipeline yaml")
228
254
  return v
229
255
 
256
+ @model_validator(mode="after")
257
+ def _locked_requires_lockfiles(self) -> "PipelineConfig":
258
+ if self.deps_policy == "locked" and not self.lockfiles:
259
+ raise ValueError(
260
+ "deps_policy 'locked' requires at least one entry in "
261
+ "'lockfiles:' — name the lockfile(s) the locked boundary "
262
+ "enforces, or set 'deps_policy: open'")
263
+ return self
264
+
230
265
  @model_validator(mode="after")
231
266
  def _check_graph(self) -> "PipelineConfig":
232
267
  names = [s.name for s in self.steps]
@@ -186,6 +186,23 @@ def checkpoint_exclude_pathspec(state_dir: str | None, cwd: Path) -> str | None:
186
186
  return f":(exclude){rel}/*.sqlite*"
187
187
 
188
188
 
189
+ def tooling_exclude_pathspec(tooling_paths: list[str] | None) -> list[str]:
190
+ """git pathspecs excluding tooling paths from ``git add -A`` (020).
191
+
192
+ The loop must never commit its own tooling (pipeline, agent defs,
193
+ additional files, aidlc-docs, tools): the run-start integrity check
194
+ requires those paths to be clean relative to HEAD, so the auto-commit
195
+ must sweep everything *except* them. Empty when there is nothing to
196
+ exclude."""
197
+ specs = []
198
+ for rel in (tooling_paths or []):
199
+ rel = rel.replace(os.sep, "/")
200
+ if rel in ("", ".", ".."):
201
+ continue
202
+ specs.append(f":(exclude){rel}")
203
+ return specs
204
+
205
+
189
206
  def commit(state: dict, params: dict) -> dict:
190
207
  """The pipeline's only commit point. Skipped loudly when the worktree was
191
208
  dirty at run start, so the auto-commit can never swallow pre-existing
@@ -205,9 +222,13 @@ def commit(state: dict, params: dict) -> dict:
205
222
  cwd = Path.cwd()
206
223
  hooks = _trusted_hooks_dir()
207
224
  add_argv = ["-c", f"core.hooksPath={hooks}", "add", "-A"]
208
- exclude = checkpoint_exclude_pathspec(state.get("state_dir"), cwd)
209
- if exclude:
210
- add_argv += ["--", exclude]
225
+ excludes = []
226
+ checkpoint_exclude = checkpoint_exclude_pathspec(state.get("state_dir"), cwd)
227
+ if checkpoint_exclude:
228
+ excludes.append(checkpoint_exclude)
229
+ excludes.extend(tooling_exclude_pathspec(state.get("tooling_paths")))
230
+ if excludes:
231
+ add_argv += ["--", *excludes]
211
232
  added = _git(add_argv, cwd=cwd)
212
233
  if added.returncode != 0:
213
234
  # The add is the step that touches the most files (a locked DB, a
@@ -71,7 +71,8 @@ def _registry_for(cfg_path: Path) -> tuple[Registry, PipelineConfig]:
71
71
  run path instead of being rebuilt."""
72
72
  cfg = load_config(cfg_path)
73
73
  registry = default_registry()
74
- load_extensions(cfg.extensions, registry)
74
+ load_extensions(cfg.extensions, registry,
75
+ extensions_dir=cfg.extensions_dir, workspace=Path.cwd())
75
76
  return registry, cfg
76
77
 
77
78
 
@@ -0,0 +1,77 @@
1
+ """Supply-chain dependency dial (021): canonical policy injection and
2
+ missing-dependency declaration parsing.
3
+
4
+ The rule text lives HERE, in the engine, not in consumer `dev.agent.md` prose
5
+ — the hand-authored rule is exactly the class that deadlocked the NuGet cache
6
+ in 018. `deps_policy: locked` (the default) bounds the agent to the
7
+ human-owned lockfile and fails a run fast when an agent declares a missing
8
+ dependency; `deps_policy: open` injects the bootstrap protocol (the agent may
9
+ add exact-pinned manifest entries for the restore step to fetch).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ if TYPE_CHECKING:
17
+ from agentic_dev_loop.core.config import PipelineConfig
18
+
19
+ MISSING_DEPS_HEADER = "## Missing dependencies"
20
+
21
+ _LOCKED_TEMPLATE = (
22
+ "You may only use dependencies already present in {lockfiles}. If the "
23
+ "design requires a dependency that is not present, declare it in your "
24
+ "final response under the fixed header `{header}` (one `name==version` "
25
+ "per line) and finish what you can. Do not add it to the manifest."
26
+ )
27
+
28
+ _OPEN_TEXT = (
29
+ "If the design needs a dependency that is not already present, add the "
30
+ "exact-pinned entry to the dependency manifest directly (do not run a "
31
+ "package-manager `add` command — it may need the network). Your offline "
32
+ "builds may fail until the restore step fetches it — that is expected; "
33
+ "write the code to match and leave the manifest entry in place."
34
+ )
35
+
36
+
37
+ def _lockfile_label(cfg: "PipelineConfig") -> str:
38
+ if cfg.lockfiles:
39
+ return ", ".join(f"`{name}`" for name in cfg.lockfiles)
40
+ return "the lockfile"
41
+
42
+
43
+ def policy_injection(cfg: "PipelineConfig") -> str:
44
+ """The canonical dependency-policy paragraph injected into every agent
45
+ step's context, keyed on ``deps_policy`` and the declared lockfiles."""
46
+ if cfg.deps_policy == "open":
47
+ return _OPEN_TEXT
48
+ return _LOCKED_TEMPLATE.format(
49
+ lockfiles=_lockfile_label(cfg), header=MISSING_DEPS_HEADER)
50
+
51
+
52
+ def parse_missing_dependencies(text: str | None) -> list[str]:
53
+ """Parse a final agent response for the fixed ``## Missing dependencies``
54
+ section. Returns the declared pins (one non-empty line each) beneath the
55
+ header, until the next ``## `` section or end of text. An absent header or
56
+ an empty/malformed section yields an empty list — only a non-empty
57
+ declaration triggers the fail-fast."""
58
+ if not text:
59
+ return []
60
+ lines = text.splitlines()
61
+ for i, line in enumerate(lines):
62
+ if line.strip() == MISSING_DEPS_HEADER:
63
+ pins: list[str] = []
64
+ for rest in lines[i + 1:]:
65
+ if rest.startswith("## "):
66
+ break
67
+ stripped = rest.strip()
68
+ if stripped:
69
+ pins.append(stripped)
70
+ return pins
71
+ return []
72
+
73
+
74
+ def extension_message(cfg: "PipelineConfig", pins: list[str]) -> str:
75
+ """The fail-fast message naming the lockfile(s) and the declared pins."""
76
+ return (f"Extend {_lockfile_label(cfg)} with these pins and re-run:\n\n"
77
+ + "\n".join(pins))