agentic-dev-loop 1.0.0a31__tar.gz → 1.0.0a34__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.0a31/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a34}/PKG-INFO +20 -5
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/README.md +19 -4
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/core/config.py +37 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/core/paths.py +22 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/agents.py +22 -5
- agentic_dev_loop-1.0.0a34/src/agentic_dev_loop/engine/allowlist.py +66 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/extensions/aidlc.py +6 -0
- agentic_dev_loop-1.0.0a34/src/agentic_dev_loop/engine/forbidden.py +91 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/graph.py +4 -1
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/isolation.py +19 -3
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/stages.py +130 -39
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/sync_back.py +24 -4
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34/src/agentic_dev_loop.egg-info}/PKG-INFO +20 -5
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop.egg-info/SOURCES.txt +4 -0
- agentic_dev_loop-1.0.0a34/tests/test_035_finalize_nudge.py +211 -0
- agentic_dev_loop-1.0.0a34/tests/test_036_banned_commands_and_lane_boundary.py +273 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_e2e_fixtures.py +4 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_032_stop_when_green.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_033_brief_step.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a31/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a34}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a34
|
|
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
|
|
@@ -224,6 +224,8 @@ steps:
|
|
|
224
224
|
agent: qa
|
|
225
225
|
artifact: qa-report.md
|
|
226
226
|
verdicts: { pass: PASS, fail: FAIL }
|
|
227
|
+
max_iterations: 100
|
|
228
|
+
finalize_nudge_turns: 10
|
|
227
229
|
on_pass: commit
|
|
228
230
|
on_fail: { goto: dev, retry: true }
|
|
229
231
|
|
|
@@ -239,6 +241,8 @@ steps:
|
|
|
239
241
|
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
240
242
|
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
241
243
|
| `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
|
|
244
|
+
| `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
|
|
245
|
+
| `custom_prompt:` | Planned (planning-docs/039). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
|
|
242
246
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
243
247
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
244
248
|
| `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
|
|
@@ -338,6 +342,16 @@ steps:
|
|
|
338
342
|
to exactly the declared hosts for that stage; the default stays network-off
|
|
339
343
|
everywhere. Reserve it for stages that need it (QA, integration). The
|
|
340
344
|
residual risks are documented in [docs/setup.md](docs/setup.md).
|
|
345
|
+
- **Host-service reachability (planned - planning-docs/037, 038).** A host
|
|
346
|
+
service reached at `host.docker.internal` through the proxy fails today:
|
|
347
|
+
the policy admits it, but the proxy dials from the host, where that
|
|
348
|
+
container-only name does not resolve (a 502 for the client). 037 rewrites
|
|
349
|
+
`host.docker.internal` to host loopback at the proxy dial site, so signed
|
|
350
|
+
host-service traffic (an S3 SDK against a local MinIO, say) reaches the
|
|
351
|
+
host through the enforced, logged path. 038 then replaces the
|
|
352
|
+
host-gateway model with a per-run internal network: sandboxes get no host
|
|
353
|
+
route, and a containerized proxy on a second host-access network is the
|
|
354
|
+
only egress path.
|
|
341
355
|
|
|
342
356
|
### Sync-back conflicts (host edits during a run)
|
|
343
357
|
|
|
@@ -421,9 +435,10 @@ an agent burned budget learning it mid-run:
|
|
|
421
435
|
the dev agent's next context; the dev prompt makes every listed issue a
|
|
422
436
|
required fix.
|
|
423
437
|
- **No git mutations, ever.** The `commit` step is the single commit point.
|
|
424
|
-
- **`max_iterations` per role.** qa is bounded probing (
|
|
425
|
-
|
|
426
|
-
|
|
438
|
+
- **`max_iterations` per role.** qa is bounded probing (100 in this
|
|
439
|
+
repo's dogfood loop, with `finalize_nudge_turns: 10` so it never ends
|
|
440
|
+
silently at the cap - both provisional); dev is open-ended construction
|
|
441
|
+
and stays uncapped (the engine default).
|
|
427
442
|
|
|
428
443
|
### The pipeline file
|
|
429
444
|
|
|
@@ -438,7 +453,7 @@ adapt per repo.
|
|
|
438
453
|
| editor-tests | command | network + `presets: [package-registries]` | frontend suites that cannot run offline (`npm ci && npm test`). A separate step, never merged into `test`: the step name is the failure diagnosis, its timeout does not contend the backend suite's, and a retry re-runs only its own chain |
|
|
439
454
|
| conformance | command | network + `presets: [package-registries]` | repo-specific fitness checks (the consumer repo: jscpd + DDD rules) |
|
|
440
455
|
| review | agent | ro, offline | static conformance verdict |
|
|
441
|
-
| qa | agent | per repo (see table above) | live-behaviour probes; `max_iterations:
|
|
456
|
+
| qa | agent | per repo (see table above) | live-behaviour probes; `max_iterations: 100` + `finalize_nudge_turns: 10` (provisional); inject secrets only if the probes need them |
|
|
442
457
|
| commit | action | host-side | the single commit point |
|
|
443
458
|
|
|
444
459
|
Adaptation checklist for a new repo:
|
|
@@ -182,6 +182,8 @@ steps:
|
|
|
182
182
|
agent: qa
|
|
183
183
|
artifact: qa-report.md
|
|
184
184
|
verdicts: { pass: PASS, fail: FAIL }
|
|
185
|
+
max_iterations: 100
|
|
186
|
+
finalize_nudge_turns: 10
|
|
185
187
|
on_pass: commit
|
|
186
188
|
on_fail: { goto: dev, retry: true }
|
|
187
189
|
|
|
@@ -197,6 +199,8 @@ steps:
|
|
|
197
199
|
| `brief_inputs:` | Optional agent-step input spec: `{ include: [<paths/globs>], env_facts: [<strings>] }`. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as that step's task message, and regenerates the step only when those inputs change. A produces-only agent step such as `brief` is the documented pattern for per-run context digestion. |
|
|
198
200
|
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
199
201
|
| `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
|
|
202
|
+
| `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
|
|
203
|
+
| `custom_prompt:` | Planned (planning-docs/039). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
|
|
200
204
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
201
205
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
202
206
|
| `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
|
|
@@ -296,6 +300,16 @@ steps:
|
|
|
296
300
|
to exactly the declared hosts for that stage; the default stays network-off
|
|
297
301
|
everywhere. Reserve it for stages that need it (QA, integration). The
|
|
298
302
|
residual risks are documented in [docs/setup.md](docs/setup.md).
|
|
303
|
+
- **Host-service reachability (planned - planning-docs/037, 038).** A host
|
|
304
|
+
service reached at `host.docker.internal` through the proxy fails today:
|
|
305
|
+
the policy admits it, but the proxy dials from the host, where that
|
|
306
|
+
container-only name does not resolve (a 502 for the client). 037 rewrites
|
|
307
|
+
`host.docker.internal` to host loopback at the proxy dial site, so signed
|
|
308
|
+
host-service traffic (an S3 SDK against a local MinIO, say) reaches the
|
|
309
|
+
host through the enforced, logged path. 038 then replaces the
|
|
310
|
+
host-gateway model with a per-run internal network: sandboxes get no host
|
|
311
|
+
route, and a containerized proxy on a second host-access network is the
|
|
312
|
+
only egress path.
|
|
299
313
|
|
|
300
314
|
### Sync-back conflicts (host edits during a run)
|
|
301
315
|
|
|
@@ -379,9 +393,10 @@ an agent burned budget learning it mid-run:
|
|
|
379
393
|
the dev agent's next context; the dev prompt makes every listed issue a
|
|
380
394
|
required fix.
|
|
381
395
|
- **No git mutations, ever.** The `commit` step is the single commit point.
|
|
382
|
-
- **`max_iterations` per role.** qa is bounded probing (
|
|
383
|
-
|
|
384
|
-
|
|
396
|
+
- **`max_iterations` per role.** qa is bounded probing (100 in this
|
|
397
|
+
repo's dogfood loop, with `finalize_nudge_turns: 10` so it never ends
|
|
398
|
+
silently at the cap - both provisional); dev is open-ended construction
|
|
399
|
+
and stays uncapped (the engine default).
|
|
385
400
|
|
|
386
401
|
### The pipeline file
|
|
387
402
|
|
|
@@ -396,7 +411,7 @@ adapt per repo.
|
|
|
396
411
|
| editor-tests | command | network + `presets: [package-registries]` | frontend suites that cannot run offline (`npm ci && npm test`). A separate step, never merged into `test`: the step name is the failure diagnosis, its timeout does not contend the backend suite's, and a retry re-runs only its own chain |
|
|
397
412
|
| conformance | command | network + `presets: [package-registries]` | repo-specific fitness checks (the consumer repo: jscpd + DDD rules) |
|
|
398
413
|
| review | agent | ro, offline | static conformance verdict |
|
|
399
|
-
| qa | agent | per repo (see table above) | live-behaviour probes; `max_iterations:
|
|
414
|
+
| qa | agent | per repo (see table above) | live-behaviour probes; `max_iterations: 100` + `finalize_nudge_turns: 10` (provisional); inject secrets only if the probes need them |
|
|
400
415
|
| commit | action | host-side | the single commit point |
|
|
401
416
|
|
|
402
417
|
Adaptation checklist for a new repo:
|
|
@@ -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.0a34"
|
|
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"
|
|
@@ -387,9 +387,11 @@ class StepConfig(BaseModel):
|
|
|
387
387
|
timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
|
|
388
388
|
terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
|
|
389
389
|
stop_when: StopWhenSpec | None = None # agent steps only (032)
|
|
390
|
+
finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
|
|
390
391
|
brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
|
|
391
392
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
392
393
|
setup: str | None = None # BR-13: command steps only
|
|
394
|
+
forbidden_commands: list[str] = [] # banned-command regex deny-list (036 D1)
|
|
393
395
|
network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
|
|
394
396
|
# unit-5: per-step mount granularity + egress policy. Empty mounts =
|
|
395
397
|
# strict-by-default (BR-1.1); egress None = strict empty allowlist
|
|
@@ -419,6 +421,26 @@ class StepConfig(BaseModel):
|
|
|
419
421
|
def _coerce_produces(cls, v: Any) -> Any:
|
|
420
422
|
return [v] if isinstance(v, str) else v
|
|
421
423
|
|
|
424
|
+
@field_validator("forbidden_commands", mode="before")
|
|
425
|
+
@classmethod
|
|
426
|
+
def _coerce_forbidden_commands(cls, v: Any) -> Any:
|
|
427
|
+
return [v] if isinstance(v, str) else v
|
|
428
|
+
|
|
429
|
+
@field_validator("forbidden_commands")
|
|
430
|
+
@classmethod
|
|
431
|
+
def _valid_forbidden_commands(cls, v: list[str]) -> list[str]:
|
|
432
|
+
for pattern in v:
|
|
433
|
+
if not isinstance(pattern, str) or not pattern.strip():
|
|
434
|
+
raise ValueError(
|
|
435
|
+
"forbidden_commands entries must be non-empty strings")
|
|
436
|
+
try:
|
|
437
|
+
re.compile(pattern)
|
|
438
|
+
except re.error as e:
|
|
439
|
+
raise ValueError(
|
|
440
|
+
f"forbidden_commands entry {pattern!r} is not a valid "
|
|
441
|
+
f"regex: {e}") from e
|
|
442
|
+
return v
|
|
443
|
+
|
|
422
444
|
@model_validator(mode="after")
|
|
423
445
|
def _check_shape(self) -> "StepConfig":
|
|
424
446
|
for key in ("agent", "action", "command"):
|
|
@@ -460,6 +482,21 @@ class StepConfig(BaseModel):
|
|
|
460
482
|
raise ValueError(
|
|
461
483
|
f"step {self.name!r}: 'stop_when' requires 'agent'"
|
|
462
484
|
)
|
|
485
|
+
if self.finalize_nudge_turns is not None and self.agent is None:
|
|
486
|
+
raise ValueError(
|
|
487
|
+
f"step {self.name!r}: 'finalize_nudge_turns' requires 'agent'"
|
|
488
|
+
)
|
|
489
|
+
if self.finalize_nudge_turns is not None and self.max_iterations is None:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"step {self.name!r}: 'finalize_nudge_turns' requires "
|
|
492
|
+
"'max_iterations'"
|
|
493
|
+
)
|
|
494
|
+
if (self.finalize_nudge_turns is not None
|
|
495
|
+
and self.finalize_nudge_turns >= self.max_iterations):
|
|
496
|
+
raise ValueError(
|
|
497
|
+
f"step {self.name!r}: 'finalize_nudge_turns' must be less "
|
|
498
|
+
"than 'max_iterations'"
|
|
499
|
+
)
|
|
463
500
|
if self.brief_inputs is not None and self.agent is None:
|
|
464
501
|
raise ValueError(
|
|
465
502
|
f"step {self.name!r}: 'brief_inputs' requires 'agent'"
|
|
@@ -67,3 +67,25 @@ def dir_excluded(dir_rel: str, excludes: tuple[str, ...] | list[str]) -> bool:
|
|
|
67
67
|
if parts[:len(spec_parts)] == spec_parts:
|
|
68
68
|
return True
|
|
69
69
|
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def path_allowed(rel: str, allowlist: tuple[str, ...] | list[str]) -> bool:
|
|
73
|
+
"""True when a repo-relative posix path matches an allowed path/glob.
|
|
74
|
+
|
|
75
|
+
Mirrors ``path_excluded`` semantics: literal entries match by component
|
|
76
|
+
prefix (the dir and everything under it); wildcard entries match by
|
|
77
|
+
``fnmatch.fnmatchcase`` on the full posix relpath. A ``.`` (or blank)
|
|
78
|
+
entry allows the whole tree.
|
|
79
|
+
"""
|
|
80
|
+
parts = relative_parts(rel)
|
|
81
|
+
for spec in allowlist:
|
|
82
|
+
if _has_wildcards(spec):
|
|
83
|
+
if fnmatch.fnmatchcase(rel, spec):
|
|
84
|
+
return True
|
|
85
|
+
else:
|
|
86
|
+
spec_parts = relative_parts(spec)
|
|
87
|
+
if not spec_parts:
|
|
88
|
+
return True
|
|
89
|
+
if parts[:len(spec_parts)] == spec_parts:
|
|
90
|
+
return True
|
|
91
|
+
return False
|
{agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
@@ -15,6 +15,7 @@ from openhands.tools.file_editor import FileEditorTool # noqa: F401 - registers
|
|
|
15
15
|
from openhands.tools.task_tracker import TaskTrackerTool # noqa: F401 - registers "task_tracker"
|
|
16
16
|
from openhands.tools.terminal import TerminalTool # noqa: F401 - registers "terminal"
|
|
17
17
|
|
|
18
|
+
from .forbidden import ForbiddenRule, match_forbidden_command
|
|
18
19
|
from .isolation import (SandboxError, SandboxExecutionRequest, command_argv,
|
|
19
20
|
resolve_sandbox_env)
|
|
20
21
|
|
|
@@ -68,7 +69,9 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
68
69
|
pipeline: str | None = None, step: str | None = None,
|
|
69
70
|
network: bool = False, mount_set=None, egress_policy=None,
|
|
70
71
|
terminal_timeout: float | None = None,
|
|
71
|
-
on_terminal_result=None
|
|
72
|
+
on_terminal_result=None,
|
|
73
|
+
forbidden_commands: tuple[ForbiddenRule, ...] | None = None,
|
|
74
|
+
allowlist: tuple[str, ...] | None = None):
|
|
72
75
|
self._sandbox = sandbox
|
|
73
76
|
self._workspace = workspace
|
|
74
77
|
self._env = env or {}
|
|
@@ -79,6 +82,8 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
79
82
|
self._egress_policy = egress_policy
|
|
80
83
|
self._terminal_timeout = terminal_timeout
|
|
81
84
|
self._on_terminal_result = on_terminal_result
|
|
85
|
+
self._forbidden_commands = forbidden_commands
|
|
86
|
+
self._allowlist = allowlist
|
|
82
87
|
|
|
83
88
|
def __call__(self, action, conversation=None):
|
|
84
89
|
from openhands.tools.terminal import TerminalObservation
|
|
@@ -99,11 +104,19 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
99
104
|
timeout = action.timeout
|
|
100
105
|
if timeout is None and self._terminal_timeout is not None:
|
|
101
106
|
timeout = self._terminal_timeout
|
|
107
|
+
argv = command_argv(action.command)
|
|
108
|
+
forbidden = match_forbidden_command(argv, self._forbidden_commands)
|
|
109
|
+
if forbidden is not None:
|
|
110
|
+
return TerminalObservation.from_text(
|
|
111
|
+
f"[forbidden: {forbidden}]", is_error=True,
|
|
112
|
+
command=action.command, exit_code=None, timeout=False,
|
|
113
|
+
metadata=CmdOutputMetadata(exit_code=-1))
|
|
102
114
|
request = SandboxExecutionRequest(
|
|
103
|
-
argv=
|
|
115
|
+
argv=argv, cwd=self._workspace,
|
|
104
116
|
env=self._env, timeout=timeout,
|
|
105
117
|
pipeline=self._pipeline, step=self._step, network=self._network,
|
|
106
|
-
mount_set=self._mount_set, egress_policy=self._egress_policy
|
|
118
|
+
mount_set=self._mount_set, egress_policy=self._egress_policy,
|
|
119
|
+
allowlist=self._allowlist)
|
|
107
120
|
try:
|
|
108
121
|
result = self._sandbox.run(request)
|
|
109
122
|
except SandboxError as exc:
|
|
@@ -142,7 +155,9 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
142
155
|
mount_set=None, egress_policy=None,
|
|
143
156
|
terminal_timeout: float | None = None,
|
|
144
157
|
on_terminal_result=None,
|
|
145
|
-
secrets: dict[str, str] | None = None
|
|
158
|
+
secrets: dict[str, str] | None = None,
|
|
159
|
+
forbidden_commands: tuple[ForbiddenRule, ...] | None = None,
|
|
160
|
+
allowlist: tuple[str, ...] | None = None) -> None:
|
|
146
161
|
"""Per-run re-registration of the `terminal` name (KD-4): a callable
|
|
147
162
|
factory that builds the SDK terminal with a `SandboxTerminalExecutor`.
|
|
148
163
|
Pipelines are sequential, so re-registering per agent step is safe.
|
|
@@ -161,6 +176,8 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
161
176
|
pipeline=pipeline, step=step, network=network,
|
|
162
177
|
mount_set=mount_set, egress_policy=egress_policy,
|
|
163
178
|
terminal_timeout=terminal_timeout,
|
|
164
|
-
on_terminal_result=on_terminal_result
|
|
179
|
+
on_terminal_result=on_terminal_result,
|
|
180
|
+
forbidden_commands=forbidden_commands,
|
|
181
|
+
allowlist=allowlist))
|
|
165
182
|
|
|
166
183
|
register_tool("terminal", factory)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Engine-lane allowlist (036 D2 v1).
|
|
2
|
+
|
|
3
|
+
The active AI-DLC unit directory may carry ``engine-allowlist.yaml`` - a YAML
|
|
4
|
+
list of repo-relative allowed paths/globs. Absent = unrestricted (backward
|
|
5
|
+
compatible). Present = sync-back asserts every merged/synced path matches an
|
|
6
|
+
entry and fails the step otherwise. v2 (read-only export root with rw
|
|
7
|
+
bind-mounts for the allowlisted paths) is deferred.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from agentic_dev_loop.core.config import _validate_declared_path
|
|
17
|
+
from .loaders import select_loader
|
|
18
|
+
|
|
19
|
+
ALLOWLIST_FILENAME = "engine-allowlist.yaml"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_engine_allowlist(path: str | Path) -> list[str]:
|
|
23
|
+
"""Parse and validate an ``engine-allowlist.yaml`` file.
|
|
24
|
+
|
|
25
|
+
The file is a plain YAML list of repo-relative path/glob strings (the
|
|
26
|
+
same declared-path vocabulary as ``sync_excludes:``). An empty file is a
|
|
27
|
+
valid, deliberately empty allowlist (nothing matches, so every synced
|
|
28
|
+
path violates the lane). Missing files are handled by the caller as the
|
|
29
|
+
unrestricted case, not here.
|
|
30
|
+
"""
|
|
31
|
+
path = Path(path)
|
|
32
|
+
if not path.is_file():
|
|
33
|
+
raise FileNotFoundError(f"engine allowlist file not found: {path}")
|
|
34
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
35
|
+
if data is None:
|
|
36
|
+
return []
|
|
37
|
+
if not isinstance(data, list):
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"{ALLOWLIST_FILENAME} must be a YAML list of paths/globs, "
|
|
40
|
+
f"got {type(data).__name__}")
|
|
41
|
+
return [_validate_declared_path(entry, allow_wildcards=True)
|
|
42
|
+
for entry in data]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve_engine_allowlist(registry, args) -> list[str] | None:
|
|
46
|
+
"""Resolve the active unit's allowlist, or None when unrestricted.
|
|
47
|
+
|
|
48
|
+
Only AI-DLC unit loaders expose a unit directory; other loaders (plan,
|
|
49
|
+
none) have no lane boundary. The file lives at
|
|
50
|
+
``<unit_dir>/engine-allowlist.yaml``.
|
|
51
|
+
"""
|
|
52
|
+
loader = select_loader(registry, args)
|
|
53
|
+
if loader is None:
|
|
54
|
+
return None
|
|
55
|
+
unit_dir = getattr(loader, "unit_dir", None)
|
|
56
|
+
if not callable(unit_dir):
|
|
57
|
+
return None
|
|
58
|
+
try:
|
|
59
|
+
path = Path(unit_dir(args)) / ALLOWLIST_FILENAME
|
|
60
|
+
except (FileNotFoundError, RuntimeError):
|
|
61
|
+
# No active AI-DLC unit can be determined (e.g. a resumed run with no
|
|
62
|
+
# aidlc-state file) -> no lane boundary, exactly like an absent file.
|
|
63
|
+
return None
|
|
64
|
+
if not path.is_file():
|
|
65
|
+
return None
|
|
66
|
+
return load_engine_allowlist(path)
|
|
@@ -138,6 +138,12 @@ class AidlcDocumentLoader:
|
|
|
138
138
|
_, sources = resolve_unit_docs_with_sources(self.docs_dir, unit)
|
|
139
139
|
return sources
|
|
140
140
|
|
|
141
|
+
def unit_dir(self, args: argparse.Namespace) -> Path:
|
|
142
|
+
"""The active unit directory (036): where the engine lane boundary
|
|
143
|
+
looks for ``engine-allowlist.yaml``."""
|
|
144
|
+
unit = getattr(args, "aidlc_unit", None) or get_current_unit(self.docs_dir)
|
|
145
|
+
return Path(self.docs_dir) / "construction" / unit
|
|
146
|
+
|
|
141
147
|
|
|
142
148
|
class AidlcExtension:
|
|
143
149
|
name = "ai-dlc"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Banned-command deny-list (036 D1).
|
|
2
|
+
|
|
3
|
+
The sandbox terminal enforces a deny-list before any shell spawn: a command
|
|
4
|
+
whose argv matches a step's ``forbidden_commands:`` regex (merged with the
|
|
5
|
+
engine defaults) returns a structured observation instead of reaching the
|
|
6
|
+
sandbox. Command steps are unaffected - they are declared infrastructure and
|
|
7
|
+
already fail fast on their exit code.
|
|
8
|
+
|
|
9
|
+
The default list carries human-readable labels; custom entries use their
|
|
10
|
+
regex text as the label. ``dotnet restore`` and ``dotnet add`` are network
|
|
11
|
+
gated: denied only while the step's ``network:`` flag is off.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ForbiddenRule:
|
|
22
|
+
label: str
|
|
23
|
+
regex: re.Pattern[str]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DEFAULT_PATTERNS: tuple[tuple[str, str], ...] = (
|
|
27
|
+
("git commit", r"\bgit\s+commit\b"),
|
|
28
|
+
("git push", r"\bgit\s+push\b"),
|
|
29
|
+
("git rebase", r"\bgit\s+rebase\b"),
|
|
30
|
+
("git checkout", r"\bgit\s+checkout\b"),
|
|
31
|
+
("git reset", r"\bgit\s+reset\b"),
|
|
32
|
+
("git tag", r"\bgit\s+tag\b"),
|
|
33
|
+
("npm install", r"\bnpm\s+install\b"),
|
|
34
|
+
("npm ci", r"\bnpm\s+ci\b"),
|
|
35
|
+
("pip install", r"\bpip\s+install\b"),
|
|
36
|
+
("uv add", r"\buv\s+add\b"),
|
|
37
|
+
("pip-compile", r"\bpip-compile\b"),
|
|
38
|
+
("dotnet add package", r"\bdotnet\s+add\s+package\b"),
|
|
39
|
+
("npx playwright", r"\bnpx\s+playwright\b"),
|
|
40
|
+
("playwright", r"\bplaywright\b"),
|
|
41
|
+
("docker", r"\bdocker\b"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
_NETWORK_GATED_PATTERNS: tuple[tuple[str, str], ...] = (
|
|
45
|
+
("dotnet restore", r"\bdotnet\s+restore\b"),
|
|
46
|
+
("dotnet add", r"\bdotnet\s+add\b"),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _compile(label: str, pattern: str) -> ForbiddenRule:
|
|
51
|
+
return ForbiddenRule(label=label, regex=re.compile(pattern, re.IGNORECASE))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
DEFAULT_FORBIDDEN_RULES: tuple[ForbiddenRule, ...] = tuple(
|
|
55
|
+
_compile(label, pattern) for label, pattern in _DEFAULT_PATTERNS)
|
|
56
|
+
|
|
57
|
+
NETWORK_GATED_FORBIDDEN_RULES: tuple[ForbiddenRule, ...] = tuple(
|
|
58
|
+
_compile(label, pattern) for label, pattern in _NETWORK_GATED_PATTERNS)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def resolve_forbidden_rules(custom_patterns: list[str] | tuple[str, ...] | None,
|
|
62
|
+
network: bool) -> tuple[ForbiddenRule, ...]:
|
|
63
|
+
"""Merge the engine defaults with a step's custom regex list.
|
|
64
|
+
|
|
65
|
+
Custom entries are appended after the defaults, so the first match wins
|
|
66
|
+
on overlap. Network-gated defaults are included only while ``network`` is
|
|
67
|
+
off; there is no disable escape hatch for the engine defaults.
|
|
68
|
+
"""
|
|
69
|
+
rules = list(DEFAULT_FORBIDDEN_RULES)
|
|
70
|
+
if not network:
|
|
71
|
+
rules.extend(NETWORK_GATED_FORBIDDEN_RULES)
|
|
72
|
+
for pattern in custom_patterns or ():
|
|
73
|
+
rules.append(_compile(pattern, pattern))
|
|
74
|
+
return tuple(rules)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def match_forbidden_command(argv: list[str],
|
|
78
|
+
rules: tuple[ForbiddenRule, ...] | None) -> str | None:
|
|
79
|
+
"""Return the label of the first matching rule for ``argv``, else None.
|
|
80
|
+
|
|
81
|
+
Match is a regex search over the space-joined argv, so shell-wrapped
|
|
82
|
+
commands (``/bin/sh -c "git commit ..."``) still match. Empty argv never
|
|
83
|
+
matches.
|
|
84
|
+
"""
|
|
85
|
+
if not argv:
|
|
86
|
+
return None
|
|
87
|
+
text = " ".join(argv)
|
|
88
|
+
for rule in rules or ():
|
|
89
|
+
if rule.regex.search(text):
|
|
90
|
+
return rule.label
|
|
91
|
+
return None
|
{agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
@@ -18,6 +18,7 @@ from langgraph.graph import END, START, StateGraph
|
|
|
18
18
|
|
|
19
19
|
from agentic_dev_loop import viz
|
|
20
20
|
from .actions import git_worktree_clean, kill_listeners_on_port
|
|
21
|
+
from .allowlist import resolve_engine_allowlist
|
|
21
22
|
from agentic_dev_loop.core.config import load_config
|
|
22
23
|
from .hostboundary import (check_orchestrator_install,
|
|
23
24
|
check_tooling_integrity, tooling_rel_paths)
|
|
@@ -170,11 +171,13 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
|
|
|
170
171
|
def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
171
172
|
sandbox, resolved_secrets, tooling_paths) -> tuple[bool, int]:
|
|
172
173
|
export_root = getattr(sandbox, "export_root", None)
|
|
174
|
+
engine_allowlist = resolve_engine_allowlist(registry, args)
|
|
173
175
|
runtime = Runtime(cfg=cfg, registry=registry, llm=build_llm(cfg.llm),
|
|
174
176
|
agents_dir=base / cfg.agents_dir, state_dir=state_dir,
|
|
175
177
|
workspace=Path.cwd(), sandbox=sandbox,
|
|
176
178
|
sandbox_cwd=Path(export_root) if export_root else None,
|
|
177
|
-
secrets=resolved_secrets
|
|
179
|
+
secrets=resolved_secrets,
|
|
180
|
+
engine_allowlist=engine_allowlist)
|
|
178
181
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
179
182
|
|
|
180
183
|
resume = getattr(args, "resume", None)
|
{agentic_dev_loop-1.0.0a31 → agentic_dev_loop-1.0.0a34}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -121,6 +121,10 @@ class SandboxExecutionRequest:
|
|
|
121
121
|
# Host-side audit metadata (BR-11) - never sent into the container.
|
|
122
122
|
pipeline: str | None = None
|
|
123
123
|
step: str | None = None
|
|
124
|
+
# 036 lane boundary (v1): the unit allowlist every synced path must match.
|
|
125
|
+
# None = unrestricted (no engine-allowlist.yaml); an empty tuple = present
|
|
126
|
+
# but empty file (deny all synced paths).
|
|
127
|
+
allowlist: tuple[str, ...] | None = None
|
|
124
128
|
|
|
125
129
|
def __post_init__(self) -> None:
|
|
126
130
|
if not self.argv:
|
|
@@ -1071,7 +1075,7 @@ class DockerBackend(Sandbox):
|
|
|
1071
1075
|
result = self._exec(request, prof.name)
|
|
1072
1076
|
report: SyncBackReport | None = None
|
|
1073
1077
|
if mount_set.rw_paths() and self._export_root is not None:
|
|
1074
|
-
report = self._sync_back(mount_set)
|
|
1078
|
+
report = self._sync_back(mount_set, request.allowlist)
|
|
1075
1079
|
if report is not None and (report.synced_paths or report.merged_paths):
|
|
1076
1080
|
result = SandboxExecutionResult(
|
|
1077
1081
|
exit_code=result.exit_code, stdout=result.stdout,
|
|
@@ -1169,7 +1173,8 @@ class DockerBackend(Sandbox):
|
|
|
1169
1173
|
export_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1170
1174
|
shutil.copy2(host_path, export_path, follow_symlinks=False)
|
|
1171
1175
|
|
|
1172
|
-
def _sync_back(self, mount_set: MountSet
|
|
1176
|
+
def _sync_back(self, mount_set: MountSet,
|
|
1177
|
+
allowlist: tuple[str, ...] | None = None) -> SyncBackReport | None:
|
|
1173
1178
|
"""Sync declared-rw export changes back to the real repo (BR-4/F1),
|
|
1174
1179
|
git-aware, host-side, with the sandbox step already complete.
|
|
1175
1180
|
|
|
@@ -1184,7 +1189,18 @@ class DockerBackend(Sandbox):
|
|
|
1184
1189
|
self._export_root, self._workspace, list(mount_set.rw_paths()),
|
|
1185
1190
|
baseline=self._baseline, step_id=None,
|
|
1186
1191
|
cache_paths=self._cache_paths, excludes=self._sync_excludes,
|
|
1187
|
-
merge_on_conflict=self._merge_on_conflict)
|
|
1192
|
+
merge_on_conflict=self._merge_on_conflict, allowlist=allowlist)
|
|
1193
|
+
|
|
1194
|
+
# 036 lane boundary (v1): fail the step before any copy/delete/merge
|
|
1195
|
+
# so an out-of-lane write is never promoted into the real repo.
|
|
1196
|
+
if report.lane_violations:
|
|
1197
|
+
_announce(
|
|
1198
|
+
"lane violation: "
|
|
1199
|
+
+ ", ".join(report.lane_violations)
|
|
1200
|
+
+ " outside engine-allowlist.yaml")
|
|
1201
|
+
raise SandboxError(
|
|
1202
|
+
"engine lane violation: synced/merged paths outside the unit "
|
|
1203
|
+
"engine-allowlist.yaml: " + ", ".join(report.lane_violations))
|
|
1188
1204
|
|
|
1189
1205
|
# 029 step 2: union-merge declared merge paths first, so a mixed
|
|
1190
1206
|
# merge+hard conflict still converges the append-style logs before
|