From 113cfef15f3ab7941f0d0c4eacbd788a3c2b29d9 Mon Sep 17 00:00:00 2001 From: Ivan Oparin Date: Thu, 27 Aug 2026 03:32:09 +0400 Subject: [PATCH] feat: accept Curator Protocol v1.0.0-rc.10 Advance the accepted protocol revision from 1.0.0-rc.5 to 1.0.0-rc.10 (curator-spec b8b03d5, the merge of the pinned-agent authentication tail the manager already emits). Conformance assertions now match the rc.10 corpus; the manifest digest moves to 803918bf. The audited-hash ledger records the test change. The external-build-repositories header now states the rc.10 schema-8 boundary with the new revision and digest, the body pairs schema-7 with marker v3 and schema-8 with marker v4, and the pinned-agent form cites the spec as its source: the third canonical authentication tail, RECOMMENDED. The skill-authoring link follows the accepted core to v1.0.0-rc.10. --- ...7ok_protocol-isolation-classification.json | 11 +- LOGBOOK.md | 106 ++++++++++++++++++ docs/external-build-repositories.md | 29 ++--- docs/skill-authoring.md | 2 +- src/csk/build_repository.py | 23 +++- tests/test_build_metadata.py | 12 +- tests/test_protocol_conformance.py | 35 +++--- ...est_rc5_external_repository_conformance.py | 5 + tests/test_schema_v7_repository.py | 59 ++++++++-- 9 files changed, 231 insertions(+), 51 deletions(-) diff --git a/.research/TASK-260803-2ol7ok_protocol-isolation-classification.json b/.research/TASK-260803-2ol7ok_protocol-isolation-classification.json index 7284893..0670e42 100644 --- a/.research/TASK-260803-2ol7ok_protocol-isolation-classification.json +++ b/.research/TASK-260803-2ol7ok_protocol-isolation-classification.json @@ -4,7 +4,7 @@ "source_commit": "2bfe3d64e9142d62e8ea3f92558eeee331f4578a", "protocol_commit": "0ed5c691e9208eea52f21db2fc05e226ce3516fd", "audited_files_sha256": { - "tests/test_protocol_conformance.py": "0be1178f57ee6b174359fb5317d41b2ff311f78be09cf590bb5a09d57400368a", + "tests/test_protocol_conformance.py": "666c712dc4e9f2784b2c1f26aa1f313e22bf764e2d2b8e82661e225ad1a71e5a", "tests/protocol_conformance_adapters.py": "ee933fa93c93145de754654b1b3e2d17f57d792adfa5170cae32e23ea5701a07", "tests/protocol_lifecycle_observations.py": "21e08f59921f199362c4ad13cf03ce6f97c7383a98489aebe142e1dd098bf3bd", "tests/conftest.py": "2de2c199e54a4fef8321aeefac1448699adc4428276ade7c361d1bbd1d905533" @@ -27,6 +27,15 @@ "change": "rebound the module from the rc.6 released suite (0c81c1f8, manifest sha256:12e58b82) to the rc.9 one (0ed5c691, manifest sha256:803918bf): the two identity constants, the release record it reads (release/1.0.0-rc.9.json and its claim_v5 block), and the qualification vector's protocol_version, which rc.9 moved in lockstep; and advanced the in-scope inventory counts the module pins for exhaustiveness -- agent-skill-v6 and csk-skill-v6 go 24 -> 28 cases each and SCHEMA_CASES 102 -> 110, because rc.9 adds four invalid-v8-* cases to each of those two v6 families; every other pinned count is unchanged", "isolation_neutral": true, "rationale": "No test function was added, removed, renamed or restructured, and no helper's reachable state changed: the edit replaces literal expectations inside existing bodies and the module-level identity block. What moved is the SUITE, not the code -- rc.9 is a strict superset of rc.6 (0 files removed, 6 changed, 243 added), and the only collection effect is 8 more test_rc6_generated_schema_case_is_consumed parametrisations from the 8 invalid-v8 cases rc.9 adds under agent-skill-v6 and csk-skill-v6. Those inherit the footprint, atomic cluster and shard of the 102 existing rows of that same function, which are uniform. Every other node keeps its footprint, cluster and shard; only source_line shifts, because the audited file gained lines above them." + }, + { + "task_id": "TASK-260827-d319oi", + "file": "tests/test_protocol_conformance.py", + "previous_sha256": "0be1178f57ee6b174359fb5317d41b2ff311f78be09cf590bb5a09d57400368a", + "sha256": "666c712dc4e9f2784b2c1f26aa1f313e22bf764e2d2b8e82661e225ad1a71e5a", + "change": "advanced the accepted Curator Protocol revision to 1.0.0-rc.10 (b8b03d597ac83d158a0eadd9d0b25d2e883de1a3, curator-spec#22): rewrote the module header to state that rc.10 republishes the rc.9 corpus byte for byte so the pinned manifest digest sha256:803918bf authenticates a root served by either revision; deleted the module-level pytest.skip that routed an rc.5 root (sha256:b6f56aac) past this module; and extended test_rc6_claim_versions_remain_separate from claim v1..v3 to v1..v5. No identity constant, count or expectation value changed.", + "isolation_neutral": true, + "rationale": "No test function was added, removed, renamed or restructured, and no parametrisation source moved: SCHEMA_CASES, BUILD_DRIVER_VECTORS, HOST_POLICY_VECTORS, CLAIM_QUALIFICATION_VECTORS and MANAGER_LIFECYCLE_VECTORS all read the same suite bytes as before, because rc.10 changed profiles/manager.md only and left conformance/v1 identical to rc.9 (manifest sha256:803918bf at both revisions). The node-id set is therefore unchanged and every node keeps its footprint, atomic cluster and shard; only source_line shifts, because the header grew and the skip block shrank. The claim-versions edit adds two read-only _json() reads of schema-cases/conformance-claim-v4 and -v5 inside an existing body whose cluster already covers authenticated corpus reads, and touches no process, filesystem or environment state. Deleting the rc.5 skip removes a code path, it does not add one: with the skip gone an rc.5 root fails in _root() at module import instead of skipping the whole module, which is strictly louder and cannot make a shard's node set larger." } ], "method": { diff --git a/LOGBOOK.md b/LOGBOOK.md index 7e61716..24a55c7 100644 --- a/LOGBOOK.md +++ b/LOGBOOK.md @@ -2747,3 +2747,109 @@ runs before the task can satisfy its hosted acceptance gate. ## 2026-08-24 TASK-260824-3gv521: Schema v7 Build Repository Transport Documentation Updated `docs/skill-authoring.md` section 3 (schema v7) to document support for both `git@host:path.git` and `https://host/path.git` transport forms in `build_repositories.*.git`. Clarified that credential selection is required for private repositories in both cases (`build_ssh` and `build_https` scopes), and that choice of transport does not dictate installation access. Stated explicitly that HTTPS URLs must carry the `.git` suffix to avoid GitLab 301 redirects failing under `http.followRedirects=false` with `build_repository_source_unavailable`. Verified error identifier in `src/csk/git_admission.py` and ran `tests/test_git_admission.py` green (exit 0). + + +## 2026-08-26 TASK-260826-29y82b: Spec22 Pin Delta Documentation Update + +Updated `docs/external-build-repositories.md` header block to reflect protocol advancement to Curator Protocol `1.0.0-rc.9` schema-8, accepted protocol revision commit `0ed5c691e9208eea52f21db2fc05e226ce3516fd`, and derived `conformance/v1/manifest.json` SHA-256 `803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403`. Derived manifest digest by cloning `relux-works/curator-spec` at commit `0ed5c691` in `.temp/` and calculating `shasum -a 256 conformance/v1/manifest.json`. Revised credential selection section (around line 232) so the pinned-agent form (`both`) cites `curator-spec#22` as its source (the third canonical authentication-tail form, RECOMMENDED), removing any remaining implication that `csk` goes beyond the spec. Obeyed prose style rules by replacing em-dashes with colons. + + +## 2026-08-26 TASK-260826-29y82b review: the rc.9 header advance contradicts the rc.5 boundary it documents + +The repository holds two protocol pins on purpose and the delta collapsed them into one. +`src/csk/build_repository.py:18-19` still declares `PROTOCOL_VERSION = "1.0.0-rc.5"` and +`CONFORMANCE_MANIFEST_SHA256 = "b6f56aac..."` for the `go-repository-v1` boundary, enforced green by +`tests/test_schema_v7_repository.py::test_rc5_contract_pin`. The candidate and released suite moved +to rc.9 with manifest `803918bf...` (`tests/test_protocol_conformance.py:97-100`), and that module +skips itself on the rc.5 root with the reason stated at `:91`: "rc.5 root is handled by the +external-repository consumer". `docs/external-build-repositories.md` documents the consumer, so +advancing its header to rc.9 asserts something a passing assertion denies. Whichever pin the header +is meant to carry, it cannot carry both silently. + +`curator-spec#22` has not landed and cannot be cited. At the pinned revision `0ed5c691`, +`profiles/manager.md:1384-1387` says the authentication tail is exactly either +`IdentitiesOnly=yes IdentityAgent=none -i ` or +`IdentitiesOnly=no IdentityFile=none IdentityAgent=`. There is no third form, no +RECOMMENDED, and no `#22` anywhere in the spec's files or merged history, which reaches `#27`, +`#28`, `#29`. `.research/260822_tz-docs-0.14.md:194` made the delta conditional on that issue +landing. Meanwhile `src/csk/git_admission.py:505-519` emits a third tail +(`IdentitiesOnly=yes` with both `IdentityAgent=` and `-i `) that the pinned spec +does not admit, so the caveat that csk goes beyond the spec was accurate when it was removed. + +The manifest digest itself is sound. An independent clone at `0ed5c691` reproduces +`803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403` for `conformance/v1/manifest.json`. + +Board note: the Change Request delta was empty because the story worktree at +`.temp/STORY-260824-3rzqxr/worktree` belongs to `cocoaskills-taskboard` and has no `docs/`, while the +task's tooling note pinned the work directory to this repository. The edits live here uncommitted. + + +## 2026-08-27 TASK-260827-d319oi: rc.10 is a revision move, not a corpus move + +Advanced the accepted Curator Protocol revision for the `go-repository-v1` boundary from +`1.0.0-rc.5` to `1.0.0-rc.10` (`relux-works/curator-spec` `b8b03d597ac83d158a0eadd9d0b25d2e883de1a3`, +tag `v1.0.0-rc.10`, curator-spec#22), which is what makes the pinned-agent authentication tail +`src/csk/git_admission.py` already emits a spec-admitted RECOMMENDED third canonical form and unblocks +the header that TASK-260826-29y82b review rejected. + +The finding that shaped the change: **rc.10 does not republish a new corpus.** +`git diff v1.0.0-rc.9..v1.0.0-rc.10` touches `profiles/manager.md` only. `conformance/v1/manifest.json` +hashes to `803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403` at *both* `0ed5c691` +(rc.9) and `b8b03d59` (rc.10); that corpus still **declares** `protocol_version` `1.0.0-rc.9`; and the +spec publishes **no** `release/1.0.0-rc.10.json` — the newest release descriptor is `1.0.0-rc.9.json`. +So the accepted revision and the corpus identity are two different facts at rc.10, and the pair +(`1.0.0-rc.10`, `sha256:803918bf`) is only coherent if they are declared separately. They now are: +`build_repository.CONFORMANCE_CORPUS_PROTOCOL_VERSION = "1.0.0-rc.9"` sits beside `PROTOCOL_VERSION`, +`tests/test_schema_v7_repository.py::test_accepted_contract_pin` asserts they are *not* equal, and +`test_released_accepted_schema_cases` now asserts the authenticated root's declared version as well as +its digest. Collapsing the two is exactly the failure the previous review caught. + +Verified rather than string-replaced, and therefore left alone: +`tests/test_rc5_external_repository_conformance.py` `PROTOCOL` stays `1.0.0-rc.5` (separate corpus, +byte-pinned at `cc9e9c0f`, whose own manifest declares rc.5 — advancing it would assert what the +pinned bytes deny); `conformance-claim-v3` stays on rc.5 in both the corpus valid case and the schema +`const` at rc.10; `go_v1.NATIVE_CONTROL_INVENTORY_VERSION` stays `rc5-native-control-inventory-v1` +because that literal is still in the rc.10 host-policy vectors. + +Fail-closed surface grew, not shrank. The module-level `pytest.skip` in +`tests/test_protocol_conformance.py` that routed an rc.5 root (`sha256:b6f56aac`) past the whole +module is gone: no consumer of an rc.5 conformance root remains, so such a root is now simply wrong +and fails in `_root()` at import instead of silently skipping a conformance consumer. The corpus's +new `invalid-v8-*` cases are consumed: the schemas-1-through-6 sweep went 96 -> 144 rows (132 of them +rejections, counted separately so a corpus that stopped shipping the newer-field cases cannot pass as +a full sweep), and the v7 sweep went 95 -> 103. `test_rc6_claim_versions_remain_separate` went from +v1..v3 to v1..v5 so the schema-8 landing's claim versions cannot drift unobserved. + +`RELEASED_SUITE_PIN` and `.github/ci/candidate-suite.json` were deliberately **not** moved. They are a +different pin with different semantics and their own three-runner qualification evidence, and they +already serve the identical corpus digest. + +Evidence: `python -m mypy` exit 0 (75 files). Full `pytest` exit 0. Root-gated reruns with +`CURATOR_SCHEMA_V7_ROOT` and `CURATOR_CONFORMANCE_ROOT` pointed at the rc.10 checkout, plus an +expected-red mutant that hands the retired rc.5 corpus to the accepted-revision gate and fails on the +digest (exit 1). `.research/TASK-260803-2ol7ok_protocol-isolation-classification.json` re-audited to +`666c712dc4e9f2784b2c1f26aa1f313e22bf764e2d2b8e82661e225ad1a71e5a` with an isolation-neutral +rationale: no node id added, removed or renamed. + +## 2026-08-27 TASK-260827-d319oi finalization: a green full suite is not evidence for the corpus pins + +Verifying the rc.10 pin move surfaced a measurement trap worth recording. The conformance consumers +are `skipif`-gated on `CURATOR_CONFORMANCE_ROOT` / `CURATOR_SCHEMA_V7_ROOT`, and those variables are +set only in CI. Run the required subset with no roots in the environment and it reports +`40 passed, 168 skipped` — **168 of 208**. Every corpus assertion this task turns on (the manifest +digest, the corpus's declared `protocol_version`, the 103 / 144 / 132 case counts) is inside that +skipped set. + +So the orchestrator's `1797 passed, 245 skipped` full-suite run, executed without `CURATOR_*` in its +environment, is green *and* silent about the pins it was taken as confirming. Rerunning +`tests/test_schema_v7_repository.py` with `CURATOR_SCHEMA_V7_ROOT` pointed at an extracted `b8b03d5` +tree gives `43 passed, 0 skipped` — that is the run that actually exercises them. + +Two mutants confirm the new bounds bind rather than decorate: narrowing `case_count == 103` to `102` +fails with `assert 103 == 102` (the real corpus does ship 103), and flipping +`ACCEPTED_CORPUS_PROTOCOL_VERSION` to `1.0.0-rc.10` fails with +`assert '1.0.0-rc.9' == '1.0.0-rc.10'` — the exact rc.10/rc.9 conflation this pin move exists to +prevent, proven red rather than argued. + +Takeaway for the next pin move: when reporting corpus-pin evidence, cite a run whose skip count shows +the corpus consumers were reached. A full-suite pass with the roots unset proves nothing about them. diff --git a/docs/external-build-repositories.md b/docs/external-build-repositories.md index aa0db55..f1ce777 100644 --- a/docs/external-build-repositories.md +++ b/docs/external-build-repositories.md @@ -1,20 +1,20 @@ # External build repositories -CocoaSkills implements the Curator Protocol `1.0.0-rc.5` schema-7 +CocoaSkills implements the Curator Protocol `1.0.0-rc.10` schema-8 `go-repository-v1` boundary. It builds an executable from a separately locked Git repository while keeping the skill package unable to select credentials, Git configuration, hooks, compiler flags, output paths, wrappers, or signing. The accepted protocol revision is -`f5d7673039226ab81de2f4f87e2155ae995c4df3`; its `conformance/v1/manifest.json` +`b8b03d597ac83d158a0eadd9d0b25d2e883de1a3`; its `conformance/v1/manifest.json` SHA-256 is -`b6f56aacc0e37dcc6692f73f641bff761e89b645adfe20a47a06d81c6fda204c`. +`803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403`. The external-repository corpus is supplied to tests independently, so the csk consumer imports no Curator implementation package or internal fixture value. ## Skill declaration -An `agent-skill.json` schema-7 declaration binds a canonical network identity, +An `agent-skill.json` schema-7 or schema-8 declaration binds a canonical network identity, an exact Git object ID, and optionally an exact tag: ```json @@ -214,7 +214,7 @@ selection in the global config, keyed by a canonical-identity prefix: } ``` -A scope is a segment prefix of the schema-7 canonical repository identity +A scope is a segment prefix of the schema-7 or schema-8 canonical repository identity (`host/path`): matching happens only on whole `/` boundaries, and the longest matching scope wins, so a key granted to one namespace never reaches a repository outside it. Flags win over `CSK_BUILD_SSH_*`, and both win over @@ -223,14 +223,15 @@ every configured scope. A scope needs at least one of `agent` or `identity`; each alone is a complete selection: -- `{"agent": "auto"}` — agent-only. The install adopts the operator's live +- `{"agent": "auto"}`: agent-only. The install adopts the operator's live `SSH_AUTH_SOCK` at run time and the agent signs with its loaded keys in turn. No key file is named, so a populated agent can exhaust the server's `MaxAuthTries` budget before reaching the right key. -- `{"identity": "~/.ssh/key"}` — identity-file only, for an unencrypted key +- `{"identity": "~/.ssh/key"}`: identity-file only, for an unencrypted key on disk (`IdentityAgent=none`). -- both — the recommended form for passphrase-protected keys: the agent holds - the private key and the named `.pub` pins which single key is offered. +- both: pinned-agent form. The third canonical authentication-tail form, + RECOMMENDED, per curator-spec#22. The agent holds the private key and the + named `.pub` pins which single key is offered. Manage the map with: @@ -243,14 +244,14 @@ csk config build-ssh remove gitlab.example.com/portals/infra Before any fetch, the install resolves credentials for every declared SSH build repository. On an operator terminal an unmatched repository prompts with -a menu of **detected candidates** — the live agent socket (with its loaded key -count) and the `.pub` files below `~/.ssh` — so the usual answer is a single +a menu of **detected candidates** (the live agent socket with its loaded key +count and the `.pub` files below `~/.ssh`), so the usual answer is a single Enter on the default "agent + pinned key" entry. Discovery only lists what exists; nothing is ever used without the operator's explicit selection, and nothing persists without the explicit scope choice. A non-interactive run fails closed with `build_repository_ssh_credential_missing` and ready-to-run `csk config build-ssh add` commands built from the same detected candidates. `csk install --dry-run` -reports which source — flags, environment, or a config scope — covered each +reports which source (flags, environment, or a config scope) covered each repository. CocoaSkills writes a private wrapper carrying one pinned `ssh` argv and points @@ -271,8 +272,8 @@ The fixed Go contract is the same `manager-worker-v1` session documented in the main README: native toolchain, vendored modules, no network, no workspace, no cgo, internal linking, and manager-derived output. External builds use a receipt-v2 cache below `/external-builds`; schema-7 installations use -marker v3 and may contain local receipt-v1 and external receipt-v2 commands -together. +marker v3 and schema-8 installations use marker v4; both may contain local +receipt-v1 and external receipt-v2 commands together. Project install publishes `.agents/bin/`; global install publishes `/global/bin/`. Both managed launchers point directly at the diff --git a/docs/skill-authoring.md b/docs/skill-authoring.md index 9b41af8..b5dbfbd 100644 --- a/docs/skill-authoring.md +++ b/docs/skill-authoring.md @@ -1,6 +1,6 @@ # Руководство по созданию скиллов CocoaSkills -Это руководство определяет рекомендуемый контракт для репозиториев скиллов CocoaSkills. Документ служит практическим руководством для авторов к [RFC 0003](v0.5-design.md) и текущему [принятому ядру протокола Curator rc.5](https://github.com/relux-works/curator-spec/blob/v1.0.0-rc.5/protocol/core.md). +Это руководство определяет рекомендуемый контракт для репозиториев скиллов CocoaSkills. Документ служит практическим руководством для авторов к [RFC 0003](v0.5-design.md) и текущему [принятому ядру протокола Curator rc.10](https://github.com/relux-works/curator-spec/blob/v1.0.0-rc.10/protocol/core.md). ## 1. Структура репозитория diff --git a/src/csk/build_repository.py b/src/csk/build_repository.py index 942ed67..6a9d511 100644 --- a/src/csk/build_repository.py +++ b/src/csk/build_repository.py @@ -15,8 +15,23 @@ DESCRIPTOR_NAME = "skill-build.json" GO_REPOSITORY_V1_DRIVER = "go-repository-v1" -PROTOCOL_VERSION = "1.0.0-rc.5" -CONFORMANCE_MANIFEST_SHA256 = "b6f56aacc0e37dcc6692f73f641bff761e89b645adfe20a47a06d81c6fda204c" + +# The accepted Curator Protocol revision for the go-repository-v1 boundary. +# 1.0.0-rc.10 is relux-works/curator-spec +# b8b03d597ac83d158a0eadd9d0b25d2e883de1a3 (curator-spec#22), the revision that +# admits the pinned-agent authentication tail this manager emits +# (git_admission.py) as the RECOMMENDED third canonical form. +# +# rc.10 republishes the rc.9 conformance corpus byte for byte -- #22 changed +# profiles/manager.md only -- so the corpus at the accepted revision still +# declares protocol_version 1.0.0-rc.9 and the spec publishes no +# release/1.0.0-rc.10.json. The accepted revision and the corpus identity are +# two different facts, so they are declared separately here and asserted +# separately in tests/test_schema_v7_repository.py: a pin move that advances one +# without the other goes red instead of silently collapsing the two. +PROTOCOL_VERSION = "1.0.0-rc.10" +CONFORMANCE_MANIFEST_SHA256 = "803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403" +CONFORMANCE_CORPUS_PROTOCOL_VERSION = "1.0.0-rc.9" _HOST_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9.-]*$") _SSH_USER_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$") @@ -97,7 +112,9 @@ def parse_repository_source(raw: str) -> RepositorySource: or any(character in raw for character in "%?#\\") or any(character.isspace() or unicodedata.category(character) == "Cc" for character in raw) ): - raise BuildRepositoryError("repository git source is not in the released rc.5 grammar") + raise BuildRepositoryError( + f"repository git source is not in the released {PROTOCOL_VERSION} grammar" + ) host: str repository_path: str diff --git a/tests/test_build_metadata.py b/tests/test_build_metadata.py index 0e1c4ab..8ca4dc4 100644 --- a/tests/test_build_metadata.py +++ b/tests/test_build_metadata.py @@ -25,11 +25,13 @@ from csk.builds.toolchain import NativeTarget -# The released suite at RELEASED_SUITE_PIN -- 1.0.0-rc.9 since the schema-8 -# landing -- publishes expected/marker-v2.json, the marker-v2 writer golden this -# manager's own marker output is compared against. The rc.5 suite -# (sha256:b6f56aac...) carries no writer golden, so no single root can satisfy -# both this digest and the conformance consumer. +# The released suite at RELEASED_SUITE_PIN publishes expected/marker-v2.json, +# the marker-v2 writer golden this manager's own marker output is compared +# against. That corpus declares protocol_version 1.0.0-rc.9 and is republished +# unchanged at the accepted revision 1.0.0-rc.10 +# (build_repository.PROTOCOL_VERSION), so the digest below authenticates a root +# served by either revision. The retired rc.5 suite (sha256:b6f56aac...) +# carries no writer golden and no longer authenticates here at all. EXPECTED_MANIFEST_SHA256 = ( "sha256:803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403" ) diff --git a/tests/test_protocol_conformance.py b/tests/test_protocol_conformance.py index 2374127..be51378 100644 --- a/tests/test_protocol_conformance.py +++ b/tests/test_protocol_conformance.py @@ -71,11 +71,18 @@ ROOT_TEXT = os.environ.get("CURATOR_CONFORMANCE_ROOT") pytestmark = pytest.mark.skipif(not ROOT_TEXT, reason="CURATOR_CONFORMANCE_ROOT is not set") -# rc.5 carries the separately versioned external-repository corpus consumed by -# test_rc5_external_repository_conformance.py. This module intentionally binds -# the released manager/build corpus at RELEASED_SUITE_PIN -- 1.0.0-rc.9 since -# the schema-8 landing -- so do not make an rc.5 root fail collection merely -# because both authenticated consumers share the conventional root env. +# This module binds the released manager/build corpus at RELEASED_SUITE_PIN. +# The corpus that revision publishes declares protocol_version 1.0.0-rc.9 and is +# republished byte for byte at the accepted revision 1.0.0-rc.10 +# (build_repository.PROTOCOL_VERSION), so the digest below authenticates both. +# +# The retired rc.5 corpus (sha256:b6f56aac...) used to be routed past this +# module with a module-level skip. Nothing consumes an rc.5 conformance root +# any more -- the separately versioned external-repository corpus consumed by +# test_rc5_external_repository_conformance.py has its own digest and its own +# CURATOR_EXTERNAL_REPOSITORY_CORPUS_ROOT env -- so a root this module cannot +# authenticate is now a wrong root and fails in _root() instead of silently +# skipping an entire conformance consumer. # # The test names keep their historical rc6_ prefix on purpose. The Windows # protocol lane pins an ORDERED node-id baseline in @@ -83,16 +90,6 @@ # isolation classification; renaming the functions would rewrite every one of # those rows for no behavioural gain and is a separate change. What the module # authenticates is the identity below, not the prefix. -if ROOT_TEXT: - _candidate_manifest = Path(ROOT_TEXT) / "manifest.json" - if _candidate_manifest.is_file() and hashlib.sha256( - _candidate_manifest.read_bytes() - ).hexdigest() == "b6f56aacc0e37dcc6692f73f641bff761e89b645adfe20a47a06d81c6fda204c": - pytest.skip( - "rc.5 root is handled by the external-repository consumer", - allow_module_level=True, - ) - EXPECTED_CANDIDATE_MANIFEST_SHA256 = ( "sha256:803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403" ) @@ -954,14 +951,20 @@ def test_rc6_native_control_inventory_is_closed_and_exhaustive() -> None: def test_rc6_claim_versions_remain_separate() -> None: + # Every claim version the corpus ships, not a prefix of them: a claim + # version that silently re-pointed at a newer protocol revision is exactly + # the drift this asserts against, and only checking v1..v3 would let the + # schema-8 landing's v4 and v5 drift unobserved. claims = [ _json(f"schema-cases/conformance-claim-v{version}/valid.json") - for version in (1, 2, 3) + for version in (1, 2, 3, 4, 5) ] assert [(claim["schema_version"], claim["protocol_version"]) for claim in claims] == [ (1, "1.0.0-rc.3"), (2, "1.0.0-rc.4"), (3, "1.0.0-rc.5"), + (4, "1.0.0-rc.8"), + (5, "1.0.0-rc.9"), ] assert "build_drivers" not in claims[0] assert "build_drivers" not in claims[1] diff --git a/tests/test_rc5_external_repository_conformance.py b/tests/test_rc5_external_repository_conformance.py index 401f5cd..20a1fc8 100644 --- a/tests/test_rc5_external_repository_conformance.py +++ b/tests/test_rc5_external_repository_conformance.py @@ -17,6 +17,11 @@ reason="CURATOR_EXTERNAL_REPOSITORY_CORPUS_ROOT is not set", ) +# This corpus is versioned separately from the Curator Protocol revision this +# manager accepts (build_repository.PROTOCOL_VERSION, now 1.0.0-rc.10). Its +# bytes are pinned by ACCEPTED_MANIFEST_SHA256 below and they declare +# 1.0.0-rc.5, so advancing the value here would assert something the pinned +# bytes contradict. It moves only when the corpus itself is republished. PROTOCOL = "1.0.0-rc.5" CORPUS_VERSION = "rc5-external-repository-interop-v1" ACCEPTED_MANIFEST_SHA256 = ( diff --git a/tests/test_schema_v7_repository.py b/tests/test_schema_v7_repository.py index b6cbdef..bd83cf5 100644 --- a/tests/test_schema_v7_repository.py +++ b/tests/test_schema_v7_repository.py @@ -10,7 +10,12 @@ from csk import build_repository, dev_substitutions, skillspec -RC5_MANIFEST_SHA256 = "b6f56aacc0e37dcc6692f73f641bff761e89b645adfe20a47a06d81c6fda204c" +# The conformance corpus published at the accepted revision (1.0.0-rc.10, +# curator-spec b8b03d59). rc.10 republishes the rc.9 corpus byte for byte, so +# the digest below is the rc.9 digest and the corpus still declares +# protocol_version 1.0.0-rc.9. Both facts are pinned separately on purpose. +ACCEPTED_MANIFEST_SHA256 = "803918bf8672f76cf990985e51db213b826674cd5bb54fbf47731b8404b44403" +ACCEPTED_CORPUS_PROTOCOL_VERSION = "1.0.0-rc.9" def _write_json(path: Path, payload: object) -> None: @@ -48,9 +53,19 @@ def _repository_manifest(**command_overrides: object) -> dict[str, object]: } -def test_rc5_contract_pin() -> None: - assert build_repository.PROTOCOL_VERSION == "1.0.0-rc.5" - assert build_repository.CONFORMANCE_MANIFEST_SHA256 == RC5_MANIFEST_SHA256 +def test_accepted_contract_pin() -> None: + assert build_repository.PROTOCOL_VERSION == "1.0.0-rc.10" + assert build_repository.CONFORMANCE_MANIFEST_SHA256 == ACCEPTED_MANIFEST_SHA256 + # The accepted revision is not the corpus identity. rc.10 carries the rc.9 + # corpus unchanged, and conflating the two is what a stale pin move does. + assert ( + build_repository.CONFORMANCE_CORPUS_PROTOCOL_VERSION + == ACCEPTED_CORPUS_PROTOCOL_VERSION + ) + assert ( + build_repository.CONFORMANCE_CORPUS_PROTOCOL_VERSION + != build_repository.PROTOCOL_VERSION + ) def test_schema_v7_parses_declared_repository_and_command(tmp_path: Path) -> None: @@ -138,7 +153,7 @@ def test_repository_source_canonicalization(raw: str, identity: str, transport: "https://git.example.com/a\u0085b", ], ) -def test_repository_source_rejects_non_rc5_forms(raw: str) -> None: +def test_repository_source_rejects_non_released_forms(raw: str) -> None: with pytest.raises(build_repository.BuildRepositoryError): build_repository.parse_repository_source(raw) @@ -306,13 +321,20 @@ def test_local_selector_normalization_is_idempotent() -> None: assert "//" not in normalized -def test_released_rc5_schema_cases(tmp_path: Path) -> None: +def test_released_accepted_schema_cases(tmp_path: Path) -> None: root_text = os.environ.get("CURATOR_SCHEMA_V7_ROOT") if root_text is None: pytest.skip("CURATOR_SCHEMA_V7_ROOT is not set") root = Path(root_text) manifest = root / "manifest.json" - assert hashlib.sha256(manifest.read_bytes()).hexdigest() == RC5_MANIFEST_SHA256 + raw_manifest = manifest.read_bytes() + assert hashlib.sha256(raw_manifest).hexdigest() == ACCEPTED_MANIFEST_SHA256 + # Authenticating the bytes proves which corpus was read; asserting the + # version it declares proves the corpus is the one the accepted revision + # republishes rather than a later one that happens to be handed over. + assert ( + json.loads(raw_manifest)["protocol_version"] == ACCEPTED_CORPUS_PROTOCOL_VERSION + ) case_count = 0 for suite, manifest_name in (("agent-skill-v7", "agent-skill.json"), ("csk-skill-v7", "csk-skill.json")): @@ -342,19 +364,28 @@ def test_released_rc5_schema_cases(tmp_path: Path) -> None: else: with pytest.raises(dev_substitutions.DevSubstitutionError): dev_substitutions.parse_manifest(case_path.read_bytes(), tmp_path) - assert case_count == 95 + # 103 at the accepted revision: the schema-8 landing added the four + # invalid-v8-* rejection cases to each of the two v7 suites. + assert case_count == 103 -def test_released_schemas_1_through_6_do_not_accept_v7_fields(tmp_path: Path) -> None: +def test_released_schemas_1_through_6_do_not_accept_v7_or_v8_fields( + tmp_path: Path, +) -> None: root_text = os.environ.get("CURATOR_SCHEMA_V7_ROOT") if root_text is None: pytest.skip("CURATOR_SCHEMA_V7_ROOT is not set") cases_root = Path(root_text) / "schema-cases" case_count = 0 + rejected = 0 for version in range(1, 7): for prefix, manifest_name in (("agent-skill", "agent-skill.json"), ("csk-skill", "csk-skill.json")): suite = cases_root / f"{prefix}-v{version}" - selected = [suite / "valid.json", *sorted(suite.glob("invalid-v7-*.json"))] + selected = [ + suite / "valid.json", + *sorted(suite.glob("invalid-v7-*.json")), + *sorted(suite.glob("invalid-v8-*.json")), + ] for case_path in selected: case_count += 1 snapshot = tmp_path / f"{prefix}-v{version}" / case_path.stem @@ -363,9 +394,15 @@ def test_released_schemas_1_through_6_do_not_accept_v7_fields(tmp_path: Path) -> if case_path.name == "valid.json": skillspec.load_skill_spec(snapshot) else: + rejected += 1 with pytest.raises(skillspec.SkillSpecError): skillspec.load_skill_spec(snapshot) - assert case_count == 96 + # 96 v7-rejection rows plus the 48 invalid-v8-* rows the schema-8 landing + # added (6 versions x 2 manifest families x 4 cases). Counting the + # rejections separately keeps a corpus that silently stopped shipping the + # newer-field cases from passing as a full sweep. + assert case_count == 144 + assert rejected == 132 def test_released_source_identity_vectors() -> None: