feat(orchestration): compose security scenarios
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a02929-244b-7391-b933-c04010e8eedb
This commit is contained in:
tegwick 2026-08-23 12:40:52 +02:00
parent ad46cc89fc
commit d96aab2321
20 changed files with 1464 additions and 30 deletions

View file

@ -13,7 +13,7 @@ python3 tools/playbook-capability-contract/playbook_contract_validator.py \
../railiance-infra/capabilities/playbooks/railiance-infra.bootstrap-host.yaml
```
Validate and compose a sample scenario:
Validate and compose the legacy single-provider conformance sample:
```bash
python3 tools/playbook-capability-contract/playbook_contract_validator.py \
@ -21,6 +21,17 @@ python3 tools/playbook-capability-contract/playbook_contract_validator.py \
--scenario examples/playbook-capability-contract/scenario-s1-host-bootstrap.yaml
```
The `--scenario` path exists for v0.1 conformance compatibility. It refuses
ambiguous providers and unselected overrides, but it does not perform trust
sequencing or build a complete owner handoff. Use the canonical Security
Scenario Composer for deterministic multi-provider plans:
```bash
python3 tools/security-scenario-composer/security_scenario_composer.py \
--scenario examples/security-scenarios/c0-local-identity.yaml \
capabilities/playbooks/net-kingdom.local-identity.yaml
```
Run tests:
```bash

View file

