agentic-dev-loop 1.0.0a8__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.
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/PKG-INFO +42 -3
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/README.md +41 -2
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/config.py +59 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/agents.py +7 -3
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/dryrun.py +2 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/graph.py +10 -3
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/isolation.py +10 -2
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/outputs.py +4 -1
- agentic_dev_loop-1.0.0a9/src/agentic_dev_loop/engine/secrets.py +118 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/stages.py +19 -7
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/PKG-INFO +42 -3
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/SOURCES.txt +4 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_e2e_fixtures.py +5 -1
- agentic_dev_loop-1.0.0a9/tests/test_secrets.py +221 -0
- agentic_dev_loop-1.0.0a9/tests/test_secrets_integration.py +67 -0
- agentic_dev_loop-1.0.0a9/tests/test_secrets_pipeline.py +106 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_viz.py +2 -1
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_config.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_isolation.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a8 → 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.
|
|
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
|
|
@@ -192,9 +192,49 @@ steps:
|
|
|
192
192
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
193
193
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
194
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). |
|
|
195
196
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
196
197
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
197
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
|
+
|
|
198
238
|
### Per-stage networking
|
|
199
239
|
|
|
200
240
|
The sandbox is network-off by default. A step that legitimately needs egress —
|
|
@@ -226,8 +266,7 @@ steps:
|
|
|
226
266
|
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
227
267
|
cd agentic-dev-loop
|
|
228
268
|
./setup-venv.sh # idempotent .venv setup (never touches bare pip)
|
|
229
|
-
#
|
|
230
|
-
# echo 'OPENAI_API_KEY=...' > .env
|
|
269
|
+
export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
|
|
231
270
|
python -m pytest tests/ -q # fast suite
|
|
232
271
|
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
233
272
|
./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
|
|
@@ -153,9 +153,49 @@ steps:
|
|
|
153
153
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
154
154
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
155
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). |
|
|
156
157
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
157
158
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
158
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
|
+
|
|
159
199
|
### Per-stage networking
|
|
160
200
|
|
|
161
201
|
The sandbox is network-off by default. A step that legitimately needs egress —
|
|
@@ -187,8 +227,7 @@ steps:
|
|
|
187
227
|
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
188
228
|
cd agentic-dev-loop
|
|
189
229
|
./setup-venv.sh # idempotent .venv setup (never touches bare pip)
|
|
190
|
-
#
|
|
191
|
-
# echo 'OPENAI_API_KEY=...' > .env
|
|
230
|
+
export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
|
|
192
231
|
python -m pytest tests/ -q # fast suite
|
|
193
232
|
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
194
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.
|
|
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"
|
|
@@ -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
|
|
|
@@ -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
|
|
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)
|
{agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -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
|
|
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
|
|
{agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/src/agentic_dev_loop/engine/outputs.py
RENAMED
|
@@ -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]:
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Host-side secret store (unit 019): provider registry, run-start resolution,
|
|
2
|
+
the ``.env*`` ban, and output scrubbing.
|
|
3
|
+
|
|
4
|
+
Secrets are declared in the pipeline yaml as refs only — never values. At run
|
|
5
|
+
start the engine resolves every declared ref host-side (``env`` provider
|
|
6
|
+
first) and injects each value into a declaring step's exec env only. The
|
|
7
|
+
``.env*`` ban keeps secret-bearing files out of the workspace, and the
|
|
8
|
+
scrubber keeps resolved values out of every engine-written output path.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from collections.abc import Iterable, Mapping
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Protocol
|
|
17
|
+
|
|
18
|
+
from agentic_dev_loop.core.config import PipelineConfig, StepConfig
|
|
19
|
+
|
|
20
|
+
REDACTED = "[REDACTED]"
|
|
21
|
+
ENV_FILE_WHITELIST = frozenset({".env.example", ".env.template", ".env.sample"})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SecretResolutionError(RuntimeError):
|
|
25
|
+
"""A loud fail-fast secret failure naming the ref, step(s), and provider."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SecretProvider(Protocol):
|
|
29
|
+
"""A provider resolves locator keys to values, host-side, once per run."""
|
|
30
|
+
|
|
31
|
+
name: str
|
|
32
|
+
|
|
33
|
+
def resolve(self, keys: list[str]) -> dict[str, str]:
|
|
34
|
+
"""Resolve each locator key; a missing key raises loudly."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class EnvProvider:
|
|
39
|
+
"""Resolve secrets from host environment variables (the shipped default).
|
|
40
|
+
``key`` is both the host variable to read and the step env name to inject."""
|
|
41
|
+
|
|
42
|
+
name = "env"
|
|
43
|
+
|
|
44
|
+
def __init__(self, environ: Mapping[str, str] | None = None):
|
|
45
|
+
self._environ = os.environ if environ is None else environ
|
|
46
|
+
|
|
47
|
+
def resolve(self, keys: list[str]) -> dict[str, str]:
|
|
48
|
+
missing = sorted(k for k in keys if k not in self._environ)
|
|
49
|
+
if missing:
|
|
50
|
+
raise SecretResolutionError(
|
|
51
|
+
"environment variable(s) not set: " + ", ".join(missing))
|
|
52
|
+
return {k: self._environ[k] for k in keys}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
PROVIDERS: dict[str, SecretProvider] = {"env": EnvProvider()}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_secrets(cfg: PipelineConfig,
|
|
59
|
+
environ: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
60
|
+
"""Resolve every declared ref host-side once (run start). Returns
|
|
61
|
+
``{ref_name: value}``. An unresolvable ref or unknown provider fails fast,
|
|
62
|
+
naming the ref, the declaring step(s), and the provider error."""
|
|
63
|
+
if environ is not None:
|
|
64
|
+
providers = {"env": EnvProvider(environ)}
|
|
65
|
+
else:
|
|
66
|
+
providers = PROVIDERS
|
|
67
|
+
resolved: dict[str, str] = {}
|
|
68
|
+
for ref, spec in cfg.secrets.items():
|
|
69
|
+
provider = providers.get(spec.from_)
|
|
70
|
+
if provider is None:
|
|
71
|
+
raise SecretResolutionError(
|
|
72
|
+
f"secret ref {ref!r}: unknown provider {spec.from_!r}")
|
|
73
|
+
try:
|
|
74
|
+
resolved[ref] = provider.resolve([spec.key])[spec.key]
|
|
75
|
+
except SecretResolutionError as e:
|
|
76
|
+
steps = [s.name for s in cfg.steps if ref in s.secrets]
|
|
77
|
+
raise SecretResolutionError(
|
|
78
|
+
f"secret ref {ref!r} (provider {spec.from_!r}, key {spec.key!r})"
|
|
79
|
+
f" used by step(s) {steps or ['<none>']}: {e}") from e
|
|
80
|
+
return resolved
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def step_secret_env(cfg: PipelineConfig, step: StepConfig,
|
|
84
|
+
resolved: Mapping[str, str]) -> dict[str, str]:
|
|
85
|
+
"""The ``{env_name: value}`` mapping to inject for one step: only the refs
|
|
86
|
+
it declares, keyed by each ref's ``key`` (the env name the step reads)."""
|
|
87
|
+
return {cfg.secrets[ref].key: resolved[ref] for ref in step.secrets}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def redact_values(text: str, values: Iterable[str]) -> str:
|
|
91
|
+
"""Replace every resolved secret value in ``text`` with ``[REDACTED]``.
|
|
92
|
+
Longest-first so a value that is a substring of another is fully removed."""
|
|
93
|
+
if not text:
|
|
94
|
+
return text
|
|
95
|
+
secrets = sorted({v for v in values if v}, key=len, reverse=True)
|
|
96
|
+
for value in secrets:
|
|
97
|
+
text = text.replace(value, REDACTED)
|
|
98
|
+
return text
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def scan_env_files(workspace: Path | str) -> None:
|
|
102
|
+
"""The ``.env*`` ban: any ``.env*`` file at the workspace root — other than
|
|
103
|
+
the convention fixtures (``.env.example`` / ``.env.template`` /
|
|
104
|
+
``.env.sample``) — fails fast, naming the files and the remedy. Applies
|
|
105
|
+
regardless of ``network:`` flags: the workspace must simply not hold
|
|
106
|
+
secrets."""
|
|
107
|
+
root = Path(workspace)
|
|
108
|
+
hits = sorted(
|
|
109
|
+
p.name for p in root.glob(".env*")
|
|
110
|
+
if p.is_file() and p.name not in ENV_FILE_WHITELIST
|
|
111
|
+
)
|
|
112
|
+
if hits:
|
|
113
|
+
raise SecretResolutionError(
|
|
114
|
+
"secret-bearing file(s) found in the workspace root: "
|
|
115
|
+
+ ", ".join(hits)
|
|
116
|
+
+ " — move these values into `secrets:` refs / host env and "
|
|
117
|
+
"delete the files (`.env.example`, `.env.template`, `.env.sample` "
|
|
118
|
+
"are allowed fixtures)")
|
|
@@ -23,7 +23,7 @@ import os
|
|
|
23
23
|
import subprocess
|
|
24
24
|
import sys
|
|
25
25
|
import traceback
|
|
26
|
-
from dataclasses import dataclass
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from typing import TYPE_CHECKING
|
|
29
29
|
|
|
@@ -34,6 +34,7 @@ from .agents import load_agent, register_sandbox_terminal
|
|
|
34
34
|
from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
|
|
35
35
|
from .isolation import SandboxExecutionRequest, command_argv, resolve_sandbox_env
|
|
36
36
|
from .outputs import CapturedOutput
|
|
37
|
+
from .secrets import redact_values, step_secret_env
|
|
37
38
|
from agentic_dev_loop.core.verdict import Verdict
|
|
38
39
|
from .proc import (child_env, host_shell_execution, kill_process_tree,
|
|
39
40
|
needs_shell, split_command)
|
|
@@ -60,6 +61,7 @@ class Runtime:
|
|
|
60
61
|
state_dir: Path
|
|
61
62
|
workspace: Path
|
|
62
63
|
sandbox: Sandbox | None = None
|
|
64
|
+
secrets: dict[str, str] = field(default_factory=dict)
|
|
63
65
|
|
|
64
66
|
|
|
65
67
|
def prompt_dir(runtime: Runtime) -> str:
|
|
@@ -112,7 +114,9 @@ def run_agent(agent_name: str, message: str, runtime: Runtime,
|
|
|
112
114
|
workspace=str(runtime.workspace),
|
|
113
115
|
pipeline=runtime.cfg.name,
|
|
114
116
|
step=step.name if step is not None else None,
|
|
115
|
-
network=step.network if step is not None else False
|
|
117
|
+
network=step.network if step is not None else False,
|
|
118
|
+
secrets=(step_secret_env(runtime.cfg, step, runtime.secrets)
|
|
119
|
+
if step is not None else None))
|
|
116
120
|
agent = load_agent(runtime.agents_dir, agent_name, runtime.llm)
|
|
117
121
|
conv = Conversation(agent=agent, workspace=str(runtime.workspace),
|
|
118
122
|
max_iteration_per_run=max_iterations)
|
|
@@ -123,7 +127,9 @@ def run_agent(agent_name: str, message: str, runtime: Runtime,
|
|
|
123
127
|
|
|
124
128
|
if step is not None and (step.artifact is not None or step.produces):
|
|
125
129
|
artifact = step.artifact if step.artifact is not None else step.produces[0]
|
|
126
|
-
captured = CapturedOutput.from_agent_result(
|
|
130
|
+
captured = CapturedOutput.from_agent_result(
|
|
131
|
+
conv, artifact, runtime.state_dir,
|
|
132
|
+
redactions=tuple(runtime.secrets.values()))
|
|
127
133
|
if captured.is_err():
|
|
128
134
|
return AgentResult(ok=True, truncated=truncated,
|
|
129
135
|
capture_error=captured.error.value)
|
|
@@ -135,7 +141,7 @@ def run_agent(agent_name: str, message: str, runtime: Runtime,
|
|
|
135
141
|
response=captured.unwrap().raw_text)
|
|
136
142
|
return AgentResult(ok=True, truncated=truncated)
|
|
137
143
|
except Exception:
|
|
138
|
-
err = traceback.format_exc()
|
|
144
|
+
err = redact_values(traceback.format_exc(), runtime.secrets.values())
|
|
139
145
|
print(err[-4000:], file=sys.stderr, flush=True)
|
|
140
146
|
runtime.state_dir.mkdir(parents=True, exist_ok=True)
|
|
141
147
|
error_path = runtime.state_dir / LAST_ERROR
|
|
@@ -338,7 +344,8 @@ def make_command_node(runtime: Runtime, step: StepConfig):
|
|
|
338
344
|
timeout = step.timeout if step.timeout is not None else DEFAULT_COMMAND_TIMEOUT
|
|
339
345
|
sandbox = runtime.sandbox
|
|
340
346
|
if sandbox is not None:
|
|
341
|
-
env = resolve_sandbox_env(
|
|
347
|
+
env = resolve_sandbox_env(
|
|
348
|
+
step.env, secrets=step_secret_env(runtime.cfg, step, runtime.secrets))
|
|
342
349
|
else:
|
|
343
350
|
env = child_env(runtime.cfg.env_allowlist, runtime.cfg.llm.api_key_env)
|
|
344
351
|
|
|
@@ -352,9 +359,12 @@ def make_command_node(runtime: Runtime, step: StepConfig):
|
|
|
352
359
|
sandbox=sandbox, pipeline=runtime.cfg.name, step=step.name,
|
|
353
360
|
network=step.network)
|
|
354
361
|
except Exception:
|
|
355
|
-
print(traceback.format_exc()
|
|
362
|
+
print(redact_values(traceback.format_exc(),
|
|
363
|
+
runtime.secrets.values())[-4000:],
|
|
364
|
+
file=sys.stderr, flush=True)
|
|
356
365
|
return {"failed": True, "failed_step": step.name}
|
|
357
366
|
if output:
|
|
367
|
+
output = redact_values(output, runtime.secrets.values())
|
|
358
368
|
print(output, end="" if output.endswith("\n") else "\n", flush=True)
|
|
359
369
|
merged = {**state["step_verdicts"],
|
|
360
370
|
step.name: "pass" if exit_code == 0 else "fail"}
|
|
@@ -383,7 +393,9 @@ def make_action_node(runtime: Runtime, step: StepConfig):
|
|
|
383
393
|
try:
|
|
384
394
|
return fn(state, step.params) or {}
|
|
385
395
|
except Exception:
|
|
386
|
-
print(traceback.format_exc()
|
|
396
|
+
print(redact_values(traceback.format_exc(),
|
|
397
|
+
runtime.secrets.values())[-4000:],
|
|
398
|
+
file=sys.stderr, flush=True)
|
|
387
399
|
return {"failed": True, "failed_step": step.name}
|
|
388
400
|
|
|
389
401
|
return node
|
{agentic_dev_loop-1.0.0a8 → agentic_dev_loop-1.0.0a9}/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.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
|
|
@@ -192,9 +192,49 @@ steps:
|
|
|
192
192
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
193
193
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
194
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). |
|
|
195
196
|
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
196
197
|
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
197
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
|
+
|
|
198
238
|
### Per-stage networking
|
|
199
239
|
|
|
200
240
|
The sandbox is network-off by default. A step that legitimately needs egress —
|
|
@@ -226,8 +266,7 @@ steps:
|
|
|
226
266
|
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
227
267
|
cd agentic-dev-loop
|
|
228
268
|
./setup-venv.sh # idempotent .venv setup (never touches bare pip)
|
|
229
|
-
#
|
|
230
|
-
# echo 'OPENAI_API_KEY=...' > .env
|
|
269
|
+
export OPENAI_API_KEY=... # set your LLM key in the host env (no .env in the repo)
|
|
231
270
|
python -m pytest tests/ -q # fast suite
|
|
232
271
|
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
233
272
|
./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
|