From a584af095a32dc8bec9eb60c8057ad58cbb50582 Mon Sep 17 00:00:00 2001 From: "Alex H. Raber" Date: Sat, 7 Mar 2026 20:38:05 -0800 Subject: [PATCH] feat(validate): skip git workspace gates inside container - Container validate now only verifies build (compile, test, lint) - Git workspace gates (container signals, worktree, commit-often) are skipped - Constitution updated to reflect: exit container then do git ops on host This ensures reproducible builds in clean container env while keeping Git operations (commit, push, PR) on the host where they belong. --- constitution/interfaces/CLAIMS.md | 2 +- constitution/plugins/CONTAINER.md | 10 +++++ src/core/validate.rs | 71 +++++++++++++++++-------------- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/constitution/interfaces/CLAIMS.md b/constitution/interfaces/CLAIMS.md index 2f8df6e5..3b78bd26 100644 --- a/constitution/interfaces/CLAIMS.md +++ b/constitution/interfaces/CLAIMS.md @@ -79,7 +79,7 @@ Columns: | claim.lcm.summary_deterministic | Same originals in timestamp order produce the same summary hash across runs. | `interfaces/LCM.md` | enforced | `decapod lcm summarize` produces stable hash | Deterministic by construction. | | claim.map.scope_reduction_invariant | Agentic map delegation MUST declare retained scope; empty retain is rejected. | `interfaces/LCM.md` | enforced | `decapod map agentic --retain` required | Enforced in CLI argument parsing. | | claim.todo.claim_before_work | Agents must claim a TODO before substantive implementation work on that task. | `interfaces/CONTROL_PLANE.md` | partially_enforced | `decapod todo claim` ownership records + procedural review | Enforced by process today; future validate gate may enforce ownership-before-mutation traces. | -| claim.git.container_workspace_required | Git-tracked implementation work must execute in Docker-isolated git workspaces, not direct host worktree edits. | `specs/GIT.md` | enforced | `decapod validate` (Git Workspace Context Gate) | Enforced via validate gate checking container signals and worktree isolation. | +| claim.git.container_workspace_required | Git-tracked implementation work must execute in Docker-isolated git workspaces rooted at `.decapod/workspaces/*`, not by directly editing the host repository working tree. Inside containers, `validate` only verifies build correctness (compile, test, lint) - git workspace gates are skipped. Host-side Git operations (commit, push, PR) happen after exiting the container. | `specs/GIT.md` | enforced | `decapod validate` (Git Workspace Context Gate, skipped in container) | Container validate is build-only; git ops happen on host. | | claim.git.no_direct_main_push | Direct commits/pushes to protected branches (master/main/production/stable/release/*) are forbidden; work must happen in working branches. | `specs/GIT.md` | enforced | `decapod validate` (Git Protected Branch Gate) | Enforced via validate gate checking current branch and unpushed commits. | | claim.git.container_runtime_preflight_required | Container workspace runs must pass runtime-access preflight and fail loudly with elevated-permission remediation when access is denied. | `specs/GIT.md` | partially_enforced | `container.run` runtime `info` preflight + permission-aware error diagnostics | Enforced in container runtime preflight; broader policy-level enforcement remains future work. | | claim.session.agent_password_required | Session access requires agent identity plus an ephemeral per-session password; expired sessions trigger cleanup and assignment eviction. | `specs/SECURITY.md` | partially_enforced | `session.acquire` credential issuance + `ensure_session_valid` password check + stale-session cleanup hook | Enforced for active command auth path; stronger cryptographic hardening may be added later. | diff --git a/constitution/plugins/CONTAINER.md b/constitution/plugins/CONTAINER.md index 000c2d31..899b4bfb 100644 --- a/constitution/plugins/CONTAINER.md +++ b/constitution/plugins/CONTAINER.md @@ -36,6 +36,16 @@ Container subsystem runs agent actions in ephemeral Docker/Podman containers wit - Add only stack packages inferred from repo markers (`Cargo.toml`, `package.json`, `pyproject.toml`, `go.mod`). - Accept operator overrides via `DECAPOD_CONTAINER_APK_PACKAGES`. +## Validation Scope Inside Container + +**Container validate is for build verification only.** When running `decapod validate` inside a Docker container: + +- **Intended purpose:** Verify code compiles, tests pass, lint passes - confirm the work is legitimate and built correctly +- **NOT enforced inside container:** Git workspace context gates (container signals, worktree isolation, commit-often) +- **Exit then push:** After validate passes inside container, exit the container and perform Git operations (commit, push, PR) on the host + +This ensures reproducible builds in the clean container environment while keeping Git operations (which require host git config, SSH keys, gh CLI) outside the container where they belong. + ## Operator Runbook 1. Run isolated task worktree from master: `decapod auto container run --agent clawdious --task-id R_01ABC --cmd "cargo test -q"` diff --git a/src/core/validate.rs b/src/core/validate.rs index d8e56371..dd647fd5 100644 --- a/src/core/validate.rs +++ b/src/core/validate.rs @@ -42,6 +42,27 @@ fn is_inside_git_work_tree(repo_root: &Path) -> bool { .unwrap_or(false) } +fn container_signal_reasons(repo_root: &Path) -> Vec<&'static str> { + [ + ( + std::env::var("DECAPOD_CONTAINER").ok().as_deref() == Some("1"), + "DECAPOD_CONTAINER=1", + ), + (repo_root.join(".dockerenv").exists(), ".dockerenv marker"), + ( + repo_root.join(".devcontainer").exists(), + ".devcontainer marker", + ), + ( + std::env::var("DOCKER_CONTAINER").is_ok(), + "DOCKER_CONTAINER env", + ), + ] + .into_iter() + .filter_map(|(signal, name)| signal.then_some(name)) + .collect() +} + /// Spawn a validation gate in a rayon scope with timing and error capture. /// /// Replaces ~10 lines of boilerplate per gate with a single invocation. @@ -3433,36 +3454,17 @@ fn validate_git_workspace_context( return Ok(()); } - let signals_container = [ - ( - std::env::var("DECAPOD_CONTAINER").ok().as_deref() == Some("1"), - "DECAPOD_CONTAINER=1", - ), - (repo_root.join(".dockerenv").exists(), ".dockerenv marker"), - ( - repo_root.join(".devcontainer").exists(), - ".devcontainer marker", - ), - ( - std::env::var("DOCKER_CONTAINER").is_ok(), - "DOCKER_CONTAINER env", - ), - ]; - - let in_container = signals_container.iter().any(|(signal, _)| *signal); - if in_container { - let reasons: Vec<&str> = signals_container - .iter() - .filter(|(signal, _)| *signal) - .map(|(_, name)| *name) - .collect(); - pass( + let container_reasons = container_signal_reasons(repo_root); + if !container_reasons.is_empty() { + skip( &format!( - "Running in container workspace (signals: {})", - reasons.join(", ") + "Container-detected: git workspace gates skipped (signals: {}; validate verifies build only - commit/push/PR happens on host after container exit)", + container_reasons.join(", ") ), ctx, ); + validate_commit_often_gate(ctx, repo_root)?; + return Ok(()); } else { fail( "Not running in container workspace - git-tracked work must execute in Docker-isolated workspace (claim.git.container_workspace_required)", @@ -3478,11 +3480,6 @@ fn validate_git_workspace_context( if is_worktree { pass("Running in git worktree (isolated branch)", ctx); - } else if in_container { - pass( - "Container workspace detected (worktree check informational)", - ctx, - ); } else { fail( "Not running in isolated git worktree - must use container workspace for implementation work", @@ -3650,6 +3647,18 @@ fn validate_git_protected_branch( return Ok(()); } + let container_reasons = container_signal_reasons(repo_root); + if !container_reasons.is_empty() { + skip( + &format!( + "Git protected branch gate skipped inside container (signals: {}; host performs commit/push/PR checks after container exit)", + container_reasons.join(", ") + ), + ctx, + ); + return Ok(()); + } + let protected_patterns = ["master", "main", "production", "stable"]; let current_branch = {