@ -60,6 +60,9 @@ TRUST_STATES = {
"tenant_onboarding_trust",
}
SCENARIO_AUTHORITIES = {"platform", "netkingdom", "tenant"}
SECRET_REFERENCE_PATTERN = re.compile(
r"(?:[a-z][a-z0-9+.-]*://\S+|(?:/|\./|\.\./)\S+)"
)
@dataclass
@ -127,6 +130,10 @@ def is_type(value: Any, declared_type: str) -> bool:
return False
def is_secret_reference(value: Any) -> bool:
return isinstance(value, str) and SECRET_REFERENCE_PATTERN.fullmatch(value) is not None
def validate_constraints(value: Any, param: dict[str, Any], path: str) -> list[Issue]:
issues: list[Issue] = []
constraints = param.get("constraints", {})
@ -277,6 +284,18 @@ def validate_parameters(spec: dict[str, Any]) -> list[Issue]:
if item.get("sensitivity") in {"security_sensitive", "secret_reference"} and item.get("tuning_authority") == "tenant_tunable":
issues.append(issue("ERROR", path, "security-sensitive parameters cannot be tenant_tunable"))
if item.get("sensitivity") == "secret_reference":
if declared_type != "string":
issues.append(issue("ERROR", f"{path}.type", "secret_reference parameters must use string type"))
if "default" in item and not is_secret_reference(item["default"]):
issues.append(
issue(
"ERROR",
f"{path}.default",
"secret_reference default must be a URI or explicit absolute/relative path",
)
)
if "default" in item and declared_type in PARAM_TYPES and not is_type(item["default"], declared_type):
issues.append(issue("ERROR", f"{path}.default", f"default does not match type {declared_type!r}"))
if declared_type in PARAM_TYPES:
@ -405,6 +424,15 @@ def validate_override_allowed(param: dict[str, Any], value: Any, scenario_author
if scenario_authority == "tenant" and sensitivity in {"security_sensitive", "secret_reference"}:
issues.append(issue("ERROR", path, f"tenant authority cannot override {sensitivity} parameter {name!r}"))
if sensitivity == "secret_reference" and not is_secret_reference(value):
issues.append(
issue(
"ERROR",
path,
f"override for {name!r} must be a secret-reference URI or explicit absolute/relative path",
)
)
if not is_type(value, declared_type):
issues.append(issue("ERROR", path, f"override for {name!r} does not match type {declared_type!r}"))
issues.extend(validate_constraints(value, param, path))
@ -433,10 +461,23 @@ def compose_scenario(declarations: list[Declaration], scenario: dict[str, Any])
selected: list[Declaration] = []
for cap_id in required_caps:
matches = [declaration for declaration in declarations if cap_id in declaration.capabilities]
matches = sorted(
[declaration for declaration in declarations if cap_id in declaration.capabilities],
key=lambda declaration: declaration.id,
)
if not matches:
issues.append(issue("ERROR", "scenario.requires.capabilities", f"no declaration provides {cap_id!r}"))
continue
if len(matches) > 1:
issues.append(
issue(
"ERROR",
"scenario.requires.capabilities",
f"ambiguous providers {[declaration.id for declaration in matches]}; "
"use the Security Scenario Composer to pin one explicitly",
)
)
continue
selected.append(matches[0])
# Preserve order while deduplicating declarations selected for several capabilities.
@ -444,6 +485,16 @@ def compose_scenario(declarations: list[Declaration], scenario: dict[str, Any])
for declaration in selected:
selected_by_id.setdefault(declaration.id, declaration)
for declaration_id in overrides:
if declaration_id not in selected_by_id:
issues.append(
issue(
"ERROR",
f"scenario.parameter_overrides.{declaration_id}",
"override targets an unselected declaration",
)
)
composed = {
"scenario": scenario.get("id", "scenario:unnamed"),
"authority": authority,

View file

@ -142,6 +142,81 @@ def test_tenant_tunable_secret_reference_fails(tmp_path):
assert any("security-sensitive parameters cannot be tenant_tunable" in msg for msg in error_messages(issues))
def test_plaintext_secret_reference_default_fails(tmp_path):
data = valid_declaration()
data["spec"]["parameters"][2].update(
{
"type": "string",
"default": "plaintext-looking-value",
"sensitivity": "secret_reference",
"tuning_authority": "platform_only",
}
)
declaration = declaration_from(data, tmp_path)
issues = validator.validate_declaration(declaration)
assert any("secret_reference default must be" in msg for msg in error_messages(issues))
def test_secret_reference_uri_override_passes(tmp_path):
data = valid_declaration()
data["spec"]["parameters"][2].update(
{
"name": "credential_ref",
"type": "string",
"default": "openbao://kv/platform/reference",
"sensitivity": "secret_reference",
"tuning_authority": "platform_only",
}
)
declaration = declaration_from(data, tmp_path)
scenario = {
"id": "scenario:secret-reference",
"authority": "platform",
"requires": {"capabilities": ["s1.os-baseline"]},
"parameter_overrides": {
"railiance-infra.bootstrap-host": {
"target_hosts": ["railiance01"],
"credential_ref": "kubernetes://platform/reference#token",
}
},
}
issues, _ = validator.compose_scenario([declaration], scenario)
assert error_messages(issues) == []
def test_plaintext_secret_reference_override_fails(tmp_path):
data = valid_declaration()
data["spec"]["parameters"][2].update(
{
"name": "credential_ref",
"type": "string",
"default": "openbao://kv/platform/reference",
"sensitivity": "secret_reference",
"tuning_authority": "platform_only",
}
)
declaration = declaration_from(data, tmp_path)
scenario = {
"id": "scenario:bad-secret-reference",
"authority": "platform",
"requires": {"capabilities": ["s1.os-baseline"]},
"parameter_overrides": {
"railiance-infra.bootstrap-host": {
"target_hosts": ["railiance01"],
"credential_ref": "plaintext-looking-value",
}
},
}
issues, _ = validator.compose_scenario([declaration], scenario)
assert any("must be a secret-reference URI" in msg for msg in error_messages(issues))
def test_scenario_composition_selects_and_overrides(tmp_path):
declaration = declaration_from(valid_declaration(), tmp_path)
scenario = {
@ -196,3 +271,36 @@ def test_required_parameter_without_override_fails(tmp_path):
issues, _ = validator.compose_scenario([declaration], scenario)
assert any("required parameter has no default or override" in msg for msg in error_messages(issues))
def test_demo_composition_refuses_ambiguous_provider(tmp_path):
first = declaration_from(valid_declaration(), tmp_path)
second_data = valid_declaration()
second_data["metadata"]["id"] = "railiance-infra.bootstrap-host-alternative"
second_path = tmp_path / "alternative.yaml"
second_path.write_text(yaml.safe_dump(second_data, sort_keys=False), encoding="utf-8")
second = validator.Declaration(path=second_path, data=second_data)
scenario = {
"id": "scenario:ambiguous-provider",
"authority": "platform",
"requires": {"capabilities": ["s1.os-baseline"]},
"parameter_overrides": {},
}
issues, _ = validator.compose_scenario([second, first], scenario)
assert any("ambiguous providers" in msg for msg in error_messages(issues))
def test_demo_composition_refuses_unselected_override(tmp_path):
declaration = declaration_from(valid_declaration(), tmp_path)
scenario = {
"id": "scenario:unselected-override",
"authority": "platform",
"requires": {"capabilities": ["s1.os-baseline"]},
"parameter_overrides": {"owner.unselected": {"target_hosts": ["railiance01"]}},
}
issues, _ = validator.compose_scenario([declaration], scenario)
assert any("override targets an unselected declaration" in msg for msg in error_messages(issues))