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.
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/PKG-INFO +79 -10
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/README.md +78 -9
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/config.py +11 -6
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/actions.py +24 -3
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/cli.py +2 -1
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/dryrun.py +17 -5
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/graph.py +26 -8
- agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/hostboundary.py +137 -0
- agentic_dev_loop-1.0.0a10/src/agentic_dev_loop/engine/registry.py +291 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/stages.py +75 -6
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/PKG-INFO +79 -10
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/SOURCES.txt +3 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_actions.py +39 -1
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_cli.py +2 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_config.py +6 -5
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_dry_run.py +39 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_graph.py +22 -2
- agentic_dev_loop-1.0.0a10/tests/test_hostboundary.py +136 -0
- agentic_dev_loop-1.0.0a10/tests/test_hostboundary_integration.py +91 -0
- agentic_dev_loop-1.0.0a10/tests/test_registry.py +243 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_stages.py +50 -1
- agentic_dev_loop-1.0.0a9/src/agentic_dev_loop/engine/registry.py +0 -133
- agentic_dev_loop-1.0.0a9/tests/test_registry.py +0 -114
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/tests/test_verdict.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
257
|
-
never
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
277
|
-
|
|
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.
|
|
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
|
|
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
|
|
218
|
-
never
|
|
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
|
-
|
|
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
|
-
|
|
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`
|
|
238
|
-
|
|
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.
|
|
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 +
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
{agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
@@ -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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
{agentic_dev_loop-1.0.0a9 → agentic_dev_loop-1.0.0a10}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
#
|
|
112
|
-
#
|
|
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")
|