agentic-dev-loop 1.0.0a7__tar.gz → 1.0.0a9__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.

Files changed (76) hide show
  1. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/PKG-INFO +48 -59
  2. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/README.md +42 -50
  3. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/pyproject.toml +9 -22
  4. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/__init__.py +1 -8
  5. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/config.py +59 -0
  6. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/verdict.py +1 -1
  7. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/__init__.py +1 -1
  8. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/agents.py +7 -3
  9. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/dryrun.py +2 -0
  10. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/graph.py +10 -3
  11. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/isolation.py +10 -2
  12. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/outputs.py +7 -4
  13. agentic_dev_loop-1.0.0a9/src/agentic_dev_loop/engine/secrets.py +118 -0
  14. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/stages.py +19 -7
  15. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/viz/__init__.py +2 -3
  16. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/viz/__main__.py +2 -5
  17. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/viz/server.py +3 -36
  18. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/PKG-INFO +48 -59
  19. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/SOURCES.txt +5 -11
  20. agentic_dev_loop-1.0.0a9/src/agentic_dev_loop.egg-info/entry_points.txt +2 -0
  21. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/requires.txt +3 -8
  22. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_e2e_fixtures.py +5 -1
  23. agentic_dev_loop-1.0.0a9/tests/test_module_contract.py +94 -0
  24. agentic_dev_loop-1.0.0a9/tests/test_secrets.py +221 -0
  25. agentic_dev_loop-1.0.0a9/tests/test_secrets_integration.py +67 -0
  26. agentic_dev_loop-1.0.0a9/tests/test_secrets_pipeline.py +106 -0
  27. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_viz.py +2 -1
  28. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/core/project.py +0 -64
  29. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/core/projection.py +0 -206
  30. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/core/workflow.js.template +0 -236
  31. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/engine/commit_shim.py +0 -56
  32. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/viz/ingest.py +0 -25
  33. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/workflow/__init__.py +0 -80
  34. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop/workflow/cli.py +0 -40
  35. agentic_dev_loop-1.0.0a7/src/agentic_dev_loop.egg-info/entry_points.txt +0 -4
  36. agentic_dev_loop-1.0.0a7/tests/test_unit4_golden.py +0 -60
  37. agentic_dev_loop-1.0.0a7/tests/test_unit4_pbt.py +0 -283
  38. agentic_dev_loop-1.0.0a7/tests/test_unit4_viz_ingest.py +0 -92
  39. agentic_dev_loop-1.0.0a7/tests/test_unit4_workflow.py +0 -136
  40. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/LICENSE +0 -0
  41. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/setup.cfg +0 -0
  42. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/__init__.py +0 -0
  43. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/results.py +0 -0
  44. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/actions.py +0 -0
  45. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/cli.py +0 -0
  46. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  47. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  48. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/llm.py +0 -0
  49. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/loaders.py +0 -0
  50. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/proc.py +0 -0
  51. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/registry.py +0 -0
  52. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/py.typed +0 -0
  53. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  54. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  55. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_actions.py +0 -0
  56. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_agents.py +0 -0
  57. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_aidlc.py +0 -0
  58. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_cli.py +0 -0
  59. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_config.py +0 -0
  60. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_dry_run.py +0 -0
  61. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_graph.py +0 -0
  62. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_isolation.py +0 -0
  63. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_isolation_integration.py +0 -0
  64. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_isolation_pbt.py +0 -0
  65. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_llm.py +0 -0
  66. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_loaders.py +0 -0
  67. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_outputs.py +0 -0
  68. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_outputs_pbt.py +0 -0
  69. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_proc.py +0 -0
  70. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_publish.py +0 -0
  71. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_registry.py +0 -0
  72. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_results.py +0 -0
  73. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_sdk_compat.py +0 -0
  74. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_source_resolution.py +0 -0
  75. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_stages.py +0 -0
  76. {agentic_dev_loop-1.0.0a7 → agentic_dev_loop-1.0.0a9}/tests/test_verdict.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a7
3
+ Version: 1.0.0a9
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
@@ -27,15 +27,12 @@ Description-Content-Type: text/markdown
27
27
  License-File: LICENSE
28
28
  Requires-Dist: pydantic>=2.0
29
29
  Requires-Dist: PyYAML>=6.0
