agentic-dev-loop 1.0.0a8__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.0a8 → agentic_dev_loop-1.0.0a10}/PKG-INFO +120 -12
  2. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/README.md +119 -11
  3. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/config.py +67 -3
  5. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/actions.py +24 -3
  6. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/agents.py +7 -3
  7. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/cli.py +2 -1
  8. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/dryrun.py +19 -5
  9. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/graph.py +32 -7
  10. agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/hostboundary.py +137 -0
  11. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/isolation.py +10 -2
  12. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/outputs.py +4 -1
  13. agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/registry.py +291 -0
  14. agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/secrets.py +118 -0
  15. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/stages.py +94 -13
  16. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/PKG-INFO +120 -12
  17. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/SOURCES.txt +7 -0
  18. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_actions.py +39 -1
  19. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_cli.py +2 -0
  20. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_config.py +6 -5
  21. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_dry_run.py +39 -0
  22. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_e2e_fixtures.py +5 -1
  23. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_graph.py +22 -2
  24. agentic_dev_loop-1.0.0a10/tests/test_hostboundary.py +136 -0
  25. agentic_dev_loop-1.0.0a10/tests/test_hostboundary_integration.py +91 -0
  26. agentic_dev_loop-1.0.0a10/tests/test_registry.py +243 -0
  27. agentic_dev_loop-1.0.0a10/tests/test_secrets.py +221 -0
  28. agentic_dev_loop-1.0.0a10/tests/test_secrets_integration.py +67 -0
  29. agentic_dev_loop-1.0.0a10/tests/test_secrets_pipeline.py +106 -0
  30. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_stages.py +50 -1
  31. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_viz.py +2 -1
  32. agentic_dev_loop-1.0.0a8/src/agentic_dev_loop/engine/registry.py +0 -133
  33. agentic_dev_loop-1.0.0a8/tests/test_registry.py +0 -114
  34. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/LICENSE +0 -0
  35. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/setup.cfg +0 -0
  36. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/__init__.py +0 -0
  37. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/__init__.py +0 -0
  38. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/results.py +0 -0
  39. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/verdict.py +0 -0
  40. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/__init__.py +0 -0
  41. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  42. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  43. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/llm.py +0 -0
  44. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/loaders.py +0 -0
  45. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/proc.py +0 -0
  46. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/py.typed +0 -0
  47. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/__init__.py +0 -0
  48. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/__main__.py +0 -0
  49. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/server.py +0 -0
  50. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  51. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  52. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  53. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  54. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_agents.py +0 -0
  55. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_aidlc.py +0 -0
  56. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_isolation.py +0 -0
  57. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_isolation_integration.py +0 -0
  58. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_isolation_pbt.py +0 -0
  59. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_llm.py +0 -0
  60. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_loaders.py +0 -0
  61. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_module_contract.py +0 -0
  62. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_outputs.py +0 -0
  63. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_outputs_pbt.py +0 -0
  64. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_proc.py +0 -0
  65. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_publish.py +0 -0
  66. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_results.py +0 -0
  67. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_sdk_compat.py +0 -0
  68. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/tests/test_source_resolution.py +0 -0
  69. {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a10}/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.0a8
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,12 +205,53 @@ 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). |
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). |
195
213
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
196
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. |
216
+
217
+ ### Secrets (`secrets:`) — refs only, never values
218
+
219
+ Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
220
+ Declare the refs at the top level and opt each step into the refs it needs:
221
+
222
+ ```yaml
223
+ secrets: # refs only — never values
224
+ kinde-client-secret: # ref name (arbitrary)
225
+ from: env # provider name
226
+ key: KINDIE_CLIENT_SECRET # provider-specific locator
227
+ r2-keys:
228
+ from: env
229
+ key: YOSK_R2_KEYS
230
+
231
+ steps:
232
+ - name: qa
233
+ agent: qa
234
+ secrets: [kinde-client-secret, r2-keys]
235
+ ```
236
+
237
+ - The engine resolves every declared ref **host-side at run start** (the `env`
238
+ provider reads the named host environment variable). A missing variable, an
239
+ unknown provider, or a ref a step lists without declaring it fails fast —
240
+ naming the ref, the step, and the provider error. No silent fallback.
241
+ - A resolved secret is injected into a step's exec env **only when that step
242
+ lists the ref** — the same path step `env:` uses, no new channel. The secret
243
+ never reaches the provision container, `state_dir`, or any other step.
244
+ - Prefer a mechanical `command` step for secret-consuming work (a script runs
245
+ the app with creds) so the LLM and the secret never meet; an `agent` step
246
+ holds a secret only when its job genuinely requires it.
247
+ - **`.env*` ban.** At run start the workspace root is scanned for `.env*`
248
+ files. Any hit fails fast, naming the files, with the remedy text. The
249
+ convention fixtures `.env.example`, `.env.template`, and `.env.sample` are
250
+ whitelisted. This applies regardless of `network:` flags.
251
+ - **Output scrubbing.** Resolved secret values are scrubbed from every
252
+ engine-written output — captured step output, retry notes, artifacts, and
253
+ `state_dir` files. (Hygiene: the declaring step's agent has already seen
254
+ the value in its own env.)
197
255
 
198
256
  ### Per-stage networking
199
257
 
@@ -213,29 +271,79 @@ steps:
213
271
  sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
214
272
  on the Docker bridge network — the default network-none container is always
215
273
  created too, so no infrastructure work happens mid-run.
216
- - **Command and agent steps** may declare it. Action steps are host-side and
217
- 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).
218
276
  - **Author opt-in is the control.** A networked step gives its agent raw
219
277
  egress for that stage; the default stays network-off everywhere. Reserve it
220
278
  for stages that need it (QA, integration). The residual risk is documented
221
279
  in [docs/setup.md](docs/setup.md).
222
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
+
223
330
  ## Development environment
224
331
 
225
332
  ```bash
226
333
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
227
334
  cd agentic-dev-loop
228
- ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
229
- # create .env with your LLM key (loaded by trigger-agentic-dev-loop.sh, never committed):
230
- # echo 'OPENAI_API_KEY=...' > .env
231
- python -m pytest tests/ -q # fast suite
335
+ pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
336
+ export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
337
+ python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
232
338
  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
339
+ agentic-dev-loop agentic-dev-loop/pipeline.yaml --dry-run # run the shipped pipeline
234
340
  ./run-demo.sh # viz demo — scripted run replay, no pipeline needed
235
341
  ```
236
342
 
237
- - `setup-venv.sh` is safe to re-run and picks the right interpreter even on
238
- 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.
239
347
  - Docker-marked integration tests need a local daemon; CI runs them on Linux.
240
348
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
241
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,12 +166,53 @@ 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). |
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). |
156
174
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
157
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
+
178
+ ### Secrets (`secrets:`) — refs only, never values
179
+
180
+ Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
181
+ Declare the refs at the top level and opt each step into the refs it needs:
182
+
183
+ ```yaml
184
+ secrets: # refs only — never values
185
+ kinde-client-secret: # ref name (arbitrary)
186
+ from: env # provider name
187
+ key: KINDIE_CLIENT_SECRET # provider-specific locator
188
+ r2-keys:
189
+ from: env
190
+ key: YOSK_R2_KEYS
191
+
192
+ steps:
193
+ - name: qa
194
+ agent: qa
195
+ secrets: [kinde-client-secret, r2-keys]
196
+ ```
197
+
198
+ - The engine resolves every declared ref **host-side at run start** (the `env`
199
+ provider reads the named host environment variable). A missing variable, an
200
+ unknown provider, or a ref a step lists without declaring it fails fast —
201
+ naming the ref, the step, and the provider error. No silent fallback.
202
+ - A resolved secret is injected into a step's exec env **only when that step
203
+ lists the ref** — the same path step `env:` uses, no new channel. The secret
204
+ never reaches the provision container, `state_dir`, or any other step.
205
+ - Prefer a mechanical `command` step for secret-consuming work (a script runs
206
+ the app with creds) so the LLM and the secret never meet; an `agent` step
207
+ holds a secret only when its job genuinely requires it.
208
+ - **`.env*` ban.** At run start the workspace root is scanned for `.env*`
209
+ files. Any hit fails fast, naming the files, with the remedy text. The
210
+ convention fixtures `.env.example`, `.env.template`, and `.env.sample` are
211
+ whitelisted. This applies regardless of `network:` flags.
212
+ - **Output scrubbing.** Resolved secret values are scrubbed from every
213
+ engine-written output — captured step output, retry notes, artifacts, and
214
+ `state_dir` files. (Hygiene: the declaring step's agent has already seen
215
+ the value in its own env.)
158
216
 
159
217
  ### Per-stage networking
160
218
 
@@ -174,29 +232,79 @@ steps:
174
232
  sandbox container (`--read-only`, `--cap-drop ALL`, single workspace mount)
175
233
  on the Docker bridge network — the default network-none container is always
176
234
  created too, so no infrastructure work happens mid-run.
