agentic-dev-loop 0.9.0__py3-none-any.whl → 0.9.2__py3-none-any.whl

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.
@@ -16,9 +16,6 @@ from agentic_dev_loop.core.paths import _has_wildcards, path_excluded, relative_
16
16
  TERMINALS = frozenset({"success", "failure"})
17
17
  KNOWN_SECRET_PROVIDERS = frozenset({"env"})
18
18
  MOUNT_MODES = frozenset({"rw", "ro"})
19
- # Host-only dirs a mount path may never name (022): they are masked in
20
- # the sandbox with a read-only tmpfs, so declaring them would be a decoy.
21
- HOST_ONLY_MOUNT_DIRS = frozenset({".venv"})
22
19
  # The egress preset names the engine ships (BR-9). Config validates the
23
20
  # name here; the target data lives in engine/presets.py (preset data,
24
21
  # never mechanism - D-2).
@@ -52,8 +49,8 @@ def _validate_mount_path(path: str) -> str:
52
49
  """Validate one repo-relative mount path (BR-1.3).
53
50
 
54
51
  Rejects (loudly) an empty path, an absolute path, ``..`` traversal,
55
- any ``.git`` component, and any host-only dir (``.venv``). The path
56
- is normalised to forward-slash form and returned.
52
+ and any ``.git`` component. The path is normalised to forward-slash
53
+ form and returned.
57
54
  """
58
55
  if not isinstance(path, str) or not path.strip():
59
56
  raise ValueError("mount path must be a non-empty string")
@@ -70,9 +67,6 @@ def _validate_mount_path(path: str) -> str:
70
67
  if seg == ".git":
71
68
  raise ValueError(
72
69
  f"mount path {path!r} must never name '.git'")
73
- if seg in HOST_ONLY_MOUNT_DIRS:
74
- raise ValueError(
75
- f"mount path {path!r} names the host-only dir {seg!r}")
76
70
  return "/".join(parts) if parts else "."
77
71
 
78
72
 
@@ -80,7 +74,7 @@ def _validate_declared_path(path: str, *, allow_wildcards: bool) -> str:
80
74
  """Validate one declared ``caches:``/``sync_excludes:`` entry (028).
81
75
 
82
76
  Same repo-relative contract as mount paths: non-empty, no absolute paths,
83
- no ``..`` traversal, no ``.git`` component, no host-only dir. Wildcard
77
+ no ``..`` traversal, no ``.git`` component. Wildcard
84
78
  entries (``sync_excludes:`` only) skip wildcard segments in the literal
85
79
  checks; the literal segments still obey the same rules. The normalised
86
80
  forward-slash form is returned.
@@ -102,9 +96,6 @@ def _validate_declared_path(path: str, *, allow_wildcards: bool) -> str:
102
96
  if seg == ".git":
103
97
  raise ValueError(
104
98
  f"declared path {path!r} must never name '.git'")
105
- if seg in HOST_ONLY_MOUNT_DIRS:
106
- raise ValueError(
107
- f"declared path {path!r} names the host-only dir {seg!r}")
108
99
  return "/".join(parts) if parts else "."
109
100
 
110
101
 
@@ -220,24 +211,32 @@ class SecretSpec(BaseModel):
220
211
  class IsolationConfig(BaseModel):
221
212
  """Optional `isolation:` section - configures the always-on sandbox (BR-2).
222
213
  Absence means opinionated defaults, never unsandboxed behaviour. Unknown
223
- keys fail at load (BR-5). `state_dir` is HOST-ONLY (stamps + audit) and is
224
- never mounted into the sandbox (Q1)."""
214
+ keys fail at load (BR-5). `state_dir` is HOST-ONLY (host-side caches +
215
+ audit) and is never mounted into the sandbox (Q1)."""
225
216
 
226
217
  model_config = ConfigDict(extra="forbid")
227
218
 