30
- Provides-Extra: engine
31
- Requires-Dist: openhands-sdk~=1.17.0; extra == "engine"
32
- Requires-Dist: openhands-tools~=1.17.0; extra == "engine"
33
- Requires-Dist: agent-client-protocol<1.0.0; extra == "engine"
34
- Requires-Dist: langgraph>=1.2; extra == "engine"
35
- Requires-Dist: langgraph-checkpoint-sqlite>=3.0; extra == "engine"
36
- Provides-Extra: workflow
30
+ Requires-Dist: openhands-sdk~=1.17.0
31
+ Requires-Dist: openhands-tools~=1.17.0
32
+ Requires-Dist: agent-client-protocol<1.0.0
33
+ Requires-Dist: langgraph>=1.2
34
+ Requires-Dist: langgraph-checkpoint-sqlite>=3.0
37
35
  Provides-Extra: dev
38
- Requires-Dist: agentic-dev-loop[engine]; extra == "dev"
39
36
  Requires-Dist: hypothesis>=6.0; extra == "dev"
40
37
  Requires-Dist: pytest>=8.0; extra == "dev"
41
38
  Dynamic: license-file
@@ -65,11 +62,6 @@ flowchart LR
65
62
  - **Sandboxed by default.** The agent's shell and every command step run inside
66
63
  an always-on Docker sandbox. There is no opt-out.
67
64
 
