From ea7c11265b7f029909f5160a71072730818bfd50 Mon Sep 17 00:00:00 2001 From: Paul O'Reilly Date: Fri, 2 Oct 2026 20:09:30 +1300 Subject: [PATCH] fix(repo-readonly): own read-only deploy key; workflow-script drops write-capable gitea-ssh/v1 repo-readonly/v1 now declares write_targets: [], an ssh_hosts alias and its own secrets_required (account platform-readonly.space:platform, a read-only Gitea deploy key), and stages the key before cloning. workflow-script/v1 no longer layers gitea-ssh/v1, so script nodes cannot push. Invariants updated: credential layers in the composite must declare write_targets: []. Bug 45a48685-23e9-4605-af1f-d93306108cef Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF --- .../workflow-script/v1/harness.yaml | 1 - .../contexts/repo-readonly/v1/harness.yaml | 17 +++++++++++- harnesses/contexts/repo-readonly/v1/init.sh | 12 +++++++++ .../test_workflow_script_composite_layers.py | 3 ++- ...test_workflow_script_composite_resolves.py | 26 +++++++++---------- 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/harnesses/composites/workflow-script/v1/harness.yaml b/harnesses/composites/workflow-script/v1/harness.yaml index d57386c..f1dc995 100644 --- a/harnesses/composites/workflow-script/v1/harness.yaml +++ b/harnesses/composites/workflow-script/v1/harness.yaml @@ -32,5 +32,4 @@ layers: # 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 index 93650d2..320ce2b 100644 --- a/harnesses/contexts/repo-readonly/v1/harness.yaml +++ b/harnesses/contexts/repo-readonly/v1/harness.yaml @@ -5,6 +5,11 @@ description: "Read-only project-repo clone for harness-path kind: script nodes ( requires: [] provides: [] +# HC-11: read-only layer. No push target; the credential below is a read-only +# deploy key (Gitea key id 20 on skynet/agent-runtimes), so a script node +# cannot write even if its code tries (bug 45a48685). +write_targets: [] + # 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 @@ -21,4 +26,14 @@ env: scripts: init: "./init.sh" -secrets_required: [] +ssh_hosts: + - alias: gitea.oreillyit.nz-ai-enablement + hostname: gitea.oreillyit.nz + user: git + identity_secret: GITEA_SSH_KEY + +secrets_required: + - name: gitea-ssh-readonly + account_ref: "platform-readonly.space:platform" + mount_path: /run/agent/secrets/gitea-ssh-readonly + mode: "0400" diff --git a/harnesses/contexts/repo-readonly/v1/init.sh b/harnesses/contexts/repo-readonly/v1/init.sh index 93211bb..d5b8d0a 100755 --- a/harnesses/contexts/repo-readonly/v1/init.sh +++ b/harnesses/contexts/repo-readonly/v1/init.sh @@ -52,6 +52,18 @@ if [ -d /workspace/project ] && [ -n "$(ls -A /workspace/project 2>/dev/null)" ] exit 0 fi +# Stage the ESO-mounted read-only deploy key where the harness-init +# ~/.ssh/config IdentityFile expects it (same destination gitea-ssh/v1 used; +# that layer is no longer part of workflow-script/v1 — bug 45a48685). +SSH_KEY_SRC="/run/agent/secrets/gitea-ssh-readonly/private_key" +SSH_KEY_DST="/home/agent/.ssh/gitea-oreillyit-nz-ai-enablement" +if [ ! -r "$SSH_KEY_SRC" ]; then + echo "ERROR: read-only SSH key not found at $SSH_KEY_SRC — ESO ExternalSecret not Ready?" >&2 + exit 1 +fi +mkdir -p /home/agent/.ssh +install -m 0600 -o agent -g agent "$SSH_KEY_SRC" "$SSH_KEY_DST" + mkdir -p /workspace echo "Cloning (read-only): $AGENT_REPO_URL -> /workspace/project" diff --git a/tests/invariants/test_workflow_script_composite_layers.py b/tests/invariants/test_workflow_script_composite_layers.py index 3d3824b..28489cb 100644 --- a/tests/invariants/test_workflow_script_composite_layers.py +++ b/tests/invariants/test_workflow_script_composite_layers.py @@ -16,11 +16,12 @@ def _load(kind_dir: str, name: str) -> dict: return yaml.safe_load((_ROOT / kind_dir / name / "v1" / "harness.yaml").read_text()) -def test_workflow_script_layers_shared_variables_then_cp_service_token(): +def test_workflow_script_layers_shared_variables_cp_service_token_repo_readonly(): doc = _load("composites", "workflow-script") assert doc["layers"] == [ {"context": "shared-variables/v1"}, {"context": "cp-service-token/v1"}, + {"context": "repo-readonly/v1"}, ] diff --git a/tests/invariants/test_workflow_script_composite_resolves.py b/tests/invariants/test_workflow_script_composite_resolves.py index 5c81889..81e57b4 100644 --- a/tests/invariants/test_workflow_script_composite_resolves.py +++ b/tests/invariants/test_workflow_script_composite_resolves.py @@ -203,27 +203,25 @@ def test_cp_service_token_layer_already_sets_cp_service_token_file_env(): ) -def test_workflow_script_composite_no_forge_credentials_no_write_targets_needed(): - """workflow-script/v1 has no forge credentials of its own and so does - NOT need to declare ``write_targets`` (HC-11). This is the inverse of - the forge-credentials invariant: a non-credential composite is fine - without that field. +def test_workflow_script_composite_forge_credentials_are_read_only(): + """Any layer in workflow-script/v1 that carries a forge credential MUST + declare ``write_targets: []`` (HC-11): a script node may read the project + repo but never push. The write-capable gitea-ssh/v1 layer must not be + present (bug 45a48685). - Spec: spec/harness.md HC-11 (only layers with forge credentials MUST - declare write_targets). + Spec: spec/harness.md HC-11, spec/typed-workflows.md WF-COMP-34. """ parsed = _load_layer("composites", "workflow-script") - # Walk every layer entry, ensure none introduce a forge credential. from tests.invariants.forge_credentials import _layer_has_forge_credentials for layer in parsed["layers"]: kind_name, ref = next(iter(layer.items())) name = ref.split("/")[0] + assert name != "gitea-ssh", "workflow-script/v1 must not layer the write-capable gitea-ssh/v1 (bug 45a48685)" kind_dir = _KIND_TO_DIR[kind_name] sub = _load_layer(kind_dir, name) - assert not _layer_has_forge_credentials(sub), ( - f"workflow-script/v1's layer {name} introduced a forge credential; " - f"HC-11 would then require write_targets to be declared on the " - f"composite, and a script-node composite should never carry " - f"forge credentials (WF-COMP-31)." - ) \ No newline at end of file + if _layer_has_forge_credentials(sub): + assert sub.get("write_targets") == [], ( + f"workflow-script/v1's layer {name} carries a forge credential " + f"but does not declare write_targets: [] (HC-11 read-only)." + )