219
+ @model_validator(mode="before")
220
+ @classmethod
221
+ def _reject_removed_keys(cls, data: Any) -> Any:
222
+ """052: removed isolation keys fail loud with a migration message."""
223
+ if isinstance(data, dict):
224
+ if "sandbox_venv_dir" in data:
225
+ raise ValueError(
226
+ "isolation.sandbox_venv_dir was removed: venvs are "
227
+ "per-run, built by a setup hook into the export; remove "
228
+ "this key")
229
+ if "refresh" in data:
230
+ raise ValueError(
231
+ "isolation.refresh was removed: setup now runs fresh at "
232
+ "every step entry with no stamping; remove this key")
233
+ return data
234
+
228
235
  image: str | None = None # per-pipeline override (KD-2 ref rules)
229
- refresh: bool = False # force re-provision (NFR-5)
230
236
  memory: str | None = None # override exec container memory
231
237
  cpus: float | None = None # override exec container cpus
232
238
  allow_git_mutations: bool = False # BR-6 knob
233
- state_dir: str = "~/.agentic-dev-loop/state" # stamps + audit (host-only)
234
- # 022 relocation opt-in: an out-of-workspace host dir that is bind-mounted
235
- # at ``{ws}/.venv-sandbox`` (rw) in both the exec and provision containers,
236
- # keeping the workspace free of container venv artifacts. Unset keeps the
237
- # legacy workspace-local ``.venv-sandbox`` unchanged. Must resolve outside
238
- # the workspace - validated at startup, not load (the workspace is unknown
239
- # here).
240
- sandbox_venv_dir: str | None = None
239
+ state_dir: str = "~/.agentic-dev-loop/state" # host-side caches + audit
241
240
  # 023/025: optional host path to a Docker seccomp profile JSON, or a
242
241
  # ``bundled:<name>`` sentinel resolved at load to a packaged profile
243
242
  # (e.g. ``bundled:chromium-userns``). When set, the exec container runs
@@ -402,8 +401,7 @@ class StepConfig(BaseModel):
402
401
  reasoning_effort: ReasoningEffort | None = None
403
402
  custom_prompt: str | None = None # agent steps only (040); file path resolved at load
404
403
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
405
- setup: str | None = None # BR-13: command steps only
406
- setup_probe: str | None = None # 045: consumer probe replacing the venv probe on a warm stamp; requires `setup`; probe text is NOT part of the stamp key (a probe-only change needs --refresh/--clean-stamps)
404
+ setup: str | None = None # command or agent steps; runs fresh at each step entry (052)
407
405
  forbidden_commands: list[str] = [] # banned-command regex deny-list (036 D1)
408
406
  network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
409
407
  # unit-5: per-step mount granularity + egress policy. Empty mounts =
@@ -432,6 +430,16 @@ class StepConfig(BaseModel):
432
430
  return "command"
433
431
  return "action"
434
432
 
433
+ @model_validator(mode="before")
434
+ @classmethod
435
+ def _reject_removed_keys(cls, data: Any) -> Any:
436
+ """052: removed step keys fail loud with a migration message."""
437
+ if isinstance(data, dict) and "setup_probe" in data:
438
+ raise ValueError(
439
+ "step setup_probe was removed: setup now runs fresh at every "
440
+ "step entry with no stamping; remove this key")
441
+ return data
442
+
435
443
  @field_validator("produces", mode="before")
436
444
  @classmethod
437
445
  def _coerce_produces(cls, v: Any) -> Any:
@@ -464,13 +472,6 @@ class StepConfig(BaseModel):
464
472
  raise ValueError("custom_prompt must be a non-empty string")
465
473
  return v
466
474
 
467
- @field_validator("setup_probe")
468
- @classmethod
469
- def _nonempty_setup_probe(cls, v: str | None) -> str | None:
470
- if v is not None and not v.strip():
471
- raise ValueError("setup_probe must be a non-empty string")
472
- return v
473
-
474
475
  @model_validator(mode="after")
475
476
  def _check_shape(self) -> "StepConfig":
476
477
  for key in ("agent", "action", "command"):
@@ -566,15 +567,10 @@ class StepConfig(BaseModel):
566
567
  raise ValueError(
567
568
  f"step {self.name!r}: 'custom_prompt' requires 'agent'"
568
569
  )
569
- if (self.setup is not None and self.command is None
570
- and self.agent is None and self.brief_inputs is None):
570
+ if self.setup is not None and self.command is None and self.agent is None:
571
571
  raise ValueError(
572
- f"step {self.name!r}: 'setup' is only allowed on 'command', "
573
- "'agent', or 'brief_inputs' steps"
574
- )
575
- if self.setup_probe is not None and self.setup is None:
576
- raise ValueError(
577
- f"step {self.name!r}: 'setup_probe' requires 'setup'"
572
+ f"step {self.name!r}: 'setup' is only allowed on 'command' or "
573
+ "'agent' steps (052)"
578
574
  )
579
575
  if self.secrets and self.action is not None:
580
576
  raise ValueError(
@@ -624,11 +620,10 @@ class PipelineConfig(BaseModel):
624
620
  # missing-dependency fail-fast.
625
621
  deps_policy: str = "locked"
626
622
  lockfiles: list[str] = []
627
- # 028: pipeline-declared toolchain caches (rw bind-mounts from the real
628
- # workspace) and run-wide sync-back excludes (scratch semantics: skipped
629
- # from both the export copy and sync-back). Both default to [] - absent
630
- # means "no toolchain caches beyond the engine's own sandbox venv" and
631
- # "no sync-back excludes".
623
+ # 028/052: pipeline-declared toolchain caches (rw bind-mounts from the
624
+ # host-side cache dir ``{isolation.state_dir}/caches/<name>``) and
625
+ # run-wide sync-back excludes (scratch semantics: skipped from both the
626
+ # export copy and sync-back). Both default to [].
632
627
  caches: list[str] = []
633
628
  sync_excludes: list[str] = []
634
629
  # 029: append-style logs that sync-back union-merges on a concurrent
@@ -715,25 +710,6 @@ class PipelineConfig(BaseModel):
715
710
  f"the host-only state_dir {self.state_dir!r}")
716
711
  return self
717
712
 
718
- @model_validator(mode="after")
719
- def _caches_avoid_state_dir(self) -> "PipelineConfig":
720
- """028: a declared cache may never name (or sit under) the host-only
721
- run-state dir - it would bind-mount the engine's own state into the
722
- sandbox as a rw cache."""
723
- sd = self.state_dir.strip().rstrip("/\\")
724
- if not sd or sd in ("/", "."):
725
- return self
726
- sd_parts = relative_parts(sd)
727
- if not sd_parts:
728
- return self
729
- for cache in self.caches:
730
- cp = relative_parts(cache)
731
- if cp[:len(sd_parts)] == sd_parts:
732
- raise ValueError(
733
- f"cache path {cache!r} names the host-only state_dir "
734
- f"{self.state_dir!r}")
735
- return self
736
-
737
713
  @model_validator(mode="after")
738
714
  def _mounts_avoid_caches(self) -> "PipelineConfig":
739
715
  """028/BR-1.3: a step mount equal to or under a declared cache is
@@ -799,21 +775,6 @@ class PipelineConfig(BaseModel):
799
775
  f"{('/'.join(ep))!r}")
800
776
  return self
801
777
 
802
- @model_validator(mode="after")
803
- def _sandbox_venv_not_declared_cache(self) -> "PipelineConfig":
804
- """028: the sandbox venv is engine infrastructure, never author data.
805
- Declaring ``.venv-sandbox`` in ``caches:`` while the 022 relocation
806
- (``isolation.sandbox_venv_dir``) is set is a contradiction."""
807
- if self.isolation is None or self.isolation.sandbox_venv_dir is None:
808
- return self
809
- for cache in self.caches:
810
- if relative_parts(cache) == (".venv-sandbox",):
811
- raise ValueError(
812
- "declaring '.venv-sandbox' in 'caches:' contradicts "
813
- "'isolation.sandbox_venv_dir' - the sandbox venv is "
814
- "relocated outside the workspace (022)")
815
- return self
816
-
817
778
  @model_validator(mode="after")
818
779
  def _warn_shadowed_artifacts(self) -> "PipelineConfig":
819
780
  """028: an artifact/produces path matching a sync-exclude would never