68
- Two modes share one `pipeline.yaml`: **engine mode** (the sandboxed, resumable
69
- loop above) and **workflow mode** — a Claude Dynamic Workflow exported from the
70
- same pipeline that runs in Claude Code's own environment, with the live graph
71
- visualiser. See [Workflow mode](#workflow-mode-claude-code).
72
-
73
65
  ---
74
66
 
75
67
  ## Core features
@@ -98,13 +90,8 @@ LLM timeout (agent runs are long).
98
90
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
99
91
 
100
92
  ```bash
101
- # engine mode (sandboxed loop — the full install)
102
- pip install "agentic-dev-loop[engine]==1.0.0a6"
93
+ pip install "agentic-dev-loop==1.0.0a8"
103
94
  agentic-dev-loop --version
104
-
105
- # workflow mode (Claude Code — core + viz only, no SDK/Docker deps)
106
- pip install "agentic-dev-loop==1.0.0a6"
107
- agentic-dev-loop-workflow install-skill
108
95
  ```
109
96
 
110
97
  On the first run the engine pulls the **digest-pinned** dev image
@@ -115,43 +102,6 @@ image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
115
102
  troubleshooting for every fail-fast code live in
116
103
  [docs/setup.md](docs/setup.md).
117
104
 
118
- ## Workflow mode (Claude Code)
119
-
120
- For developers who know and love the agentic dev loop but are forced to use
121
- Claude Code at work: workflow mode exports a **Claude Dynamic Workflow** from
122
- the `pipeline.yaml` they already trust, plus the visualiser they trust. It adds
123
- the deterministic loop through Claude's mechanism — nothing more. **No Docker
124
- sandboxing, and no new security claims**: workflow steps run in Claude Code's
125
- own environment under its permission model (a documented trust-model change
126
- from engine mode).
127
-
128
- Install the skill once:
129
-
130
- ```bash
131
- pip install "agentic-dev-loop==1.0.0a6" # base install: core + viz, no SDK/Docker
132
- agentic-dev-loop-workflow install-skill # → ~/.claude/skills/agentic-dev-loop/
133
- ```
134
-
135
- Then invoke `/agentic-dev-loop` in Claude Code. The skill walks through:
136
-
137
- 1. **Build** — `python -m agentic_dev_loop.core.project <pipeline.yaml>` loads +
138
- validates the pipeline, verifies every agent step's `.agent.md` prompt exists
139
- (a missing prompt fails the build loudly, naming the file), and writes a
140
- self-contained `.agentic-dev-loop/workflow.js`. Steps are frozen at creation
141
- — mid-run `pipeline.yaml` edits never affect a running workflow.
142
- 2. **Run** — the generated JS routes `on_pass` / `on_fail` / `goto` with its own
143
- attempt counter, injects the failing subagent's output as the next retry note,
144
- launches the live-graph server (localhost, token-authenticated ingest), and
145
- invokes the docker-free commit shim, which refuses loudly unless the worktree
146
- was clean at run start.
147
-
148
- Engine-only fields (`isolation:`, per-step `network:`, `setup:`) are ignored in
149
- workflow mode with no effect on the build or run.
150
-
151
- The base install is deliberately dependency-light (`pydantic` + `PyYAML` only);
152
- the heavy OpenHands-SDK engine lives in the `[engine]` extra. The `core` and
153
- `viz` packages are guarded against pulling engine deps.
154
-
155
105
  ## Usage
156
106
 
157
107
  ```bash
@@ -242,9 +192,49 @@ steps:
242
192
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
243
193
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
244
194
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` grants egress to a command or agent step (default off) — see [Per-stage networking](#per-stage-networking). |
195
+ | `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). |
245
196
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
246
197
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
247
198
 
199
+ ### Secrets (`secrets:`) — refs only, never values
200
+
201
+ Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
202
+ Declare the refs at the top level and opt each step into the refs it needs:
203
+
204
+ ```yaml
205
+ secrets: # refs only — never values
206
+ kinde-client-secret: # ref name (arbitrary)
207
+ from: env # provider name
208
+ key: KINDIE_CLIENT_SECRET # provider-specific locator
209
+ r2-keys:
210
+ from: env
211
+ key: YOSK_R2_KEYS
212
+
213
+ steps:
214
+ - name: qa
215
+ agent: qa
216
+ secrets: [kinde-client-secret, r2-keys]
217
+ ```
218
+
219
+ - The engine resolves every declared ref **host-side at run start** (the `env`
220
+ provider reads the named host environment variable). A missing variable, an
221
+ unknown provider, or a ref a step lists without declaring it fails fast —
222
+ naming the ref, the step, and the provider error. No silent fallback.
223
+ - A resolved secret is injected into a step's exec env **only when that step
224
+ lists the ref** — the same path step `env:` uses, no new channel. The secret
225
+ never reaches the provision container, `state_dir`, or any other step.
226
+ - Prefer a mechanical `command` step for secret-consuming work (a script runs
227
+ the app with creds) so the LLM and the secret never meet; an `agent` step
228
+ holds a secret only when its job genuinely requires it.
229
+ - **`.env*` ban.** At run start the workspace root is scanned for `.env*`
230
+ files. Any hit fails fast, naming the files, with the remedy text. The
231
+ convention fixtures `.env.example`, `.env.template`, and `.env.sample` are
232
+ whitelisted. This applies regardless of `network:` flags.
233
+ - **Output scrubbing.** Resolved secret values are scrubbed from every
234
+ engine-written output — captured step output, retry notes, artifacts, and
235
+ `state_dir` files. (Hygiene: the declaring step's agent has already seen
236
+ the value in its own env.)
237
+
248
238
  ### Per-stage networking
249
239
 
250
240
  The sandbox is network-off by default. A step that legitimately needs egress —
@@ -276,8 +266,7 @@ steps:
276
266
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
277
267
  cd agentic-dev-loop
278
268
  ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
279
- # create .env with your LLM key (loaded by trigger-agentic-dev-loop.sh, never committed):
280
- # echo 'OPENAI_API_KEY=...' > .env
269
+ export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
281
270
  python -m pytest tests/ -q # fast suite
282
271
  python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
283
272
  ./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
@@ -23,11 +23,6 @@ flowchart LR
23
23
  - **Sandboxed by default.** The agent's shell and every command step run inside
24
24
  an always-on Docker sandbox. There is no opt-out.
25
25
 
26
- Two modes share one `pipeline.yaml`: **engine mode** (the sandboxed, resumable
27
- loop above) and **workflow mode** — a Claude Dynamic Workflow exported from the
28
- same pipeline that runs in Claude Code's own environment, with the live graph
29
- visualiser. See [Workflow mode](#workflow-mode-claude-code).
30
-
31
26
  ---
32
27
 
33
28
  ## Core features
@@ -56,13 +51,8 @@ LLM timeout (agent runs are long).
56
51
  | LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
57
52
 
58
53
  ```bash
59
- # engine mode (sandboxed loop — the full install)
60
- pip install "agentic-dev-loop[engine]==1.0.0a6"
54
+ pip install "agentic-dev-loop==1.0.0a8"
61
55
  agentic-dev-loop --version
62
-
63
- # workflow mode (Claude Code — core + viz only, no SDK/Docker deps)
64
- pip install "agentic-dev-loop==1.0.0a6"
65
- agentic-dev-loop-workflow install-skill
66
56
  ```
67
57
 
68
58
  On the first run the engine pulls the **digest-pinned** dev image
@@ -73,43 +63,6 @@ image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
73
63
  troubleshooting for every fail-fast code live in
74
64
  [docs/setup.md](docs/setup.md).
75
65
 
76
- ## Workflow mode (Claude Code)
77
-
78
- For developers who know and love the agentic dev loop but are forced to use
79
- Claude Code at work: workflow mode exports a **Claude Dynamic Workflow** from
80
- the `pipeline.yaml` they already trust, plus the visualiser they trust. It adds
81
- the deterministic loop through Claude's mechanism — nothing more. **No Docker
82
- sandboxing, and no new security claims**: workflow steps run in Claude Code's
83
- own environment under its permission model (a documented trust-model change
84
- from engine mode).
85
-
86
- Install the skill once:
87
-
88
- ```bash
89
- pip install "agentic-dev-loop==1.0.0a6" # base install: core + viz, no SDK/Docker
90
- agentic-dev-loop-workflow install-skill # → ~/.claude/skills/agentic-dev-loop/
91
- ```
92
-
93
- Then invoke `/agentic-dev-loop` in Claude Code. The skill walks through:
94
-
95
- 1. **Build** — `python -m agentic_dev_loop.core.project <pipeline.yaml>` loads +
96
- validates the pipeline, verifies every agent step's `.agent.md` prompt exists
97
- (a missing prompt fails the build loudly, naming the file), and writes a
98
- self-contained `.agentic-dev-loop/workflow.js`. Steps are frozen at creation
99
- — mid-run `pipeline.yaml` edits never affect a running workflow.
100
- 2. **Run** — the generated JS routes `on_pass` / `on_fail` / `goto` with its own
101
- attempt counter, injects the failing subagent's output as the next retry note,
102
- launches the live-graph server (localhost, token-authenticated ingest), and
103
- invokes the docker-free commit shim, which refuses loudly unless the worktree
104
- was clean at run start.
105
-
106
- Engine-only fields (`isolation:`, per-step `network:`, `setup:`) are ignored in
107
- workflow mode with no effect on the build or run.
108
-
109
- The base install is deliberately dependency-light (`pydantic` + `PyYAML` only);
110
- the heavy OpenHands-SDK engine lives in the `[engine]` extra. The `core` and
111
- `viz` packages are guarded against pulling engine deps.
112
-
113
66
  ## Usage
114
67
 
115
68
  ```bash
@@ -200,9 +153,49 @@ steps:
200
153
  | `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
201
154
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
202
155
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps, network ON, stamp-cached); `network: true` grants egress to a command or agent step (default off) — see [Per-stage networking](#per-stage-networking). |
156
+ | `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). |
203
157
  | `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
204
158
  | `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
205
159
 
160
+ ### Secrets (`secrets:`) — refs only, never values
161
+
162
+ Secret values never belong in the pipeline yaml, the workspace, or `state_dir`.
163
+ Declare the refs at the top level and opt each step into the refs it needs:
164
+
165
+ ```yaml
166
+ secrets: # refs only — never values
167
+ kinde-client-secret: # ref name (arbitrary)
168
+ from: env # provider name
169
+ key: KINDIE_CLIENT_SECRET # provider-specific locator
170
+ r2-keys:
171
+ from: env
172
+ key: YOSK_R2_KEYS
173
+
174
+ steps:
175
+ - name: qa
176
+ agent: qa
177
+ secrets: [kinde-client-secret, r2-keys]
178
+ ```
179
+
180
+ - The engine resolves every declared ref **host-side at run start** (the `env`
181
+ provider reads the named host environment variable). A missing variable, an
182
+ unknown provider, or a ref a step lists without declaring it fails fast —
183
+ naming the ref, the step, and the provider error. No silent fallback.
184
+ - A resolved secret is injected into a step's exec env **only when that step
185
+ lists the ref** — the same path step `env:` uses, no new channel. The secret
186
+ never reaches the provision container, `state_dir`, or any other step.
187
+ - Prefer a mechanical `command` step for secret-consuming work (a script runs
188
+ the app with creds) so the LLM and the secret never meet; an `agent` step
189
+ holds a secret only when its job genuinely requires it.
190
+ - **`.env*` ban.** At run start the workspace root is scanned for `.env*`
191
+ files. Any hit fails fast, naming the files, with the remedy text. The
192
+ convention fixtures `.env.example`, `.env.template`, and `.env.sample` are
193
+ whitelisted. This applies regardless of `network:` flags.
194
+ - **Output scrubbing.** Resolved secret values are scrubbed from every
195
+ engine-written output — captured step output, retry notes, artifacts, and
196
+ `state_dir` files. (Hygiene: the declaring step's agent has already seen
197
+ the value in its own env.)
198
+
206
199
  ### Per-stage networking
207
200
 
208
201
  The sandbox is network-off by default. A step that legitimately needs egress —
@@ -234,8 +227,7 @@ steps:
234
227
  git clone git@github.com:lukedawilson/agentic-dev-loop.git
235
228
  cd agentic-dev-loop
236
229
  ./setup-venv.sh # idempotent .venv setup (never touches bare pip)
237
- # create .env with your LLM key (loaded by trigger-agentic-dev-loop.sh, never committed):
238
- # echo 'OPENAI_API_KEY=...' > .env
230
+ export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
239
231
  python -m pytest tests/ -q # fast suite
240
232
  python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
241
233
  ./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a7"
7
+ version = "1.0.0a9"
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"
@@ -32,23 +32,8 @@ classifiers = [
32
32
  "Typing :: Typed",
33
33
  ]
34
34
  dependencies = [
35
- # Base install = workflow mode (core + viz + workflow CLI). Deliberately
36
- # dependency-light (TS-1 / NFR-4.1 / BR-2): pydantic + PyYAML only. The
37
- # heavy OpenHands-SDK engine deps live in the [engine] extra below.
38
35
  "pydantic>=2.0",
39
36
  "PyYAML>=6.0",
40
- ]
41
-
42
- [project.urls]
43
- Homepage = "https://github.com/lukedawilson/agentic-dev-loop"
44
-
45
- [project.scripts]
46
- agentic-dev-loop = "agentic_dev_loop.engine.cli:main"
47
- agentic-dev-loop-workflow = "agentic_dev_loop.workflow.cli:main"
48
- agentic-dev-loop-commit = "agentic_dev_loop.engine.commit_shim:main"
49
-
50
- [project.optional-dependencies]
51
- engine = [
52
37
  "openhands-sdk ~=1.17.0",
53
38
  "openhands-tools ~=1.17.0",
54
39
  # openhands-sdk declares agent-client-protocol>=0.8.1 with NO upper bound;
@@ -59,12 +44,15 @@ engine = [
59
44
  "langgraph>=1.2",
60
45
  "langgraph-checkpoint-sqlite>=3.0",
61
46
  ]
62
- # Workflow mode is the BASE install — this empty extra exists only so
63
- # `pip install agentic-dev-loop[workflow]` reads naturally and pulls nothing
64
- # beyond the light base (core + viz). See README / NFR-4.1.
65
- workflow = []
47
+
48
+ [project.urls]
49
+ Homepage = "https://github.com/lukedawilson/agentic-dev-loop"
50
+
51
+ [project.scripts]
52
+ agentic-dev-loop = "agentic_dev_loop.engine.cli:main"
53
+
54
+ [project.optional-dependencies]
66
55
  dev = [
67
- "agentic-dev-loop[engine]",
68
56
  "hypothesis>=6.0",
69
57
  "pytest>=8.0",
70
58
  ]
@@ -74,7 +62,6 @@ where = ["src"]
74
62
 
75
63
  [tool.setuptools.package-data]
76
64
  agentic_dev_loop = ["py.typed"]
77
- "agentic_dev_loop.core" = ["workflow.js.template"]
78
65
 
79
66
  [tool.pytest.ini_options]
80
67
  markers = [
@@ -1,4 +1,4 @@
1
- """Pipeline model + parse + workflow projection (unit-4 core).
1
+ """Pipeline model + parse + results (core).
2
2
 
3
3
  Dependency-light by contract (BR-2): pydantic + PyYAML + stdlib only. A guard
4
4
  test fails if this package imports engine/viz or heavy deps.
@@ -9,17 +9,10 @@ from .config import (PipelineConfig, StepConfig, LlmConfig, IsolationConfig,
9
9
  default_state_dir, TERMINALS)
10
10
  from .results import Result
11
11
  from .verdict import Verdict, VerdictError, parse_verdict, RETRY
12
- from .projection import (ProjectedStep, WorkflowProjection, project_workflow,
13
- validate_prompt_refs, render_workflow_js,
14
- clean_at_start_flag, pipeline_hash, core_version,
15
- WORKFLOW_TEMPLATE)
16
12
 
17
13
  __all__ = [
18
14
  "PipelineConfig", "StepConfig", "LlmConfig", "IsolationConfig",
19
15
  "VerdictSpec", "Route", "route_target", "load_config",
20
16
  "default_state_dir", "TERMINALS",
21
17
  "Result", "Verdict", "VerdictError", "parse_verdict", "RETRY",
22
- "ProjectedStep", "WorkflowProjection", "project_workflow",
23
- "validate_prompt_refs", "render_workflow_js", "clean_at_start_flag",
24
- "pipeline_hash", "core_version", "WORKFLOW_TEMPLATE",
25
18
  ]
@@ -11,6 +11,7 @@ import yaml
11
11
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
12
12
 
13
13
  TERMINALS = frozenset({"success", "failure"})
14
+ KNOWN_SECRET_PROVIDERS = frozenset({"env"})
14
15
 
15
16
 
16
17
  class VerdictSpec(BaseModel):
@@ -36,6 +37,34 @@ def route_target(route: RouteTarget | None) -> str | None:
36
37
  return route.goto if isinstance(route, Route) else route
37
38
 
38
39
 
40
+ class SecretSpec(BaseModel):
41
+ """One ``secrets:`` ref — the locator only, never a value. ``from`` names
42
+ the provider (``env`` first); ``key`` is that provider's locator (for
43
+ ``env``, the host environment variable to read and the step env name to
44
+ inject). Unknown keys fail at load (extra="forbid")."""
45
+
46
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
47
+
48
+ from_: str = Field(alias="from")
49
+ key: str
50
+
51
+ @field_validator("from_")
52
+ @classmethod
53
+ def _known_provider(cls, v: str) -> str:
54
+ if v not in KNOWN_SECRET_PROVIDERS:
55
+ raise ValueError(
56
+ f"unknown secret provider {v!r} (known: "
57
+ f"{sorted(KNOWN_SECRET_PROVIDERS)})")
58
+ return v
59
+
60
+ @field_validator("key")
61
+ @classmethod
62
+ def _nonempty_key(cls, v: str) -> str:
63
+ if not v.strip():
64
+ raise ValueError("secret key must be a non-empty string")
65
+ return v
66
+
67
+
39
68
  class IsolationConfig(BaseModel):
40
69
  """Optional `isolation:` section — configures the always-on sandbox (BR-2).
41
70
  Absence means opinionated defaults, never unsandboxed behaviour. Unknown
@@ -86,6 +115,7 @@ class StepConfig(BaseModel):
86
115
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
87
116
  setup: str | None = None # BR-13: command steps only
88
117
  network: bool = False # per-stage egress (command + agent steps; action steps reject)
118
+ secrets: list[str] = [] # declared ref names; action steps reject
89
119
  on_pass: RouteTarget | None = None
90
120
  on_fail: RouteTarget | None = None
91
121
 
@@ -148,6 +178,11 @@ class StepConfig(BaseModel):
148
178
  f"step {self.name!r}: 'network' is only allowed on 'agent' or "
149
179
  "'command' steps"
150
180
  )
181
+ if self.secrets and self.action is not None:
182
+ raise ValueError(
183
+ f"step {self.name!r}: 'secrets' is only allowed on 'agent' or "
184
+ "'command' steps"
185
+ )
151
186
  return self
152
187
 
153
188
 
@@ -170,10 +205,28 @@ class PipelineConfig(BaseModel):
170
205
  llm: LlmConfig
171
206
  isolation: IsolationConfig | None = None # None = always-on defaults
172
207
  state_dir: str = ".pr" # default relocated to ~/.agentic-dev-loop/runs/<hash>/ at load
208
+ secrets: dict[str, SecretSpec] = {} # ref name -> locator; values never appear here
173
209
  max_attempts: int = 3
174
210
  agents_dir: str
175
211
  steps: list[StepConfig] = Field(min_length=1)
176
212
 
213
+ @field_validator("secrets", mode="before")
214
+ @classmethod
215
+ def _reject_value_shaped_refs(cls, v: Any) -> Any:
216
+ if v is None:
217
+ return {}
218
+ if not isinstance(v, dict):
219
+ raise ValueError(
220
+ "'secrets' must be a mapping of ref name to "
221
+ "{from: ..., key: ...}")
222
+ for name, spec in v.items():
223
+ if not isinstance(spec, dict):
224
+ raise ValueError(
225
+ f"secret ref {name!r} must be a mapping "
226
+ "{from: ..., key: ...}, not a literal value — secret "
227
+ "values never belong in the pipeline yaml")
228
+ return v
229
+
177
230
  @model_validator(mode="after")
178
231
  def _check_graph(self) -> "PipelineConfig":
179
232
  names = [s.name for s in self.steps]
@@ -191,6 +244,12 @@ class PipelineConfig(BaseModel):
191
244
  raise ValueError(
192
245
  f"step {step.name!r}: unknown route target {target!r}"
193
246
  )
247
+ for step in self.steps:
248
+ for ref in step.secrets:
249
+ if ref not in self.secrets:
250
+ raise ValueError(
251
+ f"step {step.name!r}: secret ref {ref!r} is not "
252
+ "declared in the top-level 'secrets:' section")
194
253
  return self
195
254
 
196
255
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  `Verdict.from_text` parses the captured response for routing; expected
4
4
  conditions are typed `Result` errors, never exceptions. `parse_verdict`
5
- collapses that `Result` to a route string for workflow consumers: a clear
5
+ collapses that `Result` to a route string for routing consumers: a clear
6
6
  decision maps to its canonical value, anything unclear maps to ``retry`` —
7
7
  totality for arbitrary input (no crash, no silent routing).
8
8
  """
@@ -1,2 +1,2 @@
1
1
  """Engine package — graph/stages/sandbox/CLI and the heavy OpenHands-SDK
2
- dependencies (unit-4 re-homing, FR-4.1)."""
2
+ dependencies."""
@@ -110,13 +110,17 @@ class SandboxTerminalExecutor(ToolExecutor):
110
110
  def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None] | None = None,
111
111
  workspace: str = "", pipeline: str | None = None,
112
112
  step: str | None = None,
113
- network: bool = False) -> None:
113
+ network: bool = False,
114
+ secrets: dict[str, str] | None = None) -> None:
114
115
  """Per-run re-registration of the `terminal` name (KD-4): a callable
115
116
  factory that builds the SDK terminal with a `SandboxTerminalExecutor`.
116
- Pipelines are sequential, so re-registering per agent step is safe."""
117
+ Pipelines are sequential, so re-registering per agent step is safe.
118
+
119
+ ``secrets`` (019) is the per-step resolved secret env, injected into the
120
+ terminal's exec env only for steps that declared the refs."""
117
121
  from openhands.sdk.tool import register_tool
118
122
 
119
- resolved_env = resolve_sandbox_env(env)
123
+ resolved_env = resolve_sandbox_env(env, secrets=secrets)
120
124
 
121
125
  def factory(conv_state, **params):
122
126
  return TerminalTool.create(
@@ -17,6 +17,7 @@ from .loaders import resolve_docs
17
17
  from .isolation import (isolation_state_dir, resolve_effective_profile,
18
18
  resolve_image_ref)
19
19
  from .registry import Registry, default_registry, load_extensions
20
+ from .secrets import scan_env_files
20
21
 
21
22
 
22
23
  def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
@@ -113,6 +114,7 @@ def dry_run(cfg_path, args, registry: Registry | None = None) -> int:
113
114
  cfg_path = Path(cfg_path)
114
115
  cfg = load_config(cfg_path)
115
116
  base = cfg_path.resolve().parent
117
+ scan_env_files(Path.cwd())
116
118
 
117
119
  if registry is None:
118
120
  registry = default_registry()
@@ -22,6 +22,7 @@ from agentic_dev_loop.core.config import load_config
22
22
  from .isolation import startup_sandbox
23
23
  from .llm import build_llm
24
24
  from .loaders import resolve_docs
25
+ from .secrets import resolve_secrets, scan_env_files
25
26
  from .registry import Registry, default_registry, load_extensions
26
27
  from .stages import (Runtime, build_context, clear_attempt_artifacts,
27
28
  make_action_node, make_agent_node, make_bump_node,
@@ -107,6 +108,11 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
107
108
  base = cfg_path.resolve().parent
108
109
  max_attempts = getattr(args, "max_attempts", None) or cfg.max_attempts
109
110
 
111
+ # 019: refuse secret-bearing workspace files and resolve declared secret
112
+ # refs host-side BEFORE any sandbox/LLM work — fail fast, loudly.
113
+ scan_env_files(Path.cwd())
114
+ resolved_secrets = resolve_secrets(cfg)
115
+
110
116
  if registry is None:
111
117
  registry = default_registry()
112
118
  load_extensions(cfg.extensions, registry)
@@ -122,16 +128,17 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
122
128
  refresh=getattr(args, "refresh", False))
123
129
  try:
124
130
  return _run_pipeline(cfg, base, registry, state_dir, max_attempts,
125
- args, viz_bus, sandbox)
131
+ args, viz_bus, sandbox, resolved_secrets)
126
132
  finally:
127
133
  sandbox.close()
128
134
 
129
135
 
130
136
  def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
131
- sandbox) -> tuple[bool, int]:
137
+ sandbox, resolved_secrets) -> tuple[bool, int]:
132
138
  runtime = Runtime(cfg=cfg, registry=registry, llm=build_llm(cfg.llm),
133
139
  agents_dir=base / cfg.agents_dir, state_dir=state_dir,
134
- workspace=Path.cwd(), sandbox=sandbox)
140
+ workspace=Path.cwd(), sandbox=sandbox,
141
+ secrets=resolved_secrets)
135
142
  state_dir.mkdir(parents=True, exist_ok=True)
