#!/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 ==="