agentic-dev-loop 1.0.0a15__tar.gz → 1.0.0a17__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.0a15 → agentic_dev_loop-1.0.0a17}/PKG-INFO +3 -3
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/README.md +2 -2
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/config.py +9 -5
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/actions.py +1 -1
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/dryrun.py +2 -1
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/graph.py +16 -7
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/hostboundary.py +1 -1
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/isolation.py +13 -2
- agentic_dev_loop-1.0.0a17/src/agentic_dev_loop/engine/seccomp/__init__.py +72 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/stages.py +53 -14
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/PKG-INFO +3 -3
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_actions.py +1 -1
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_agents.py +1 -1
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_config.py +3 -3
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_graph.py +35 -10
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_isolation.py +105 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_qa_prompt.py +8 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_secrets.py +4 -4
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_stages.py +64 -11
- agentic_dev_loop-1.0.0a15/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -27
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/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.0a17
|
|
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
|
|
@@ -213,7 +213,7 @@ steps:
|
|
|
213
213
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
214
214
|
| `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). |
|
|
215
215
|
| `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). |
|
|
216
|
-
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
216
|
+
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
217
217
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
|
|
218
218
|
| `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. |
|
|
219
219
|
| `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
|
|
@@ -231,7 +231,7 @@ secrets: # refs only - never values
|
|
|
231
231
|
key: KINDIE_CLIENT_SECRET # provider-specific locator
|
|
232
232
|
r2-keys:
|
|
233
233
|
from: env
|
|
234
|
-
key:
|
|
234
|
+
key: R2_KEYS
|
|
235
235
|
|
|
236
236
|
steps:
|
|
237
237
|
- name: qa
|
|
@@ -171,7 +171,7 @@ steps:
|
|
|
171
171
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
172
172
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` grants egress to a command or agent step (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
173
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). |
|
|
174
|
-
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
174
|
+
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
175
175
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
|
|
176
176
|
| `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
|
|
177
177
|
| `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
|
|
@@ -189,7 +189,7 @@ secrets: # refs only - never values
|
|
|
189
189
|
key: KINDIE_CLIENT_SECRET # provider-specific locator
|
|
190
190
|
r2-keys:
|
|
191
191
|
from: env
|
|
192
|
-
key:
|
|
192
|
+
key: R2_KEYS
|
|
193
193
|
|
|
194
194
|
steps:
|
|
195
195
|
- name: qa
|
|
@@ -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.0a17"
|
|
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"
|
|
@@ -87,11 +87,15 @@ class IsolationConfig(BaseModel):
|
|
|
87
87
|
# the workspace - validated at startup, not load (the workspace is unknown
|
|
88
88
|
# here).
|
|
89
89
|
sandbox_venv_dir: str | None = None
|
|
90
|
-
# 023: optional host path to a Docker seccomp profile JSON
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
94
|
-
#
|
|
90
|
+
# 023/025: optional host path to a Docker seccomp profile JSON, or a
|
|
91
|
+
# ``bundled:<name>`` sentinel resolved at load to a packaged profile
|
|
92
|
+
# (e.g. ``bundled:chromium-userns``). When set, the exec container runs
|
|
93
|
+
# with ``--security-opt seccomp=<path>`` in place of Docker's default
|
|
94
|
+
# profile, plus ``--cap-add SYS_CHROOT`` (the profile gates chroot(2) on
|
|
95
|
+
# exactly that capability). Used to allow Chromium's namespace sandbox
|
|
96
|
+
# (userns syscalls) under --cap-drop ALL. An unknown sentinel name fails
|
|
97
|
+
# at load naming the known bundled profiles; a raw path is validated by
|
|
98
|
+
# Docker at container-create time, not at load.
|
|
95
99
|
seccomp_profile: str | None = None
|
|
96
100
|
|
|
97
101
|
@field_validator("memory")
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/actions.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Built-in pipeline actions. Node-shaped: (state, params) -> state update.
|
|
2
2
|
|
|
3
|
-
`commit` policy (ported unchanged from
|
|
3
|
+
`commit` policy (ported unchanged from the consumer): agents never commit mid-run;
|
|
4
4
|
the pipeline commits exactly once, and only when the worktree was clean at
|
|
5
5
|
run start - a dirty-at-start run is committed by the human, so the
|
|
6
6
|
auto-commit can never swallow pre-existing unrelated changes (run 004: the
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
@@ -16,7 +16,7 @@ from agentic_dev_loop.core.config import PipelineConfig, StepConfig, load_config
|
|
|
16
16
|
from .hostboundary import check_orchestrator_install, check_tooling_integrity
|
|
17
17
|
from .loaders import resolve_docs
|
|
18
18
|
from .isolation import (isolation_state_dir, resolve_effective_profile,
|
|
19
|
-
resolve_image_ref)
|
|
19
|
+
resolve_image_ref, resolved_seccomp_profile)
|
|
20
20
|
from .registry import (Registry, default_registry, load_extensions,
|
|
21
21
|
validate_action_network)
|
|
22
22
|
from .secrets import scan_env_files
|
|
@@ -134,6 +134,7 @@ def dry_run(cfg_path, args, registry: Registry | None = None) -> int:
|
|
|
134
134
|
|
|
135
135
|
_validate_agents(cfg, base / cfg.agents_dir)
|
|
136
136
|
_validate_actions(cfg, registry)
|
|
137
|
+
resolved_seccomp_profile(cfg) # fails loud on an unknown bundled:<name>
|
|
137
138
|
|
|
138
139
|
loader, docs, subject = resolve_docs(cfg, base, registry, args)
|
|
139
140
|
image, memory, cpus = _resolved_image(cfg)
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
@@ -27,19 +27,27 @@ from .loaders import resolve_docs
|
|
|
27
27
|
from .secrets import resolve_secrets, scan_env_files
|
|
28
28
|
from .registry import (Registry, default_registry, load_extensions,
|
|
29
29
|
validate_action_network)
|
|
30
|
-
from .stages import (Runtime,
|
|
30
|
+
from .stages import (Runtime, clear_attempt_artifacts,
|
|
31
31
|
make_action_node, make_agent_node, make_bump_node,
|
|
32
32
|
make_command_node, make_router, success_node)
|
|
33
33
|
|
|
34
34
|
CHECKPOINT_DB = "loop-checkpoints.sqlite" # under state_dir, one DB per workspace
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
def _merge_artifacts(a: dict[str, str], b: dict[str, str]) -> dict[str, str]:
|
|
38
|
+
"""Accumulate per-step artifact text across nodes. LangGraph gives
|
|
39
|
+
un-annotated fields last-write-wins, so without this reducer one node's
|
|
40
|
+
entry would silently replace the accumulated dict."""
|
|
41
|
+
return {**a, **b}
|
|
42
|
+
|
|
43
|
+
|
|
37
44
|
class LoopState(TypedDict):
|
|
38
45
|
"""State threaded through the graph. `notes` accumulates across attempts
|
|
39
|
-
(append reducer)
|
|
46
|
+
(append reducer), `artifacts` accumulates across stages (merge reducer);
|
|
47
|
+
every other key is last-write-wins."""
|
|
40
48
|
subject: str
|
|
41
49
|
docs: dict[str, str]
|
|
42
|
-
|
|
50
|
+
artifacts: Annotated[dict[str, str], _merge_artifacts] # step name -> artifact text
|
|
43
51
|
attempt: int
|
|
44
52
|
max_attempts: int
|
|
45
53
|
notes: Annotated[list[str], operator.add]
|
|
@@ -95,7 +103,7 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
|
|
|
95
103
|
registry: Registry | None = None) -> tuple[bool, int]:
|
|
96
104
|
"""Load a pipeline YAML, resolve docs via the selected document loader,
|
|
97
105
|
and run the graph to completion. Returns (success, attempts) - the
|
|
98
|
-
public API,
|
|
106
|
+
public API, the consumer's `construct`/`resume_loop` equivalent.
|
|
99
107
|
|
|
100
108
|
`args` carries the parsed CLI namespace: loader flags, --resume,
|
|
101
109
|
--max-attempts (which overrides the YAML value when set), and --no-viz.
|
|
@@ -163,7 +171,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
163
171
|
config = {"configurable": {"thread_id": thread_id}}
|
|
164
172
|
db = str(state_dir / CHECKPOINT_DB)
|
|
165
173
|
if resume:
|
|
166
|
-
initial = None # state (docs/subject/
|
|
174
|
+
initial = None # state (docs/subject/artifacts) comes from the checkpoint
|
|
167
175
|
else:
|
|
168
176
|
_loader, docs, subject = resolve_docs(cfg, base, registry, args)
|
|
169
177
|
# A fresh run owns state_dir: drop cross-run leftovers (a previous
|
|
@@ -173,7 +181,7 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
173
181
|
initial: LoopState = {
|
|
174
182
|
"subject": subject,
|
|
175
183
|
"docs": docs,
|
|
176
|
-
"
|
|
184
|
+
"artifacts": {},
|
|
177
185
|
"attempt": 1,
|
|
178
186
|
"max_attempts": max_attempts,
|
|
179
187
|
"notes": [],
|
|
@@ -221,7 +229,8 @@ def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
|
|
|
221
229
|
f"Failure details: {state_dir}/last-error.md",
|
|
222
230
|
flush=True)
|
|
223
231
|
initial = {**values, "failed": False,
|
|
224
|
-
"resume_target": target, "failed_step": None
|
|
232
|
+
"resume_target": target, "failed_step": None,
|
|
233
|
+
"artifacts": values.get("artifacts", {})}
|
|
225
234
|
|
|
226
235
|
if bus is None and not getattr(args, "no_viz", True):
|
|
227
236
|
start_state = initial if initial is not None \
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
@@ -29,7 +29,7 @@ if TYPE_CHECKING:
|
|
|
29
29
|
from agentic_dev_loop.core.config import PipelineConfig
|
|
30
30
|
|
|
31
31
|
# Tooling directories are workspace-root-relative fixed names (the same layout
|
|
32
|
-
# the dogfood pipeline and
|
|
32
|
+
# the dogfood pipeline and the consumer repo use); the other tooling paths are config- or
|
|
33
33
|
# pipeline-file-relative.
|
|
34
34
|
TOOLING_DIR_NAMES = ("aidlc-docs", "tools")
|
|
35
35
|
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -37,6 +37,7 @@ from typing import Protocol
|
|
|
37
37
|
|
|
38
38
|
from agentic_dev_loop.core.config import IsolationConfig, PipelineConfig
|
|
39
39
|
from .proc import needs_shell, split_command
|
|
40
|
+
from .seccomp import resolve_seccomp_profile
|
|
40
41
|
|
|
41
42
|
# ---------------------------------------------------------------------------
|
|
42
43
|
# Constants (KD-2 / ID-4)
|
|
@@ -480,7 +481,9 @@ def docker_create_payload(*, run_id: str, image: str, profile: EffectiveProfile,
|
|
|
480
481
|
CAP_SYS_CHROOT; the profile gates chroot(2) on exactly that capability,
|
|
481
482
|
so the payload adds it back (the bounding set is otherwise empty under
|
|
482
483
|
--cap-drop ALL). Without the cap the zygote dies even with the profile
|
|
483
|
-
(023 host-side gate 2).
|
|
484
|
+
(023 host-side gate 2). The ``--cap-add SYS_CHROOT`` applies to any
|
|
485
|
+
seccomp profile by design: the bundled profile requires it, and custom
|
|
486
|
+
profiles are expected to follow the same cap-gated pattern."""
|
|
484
487
|
payload = [
|
|
485
488
|
"docker", "run", "--detach", "--rm", "--name",
|
|
486
489
|
exec_container_name(run_id, network),
|
|
@@ -956,6 +959,14 @@ def isolation_state_dir(cfg: PipelineConfig) -> Path:
|
|
|
956
959
|
return Path(iso.state_dir).expanduser()
|
|
957
960
|
|
|
958
961
|
|
|
962
|
+
def resolved_seccomp_profile(cfg: PipelineConfig) -> str | None:
|
|
963
|
+
"""The pipeline's seccomp profile with any ``bundled:<name>`` sentinel
|
|
964
|
+
expanded to a real host path (025). A raw path passes through unchanged
|
|
965
|
+
and is still validated by Docker at container-create time."""
|
|
966
|
+
return resolve_seccomp_profile(
|
|
967
|
+
cfg.isolation.seccomp_profile if cfg.isolation else None)
|
|
968
|
+
|
|
969
|
+
|
|
959
970
|
def resolve_image_ref(image: str, environ: Mapping[str, str] | None = None) -> str:
|
|
960
971
|
"""AGENTIC_DEV_LOOP_DEV_IMAGE override (tag OR digest) → else the profile
|
|
961
972
|
image (digest-pinned) → if empty, fail fast AE-SBX-005."""
|
|
@@ -976,6 +987,7 @@ def startup_sandbox(cfg: PipelineConfig, *, workspace: Path,
|
|
|
976
987
|
refresh: bool | None = None) -> Sandbox:
|
|
977
988
|
"""Eager startup sequence (Q2): probe → image → backend → ensure_image →
|
|
978
989
|
create+start → pre-provision every declared setup (stamp-cached)."""
|
|
990
|
+
seccomp_profile = resolved_seccomp_profile(cfg)
|
|
979
991
|
probe_docker(runner)
|
|
980
992
|
profile = resolve_effective_profile(cfg.isolation)
|
|
981
993
|
image = resolve_image_ref(profile.image)
|
|
@@ -983,7 +995,6 @@ def startup_sandbox(cfg: PipelineConfig, *, workspace: Path,
|
|
|
983
995
|
needs_network = any(step.network for step in cfg.steps)
|
|
984
996
|
sandbox_venv_dir = resolve_sandbox_venv_dir(
|
|
985
997
|
cfg.isolation.sandbox_venv_dir if cfg.isolation else None, workspace)
|
|
986
|
-
seccomp_profile = cfg.isolation.seccomp_profile if cfg.isolation else None
|
|
987
998
|
backend = DockerBackend(
|
|
988
999
|
image=image, workspace=str(workspace), iso_state_dir=str(iso_state_dir),
|
|
989
1000
|
profile=profile, runner=runner, network=needs_network,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Bundled Docker seccomp profiles (023).
|
|
2
|
+
|
|
3
|
+
Docker's default seccomp profile blocks the user-namespace syscalls Chromium's
|
|
4
|
+
namespace sandbox needs, which makes ``chromium.launch(chromiumSandbox=True)``
|
|
5
|
+
die at the zygote under ``--cap-drop ALL``. These profiles are Docker's default
|
|
6
|
+
profile with an allowlist for those userns syscalls; pass one as
|
|
7
|
+
``isolation.seccomp_profile`` to swap it in. The ``bundled:<name>`` sentinel
|
|
8
|
+
(025) resolves a profile by name so consumers declare intent instead of
|
|
9
|
+
rediscovering the packaged path.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from importlib.resources import as_file, files
|
|
15
|
+
|
|
16
|
+
CHROMIUM_USERNS_NAME = "chromium-userns"
|
|
17
|
+
CHROMIUM_USERNS_PROFILE = "chromium-userns.json"
|
|
18
|
+
|
|
19
|
+
# ``bundled:<name>`` sentinel suffix -> packaged JSON filename. This is the
|
|
20
|
+
# single registry new bundled profiles slot into (025).
|
|
21
|
+
BUNDLED_PROFILES = {
|
|
22
|
+
CHROMIUM_USERNS_NAME: CHROMIUM_USERNS_PROFILE,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _profile_path(filename: str) -> str:
|
|
27
|
+
"""Host filesystem path to a packaged seccomp profile JSON."""
|
|
28
|
+
traversable = files("agentic_dev_loop.engine.seccomp").joinpath(filename)
|
|
29
|
+
with as_file(traversable) as path:
|
|
30
|
+
return str(path)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def bundled_profile_path(name: str) -> str:
|
|
34
|
+
"""Host filesystem path to the named bundled seccomp profile.
|
|
35
|
+
|
|
36
|
+
Raises ``ValueError`` listing every known ``bundled:<name>`` when ``name``
|
|
37
|
+
is unknown, so a bad sentinel fails loud at load instead of at
|
|
38
|
+
container-create time.
|
|
39
|
+
"""
|
|
40
|
+
filename = BUNDLED_PROFILES.get(name)
|
|
41
|
+
if filename is None:
|
|
42
|
+
known = ", ".join(f"bundled:{n}" for n in sorted(BUNDLED_PROFILES))
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"unknown bundled seccomp profile {name!r} (known: {known})")
|
|
45
|
+
return _profile_path(filename)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def chromium_userns_profile_path() -> str:
|
|
49
|
+
"""Host filesystem path to the bundled Chromium userns seccomp profile.
|
|
50
|
+
|
|
51
|
+
Docker's ``--security-opt seccomp=<path>`` takes a host path, so the JSON
|
|
52
|
+
must resolve to a real file. Wheels unpack to disk, so ``as_file`` returns
|
|
53
|
+
the installed path without copying.
|
|
54
|
+
"""
|
|
55
|
+
return bundled_profile_path(CHROMIUM_USERNS_NAME)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_seccomp_profile(value: str | None) -> str | None:
|
|
59
|
+
"""Expand a ``bundled:<name>`` sentinel to its real profile path (025).
|
|
60
|
+
|
|
61
|
+
A raw host path passes through unchanged (Docker still validates it at
|
|
62
|
+
container-create time, unchanged from 023). An unknown or empty bundled
|
|
63
|
+
name fails loud with every known ``bundled:<name>``.
|
|
64
|
+
"""
|
|
65
|
+
if value is None or not value.startswith("bundled:"):
|
|
66
|
+
return value
|
|
67
|
+
name = value[len("bundled:"):]
|
|
68
|
+
if not name:
|
|
69
|
+
known = ", ".join(f"bundled:{n}" for n in sorted(BUNDLED_PROFILES))
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"seccomp_profile 'bundled:' is missing a name (known: {known})")
|
|
72
|
+
return bundled_profile_path(name)
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Stage nodes, routers, and the run_agent port.
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The consumer loop's hardcoded stages become per-step closures built from the
|
|
4
4
|
pipeline config; the fixed policies stay here, not in config:
|
|
5
5
|
|
|
6
6
|
- per-attempt hygiene: every declared artifact/produces file + last-error.md
|
|
@@ -49,6 +49,7 @@ if TYPE_CHECKING:
|
|
|
49
49
|
DEFAULT_MAX_ITERATIONS = 500
|
|
50
50
|
DEFAULT_COMMAND_TIMEOUT = 600.0 # seconds - a pytest-class suite budget
|
|
51
51
|
LAST_ERROR = "last-error.md"
|
|
52
|
+
ARTIFACT_CONTEXT_CAP = 24_000 # chars per artifact injected into a step's context
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
@dataclass
|
|
@@ -68,7 +69,7 @@ class Runtime:
|
|
|
68
69
|
def prompt_dir(runtime: Runtime) -> str:
|
|
69
70
|
"""state_dir as agents must address it. Agents resolve prompt paths
|
|
70
71
|
against their workspace, so a bare `state_dir.name` only reaches the real
|
|
71
|
-
state_dir when the pipeline file sits at the workspace root -
|
|
72
|
+
state_dir when the pipeline file sits at the workspace root - the consumer runs
|
|
72
73
|
agentic-dev-loop/pipeline.yaml from the repo root, where `.pr` and
|
|
73
74
|
`agentic-dev-loop/.pr` diverge. Workspace-relative when under the workspace,
|
|
74
75
|
the absolute path when outside it."""
|
|
@@ -196,15 +197,41 @@ def _run_command(command: str, workspace: Path, timeout: float,
|
|
|
196
197
|
return None, out.decode("utf-8", "replace"), True
|
|
197
198
|
|
|
198
199
|
|
|
200
|
+
def _artifact_file(runtime: Runtime, step_name: str) -> str | None:
|
|
201
|
+
"""The declared artifact/produces filename a step writes, keyed by step
|
|
202
|
+
name - the artifacts dict only carries step name -> captured text."""
|
|
203
|
+
for step in runtime.cfg.steps:
|
|
204
|
+
if step.name == step_name:
|
|
205
|
+
if step.artifact is not None:
|
|
206
|
+
return step.artifact
|
|
207
|
+
if step.produces:
|
|
208
|
+
return step.produces[0]
|
|
209
|
+
return None
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
199
213
|
def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
|
|
200
|
-
notes: list[str] | None = None
|
|
214
|
+
notes: list[str] | None = None,
|
|
215
|
+
artifacts: dict[str, str] | None = None) -> str:
|
|
201
216
|
"""Assemble the attempt's shared message: subject, doc bundle, the
|
|
202
217
|
canonical dependency-policy paragraph (021), the expected-output-paths
|
|
203
218
|
section generated from step defs (a prompt merely naming files lets agents
|
|
204
|
-
skip them), and previous-attempt
|
|
219
|
+
skip them), accumulated stage artifacts (027), and previous-attempt
|
|
220
|
+
notes."""
|
|
205
221
|
parts = [f"You are working on {subject}.\n"]
|
|
206
222
|
for name, content in docs.items():
|
|
207
223
|
parts.append(f"## {name}\n\n{content}")
|
|
224
|
+
if artifacts:
|
|
225
|
+
parts.append("## Stage artifacts")
|
|
226
|
+
for step_name, content in artifacts.items():
|
|
227
|
+
capped = content[:ARTIFACT_CONTEXT_CAP]
|
|
228
|
+
if len(content) > ARTIFACT_CONTEXT_CAP:
|
|
229
|
+
filename = _artifact_file(runtime, step_name)
|
|
230
|
+
path = (f"{prompt_dir(runtime)}/{filename}" if filename
|
|
231
|
+
else prompt_dir(runtime))
|
|
232
|
+
capped += (f"\n\n[... content truncated - full text at "
|
|
233
|
+
f"`{path}`]")
|
|
234
|
+
parts.append(f"### {step_name}\n\n{capped}")
|
|
208
235
|
parts.append("## Dependency policy\n\n" + policy_injection(runtime.cfg))
|
|
209
236
|
lines = []
|
|
210
237
|
for step in runtime.cfg.steps:
|
|
@@ -240,7 +267,7 @@ def clear_attempt_artifacts(runtime: Runtime) -> None:
|
|
|
240
267
|
|
|
241
268
|
def _unclear_target(runtime: Runtime, step: StepConfig) -> str:
|
|
242
269
|
"""Where the unclear-verdict retry loops to: on_fail's goto when that is
|
|
243
|
-
a step, else the entry step (
|
|
270
|
+
a step, else the entry step (the consumer always looped back to dev)."""
|
|
244
271
|
target = route_target(step.on_fail)
|
|
245
272
|
if target is not None and target not in TERMINALS:
|
|
246
273
|
return target
|
|
@@ -295,8 +322,11 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
295
322
|
attempt = state["attempt"]
|
|
296
323
|
subject = state["subject"]
|
|
297
324
|
max_iterations = step.max_iterations or DEFAULT_MAX_ITERATIONS
|
|
325
|
+
message = build_context(runtime, state["docs"], state["subject"],
|
|
326
|
+
state["notes"],
|
|
327
|
+
artifacts=state.get("artifacts", {}))
|
|
298
328
|
print(f"[{subject}] Running {step.agent} agent...", flush=True)
|
|
299
|
-
result = run_agent(step.agent,
|
|
329
|
+
result = run_agent(step.agent, message, runtime,
|
|
300
330
|
max_iterations=max_iterations, step=step)
|
|
301
331
|
if not result.ok:
|
|
302
332
|
print(f"[{subject}] {step.name} agent failed on attempt {attempt}.", flush=True)
|
|
@@ -330,7 +360,10 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
330
360
|
if verdicts_map is None:
|
|
331
361
|
merged = {**state["step_verdicts"], step.name: "pass"} if captures else \
|
|
332
362
|
state["step_verdicts"]
|
|
333
|
-
|
|
363
|
+
update = {"step_verdicts": merged, "failed": False}
|
|
364
|
+
if captures and result.response is not None:
|
|
365
|
+
update["artifacts"] = {step.name: result.response}
|
|
366
|
+
return update
|
|
334
367
|
|
|
335
368
|
parsed = Verdict.from_text(result.response or "", verdicts_map)
|
|
336
369
|
merged = {**state["step_verdicts"], step.name: None}
|
|
@@ -340,16 +373,24 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
340
373
|
if verdict == "pass":
|
|
341
374
|
print(f"[{subject}] {step.name} VERDICT: PASS on attempt {attempt}.",
|
|
342
375
|
flush=True)
|
|
343
|
-
|
|
376
|
+
update = {"step_verdicts": merged, "failed": False}
|
|
377
|
+
if captures and result.response is not None:
|
|
378
|
+
update["artifacts"] = {step.name: result.response}
|
|
379
|
+
return update
|
|
344
380
|
if verdict == "unverifiable":
|
|
345
381
|
print(f"[{subject}] {step.name} qa verdict: UNVERIFIABLE "
|
|
346
382
|
f"(moves on).", flush=True)
|
|
347
|
-
|
|
383
|
+
update = {"step_verdicts": merged, "failed": False}
|
|
384
|
+
if captures and result.response is not None:
|
|
385
|
+
update["artifacts"] = {step.name: result.response}
|
|
386
|
+
return update
|
|
348
387
|
print(f"[{subject}] {step.name} verdict: fail on attempt {attempt}.",
|
|
349
388
|
flush=True)
|
|
350
389
|
update = {"step_verdicts": merged,
|
|
351
390
|
"notes": [f"## {step.name} output (attempt {attempt})\n\n"
|
|
352
391
|
f"{result.response}"]}
|
|
392
|
+
if captures and result.response is not None:
|
|
393
|
+
update["artifacts"] = {step.name: result.response}
|
|
353
394
|
if isinstance(step.on_fail, Route) and step.on_fail.retry:
|
|
354
395
|
update["retry_target"] = step.on_fail.goto
|
|
355
396
|
return update
|
|
@@ -548,16 +589,14 @@ def make_router(runtime: Runtime, step: StepConfig):
|
|
|
548
589
|
|
|
549
590
|
def make_bump_node(runtime: Runtime):
|
|
550
591
|
"""Close the retry cycle: consume one attempt, clear per-attempt
|
|
551
|
-
artifacts, and
|
|
552
|
-
|
|
592
|
+
artifacts, and reset verdicts/failed for the next attempt. The retried
|
|
593
|
+
agent's message is rebuilt per run by make_agent_node from the
|
|
594
|
+
accumulated notes and artifacts (027)."""
|
|
553
595
|
|
|
554
596
|
def node(state: dict) -> dict:
|
|
555
597
|
clear_attempt_artifacts(runtime)
|
|
556
|
-
context = build_context(runtime, state["docs"], state["subject"],
|
|
557
|
-
state["notes"])
|
|
558
598
|
return {"attempt": state["attempt"] + 1,
|
|
559
599
|
"step_verdicts": {},
|
|
560
|
-
"context": context,
|
|
561
600
|
"failed": False}
|
|
562
601
|
|
|
563
602
|
return node
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/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.0a17
|
|
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
|
|
@@ -213,7 +213,7 @@ steps:
|
|
|
213
213
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
214
214
|
| `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). |
|
|
215
215
|
| `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). |
|
|
216
|
-
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
216
|
+
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
217
217
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
|
|
218
218
|
| `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. |
|
|
219
219
|
| `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
|
|
@@ -231,7 +231,7 @@ secrets: # refs only - never values
|
|
|
231
231
|
key: KINDIE_CLIENT_SECRET # provider-specific locator
|
|
232
232
|
r2-keys:
|
|
233
233
|
from: env
|
|
234
|
-
key:
|
|
234
|
+
key: R2_KEYS
|
|
235
235
|
|
|
236
236
|
steps:
|
|
237
237
|
- name: qa
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Action tests - ported from
|
|
1
|
+
"""Action tests - ported from the consumer's TestKillListenersOnPort /
|
|
2
2
|
TestGitWorktreeClean / TestCommitNode, retargeted at the node-shaped
|
|
3
3
|
`(state, params) -> dict` API. Real processes and real git repos throughout:
|
|
4
4
|
git and sockets are local utilities, not network boundaries."""
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""load_agent tests - ported from
|
|
1
|
+
"""load_agent tests - ported from the consumer's TestLoadAgent, retargeted at the
|
|
2
2
|
library signature which also builds the Agent via the SDK factory. Real
|
|
3
3
|
agent files on disk (fail-fast file policy); real LLM/Agent SDK objects
|
|
4
4
|
(constructed locally, no network)."""
|
|
@@ -6,14 +6,14 @@ from pydantic import ValidationError
|
|
|
6
6
|
|
|
7
7
|
from agentic_dev_loop.core.config import Route, default_state_dir, load_config
|
|
8
8
|
|
|
9
|
-
# The plan's
|
|
9
|
+
# The plan's reference pipeline (consumer-equivalent) (009, YAML schema section),
|
|
10
10
|
# verbatim except for two substitutions: 012 replaced the agent step named
|
|
11
11
|
# `test` (artifact ci-fix.md, PASS/FAIL) with the mechanical command step
|
|
12
12
|
# named `test` - same position, same on_fail wiring; and 009 dropped the
|
|
13
13
|
# port_sweep step (its sweep moved into engine startup, no longer a pipeline
|
|
14
14
|
# step). This is the canonical pin: the documented schema must load.
|
|
15
15
|
FULL_YAML = """\
|
|
16
|
-
name:
|
|
16
|
+
name: reference-construction
|
|
17
17
|
|
|
18
18
|
extensions:
|
|
19
19
|
- agentic_dev_loop.engine.extensions.aidlc:AidlcExtension
|
|
@@ -86,7 +86,7 @@ def test_full_schema_example_round_trips(tmp_path):
|
|
|
86
86
|
p.write_text(FULL_YAML)
|
|
87
87
|
cfg = load_config(p)
|
|
88
88
|
|
|
89
|
-
assert cfg.name == "
|
|
89
|
+
assert cfg.name == "reference-construction"
|
|
90
90
|
assert cfg.extensions == ["agentic_dev_loop.engine.extensions.aidlc:AidlcExtension"]
|
|
91
91
|
assert cfg.additional_files == ["coding-standards-ddd.yaml"]
|
|
92
92
|
assert cfg.state_dir == ".pr"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Graph builder + run_pipeline behavioral tests.
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The consumer-equivalent YAML below must reproduce the hardcoded loop's behavior:
|
|
4
4
|
dev → check → review → qa → commit, retries to dev, failure on
|
|
5
5
|
exhaustion/exception. run_agent is stubbed (the SDK network boundary);
|
|
6
6
|
everything else - files, git, checkpoints - is real."""
|
|
@@ -21,8 +21,8 @@ from agentic_dev_loop.engine.registry import Registry, default_registry, load_ex
|
|
|
21
21
|
from agentic_dev_loop.engine.stages import Runtime
|
|
22
22
|
from agentic_dev_loop.viz import VizBus
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
name:
|
|
24
|
+
REFERENCE_YAML = """\
|
|
25
|
+
name: reference-construction
|
|
26
26
|
extensions:
|
|
27
27
|
- agentic_dev_loop.engine.extensions.aidlc:AidlcExtension
|
|
28
28
|
additional_files:
|
|
@@ -101,10 +101,10 @@ def git(repo: Path, *args: str) -> str:
|
|
|
101
101
|
|
|
102
102
|
@pytest.fixture
|
|
103
103
|
def repo(tmp_path, monkeypatch, fake_sandbox):
|
|
104
|
-
"""A real git repo with the
|
|
104
|
+
"""A real git repo with the consumer-equivalent pipeline committed clean."""
|
|
105
105
|
repo = tmp_path / "repo"
|
|
106
106
|
repo.mkdir()
|
|
107
|
-
(repo / "pipeline.yaml").write_text(
|
|
107
|
+
(repo / "pipeline.yaml").write_text(REFERENCE_YAML)
|
|
108
108
|
(repo / "coding-standards-ddd.yaml").write_text("STANDARDS")
|
|
109
109
|
(repo / "plan.md").write_text("THE PLAN")
|
|
110
110
|
git(repo, "init")
|
|
@@ -131,7 +131,7 @@ def script_all_pass(fake_agents):
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
class TestTopology:
|
|
134
|
-
def
|
|
134
|
+
def test_reference_pipeline_node_set(self, repo):
|
|
135
135
|
cfg = load_config(Path("pipeline.yaml"))
|
|
136
136
|
registry = default_registry()
|
|
137
137
|
load_extensions(cfg.extensions, registry)
|
|
@@ -143,7 +143,7 @@ class TestTopology:
|
|
|
143
143
|
"bump", "success"} <= nodes
|
|
144
144
|
|
|
145
145
|
def test_unknown_action_fails_at_build(self, repo):
|
|
146
|
-
(repo / "bad.yaml").write_text(
|
|
146
|
+
(repo / "bad.yaml").write_text(REFERENCE_YAML.replace(
|
|
147
147
|
"action: commit", "action: no_such_action"))
|
|
148
148
|
cfg = load_config(Path("bad.yaml"))
|
|
149
149
|
runtime = Runtime(cfg=cfg, registry=default_registry(),
|
|
@@ -178,6 +178,31 @@ class TestRunPipeline:
|
|
|
178
178
|
second_dev = fake_agents.messages_for("dev")[1]
|
|
179
179
|
assert "broken build" in second_dev # fail note reached the next attempt
|
|
180
180
|
|
|
181
|
+
def test_downstream_agent_sees_prior_artifact(self, repo, fake_agents):
|
|
182
|
+
script_all_pass(fake_agents)
|
|
183
|
+
ok, attempts = run_pipeline("pipeline.yaml", args())
|
|
184
|
+
assert (ok, attempts) == (True, 1)
|
|
185
|
+
(dev_msg,) = fake_agents.messages_for("dev")
|
|
186
|
+
(check_msg,) = fake_agents.messages_for("check")
|
|
187
|
+
assert "## Stage artifacts" not in dev_msg # runs before anything accumulates
|
|
188
|
+
assert "## Stage artifacts" in check_msg
|
|
189
|
+
assert "built it" in check_msg # dev's captured response reached check
|
|
190
|
+
|
|
191
|
+
def test_retry_injects_review_artifact_notes_and_prior_dev(self, repo,
|
|
192
|
+
fake_agents):
|
|
193
|
+
script_all_pass(fake_agents)
|
|
194
|
+
fake_agents.set("review", [
|
|
195
|
+
("write", "review-findings.md", "VERDICT: NEEDS CHANGES\nfix the thing"),
|
|
196
|
+
("write", "review-findings.md", "VERDICT: APPROVED"),
|
|
197
|
+
])
|
|
198
|
+
ok, attempts = run_pipeline("pipeline.yaml", args())
|
|
199
|
+
assert (ok, attempts) == (True, 2)
|
|
200
|
+
second_dev = fake_agents.messages_for("dev")[1]
|
|
201
|
+
assert "## Stage artifacts" in second_dev
|
|
202
|
+
assert "built it" in second_dev # dev's own attempt-1 artifact
|
|
203
|
+
assert "fix the thing" in second_dev # review artifact + fail note
|
|
204
|
+
assert "## Previous attempt feedback" in second_dev
|
|
205
|
+
|
|
181
206
|
def test_exhaustion_fails(self, repo, fake_agents):
|
|
182
207
|
script_all_pass(fake_agents)
|
|
183
208
|
fake_agents.set("check", [("write", "ci-fix.md", "VERDICT: FAIL")])
|
|
@@ -248,7 +273,7 @@ class TestRunPipeline:
|
|
|
248
273
|
# is not a tooling modification - the loader's missing-file guard is
|
|
249
274
|
# what catches it.
|
|
250
275
|
(repo / "pipeline.yaml").write_text(
|
|
251
|
-
|
|
276
|
+
REFERENCE_YAML.replace("coding-standards-ddd.yaml",
|
|
252
277
|
"no-such-shared-file.yaml"))
|
|
253
278
|
git(repo, "add", "-A")
|
|
254
279
|
git(repo, "commit", "-m", "reference missing additional file")
|
|
@@ -257,7 +282,7 @@ class TestRunPipeline:
|
|
|
257
282
|
assert fake_agents.calls == []
|
|
258
283
|
|
|
259
284
|
def test_empty_loader_docs_fail_fast(self, repo, fake_agents):
|
|
260
|
-
(repo / "empty.yaml").write_text(
|
|
285
|
+
(repo / "empty.yaml").write_text(REFERENCE_YAML.replace(
|
|
261
286
|
"agentic_dev_loop.engine.extensions.aidlc:AidlcExtension",
|
|
262
287
|
"sample_ext:EmptyLoaderExtension"))
|
|
263
288
|
git(repo, "add", "-A")
|
|
@@ -267,7 +292,7 @@ class TestRunPipeline:
|
|
|
267
292
|
assert fake_agents.calls == []
|
|
268
293
|
|
|
269
294
|
|
|
270
|
-
LOCKED_YAML =
|
|
295
|
+
LOCKED_YAML = REFERENCE_YAML.replace(
|
|
271
296
|
"deps_policy: open\n",
|
|
272
297
|
"deps_policy: locked\nlockfiles: [uv.lock]\n",
|
|
273
298
|
1)
|
|
@@ -1097,6 +1097,54 @@ class TestStartupSandbox:
|
|
|
1097
1097
|
finally:
|
|
1098
1098
|
sandbox.close()
|
|
1099
1099
|
|
|
1100
|
+
def test_bundled_seccomp_sentinel_resolved_and_plumbed(self, tmp_path, monkeypatch):
|
|
1101
|
+
"""025: a ``bundled:chromium-userns`` sentinel is expanded to the real
|
|
1102
|
+
packaged path before the exec payload is built, so the payload sees a
|
|
1103
|
+
plain path and emits the profile plus the cap-gated SYS_CHROOT add."""
|
|
1104
|
+
from agentic_dev_loop.engine.isolation import startup_sandbox
|
|
1105
|
+
from agentic_dev_loop.engine.seccomp import chromium_userns_profile_path
|
|
1106
|
+
|
|
1107
|
+
ws = tmp_path / "ws"
|
|
1108
|
+
ws.mkdir()
|
|
1109
|
+
cfg = self._cfg(tmp_path, with_setup=False)
|
|
1110
|
+
cfg.isolation.seccomp_profile = "bundled:chromium-userns"
|
|
1111
|
+
monkeypatch.setattr("agentic_dev_loop.engine.isolation.shutil.which",
|
|
1112
|
+
lambda *a: "/usr/bin/docker")
|
|
1113
|
+
monkeypatch.setenv("AGENTIC_DEV_LOOP_DEV_IMAGE", "img:tag")
|
|
1114
|
+
runner = FakeRunner(
|
|
1115
|
+
ok("1.54"), # probe version
|
|
1116
|
+
ok("linux"), # probe ostype
|
|
1117
|
+
ok(""), # image inspect
|
|
1118
|
+
ok(""), # docker run -d --rm (exec container)
|
|
1119
|
+
)
|
|
1120
|
+
sandbox = startup_sandbox(cfg, workspace=ws, runner=runner)
|
|
1121
|
+
try:
|
|
1122
|
+
run_argv = [c for c in runner.calls if c[:2] == ["docker", "run"]][0]
|
|
1123
|
+
resolved = chromium_userns_profile_path()
|
|
1124
|
+
assert "--security-opt" in run_argv
|
|
1125
|
+
assert run_argv[run_argv.index("--security-opt") + 1] == f"seccomp={resolved}"
|
|
1126
|
+
assert "--cap-add" in run_argv
|
|
1127
|
+
assert run_argv[run_argv.index("--cap-add") + 1] == "SYS_CHROOT"
|
|
1128
|
+
finally:
|
|
1129
|
+
sandbox.close()
|
|
1130
|
+
|
|
1131
|
+
def test_unknown_bundled_seccomp_name_fails_before_docker_probe(self, tmp_path, monkeypatch):
|
|
1132
|
+
"""025: an unknown bundled:<name> fails loud at startup, before any
|
|
1133
|
+
Docker probe runs."""
|
|
1134
|
+
from agentic_dev_loop.engine.isolation import startup_sandbox
|
|
1135
|
+
|
|
1136
|
+
ws = tmp_path / "ws"
|
|
1137
|
+
ws.mkdir()
|
|
1138
|
+
cfg = self._cfg(tmp_path, with_setup=False)
|
|
1139
|
+
cfg.isolation.seccomp_profile = "bundled:unknown-name"
|
|
1140
|
+
monkeypatch.setattr("agentic_dev_loop.engine.isolation.shutil.which",
|
|
1141
|
+
lambda *a: "/usr/bin/docker")
|
|
1142
|
+
monkeypatch.setenv("AGENTIC_DEV_LOOP_DEV_IMAGE", "img:tag")
|
|
1143
|
+
runner = FakeRunner()
|
|
1144
|
+
with pytest.raises(ValueError, match="bundled:chromium-userns"):
|
|
1145
|
+
startup_sandbox(cfg, workspace=ws, runner=runner)
|
|
1146
|
+
assert runner.calls == []
|
|
1147
|
+
|
|
1100
1148
|
def test_in_workspace_sandbox_venv_dir_fails_loud(self, tmp_path, monkeypatch):
|
|
1101
1149
|
from agentic_dev_loop.engine.isolation import startup_sandbox
|
|
1102
1150
|
|
|
@@ -1155,3 +1203,60 @@ class TestBundledSeccompProfile:
|
|
|
1155
1203
|
path = Path(chromium_userns_profile_path())
|
|
1156
1204
|
assert path.is_file()
|
|
1157
1205
|
assert json.loads(path.read_text(encoding="utf-8"))["defaultAction"] == "SCMP_ACT_ERRNO"
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
class TestResolveSeccompProfile:
|
|
1209
|
+
"""025: the ``bundled:<name>`` sentinel resolves to a packaged profile
|
|
1210
|
+
path; raw host paths pass through unchanged; unknown names fail loud with
|
|
1211
|
+
every known bundled name."""
|
|
1212
|
+
|
|
1213
|
+
def test_bundled_sentinel_resolves_to_real_path(self):
|
|
1214
|
+
from agentic_dev_loop.engine.seccomp import (chromium_userns_profile_path,
|
|
1215
|
+
resolve_seccomp_profile)
|
|
1216
|
+
|
|
1217
|
+
resolved = resolve_seccomp_profile("bundled:chromium-userns")
|
|
1218
|
+
assert resolved == chromium_userns_profile_path()
|
|
1219
|
+
assert Path(resolved).is_file()
|
|
1220
|
+
|
|
1221
|
+
def test_unknown_bundled_name_lists_known(self):
|
|
1222
|
+
from agentic_dev_loop.engine.seccomp import resolve_seccomp_profile
|
|
1223
|
+
|
|
1224
|
+
with pytest.raises(ValueError, match="bundled:chromium-userns"):
|
|
1225
|
+
resolve_seccomp_profile("bundled:unknown-name")
|
|
1226
|
+
|
|
1227
|
+
def test_empty_bundled_name_lists_known(self):
|
|
1228
|
+
from agentic_dev_loop.engine.seccomp import resolve_seccomp_profile
|
|
1229
|
+
|
|
1230
|
+
with pytest.raises(ValueError, match="bundled:chromium-userns"):
|
|
1231
|
+
resolve_seccomp_profile("bundled:")
|
|
1232
|
+
|
|
1233
|
+
def test_none_passes_through(self):
|
|
1234
|
+
from agentic_dev_loop.engine.seccomp import resolve_seccomp_profile
|
|
1235
|
+
|
|
1236
|
+
assert resolve_seccomp_profile(None) is None
|
|
1237
|
+
|
|
1238
|
+
def test_raw_path_passes_through(self):
|
|
1239
|
+
from agentic_dev_loop.engine.seccomp import resolve_seccomp_profile
|
|
1240
|
+
|
|
1241
|
+
assert resolve_seccomp_profile(
|
|
1242
|
+
"/host/chromium-userns.json") == "/host/chromium-userns.json"
|
|
1243
|
+
|
|
1244
|
+
def test_bundled_profile_path_unknown_name(self):
|
|
1245
|
+
from agentic_dev_loop.engine.seccomp import bundled_profile_path
|
|
1246
|
+
|
|
1247
|
+
with pytest.raises(ValueError, match="bundled:chromium-userns"):
|
|
1248
|
+
bundled_profile_path("nope")
|
|
1249
|
+
|
|
1250
|
+
def test_sentinel_resolves_into_payload(self):
|
|
1251
|
+
"""025: the resolved sentinel path reaches ``docker_create_payload``
|
|
1252
|
+
unchanged, producing the profile security-opt and the cap-add."""
|
|
1253
|
+
from agentic_dev_loop.engine.seccomp import resolve_seccomp_profile
|
|
1254
|
+
|
|
1255
|
+
resolved = resolve_seccomp_profile("bundled:chromium-userns")
|
|
1256
|
+
payload = docker_create_payload(
|
|
1257
|
+
run_id="r1", image="img",
|
|
1258
|
+
profile=EffectiveProfile(True, True, True, True, True,
|
|
1259
|
+
"img", "4g", 2.0),
|
|
1260
|
+
workspace="/ws", seccomp_profile=resolved)
|
|
1261
|
+
assert payload[payload.index("--security-opt") + 1] == f"seccomp={resolved}"
|
|
1262
|
+
assert payload[payload.index("--cap-add") + 1] == "SYS_CHROOT"
|
|
@@ -27,3 +27,11 @@ def test_unverifiable_verdict_rules_present():
|
|
|
27
27
|
|
|
28
28
|
def test_inconclusive_budget_rule_retained():
|
|
29
29
|
assert "INCONCLUSIVE" in _text()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_probe_plan_rules_present():
|
|
33
|
+
text = _text()
|
|
34
|
+
assert "**Variant matrix.**" in text
|
|
35
|
+
assert "**Outcome probes.**" in text
|
|
36
|
+
assert "**Concurrency probes.**" in text
|
|
37
|
+
assert "**Error messages are assertions.**" in text
|
|
@@ -38,7 +38,7 @@ def _cfg_with_secrets(tmp_path) -> PipelineConfig:
|
|
|
38
38
|
data = _base()
|
|
39
39
|
data["secrets"] = {
|
|
40
40
|
"kinde-client-secret": {"from": "env", "key": "KINDIE_CLIENT_SECRET"},
|
|
41
|
-
"r2-keys": {"from": "env", "key": "
|
|
41
|
+
"r2-keys": {"from": "env", "key": "R2_KEYS"},
|
|
42
42
|
}
|
|
43
43
|
data["steps"] = [
|
|
44
44
|
{"name": "qa", "agent": "qa", "secrets": ["kinde-client-secret"],
|
|
@@ -127,7 +127,7 @@ class TestProviderResolution:
|
|
|
127
127
|
def test_resolve_secrets_returns_ref_keyed_values(self, tmp_path):
|
|
128
128
|
cfg = _cfg_with_secrets(tmp_path)
|
|
129
129
|
resolved = resolve_secrets(cfg, {"KINDIE_CLIENT_SECRET": "kinde-secret",
|
|
130
|
-
"
|
|
130
|
+
"R2_KEYS": "r2-keys"})
|
|
131
131
|
assert resolved == {"kinde-client-secret": "kinde-secret",
|
|
132
132
|
"r2-keys": "r2-keys"}
|
|
133
133
|
|
|
@@ -137,14 +137,14 @@ class TestProviderResolution:
|
|
|
137
137
|
resolve_secrets(cfg, {"KINDIE_CLIENT_SECRET": "kinde-secret"})
|
|
138
138
|
msg = str(e.value)
|
|
139
139
|
assert "r2-keys" in msg
|
|
140
|
-
assert "
|
|
140
|
+
assert "R2_KEYS" in msg
|
|
141
141
|
assert "env" in msg
|
|
142
142
|
assert "['<none>']" in msg # r2-keys is declared but unused by any step
|
|
143
143
|
|
|
144
144
|
def test_resolve_secrets_missing_used_ref_names_step(self, tmp_path):
|
|
145
145
|
cfg = _cfg_with_secrets(tmp_path)
|
|
146
146
|
with pytest.raises(SecretResolutionError) as e:
|
|
147
|
-
resolve_secrets(cfg, {"
|
|
147
|
+
resolve_secrets(cfg, {"R2_KEYS": "r2-keys"})
|
|
148
148
|
assert "kinde-client-secret" in str(e.value)
|
|
149
149
|
assert "['qa']" in str(e.value)
|
|
150
150
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Stage nodes, routers, context assembly, and the run_agent port.
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The consumer hardcoded stages (dev/test/review/qa + bump) become per-step node
|
|
4
4
|
and router closures built from the pipeline config; the fixed policies
|
|
5
5
|
(hygiene, verdict-first truncation, unclear-verdict retry, notes) stay here."""
|
|
6
6
|
|
|
@@ -17,8 +17,9 @@ from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
|
17
17
|
from agentic_dev_loop.core.config import PipelineConfig
|
|
18
18
|
from agentic_dev_loop.engine.registry import Registry, default_registry
|
|
19
19
|
from agentic_dev_loop.engine.stages import (
|
|
20
|
-
Runtime, _run_command, build_context,
|
|
21
|
-
|
|
20
|
+
ARTIFACT_CONTEXT_CAP, Runtime, _run_command, build_context,
|
|
21
|
+
make_action_node, make_agent_node, make_bump_node, make_command_node,
|
|
22
|
+
make_router, run_agent,
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
|
|
@@ -53,7 +54,7 @@ def runtime(tmp_path, fake_sandbox):
|
|
|
53
54
|
|
|
54
55
|
def make_state(**over):
|
|
55
56
|
state = {"subject": "plan plan.md", "docs": {"plan.md": "THE PLAN"},
|
|
56
|
-
"
|
|
57
|
+
"artifacts": {}, "attempt": 1, "max_attempts": 3, "notes": [],
|
|
57
58
|
"failed": False, "outcome": None, "commit_allowed": False,
|
|
58
59
|
"step_verdicts": {}, "retry_target": None, "thread_id": "t1"}
|
|
59
60
|
state.update(over)
|
|
@@ -78,7 +79,7 @@ class TestBuildContext:
|
|
|
78
79
|
assert "## Previous attempt feedback\n\nnote one\n\nnote two" in ctx
|
|
79
80
|
|
|
80
81
|
def test_output_paths_workspace_relative_for_subdirectory_pipeline(self, tmp_path):
|
|
81
|
-
"""
|
|
82
|
+
"""consumer layout: the pipeline YAML lives in a subdirectory while the
|
|
82
83
|
workspace is the repo root - the prompt path must reach the real
|
|
83
84
|
state_dir; state_dir.name alone sends agents to the wrong directory
|
|
84
85
|
(U18 burned 3 attempts on an unfindable ci-fix.md)."""
|
|
@@ -114,6 +115,27 @@ class TestBuildContext:
|
|
|
114
115
|
assert "dev agent writes:" in ctx
|
|
115
116
|
assert "test agent writes:" not in ctx
|
|
116
117
|
|
|
118
|
+
def test_artifacts_render_one_subsection_per_artifact_in_order(self, runtime):
|
|
119
|
+
ctx = build_context(runtime, {}, "s", [],
|
|
120
|
+
artifacts={"dev": "summary text",
|
|
121
|
+
"review": "findings text"})
|
|
122
|
+
assert "## Stage artifacts" in ctx
|
|
123
|
+
assert "### dev\n\nsummary text" in ctx
|
|
124
|
+
assert "### review\n\nfindings text" in ctx
|
|
125
|
+
assert ctx.index("### dev") < ctx.index("### review") # dict order
|
|
126
|
+
|
|
127
|
+
def test_artifact_content_capped_with_run_state_path_marker(self, runtime):
|
|
128
|
+
big = "x" * (ARTIFACT_CONTEXT_CAP + 100)
|
|
129
|
+
ctx = build_context(runtime, {}, "s", [], artifacts={"dev": big})
|
|
130
|
+
assert "x" * ARTIFACT_CONTEXT_CAP in ctx
|
|
131
|
+
assert "x" * (ARTIFACT_CONTEXT_CAP + 100) not in ctx
|
|
132
|
+
assert ("[... content truncated - full text at "
|
|
133
|
+
"`.pr/implementation-summary.md`]") in ctx
|
|
134
|
+
|
|
135
|
+
def test_no_artifacts_renders_no_section(self, runtime):
|
|
136
|
+
ctx = build_context(runtime, {}, "s", [])
|
|
137
|
+
assert "## Stage artifacts" not in ctx
|
|
138
|
+
|
|
117
139
|
|
|
118
140
|
class TestDepsPolicyContext:
|
|
119
141
|
def test_locked_policy_injected(self, runtime):
|
|
@@ -182,6 +204,40 @@ class TestAgentNode:
|
|
|
182
204
|
(note,) = update["notes"]
|
|
183
205
|
assert "broken build" in note and "attempt 2" in note
|
|
184
206
|
|
|
207
|
+
def test_no_verdicts_branch_captures_artifact(self, runtime, fake_agents):
|
|
208
|
+
fake_agents.set("dev", [("write", "implementation-summary.md", "MARKER-DEV")])
|
|
209
|
+
update = make_agent_node(runtime, runtime.cfg.steps[0])(make_state())
|
|
210
|
+
assert update["artifacts"] == {"dev": "MARKER-DEV"}
|
|
211
|
+
|
|
212
|
+
def test_pass_verdict_captures_artifact(self, runtime, fake_agents):
|
|
213
|
+
fake_agents.set("check", [("write", "ci-fix.md", "MARKER-CHECK\nVERDICT: PASS\n")])
|
|
214
|
+
update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
|
|
215
|
+
assert update["artifacts"] == {"check": "MARKER-CHECK\nVERDICT: PASS\n"}
|
|
216
|
+
|
|
217
|
+
def test_fail_verdict_captures_artifact(self, runtime, fake_agents):
|
|
218
|
+
fake_agents.set("check", [("write", "ci-fix.md", "MARKER-FAIL\nVERDICT: FAIL\n")])
|
|
219
|
+
update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
|
|
220
|
+
assert update["artifacts"] == {"check": "MARKER-FAIL\nVERDICT: FAIL\n"}
|
|
221
|
+
|
|
222
|
+
def test_unverifiable_verdict_captures_artifact(self, runtime, fake_agents):
|
|
223
|
+
step = make_cfg([
|
|
224
|
+
{"name": "qa", "agent": "qa", "artifact": "qa-report.md",
|
|
225
|
+
"verdicts": {"pass": "PASS", "fail": "FAIL",
|
|
226
|
+
"unverifiable": "UNVERIFIABLE"},
|
|
227
|
+
"on_pass": "success"}]).steps[0]
|
|
228
|
+
fake_agents.set("qa", [("write", "qa-report.md", "MARKER-UNVER\nVERDICT: UNVERIFIABLE\n")])
|
|
229
|
+
update = make_agent_node(runtime, step)(make_state())
|
|
230
|
+
assert update["artifacts"] == {"qa": "MARKER-UNVER\nVERDICT: UNVERIFIABLE\n"}
|
|
231
|
+
|
|
232
|
+
def test_capture_error_branch_returns_no_artifacts(self, runtime, fake_agents):
|
|
233
|
+
update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
|
|
234
|
+
assert "artifacts" not in update
|
|
235
|
+
|
|
236
|
+
def test_unclear_branch_returns_no_artifacts(self, runtime, fake_agents):
|
|
237
|
+
fake_agents.set("check", [("write", "ci-fix.md", "undecided")])
|
|
238
|
+
update = make_agent_node(runtime, runtime.cfg.steps[1])(make_state())
|
|
239
|
+
assert "artifacts" not in update
|
|
240
|
+
|
|
185
241
|
def test_verdicts_merge_with_prior_steps(self, runtime, fake_agents):
|
|
186
242
|
fake_agents.set("check", [("write", "ci-fix.md", "VERDICT: PASS")])
|
|
187
243
|
state = make_state(step_verdicts={"dev": "pass"})
|
|
@@ -528,14 +584,11 @@ class TestBumpNode:
|
|
|
528
584
|
def test_consumes_attempt_resets_verdicts_rebuilds_cleans(self, runtime):
|
|
529
585
|
for stale in ("ci-fix.md", "implementation-summary.md", "last-error.md"):
|
|
530
586
|
(runtime.state_dir / stale).write_text("stale")
|
|
531
|
-
state = make_state(attempt=1, notes=["N1"],
|
|
587
|
+
state = make_state(attempt=1, notes=["N1"], artifacts={"dev": "D1"},
|
|
532
588
|
step_verdicts={"check": "fail"})
|
|
533
589
|
update = make_bump_node(runtime)(state)
|
|
534
|
-
assert update
|
|
535
|
-
assert
|
|
536
|
-
assert update["failed"] is False
|
|
537
|
-
assert "## Previous attempt feedback\n\nN1" in update["context"]
|
|
538
|
-
assert "## plan.md\n\nTHE PLAN" in update["context"]
|
|
590
|
+
assert update == {"attempt": 2, "step_verdicts": {}, "failed": False}
|
|
591
|
+
assert "context" not in update # message is rebuilt per run (027)
|
|
539
592
|
for stale in ("ci-fix.md", "implementation-summary.md", "last-error.md"):
|
|
540
593
|
assert not (runtime.state_dir / stale).exists()
|
|
541
594
|
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
"""Bundled Docker seccomp profiles (023).
|
|
2
|
-
|
|
3
|
-
Docker's default seccomp profile blocks the user-namespace syscalls Chromium's
|
|
4
|
-
namespace sandbox needs, which makes ``chromium.launch(chromiumSandbox=True)``
|
|
5
|
-
die at the zygote under ``--cap-drop ALL``. These profiles are Docker's default
|
|
6
|
-
profile with an allowlist for those userns syscalls; pass one as
|
|
7
|
-
``isolation.seccomp_profile`` to swap it in.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
from importlib.resources import as_file, files
|
|
13
|
-
|
|
14
|
-
CHROMIUM_USERNS_PROFILE = "chromium-userns.json"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def chromium_userns_profile_path() -> str:
|
|
18
|
-
"""Host filesystem path to the bundled Chromium userns seccomp profile.
|
|
19
|
-
|
|
20
|
-
Docker's ``--security-opt seccomp=<path>`` takes a host path, so the JSON
|
|
21
|
-
must resolve to a real file. Wheels unpack to disk, so ``as_file`` returns
|
|
22
|
-
the installed path without copying.
|
|
23
|
-
"""
|
|
24
|
-
traversable = files("agentic_dev_loop.engine.seccomp").joinpath(
|
|
25
|
-
CHROMIUM_USERNS_PROFILE)
|
|
26
|
-
with as_file(traversable) as path:
|
|
27
|
-
return str(path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/results.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/core/verdict.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/loaders.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/registry.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/engine/secrets.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/__init__.py
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop/viz/__main__.py
RENAMED
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/requires.txt
RENAMED
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/src/agentic_dev_loop.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agentic_dev_loop-1.0.0a15 → agentic_dev_loop-1.0.0a17}/tests/test_hostboundary_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|