From bb9b9b556c639f58a2e9f3b691bf11561fbb1834 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 11 Oct 2026 09:09:24 +0200 Subject: [PATCH] Select explicitly pinned security case type revisions without fallback Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade --- .../source-integration-validation.json | 43 +++++++ ...e12-source-acknowledgment-followthrough.md | 10 +- ...pe-version-selection-net-owner-review.json | 41 +++++++ ...on-selection-net-owner-review.sources.json | 96 +++++++++++++++ tools/security-case-types/README.md | 17 +++ .../security_case_validator.py | 77 ++++++++++-- .../tests/test_explicit_version_selection.py | 114 ++++++++++++++++++ workplans/ADHOC-2026-10-11.md | 14 +++ workplans/NK-WP-0048-security-case-types.md | 12 ++ 9 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json create mode 100644 docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.json create mode 100644 docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.sources.json create mode 100644 tools/security-case-types/tests/test_explicit_version_selection.py diff --git a/docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json b/docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json new file mode 100644 index 0000000..71d1483 --- /dev/null +++ b/docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json @@ -0,0 +1,43 @@ +{ + "owner_source_commit": "f6856cdb53c051e35c984bad927395b7e8a8e5e0", + "orientation": { + "package_version": "0.2.6", + "digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", + "start_source_status": "withheld; source changed", + "source_review": "AGENTS/INTENT/SECURITY-COMPANION and relevant actual validator/acknowledgment/workplan reviewed; no guidance or authority inferred" + }, + "tests_passed": 43, + "default11_type_status": "acknowledged", + "explicit12_type_status": "acknowledged", + "both_ordinary_admission_refused": true, + "actual_original_and_staged11_cli_render_bytes_identical": true, + "default11_render_digest": "sha256:e64b56e4590b57b858303fcda1a474693e3dd821bc83df26646fe78fc345498a", + "version12_preparation_render_digest": "sha256:c0afd5576cdae0cbc807eccb2b9fa87ac668f15e32ec7cf79d295bdb23c75b35", + "canonical11_files": { + "security-case-types/oidc-browser-relying-party/README.md": "c01ec1a3f058eab9e6c461e898166d496adb8542ea46f9e198ced4de83b997ef", + "security-case-types/oidc-browser-relying-party/decision/build_intake.py": "d214019a10801e14d7986474ff83370482116df6a0fdde6ecb255d75645b3b04", + "security-case-types/oidc-browser-relying-party/decision/intake.json": "a0ab9c57c71359b9ed31b9d6b04fd464db3e41bb6fe6f39bc6ac0b85556765e0", + "security-case-types/oidc-browser-relying-party/decision/packet.md": "452ea42168ddef64dc5498ba30ae63ff2ff47a3919f55e22f6379d668ea76c28", + "security-case-types/oidc-browser-relying-party/examples/example-public-portal.yaml": "41cc5bb4842209f5f86830d1e8b4b010c6ef06a050c4160faae06a1dfcd9b6e9", + "security-case-types/oidc-browser-relying-party/examples/forgejo.yaml": "96c8dcd4b4182893b3a005137b880c0a80d4aa07a2668b29964e1db33605e050", + "security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party.py": "7670ad0e415ca9941255bbc111b91fb5c9c3ae41851096d53d85fe7ba1d8660f", + "security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party_contract.py": "ed30cd00ae22c32fbe2184443a2a8a7775ea5c231d1dd8070f230d547051f4e0", + "security-case-types/oidc-browser-relying-party/type.yaml": "f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c" + }, + "historical_accepted11_case_sha256": "bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d", + "acknowledged12_files": { + "security-case-type-revisions/1.2/oidc-browser-relying-party/README.md": "4c82d9517d423ada75e680a05b0ecc6dfa0bf9f877a92b68f3ab93c1a86e405e", + "security-case-type-revisions/1.2/oidc-browser-relying-party/decision/acknowledgment.json": "2dcc220febf125a17b3769f7014f18b63979d7121e0f7056a2b0d8b1ae6031b0", + "security-case-type-revisions/1.2/oidc-browser-relying-party/examples/example-public-portal.yaml": "8d6457248415daa6fc509da4e838aae0b66d7808e9da8e00f4b85a1e7faed10d", + "security-case-type-revisions/1.2/oidc-browser-relying-party/examples/forgejo.yaml": "1c868c1da7d9077fae97bc6bc029706ec3dc418cfeaae30dc0f92b5b4ba6f2ab", + "security-case-type-revisions/1.2/oidc-browser-relying-party/tests/test_source_contract.py": "4a958eb2d0b90c1ffd08300d3299e4c31c827dcd7fb3cb4c22df91cc29986124", + "security-case-type-revisions/1.2/oidc-browser-relying-party/type.yaml": "9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578" + }, + "provider_declaration_inputs": { + "/home/worsch/net-kingdom/capabilities/playbooks/net-kingdom.keycape-client-registration.yaml": "cd81e02c283d514492b7a99062c9ede59a7ae124736de1270a121bf0a96a984e" + }, + "ordinary_admission_or_type_implemented_or_case_accepted_or_native_completed": false, + "native_or_credentials_accessed": false, + "formal_net_owner_review": false, + "NK_WP_0048_T06_remains_wait": true +} diff --git a/docs/forgejo-type12-source-acknowledgment-followthrough.md b/docs/forgejo-type12-source-acknowledgment-followthrough.md index 6a3e118..3f5ab53 100644 --- a/docs/forgejo-type12-source-acknowledgment-followthrough.md +++ b/docs/forgejo-type12-source-acknowledgment-followthrough.md @@ -11,7 +11,7 @@ immutable. Independent acknowledged-package validator and22 tests pass; closed schema/version load and identical c0afd preparation render are proved. Current canonical1.1 package and accepted1.1 case remain unchanged, without -inherited1.2 authority. Current load_type reads one type.yaml per id and requires +inherited1.2 authority. At source acknowledgment, load_type read one type.yaml per id and required exact version, so selecting1.2 as default would break that1.1 case. The explicit version root loads1.2 through existing --types/--types-root input, only for source validation/preparation. This record changes no resolver/default, native broker @@ -37,3 +37,11 @@ No new login/remint or speculative delete authority follows from this record. Existing native acceptance and human gates remain in their current owner/project records. This document adds no broad plan, promoted residual or implementation permission and marks no existing implementation task done. + +## Source-only explicit selection integration — 2026-10-11 + +A bounded optional version_roots input and CLI --version-root VERSION=PATH now prepare exact1.2 selection alongside the unchanged --types default. Matching exact version chooses one declared root; missing/mismatched/duplicate/symlinked source refuses without fallback. Source type ID/version must equal the case reference, including direct validation/render callers. Existing two-argument callers remain default-only; no automatic Mason/Warden/native adoption or highest/latest version resolver is introduced. See tools/security-case-types/README.md and docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json. + +Actual canonical1.1 is acknowledged (not implemented); its historical accepted case is retained exactly, including ordinary-admission refusal and the existing nonexecuting preparation behavior. Both1.1 and acknowledged1.2 remain refused in ordinary admission. The original and staged1.1 preparation CLI output bytes match exactly (digest e64b56e4...);1.2 mapped preparation equals its direct-root render (digest c0afd557...). No type status/canonical case/acknowledgment is rewritten. Forty-three validator tests cover exact selection, malformed/traversal/ambiguity/no-fallback, pin separation, deterministic plan-only rendering and unchanged ordinary gates. These are maker source checks, not formal NetKingdom owner agreement or native implementation proof. + +NK-WP-0048-T06 remains wait in its existing ORGREF cross-owner driver. Source selection alone does not provide first-reference owner receipts, implemented type, case acceptance/materialized/live status, owner adapters/global authenticated verifier, broker source reconciliation or attended/native proof. Those existing remaining gates and approvals are unchanged. diff --git a/docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.json b/docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.json new file mode 100644 index 0000000..235804c --- /dev/null +++ b/docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.json @@ -0,0 +1,41 @@ +{ + "reviewer": "net-kingdom-owner (agent net-kingdom, codex)", + "decision": "approved", + "work_item": "NK-WP-ADHOC-2026-10-11-T06", + "scope": "explicit-version-source-selection-review-only", + "reviewed_manifest_sha256": "88238f29ebeceee36f6f6e77bb19cb43837b5bc03bce21c16acc50399b302084", + "reviewed_validator_sha256": "5b9e46a5d0a8f387c6e0d87216c5695c03753c564378b1e43c07b4628cd07dc7", + "comment": "Established NetKingdom owner only, existing completedT06 anchor verified before this source followthrough review. INTENT Taxonomy, inspectable identity contracts and Meta-Orchestration support exact explicit source version selection, immutable predecessor history and owner-controlled later integration. This optional selector prepares source without discovering or promoting authority; canonicalcase/type acceptance and native owner gates remain distinct. Currentdefault1.1 is acknowledged/unimplemented, not implemented.", + "independent_validation": { + "all_six_staged_and_original_sha_guards_matched": true, + "isolated_validator_and_version_selection_tests_passed": 43, + "acknowledged12_package_compatibility_tests_passed": 22, + "temporary_snapshot_setup_initial_collection_failure": "Missing playbook-capability-contract dependency in my temporary copy; copied unchanged actual dependency and reran successfully without candidate source edits.", + "actual_canonical11_and_acknowledged12_file_pins_match_candidate_evidence": true, + "both_ordinary_admission_refused": true, + "default11_preparation_render_digest": "sha256:e64b56e4590b57b858303fcda1a474693e3dd821bc83df26646fe78fc345498a", + "explicit12_preparation_render_digest": "sha256:c0afd5576cdae0cbc807eccb2b9fa87ac668f15e32ec7cf79d295bdb23c75b35", + "execution_permitted": false + }, + "selection_contract": [ + "Existing two-argument callers retain their supplied default root; unmapped versions use that root.", + "A matching explicit version selects one supplied Path root only; missing/wrong source refuses without falling back, directory scanning or highest/latest selection.", + "Duplicate or invalid CLI mappings refuse even when identical; closed exact reference rejects traversal/unpinned selectors.", + "Selected root/type directory/typefile symlink and duplicate YAML source keys refuse; exact source id/version match required.", + "Direct case validation now refuses mixed case/type version inputs, including in source preparation mode.", + "Explicit roots are caller-supplied source inputs, not authenticated owner provenance or new acknowledgment; exact content hashes/receipts still bind acceptance." + ], + "retained_limits": { + "canonical11_package_or_case_changed": false, + "acknowledged12_package_or_receipt_changed": false, + "type_implemented": false, + "case_accepted_by_this_change": false, + "owner_adapter_installed": false, + "global_native_evidence_verifier_supplied": false, + "bound_claims_confirmed": false, + "native_action_or_credential_access": false, + "grant_or_first_case_complete": false, + "NK_WP_0048_T06_status": "wait" + }, + "actual_completed_workrecord_sha256": "a09f12d10bbb6fe89daca687b619f79583836afa9b68664ef1442787975d9065" +} diff --git a/docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.sources.json b/docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.sources.json new file mode 100644 index 0000000..b66bc26 --- /dev/null +++ b/docs/reviews/2026-10-11-explicit-type-version-selection-net-owner-review.sources.json @@ -0,0 +1,96 @@ +{ + "candidate_files": [ + { + "repo": "net-kingdom", + "path": "docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json", + "staged_path": "/tmp/nk-explicit-version-selection-stage/net-kingdom/docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json", + "original_sha256": null, + "staged_sha256": "de288546b8d1c797fc4cc63978d586a6f61d5cc4782ded67dafc907dd09723fa" + }, + { + "repo": "net-kingdom", + "path": "docs/forgejo-type12-source-acknowledgment-followthrough.md", + "staged_path": "/tmp/nk-explicit-version-selection-stage/net-kingdom/docs/forgejo-type12-source-acknowledgment-followthrough.md", + "original_sha256": "1755b5c45f30f622ba7bbe67f64bf310de61ea3a2cd192472dc2c0bcedc7562a", + "staged_sha256": "fda468e5cbe346217bc1bb481e89563fd76778ad67f64a41e8385057f528c221" + }, + { + "repo": "net-kingdom", + "path": "tools/security-case-types/README.md", + "staged_path": "/tmp/nk-explicit-version-selection-stage/net-kingdom/tools/security-case-types/README.md", + "original_sha256": "c000d0b6ca18ce15b777372ca00b813d15700c652ac1040d1ee883712aeee4ac", + "staged_sha256": "03e1c503bc60990214d5b1a7cec8878cf47fc892d82c82d682f3490e33dc71f5" + }, + { + "repo": "net-kingdom", + "path": "tools/security-case-types/security_case_validator.py", + "staged_path": "/tmp/nk-explicit-version-selection-stage/net-kingdom/tools/security-case-types/security_case_validator.py", + "original_sha256": "ef380a85ca9949e0ebc59d1cdce422bd45e362eb976330b2b72b8fd73784ae0a", + "staged_sha256": "5b9e46a5d0a8f387c6e0d87216c5695c03753c564378b1e43c07b4628cd07dc7" + }, + { + "repo": "net-kingdom", + "path": "tools/security-case-types/tests/test_explicit_version_selection.py", + "staged_path": "/tmp/nk-explicit-version-selection-stage/net-kingdom/tools/security-case-types/tests/test_explicit_version_selection.py", + "original_sha256": null, + "staged_sha256": "0592c751ed75f27231d3ca2a2eb97af7e2f45b79b0572fbdf8cadbe3738aa76d" + }, + { + "repo": "net-kingdom", + "path": "workplans/NK-WP-0048-security-case-types.md", + "staged_path": "/tmp/nk-explicit-version-selection-stage/net-kingdom/workplans/NK-WP-0048-security-case-types.md", + "original_sha256": "a7f8e24b4da32a421c4877fbfa9e0914233b7d6f8ed7e71165998ce3cc501ff0", + "staged_sha256": "c5f0e3bce81ae662d04158689ee250a54af36e17c5bac32858ba889795597001" + } + ], + "source_evidence": { + "owner_source_commit": "f6856cdb53c051e35c984bad927395b7e8a8e5e0", + "orientation": { + "package_version": "0.2.6", + "digest": "8d1d6a9d4d07fec7b619618de7dfe2be2254c1790f85f4198c7fe19db9751c02", + "start_source_status": "withheld; source changed", + "source_review": "AGENTS/INTENT/SECURITY-COMPANION and relevant actual validator/acknowledgment/workplan reviewed; no guidance or authority inferred" + }, + "tests_passed": 43, + "default11_type_status": "acknowledged", + "explicit12_type_status": "acknowledged", + "both_ordinary_admission_refused": true, + "actual_original_and_staged11_cli_render_bytes_identical": true, + "default11_render_digest": "sha256:e64b56e4590b57b858303fcda1a474693e3dd821bc83df26646fe78fc345498a", + "version12_preparation_render_digest": "sha256:c0afd5576cdae0cbc807eccb2b9fa87ac668f15e32ec7cf79d295bdb23c75b35", + "canonical11_files": { + "security-case-types/oidc-browser-relying-party/README.md": "c01ec1a3f058eab9e6c461e898166d496adb8542ea46f9e198ced4de83b997ef", + "security-case-types/oidc-browser-relying-party/decision/build_intake.py": "d214019a10801e14d7986474ff83370482116df6a0fdde6ecb255d75645b3b04", + "security-case-types/oidc-browser-relying-party/decision/intake.json": "a0ab9c57c71359b9ed31b9d6b04fd464db3e41bb6fe6f39bc6ac0b85556765e0", + "security-case-types/oidc-browser-relying-party/decision/packet.md": "452ea42168ddef64dc5498ba30ae63ff2ff47a3919f55e22f6379d668ea76c28", + "security-case-types/oidc-browser-relying-party/examples/example-public-portal.yaml": "41cc5bb4842209f5f86830d1e8b4b010c6ef06a050c4160faae06a1dfcd9b6e9", + "security-case-types/oidc-browser-relying-party/examples/forgejo.yaml": "96c8dcd4b4182893b3a005137b880c0a80d4aa07a2668b29964e1db33605e050", + "security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party.py": "7670ad0e415ca9941255bbc111b91fb5c9c3ae41851096d53d85fe7ba1d8660f", + "security-case-types/oidc-browser-relying-party/tests/test_oidc_browser_relying_party_contract.py": "ed30cd00ae22c32fbe2184443a2a8a7775ea5c231d1dd8070f230d547051f4e0", + "security-case-types/oidc-browser-relying-party/type.yaml": "f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c" + }, + "historical_accepted11_case_sha256": "bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d", + "acknowledged12_files": { + "security-case-type-revisions/1.2/oidc-browser-relying-party/README.md": "4c82d9517d423ada75e680a05b0ecc6dfa0bf9f877a92b68f3ab93c1a86e405e", + "security-case-type-revisions/1.2/oidc-browser-relying-party/decision/acknowledgment.json": "2dcc220febf125a17b3769f7014f18b63979d7121e0f7056a2b0d8b1ae6031b0", + "security-case-type-revisions/1.2/oidc-browser-relying-party/examples/example-public-portal.yaml": "8d6457248415daa6fc509da4e838aae0b66d7808e9da8e00f4b85a1e7faed10d", + "security-case-type-revisions/1.2/oidc-browser-relying-party/examples/forgejo.yaml": "1c868c1da7d9077fae97bc6bc029706ec3dc418cfeaae30dc0f92b5b4ba6f2ab", + "security-case-type-revisions/1.2/oidc-browser-relying-party/tests/test_source_contract.py": "4a958eb2d0b90c1ffd08300d3299e4c31c827dcd7fb3cb4c22df91cc29986124", + "security-case-type-revisions/1.2/oidc-browser-relying-party/type.yaml": "9e60ff569fb0d860a2bbde7bbb0991a54d4b0688f62b56df0fa5d161fd624578" + }, + "provider_declaration_inputs": { + "/home/worsch/net-kingdom/capabilities/playbooks/net-kingdom.keycape-client-registration.yaml": "cd81e02c283d514492b7a99062c9ede59a7ae124736de1270a121bf0a96a984e" + }, + "ordinary_admission_or_type_implemented_or_case_accepted_or_native_completed": false, + "native_or_credentials_accessed": false, + "formal_net_owner_review": false, + "NK_WP_0048_T06_remains_wait": true + }, + "commands": [ + "PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -p no:cacheprovider /tmp/nk-explicit-version-owner-checkout/net-kingdom/tools/security-case-types/tests -q", + "PYTHONDONTWRITEBYTECODE=1 NET_KINGDOM_ROOT=/tmp/nk-explicit-version-owner-checkout/net-kingdom python3 -m pytest -p no:cacheprovider /tmp/nk-explicit-version-owner-checkout/net-kingdom/security-case-type-revisions/1.2/oidc-browser-relying-party/tests -q" + ], + "owner_intent_sha256": "fd2ba8dff9dfd5583ec6d8b0348f273bfe00c59923b5d5d47cd22b1fb4658c54", + "owner_instructions_sha256": "065d47b7bdbfe829672a2a6161a7f8cf243237a1210fb61d82f153fa37f22131", + "existing_owner_workrecord_original_sha256": "a09f12d10bbb6fe89daca687b619f79583836afa9b68664ef1442787975d9065" +} diff --git a/tools/security-case-types/README.md b/tools/security-case-types/README.md index 7259a11..45e1106 100644 --- a/tools/security-case-types/README.md +++ b/tools/security-case-types/README.md @@ -33,3 +33,20 @@ the type id, and rewrite every section. The template's tests travel with it: rename `tests/test_type.py` to `tests/test__contract.py` so module names stay unique. Tests: `python3 -m pytest tools/security-case-types/tests security-case-types/*/tests`. + +## Explicit source versions + +The default --types root is unchanged. Select a separately acknowledged source version explicitly, without moving its package into the default root: + +```bash +python3 tools/security-case-types/security_case_validator.py validate-case \ + security-case-type-revisions/1.2/oidc-browser-relying-party/examples/forgejo.yaml \ + --types security-case-types \ + --version-root 1.2=security-case-type-revisions/1.2 +``` + +This ordinary command refuses:1.2 is acknowledged, not implemented. For source-only preparation, the existing --allow-unimplemented option may be added; its render is deterministic and execution.permitted remains false. Current canonical1.1 is also acknowledged and its historical accepted case/normal refusal/preparation render remain exactly retained. No source acknowledgment or historical case acceptance is an implementation/native proof waiver. + +The Python API is load_type(default_root, exact_reference, version_roots={"1.2": explicit_root}). Each key must be an exact major.minor and each root a Path. CLI --version-root VERSION=PATH may repeat for DIFFERENT versions. A matching explicit version selects exactly one root; a missing/wrong/ambiguous source refuses without trying the default or another revision. Versions without a mapping continue using --types. No directory discovery, highest/latest selection, status promotion or automatic owner-adapter adoption occurs. Duplicate mappings (even identical), duplicate YAML fields, mismatched source ID/version, malformed/traversal references and selected symlinks refuse. Direct case validation also requires the case reference to match the supplied type ID/version. + +An explicit root is source input, not an owner acknowledgment or grant. Callers retain exact source hashes/type/case/render in their own reviewed receipts. Existing two-argument load_type callers, including ops-mason, retain their default behavior until their owner deliberately adopts the optional input. Adapter/global provenance/native first-reference gates remain separate in NK-WP-0048-T06 and the existing ORGREF driver. diff --git a/tools/security-case-types/security_case_validator.py b/tools/security-case-types/security_case_validator.py index bd95f3d..03e00d3 100644 --- a/tools/security-case-types/security_case_validator.py +++ b/tools/security-case-types/security_case_validator.py @@ -361,18 +361,69 @@ def _validate_acceptance(acc: Any, issues: list[Issue]) -> None: # ── Cases ──────────────────────────────────────────────────────────────────── -def load_type(types_root: Path, ref: str) -> tuple[list[Issue], dict[str, Any] | None, Path | None]: - m = TYPE_REF.fullmatch(ref or "") +def parse_version_roots(entries: list[str]) -> tuple[list[Issue], dict[str, Path]]: + """Closed explicit selectors; duplicate versions are ambiguous, even same path.""" + roots: dict[str, Path] = {} + for entry in entries: + if not isinstance(entry, str): + return [err("types.version_roots", "must be VERSION=PATH entries")], {} + version, separator, path = entry.partition("=") + if not separator or not VERSION.fullmatch(version) or not path or version in roots: + return [err("types.version_roots", "needs unique .= entries")], {} + roots[version] = Path(path) + return [], roots + + +class _UniqueTypeLoader(yaml.SafeLoader): + """A duplicate source field must not choose an implicit winning revision.""" + + +def _unique_type_mapping(loader, node, deep=False): + result = {} + for key_node, value_node in node.value: + key = loader.construct_object(key_node, deep=deep) + if key in result: + raise ValueError("duplicate type source key") + result[key] = loader.construct_object(value_node, deep=deep) + return result + + +_UniqueTypeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _unique_type_mapping) + + +def load_type(types_root: Path, ref: str, *, version_roots: dict[str, Path] | None = None + ) -> tuple[list[Issue], dict[str, Any] | None, Path | None]: + """Load exactly the named version from one selected root; never search/fallback. + + Existing two-argument callers retain their explicit default root. New callers + may name per-version roots. A selection is source input, not implementation, + case acceptance, source acknowledgment or native authority. + """ + m = TYPE_REF.fullmatch(ref if isinstance(ref, str) else "") if not m: return [err("case.spec.type", "must be @.")], None, None - type_dir = types_root / m.group(1) + if version_roots is not None and (not isinstance(version_roots, dict) or + any(not isinstance(version, str) or not VERSION.fullmatch(version) or + not isinstance(root, Path) for version, root in version_roots.items())): + return [err("types.version_roots", "must map exact versions to Path roots")], None, None + selected = (version_roots or {}).get(m.group(2), Path(types_root)) + type_dir = selected / m.group(1) type_file = type_dir / "type.yaml" + if selected.is_symlink() or type_dir.is_symlink() or type_file.is_symlink(): + return [err("case.spec.type", "symlinked selected type source refused")], None, None if not type_file.is_file(): - return [err("case.spec.type", f"unknown type {m.group(1)!r}")], None, None - data = yaml.safe_load(type_file.read_text()) or {} - if str(data.get("spec", {}).get("version")) != m.group(2): + return [err("case.spec.type", f"unknown type {m.group(1)!r} in selected root")], None, None + try: + data = yaml.load(type_file.read_text(), Loader=_UniqueTypeLoader) + except (OSError, ValueError, TypeError, yaml.YAMLError): + return [err("case.spec.type", "invalid or ambiguous type source")], None, None + if not isinstance(data, dict) or not isinstance(data.get("spec"), dict): + return [err("case.spec.type", "type source needs a spec mapping")], None, None + if data["spec"].get("id") != m.group(1): + return [err("case.spec.type", "selected type source id does not match reference")], None, None + if str(data["spec"].get("version")) != m.group(2): return [err("case.spec.type", f"type {m.group(1)!r} is at version " - f"{data.get('spec', {}).get('version')}, not {m.group(2)}")], None, None + f"{data['spec'].get('version')}, not {m.group(2)}")], None, None return [], data, type_dir @@ -391,6 +442,8 @@ def validate_case(case: dict[str, Any], type_data: dict[str, Any], *, require_im if not owner.get("repo") or not owner.get("person"): issues.append(err("case.spec.owner", "needs repo and person")) tspec = type_data.get("spec", {}) + if spec.get("type") != f"{tspec.get('id')}@{tspec.get('version')}": + issues.append(err("case.spec.type", "case reference does not match selected type id/version")) if require_implemented and tspec.get("status") != "implemented": issues.append(err("case.spec.type", f"type {tspec.get('id')!r} is {tspec.get('status')!r}; " "only implemented types accept cases")) @@ -564,6 +617,8 @@ def main(argv: list[str] | None = None) -> int: vc = sub.add_parser("validate-case") vc.add_argument("case") vc.add_argument("--types", required=True) + vc.add_argument("--version-root", action="append", default=[], metavar="VERSION=PATH", + help="select an exact version root; no search or fallback") vc.add_argument("--allow-unimplemented", action="store_true", help="validate an example case against a type that is not implemented yet") rd = sub.add_parser("render") @@ -572,6 +627,8 @@ def main(argv: list[str] | None = None) -> int: rd.add_argument("--repos-root", default=str(TOOL_DIR.parents[2]), help="directory holding the owning repositories (default: the parent of this checkout)") rd.add_argument("--types", required=True) + rd.add_argument("--version-root", action="append", default=[], metavar="VERSION=PATH", + help="select an exact version root; no search or fallback") rd.add_argument("--allow-unimplemented", action="store_true") args = ap.parse_args(argv) @@ -584,7 +641,11 @@ def main(argv: list[str] | None = None) -> int: return code case = yaml.safe_load(Path(args.case).read_text()) or {} - issues, type_data, _ = load_type(Path(args.types), (case.get("spec") or {}).get("type", "")) + selection_issues, version_roots = parse_version_roots(args.version_root) + if has_errors(selection_issues): + return _report(selection_issues) + issues, type_data, _ = load_type(Path(args.types), (case.get("spec") or {}).get("type", ""), + version_roots=version_roots) if type_data is None: return _report(issues) if args.cmd == "validate-case": diff --git a/tools/security-case-types/tests/test_explicit_version_selection.py b/tools/security-case-types/tests/test_explicit_version_selection.py new file mode 100644 index 0000000..6194aac --- /dev/null +++ b/tools/security-case-types/tests/test_explicit_version_selection.py @@ -0,0 +1,114 @@ +"""Exact source selection is not type implementation or native admission.""" +import copy +import importlib.util +import json +from pathlib import Path +import subprocess +import sys + +import pytest +import yaml + +TOOL=Path(__file__).resolve().parents[1] +REPO=TOOL.parents[1] +spec=importlib.util.spec_from_file_location('version_selection_validator',TOOL/'security_case_validator.py') +v=importlib.util.module_from_spec(spec);spec.loader.exec_module(v) +DEFAULT=REPO/'security-case-types' +REVISION=REPO/'security-case-type-revisions/1.2' +REF='oidc-browser-relying-party' +CASE11=REPO/'security-cases/forgejo-netkingdom-oidc.yaml' +CASE12=REVISION/REF/'examples/forgejo.yaml' + + +def load(ref,roots=None):return v.load_type(DEFAULT,ref,version_roots=roots) + + +def test_version12_selected_exactly_default11_unchanged(): + before,data11,path11=load(REF+'@1.1') + after,again,againpath=load(REF+'@1.1',{'1.2':REVISION}) + assert not before and not after and data11==again and path11==againpath==DEFAULT/REF + issues,data12,path12=load(REF+'@1.2',{'1.2':REVISION}) + assert not issues and data12['spec']['status']=='acknowledged' + assert path12==REVISION/REF and data11['spec']['status']=='acknowledged' + + +@pytest.mark.parametrize('root',[Path('/nonexistent-type-revision'),DEFAULT]) +def test_missing_or_wrong_explicit_version_never_falls_back(root): + issues,data,_=load(REF+'@1.2',{'1.2':root}) + assert v.has_errors(issues) and data is None + + +def test_no_mapping_does_not_discover_acknowledged_package(): + issues,data,_=load(REF+'@1.2') + assert data is None and v.has_errors(issues) + + +@pytest.mark.parametrize('ref',['../oidc-browser-relying-party@1.2',REF+'@../1.2',REF+'@latest',REF+'@1.2/other',None]) +def test_traversal_malformed_unpinned_reference_refused(ref): + issues,data,_=load(ref,{'1.2':REVISION}) + assert data is None and v.has_errors(issues) + + +@pytest.mark.parametrize('entries',[['1.2=x','1.2=y'],['1.2=x','1.2=x'],['../1.2=x'],['1.2='],['latest=x'],['1.2']]) +def test_ambiguous_or_invalid_root_mapping_refused(entries): + issues,roots=v.parse_version_roots(entries) + assert v.has_errors(issues) and roots=={} + + +def test_selected_source_id_or_duplicate_revision_fields_refused(tmp_path): + folder=tmp_path/REF;folder.mkdir();source=yaml.safe_load((REVISION/REF/'type.yaml').read_text()) + source['spec']['id']='different-id';(folder/'type.yaml').write_text(yaml.safe_dump(source)) + issues,data,_=load(REF+'@1.2',{'1.2':tmp_path}) + assert data is None and v.has_errors(issues) + (folder/'type.yaml').write_text('spec:\n id: oidc-browser-relying-party\n version: "1.1"\n version: "1.2"\n') + issues,data,_=load(REF+'@1.2',{'1.2':tmp_path}) + assert data is None and v.has_errors(issues) + + +def test_selected_symlink_cannot_escape_root(tmp_path): + (tmp_path/REF).symlink_to(REVISION/REF,target_is_directory=True) + issues,data,_=load(REF+'@1.2',{'1.2':tmp_path}) + assert data is None and v.has_errors(issues) + + +def test_direct_case_validation_cannot_mix_source_pins(): + _,type12,_=load(REF+'@1.2',{'1.2':REVISION}) + case11=yaml.safe_load(CASE11.read_text()) + assert any('does not match selected type' in x.message for x in v.validate_case(case11,type12,require_implemented=False)) + _,type11,_=load(REF+'@1.1') + case12=yaml.safe_load(CASE12.read_text()) + assert any('does not match selected type' in x.message for x in v.validate_case(case12,type11,require_implemented=False)) + + +def test_acknowledged12_load_does_not_admit_or_render_ordinary_case(): + _,type12,_=load(REF+'@1.2',{'1.2':REVISION});case=yaml.safe_load(CASE12.read_text()) + assert any('only implemented types accept cases' in x.message for x in v.validate_case(case,type12)) + issues,rendered=v.render_case(case,type12,[]) + assert rendered is None and v.has_errors(issues) + + +def cli(*args):return subprocess.run([sys.executable,str(TOOL/'security_case_validator.py'),*map(str,args)],capture_output=True,text=True) + + +def test_cli_default11_and_mapped12_admission_boundaries(): + original=cli('validate-case',CASE11,'--types',DEFAULT) + mapped=cli('validate-case',CASE11,'--types',DEFAULT,'--version-root','1.2='+str(REVISION)) + assert original.returncode==mapped.returncode==1 and original.stderr==mapped.stderr and 'only implemented types' in original.stderr + refused=cli('validate-case',CASE12,'--types',DEFAULT,'--version-root','1.2='+str(REVISION)) + assert refused.returncode==1 and 'only implemented types' in refused.stderr + preparation=cli('validate-case',CASE12,'--types',DEFAULT,'--version-root','1.2='+str(REVISION),'--allow-unimplemented') + assert preparation.returncode==0 + + +def test_cli_render_source_selection_isolated_deterministic_plan_only(): + owners=Path('/home/worsch') + before=cli('render',CASE11,'--types',DEFAULT,'--repos-root',owners,'--allow-unimplemented') + selected11=cli('render',CASE11,'--types',DEFAULT,'--repos-root',owners,'--version-root','1.2='+str(REVISION),'--allow-unimplemented') + assert before.returncode==selected11.returncode==0 and before.stdout==selected11.stdout + after=cli('render',CASE12,'--types',DEFAULT,'--repos-root',owners,'--version-root','1.2='+str(REVISION),'--allow-unimplemented') + direct=cli('render',CASE12,'--types',REVISION,'--repos-root',owners,'--allow-unimplemented') + assert after.returncode==direct.returncode==0 and after.stdout==direct.stdout + a,b=json.loads(before.stdout),json.loads(after.stdout) + assert a['type'].endswith('@1.1') and b['type'].endswith('@1.2') and a['digest']!=b['digest'] + assert a['execution']['permitted'] is b['execution']['permitted'] is False + assert b['composition']['execution']['permitted'] is False diff --git a/workplans/ADHOC-2026-10-11.md b/workplans/ADHOC-2026-10-11.md index 6ed614c..19f1085 100644 --- a/workplans/ADHOC-2026-10-11.md +++ b/workplans/ADHOC-2026-10-11.md @@ -162,3 +162,17 @@ canonicalcase acceptance. Requesting coordinator records actual foundergo; no worker native/credential/grant action. Canonical1.2case remains proposed without acceptance; implementation followthrough remains existing NK0048-T06/project owner tasks. No new trial, taskT07, broad plan or whole-workplan finish. + +Explicit version-selection source followthrough review, same completed owner T06 +anchor: reviewed exact six-file source manifest +88238f29ebeceee36f6f6e77bb19cb43837b5bc03bce21c16acc50399b302084. +Independent43 validator/selection tests and22 acknowledged1.2 package compatibility +tests pass in isolated source snapshot. No mappings preserve default1.1; explicit +1.2 loads only its selected exact root with no fallback/discovery, and mismatched +case/type references, duplicate mappings/YAML keys and selected symlinks refuse. +Both types remain acknowledged/unimplemented; historical accepted1.1 case and +its normal refusal remain unchanged. Preparation digests stay e64b56e4.../c0afd557... +and executionfalse. Canonical/acknowledgment hashes verified unchanged; source +loader selection confers no case admission, native adapter/global verifier, +materialization, grant or implemented status. Existing NK0048-T06 remains wait. +No new source-ack trial or broad implementation plan is created. diff --git a/workplans/NK-WP-0048-security-case-types.md b/workplans/NK-WP-0048-security-case-types.md index 49a81fc..d50a1cb 100644 --- a/workplans/NK-WP-0048-security-case-types.md +++ b/workplans/NK-WP-0048-security-case-types.md @@ -547,3 +547,15 @@ receipt; no1.1 approval is inherited. Source-recording review/test proof and exa next gates are in docs/forgejo-type12-source-acknowledgment-followthrough.md. T06 remains wait and T07 remains its existing dependent task; type is NOT implemented and no first-case/native grant or broader workplan finish is asserted. + +2026-10-11 — bounded explicit version selection source preparation for T06: +The validator now offers optional exact version_roots/--version-root VERSION=PATH +selection with duplicate/mismatch/traversal/symlink refusal and no fallback. The +default1.1 source and historicalacceptedcase hashes/ordinaryrefusal/preparation +render stayunchanged; actual1.1 is acknowledged, not implemented. Acknowledged1.2 +ordinaryadmission also refuses; optionalpreparation remains nonexecuting.43local +validatorchecks pass and original1.1 CLIrenderbytes match exactly. Makerproof: +docs/evidence/2026-10-11-explicit-type-version-selection/source-integration-validation.json. +FormalNetownerreview/reference-source integration and all existing native adapter, +provenance/broker/materialization/attendedproof gates remain; T06 stayswait, no +new residual or firstcase/typeimplementation/native grant is inferred.