feat: STATE_TX_FROM_STATES on state-tx nodes; workflow-script composite invariant; refresh legacy-mount NOTE
Claude-Session: https://claude.ai/code/session_01N218bERyNeLhbLmDRDvbWF
This commit is contained in:
@@ -57,15 +57,22 @@ nodes:
|
|||||||
runtime_env:
|
runtime_env:
|
||||||
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
STATE_TX_NEW_STATE: verify-pending
|
STATE_TX_NEW_STATE: verify-pending
|
||||||
# NOTE (script-node credentials, M43): kind: script nodes are harness-exempt
|
STATE_TX_FROM_STATES: "pending,open"
|
||||||
# (WF-COMP-6) but now carry the AU-51 internal-service bearer token by
|
# NOTE (script-node credentials): kind: script nodes are harness-exempt
|
||||||
# default — see spec/typed-workflows.md WF-COMP-26. The compiler emits
|
# (WF-COMP-6), but the flag-on script path composes the workflow-script/v1
|
||||||
# `secrets_required: [CP_SERVICE_TOKEN]` and
|
# composite (shared-variables/v1 + cp-service-token/v1; WF-COMP-31). The
|
||||||
# `runtime_env.CP_SERVICE_TOKEN_FILE=/run/secrets/CP_SERVICE_TOKEN` on
|
# cp-service-token/v1 layer restages the AU-51 internal-service bearer token
|
||||||
# every script node; the dispatcher's `_run_script_task` legacy-key
|
# and exports CP_SERVICE_TOKEN_FILE=/run/agent/cp-service-token/token, which
|
||||||
# branch mounts the dispatcher-process-env CP_SERVICE_TOKEN value as a
|
# all three nodes above read via
|
||||||
# file. All three nodes above read it via
|
# `open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()`. The legacy
|
||||||
# `open(os.environ["CP_SERVICE_TOKEN_FILE"]).read().strip()`. Each script
|
# WF-COMP-26 mount (`secrets_required: [CP_SERVICE_TOKEN]` and
|
||||||
# still resolves its own actor identity (OIDC_IDENTITY_EMAIL or
|
# `runtime_env.CP_SERVICE_TOKEN_FILE=/run/secrets/CP_SERVICE_TOKEN`, backed by
|
||||||
|
# the dispatcher's legacy-key branch) is removed (WF-COMP-33 step 10). Each
|
||||||
|
# script still resolves its own actor identity (OIDC_IDENTITY_EMAIL or
|
||||||
# CP_Caller-Identity header — see scripts/workflow/tag-set and
|
# CP_Caller-Identity header — see scripts/workflow/tag-set and
|
||||||
# parent-link-check) for the write envelope.
|
# parent-link-check) for the write envelope.
|
||||||
|
#
|
||||||
|
# STATE_TX_FROM_STATES on the state-tx node is the compare-and-set guard: the
|
||||||
|
# live flow_state must be one of the listed states (or already the target),
|
||||||
|
# else state-tx exits 2 (unexpected_current_state). pending,open mirror
|
||||||
|
# `input.state` + `escalation_input_states`.
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ nodes:
|
|||||||
runtime_env:
|
runtime_env:
|
||||||
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
STATE_TX_NEW_STATE: test-skeleton-ready
|
STATE_TX_NEW_STATE: test-skeleton-ready
|
||||||
|
STATE_TX_FROM_STATES: scoped-tagged
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ nodes:
|
|||||||
runtime_env:
|
runtime_env:
|
||||||
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
CP_URL: "http://controlplane.agent-runtimes.svc.cluster.local:8100"
|
||||||
STATE_TX_NEW_STATE: test-validated
|
STATE_TX_NEW_STATE: test-validated
|
||||||
|
STATE_TX_FROM_STATES: test-write-pending
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""state-tx script nodes must declare CP_URL and STATE_TX_NEW_STATE.
|
"""state-tx script nodes must declare CP_URL, STATE_TX_NEW_STATE and STATE_TX_FROM_STATES.
|
||||||
|
|
||||||
scripts/workflow/state-tx fails with exit 2 when either is missing.
|
scripts/workflow/state-tx fails with exit 2 when any is missing
|
||||||
|
(from_states_missing for STATE_TX_FROM_STATES).
|
||||||
CP_SERVICE_TOKEN_FILE is supplied by the workflow-script/v1 composite, not
|
CP_SERVICE_TOKEN_FILE is supplied by the workflow-script/v1 composite, not
|
||||||
runtime_env, and must not be declared here. STATE_TX_NEW_STATE must equal the
|
runtime_env, and must not be declared here. STATE_TX_NEW_STATE must equal the
|
||||||
template's own output.state.
|
template's own output.state. STATE_TX_FROM_STATES is a comma list of acceptable
|
||||||
|
current flow_states and must be non-empty.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -14,10 +16,27 @@ import yaml
|
|||||||
|
|
||||||
_DIR = Path(__file__).resolve().parents[2] / "task-templates" / "workflow"
|
_DIR = Path(__file__).resolve().parents[2] / "task-templates" / "workflow"
|
||||||
|
|
||||||
|
# Every template with a state-tx node, discovered from disk so new @N variants
|
||||||
@pytest.mark.parametrize(
|
# are covered automatically.
|
||||||
"name", ["test-validate@1.yaml", "frozen-fixture-emit@1.yaml"]
|
_NAMES = sorted(
|
||||||
|
p.name
|
||||||
|
for p in _DIR.glob("*.yaml")
|
||||||
|
if any(
|
||||||
|
n.get("cmd") == "scripts/workflow/state-tx"
|
||||||
|
for n in (yaml.safe_load(p.read_text()).get("nodes") or [])
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_tx_templates_discovered():
|
||||||
|
assert {
|
||||||
|
"test-validate@1.yaml",
|
||||||
|
"frozen-fixture-emit@1.yaml",
|
||||||
|
"bug-ancestor-auto-advance@1.yaml",
|
||||||
|
} <= set(_NAMES)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", _NAMES)
|
||||||
def test_state_tx_node_declares_required_runtime_env(name):
|
def test_state_tx_node_declares_required_runtime_env(name):
|
||||||
doc = yaml.safe_load((_DIR / name).read_text())
|
doc = yaml.safe_load((_DIR / name).read_text())
|
||||||
nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"]
|
nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"]
|
||||||
@@ -27,3 +46,17 @@ def test_state_tx_node_declares_required_runtime_env(name):
|
|||||||
assert env.get("STATE_TX_NEW_STATE") == doc["output"]["state"]
|
assert env.get("STATE_TX_NEW_STATE") == doc["output"]["state"]
|
||||||
assert "CP_SERVICE_TOKEN_FILE" not in env
|
assert "CP_SERVICE_TOKEN_FILE" not in env
|
||||||
assert not any(k.startswith("OIDC_") for k in env)
|
assert not any(k.startswith("OIDC_") for k in env)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", _NAMES)
|
||||||
|
def test_state_tx_node_declares_from_states(name):
|
||||||
|
doc = yaml.safe_load((_DIR / name).read_text())
|
||||||
|
nodes = [n for n in doc["nodes"] if n.get("cmd") == "scripts/workflow/state-tx"]
|
||||||
|
env = nodes[0].get("runtime_env") or {}
|
||||||
|
raw = env.get("STATE_TX_FROM_STATES")
|
||||||
|
assert isinstance(raw, str) and raw.strip()
|
||||||
|
states = [s.strip() for s in raw.split(",")]
|
||||||
|
assert all(states), f"empty entry in STATE_TX_FROM_STATES {raw!r}"
|
||||||
|
assert doc["output"]["state"] not in states
|
||||||
|
# The template's own input state must be an acceptable from-state.
|
||||||
|
assert doc["input"]["state"] in states
|
||||||
|
|||||||
34
tests/invariants/test_workflow_script_composite_layers.py
Normal file
34
tests/invariants/test_workflow_script_composite_layers.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""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_then_cp_service_token():
|
||||||
|
doc = _load("composites", "workflow-script")
|
||||||
|
assert doc["layers"] == [
|
||||||
|
{"context": "shared-variables/v1"},
|
||||||
|
{"context": "cp-service-token/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"
|
||||||
Reference in New Issue
Block a user