agentic-dev-loop 1.0.0a35__tar.gz → 1.0.0a37__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.0a35/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a37}/PKG-INFO +19 -17
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/README.md +18 -16
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/agent_error_guide.md +1 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/egress_policy.py +16 -6
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/egress_proxy.py +112 -17
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/isolation.py +333 -51
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/sync_back.py +1 -1
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37/src/agentic_dev_loop.egg-info}/PKG-INFO +19 -17
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_agent_error_guide.py +6 -3
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_e2e_fixtures.py +91 -10
- agentic_dev_loop-1.0.0a37/tests/test_egress_proxy.py +428 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_isolation.py +210 -36
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_isolation_integration.py +4 -4
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_isolation_pbt.py +4 -3
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_sync_back.py +1 -1
- agentic_dev_loop-1.0.0a35/tests/test_egress_proxy.py +0 -205
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/core/config.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/stages.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_032_stop_when_green.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_033_brief_step.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a35/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a37}/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.0a37
|
|
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
|
|
@@ -242,7 +242,7 @@ steps:
|
|
|
242
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. |
|
|
243
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
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/
|
|
245
|
+
| `custom_prompt:` | Planned (planning-docs/040). 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. |
|
|
246
246
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
247
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). |
|
|
248
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). |
|
|
@@ -328,11 +328,13 @@ steps:
|
|
|
328
328
|
a command like `curl ... -o spec.json` has no writable cwd to write into.
|
|
329
329
|
Declare `mounts: [{ path: ".", mode: rw }]` (or a narrower rw path) for any
|
|
330
330
|
networked step that needs to produce files.
|
|
331
|
-
- **`network: true` is
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
- **`network: true` is scoped by the `egress:` allowlist, and egress is
|
|
332
|
+
structurally enforced.** A networked step runs on a per-run internal
|
|
333
|
+
docker network with no route to the host. Its traffic is forced
|
|
334
|
+
through a containerized egress proxy (injected via
|
|
335
|
+
`HTTP_PROXY`/`HTTPS_PROXY`, pointed at the proxy container's DNS name
|
|
336
|
+
on the internal network); a request to an undeclared host is denied
|
|
337
|
+
and logged as an enforcement event. The engine never adds an
|
|
336
338
|
undeclared target. `egress: { class: browser }` is the explicit
|
|
337
339
|
broad-but-logged class (every connection logged, nothing filtered); command
|
|
338
340
|
and agent steps default to a strict allowlist.
|
|
@@ -342,16 +344,16 @@ steps:
|
|
|
342
344
|
to exactly the declared hosts for that stage; the default stays network-off
|
|
343
345
|
everywhere. Reserve it for stages that need it (QA, integration). The
|
|
344
346
|
residual risks are documented in [docs/setup.md](docs/setup.md).
|
|
345
|
-
- **Host-service reachability (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
host
|
|
352
|
-
host
|
|
353
|
-
|
|
354
|
-
|
|
347
|
+
- **Host-service reachability (planning-docs/037, 038).** The egress
|
|
348
|
+
proxy is a container on the run's internal network with a second
|
|
349
|
+
host-access attachment carrying the `host.docker.internal`
|
|
350
|
+
host-gateway alias, so the name resolves natively inside the proxy
|
|
351
|
+
container and host services (an S3 SDK against a local MinIO, say)
|
|
352
|
+
are reached through the enforced, logged path. Sandbox containers
|
|
353
|
+
have no host route: the internal network carries no default route to
|
|
354
|
+
the host, and sandbox containers get no host-gateway alias.
|
|
355
|
+
`dial_rewrites` on the proxy remains the platform override for hosts
|
|
356
|
+
where the alias does not resolve.
|
|
355
357
|
|
|
356
358
|
### Sync-back conflicts (host edits during a run)
|
|
357
359
|
|
|
@@ -200,7 +200,7 @@ steps:
|
|
|
200
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. |
|
|
201
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
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/
|
|
203
|
+
| `custom_prompt:` | Planned (planning-docs/040). 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. |
|
|
204
204
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
205
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). |
|
|
206
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). |
|
|
@@ -286,11 +286,13 @@ steps:
|
|
|
286
286
|
a command like `curl ... -o spec.json` has no writable cwd to write into.
|
|
287
287
|
Declare `mounts: [{ path: ".", mode: rw }]` (or a narrower rw path) for any
|
|
288
288
|
networked step that needs to produce files.
|
|
289
|
-
- **`network: true` is
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
289
|
+
- **`network: true` is scoped by the `egress:` allowlist, and egress is
|
|
290
|
+
structurally enforced.** A networked step runs on a per-run internal
|
|
291
|
+
docker network with no route to the host. Its traffic is forced
|
|
292
|
+
through a containerized egress proxy (injected via
|
|
293
|
+
`HTTP_PROXY`/`HTTPS_PROXY`, pointed at the proxy container's DNS name
|
|
294
|
+
on the internal network); a request to an undeclared host is denied
|
|
295
|
+
and logged as an enforcement event. The engine never adds an
|
|
294
296
|
undeclared target. `egress: { class: browser }` is the explicit
|
|
295
297
|
broad-but-logged class (every connection logged, nothing filtered); command
|
|
296
298
|
and agent steps default to a strict allowlist.
|
|
@@ -300,16 +302,16 @@ steps:
|
|
|
300
302
|
to exactly the declared hosts for that stage; the default stays network-off
|
|
301
303
|
everywhere. Reserve it for stages that need it (QA, integration). The
|
|
302
304
|
residual risks are documented in [docs/setup.md](docs/setup.md).
|
|
303
|
-
- **Host-service reachability (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
host
|
|
310
|
-
host
|
|
311
|
-
|
|
312
|
-
|
|
305
|
+
- **Host-service reachability (planning-docs/037, 038).** The egress
|
|
306
|
+
proxy is a container on the run's internal network with a second
|
|
307
|
+
host-access attachment carrying the `host.docker.internal`
|
|
308
|
+
host-gateway alias, so the name resolves natively inside the proxy
|
|
309
|
+
container and host services (an S3 SDK against a local MinIO, say)
|
|
310
|
+
are reached through the enforced, logged path. Sandbox containers
|
|
311
|
+
have no host route: the internal network carries no default route to
|
|
312
|
+
the host, and sandbox containers get no host-gateway alias.
|
|
313
|
+
`dial_rewrites` on the proxy remains the platform override for hosts
|
|
314
|
+
where the alias does not resolve.
|
|
313
315
|
|
|
314
316
|
### Sync-back conflicts (host edits during a run)
|
|
315
317
|
|
|
@@ -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.0a37"
|
|
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"
|
|
@@ -4,3 +4,4 @@
|
|
|
4
4
|
| AE-SBX-010 | The sandbox could not create the execution container for the command, so the command did not run. | The command is not the problem: retry once; if it persists, mention the code in your final response and stop. |
|
|
5
5
|
| AE-SBX-016 | The loop refused to sync an edit because the real repo changed underneath on a path this workspace also touches; the file in this workspace is stale and its edits will not be applied. | The command is not the problem: retry once, do not edit or work around the named path, and mention the code in your final response if it persists. |
|
|
6
6
|
| AE-SBX-018 | The network egress proxy for this step stopped, so the sandbox refused the command rather than run without enforcement. | The command is not the problem: do not retry it, mention the code in your final response, and stop. |
|
|
7
|
+
| AE-SBX-020 | The sandbox could not start the network egress proxy for this step, so the command did not run. | The command is not the problem: retry once; if it persists, mention the code in your final response and stop. |
|
{agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/egress_policy.py
RENAMED
|
@@ -10,11 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from dataclasses import dataclass
|
|
12
12
|
|
|
13
|
-
from agentic_dev_loop.core.config import (
|
|
14
|
-
EgressSpec, parse_egress_port, parse_egress_target,
|
|
15
|
-
)
|
|
16
|
-
from .presets import expand_presets
|
|
17
|
-
|
|
18
13
|
ALLOWLIST = "allowlist"
|
|
19
14
|
# The explicit broad-but-logged browser class (BR-7): no target filtering,
|
|
20
15
|
# every connection logged host-side.
|
|
@@ -32,6 +27,7 @@ class TargetEntry:
|
|
|
32
27
|
|
|
33
28
|
@classmethod
|
|
34
29
|
def from_spec(cls, spec: str) -> "TargetEntry":
|
|
30
|
+
from agentic_dev_loop.core.config import parse_egress_target
|
|
35
31
|
host, port = parse_egress_target(spec)
|
|
36
32
|
return cls(host=host, port=port)
|
|
37
33
|
|
|
@@ -51,6 +47,7 @@ class EgressPolicy:
|
|
|
51
47
|
policy_class: str = ALLOWLIST
|
|
52
48
|
|
|
53
49
|
def to_spec(self) -> EgressSpec:
|
|
50
|
+
from agentic_dev_loop.core.config import EgressSpec
|
|
54
51
|
cls = None if self.policy_class == ALLOWLIST else BROWSER
|
|
55
52
|
return EgressSpec(
|
|
56
53
|
class_=cls,
|
|
@@ -72,6 +69,7 @@ def assemble_egress_policy(egress: EgressSpec | None) -> EgressPolicy:
|
|
|
72
69
|
cls = egress.class_ or ALLOWLIST
|
|
73
70
|
if cls == BROWSER:
|
|
74
71
|
return EgressPolicy(policy_class=BROWSER)
|
|
72
|
+
from .presets import expand_presets
|
|
75
73
|
entries: list[TargetEntry] = []
|
|
76
74
|
if egress.presets:
|
|
77
75
|
for spec in expand_presets(egress.presets):
|
|
@@ -81,6 +79,18 @@ def assemble_egress_policy(egress: EgressSpec | None) -> EgressPolicy:
|
|
|
81
79
|
return EgressPolicy(targets=tuple(entries), policy_class=ALLOWLIST)
|
|
82
80
|
|
|
83
81
|
|
|
82
|
+
def _parse_port(spec: str, port: str) -> int | None:
|
|
83
|
+
"""Validate a target port (BR-6.3), standalone-safe (no engine package)."""
|
|
84
|
+
if port == "":
|
|
85
|
+
raise ValueError(f"malformed egress target {spec!r}: empty port")
|
|
86
|
+
if not port.isdigit():
|
|
87
|
+
raise ValueError(f"malformed egress target {spec!r}: port {port!r} not numeric")
|
|
88
|
+
n = int(port)
|
|
89
|
+
if not 1 <= n <= 65535:
|
|
90
|
+
raise ValueError(f"malformed egress target {spec!r}: port {n} out of range")
|
|
91
|
+
return n
|
|
92
|
+
|
|
93
|
+
|
|
84
94
|
def _split_host_port(hostname: str) -> tuple[str, int | None]:
|
|
85
95
|
"""Split a CONNECT target into (host, port). Bare hostnames carry no port."""
|
|
86
96
|
value = hostname.strip()
|
|
@@ -91,7 +101,7 @@ def _split_host_port(hostname: str) -> tuple[str, int | None]:
|
|
|
91
101
|
host = value[1:end]
|
|
92
102
|
rest = value[end + 1:]
|
|
93
103
|
if rest.startswith(":"):
|
|
94
|
-
return host,
|
|
104
|
+
return host, _parse_port(hostname, rest[1:])
|
|
95
105
|
return host, None
|
|
96
106
|
if value.count(":") == 1:
|
|
97
107
|
host, port = value.rsplit(":", 1)
|
{agentic_dev_loop-1.0.0a35 → agentic_dev_loop-1.0.0a37}/src/agentic_dev_loop/engine/egress_proxy.py
RENAMED
|
@@ -8,13 +8,21 @@ request hostname (CONNECT target) per connection against the expanded policy
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import argparse
|
|
11
12
|
import asyncio
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
12
15
|
import threading
|
|
16
|
+
import time
|
|
13
17
|
from dataclasses import dataclass, field
|
|
14
18
|
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
15
20
|
from urllib.parse import urlsplit
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
if __package__:
|
|
23
|
+
from .egress_policy import EgressPolicy, TargetEntry, match_target
|
|
24
|
+
else: # standalone container-mode entrypoint (run as a script)
|
|
25
|
+
from egress_policy import EgressPolicy, TargetEntry, match_target
|
|
18
26
|
|
|
19
27
|
|
|
20
28
|
@dataclass(frozen=True)
|
|
@@ -33,6 +41,8 @@ class EgressProxy:
|
|
|
33
41
|
|
|
34
42
|
policy: EgressPolicy
|
|
35
43
|
events: list[EnforcementEvent] = field(default_factory=list)
|
|
44
|
+
dial_rewrites: dict[str, str] = field(default_factory=dict)
|
|
45
|
+
event_file: Path | None = None
|
|
36
46
|
|
|
37
47
|
def __post_init__(self) -> None:
|
|
38
48
|
self._server: asyncio.AbstractServer | None = None
|
|
@@ -47,24 +57,40 @@ class EgressProxy:
|
|
|
47
57
|
"""True while the proxy's server thread is running."""
|
|
48
58
|
return self._thread is not None and self._thread.is_alive()
|
|
49
59
|
|
|
50
|
-
def start(self, host: str = "127.0.0.1", port: int = 0
|
|
60
|
+
def start(self, host: str = "127.0.0.1", port: int = 0,
|
|
61
|
+
bind_timeout: float = 5.0) -> int:
|
|
51
62
|
"""Bind the proxy on ``host:port`` (ephemeral when port=0) and serve
|
|
52
|
-
in a background thread. Returns the bound port.
|
|
63
|
+
in a background thread. Returns the bound port.
|
|
64
|
+
|
|
65
|
+
``bind_timeout`` bounds how long the bind may wait for the requested
|
|
66
|
+
address to appear (the container-mode entrypoint binds an
|
|
67
|
+
internal-network IP that is attached after the container starts)."""
|
|
53
68
|
self._loop = asyncio.new_event_loop()
|
|
54
69
|
|
|
55
70
|
def _run() -> None:
|
|
56
71
|
asyncio.set_event_loop(self._loop)
|
|
57
|
-
|
|
58
|
-
|
|
72
|
+
deadline = time.monotonic() + bind_timeout
|
|
73
|
+
while True:
|
|
74
|
+
try:
|
|
75
|
+
self._server = self._loop.run_until_complete(
|
|
76
|
+
asyncio.start_server(self._handle, host, port))
|
|
77
|
+
break
|
|
78
|
+
except OSError:
|
|
79
|
+
if time.monotonic() >= deadline:
|
|
80
|
+
raise
|
|
81
|
+
time.sleep(0.1)
|
|
59
82
|
self._port = self._server.sockets[0].getsockname()[1]
|
|
60
83
|
self._loop.run_forever()
|
|
61
84
|
|
|
62
85
|
self._thread = threading.Thread(target=_run, daemon=True)
|
|
63
86
|
self._thread.start()
|
|
64
|
-
deadline = datetime.now().timestamp() +
|
|
87
|
+
deadline = datetime.now().timestamp() + bind_timeout + 1.0
|
|
65
88
|
while self._port is None:
|
|
89
|
+
if not self._thread.is_alive():
|
|
90
|
+
raise RuntimeError("egress proxy failed to bind")
|
|
66
91
|
if datetime.now().timestamp() > deadline:
|
|
67
|
-
raise RuntimeError(
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
f"egress proxy failed to bind within {bind_timeout}s")
|
|
68
94
|
threading.Event().wait(0.01)
|
|
69
95
|
return self._port
|
|
70
96
|
|
|
@@ -138,7 +164,7 @@ class EgressProxy:
|
|
|
138
164
|
# upstream answers with a decode alert - the restore step's SSL
|
|
139
165
|
# TLSV1_ALERT_DECODE_ERROR).
|
|
140
166
|
await _drain_headers(reader)
|
|
141
|
-
conn = await _open(host, port)
|
|
167
|
+
conn = await self._open(host, port)
|
|
142
168
|
if conn is None:
|
|
143
169
|
_write(writer, b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
|
|
144
170
|
return
|
|
@@ -159,7 +185,7 @@ class EgressProxy:
|
|
|
159
185
|
_write(writer, b"HTTP/1.1 403 Forbidden\r\nContent-Length: 0\r\n\r\n")
|
|
160
186
|
return
|
|
161
187
|
self._record(host, True)
|
|
162
|
-
conn = await _open(host, port)
|
|
188
|
+
conn = await self._open(host, port)
|
|
163
189
|
if conn is None:
|
|
164
190
|
_write(writer, b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
|
|
165
191
|
return
|
|
@@ -174,9 +200,23 @@ class EgressProxy:
|
|
|
174
200
|
await _relay_both(reader, writer, upstream_reader, upstream_writer)
|
|
175
201
|
|
|
176
202
|
def _record(self, hostname: str, allowed: bool) -> None:
|
|
177
|
-
|
|
203
|
+
event = EnforcementEvent(
|
|
178
204
|
hostname=hostname, allowed=allowed,
|
|
179
|
-
timestamp=datetime.now(timezone.utc).isoformat())
|
|
205
|
+
timestamp=datetime.now(timezone.utc).isoformat())
|
|
206
|
+
self.events.append(event)
|
|
207
|
+
if self.event_file is not None:
|
|
208
|
+
_append_event_file(self.event_file, event)
|
|
209
|
+
|
|
210
|
+
async def _open(self, host: str, port: int):
|
|
211
|
+
"""Dial the upstream, applying the configured dial rewrite (D8).
|
|
212
|
+
``match_target`` already ran against the original hostname, and the
|
|
213
|
+
request/Host header keep that hostname; only the upstream dial address
|
|
214
|
+
is rewritten."""
|
|
215
|
+
dial_host = self.dial_rewrites.get(host, host)
|
|
216
|
+
try:
|
|
217
|
+
return await asyncio.open_connection(dial_host, port)
|
|
218
|
+
except OSError:
|
|
219
|
+
return None
|
|
180
220
|
|
|
181
221
|
|
|
182
222
|
def _parse_request_line(line: bytes) -> tuple[str, str, str]:
|
|
@@ -207,12 +247,6 @@ def _write(writer: asyncio.StreamWriter, data: bytes) -> None:
|
|
|
207
247
|
writer.write(data)
|
|
208
248
|
|
|
209
249
|
|
|
210
|
-
async def _open(host: str, port: int):
|
|
211
|
-
try:
|
|
212
|
-
return await asyncio.open_connection(host, port)
|
|
213
|
-
except OSError:
|
|
214
|
-
return None
|
|
215
|
-
|
|
216
250
|
|
|
217
251
|
async def _forward_headers(reader, upstream, host: str) -> None:
|
|
218
252
|
saw_host = False
|
|
@@ -262,3 +296,64 @@ async def _relay(src, dst) -> None:
|
|
|
262
296
|
dst.write_eof()
|
|
263
297
|
except Exception:
|
|
264
298
|
pass
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _append_event_file(path: Path, event: EnforcementEvent) -> None:
|
|
303
|
+
"""Append one enforcement event to the mounted JSONL file (best-effort:
|
|
304
|
+
a broken events file must never break enforcement)."""
|
|
305
|
+
record = {
|
|
306
|
+
"hostname": event.hostname,
|
|
307
|
+
"allowed": event.allowed,
|
|
308
|
+
"timestamp": event.timestamp,
|
|
309
|
+
}
|
|
310
|
+
try:
|
|
311
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
312
|
+
fh.write(json.dumps(record, sort_keys=True) + "\n")
|
|
313
|
+
except OSError:
|
|
314
|
+
pass
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def policy_to_json(policy: EgressPolicy) -> str:
|
|
318
|
+
"""Serialize a resolved policy for the container-mode entrypoint."""
|
|
319
|
+
return json.dumps({
|
|
320
|
+
"class": policy.policy_class,
|
|
321
|
+
"targets": [{"host": t.host, "port": t.port} for t in policy.targets],
|
|
322
|
+
}, sort_keys=True)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def policy_from_json(text: str) -> EgressPolicy:
|
|
326
|
+
"""Rebuild a resolved policy from the entrypoint's ``--policy`` JSON."""
|
|
327
|
+
data = json.loads(text)
|
|
328
|
+
return EgressPolicy(
|
|
329
|
+
targets=tuple(
|
|
330
|
+
TargetEntry(host=t["host"], port=t.get("port"))
|
|
331
|
+
for t in data.get("targets", [])),
|
|
332
|
+
policy_class=data.get("class", "allowlist"),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main(argv: list[str] | None = None) -> int:
|
|
337
|
+
"""Container-mode entrypoint: serve the policy on the internal network
|
|
338
|
+
and append enforcement events to a mounted file until SIGTERM."""
|
|
339
|
+
parser = argparse.ArgumentParser(description="agentic-dev-loop egress proxy")
|
|
340
|
+
parser.add_argument("--policy", required=True)
|
|
341
|
+
parser.add_argument("--events", required=True)
|
|
342
|
+
parser.add_argument("--dial-rewrites", default="{}")
|
|
343
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
344
|
+
parser.add_argument("--port", type=int, default=0)
|
|
345
|
+
parser.add_argument("--bind-timeout", type=float, default=5.0)
|
|
346
|
+
args = parser.parse_args(argv)
|
|
347
|
+
|
|
348
|
+
proxy = EgressProxy(
|
|
349
|
+
policy=policy_from_json(args.policy),
|
|
350
|
+
dial_rewrites=json.loads(args.dial_rewrites),
|
|
351
|
+
event_file=Path(args.events),
|
|
352
|
+
)
|
|
353
|
+
proxy.start(host=args.host, port=args.port, bind_timeout=args.bind_timeout)
|
|
354
|
+
threading.Event().wait()
|
|
355
|
+
return 0
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
if __name__ == "__main__":
|
|
359
|
+
sys.exit(main())
|