177
- - **Command and agent steps** may declare it. Action steps are host-side and
178
- 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).
179
237
  - **Author opt-in is the control.** A networked step gives its agent raw
180
238
  egress for that stage; the default stays network-off everywhere. Reserve it
181
239
  for stages that need it (QA, integration). The residual risk is documented
182
240
  in [docs/setup.md](docs/setup.md).
183
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
+
184
291
  ## Development environment
185
292
 
186
293
  ```bash
187
294
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
188
295
  cd agentic-dev-loop
189
- ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
190
- # create .env with your LLM key (loaded by trigger-agentic-dev-loop.sh, never committed):
191
- # echo 'OPENAI_API_KEY=...' > .env
192
- python -m pytest tests/ -q # fast suite
296
+ pipx install "agentic-dev-loop==1.0.0a9" # engine venv OUTSIDE the workspace (self-check)
297
+ export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
298
+ python -m pytest tests/ -q # fast suite (in-repo .venv is for tests only)
193
299
  python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
194
- ./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
195
301
  ./run-demo.sh # viz demo — scripted run replay, no pipeline needed
196
302
  ```
197
303
 
198
- - `setup-venv.sh` is safe to re-run and picks the right interpreter even on
199
- 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.
200
308
  - Docker-marked integration tests need a local daemon; CI runs them on Linux.
201
309
  - The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
202
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.0a8"
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"
@@ -11,6 +11,7 @@ import yaml
11
11
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
12
12
 
13
13
  TERMINALS = frozenset({"success", "failure"})
14
+ KNOWN_SECRET_PROVIDERS = frozenset({"env"})
14
15
 
15
16
 
16
17
  class VerdictSpec(BaseModel):
@@ -36,6 +37,34 @@ def route_target(route: RouteTarget | None) -> str | None:
36
37
  return route.goto if isinstance(route, Route) else route
37
38
 
38
39
 
40
+ class SecretSpec(BaseModel):
41
+ """One ``secrets:`` ref — the locator only, never a value. ``from`` names
42
+ the provider (``env`` first); ``key`` is that provider's locator (for
43
+ ``env``, the host environment variable to read and the step env name to
44
+ inject). Unknown keys fail at load (extra="forbid")."""
45
+
46
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
47
+
48
+ from_: str = Field(alias="from")
49
+ key: str
50
+
51
+ @field_validator("from_")
52
+ @classmethod
53
+ def _known_provider(cls, v: str) -> str:
54
+ if v not in KNOWN_SECRET_PROVIDERS:
55
+ raise ValueError(
56
+ f"unknown secret provider {v!r} (known: "
57
+ f"{sorted(KNOWN_SECRET_PROVIDERS)})")
58
+ return v
59
+
60
+ @field_validator("key")
61
+ @classmethod
62
+ def _nonempty_key(cls, v: str) -> str:
63
+ if not v.strip():
64
+ raise ValueError("secret key must be a non-empty string")
65
+ return v
66
+
67
+
39
68
  class IsolationConfig(BaseModel):