@@ -14,8 +14,7 @@ from pathlib import Path
14
14
 
15
15
  from agentic_dev_loop.core.config import PipelineConfig, load_config
16
16
  from .containers import containers_report
17
- from .isolation import (clean_stamps, isolation_state_dir,
18
- reap_orphaned_containers)
17
+ from .isolation import isolation_state_dir, reap_orphaned_containers
19
18
  from .loaders import mark_plan_completed
20
19
  from .registry import Registry, default_registry, load_extensions
21
20
 
@@ -82,15 +81,10 @@ def _core_parser() -> argparse.ArgumentParser:
82
81
  help="preferred port for the live graph view "
83
82
  "(default: 8321; falls back to a free port when "
84
83
  "busy)")
85
- parser.add_argument("--refresh", action="store_true",
86
- help="force re-provision of every setup: hook "
87
- "(bypasses the stamp cache)")
88
84
  parser.add_argument("--dry-run", action="store_true",
89
85
  help="validate the pipeline and print a summary plan "
90
86
  "without executing anything (no LLM, no "
91
87
  "sandbox, no state writes)")
92
- parser.add_argument("--clean-stamps", action="store_true",
93
- help="delete cached provision stamps and exit")
94
88
  _add_version_flag(parser)
95
89
  return parser
96
90
 
@@ -185,18 +179,12 @@ def main(argv: list[str] | None = None) -> int:
185
179
  for loader in registry.document_loaders:
186
180
  loader.add_cli_args(parser)
187
181
  args = parser.parse_args(argv)
188
- if args.clean_stamps:
189
- state = isolation_state_dir(cfg)
190
- removed = clean_stamps(state)
191
- print(f"Removed {removed} cached provision stamp(s) from "
192
- f"{state / 'stamps'}", flush=True)
193
- return 0
194
182
  if args.dry_run:
195
183
  from .dryrun import dry_run
196
184
  return dry_run(args.pipeline, args, registry)
197
185
  # The run path alone needs the heavy graph/SDK imports, and --dry-run
198
186
  # needs the dryrun import chain in-branch; defer both so `containers`,
199
- # --clean-stamps, --help, and --version stay side-effect free. 042 D4: a
187
+ # --help, and --version stay side-effect free. 042 D4: a
200
188
  # SIGTERM here raises SystemExit(143) so graph.run_pipeline's finally
201
189
  # still closes the sandbox; a second SIGTERM hard-exits.
202
190
  previous = _install_sigterm_handler()
@@ -160,15 +160,14 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
160
160
  if not state_dir.is_absolute():
161
161
  state_dir = base / state_dir
162
162
 
163
- # Eager startup (Q2): probe -> image -> create+start exec container ->
164
- # pre-provision all declared setups, BEFORE the graph builds. Zero infra
165
- # operations mid-run; the only sandbox call during steps is run(request).
163
+ # Startup (052): probe -> image -> create+start exec container. No setup
164
+ # runs here: each step entry provisions its own setup hook, fresh, into
165
+ # the export.
166
166
  # 042 D3: startup is close-protected - a post-create provision failure
167
167
  # must still reach close() and reap the container it created.
168
168
  sandbox = None
169
169
  try:
170
170
  sandbox = startup_sandbox(cfg, workspace=Path.cwd(),
171
- refresh=getattr(args, "refresh", False),
172
171
  state_dir=state_dir)
173
172
  return _run_pipeline(cfg, base, registry, state_dir, max_attempts,
174
173
  args, viz_bus, sandbox, resolved_secrets,