R3b [fw]: delete legacy workflow-script/v1 composite and cp-service-token/v1 context (dbc5efc6)
Control plane 0.0.5.1890 no longer falls back to either. Invariant tests now assert both stay gone. Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
This commit is contained in:
@@ -1,35 +0,0 @@
|
|||||||
kind: composite
|
|
||||||
name: workflow-script
|
|
||||||
version: 1
|
|
||||||
description: "Internal composite for the flag-on kind: script path (typed-workflows WF-COMP-31 / workflow-variables WV-19). Brings the shared-variables filesystem context AND the AU-51 CP write credential (cp-service-token/v1 sets CP_SERVICE_TOKEN_FILE in the entrypoint process env before shell_runner runs). NOT a model-bearing composite — the kind: script compiled shape is cli=\"shell\", params.cmd=<cmd>; this composite provides the harness layers that path needs and nothing else."
|
|
||||||
|
|
||||||
# Order matters only for HC-6 env last-writer-wins / HC-8 git identity. The two
|
|
||||||
# layers here have disjoint env vars (shared-variables sets WORKFLOW_VARIABLES_*
|
|
||||||
# paths, cp-service-token sets CP_SERVICE_TOKEN_FILE) and no overlapping git
|
|
||||||
# identity, so the listed order has no observable merge effect — both layers
|
|
||||||
# reach the script-node container either way.
|
|
||||||
layers:
|
|
||||||
# spec/workflow-variables.md WV-19 / spec/harness.md HD-14 — agent-facing
|
|
||||||
# /variables/ context (CLAUDE.md mounted at /opt/harness/context/shared-variables/CLAUDE.md)
|
|
||||||
# plus WORKFLOW_VARIABLES_ROOT=/variables and templated WORKFLOW_VARIABLES_SELF=<step_path>.
|
|
||||||
# Provides the [shared-variables] tag used by the dispatcher's extra_layers injection.
|
|
||||||
- context: shared-variables/v1
|
|
||||||
# spec/typed-workflows.md WF-COMP-31 "AU-51 credential env seam" — this layer's
|
|
||||||
# init.sh restages the ESO-mounted CP-service-token secret (root-owned mode 0400)
|
|
||||||
# to an agent-owned 0600 copy at /run/agent/cp-service-token/token and exports
|
|
||||||
# CP_SERVICE_TOKEN_FILE pointing at it. Verified: env.CP_SERVICE_TOKEN_FILE is set
|
|
||||||
# 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
|
|
||||||
# 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: repo-readonly/v1
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
kind: context
|
|
||||||
name: cp-service-token
|
|
||||||
version: 1
|
|
||||||
description: "CP-internal service bearer token — authenticates agent-container write calls back to the control plane (AU-51/AU-51b, bug b3a96acc)"
|
|
||||||
requires: []
|
|
||||||
provides: []
|
|
||||||
|
|
||||||
# AU-51b: the ESO-mounted secret is root-owned mode 0400 (init.sh runs as
|
|
||||||
# root, before the gosu drop to the agent user, uid 1000) — unreadable by
|
|
||||||
# the agent process directly. Confirmed live 2026-09-03: a real agent
|
|
||||||
# session hit exactly this ("cp-cli invocation can't proceed... token is
|
|
||||||
# root-owned mode 0400 so it's not readable by the agent user"). Mirrors
|
|
||||||
# anthropic-cloud-paul-oauth/v1's pattern: init.sh restages a 0600
|
|
||||||
# agent-owned copy outside the ESO mount. CP_SERVICE_TOKEN_FILE points at
|
|
||||||
# the restaged copy, not the raw mount.
|
|
||||||
#
|
|
||||||
# The value at this path MUST match the CP's CP_INTERNAL_BEARER_TOKEN
|
|
||||||
# (same account: cp-decompose-service-token, account_id
|
|
||||||
# 1d963673-6ac9-4f85-875a-2ce5323e76ad, owner (cp, cp)).
|
|
||||||
env:
|
|
||||||
CP_SERVICE_TOKEN_FILE: /run/agent/cp-service-token/token
|
|
||||||
|
|
||||||
scripts:
|
|
||||||
init: "./init.sh"
|
|
||||||
|
|
||||||
secrets_required:
|
|
||||||
- name: cp-service-token
|
|
||||||
account_ref: "1d963673-6ac9-4f85-875a-2ce5323e76ad"
|
|
||||||
mount_path: /run/agent/secrets/cp-service-token
|
|
||||||
mode: "0400"
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# cp-service-token init — restage the ESO-mounted secret to an agent-owned
|
|
||||||
# copy.
|
|
||||||
#
|
|
||||||
# Threat model: keep the ESO mount root-only (mode 0400) so the agent user
|
|
||||||
# cannot directly `cat` /run/agent/secrets/cp-service-token/*. init.sh runs
|
|
||||||
# as root (in uid-wrapper.sh, before the gosu drop) and writes a restaged
|
|
||||||
# copy at /run/agent/cp-service-token/token with mode 0600 owned by the
|
|
||||||
# agent user. This mirrors anthropic-cloud-paul-oauth/v1/init.sh's pattern.
|
|
||||||
#
|
|
||||||
# Bug discovered live 2026-09-03: without this restaging step, cp-cli
|
|
||||||
# (invoked by the agent user, uid 1000) could not read the raw 0400
|
|
||||||
# root-owned mount at all — "cp_cli invocation can't proceed... token is
|
|
||||||
# root-owned mode 0400 so it's not readable by the agent user". The context
|
|
||||||
# originally set CP_SERVICE_TOKEN_FILE directly at the raw ESO mount path,
|
|
||||||
# which is the mistake this init.sh fixes.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
ESO_TOKEN="/run/agent/secrets/cp-service-token/value"
|
|
||||||
|
|
||||||
if [ ! -r "$ESO_TOKEN" ]; then
|
|
||||||
echo "ERROR: $ESO_TOKEN not readable. Check ESO ExternalSecret acct-1d963673." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
AGENT_USER="${AGENT_USER:-agent}"
|
|
||||||
|
|
||||||
DEST_DIR="/run/agent/cp-service-token"
|
|
||||||
mkdir -p "$DEST_DIR"
|
|
||||||
|
|
||||||
DEST_FILE="$DEST_DIR/token"
|
|
||||||
cp "$ESO_TOKEN" "$DEST_FILE"
|
|
||||||
|
|
||||||
chown "$AGENT_USER:" "$DEST_DIR" "$DEST_FILE" 2>/dev/null || true
|
|
||||||
chmod 0700 "$DEST_DIR"
|
|
||||||
chmod 0600 "$DEST_FILE"
|
|
||||||
|
|
||||||
echo "cp-service-token: restaged into $DEST_FILE (0600 $AGENT_USER:$AGENT_USER)"
|
|
||||||
32
tests/invariants/test_no_legacy_script_composite.py
Normal file
32
tests/invariants/test_no_legacy_script_composite.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""Track R / R3b: the legacy AU-51 bearer composite and layer stay deleted.
|
||||||
|
|
||||||
|
``workflow-script/v1`` (the legacy script composite) and the
|
||||||
|
``cp-service-token/v1`` context were removed once the control plane stopped
|
||||||
|
falling back to them (agent-runtimes 0.0.5.1890). Nothing may re-introduce
|
||||||
|
either, and no live composite may layer the bearer context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
_HARNESSES = _ROOT / "harnesses"
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_script_composite_directory_is_gone():
|
||||||
|
assert not (_HARNESSES / "composites" / "workflow-script").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_cp_service_token_context_directory_is_gone():
|
||||||
|
assert not (_HARNESSES / "contexts" / "cp-service-token").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_composite_layers_cp_service_token():
|
||||||
|
offenders = []
|
||||||
|
for path in sorted((_HARNESSES / "composites").glob("*/v*/harness.yaml")):
|
||||||
|
doc = yaml.safe_load(path.read_text()) or {}
|
||||||
|
for layer in doc.get("layers") or []:
|
||||||
|
if str(layer.get("context", "")).startswith("cp-service-token"):
|
||||||
|
offenders.append(str(path.relative_to(_ROOT)))
|
||||||
|
assert offenders == []
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
"""workflow-script/v1 composite: exact layer order and credential env export.
|
|
||||||
|
|
||||||
Script nodes get their CP write credential only through this composite
|
|
||||||
(the legacy WF-COMP-26 mount is removed), so the layering is load-bearing.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_ROOT = Path(__file__).resolve().parents[2] / "harnesses"
|
|
||||||
|
|
||||||
|
|
||||||
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_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"},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_env_exports_cp_service_token_file():
|
|
||||||
doc = _load("composites", "workflow-script")
|
|
||||||
env: dict = dict(doc.get("env") or {})
|
|
||||||
for layer in doc["layers"]:
|
|
||||||
(kind, ref), = layer.items()
|
|
||||||
name = ref.split("/")[0]
|
|
||||||
env.update(_load(kind + "s", name).get("env") or {})
|
|
||||||
assert env["CP_SERVICE_TOKEN_FILE"] == "/run/agent/cp-service-token/token"
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
"""Workflow-script harness composition invariant (spec/typed-workflows.md WF-COMP-31
|
|
||||||
+ spec/workflow-variables.md WV-19 + spec/harness.md HD-14).
|
|
||||||
|
|
||||||
The two new framework layers — ``harnesses/contexts/shared-variables/v1/`` and
|
|
||||||
``harnesses/composites/workflow-script/v1/`` — must exist on the tree, must parse,
|
|
||||||
and the composite must resolve every layer it references to a real file on disk.
|
|
||||||
This test guards against the most common regression: a missing or renamed layer
|
|
||||||
that would only surface at the dispatcher's ``compose_harness`` call, where it
|
|
||||||
costs an entire task's worth of wasted dispatch round-trip.
|
|
||||||
|
|
||||||
Spec: /home/paul/dev/claude/projects/agent-runtimes/spec/workflow-variables.md
|
|
||||||
(WV-19, WV-20, WF-COMP-31), spec/harness.md (HD-14).
|
|
||||||
|
|
||||||
Not committed by the authoring agent per its instructions — written here so it
|
|
||||||
can run locally against the current tree.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
_HARNESSES_ROOT = Path(__file__).resolve().parents[2] / "harnesses"
|
|
||||||
|
|
||||||
# Composite layer entries use the singular kind as the dict key. The kind maps
|
|
||||||
# to a top-level directory by adding "s".
|
|
||||||
_KIND_TO_DIR = {
|
|
||||||
"context": "contexts",
|
|
||||||
"capability": "capabilities",
|
|
||||||
"composite": "composites",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _load_layer(kind_dir: str, name: str) -> dict:
|
|
||||||
p = _HARNESSES_ROOT / kind_dir / name / "v1" / "harness.yaml"
|
|
||||||
return yaml.safe_load(p.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
|
|
||||||
def test_shared_variables_layer_exists_and_parses():
|
|
||||||
"""``harnesses/contexts/shared-variables/v1/harness.yaml`` must exist,
|
|
||||||
parse as YAML, and carry ``provides: [shared-variables]`` (the HD-14 tag).
|
|
||||||
|
|
||||||
Spec: spec/workflow-variables.md WV-19; spec/harness.md HD-14.
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("contexts", "shared-variables")
|
|
||||||
assert parsed["kind"] == "context"
|
|
||||||
assert parsed["name"] == "shared-variables"
|
|
||||||
assert parsed["version"] == 1
|
|
||||||
assert parsed["provides"] == ["shared-variables"], (
|
|
||||||
"WV-19: provides MUST contain the 'shared-variables' tag — HD-14's "
|
|
||||||
"extra_layers dedup key, and the tag the dispatcher's payload-marker "
|
|
||||||
"injection matches against."
|
|
||||||
)
|
|
||||||
# WV-20: this layer's OWN env: declaration sets the two WORKFLOW_VARIABLES_*
|
|
||||||
# vars (NOT the entrypoint, NOT the container backend).
|
|
||||||
env = parsed["env"]
|
|
||||||
assert env["WORKFLOW_VARIABLES_ROOT"] == "/variables"
|
|
||||||
assert "WORKFLOW_VARIABLES_SELF" in env, (
|
|
||||||
"WV-20: WORKFLOW_VARIABLES_SELF must be set by the layer's env: "
|
|
||||||
"(templated per-node at harness-composition time on the dispatcher)"
|
|
||||||
)
|
|
||||||
# Agent-facing context: a CLAUDE.md must be mounted under /opt/harness/context/.
|
|
||||||
files = parsed.get("context_files", [])
|
|
||||||
targets = [f["target"] for f in files]
|
|
||||||
assert any(t.startswith("/opt/harness/context/shared-variables/") for t in targets), (
|
|
||||||
"WV-19: agent-facing context must be mounted under /opt/harness/context/"
|
|
||||||
"(the HC-7 namespacing point) so the agent sees it at startup."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_shared_variables_claude_md_present_and_substantial():
|
|
||||||
"""The agent-facing CLAUDE.md must be a non-trivial markdown document
|
|
||||||
(HC-7 says it is read by the agent, not a placeholder).
|
|
||||||
|
|
||||||
Spec: spec/workflow-variables.md WV-19; spec/harness.md HC-7.
|
|
||||||
"""
|
|
||||||
md = _HARNESSES_ROOT / "contexts" / "shared-variables" / "v1" / "CLAUDE.md"
|
|
||||||
assert md.exists(), "WV-19: shared-variables/v1 must ship a CLAUDE.md"
|
|
||||||
text = md.read_text(encoding="utf-8")
|
|
||||||
assert text.count("\n") >= 30, (
|
|
||||||
f"WV-19: the CLAUDE.md must explain the /variables/ contract to the "
|
|
||||||
f"agent — got {text.count(chr(10))} lines, expected >=30."
|
|
||||||
)
|
|
||||||
# Spot-check the four high-value pieces of content the agent MUST see:
|
|
||||||
assert "/variables/" in text, "must explain the filesystem root"
|
|
||||||
assert "WORKFLOW_VARIABLES_SELF" in text, "must document the env vars"
|
|
||||||
assert "wfvar" in text, "must point at the CLI helper (WV-21)"
|
|
||||||
assert "reserved" in text.lower(), "must list the reserved-name families (WV-6)"
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_composite_resolves():
|
|
||||||
"""``harnesses/composites/workflow-script/v1/harness.yaml`` must declare
|
|
||||||
exactly the two layers the spec names (WF-COMP-31) and every referenced
|
|
||||||
file must exist on disk.
|
|
||||||
|
|
||||||
Spec: spec/typed-workflows.md WF-COMP-31 ("Internal composite layered from
|
|
||||||
``shared-variables/v1`` + ``cp-service-token/v1``").
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("composites", "workflow-script")
|
|
||||||
assert parsed["kind"] == "composite"
|
|
||||||
assert parsed["name"] == "workflow-script"
|
|
||||||
assert parsed["version"] == 1
|
|
||||||
assert isinstance(parsed["layers"], list) and len(parsed["layers"]) >= 2, (
|
|
||||||
"WF-COMP-31: the composite must layer at least shared-variables/v1 + "
|
|
||||||
"cp-service-token/v1; missing one breaks the credential env seam."
|
|
||||||
)
|
|
||||||
|
|
||||||
refs = []
|
|
||||||
for layer in parsed["layers"]:
|
|
||||||
assert isinstance(layer, dict) and len(layer) == 1, (
|
|
||||||
f"each layer entry must be a single-key dict (kind: ref), got {layer}"
|
|
||||||
)
|
|
||||||
kind_name, ref = next(iter(layer.items()))
|
|
||||||
assert kind_name in _KIND_TO_DIR, f"unknown layer kind {kind_name!r}"
|
|
||||||
assert "/" in ref, f"layer ref must be '<name>/v<N>', got {ref!r}"
|
|
||||||
name, ver = ref.split("/")
|
|
||||||
assert ver.startswith("v") and ver[1:].isdigit(), (
|
|
||||||
f"layer ref version must match v<N>, got {ver!r}"
|
|
||||||
)
|
|
||||||
kind_dir = _KIND_TO_DIR[kind_name]
|
|
||||||
layer_path = _HARNESSES_ROOT / kind_dir / name / ver / "harness.yaml"
|
|
||||||
assert layer_path.exists(), (
|
|
||||||
f"WF-COMP-31: composite references a layer that does not exist on "
|
|
||||||
f"the tree: {layer_path.relative_to(_HARNESSES_ROOT)}"
|
|
||||||
)
|
|
||||||
layer_data = yaml.safe_load(layer_path.read_text(encoding="utf-8"))
|
|
||||||
assert layer_data["name"] == name, (
|
|
||||||
f"name mismatch in {layer_path}: file says {layer_data['name']!r}, "
|
|
||||||
f"ref says {name!r}"
|
|
||||||
)
|
|
||||||
refs.append((kind_name, name, ver, layer_data))
|
|
||||||
|
|
||||||
# WF-COMP-31: the two SPECIFIED layers must both be present.
|
|
||||||
ref_names = {(kind, name) for kind, name, _, _ in refs}
|
|
||||||
assert ("context", "shared-variables") in ref_names, (
|
|
||||||
"WF-COMP-31: workflow-script/v1 MUST layer shared-variables/v1 "
|
|
||||||
"(WV-19 — the agent-facing /variables/ contract)"
|
|
||||||
)
|
|
||||||
assert ("context", "cp-service-token") in ref_names, (
|
|
||||||
"WF-COMP-31 / AU-51 credential env seam: workflow-script/v1 MUST "
|
|
||||||
"layer cp-service-token/v1 so CP_SERVICE_TOKEN_FILE is in the "
|
|
||||||
"entrypoint process env before shell_runner runs."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_script_composite_brings_no_model_or_endpoint_layer():
|
|
||||||
"""``kind: script`` nodes run via the shell runner — the composite MUST
|
|
||||||
NOT bring agent-only layers (no model, endpoint, CLAUDE.md context that
|
|
||||||
drives the agent runner, etc.). This is a structural regression gate: a
|
|
||||||
future refactor that adds e.g. ``anthropic-cloud-paul-oauth/v1`` here
|
|
||||||
would silently widen the script-node attack surface.
|
|
||||||
|
|
||||||
Spec: spec/typed-workflows.md WF-COMP-31 ("the flag-on kind: script
|
|
||||||
harness ... MUST NOT bring agent-only layers").
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("composites", "workflow-script")
|
|
||||||
forbidden_substrings = (
|
|
||||||
# cloud-auth contexts the agent runner needs
|
|
||||||
"anthropic-cloud", "minimax", "qwen-code", "deepseek-code",
|
|
||||||
"alibaba-model-studio", "fable-low-effort", "z-ai",
|
|
||||||
# the model-bearing capability / context layers
|
|
||||||
"code-methodology", "spec-writing", "planning", "integration",
|
|
||||||
# harness composites that bundle a model
|
|
||||||
"code-", "planning-", "spec-writing-", "best-practices-",
|
|
||||||
"sonnet-", "opus-", "minimax-", "haiku-", "airouter-", "fable-",
|
|
||||||
)
|
|
||||||
layer_names = [
|
|
||||||
next(iter(layer.values())).split("/")[0]
|
|
||||||
for layer in parsed["layers"]
|
|
||||||
]
|
|
||||||
bad = [
|
|
||||||
n for n in layer_names
|
|
||||||
if any(sub in n for sub in forbidden_substrings)
|
|
||||||
]
|
|
||||||
assert not bad, (
|
|
||||||
f"WF-COMP-31: workflow-script/v1 MUST NOT bring agent-only layers; "
|
|
||||||
f"found: {bad}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_cp_service_token_layer_already_sets_cp_service_token_file_env():
|
|
||||||
"""WF-COMP-31 'AU-51 credential env seam': cp-service-token/v1 must set
|
|
||||||
``CP_SERVICE_TOKEN_FILE`` in its env: declaration (the layer's init.sh
|
|
||||||
restages the ESO-mounted secret to an agent-readable 0600 copy at the
|
|
||||||
path CP_SERVICE_TOKEN_FILE points at). Verified here at the YAML level
|
|
||||||
rather than at runtime — the spec's claim is structural.
|
|
||||||
|
|
||||||
Spec: spec/typed-workflows.md WF-COMP-31; spec/harness.md AU-51/AU-51b
|
|
||||||
(bug b3a96acc).
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("contexts", "cp-service-token")
|
|
||||||
env = parsed["env"]
|
|
||||||
assert "CP_SERVICE_TOKEN_FILE" in env, (
|
|
||||||
"WF-COMP-31 'AU-51 credential env seam': cp-service-token/v1 must "
|
|
||||||
"declare CP_SERVICE_TOKEN_FILE in its env: block; without it the "
|
|
||||||
"shell_runner subprocess does not inherit the AU-51 credential."
|
|
||||||
)
|
|
||||||
fpath = env["CP_SERVICE_TOKEN_FILE"]
|
|
||||||
assert fpath.endswith("/token"), (
|
|
||||||
f"CP_SERVICE_TOKEN_FILE must point at the restaged 0600 file, not "
|
|
||||||
f"the raw 0400 ESO mount — got {fpath!r}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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, spec/typed-workflows.md WF-COMP-34.
|
|
||||||
"""
|
|
||||||
parsed = _load_layer("composites", "workflow-script")
|
|
||||||
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)
|
|
||||||
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)."
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user