40
69
  """Optional `isolation:` section — configures the always-on sandbox (BR-2).
41
70
  Absence means opinionated defaults, never unsandboxed behaviour. Unknown
@@ -85,7 +114,8 @@ class StepConfig(BaseModel):
85
114
  timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
86
115
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
87
116
  setup: str | None = None # BR-13: command steps only
88
- 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
+ secrets: list[str] = [] # declared ref names; action steps reject
89
119
  on_pass: RouteTarget | None = None
90
120
  on_fail: RouteTarget | None = None
91
121
 
@@ -143,9 +173,14 @@ class StepConfig(BaseModel):
143
173
  raise ValueError(
144
174
  f"step {self.name!r}: 'setup' is only allowed on 'command' steps"
145
175
  )
146
- if self.network and self.action is not None:
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
+ if self.secrets and self.action is not None:
147
182
  raise ValueError(
148
- f"step {self.name!r}: 'network' is only allowed on 'agent' or "
183
+ f"step {self.name!r}: 'secrets' is only allowed on 'agent' or "
149
184
  "'command' steps"
150
185
  )
151
186
  return self
@@ -166,14 +201,37 @@ class PipelineConfig(BaseModel):
166
201
  name: str
167
202
  extensions: list[str] = []
168
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"
169
209
  env_allowlist: list[str] = []
170
210
  llm: LlmConfig
171
211
  isolation: IsolationConfig | None = None # None = always-on defaults
172
212
  state_dir: str = ".pr" # default relocated to ~/.agentic-dev-loop/runs/<hash>/ at load
213
+ secrets: dict[str, SecretSpec] = {} # ref name -> locator; values never appear here
173
214
  max_attempts: int = 3
174
215
  agents_dir: str
175
216
  steps: list[StepConfig] = Field(min_length=1)
176
217
 
218
+ @field_validator("secrets", mode="before")
219
+ @classmethod
220
+ def _reject_value_shaped_refs(cls, v: Any) -> Any:
221
+ if v is None:
222
+ return {}
223
+ if not isinstance(v, dict):
224
+ raise ValueError(
225
+ "'secrets' must be a mapping of ref name to "
226
+ "{from: ..., key: ...}")
227
+ for name, spec in v.items():
228
+ if not isinstance(spec, dict):
229
+ raise ValueError(
230
+ f"secret ref {name!r} must be a mapping "
231
+ "{from: ..., key: ...}, not a literal value — secret "
232
+ "values never belong in the pipeline yaml")
233
+ return v
234
+
177
235
  @model_validator(mode="after")
178
236
  def _check_graph(self) -> "PipelineConfig":
179
237
  names = [s.name for s in self.steps]
@@ -191,6 +249,12 @@ class PipelineConfig(BaseModel):
191
249
  raise ValueError(
192
250
  f"step {step.name!r}: unknown route target {target!r}"
193
251
  )
252
+ for step in self.steps:
253
+ for ref in step.secrets:
254
+ if ref not in self.secrets:
255
+ raise ValueError(
256
+ f"step {step.name!r}: secret ref {ref!r} is not "
257
+ "declared in the top-level 'secrets:' section")
194
258
  return self
195
259
 
196
260
 
@@ -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
@@ -110,13 +110,17 @@ class SandboxTerminalExecutor(ToolExecutor):
110
110
  def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None] | None = None,
111
111
  workspace: str = "", pipeline: str | None = None,
112
112
  step: str | None = None,
113
- network: bool = False) -> None:
113
+ network: bool = False,
114
+ secrets: dict[str, str] | None = None) -> None:
114
115
  """Per-run re-registration of the `terminal` name (KD-4): a callable
115
116
  factory that builds the SDK terminal with a `SandboxTerminalExecutor`.
116
- Pipelines are sequential, so re-registering per agent step is safe."""
117
+ Pipelines are sequential, so re-registering per agent step is safe.
118
+
119
+ ``secrets`` (019) is the per-step resolved secret env, injected into the
120
+ terminal's exec env only for steps that declared the refs."""
117
121
  from openhands.sdk.tool import register_tool
118
122
 
119
- resolved_env = resolve_sandbox_env(env)
123
+ resolved_env = resolve_sandbox_env(env, secrets=secrets)
120
124
 
121
125
  def factory(conv_state, **params):
122
126
  return TerminalTool.create(
@@ -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,13 @@ 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)
22
+ from .secrets import scan_env_files
20
23
 
21
24
 
22
25
  def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
@@ -31,12 +34,18 @@ def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
31
34
 
32
35
  def _validate_actions(cfg: PipelineConfig, registry: Registry) -> None:
33
36
  """Unknown action names fail at graph build time; catch them here so a
34
- 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)
35
41
  for step in cfg.steps:
36
- 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:
37
45
  raise ValueError(
38
46
  f"step {step.name!r}: unknown action {step.action!r} "
39
- f"(registered: {sorted(registry.actions)})")
47
+ f"(registered: {sorted(known)})")
48
+ validate_action_network(step.name, step.action, step.network, registry)
40
49
 
41
50
 
42
51
  def _resolved_image(cfg: PipelineConfig) -> tuple[str, str, float]:
@@ -113,10 +122,15 @@ def dry_run(cfg_path, args, registry: Registry | None = None) -> int:
113
122
  cfg_path = Path(cfg_path)
114
123
  cfg = load_config(cfg_path)
115
124
  base = cfg_path.resolve().parent
125
+ check_orchestrator_install(Path.cwd())
126
+ scan_env_files(Path.cwd())
127
+ check_tooling_integrity(cfg, cfg_path, base, Path.cwd())
116
128
 
117
129
  if registry is None:
118
130
  registry = default_registry()
119
- load_extensions(cfg.extensions, registry)
131
+ load_extensions(cfg.extensions, registry,
132
+ extensions_dir=cfg.extensions_dir,
133
+ workspace=Path.cwd())
120
134
 
121
135
  _validate_agents(cfg, base / cfg.agents_dir)
122
136
  _validate_actions(cfg, registry)