agentic-dev-loop 0.10.0__tar.gz → 0.10.3__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.
- {agentic_dev_loop-0.10.0/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.10.3}/PKG-INFO +58 -13
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/README.md +57 -12
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/pyproject.toml +3 -1
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/core/config.py +19 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/core/profiles/__init__.py +178 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/core/profiles/data/dotnet-node.yaml +22 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/core/profiles/data/python.yaml +8 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/cli.py +6 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/forbidden.py +12 -26
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/hostboundary.py +3 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/__init__.py +6 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/checks.py +272 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/doctor.py +84 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/init.py +432 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/recipes/dotnet-node.yaml +118 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/recipes/python.yaml +78 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/recipes.py +77 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/templates/dev.agent.md +133 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/templates/env.example +11 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/templates/qa.agent.md +181 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/templates/review.agent.md +53 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/templates/trigger-agentic-dev-loop.sh +97 -0
- agentic_dev_loop-0.10.3/src/agentic_dev_loop/engine/onboarding/templates.py +62 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/stages.py +1 -2
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3/src/agentic_dev_loop.egg-info}/PKG-INFO +58 -13
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop.egg-info/SOURCES.txt +24 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_036_banned_commands_and_lane_boundary.py +7 -43
- agentic_dev_loop-0.10.3/tests/test_055_cli.py +51 -0
- agentic_dev_loop-0.10.3/tests/test_055_doctor.py +163 -0
- agentic_dev_loop-0.10.3/tests/test_055_init.py +135 -0
- agentic_dev_loop-0.10.3/tests/test_055_profile_loading.py +73 -0
- agentic_dev_loop-0.10.3/tests/test_055_profiles.py +156 -0
- agentic_dev_loop-0.10.3/tests/test_055_recipes.py +90 -0
- agentic_dev_loop-0.10.3/tests/test_055_schema_lock.py +42 -0
- agentic_dev_loop-0.10.3/tests/test_055_templates.py +80 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_hostboundary.py +10 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_module_contract.py +20 -1
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_publish.py +38 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_trigger_script.py +84 -32
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_viz.py +1 -1
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/LICENSE +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/setup.cfg +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/agents.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/isolation.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_033_brief_step.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_041_brief_coverage.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_046_brief_prompt.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_actions.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_agent_error_guide.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_agents.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_cli.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_config.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_containers.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_deps.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_graph.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_llm.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_proc.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_registry.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_results.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_stages.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/tests/test_verdict.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 0.10.
|
|
3
|
+
Version: 0.10.3
|
|
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
|
|
@@ -229,6 +229,7 @@ steps:
|
|
|
229
229
|
agent: dev
|
|
230
230
|
produces: implementation-summary.md
|
|
231
231
|
terminal_timeout: 900
|
|
232
|
+
# forbidden_commands: ["npm install", "npm ci"] # per-step tool deny-list; engine defaults are git mutations + docker
|
|
232
233
|
on_pass: test
|
|
233
234
|
|
|
234
235
|
- name: test
|
|
@@ -268,6 +269,7 @@ steps:
|
|
|
268
269
|
| `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). `name` stays free-form, so multiple brief steps are allowed. |
|
|
269
270
|
| `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. |
|
|
270
271
|
| `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). |
|
|
272
|
+
| `forbidden_commands:` | Optional per-step list of regex patterns. The sandbox terminal matches them against each agent command before any shell spawn; a match returns a structured `[forbidden: <label>]` observation instead of running. A single string is coerced to a one-item list; an invalid regex fails at load. Defaults are run invariants only (git mutations, docker); tool-specific rules belong to the toolchain profile/recipe or the pipeline author. Command steps are unaffected. See [Banned commands](#banned-commands-forbidden_commands). |
|
|
271
273
|
| `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. |
|
|
272
274
|
| `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
|
|
273
275
|
| `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
|
|
@@ -275,6 +277,7 @@ steps:
|
|
|
275
277
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
276
278
|
| `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). |
|
|
277
279
|
| `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). |
|
|
280
|
+
| `profile:` (top-level) | Names a bundled toolchain profile (`python`, `dotnet-node`) or a consumer profile file path. `load_config` merges the profile at load time: isolation key-wise with the pipeline winning, caches unioned profile-first. Absent = today's behavior. |
|
|
278
281
|
| `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). |
|
|
279
282
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
|
|
280
283
|
| `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. |
|
|
@@ -283,6 +286,22 @@ steps:
|
|
|
283
286
|
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
284
287
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
285
288
|
|
|
289
|
+
### Banned commands (`forbidden_commands:`)
|
|
290
|
+
|
|
291
|
+
`forbidden_commands:` is a per-step list of regex patterns. The sandbox
|
|
292
|
+
terminal checks them against every command the agent runs, before any shell
|
|
293
|
+
spawn: a match returns a structured `[forbidden: <label>]` observation instead
|
|
294
|
+
of executing. The engine defaults are run invariants only - git mutations and
|
|
295
|
+
docker - so the deny-list is network-independent by design: `network:` stays a
|
|
296
|
+
docker/egress concern. A single string is coerced to a one-item list; an
|
|
297
|
+
invalid regex is a load error. Command steps bypass the filter by design.
|
|
298
|
+
|
|
299
|
+
Tool-specific rules are not engine defaults. A toolchain profile or bundled
|
|
300
|
+
recipe declares them per step (for example the `python` recipe denies
|
|
301
|
+
`pip install`, `uv add`, and `pip-compile` on its agent steps), and a
|
|
302
|
+
consumer pipeline authors its own for its toolchain. There is no disable
|
|
303
|
+
escape hatch for the engine defaults.
|
|
304
|
+
|
|
286
305
|
### Reasoning effort (opt-in)
|
|
287
306
|
|
|
288
307
|
`reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
|
|
@@ -446,12 +465,39 @@ console.
|
|
|
446
465
|
|
|
447
466
|
## Authoring a loop for your own repo
|
|
448
467
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
468
|
+
### Start with `agentic-dev-loop init`
|
|
469
|
+
|
|
470
|
+
`init` writes the onboarding surface from a bundled toolchain profile:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
agentic-dev-loop init --profile python # or: --profile dotnet-node
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
It detects solution files, `package.json` directories, lockfiles, README, and
|
|
477
|
+
AGENTS.md, asks only what detection cannot answer, and writes
|
|
478
|
+
`agentic-dev-loop/pipeline.yaml`, the three agent files, an executable
|
|
479
|
+
`trigger-agentic-dev-loop.sh`, `agentic-dev-loop/.engine-version` (the exact
|
|
480
|
+
installed engine version), any missing premise files, gitignore additions, and
|
|
481
|
+
an env-file template. It refuses to overwrite an existing
|
|
482
|
+
`agentic-dev-loop/` directory. A consumer-authored profile file can be passed
|
|
483
|
+
by path instead of a bundled name.
|
|
484
|
+
|
|
485
|
+
Profiles carry the environment facts every app on a toolchain shares (image,
|
|
486
|
+
cpus, caches, premise files, gitignore lines); recipes carry the gate shape and
|
|
487
|
+
command vocabulary. The pipeline references a profile with the top-level
|
|
488
|
+
`profile:` key, and `load_config` merges it at load time: the pipeline wins on
|
|
489
|
+
scalars, caches union profile-first, and absence of the key behaves exactly as
|
|
490
|
+
before.
|
|
491
|
+
|
|
492
|
+
`agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
|
|
493
|
+
Hard failures abort the run (engine pin mismatch, engine runtime availability,
|
|
494
|
+
API version, git repo, `.env*` files); warnings print and continue. `doctor
|
|
495
|
+
--json` emits the same results mechanically. The trigger repairs a pin/install
|
|
496
|
+
mismatch exactly once, then fails hard.
|
|
497
|
+
|
|
498
|
+
A consumer repo needs a pipeline file and the agent files it names, both
|
|
499
|
+
committed before the first run (the host-boundary check refuses dirty tooling,
|
|
500
|
+
and the `commit` step never commits it). `init` writes them; the layout is:
|
|
455
501
|
|
|
456
502
|
```
|
|
457
503
|
<repo>/agentic-dev-loop/pipeline.yaml
|
|
@@ -537,12 +583,11 @@ Adaptation checklist for a new repo:
|
|
|
537
583
|
|
|
538
584
|
1. `llm:` - model, `base_url`, `api_key_env`.
|
|
539
585
|
2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
|
|
540
|
-
3. `
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
4. `caches:` - toolchain
|
|
544
|
-
(`.nuget-packages`, `qa
|
|
545
|
-
`frontend-editor/node_modules`); caches are never synced back, and a
|
|
586
|
+
3. `profile:` - name a bundled profile (`python`, `dotnet-node`) or a
|
|
587
|
+
consumer profile path. The profile supplies `isolation:` (image,
|
|
588
|
+
`seccomp_profile`, cpus) and `caches:`; pipeline keys override its scalars.
|
|
589
|
+
4. `caches:` - profile-supplied toolchain caches plus any run-specific dirs
|
|
590
|
+
(`.nuget-packages`, `qa/.browsers`); caches are never synced back, and a
|
|
546
591
|
step's `setup:` hook runs with them mounted so it can warm them.
|
|
547
592
|
5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
|
|
548
593
|
6. `secrets:` - refs only, resolved from host env; each step opts in to the
|
|
@@ -185,6 +185,7 @@ steps:
|
|
|
185
185
|
agent: dev
|
|
186
186
|
produces: implementation-summary.md
|
|
187
187
|
terminal_timeout: 900
|
|
188
|
+
# forbidden_commands: ["npm install", "npm ci"] # per-step tool deny-list; engine defaults are git mutations + docker
|
|
188
189
|
on_pass: test
|
|
189
190
|
|
|
190
191
|
- name: test
|
|
@@ -224,6 +225,7 @@ steps:
|
|
|
224
225
|
| `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). `name` stays free-form, so multiple brief steps are allowed. |
|
|
225
226
|
| `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. |
|
|
226
227
|
| `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). |
|
|
228
|
+
| `forbidden_commands:` | Optional per-step list of regex patterns. The sandbox terminal matches them against each agent command before any shell spawn; a match returns a structured `[forbidden: <label>]` observation instead of running. A single string is coerced to a one-item list; an invalid regex fails at load. Defaults are run invariants only (git mutations, docker); tool-specific rules belong to the toolchain profile/recipe or the pipeline author. Command steps are unaffected. See [Banned commands](#banned-commands-forbidden_commands). |
|
|
227
229
|
| `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. |
|
|
228
230
|
| `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
|
|
229
231
|
| `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
|
|
@@ -231,6 +233,7 @@ steps:
|
|
|
231
233
|
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
232
234
|
| `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). |
|
|
233
235
|
| `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). |
|
|
236
|
+
| `profile:` (top-level) | Names a bundled toolchain profile (`python`, `dotnet-node`) or a consumer profile file path. `load_config` merges the profile at load time: isolation key-wise with the pipeline winning, caches unioned profile-first. Absent = today's behavior. |
|
|
234
237
|
| `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). |
|
|
235
238
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` - artifacts and checkpoints live outside the sandbox mount. |
|
|
236
239
|
| `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. |
|
|
@@ -239,6 +242,22 @@ steps:
|
|
|
239
242
|
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from host-side stores under `{state_dir}/caches/<name>` (auto-created); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names non-gitignored scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
240
243
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
241
244
|
|
|
245
|
+
### Banned commands (`forbidden_commands:`)
|
|
246
|
+
|
|
247
|
+
`forbidden_commands:` is a per-step list of regex patterns. The sandbox
|
|
248
|
+
terminal checks them against every command the agent runs, before any shell
|
|
249
|
+
spawn: a match returns a structured `[forbidden: <label>]` observation instead
|
|
250
|
+
of executing. The engine defaults are run invariants only - git mutations and
|
|
251
|
+
docker - so the deny-list is network-independent by design: `network:` stays a
|
|
252
|
+
docker/egress concern. A single string is coerced to a one-item list; an
|
|
253
|
+
invalid regex is a load error. Command steps bypass the filter by design.
|
|
254
|
+
|
|
255
|
+
Tool-specific rules are not engine defaults. A toolchain profile or bundled
|
|
256
|
+
recipe declares them per step (for example the `python` recipe denies
|
|
257
|
+
`pip install`, `uv add`, and `pip-compile` on its agent steps), and a
|
|
258
|
+
consumer pipeline authors its own for its toolchain. There is no disable
|
|
259
|
+
escape hatch for the engine defaults.
|
|
260
|
+
|
|
242
261
|
### Reasoning effort (opt-in)
|
|
243
262
|
|
|
244
263
|
`reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
|
|
@@ -402,12 +421,39 @@ console.
|
|
|
402
421
|
|
|
403
422
|
## Authoring a loop for your own repo
|
|
404
423
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
424
|
+
### Start with `agentic-dev-loop init`
|
|
425
|
+
|
|
426
|
+
`init` writes the onboarding surface from a bundled toolchain profile:
|
|
427
|
+
|
|
428
|
+
```bash
|
|
429
|
+
agentic-dev-loop init --profile python # or: --profile dotnet-node
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
It detects solution files, `package.json` directories, lockfiles, README, and
|
|
433
|
+
AGENTS.md, asks only what detection cannot answer, and writes
|
|
434
|
+
`agentic-dev-loop/pipeline.yaml`, the three agent files, an executable
|
|
435
|
+
`trigger-agentic-dev-loop.sh`, `agentic-dev-loop/.engine-version` (the exact
|
|
436
|
+
installed engine version), any missing premise files, gitignore additions, and
|
|
437
|
+
an env-file template. It refuses to overwrite an existing
|
|
438
|
+
`agentic-dev-loop/` directory. A consumer-authored profile file can be passed
|
|
439
|
+
by path instead of a bundled name.
|
|
440
|
+
|
|
441
|
+
Profiles carry the environment facts every app on a toolchain shares (image,
|
|
442
|
+
cpus, caches, premise files, gitignore lines); recipes carry the gate shape and
|
|
443
|
+
command vocabulary. The pipeline references a profile with the top-level
|
|
444
|
+
`profile:` key, and `load_config` merges it at load time: the pipeline wins on
|
|
445
|
+
scalars, caches union profile-first, and absence of the key behaves exactly as
|
|
446
|
+
before.
|
|
447
|
+
|
|
448
|
+
`agentic-dev-loop doctor --fast` is the per-run gate the trigger runs first.
|
|
449
|
+
Hard failures abort the run (engine pin mismatch, engine runtime availability,
|
|
450
|
+
API version, git repo, `.env*` files); warnings print and continue. `doctor
|
|
451
|
+
--json` emits the same results mechanically. The trigger repairs a pin/install
|
|
452
|
+
mismatch exactly once, then fails hard.
|
|
453
|
+
|
|
454
|
+
A consumer repo needs a pipeline file and the agent files it names, both
|
|
455
|
+
committed before the first run (the host-boundary check refuses dirty tooling,
|
|
456
|
+
and the `commit` step never commits it). `init` writes them; the layout is:
|
|
411
457
|
|
|
412
458
|
```
|
|
413
459
|
<repo>/agentic-dev-loop/pipeline.yaml
|
|
@@ -493,12 +539,11 @@ Adaptation checklist for a new repo:
|
|
|
493
539
|
|
|
494
540
|
1. `llm:` - model, `base_url`, `api_key_env`.
|
|
495
541
|
2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
|
|
496
|
-
3. `
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
4. `caches:` - toolchain
|
|
500
|
-
(`.nuget-packages`, `qa
|
|
501
|
-
`frontend-editor/node_modules`); caches are never synced back, and a
|
|
542
|
+
3. `profile:` - name a bundled profile (`python`, `dotnet-node`) or a
|
|
543
|
+
consumer profile path. The profile supplies `isolation:` (image,
|
|
544
|
+
`seccomp_profile`, cpus) and `caches:`; pipeline keys override its scalars.
|
|
545
|
+
4. `caches:` - profile-supplied toolchain caches plus any run-specific dirs
|
|
546
|
+
(`.nuget-packages`, `qa/.browsers`); caches are never synced back, and a
|
|
502
547
|
step's `setup:` hook runs with them mounted so it can warm them.
|
|
503
548
|
5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
|
|
504
549
|
6. `secrets:` - refs only, resolved from host env; each step opts in to the
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "0.10.
|
|
7
|
+
version = "0.10.3"
|
|
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"
|
|
@@ -76,8 +76,10 @@ where = ["src"]
|
|
|
76
76
|
|
|
77
77
|
[tool.setuptools.package-data]
|
|
78
78
|
agentic_dev_loop = ["py.typed"]
|
|
79
|
+
"agentic_dev_loop.core.profiles" = ["data/*.yaml"]
|
|
79
80
|
"agentic_dev_loop.engine" = ["*.md"]
|
|
80
81
|
"agentic_dev_loop.engine.seccomp" = ["*.json"]
|
|
82
|
+
"agentic_dev_loop.engine.onboarding" = ["recipes/*.yaml", "templates/*"]
|
|
81
83
|
|
|
82
84
|
[tool.pytest.ini_options]
|
|
83
85
|
markers = [
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
|
+
import importlib.metadata
|
|
6
7
|
import re
|
|
7
8
|
import warnings
|
|
8
9
|
from pathlib import Path
|
|
@@ -647,6 +648,9 @@ class PipelineConfig(BaseModel):
|
|
|
647
648
|
# edit instead of raising. Same declared-path vocabulary as
|
|
648
649
|
# ``sync_excludes:``.
|
|
649
650
|
merge_on_conflict: list[str] = []
|
|
651
|
+
# 055: the toolchain profile resolved at load time. Kept on the validated
|
|
652
|
+
# config so the run records which profile supplied its defaults.
|
|
653
|
+
profile: str | None = None
|
|
650
654
|
|
|
651
655
|
@field_validator("deps_policy")
|
|
652
656
|
@classmethod
|
|
@@ -892,6 +896,21 @@ def load_config(path: Path) -> PipelineConfig:
|
|
|
892
896
|
raise ValueError(
|
|
893
897
|
f"pipeline config must be a mapping, got {type(data).__name__}: {path}"
|
|
894
898
|
)
|
|
899
|
+
profile_ref = data.pop("profile", None)
|
|
900
|
+
if profile_ref is not None:
|
|
901
|
+
from agentic_dev_loop.core.profiles import (
|
|
902
|
+
ProfileError,
|
|
903
|
+
load_profile,
|
|
904
|
+
merge_profile,
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
try:
|
|
908
|
+
installed = importlib.metadata.version("agentic-dev-loop")
|
|
909
|
+
profile = load_profile(str(profile_ref), installed)
|
|
910
|
+
data = merge_profile(data, profile)
|
|
911
|
+
except ProfileError as exc:
|
|
912
|
+
raise ValueError(f"pipeline {path}: {exc}") from exc
|
|
913
|
+
data["profile"] = profile_ref
|
|
895
914
|
if "state_dir" not in data:
|
|
896
915
|
data["state_dir"] = default_state_dir(path)
|
|
897
916
|
cfg = PipelineConfig.model_validate(data)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Toolchain profiles: bundled data plus the load-time merge into a pipeline.
|
|
2
|
+
|
|
3
|
+
A profile supplies the environment facts every app on a toolchain shares
|
|
4
|
+
(isolation image, caches, premise files, gitignore lines, doctor probes).
|
|
5
|
+
Profiles never carry commands, gates, or steps - those live in
|
|
6
|
+
engine-resident recipes. The subpackage is intentionally dependency-light
|
|
7
|
+
(stdlib + pydantic + PyYAML) so ``core`` can resolve a profile at config
|
|
8
|
+
load time without importing the engine.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import yaml
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"PremiseFile",
|
|
22
|
+
"Profile",
|
|
23
|
+
"ProfileError",
|
|
24
|
+
"ProfileIsolation",
|
|
25
|
+
"bundled_profile_names",
|
|
26
|
+
"check_engine_min",
|
|
27
|
+
"load_profile",
|
|
28
|
+
"merge_profile",
|
|
29
|
+
"parse_version",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:(a|b|rc)(\d+))?$")
|
|
33
|
+
_RANK = {"a": 0, "b": 1, "rc": 2}
|
|
34
|
+
_FINAL = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ProfileError(ValueError):
|
|
38
|
+
"""Any profile load, parse, or compatibility failure."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PremiseFile(BaseModel):
|
|
42
|
+
model_config = ConfigDict(extra="forbid")
|
|
43
|
+
|
|
44
|
+
path: str
|
|
45
|
+
content: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ProfileIsolation(BaseModel):
|
|
49
|
+
model_config = ConfigDict(extra="forbid")
|
|
50
|
+
|
|
51
|
+
image: str
|
|
52
|
+
seccomp_profile: str | None = None
|
|
53
|
+
cpus: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Profile(BaseModel):
|
|
57
|
+
model_config = ConfigDict(extra="forbid")
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
engine_min: str
|
|
61
|
+
description: str = ""
|
|
62
|
+
isolation: ProfileIsolation
|
|
63
|
+
caches: list[str] = Field(default_factory=list)
|
|
64
|
+
premise_files: list[PremiseFile] = Field(default_factory=list)
|
|
65
|
+
gitignore_additions: list[str] = Field(default_factory=list)
|
|
66
|
+
doctor_checks: list = Field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_version(text: str) -> tuple[int, int, int, int, int]:
|
|
70
|
+
"""Parse ``X.Y.Z[a|b|rcN]`` into a directly comparable 5-tuple.
|
|
71
|
+
|
|
72
|
+
Rank ordering is alpha < beta < rc < final; the prerelease number is 0
|
|
73
|
+
for a final release so ``1.0.0rc9 < 1.0.0``.
|
|
74
|
+
"""
|
|
75
|
+
match = _VERSION_RE.match(text.strip())
|
|
76
|
+
if match is None:
|
|
77
|
+
raise ProfileError(f"unparseable version: {text!r}")
|
|
78
|
+
major, minor, patch, pre, num = match.groups()
|
|
79
|
+
if pre is None:
|
|
80
|
+
return (int(major), int(minor), int(patch), _FINAL, 0)
|
|
81
|
+
return (int(major), int(minor), int(patch), _RANK[pre], int(num))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_engine_min(engine_min: str, installed: str,
|
|
85
|
+
latest: str | None = None) -> None:
|
|
86
|
+
"""Raise unless ``installed`` satisfies ``engine_min``.
|
|
87
|
+
|
|
88
|
+
``latest`` is substituted into the fix line by the caller (doctor); when
|
|
89
|
+
omitted the floor itself is used.
|
|
90
|
+
"""
|
|
91
|
+
if parse_version(installed) < parse_version(engine_min):
|
|
92
|
+
target = latest or engine_min
|
|
93
|
+
raise ProfileError(
|
|
94
|
+
f"profile requires engine >= {engine_min}; installed {installed}. "
|
|
95
|
+
f'Upgrade: pipx install --force "agentic-dev-loop=={target}"')
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _data_dir():
|
|
99
|
+
return resources.files(__package__).joinpath("data")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def bundled_profile_names() -> list[str]:
|
|
103
|
+
return sorted(
|
|
104
|
+
entry.name[: -len(".yaml")]
|
|
105
|
+
for entry in _data_dir().iterdir()
|
|
106
|
+
if entry.name.endswith(".yaml")
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_yaml(text: str, source: str) -> dict:
|
|
111
|
+
try:
|
|
112
|
+
data = yaml.safe_load(text)
|
|
113
|
+
except yaml.YAMLError as exc:
|
|
114
|
+
raise ProfileError(f"profile {source!r}: invalid YAML: {exc}") from exc
|
|
115
|
+
if not isinstance(data, dict):
|
|
116
|
+
raise ProfileError(f"profile {source!r} must be a mapping")
|
|
117
|
+
return data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _validate(data: dict, source: str) -> Profile:
|
|
121
|
+
try:
|
|
122
|
+
return Profile.model_validate(data)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
raise ProfileError(f"profile {source!r}: {exc}") from exc
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def load_profile(reference: str, installed_version: str) -> Profile:
|
|
128
|
+
"""Load a bundled profile by name or a consumer profile by path.
|
|
129
|
+
|
|
130
|
+
Existence decides: a filesystem path that exists is read as a consumer
|
|
131
|
+
profile; anything else must name a bundled profile.
|
|
132
|
+
"""
|
|
133
|
+
candidate = Path(reference)
|
|
134
|
+
if candidate.is_file():
|
|
135
|
+
data = _read_yaml(candidate.read_text(encoding="utf-8"), reference)
|
|
136
|
+
profile = _validate(data, reference)
|
|
137
|
+
else:
|
|
138
|
+
names = bundled_profile_names()
|
|
139
|
+
if reference not in names:
|
|
140
|
+
raise ProfileError(
|
|
141
|
+
f"unknown profile '{reference}'; bundled profiles: "
|
|
142
|
+
+ ", ".join(names))
|
|
143
|
+
entry = _data_dir().joinpath(f"{reference}.yaml")
|
|
144
|
+
data = _read_yaml(entry.read_text(encoding="utf-8"), reference)
|
|
145
|
+
if data.get("name") != reference:
|
|
146
|
+
raise ProfileError(
|
|
147
|
+
f"profile {reference!r}: declared name {data.get('name')!r} "
|
|
148
|
+
f"does not match its filename")
|
|
149
|
+
profile = _validate(data, reference)
|
|
150
|
+
check_engine_min(profile.engine_min, installed_version)
|
|
151
|
+
return profile
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _dedupe(items: list[str]) -> list[str]:
|
|
155
|
+
seen: set[str] = set()
|
|
156
|
+
out: list[str] = []
|
|
157
|
+
for item in items:
|
|
158
|
+
if item not in seen:
|
|
159
|
+
seen.add(item)
|
|
160
|
+
out.append(item)
|
|
161
|
+
return out
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def merge_profile(data: dict, profile: Profile) -> dict:
|
|
165
|
+
"""Merge a profile into a pipeline dict; the pipeline wins.
|
|
166
|
+
|
|
167
|
+
Isolation merges key-wise (profile under, pipeline over). Caches union
|
|
168
|
+
profile-first, dedupe-preserving-order. Every other key is untouched.
|
|
169
|
+
"""
|
|
170
|
+
merged = dict(data)
|
|
171
|
+
profile_iso = profile.isolation.model_dump(exclude_none=True)
|
|
172
|
+
pipeline_iso = merged.get("isolation") or {}
|
|
173
|
+
if not isinstance(pipeline_iso, dict):
|
|
174
|
+
pipeline_iso = {}
|
|
175
|
+
merged["isolation"] = {**profile_iso, **pipeline_iso}
|
|
176
|
+
caches = _dedupe(list(profile.caches) + list(merged.get("caches") or []))
|
|
177
|
+
merged["caches"] = caches
|
|
178
|
+
return merged
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: dotnet-node
|
|
2
|
+
engine_min: 0.9.2
|
|
3
|
+
description: .NET backend with a Node.js frontend
|
|
4
|
+
isolation:
|
|
5
|
+
image: ghcr.io/lukedawilson/agentic-dev-loop/dev-dotnet@sha256:d15d1c6115fafc3d5f3bfb567338d61441081e5d7ff83d7770bc0b5f14f977fc
|
|
6
|
+
seccomp_profile: bundled:chromium-userns
|
|
7
|
+
cpus: 6
|
|
8
|
+
caches:
|
|
9
|
+
- .nuget-packages
|
|
10
|
+
premise_files:
|
|
11
|
+
- path: NuGet.Config
|
|
12
|
+
content: |
|
|
13
|
+
<configuration>
|
|
14
|
+
<config>
|
|
15
|
+
<add key="globalPackagesFolder" value=".nuget-packages" />
|
|
16
|
+
</config>
|
|
17
|
+
</configuration>
|
|
18
|
+
gitignore_additions:
|
|
19
|
+
- bin/
|
|
20
|
+
- obj/
|
|
21
|
+
- node_modules/
|
|
22
|
+
- .nuget-packages/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
name: python
|
|
2
|
+
engine_min: 0.9.2
|
|
3
|
+
description: Python project; the sandbox builds its own venv per run
|
|
4
|
+
isolation:
|
|
5
|
+
image: ghcr.io/lukedawilson/agentic-dev-loop/dev@sha256:890105209c127446caf20ce3dc05f0c0f5527e9d679e18cc0075e6b0ee917434
|
|
6
|
+
cpus: 2
|
|
7
|
+
gitignore_additions:
|
|
8
|
+
- .venv-sandbox/
|
|
@@ -165,6 +165,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
165
165
|
argv = list(argv)
|
|
166
166
|
if argv and argv[0] == "containers":
|
|
167
167
|
return _run_containers(argv[1:])
|
|
168
|
+
if argv and argv[0] == "init":
|
|
169
|
+
from .onboarding.init import run_init
|
|
170
|
+
return run_init(argv[1:])
|
|
171
|
+
if argv and argv[0] == "doctor":
|
|
172
|
+
from .onboarding.doctor import run_doctor
|
|
173
|
+
return run_doctor(argv[1:])
|
|
168
174
|
probe = argparse.ArgumentParser(add_help=False)
|
|
169
175
|
probe.add_argument("pipeline", type=Path, nargs="?")
|
|
170
176
|
_add_version_flag(probe)
|
{agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/forbidden.py
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Banned-command deny-list (036 D1).
|
|
1
|
+
"""Banned-command deny-list (036 D1, revised by 056).
|
|
2
2
|
|
|
3
3
|
The sandbox terminal enforces a deny-list before any shell spawn: a command
|
|
4
4
|
whose argv matches a step's ``forbidden_commands:`` regex (merged with the
|
|
@@ -6,11 +6,15 @@ engine defaults) returns a structured observation instead of reaching the
|
|
|
6
6
|
sandbox. Command steps are unaffected - they are declared infrastructure and
|
|
7
7
|
already fail fast on their exit code.
|
|
8
8
|
|
|
9
|
+
The defaults are run invariants only: git mutations and docker.
|
|
10
|
+
Tool-specific rules are not engine defaults - a step declares them with
|
|
11
|
+
``forbidden_commands`` (the bundled onboarding recipes carry the engine's
|
|
12
|
+
own toolchain guidance). The deny-list is network-independent by design;
|
|
13
|
+
network is a sandbox egress concern.
|
|
14
|
+
|
|
9
15
|
The default list carries human-readable labels; custom entries use their
|
|
10
|
-
regex text as the label.
|
|
11
|
-
gated: denied only while the step's ``network:`` flag is off.
|
|
16
|
+
regex text as the label.
|
|
12
17
|
"""
|
|
13
|
-
|
|
14
18
|
from __future__ import annotations
|
|
15
19
|
|
|
16
20
|
import re
|
|
@@ -30,22 +34,9 @@ _DEFAULT_PATTERNS: tuple[tuple[str, str], ...] = (
|
|
|
30
34
|
("git checkout", r"\bgit\s+checkout\b"),
|
|
31
35
|
("git reset", r"\bgit\s+reset\b"),
|
|
32
36
|
("git tag", r"\bgit\s+tag\b"),
|
|
33
|
-
("npm install", r"\bnpm\s+install\b"),
|
|
34
|
-
("npm ci", r"\bnpm\s+ci\b"),
|
|
35
|
-
("pip install", r"\bpip\s+install\b"),
|
|
36
|
-
("uv add", r"\buv\s+add\b"),
|
|
37
|
-
("pip-compile", r"\bpip-compile\b"),
|
|
38
|
-
("dotnet add package", r"\bdotnet\s+add\s+package\b"),
|
|
39
|
-
("npx playwright", r"\bnpx\s+playwright\b"),
|
|
40
|
-
("playwright", r"\bplaywright\b"),
|
|
41
37
|
("docker", r"\bdocker\b"),
|
|
42
38
|
)
|
|
43
39
|
|
|
44
|
-
_NETWORK_GATED_PATTERNS: tuple[tuple[str, str], ...] = (
|
|
45
|
-
("dotnet restore", r"\bdotnet\s+restore\b"),
|
|
46
|
-
("dotnet add", r"\bdotnet\s+add\b"),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
40
|
|
|
50
41
|
def _compile(label: str, pattern: str) -> ForbiddenRule:
|
|
51
42
|
return ForbiddenRule(label=label, regex=re.compile(pattern, re.IGNORECASE))
|
|
@@ -54,21 +45,16 @@ def _compile(label: str, pattern: str) -> ForbiddenRule:
|
|
|
54
45
|
DEFAULT_FORBIDDEN_RULES: tuple[ForbiddenRule, ...] = tuple(
|
|
55
46
|
_compile(label, pattern) for label, pattern in _DEFAULT_PATTERNS)
|
|
56
47
|
|
|
57
|
-
NETWORK_GATED_FORBIDDEN_RULES: tuple[ForbiddenRule, ...] = tuple(
|
|
58
|
-
_compile(label, pattern) for label, pattern in _NETWORK_GATED_PATTERNS)
|
|
59
|
-
|
|
60
48
|
|
|
61
|
-
def resolve_forbidden_rules(
|
|
62
|
-
|
|
49
|
+
def resolve_forbidden_rules(
|
|
50
|
+
custom_patterns: list[str] | tuple[str, ...] | None,
|
|
51
|
+
) -> tuple[ForbiddenRule, ...]:
|
|
63
52
|
"""Merge the engine defaults with a step's custom regex list.
|
|
64
53
|
|
|
65
54
|
Custom entries are appended after the defaults, so the first match wins
|
|
66
|
-
on overlap
|
|
67
|
-
off; there is no disable escape hatch for the engine defaults.
|
|
55
|
+
on overlap; there is no disable escape hatch for the engine defaults.
|
|
68
56
|
"""
|
|
69
57
|
rules = list(DEFAULT_FORBIDDEN_RULES)
|
|
70
|
-
if not network:
|
|
71
|
-
rules.extend(NETWORK_GATED_FORBIDDEN_RULES)
|
|
72
58
|
for pattern in custom_patterns or ():
|
|
73
59
|
rules.append(_compile(pattern, pattern))
|
|
74
60
|
return tuple(rules)
|
{agentic_dev_loop-0.10.0 → agentic_dev_loop-0.10.3}/src/agentic_dev_loop/engine/hostboundary.py
RENAMED
|
@@ -32,6 +32,8 @@ if TYPE_CHECKING:
|
|
|
32
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
|
+
TOOLING_FILE_NAMES = ("trigger-agentic-dev-loop.sh",
|
|
36
|
+
"agentic-dev-loop/.engine-version")
|
|
35
37
|
|
|
36
38
|
|
|
37
39
|
class HostBoundaryError(RuntimeError):
|
|
@@ -73,6 +75,7 @@ def tooling_rel_paths(cfg: "PipelineConfig", pipeline_path: str | Path,
|
|
|
73
75
|
(base / cfg.agents_dir).resolve(),
|
|
74
76
|
*((base / rel).resolve() for rel in cfg.additional_files),
|
|
75
77
|
*(ws / name for name in TOOLING_DIR_NAMES),
|
|
78
|
+
*(ws / name for name in TOOLING_FILE_NAMES),
|
|
76
79
|
]
|
|
77
80
|
rels = {p.relative_to(ws).as_posix() for p in candidates
|
|
78
81
|
if p.is_relative_to(ws)}
|