diff --git a/harnesses/composites/workflow-script/v1/harness.yaml b/harnesses/composites/workflow-script/v1/harness.yaml index 7c901b1..d57386c 100644 --- a/harnesses/composites/workflow-script/v1/harness.yaml +++ b/harnesses/composites/workflow-script/v1/harness.yaml @@ -21,4 +21,16 @@ layers: # on the entrypoint process environment before shell_runner runs (entrypoint reads # os.environ via env=os.environ.copy() in subprocess.run at entrypoint/runners/shell.py:73, # so the same credential channel an agent node gets reaches a script node unchanged). - - context: cp-service-token/v1 \ No newline at end of file + - context: cp-service-token/v1 + # spec/typed-workflows.md WF-COMP-34 (bug 3bee78fa, 2026-10-02) — read-only + # project-repo clone for harness-path kind: script nodes. Layered LAST so + # its clone runs after the two AU-51 wiring layers and before the script + # starts (init.sh order: shared-variables -> cp-service-token -> repo-readonly). + # When AGENT_REPO_URL is unset (the pre-fix runtime, or any template that + # does not declare agent_repo) this layer is a no-op; the V2 guard + # (entrypoint/entrypoint.py:901-906) does NOT trip because the unset case + # is a no-op there too. Layered-in instead of layering agent-repo/v1 + # because that context's fork management and push side effects are wrong + # for a read-only script node (see bug item body). + - context: gitea-ssh/v1 + - context: repo-readonly/v1 diff --git a/harnesses/contexts/repo-readonly/v1/harness.yaml b/harnesses/contexts/repo-readonly/v1/harness.yaml new file mode 100644 index 0000000..93650d2 --- /dev/null +++ b/harnesses/contexts/repo-readonly/v1/harness.yaml @@ -0,0 +1,24 @@ +kind: context +name: repo-readonly +version: 1 +description: "Read-only project-repo clone for harness-path kind: script nodes (spec/typed-workflows.md WF-COMP-34, bug 3bee78fa). Clones AGENT_REPO_URL (the upstream project repo URL emitted by the WF-COMP-34 compiler emission as payload.agent_repo.url and propagated to the container env by the dispatcher) into /workspace/project at its default branch. No fork management, no AR-39 select-base, no scratchpad directory, no .gitignore munging, no finalize.sh push. The script node reads repo files only; it never writes back through a fork. Distinct from agent-repo/v1 (the agent-node branch-per-task fork-management context) on every one of those axes." +requires: [] +provides: [] + +# WF-COMP-34: this layer's init.sh is the read-only clone. It only runs +# when AGENT_REPO_URL is set; the unset case is a no-op (a script node +# whose template omits agent_repo keeps its pre-fix silent empty workspace +# runtime exactly, verified by +# tests/test_entrypoint.py::TestV2WorkspaceAssertions::test_v2_no_agent_repo_url_skips_workspace_check). +# +# AGENT_WORKING_DIR=/workspace/project is the env var +# entrypoint.py:_apply_working_dir reads (entrypoint/entrypoint.py:908-916), +# and the path entrypoint/runners/shell.py:67 sets as the subprocess cwd +# when context["workspace"] is unset and no clone action ran. +env: + AGENT_WORKING_DIR: /workspace/project + +scripts: + init: "./init.sh" + +secrets_required: [] diff --git a/harnesses/contexts/repo-readonly/v1/init.sh b/harnesses/contexts/repo-readonly/v1/init.sh new file mode 100755 index 0000000..196bc71 --- /dev/null +++ b/harnesses/contexts/repo-readonly/v1/init.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# repo-readonly init — read-only project-repo clone for harness-path +# kind: script nodes (spec/typed-workflows.md WF-COMP-34, bug 3bee78fa). +# +# Threat model: the script node is invoked by entrypoint/runners/shell.py +# with cwd=context["workspace"]; the existing entrypoint paths set +# context["workspace"] to /workspace/project from AGENT_WORKING_DIR +# (the env var this init.sh exports via its harness.yaml env field). +# The init.sh MUST clone AGENT_REPO_URL (the upstream project repo URL +# emitted by the WF-COMP-34 compiler emission as payload.agent_repo.url +# and propagated to the container env by the dispatcher) into +# /workspace/project at its default branch — read-only, no fork, no push, +# no AR-39 select-base, no scratchpad, no .gitignore munging. +# +# Bug discovered live 2026-10-02 (bug 3bee78fa, run +# 9a368e71-d6ab-4185-a98c-9b31d41bb4b5): without this layer, every +# harness-path kind: script node ran with /workspace/project missing, +# so any preflight that read repo files (e.g. preflight-test-collection +# invoking pytest --collect-only tests/test_k8s_gc.py) saw an empty cwd +# and either reported zero files or failed with the upstream tool's own +# misconfig error. The WF-COMP-34 compiler emission (this repo's +# controlplane/typed_workflows/compiler.py, bug 3bee78fa) writes +# payload.agent_repo onto every harness-path script node's manifest, so +# the URL reaches the env when the template (or the node) declares +# agent_repo. This init.sh is the other half — the layer that actually +# clones it. + +set -euo pipefail + +echo "=== repo-readonly/v1 init.sh ===" + +# WF-COMP-34 unset-case contract: if AGENT_REPO_URL is unset, this is a +# no-op. The script node keeps its pre-fix silent empty workspace +# runtime exactly — a template that omits agent_repo does not get a +# surprise clone. The entrypoint V2 guard (entrypoint/entrypoint.py:901-906) +# only fires when AGENT_REPO_URL IS set; the unset case is a no-op on +# both sides (no clone here, no V2 trip there), so the legacy +# script_on_harness=False path's observable behaviour is preserved +# verbatim. +if [ -z "${AGENT_REPO_URL:-}" ]; then + echo "AGENT_REPO_URL unset; no clone (WF-COMP-34 unset-case contract)." + exit 0 +fi + +# If the clone target already exists (e.g. a previous layer cloned into +# it), skip — never clobber. The framework composite order +# (shared-variables/v1 first, cp-service-token/v1 second, +# repo-readonly/v1 last) guarantees neither prior layer writes here. +if [ -d /workspace/project ]; then + echo "/workspace/project already exists; skipping clone." + exit 0 +fi + +mkdir -p /workspace + +echo "Cloning (read-only): $AGENT_REPO_URL -> /workspace/project" +# Read-only clone: no checkout of a task branch, no push target, no +# remote operations beyond the initial clone. --depth 1 is enough — +# a script node reads files, not history. If history is needed later, +# the spec can be amended; today every preflight is a pytest --collect-only +# or a prettier --check or similar that does not need commit ancestry. +if git clone --depth 1 "$AGENT_REPO_URL" /workspace/project; then + echo "Cloned $AGENT_REPO_URL to /workspace/project" +else + # Loud failure: a harness-path script node that declares agent_repo + # and finds the clone fails SHOULD fail the task at init time, not + # silently leave /workspace/project missing for the V2 guard to + # catch 26 seconds later (that failure mode is the symptom this + # bug was filed for). + echo "ERROR: Failed to clone $AGENT_REPO_URL" >&2 + exit 1 +fi + +# Strip any symlinks (security: prevent /proc/1/environ exfiltration; +# mirrors agent-repo/v1's defence). +find /workspace/project -type l -exec rm {} \; 2>/dev/null || true + +echo "AGENT_WORKING_DIR=${AGENT_WORKING_DIR:-/workspace/project}" +echo "=== repo-readonly/v1 init.sh complete ===" diff --git a/task-templates/workflow/frozen-fixture-emit@1.yaml b/task-templates/workflow/frozen-fixture-emit@1.yaml index 416c9c5..7857c17 100644 --- a/task-templates/workflow/frozen-fixture-emit@1.yaml +++ b/task-templates/workflow/frozen-fixture-emit@1.yaml @@ -12,6 +12,8 @@ scope_budget: files_modified_max: 2 lines_diff_max: 200 wall_clock_seconds_max: 600 +agent_repo: + url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git" nodes: - id: frozen-fixture-emit kind: script diff --git a/task-templates/workflow/test-validate@1.yaml b/task-templates/workflow/test-validate@1.yaml index c66660b..03da882 100644 --- a/task-templates/workflow/test-validate@1.yaml +++ b/task-templates/workflow/test-validate@1.yaml @@ -12,6 +12,8 @@ scope_budget: files_modified_max: 1 lines_diff_max: 50 wall_clock_seconds_max: 600 +agent_repo: + url: "git@gitea.oreillyit.nz-ai-enablement:skynet/agent-runtimes.git" nodes: - id: preflight-test-collection kind: script