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
|
||||
|
||||
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
|
||||
declared runtime readiness:
|
||||
|
||||
|
|
|
|||
|
|
@ -62,9 +62,28 @@ def _default_data_dir(kind: str) -> Path:
|
|||
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]:
|
||||
try:
|
||||
data = yaml.safe_load(path.read_text())
|
||||
data = yaml.load(path.read_text(), Loader=_UniqueKeyLoader)
|
||||
except (OSError, yaml.YAMLError) as exc:
|
||||
raise ProfileError(f"cannot read {path}: {exc}") from exc
|
||||
if not isinstance(data, dict):
|
||||
|
|
|
|||
|
|
@ -226,3 +226,45 @@ def test_incompatible_profile_or_rein_is_rejected(
|
|||
|
||||
with pytest.raises(IncompatibleProfileError, match=match):
|
||||
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
|
||||
existing readiness unchanged. Reviewed credential owner update and refreshed
|
||||
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.
|
||||
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.
|
||||
|
||||
## 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