136
143
 
137
144
  resume = getattr(args, "resume", None)
@@ -183,11 +183,17 @@ def resolve_effective_profile(isolation: IsolationConfig | None) -> EffectivePro
183
183
 
184
184
 
185
185
  def resolve_sandbox_env(step_env: dict[str, str | None] | None, *,
186
- host_env: Mapping[str, str] | None = None) -> dict[str, str]:
186
+ host_env: Mapping[str, str] | None = None,
187
+ secrets: Mapping[str, str] | None = None) -> dict[str, str]:
187
188
  """Fixed safe base (PATH/HOME/LANG/LC_ALL/TERM) + the step's declared
188
189
  ``env:`` mapping only. A declared value of ``None`` inherits the host
189
190
  value; the global ``env_allowlist`` is never consulted and the LLM key is
190
- never auto-added (NFR-3.1 / BR-9)."""
191
+ never auto-added (NFR-3.1 / BR-9).
192
+
193
+ ``secrets`` (019) carries the per-step resolved secret env — injected last,
194
+ so a declared ref is authoritative over any same-named ``env:`` entry. It
195
+ arrives only from ``step_secret_env``, which maps only the refs the step
196
+ declared, so no secret ever reaches an undeclaring step."""
191
197
  source = os.environ if host_env is None else host_env
192
198
  env = dict(SANDBOX_ENV_BASE)
193
199
  for name, value in (step_env or {}).items():
@@ -196,6 +202,8 @@ def resolve_sandbox_env(step_env: dict[str, str | None] | None, *,
196
202
  env[name] = source[name]
197
203
  else:
198
204
  env[name] = value
205
+ for name, value in (secrets or {}).items():
206
+ env[name] = value
199
207
  return env
200
208
 
201
209
 
@@ -2,9 +2,9 @@
2
2
  channel (unit-1).
3
3
 
4
4
  The pipeline — not the agent — writes the step's artifact from the agent's
5
- final SDK response. Verdict parsing lives in `core.verdict` (shared with
6
- workflow mode); this module only owns the SDK-side capture. Expected
7
- conditions are typed `Result` errors, never exceptions.
5
+ final SDK response. Verdict parsing lives in `core.verdict`; this module only
6
+ owns the SDK-side capture. Expected conditions are typed `Result` errors,
7
+ never exceptions.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -16,6 +16,7 @@ from pathlib import Path
16
16
  from openhands.sdk.conversation.response_utils import get_agent_final_response
17
17
 
18
18
  from agentic_dev_loop.core.results import Result
19
+ from .secrets import redact_values
19
20
 
20
21
 
21
22
  class CaptureError(enum.Enum):
@@ -32,11 +33,13 @@ class CapturedOutput:
32
33
 
33
34
  @staticmethod
34
35
  def from_agent_result(
35
- conv, artifact: str, state_dir: Path
36
+ conv, artifact: str, state_dir: Path,
37
+ redactions: tuple[str, ...] = ()
36
38
  ) -> "Result[CapturedOutput, CaptureError]":
37
39
  text = _final_agent_text(conv)
38
40
  if not text.strip():
39
41
  return Result.err(CaptureError.EMPTY_RESPONSE)
42
+ text = redact_values(text, redactions)
40
43
  return Result.ok(CapturedOutput(raw_text=text, artifact_path=state_dir / artifact))
41
44
 
42
45
  def write(self) -> Result[None, CaptureError]: