agentic-dev-loop 1.0.0a9__tar.gz → 1.0.0a10__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 (69) hide show
  1. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/PKG-INFO +79 -10
  2. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/README.md +78 -9
  3. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/config.py +11 -6
  5. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/actions.py +24 -3
  6. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/cli.py +2 -1
  7. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/dryrun.py +17 -5
  8. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/graph.py +26 -8
  9. agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/hostboundary.py +137 -0
  10. agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/registry.py +291 -0
  11. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/stages.py +75 -6
  12. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/PKG-INFO +79 -10
  13. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/SOURCES.txt +3 -0
  14. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_actions.py +39 -1
  15. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_cli.py +2 -0
  16. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_config.py +6 -5
  17. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_dry_run.py +39 -0
  18. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_graph.py +22 -2
  19. agentic_dev_loop-1.0.0a10/tests/test_hostboundary.py +136 -0
  20. agentic_dev_loop-1.0.0a10/tests/test_hostboundary_integration.py +91 -0
  21. agentic_dev_loop-1.0.0a10/tests/test_registry.py +243 -0
  22. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_stages.py +50 -1
  23. agentic_dev_loop-1.0.0a9/src/agentic_dev_loop/engine/registry.py +0 -133
  24. agentic_dev_loop-1.0.0a9/tests/test_registry.py +0 -114
  25. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/LICENSE +0 -0
  26. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/setup.cfg +0 -0
  27. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/__init__.py +0 -0
  28. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/__init__.py +0 -0
  29. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/results.py +0 -0
  30. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/verdict.py +0 -0
  31. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/__init__.py +0 -0
  32. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/agents.py +0 -0
  33. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  34. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  35. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/isolation.py +0 -0
  36. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/llm.py +0 -0
  37. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/loaders.py +0 -0
  38. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/outputs.py +0 -0
  39. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/proc.py +0 -0
  40. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/secrets.py +0 -0
  41. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/py.typed +0 -0
  42. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/__init__.py +0 -0
  43. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/__main__.py +0 -0
  44. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/server.py +0 -0
  45. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  46. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  47. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  48. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  49. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_agents.py +0 -0
  50. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_aidlc.py +0 -0
  51. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_e2e_fixtures.py +0 -0
  52. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_isolation.py +0 -0
  53. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_isolation_integration.py +0 -0
  54. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_isolation_pbt.py +0 -0
  55. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_llm.py +0 -0
  56. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_loaders.py +0 -0
  57. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_module_contract.py +0 -0
  58. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_outputs.py +0 -0
  59. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_outputs_pbt.py +0 -0
  60. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_proc.py +0 -0
  61. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_publish.py +0 -0
  62. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_results.py +0 -0
  63. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_sdk_compat.py +0 -0
  64. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_secrets.py +0 -0
  65. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_secrets_integration.py +0 -0
  66. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_secrets_pipeline.py +0 -0
  67. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_source_resolution.py +0 -0
  68. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_verdict.py +0 -0
  69. {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_viz.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.0a10
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
@@ -90,10 +90,27 @@ LLM timeout (agent runs are long).
90
90
  | 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
91
 
92
92
  ```bash
93
- pip install "agentic-dev-loop==1.0.0a8"
93
+ pip install "agentic-dev-loop==1.0.0a9"
94
94
  agentic-dev-loop --version
95
95
  ```
96
96
 
97
+ The canonical consumer install is **pipx**, which puts the engine's venv
98
+ *outside* the workspace — the sandbox mount set is exactly `{workspace}`, so an
99
+ out-of-workspace install is unmounted and never agent-writable:
100
+
101
+ ```bash
102
+ pipx install "agentic-dev-loop==1.0.0a9"
103
+ ```
104
+
105
+ If you install with `uv` shortly after a release, pass `--refresh` — PyPI
106
+ serves the package index with `Cache-Control: max-age=600`, and uv otherwise
107
+ reuses a pre-upload snapshot of the index for up to 10 minutes, reporting
108
+ "no version of agentic-dev-loop==X.Y.Z" for a version that is already live:
109
+
110
+ ```bash
111
+ uv pip install --refresh "agentic-dev-loop==1.0.0a9"
112
+ ```
113
+
97
114
  On the first run the engine pulls the **digest-pinned** dev image
98
115
  (`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
99
116
  supply-chain check refuses to continue on a mismatch. If you're running
@@ -188,13 +205,14 @@ steps:
188
205
 
189
206
  | Key | Notes |
190
207
  | --- | --- |
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. |
208
+ | `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
209
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
193
210
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
194
211
  | `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
212
  | `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
213
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
197
214
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
215
+ | `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. |
198
216
 
199
217
  ### Secrets (`secrets:`) — refs only, never values
200
218
 
@@ -253,28 +271,79 @@ steps:
253
271
  sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
254
272
  on the Docker bridge network — the default network-none container is always
255
273
  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).
274
+ - **Command, agent, and sandbox-action steps** may declare it. The host-side
275
+ built-in `commit` action never takes it (refused once the registry is known).
258
276
  - **Author opt-in is the control.** A networked step gives its agent raw
259
277
  egress for that stage; the default stays network-off everywhere. Reserve it
260
278
  for stages that need it (QA, integration). The residual risk is documented
261
279
  in [docs/setup.md](docs/setup.md).
262
280
 
281
+ ## Host boundary (security model)
282
+
283
+ The orchestrator, tool layer, and the `commit` action run in the **host
284
+ process** — they hold your credentials and your full user account, so the
285
+ engine keeps anything agent-writable out of that process:
286
+
287
+ - **Consumer actions are sandbox-executed.** An action you register via an
288
+ extension is a command-shaped script (`script + params`), run through the
289
+ same sandbox path as command steps — the exit code is the verdict, and the
290
+ code never runs in the host. Only the engine's trusted built-ins (`commit`,
291
+ document loaders) run host-side.
292
+ - **Extensions load from outside the workspace.** `extensions_dir` defaults to
293
+ `~/.agentic-dev-loop/extensions`; at load time any extension or tool module
294
+ whose file resolves under the workspace is refused *before import*, naming
295
+ the file.
296
+ - **The engine installs outside the workspace.** Install with pipx so the venv
297
+ is unmounted from the sandbox; a startup self-check refuses to run if
298
+ `agentic_dev_loop` resolves under the workspace (an editable install bypasses
299
+ everything).
300
+ - **Tooling is read-only by the loop.** At run start the pipeline file,
301
+ `agents_dir`, `additional_files`, `aidlc-docs/`, and `tools/` must be
302
+ git-clean relative to HEAD — otherwise the run refuses, naming the dirty
303
+ files. The `commit` action excludes those paths from its sweep, so the loop
304
+ never commits its own tooling.
305
+ - **`commit` stays host-side, deliberately.** It holds push credentials; moving
306
+ it into the sandbox would trade host-exec-of-trusted-code for
307
+ credential-in-sandbox. It runs argv-style with a scrubbed child env and
308
+ `-c core.hooksPath=~/.agentic-dev-loop/state/hooks` so repo hooks never
309
+ execute.
310
+
311
+ Net: the host executes nothing from the workspace, reads only git-clean
312
+ control files, and the only agent-writable host-consumed thing left is the code
313
+ the loop is building.
314
+
315
+ ### Consumer migration notes
316
+
317
+ - **Actions** — replace a host-side `fn` registration with
318
+ `registry.add_sandbox_action(name, script)`. `script` is a command string
319
+ rendered with the step's `params:` plus `{subject}`; the exit code is the
320
+ verdict. `add_action` (host-side) is now engine-built-in-only and is refused
321
+ from extensions.
322
+ - **Extensions** — move workspace-resident extension modules (e.g.
323
+ `extensions/*.py`) into `~/.agentic-dev-loop/extensions/` (or whatever
324
+ `extensions_dir:` names), and reinstall the engine via pipx so it is not
325
+ editable/in-workspace.
326
+ - **Tooling** — commit the pipeline, agent defs, shared `additional_files`,
327
+ `aidlc-docs/`, and `tools/` before running: the run refuses dirty tooling,
328
+ and `commit` never commits it.
329
+
263
330
  ## Development environment
264
331
 
265
332
  ```bash
266
333
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
267
334
  cd agentic-dev-loop
268
- ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
335
+ pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
269
336
  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
337
+ python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
271
338
  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
339
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
273
340
  ./run-demo.sh # viz demo — scripted run replay, no pipeline needed
274
341
  ```
275
342
 
276
- - `setup-venv.sh` is safe to re-run and picks the right interpreter even on
277
- machines with exotic Python installations.
343
+ - `setup-venv.sh` builds the throwaway `.venv` for local `pytest` runs (the
344
+ pipeline's `test` step provisions its own `.venv-sandbox` inside the
345
+ sandbox). The loop itself must run from the pipx install — never an
346
+ in-workspace `.venv`, which the startup self-check refuses.
278
347
  - Docker-marked integration tests need a local daemon; CI runs them on Linux.
279
348
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
280
349
  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,14 @@ 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. |
159
177
 
160
178
  ### Secrets (`secrets:`) — refs only, never values
161
179
 
@@ -214,28 +232,79 @@ steps:
214
232
  sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
215
233
  on the Docker bridge network — the default network-none container is always
216
234
  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).
235
+ - **Command, agent, and sandbox-action steps** may declare it. The host-side
236
+ built-in `commit` action never takes it (refused once the registry is known).
219
237
  - **Author opt-in is the control.** A networked step gives its agent raw
220
238
  egress for that stage; the default stays network-off everywhere. Reserve it
221
239
  for stages that need it (QA, integration). The residual risk is documented
222
240
  in [docs/setup.md](docs/setup.md).
223
241
 
242
+ ## Host boundary (security model)
243
+
244
+ The orchestrator, tool layer, and the `commit` action run in the **host
245
+ process** — they hold your credentials and your full user account, so the
246
+ engine keeps anything agent-writable out of that process:
247
+
248
+ - **Consumer actions are sandbox-executed.** An action you register via an
249
+ extension is a command-shaped script (`script + params`), run through the
250
+ same sandbox path as command steps — the exit code is the verdict, and the
251
+ code never runs in the host. Only the engine's trusted built-ins (`commit`,
252
+ document loaders) run host-side.
253
+ - **Extensions load from outside the workspace.** `extensions_dir` defaults to
254
+ `~/.agentic-dev-loop/extensions`; at load time any extension or tool module
255
+ whose file resolves under the workspace is refused *before import*, naming
256
+ the file.
257
+ - **The engine installs outside the workspace.** Install with pipx so the venv
258
+ is unmounted from the sandbox; a startup self-check refuses to run if
259
+ `agentic_dev_loop` resolves under the workspace (an editable install bypasses
260
+ everything).
261
+ - **Tooling is read-only by the loop.** At run start the pipeline file,
262
+ `agents_dir`, `additional_files`, `aidlc-docs/`, and `tools/` must be
263
+ git-clean relative to HEAD — otherwise the run refuses, naming the dirty
264
+ files. The `commit` action excludes those paths from its sweep, so the loop
265
+ never commits its own tooling.
266
+ - **`commit` stays host-side, deliberately.** It holds push credentials; moving
267
+ it into the sandbox would trade host-exec-of-trusted-code for
268
+ credential-in-sandbox. It runs argv-style with a scrubbed child env and
269
+ `-c core.hooksPath=~/.agentic-dev-loop/state/hooks` so repo hooks never
270
+ execute.
271
+
272
+ Net: the host executes nothing from the workspace, reads only git-clean
273
+ control files, and the only agent-writable host-consumed thing left is the code
274
+ the loop is building.
275
+
276
+ ### Consumer migration notes
277
+
278
+ - **Actions** — replace a host-side `fn` registration with
279
+ `registry.add_sandbox_action(name, script)`. `script` is a command string
280
+ rendered with the step's `params:` plus `{subject}`; the exit code is the
281
+ verdict. `add_action` (host-side) is now engine-built-in-only and is refused
282
+ from extensions.
283
+ - **Extensions** — move workspace-resident extension modules (e.g.
284
+ `extensions/*.py`) into `~/.agentic-dev-loop/extensions/` (or whatever
285
+ `extensions_dir:` names), and reinstall the engine via pipx so it is not
286
+ editable/in-workspace.
287
+ - **Tooling** — commit the pipeline, agent defs, shared `additional_files`,
288
+ `aidlc-docs/`, and `tools/` before running: the run refuses dirty tooling,
289
+ and `commit` never commits it.
290
+
224
291
  ## Development environment
225
292
 
226
293
  ```bash
227
294
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
228
295
  cd agentic-dev-loop
229
- ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
296
+ pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
230
297
  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
298
+ python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
232
299
  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
300
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
234
301
  ./run-demo.sh # viz demo — scripted run replay, no pipeline needed
235
302
  ```
236
303
 
237
- - `setup-venv.sh` is safe to re-run and picks the right interpreter even on
238
- machines with exotic Python installations.
304
+ - `setup-venv.sh` builds the throwaway `.venv` for local `pytest` runs (the
305
+ pipeline's `test` step provisions its own `.venv-sandbox` inside the
306
+ sandbox). The loop itself must run from the pipx install — never an
307
+ in-workspace `.venv`, which the startup self-check refuses.
239
308
  - Docker-marked integration tests need a local daemon; CI runs them on Linux.
240
309
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
241
310
  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.0a10"
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"
@@ -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
@@ -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
 
@@ -13,10 +13,12 @@ from __future__ import annotations
13
13
  from pathlib import Path
14
14
 
15
15
  from agentic_dev_loop.core.config import PipelineConfig, StepConfig, load_config
16
+ from .hostboundary import check_orchestrator_install, check_tooling_integrity
16
17
  from .loaders import resolve_docs
17
18
  from .isolation import (isolation_state_dir, resolve_effective_profile,
18
19
  resolve_image_ref)
19
- from .registry import Registry, default_registry, load_extensions
20
+ from .registry import (Registry, default_registry, load_extensions,
21
+ validate_action_network)
20
22
  from .secrets import scan_env_files
21
23
 
22
24
 
@@ -32,12 +34,18 @@ def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
32
34
 
33
35
  def _validate_actions(cfg: PipelineConfig, registry: Registry) -> None:
34
36
  """Unknown action names fail at graph build time; catch them here so a
35
- dry run is a true load pass."""
37
+ dry run is a true load pass. Host-side built-in actions cannot take
38
+ ``network`` (020) — deferred here because the config schema can't
39
+ distinguish host-side from sandbox actions."""
40
+ known = set(registry.actions) | set(registry.sandbox_actions)
36
41
  for step in cfg.steps:
37
- if step.action is not None and step.action not in registry.actions:
42
+ if step.action is None:
43
+ continue
44
+ if step.action not in known:
38
45
  raise ValueError(
39
46
  f"step {step.name!r}: unknown action {step.action!r} "
40
- f"(registered: {sorted(registry.actions)})")
47
+ f"(registered: {sorted(known)})")
48
+ validate_action_network(step.name, step.action, step.network, registry)
41
49
 
42
50
 
43
51
  def _resolved_image(cfg: PipelineConfig) -> tuple[str, str, float]:
@@ -114,11 +122,15 @@ def dry_run(cfg_path, args, registry: Registry | None = None) -> int:
114
122
  cfg_path = Path(cfg_path)
115
123
  cfg = load_config(cfg_path)
116
124
  base = cfg_path.resolve().parent
125
+ check_orchestrator_install(Path.cwd())
117
126
  scan_env_files(Path.cwd())
127
+ check_tooling_integrity(cfg, cfg_path, base, Path.cwd())
118
128
 
119
129
  if registry is None:
120
130
  registry = default_registry()
121
- load_extensions(cfg.extensions, registry)
131
+ load_extensions(cfg.extensions, registry,
132
+ extensions_dir=cfg.extensions_dir,
133
+ workspace=Path.cwd())
122
134
 
123
135
  _validate_agents(cfg, base / cfg.agents_dir)
124
136
  _validate_actions(cfg, registry)
@@ -19,11 +19,14 @@ from langgraph.graph import END, START, StateGraph
19
19
  from agentic_dev_loop import viz
20
20
  from .actions import git_worktree_clean, kill_listeners_on_port
21
21
  from agentic_dev_loop.core.config import load_config
22
+ from .hostboundary import (check_orchestrator_install,
23
+ check_tooling_integrity, tooling_rel_paths)
22
24
  from .isolation import startup_sandbox
23
25
  from .llm import build_llm
24
26
  from .loaders import resolve_docs
25
27
  from .secrets import resolve_secrets, scan_env_files
26
- from .registry import Registry, default_registry, load_extensions
28
+ from .registry import (Registry, default_registry, load_extensions,
29
+ validate_action_network)
27
30
  from .stages import (Runtime, build_context, clear_attempt_artifacts,
28
31
  make_action_node, make_agent_node, make_bump_node,
29
32
  make_command_node, make_router, success_node)
@@ -46,6 +49,7 @@ class LoopState(TypedDict):
46
49
  outcome: str | None # "success" once routed to the success terminal
47
50
  commit_allowed: bool # worktree was clean at run start
48
51
  state_dir: str # resolved pipeline state dir (for the commit action)
52
+ tooling_paths: list[str] # workspace-relative paths the commit never sweeps
49
53
  step_verdicts: dict[str, str | None] # per-step pass/fail/None(unclear)
50
54
  retry_target: str | None # where the bump node loops back to
51
55
  thread_id: str
@@ -55,11 +59,17 @@ def build_graph(runtime: Runtime, checkpointer=None):
55
59
  """Compile the pipeline's state graph: one node per step (agent, command,
56
60
  or action), plus the bump retry node and the success terminal. Unknown
57
61
  action names fail here, at build time, not mid-run."""
62
+ known_actions = (set(runtime.registry.actions)
63
+ | set(runtime.registry.sandbox_actions))
58
64
  for step in runtime.cfg.steps:
59
- if step.action is not None and step.action not in runtime.registry.actions:
65
+ if step.action is None:
66
+ continue
67
+ if step.action not in known_actions:
60
68
  raise ValueError(
61
69
  f"step {step.name!r}: unknown action {step.action!r} "
62
- f"(registered: {sorted(runtime.registry.actions)})")
70
+ f"(registered: {sorted(known_actions)})")
71
+ validate_action_network(step.name, step.action, step.network,
72
+ runtime.registry)
63
73
  graph = StateGraph(LoopState)
64
74
  for step in runtime.cfg.steps:
65
75
  if step.kind == "agent":
@@ -108,14 +118,20 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
108
118
  base = cfg_path.resolve().parent
109
119
  max_attempts = getattr(args, "max_attempts", None) or cfg.max_attempts
110
120
 
111
- # 019: refuse secret-bearing workspace files and resolve declared secret
112
- # refs host-side BEFORE any sandbox/LLM work — fail fast, loudly.
121
+ # 020: the host must execute nothing from the workspace — refuse an
122
+ # in-workspace engine install, then the 019 env-file ban, before any
123
+ # sandbox/LLM work. Tooling integrity is checked once the config is known.
124
+ check_orchestrator_install(Path.cwd())
113
125
  scan_env_files(Path.cwd())
114
126
  resolved_secrets = resolve_secrets(cfg)
127
+ check_tooling_integrity(cfg, cfg_path, base, Path.cwd())
128
+ tooling_paths = tooling_rel_paths(cfg, cfg_path, base, Path.cwd())
115
129
 
116
130
  if registry is None:
117
131
  registry = default_registry()
118
- load_extensions(cfg.extensions, registry)
132
+ load_extensions(cfg.extensions, registry,
133
+ extensions_dir=cfg.extensions_dir,
134
+ workspace=Path.cwd())
119
135
 
120
136
  state_dir = Path(cfg.state_dir).expanduser()
121
137
  if not state_dir.is_absolute():
@@ -128,13 +144,14 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
128
144
  refresh=getattr(args, "refresh", False))
129
145
  try:
130
146
  return _run_pipeline(cfg, base, registry, state_dir, max_attempts,
131
- args, viz_bus, sandbox, resolved_secrets)
147
+ args, viz_bus, sandbox, resolved_secrets,
148
+ tooling_paths)
132
149
  finally:
133
150
  sandbox.close()
134
151
 
135
152
 
136
153
  def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
137
- sandbox, resolved_secrets) -> tuple[bool, int]:
154
+ sandbox, resolved_secrets, tooling_paths) -> tuple[bool, int]:
138
155
  runtime = Runtime(cfg=cfg, registry=registry, llm=build_llm(cfg.llm),
139
156
  agents_dir=base / cfg.agents_dir, state_dir=state_dir,
140
157
  workspace=Path.cwd(), sandbox=sandbox,
@@ -170,6 +187,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
170
187
  # itself dirty the worktree.
171
188
  "commit_allowed": git_worktree_clean(Path.cwd()),
172
189
  "state_dir": str(state_dir),
190
+ "tooling_paths": tooling_paths,
173
191
  "step_verdicts": {},
174
192
  "retry_target": None,
175
193
  "thread_id": thread_id,
@@ -0,0 +1,137 @@
1
+ """Host-side boundary checks (020): the host must execute nothing from the
2
+ workspace and read only git-clean control files.
3
+
4
+ Two fail-fast guards run before any sandbox/LLM work:
5
+
6
+ - ``check_orchestrator_install`` — refuses to run when the engine package
7
+ itself resolves under the workspace (an in-workspace or editable install
8
+ means the sandbox can rewrite the code the host is about to run).
9
+ - ``check_tooling_integrity`` — refuses when tooling paths (pipeline, agent
10
+ defs, additional files, aidlc-docs, tools) are dirty relative to HEAD,
11
+ closing the self-modifying loop and the self-gate (an agent-writable,
12
+ network-on gate script).
13
+
14
+ ``tooling_rel_paths`` is the single source of the tooling path set, shared by
15
+ the integrity check and the commit action's exclusion pathspec so the two can
16
+ never disagree.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import shutil
22
+ import subprocess
23
+ from pathlib import Path
24
+ from typing import TYPE_CHECKING
25
+
26
+ from .proc import child_env
27
+
28
+ if TYPE_CHECKING:
29
+ from agentic_dev_loop.core.config import PipelineConfig
30
+
31
+ # Tooling directories are workspace-root-relative fixed names (the same layout
32
+ # the dogfood pipeline and yosk use); the other tooling paths are config- or
33
+ # pipeline-file-relative.
34
+ TOOLING_DIR_NAMES = ("aidlc-docs", "tools")
35
+
36
+
37
+ class HostBoundaryError(RuntimeError):
38
+ """A loud fail-fast host-boundary failure; the message names the problem
39
+ and points at the remedy."""
40
+
41
+
42
+ def check_orchestrator_install(workspace: str | Path,
43
+ engine_file: str | Path | None = None) -> None:
44
+ """Refuse to run when the engine resolves under the workspace (020).
45
+
46
+ The sandbox mount set is exactly {workspace}, so an in-workspace (or
47
+ editable) install is agent-writable — the next run would execute
48
+ agent-modified engine code in the host process. ``engine_file`` is an
49
+ injectable seam for tests; production uses ``agentic_dev_loop.__file__``.
50
+ """
51
+ if engine_file is None:
52
+ import agentic_dev_loop
53
+ engine_file = agentic_dev_loop.__file__
54
+ resolved = Path(engine_file).resolve()
55
+ ws = Path(workspace).resolve()
56
+ if resolved.is_relative_to(ws):
57
+ raise HostBoundaryError(
58
+ f"agentic_dev_loop resolves to {resolved}, inside the workspace "
59
+ f"{ws} — the engine must be installed outside the workspace "
60
+ "(e.g. `pipx install agentic-dev-loop`); refusing to run")
61
+
62
+
63
+ def tooling_rel_paths(cfg: "PipelineConfig", pipeline_path: str | Path,
64
+ base: str | Path, workspace: str | Path) -> list[str]:
65
+ """Workspace-relative tooling paths for one pipeline, deduplicated and
66
+ sorted. Only paths inside the workspace are included — anything outside
67
+ the workspace is already outside the sandbox mount and therefore not
68
+ agent-writable."""
69
+ ws = Path(workspace).resolve()
70
+ base = Path(base).resolve()
71
+ candidates = [
72
+ Path(pipeline_path).resolve(),
73
+ (base / cfg.agents_dir).resolve(),
74
+ *((base / rel).resolve() for rel in cfg.additional_files),
75
+ *(ws / name for name in TOOLING_DIR_NAMES),
76
+ ]
77
+ rels = {p.relative_to(ws).as_posix() for p in candidates
78
+ if p.is_relative_to(ws)}
79
+ return sorted(rels)
80
+
81
+
82
+ def _git_status(paths: list[str], workspace: Path) -> tuple[int, str, str]:
83
+ git = shutil.which("git")
84
+ if git is None:
85
+ raise HostBoundaryError(
86
+ "git is not on PATH — the tooling integrity check cannot run")
87
+ result = subprocess.run(
88
+ [git, "status", "--porcelain", "--untracked-files=all", "--", *paths],
89
+ cwd=workspace, capture_output=True, text=True, check=False,
90
+ shell=False, env=child_env())
91
+ return result.returncode, result.stdout, result.stderr
92
+
93
+
94
+ def _status_paths(output: str) -> list[str]:
95
+ """The path column of ``git status --porcelain`` lines, with the two
96
+ status characters stripped (``XY path`` or ``XY path -> path``)."""
97
+ paths = []
98
+ for line in output.splitlines():
99
+ if not line:
100
+ continue
101
+ rest = line[3:] if len(line) > 3 else ""
102
+ if " -> " in rest:
103
+ rest = rest.split(" -> ", 1)[1]
104
+ if rest.strip():
105
+ paths.append(rest)
106
+ return paths
107
+
108
+
109
+ def check_tooling_integrity(cfg: "PipelineConfig", pipeline_path: str | Path,
110
+ base: str | Path, workspace: str | Path) -> None:
111
+ """Refuse when any tooling path is dirty relative to HEAD (020).
112
+
113
+ The run reads these paths host-side (pipeline, agent defs, shared
114
+ additional files, the AI-DLC docs, and the ``tools/`` scripts). An agent
115
+ that can rewrite them for the next run — or a gate script the agent can
116
+ edit — must not be trusted. Dirty files are named; the commit action
117
+ excludes the same paths so the loop never commits tooling changes.
118
+ """
119
+ rels = tooling_rel_paths(cfg, pipeline_path, base, workspace)
120
+ if not rels:
121
+ return
122
+ code, output, err = _git_status(rels, Path(workspace))
123
+ if code != 0:
124
+ # No git repository → no HEAD baseline to be clean relative to; the
125
+ # commit gate is already disabled for the same reason. Every other git
126
+ # failure is loud.
127
+ if "not a git repository" in err:
128
+ return
129
+ raise HostBoundaryError(
130
+ "tooling integrity check failed — git could not inspect the "
131
+ f"tooling paths: {err.strip() or output.strip()}")
132
+ dirty = _status_paths(output)
133
+ if dirty:
134
+ raise HostBoundaryError(
135
+ "tooling file(s) modified relative to HEAD: " + ", ".join(dirty)
136
+ + " — tooling changes must be committed (or reverted) before the "
137
+ "loop runs; the loop never commits them itself")