Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion constitution/interfaces/CLAIMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
10 changes: 10 additions & 0 deletions constitution/plugins/CONTAINER.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
71 changes: 40 additions & 31 deletions src/core/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)",
Expand All @@ -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",
Expand Down Expand Up @@ -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 = {
Expand Down
Loading