fix: reject duplicate keys in execution catalogs
All checks were successful
ci / validate (push) Successful in 2m53s
All checks were successful
ci / validate (push) Successful in 2m53s
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
parent
f5eec5b23f
commit
863fab7a3b
5 changed files with 97 additions and 1 deletions
|
|
@ -17,6 +17,11 @@ assignment / channel / activity
|
||||||
|
|
||||||
## Profiles
|
## Profiles
|
||||||
|
|
||||||
|
Catalog YAML rejects duplicate mapping keys, including collisions introduced by
|
||||||
|
merge keys. Use one explicit value per key; later entries cannot override earlier
|
||||||
|
credential routes, limits, or rein capabilities. Errors identify the file and
|
||||||
|
line without printing the duplicate key or value.
|
||||||
|
|
||||||
List and validate every committed catalog profile, including its independently
|
List and validate every committed catalog profile, including its independently
|
||||||
declared runtime readiness:
|
declared runtime readiness:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,28 @@ def _default_data_dir(kind: str) -> Path:
|
||||||
return _source_root() / ("profiles" if kind == "profiles" else "registry/reins")
|
return _source_root() / ("profiles" if kind == "profiles" else "registry/reins")
|
||||||
|
|
||||||
|
|
||||||
|
class _UniqueKeyLoader(yaml.SafeLoader):
|
||||||
|
"""Reject overwrites, including collisions introduced by YAML merges."""
|
||||||
|
|
||||||
|
def construct_mapping(self, node, deep=False):
|
||||||
|
mapping = super().construct_mapping(node, deep=deep)
|
||||||
|
seen = set()
|
||||||
|
for key_node, _ in node.value:
|
||||||
|
key = self.construct_object(key_node, deep=deep)
|
||||||
|
if key in seen:
|
||||||
|
mark = key_node.start_mark
|
||||||
|
# Key names and source snippets can themselves contain secrets.
|
||||||
|
raise yaml.YAMLError(
|
||||||
|
f"duplicate YAML key at line {mark.line + 1}, "
|
||||||
|
f"column {mark.column + 1}"
|
||||||
|
)
|
||||||
|
seen.add(key)
|
||||||
|
return mapping
|
||||||
|
|
||||||
|
|
||||||
def _read_yaml(path: Path) -> dict[str, Any]:
|
def _read_yaml(path: Path) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(path.read_text())
|
data = yaml.load(path.read_text(), Loader=_UniqueKeyLoader)
|
||||||
except (OSError, yaml.YAMLError) as exc:
|
except (OSError, yaml.YAMLError) as exc:
|
||||||
raise ProfileError(f"cannot read {path}: {exc}") from exc
|
raise ProfileError(f"cannot read {path}: {exc}") from exc
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
|
|
|
||||||
|
|
@ -226,3 +226,45 @@ def test_incompatible_profile_or_rein_is_rejected(
|
||||||
|
|
||||||
with pytest.raises(IncompatibleProfileError, match=match):
|
with pytest.raises(IncompatibleProfileError, match=match):
|
||||||
ProfileCatalog(profiles, reins).resolve("harness.test@1.0.0")
|
ProfileCatalog(profiles, reins).resolve("harness.test@1.0.0")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("kind", ["profile", "rein"])
|
||||||
|
@pytest.mark.parametrize("nested", [False, True])
|
||||||
|
def test_duplicate_catalog_keys_are_rejected(tmp_path, kind, nested):
|
||||||
|
if kind == "profile":
|
||||||
|
content = _profile()
|
||||||
|
content += ("metadata:\n route: first\n route: second\n" if nested
|
||||||
|
else "credential_route_refs: [first]\ncredential_route_refs: [second]\n")
|
||||||
|
else:
|
||||||
|
content = _rein()
|
||||||
|
content = (content.replace("session_style: unattended",
|
||||||
|
"session_style: interactive\n session_style: unattended")
|
||||||
|
if nested else content + "status: disabled\n")
|
||||||
|
_write(tmp_path, "duplicate.yaml", content)
|
||||||
|
catalog = ProfileCatalog(profile_dir=tmp_path, rein_dir=tmp_path)
|
||||||
|
with pytest.raises(ProfileError, match="duplicate YAML key"):
|
||||||
|
catalog.profiles() if kind == "profile" else catalog.reins()
|
||||||
|
|
||||||
|
|
||||||
|
def test_yaml_merge_cannot_override_catalog_values(tmp_path):
|
||||||
|
_write(tmp_path, "profile.yaml", _profile(extra='''metadata:
|
||||||
|
defaults: &defaults
|
||||||
|
runtime: pinned
|
||||||
|
selected:
|
||||||
|
<<: *defaults
|
||||||
|
runtime: different
|
||||||
|
'''))
|
||||||
|
with pytest.raises(ProfileError, match="duplicate YAML key"):
|
||||||
|
ProfileCatalog(profile_dir=tmp_path).profiles()
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonoverlapping_yaml_merge_remains_supported(tmp_path):
|
||||||
|
_write(tmp_path, "profile.yaml", _profile(extra='''metadata:
|
||||||
|
defaults: &defaults
|
||||||
|
runtime: pinned
|
||||||
|
selected:
|
||||||
|
<<: *defaults
|
||||||
|
host: local
|
||||||
|
'''))
|
||||||
|
profile = ProfileCatalog(profile_dir=tmp_path).profiles()[("harness.test", "1.0.0")]
|
||||||
|
assert profile.metadata["selected"] == {"runtime": "pinned", "host": "local"}
|
||||||
|
|
|
||||||
|
|
@ -52,3 +52,18 @@ the fix and passes afterwards.
|
||||||
Validation: full suite 104 passed; profile catalog validation passed with
|
Validation: full suite 104 passed; profile catalog validation passed with
|
||||||
existing readiness unchanged. Reviewed credential owner update and refreshed
|
existing readiness unchanged. Reviewed credential owner update and refreshed
|
||||||
the proposed policy reference in docs/anthropic-workload-key.md.
|
the proposed policy reference in docs/anthropic-workload-key.md.
|
||||||
|
|
||||||
|
## Reject ambiguous catalog YAML
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: ADHOC-2026-09-06-T03
|
||||||
|
status: done
|
||||||
|
priority: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow-up repository review found that YAML silently overwrote duplicate keys
|
||||||
|
before profile validation. Both profile and rein descriptor loading now reject
|
||||||
|
duplicate keys at every mapping depth, including merge collisions. Disjoint
|
||||||
|
merges remain supported. Four regression cases reproduced silent acceptance
|
||||||
|
before the change. Full suite: 110 passed; committed catalog validation passes.
|
||||||
|
No residuals from this bounded fix; GLAS-WP-0012 retains the live-proof work.
|
||||||
|
|
|
||||||
|
|
@ -234,3 +234,18 @@ by configuration, per-lane approval and positive/negative verification.
|
||||||
State Hub decisions are not a substitute for the durable authorization object.
|
State Hub decisions are not a substitute for the durable authorization object.
|
||||||
SECRETS-WP-0009-T03 and this plan's T02 remain waiting; runtime pinning and
|
SECRETS-WP-0009-T03 and this plan's T02 remain waiting; runtime pinning and
|
||||||
combined real proof remain required. No readiness change or real-key read.
|
combined real proof remain required. No readiness change or real-key read.
|
||||||
|
|
||||||
|
## 2026-09-06 authorization contract and policy publication
|
||||||
|
|
||||||
|
Reviewed owner commits `7b4b9e3` and `083bee7` and FLEX-WP-0021. Gate-house
|
||||||
|
GH-DEC-2026-005 resolved the contract: secrets-engine now validates the
|
||||||
|
approval-claim and decision envelope separately and removed the incorrect
|
||||||
|
State Hub authority requirement. Flex-auth published
|
||||||
|
`secrets-engine.catalog-lane.lifecycle` v1; T01/T02 are done.
|
||||||
|
|
||||||
|
Remaining owner gates are real decision/digest verification (FLEX-WP-0021-T03),
|
||||||
|
consumer service deployment and handoff (T04/T05), approval-engine deployment,
|
||||||
|
production service identity and verified lane activation. Publication is not
|
||||||
|
deployment. T02 stays waiting, and runtime pinning and combined acceptance
|
||||||
|
remain required. No new owner inbox message or activation evidence was found
|
||||||
|
in this review.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue