diff --git a/harnesses/composites/workflow-script-nocp/v1/harness.yaml b/harnesses/composites/workflow-script-nocp/v1/harness.yaml new file mode 100644 index 0000000..e3647b5 --- /dev/null +++ b/harnesses/composites/workflow-script-nocp/v1/harness.yaml @@ -0,0 +1,16 @@ +kind: composite +name: workflow-script-nocp +version: 1 +description: "Internal composite for kind: script nodes with cp_write=false (typed-workflows WF-COMP-31b, Option C). Brings the shared-variables filesystem context and the read-only project-repo clone, and NO CP credential: it layers neither cp-service-token/v1 (the AU-51 internal bearer) nor cp-write-token/v1 (the AU-55 per-step write token). Its only forge credential is repo-readonly/v1's read-only deploy key. Not model-bearing; the compiled shape is cli=\"shell\", params.cmd=." + +# Top-down order (WF-COMP-31b / WF-COMP-34): shared-variables first, repo-readonly +# LAST so its clone runs after the variable wiring and before the script starts. +# Never gitea-ssh/v1 (bug 45a48685). repo-readonly/v1 cannot be dropped: the +# entrypoint V2 guard trips when AGENT_REPO_URL is set and /workspace/project is +# missing, and every shipped script template declares agent_repo. +layers: + # spec/workflow-variables.md WV-19 / spec/harness.md HD-14 — agent-facing + # /variables/ context; provides the [shared-variables] tag. + - context: shared-variables/v1 + # spec/typed-workflows.md WF-COMP-34 — read-only project-repo clone. + - context: repo-readonly/v1 diff --git a/harnesses/composites/workflow-script-write/v1/harness.yaml b/harnesses/composites/workflow-script-write/v1/harness.yaml new file mode 100644 index 0000000..63f3dd5 --- /dev/null +++ b/harnesses/composites/workflow-script-write/v1/harness.yaml @@ -0,0 +1,16 @@ +kind: composite +name: workflow-script-write +version: 1 +description: "Internal composite for kind: script nodes with cp_write=true (typed-workflows WF-COMP-31c, Option C). Brings the shared-variables filesystem context, the AU-55 per-step CP write token (cp-write-token/v1 sets CP_WRITE_TOKEN_FILE; the dispatcher delivers the wfcw_ file into the script container only, AU-55b) and the read-only project-repo clone. Omits cp-service-token/v1: a cp_write=true container gets the AU-55 token only, never the AU-51 internal bearer. Not model-bearing; the compiled shape is cli=\"shell\", params.cmd=." + +# Top-down order (WF-COMP-31c / WF-COMP-34): shared-variables, cp-write-token, +# repo-readonly LAST. Never gitea-ssh/v1 (bug 45a48685) and never +# cp-service-token/v1. +layers: + # spec/workflow-variables.md WV-19 / spec/harness.md HD-14. + - context: shared-variables/v1 + # spec/authentication.md AU-55 / AU-55b — env-only layer; exports + # CP_WRITE_TOKEN_FILE=/run/agent/cp-write-token/token. + - context: cp-write-token/v1 + # spec/typed-workflows.md WF-COMP-34 — read-only project-repo clone. + - context: repo-readonly/v1 diff --git a/tests/invariants/test_workflow_script_option_c_composites.py b/tests/invariants/test_workflow_script_option_c_composites.py new file mode 100644 index 0000000..116ed30 --- /dev/null +++ b/tests/invariants/test_workflow_script_option_c_composites.py @@ -0,0 +1,94 @@ +"""workflow-script-nocp/v1 and workflow-script-write/v1 (Option C, WF-COMP-31b/31c). + +Layer order is load-bearing: shared-variables first, repo-readonly LAST +(WF-COMP-34), and neither composite may carry the AU-51 internal bearer +(cp-service-token/v1) or the write-capable gitea-ssh/v1. Only the write +composite carries the AU-55 per-step token layer. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + +from tests.invariants.forge_credentials import _layer_has_forge_credentials + +_ROOT = Path(__file__).resolve().parents[2] / "harnesses" +_KIND_TO_DIR = {"context": "contexts", "capability": "capabilities", "composite": "composites"} + +_NOCP = [{"context": "shared-variables/v1"}, {"context": "repo-readonly/v1"}] +_WRITE = [ + {"context": "shared-variables/v1"}, + {"context": "cp-write-token/v1"}, + {"context": "repo-readonly/v1"}, +] + + +def _load(kind_dir: str, name: str) -> dict: + return yaml.safe_load((_ROOT / kind_dir / name / "v1" / "harness.yaml").read_text()) + + +def _merged_env(doc: dict) -> dict: + env: dict = dict(doc.get("env") or {}) + for layer in doc["layers"]: + (kind, ref), = layer.items() + env.update(_load(_KIND_TO_DIR[kind], ref.split("/")[0]).get("env") or {}) + return env + + +@pytest.mark.parametrize( + "name,expected", [("workflow-script-nocp", _NOCP), ("workflow-script-write", _WRITE)] +) +def test_composite_layers_exact_order_repo_readonly_last(name, expected): + doc = _load("composites", name) + assert doc["kind"] == "composite" + assert doc["name"] == name + assert doc["version"] == 1 + assert doc["layers"] == expected + assert doc["layers"][-1] == {"context": "repo-readonly/v1"} + + +@pytest.mark.parametrize("name", ["workflow-script-nocp", "workflow-script-write"]) +def test_composite_never_carries_internal_bearer_or_write_capable_forge_key(name): + doc = _load("composites", name) + names = [next(iter(layer.values())).split("/")[0] for layer in doc["layers"]] + assert "cp-service-token" not in names + assert "gitea-ssh" not in names + for layer in doc["layers"]: + (kind, ref), = layer.items() + sub = _load(_KIND_TO_DIR[kind], ref.split("/")[0]) + if _layer_has_forge_credentials(sub): + assert sub.get("write_targets") == [], f"{ref} must be read-only (HC-11)" + + +@pytest.mark.parametrize("name", ["workflow-script-nocp", "workflow-script-write"]) +def test_every_referenced_layer_exists_and_names_match(name): + doc = _load("composites", name) + for layer in doc["layers"]: + (kind, ref), = layer.items() + lname, ver = ref.split("/") + path = _ROOT / _KIND_TO_DIR[kind] / lname / ver / "harness.yaml" + assert path.exists(), path + assert yaml.safe_load(path.read_text())["name"] == lname + + +def test_nocp_composite_exports_no_cp_credential_env(): + env = _merged_env(_load("composites", "workflow-script-nocp")) + assert "CP_SERVICE_TOKEN_FILE" not in env + assert "CP_WRITE_TOKEN_FILE" not in env + assert env["WORKFLOW_VARIABLES_ROOT"] == "/variables" + + +def test_write_composite_env_contract(): + env = _merged_env(_load("composites", "workflow-script-write")) + assert env["CP_WRITE_TOKEN_FILE"] == "/run/agent/cp-write-token/token" + assert "CP_SERVICE_TOKEN_FILE" not in env + assert env["WORKFLOW_VARIABLES_ROOT"] == "/variables" + + +def test_workflow_script_write_composite_layers_and_env_contract(): + """Spec WF-COMP-31c test handle (framework repo).""" + doc = _load("composites", "workflow-script-write") + assert doc["layers"] == _WRITE + assert _merged_env(doc)["CP_WRITE_TOKEN_FILE"] == "/run/agent/cp-write-token/token"