From 59bb30bf88d5cae11d84a2f4c1e631a381fea05a Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 08:24:18 +1000 Subject: [PATCH 01/10] docs: finalize AI 0.98.0 release notes --- crates/graphql-orm-ai/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index d7242bf..f27a5f3 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,7 +18,7 @@ checkpoint facts. For the current workspace baseline and active gates, use the [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). -## Unreleased +## [0.98.0] - 2026-09-15 ### Fixed From 98516302057bbcbd959b596f369f80b18e9c6a6e Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 08:29:48 +1000 Subject: [PATCH 02/10] build: bind auth release tags to their locked source commits --- AGENTS.md | 5 +- docs/operations/release/process.md | 8 ++++ scripts/check-release-manifest.sh | 1 + scripts/generate-release-manifest.py | 50 ++++++++++++++++---- scripts/test-release-manifest.py | 68 ++++++++++++++++++++++++++++ 5 files changed, 121 insertions(+), 11 deletions(-) create mode 100644 scripts/test-release-manifest.py diff --git a/AGENTS.md b/AGENTS.md index c970194..58ff4fe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,8 +19,9 @@ under a crate add package-local invariants. `graphql-orm-operation-catalog`, `graphql-orm-storage`, `graphql-orm-backup`, `graphql-orm-ai-tool-profiles`, `graphql-orm-ai`, `graphql-orm-router-protocol`, and `graphql-orm-router`. -- `agql-auth` remains an external exact-revision dependency. Do not modify its - repository unless the task explicitly includes it. +- `agql-auth` remains external. Adopt a reviewed full revision or a published + version release tag whose full locked commit is retained in the release manifest. + Do not modify its repository unless the task explicitly includes it. - Keep the packages independently consumable. Do not turn AI, backup, or storage into features or optional dependencies of the core ORM crate. - Preserve this acyclic dependency direction: diff --git a/docs/operations/release/process.md b/docs/operations/release/process.md index b4b6a35..d3a6531 100644 --- a/docs/operations/release/process.md +++ b/docs/operations/release/process.md @@ -160,6 +160,14 @@ CycloneDX inventory, and approval reference, and includes the archive in the release checksums and provenance attestation. A later target or feature set requires its own approval. +External `agql-auth` dependencies may select a published `v` release tag. +The manifest keeps its full locked commit in `externalGitDependencies[].revision` +and additionally records `tag`; all other external Git dependencies still require +full revision pins. Generation fails if the lockfile has no unique matching source +or disagrees with an explicit revision. Verify the auth release's attested manifest +and peeled tag before adopting it. Consumers seeking one Cargo source must use the +same tag selector: a `rev` and a tag remain different sources even at the same SHA. + Pure Rust libraries do not receive optimized binary artifacts. Downstream Cargo builds compile them from the pinned Git source. diff --git a/scripts/check-release-manifest.sh b/scripts/check-release-manifest.sh index fe4540e..308196a 100755 --- a/scripts/check-release-manifest.sh +++ b/scripts/check-release-manifest.sh @@ -7,6 +7,7 @@ if [[ $# -gt 1 ]]; then fi repository_root=$(git rev-parse --show-toplevel) +python3 "${repository_root}/scripts/test-release-manifest.py" ref=${1:-HEAD} commit=$(git -C "${repository_root}" rev-parse "${ref}^{commit}") head_commit=$(git -C "${repository_root}" rev-parse "HEAD^{commit}") diff --git a/scripts/generate-release-manifest.py b/scripts/generate-release-manifest.py index 5276382..999e73c 100644 --- a/scripts/generate-release-manifest.py +++ b/scripts/generate-release-manifest.py @@ -186,9 +186,14 @@ def manifest_packages( return result -def external_git_dependencies(metadata: dict[str, Any]) -> list[dict[str, Any]]: +def external_git_dependencies( + metadata: dict[str, Any], lock: dict[str, Any] | None = None +) -> list[dict[str, Any]]: + if lock is None: + with (ROOT / "Cargo.lock").open("rb") as handle: + lock = tomllib.load(handle) workspace_members = set(metadata["workspace_members"]) - aggregated: dict[tuple[str, str, str], set[str]] = {} + aggregated: dict[tuple[str, str, str, str], set[str]] = {} for package in metadata["packages"]: if package["id"] not in workspace_members: continue @@ -197,14 +202,40 @@ def external_git_dependencies(metadata: dict[str, Any]) -> list[dict[str, Any]]: if not source.startswith("git+"): continue parsed = urlsplit(source.removeprefix("git+")) - revisions = parse_qs(parsed.query).get("rev", []) - revision = revisions[0] if len(revisions) == 1 else "" - if not FULL_SHA_RE.fullmatch(revision): + query = parse_qs(parsed.query, keep_blank_values=True) + revisions = query.get("rev", []) + tags = query.get("tag", []) + url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}" + exact_revision = ( + set(query) == {"rev"} + and len(revisions) == 1 + and FULL_SHA_RE.fullmatch(revisions[0]) + ) + auth_release = ( + set(query) == {"tag"} + and len(tags) == 1 + and dependency["name"] == "agql-auth" + and url == "https://github.com/Dastari/agql-auth.git" + and re.fullmatch(r"v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)", tags[0]) + ) + if not (exact_revision or auth_release): raise SystemExit( - f"{package['name']}: Git dependency {dependency['name']} is not exact-revision resolved" + f"{package['name']}: Git dependency {dependency['name']} requires " + "a full revision or an agql-auth version release tag" ) - url = f"{parsed.scheme}://{parsed.netloc}{parsed.path}" - key = (dependency["name"], url, revision) + resolved = { + entry["source"].partition("#")[2] + for entry in lock.get("package", []) + if entry.get("name") == dependency["name"] + and entry.get("source", "").partition("#")[0] == source + } + if len(resolved) != 1 or not FULL_SHA_RE.fullmatch(next(iter(resolved), "")): + raise SystemExit(f"{dependency['name']}: expected exactly one full locked Git commit") + revision = resolved.pop() + if exact_revision and revision != revisions[0]: + raise SystemExit(f"{dependency['name']}: locked commit does not match the full revision") + tag = tags[0] if auth_release else "" + key = (dependency["name"], url, revision, tag) aggregated.setdefault(key, set()).add(package["name"]) return [ { @@ -212,8 +243,9 @@ def external_git_dependencies(metadata: dict[str, Any]) -> list[dict[str, Any]]: "name": name, "revision": revision, "url": url, + **({"tag": tag} if tag else {}), } - for (name, url, revision), consumers in sorted(aggregated.items()) + for (name, url, revision, tag), consumers in sorted(aggregated.items()) ] diff --git a/scripts/test-release-manifest.py b/scripts/test-release-manifest.py new file mode 100644 index 0000000..4135351 --- /dev/null +++ b/scripts/test-release-manifest.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Validate external dependency identities without network or live repositories.""" +import importlib.util +from pathlib import Path +import unittest + +spec = importlib.util.spec_from_file_location( + "release_manifest", Path(__file__).with_name("generate-release-manifest.py") +) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) + + +class ExternalGitDependenciesTests(unittest.TestCase): + commit = "a" * 40 + url = "https://github.com/Dastari/agql-auth.git" + + def fixture(self, query, commit=None, name="agql-auth"): + source = f"git+{self.url}?{query}" + metadata = {"workspace_members": ["consumer"], "packages": [{ + "id": "consumer", "name": "consumer", "dependencies": [ + {"name": name, "source": source} + ] + }]} + lock = {"package": [{"name": name, "source": f"{source}#{commit or self.commit}"}]} + return metadata, lock + + def test_full_revision_record_is_unchanged(self): + rows = module.external_git_dependencies(*self.fixture(f"rev={self.commit}")) + self.assertEqual(rows, [{"consumers": ["consumer"], "name": "agql-auth", + "revision": self.commit, "url": self.url}]) + + def test_auth_tag_records_the_locked_commit(self): + rows = module.external_git_dependencies(*self.fixture("tag=v0.19.0")) + self.assertEqual(rows[0]["revision"], self.commit) + self.assertEqual(rows[0]["tag"], "v0.19.0") + + def test_revision_and_lock_must_agree(self): + with self.assertRaisesRegex(SystemExit, "does not match"): + module.external_git_dependencies(*self.fixture(f"rev={self.commit}", "b" * 40)) + + def test_missing_or_ambiguous_lock_is_rejected(self): + metadata, lock = self.fixture("tag=v0.19.0") + with self.assertRaisesRegex(SystemExit, "exactly one"): + module.external_git_dependencies(metadata, {"package": []}) + second = dict(lock["package"][0]) + second["source"] = second["source"].replace(self.commit, "b" * 40) + lock["package"].append(second) + with self.assertRaisesRegex(SystemExit, "exactly one"): + module.external_git_dependencies(metadata, lock) + + def test_tag_reference_cannot_be_satisfied_by_same_commit_revision(self): + metadata, _ = self.fixture("tag=v0.19.0") + _, wrong_source = self.fixture(f"rev={self.commit}") + with self.assertRaisesRegex(SystemExit, "exactly one"): + module.external_git_dependencies(metadata, wrong_source) + + def test_unreviewed_references_are_rejected(self): + for query in ["branch=main", "tag=latest", "tag=v01.0.0", "rev=abcdef", + f"tag=v0.19.0&rev={self.commit}", "tag=v0.19.0&tag=v0.19.1"]: + with self.subTest(query=query), self.assertRaises(SystemExit): + module.external_git_dependencies(*self.fixture(query)) + with self.assertRaises(SystemExit): + module.external_git_dependencies(*self.fixture("tag=v0.19.0", name="other-crate")) + + +if __name__ == "__main__": + unittest.main() From bcf2c85aae16474d8390a46ee1aa49558f30f6ae Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 08:35:05 +1000 Subject: [PATCH 03/10] ci: reject workspace publication without its human reviewer --- .github/workflows/release.yml | 5 +++++ docs/operations/release/process.md | 3 +++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ac30a6..8a7de65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,8 +69,13 @@ jobs: RELEASE_ID: ${{ inputs.release_id }} INCLUDE_ROUTER: ${{ inputs.include_router_artifact }} ROUTER_APPROVAL: ${{ inputs.router_distribution_approval }} + GH_TOKEN: ${{ github.token }} run: | set -euo pipefail + # An environment name alone does not enforce human approval. + protection=$(gh api "repos/$GITHUB_REPOSITORY/environments/release" \ + --jq '.protection_rules | any(.type == "required_reviewers" and any(.reviewers[]; .type == "User" and .reviewer.login == "Dastari"))') + test "$protection" = true git fetch origin main --tags --force test "$(git rev-parse HEAD)" = "${TARGET_REF}" test "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" diff --git a/docs/operations/release/process.md b/docs/operations/release/process.md index d3a6531..49c512e 100644 --- a/docs/operations/release/process.md +++ b/docs/operations/release/process.md @@ -125,6 +125,9 @@ Run **Workspace release** manually and supply: - `include_router_artifact`: normally false for source-only releases; and - `router_distribution_approval`: required when a router binary is attached. +Configure `Dastari` as a required human reviewer on the `release` environment +and restrict it to `main` before dispatch. The guard rejects an environment +without that reviewer; naming an environment in YAML does not create protection. The protected `release` environment is the human authorization boundary and gates the workflow's entry job, so no release lane runs before approval. The workflow then: From 3b18e9d28c529fb357514afbd12add85ffecb243 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 08:57:54 +1000 Subject: [PATCH 04/10] build: adopt verified agql-auth v0.19.0 release tag --- Cargo.lock | 26 ++++++++++++------------- Cargo.toml | 2 +- crates/graphql-orm-ai/CHANGELOG.md | 2 +- docs/development/setup.md | 3 ++- docs/development/testing.md | 4 ++-- docs/reference/workspace-packages.md | 4 ++-- scripts/generate-workspace-inventory.py | 2 +- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6ac5896..5a2d3b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,7 +88,7 @@ dependencies = [ [[package]] name = "agql-auth" version = "0.19.0" -source = "git+https://github.com/Dastari/agql-auth.git?rev=1d2e9fe2e1576105212a7b340a11abf8cad0382d#1d2e9fe2e1576105212a7b340a11abf8cad0382d" +source = "git+https://github.com/Dastari/agql-auth.git?tag=v0.19.0#51f33bfa151f7471a7cedee7e89d688041f7ae05" dependencies = [ "argon2", "async-graphql", @@ -210,7 +210,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -221,7 +221,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2563,7 +2563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4401,7 +4401,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5501,7 +5501,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6780,7 +6780,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7474,7 +7474,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7566,7 +7566,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8368,7 +8368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8907,10 +8907,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8930,7 +8930,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 643c7cc..c13ce3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ license = "MIT" repository = "https://github.com/Dastari/graphql-orm" [workspace.dependencies] -agql-auth = { git = "https://github.com/Dastari/agql-auth.git", rev = "1d2e9fe2e1576105212a7b340a11abf8cad0382d", version = "0.19.0" } +agql-auth = { git = "https://github.com/Dastari/agql-auth.git", tag = "v0.19.0", version = "0.19.0" } arc-swap = "1.7" async-graphql = { version = "7", features = ["dataloader", "uuid"] } async-graphql-parser = "7" diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index f27a5f3..498bf34 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -3,7 +3,7 @@ title: "Changelog" kind: reference status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-09-02 +last_reviewed: 2026-09-15 review_by: 2027-02-01 supersedes: [] --- diff --git a/docs/development/setup.md b/docs/development/setup.md index a37984f..3259c9c 100644 --- a/docs/development/setup.md +++ b/docs/development/setup.md @@ -58,7 +58,8 @@ cargo check -p graphql-orm --no-default-features --features "sqlite mssql" Use a checked-in workspace path dependency for another package in this repository. The root `Cargo.lock` is shared, and `agql-auth` remains an -external exact-revision dependency. +external Git dependency whose full resolved commit is retained in the release +manifest, including when selected by its published version tag. For consumer dependency configuration and feature descriptions, see the [ORM reference](../reference/graphql-orm/backends.md). diff --git a/docs/development/testing.md b/docs/development/testing.md index 2e5f133..8165690 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -132,8 +132,8 @@ acceptance evidence until migrated to the owned harness. ## Authentication bridge lane -Changes involving `auth-agql` must retain the external exact revision and test -the bridge feature explicitly: +Changes involving `auth-agql` must retain one external Git selector and its +reviewed locked commit, and test the bridge feature explicitly: ```sh cargo check -p graphql-orm --no-default-features --features "sqlite auth-agql" diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index b729cb2..dc37e28 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -28,9 +28,9 @@ changes. | `graphql-orm-router-protocol` | `0.2.1` | `crates/graphql-orm-router-protocol` | none | none | | `graphql-orm-storage` | `0.6.2` | `crates/graphql-orm-storage` | `local` | none | -External exact-revision dependency: +External Git dependency: -- `agql-auth` requirement `^0.19.0`, source `git+https://github.com/Dastari/agql-auth.git?rev=1d2e9fe2e1576105212a7b340a11abf8cad0382d`, consumed by `graphql-orm`, `graphql-orm-ai`, `graphql-orm-router`. +- `agql-auth` requirement `^0.19.0`, source `git+https://github.com/Dastari/agql-auth.git?tag=v0.19.0`, consumed by `graphql-orm`, `graphql-orm-ai`, `graphql-orm-router`. diff --git a/scripts/generate-workspace-inventory.py b/scripts/generate-workspace-inventory.py index 03bd99a..f242f80 100644 --- a/scripts/generate-workspace-inventory.py +++ b/scripts/generate-workspace-inventory.py @@ -74,7 +74,7 @@ def render(metadata: dict[str, object]) -> str: lines.extend( [ "", - "External exact-revision dependency:", + "External Git dependency:", "", f"- `agql-auth` requirement `{req}`, source `{source}`, consumed by " + ", ".join(f"`{consumer}`" for consumer in consumers) From 67e9b85d566a245307b8ce85385f69fc02a76358 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 09:13:32 +1000 Subject: [PATCH 05/10] test: align backend coexistence fixture with auth release tag --- crates/graphql-orm-ai/docs/implementation-status.md | 10 +++++----- crates/graphql-orm-router/MIGRATION.md | 10 +++++++++- .../graphql-orm/tests/backend_coexistence_fixture.rs | 4 ++-- .../tests/fixtures/backend-coexistence/Cargo.lock | 10 +++++----- .../backend-coexistence/auth-service/Cargo.toml | 2 +- 5 files changed, 22 insertions(+), 14 deletions(-) diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index a663627..1d04101 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -3,17 +3,17 @@ title: "Implementation Status" kind: reference status: active owner: graphql-orm-ai-maintainers -last_reviewed: 2026-09-02 +last_reviewed: 2026-09-15 review_by: 2027-02-01 supersedes: [] --- # Implementation Status -`graphql-orm-ai` is at crate version `0.97.1` with AI schema module -`0.64.0`. It uses workspace `graphql-orm` `0.30.0`, backend-neutral -`graphql-orm-ai-tool-profiles` `0.11.0`, and external `agql-auth` -`0.19.0` at `1d2e9fe2e1576105212a7b340a11abf8cad0382d`. +Current package versions and dependency selectors are generated in the +[workspace inventory](../../../docs/reference/workspace-inventory.md). +The AI schema module remains `0.64.0`. External `agql-auth` uses the published +`v0.19.0` tag with its full resolved commit retained in `Cargo.lock`. Completed stateless local-provider turns can carry a proof-bearing contained native-item refusal after authoritative usage settlement. Those runs close as diff --git a/crates/graphql-orm-router/MIGRATION.md b/crates/graphql-orm-router/MIGRATION.md index c110081..95c83a8 100644 --- a/crates/graphql-orm-router/MIGRATION.md +++ b/crates/graphql-orm-router/MIGRATION.md @@ -3,13 +3,21 @@ title: graphql-orm-router migration guide kind: reference status: active owner: graphql-orm-router-maintainers -last_reviewed: 2026-08-13 +last_reviewed: 2026-09-15 review_by: 2027-02-07 supersedes: [] --- # graphql-orm-router migration guide +## Workspace release dependency alignment + +For workspace releases selecting `agql-auth` v0.19.0, direct consumers must use +`tag = "v0.19.0"`, locked to `51f33bfa151f7471a7cedee7e89d688041f7ae05`. +A tag and a revision selector produce different Cargo sources even at the same +commit. The library source is unchanged from the historical revision below; +no configuration or stored-data migration is needed. + ## 0.5.0 to 0.5.1 No configuration, schema, token, descriptor, or stored-data migration is diff --git a/crates/graphql-orm/tests/backend_coexistence_fixture.rs b/crates/graphql-orm/tests/backend_coexistence_fixture.rs index b4e9e67..f026f18 100644 --- a/crates/graphql-orm/tests/backend_coexistence_fixture.rs +++ b/crates/graphql-orm/tests/backend_coexistence_fixture.rs @@ -71,8 +71,8 @@ fn assert_direct_host_dependency_resolves_one_exact_agql_auth_universe() { .as_str() .expect("agql-auth source must be present"); assert!( - source.contains("rev=1d2e9fe2e1576105212a7b340a11abf8cad0382d") - && source.ends_with("#1d2e9fe2e1576105212a7b340a11abf8cad0382d"), + source.contains("?tag=v0.19.0") + && source.ends_with("#51f33bfa151f7471a7cedee7e89d688041f7ae05"), "unexpected agql-auth source: {source}", ); } diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock index c34db0a..4e62e1d 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock @@ -11,7 +11,7 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "agql-auth" version = "0.19.0" -source = "git+https://github.com/Dastari/agql-auth.git?rev=1d2e9fe2e1576105212a7b340a11abf8cad0382d#1d2e9fe2e1576105212a7b340a11abf8cad0382d" +source = "git+https://github.com/Dastari/agql-auth.git?tag=v0.19.0#51f33bfa151f7471a7cedee7e89d688041f7ae05" dependencies = [ "argon2", "async-graphql", @@ -925,7 +925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2766,7 +2766,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3373,10 +3373,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml b/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml index 7de5794..fcaa130 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -agql-auth = { git = "https://github.com/Dastari/agql-auth.git", rev = "1d2e9fe2e1576105212a7b340a11abf8cad0382d", version = "0.19.0" } +agql-auth = { git = "https://github.com/Dastari/agql-auth.git", tag = "v0.19.0", version = "0.19.0" } async-graphql = { version = "7", features = ["dataloader", "uuid"] } graphql-orm = { path = "../../../../", default-features = false, features = [ "sqlite", From 1eb124b9248575f600451bb1a2fb80b1c16c28ae Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 09:13:51 +1000 Subject: [PATCH 06/10] docs: fix generated package inventory link --- crates/graphql-orm-ai/docs/implementation-status.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index 1d04101..c5dafa8 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -11,7 +11,7 @@ supersedes: [] # Implementation Status Current package versions and dependency selectors are generated in the -[workspace inventory](../../../docs/reference/workspace-inventory.md). +[workspace inventory](../../../docs/reference/workspace-packages.md). The AI schema module remains `0.64.0`. External `agql-auth` uses the published `v0.19.0` tag with its full resolved commit retained in `Cargo.lock`. From d89ae41d424b0505490b830b581396184a1ab4ad Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 09:18:07 +1000 Subject: [PATCH 07/10] build: adopt compatible HTTP2 and TLS security fixes --- Cargo.lock | 86 +++++++++++++------------- crates/graphql-orm-router/CHANGELOG.md | 8 ++- crates/graphql-orm-router/MIGRATION.md | 4 +- 3 files changed, 53 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a2d3b6..9808659 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,7 +210,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -221,7 +221,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -600,9 +600,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "zeroize", @@ -610,9 +610,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -788,7 +788,7 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "h2 0.3.27", - "h2 0.4.15", + "h2 0.4.16", "http 0.2.12", "http 1.5.0", "http-body 0.4.6", @@ -799,7 +799,7 @@ dependencies = [ "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.43", + "rustls 0.23.45", "rustls-native-certs 0.8.4", "rustls-pki-types", "tokio", @@ -2563,7 +2563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3328,9 +3328,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27" dependencies = [ "atomic-waker", "bytes", @@ -3653,7 +3653,7 @@ dependencies = [ "reqwest-middleware", "reqwest-retry", "retry-policies", - "rustls 0.23.43", + "rustls 0.23.45", "serde", "serde_json", "sonic-rs", @@ -3774,7 +3774,7 @@ dependencies = [ "ntex", "recloser", "regex-automata", - "rustls 0.23.43", + "rustls 0.23.45", "ryu", "serde", "serde_json", @@ -4001,7 +4001,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "h2 0.4.15", + "h2 0.4.16", "http 1.5.0", "http-body 1.1.0", "httparse", @@ -4038,7 +4038,7 @@ dependencies = [ "hyper 1.11.0", "hyper-util", "log", - "rustls 0.23.43", + "rustls 0.23.45", "rustls-native-certs 0.8.4", "tokio", "tokio-rustls 0.26.4", @@ -4401,7 +4401,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5208,7 +5208,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "regex", - "rustls 0.23.43", + "rustls 0.23.45", "serde", "serde_json", "serde_urlencoded", @@ -5472,7 +5472,7 @@ dependencies = [ "ntex-net", "ntex-service", "ntex-util", - "rustls 0.23.43", + "rustls 0.23.45", ] [[package]] @@ -5501,7 +5501,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -6738,7 +6738,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.43", + "rustls 0.23.45", "socket2 0.5.10", "thiserror 2.0.18", "tokio", @@ -6760,7 +6760,7 @@ dependencies = [ "rand_pcg", "ring", "rustc-hash", - "rustls 0.23.43", + "rustls 0.23.45", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -6780,7 +6780,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7093,7 +7093,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2 0.4.15", + "h2 0.4.16", "http 1.5.0", "http-body 1.1.0", "http-body-util", @@ -7105,7 +7105,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.43", + "rustls 0.23.45", "rustls-native-certs 0.8.4", "rustls-pki-types", "serde", @@ -7136,7 +7136,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "h2 0.4.15", + "h2 0.4.16", "http 1.5.0", "http-body 1.1.0", "http-body-util", @@ -7148,7 +7148,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.43", + "rustls 0.23.45", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -7474,7 +7474,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7491,16 +7491,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.43" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "aws-lc-rs", "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.13", + "rustls-webpki 0.103.15", "subtle", "zeroize", ] @@ -7559,14 +7559,14 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.43", + "rustls 0.23.45", "rustls-native-certs 0.8.4", "rustls-platform-verifier-android", - "rustls-webpki 0.103.13", + "rustls-webpki 0.103.15", "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7587,9 +7587,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -8368,7 +8368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8473,7 +8473,7 @@ dependencies = [ "once_cell", "percent-encoding", "rust_decimal", - "rustls 0.23.43", + "rustls 0.23.45", "serde", "serde_json", "sha2 0.10.9", @@ -8665,7 +8665,7 @@ dependencies = [ "rand 0.8.6", "reqwest 0.12.28", "rsa", - "rustls 0.23.43", + "rustls 0.23.45", "serde", "serde_derive", "sha1 0.10.6", @@ -8907,10 +8907,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8930,7 +8930,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9129,7 +9129,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.43", + "rustls 0.23.45", "tokio", ] @@ -9220,7 +9220,7 @@ dependencies = [ "axum", "base64 0.22.1", "bytes", - "h2 0.4.15", + "h2 0.4.16", "http 1.5.0", "http-body 1.1.0", "http-body-util", diff --git a/crates/graphql-orm-router/CHANGELOG.md b/crates/graphql-orm-router/CHANGELOG.md index 95ccfe2..69fc84a 100644 --- a/crates/graphql-orm-router/CHANGELOG.md +++ b/crates/graphql-orm-router/CHANGELOG.md @@ -3,7 +3,7 @@ title: graphql-orm-router changelog kind: reference status: active owner: graphql-orm-router-maintainers -last_reviewed: 2026-08-13 +last_reviewed: 2026-09-15 review_by: 2027-02-07 supersedes: [] --- @@ -12,6 +12,12 @@ supersedes: [] ## 0.5.1 - 2026-09-02 +- Updated the reviewed workspace lockfile to h2 0.4.16 and rustls 0.23.45, + including rustls-webpki 0.103.15 and AWS-LC 1.18.1 / sys 0.45.0 required by + rustls. These address RUSTSEC-2026-0258 and RUSTSEC-2026-0285 before the + workspace release. The existing ADR-0008 restrictions on Hive storage and + private-key operations remain in force for the quick-xml and rsa findings. + - Replaced the full duplicate of WebSocket subscription variables with a bounded scalar-only authorization projection. Large data variables remain single-copy, so operations such as chunked uploads stay within the private diff --git a/crates/graphql-orm-router/MIGRATION.md b/crates/graphql-orm-router/MIGRATION.md index 95c83a8..fed7e4b 100644 --- a/crates/graphql-orm-router/MIGRATION.md +++ b/crates/graphql-orm-router/MIGRATION.md @@ -16,7 +16,9 @@ For workspace releases selecting `agql-auth` v0.19.0, direct consumers must use `tag = "v0.19.0"`, locked to `51f33bfa151f7471a7cedee7e89d688041f7ae05`. A tag and a revision selector produce different Cargo sources even at the same commit. The library source is unchanged from the historical revision below; -no configuration or stored-data migration is needed. +no configuration or stored-data migration is needed. The workspace lockfile +also updates h2 and rustls to their compatible security fixes; rebuild router +binaries from the reviewed lockfile. ## 0.5.0 to 0.5.1 From 5ae392681ae937532225d2235c449960ad1949a4 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 09:50:52 +1000 Subject: [PATCH 08/10] build: adopt verified MIT-licensed agql-auth v0.19.1 --- Cargo.lock | 26 +++++++++---------- Cargo.toml | 2 +- .../docs/implementation-status.md | 2 +- crates/graphql-orm-router/MIGRATION.md | 6 ++--- .../tests/backend_coexistence_fixture.rs | 6 ++--- .../fixtures/backend-coexistence/Cargo.lock | 4 +-- .../auth-service/Cargo.toml | 2 +- docs/development/testing.md | 8 +++++- docs/reference/workspace-packages.md | 2 +- 9 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9808659..e8e224b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -87,8 +87,8 @@ dependencies = [ [[package]] name = "agql-auth" -version = "0.19.0" -source = "git+https://github.com/Dastari/agql-auth.git?tag=v0.19.0#51f33bfa151f7471a7cedee7e89d688041f7ae05" +version = "0.19.1" +source = "git+https://github.com/Dastari/agql-auth.git?tag=v0.19.1#96bafbf21adbc7ad963729e1e981feaba5debe90" dependencies = [ "argon2", "async-graphql", @@ -210,7 +210,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -221,7 +221,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2563,7 +2563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4401,7 +4401,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5501,7 +5501,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6780,7 +6780,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7474,7 +7474,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7566,7 +7566,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8368,7 +8368,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8910,7 +8910,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8930,7 +8930,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8c27177b12a6399ffc08b98f76f7c9a1f4fe9fc967c784c5a071fa8d93cf7e1" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c13ce3e..5f18ead 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ license = "MIT" repository = "https://github.com/Dastari/graphql-orm" [workspace.dependencies] -agql-auth = { git = "https://github.com/Dastari/agql-auth.git", tag = "v0.19.0", version = "0.19.0" } +agql-auth = { git = "https://github.com/Dastari/agql-auth.git", tag = "v0.19.1", version = "0.19.1" } arc-swap = "1.7" async-graphql = { version = "7", features = ["dataloader", "uuid"] } async-graphql-parser = "7" diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index c5dafa8..f300c2b 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -13,7 +13,7 @@ supersedes: [] Current package versions and dependency selectors are generated in the [workspace inventory](../../../docs/reference/workspace-packages.md). The AI schema module remains `0.64.0`. External `agql-auth` uses the published -`v0.19.0` tag with its full resolved commit retained in `Cargo.lock`. +`v0.19.1` tag with its full resolved commit retained in `Cargo.lock`. Completed stateless local-provider turns can carry a proof-bearing contained native-item refusal after authoritative usage settlement. Those runs close as diff --git a/crates/graphql-orm-router/MIGRATION.md b/crates/graphql-orm-router/MIGRATION.md index fed7e4b..935f668 100644 --- a/crates/graphql-orm-router/MIGRATION.md +++ b/crates/graphql-orm-router/MIGRATION.md @@ -12,10 +12,10 @@ supersedes: [] ## Workspace release dependency alignment -For workspace releases selecting `agql-auth` v0.19.0, direct consumers must use -`tag = "v0.19.0"`, locked to `51f33bfa151f7471a7cedee7e89d688041f7ae05`. +For workspace releases selecting `agql-auth` v0.19.1, direct consumers must use +`tag = "v0.19.1"`, locked to `96bafbf21adbc7ad963729e1e981feaba5debe90`. A tag and a revision selector produce different Cargo sources even at the same -commit. The library source is unchanged from the historical revision below; +commit. The MIT-licensed library source is unchanged from the historical revision below; no configuration or stored-data migration is needed. The workspace lockfile also updates h2 and rustls to their compatible security fixes; rebuild router binaries from the reviewed lockfile. diff --git a/crates/graphql-orm/tests/backend_coexistence_fixture.rs b/crates/graphql-orm/tests/backend_coexistence_fixture.rs index f026f18..9ee0374 100644 --- a/crates/graphql-orm/tests/backend_coexistence_fixture.rs +++ b/crates/graphql-orm/tests/backend_coexistence_fixture.rs @@ -66,13 +66,13 @@ fn assert_direct_host_dependency_resolves_one_exact_agql_auth_universe() { .filter(|package| package["name"] == "agql-auth") .collect::>(); assert_eq!(agql_auth.len(), 1, "resolved metadata:\n{metadata}"); - assert_eq!(agql_auth[0]["version"], "0.19.0"); + assert_eq!(agql_auth[0]["version"], "0.19.1"); let source = agql_auth[0]["source"] .as_str() .expect("agql-auth source must be present"); assert!( - source.contains("?tag=v0.19.0") - && source.ends_with("#51f33bfa151f7471a7cedee7e89d688041f7ae05"), + source.contains("?tag=v0.19.1") + && source.ends_with("#96bafbf21adbc7ad963729e1e981feaba5debe90"), "unexpected agql-auth source: {source}", ); } diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock index 4e62e1d..27206be 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/Cargo.lock @@ -10,8 +10,8 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "agql-auth" -version = "0.19.0" -source = "git+https://github.com/Dastari/agql-auth.git?tag=v0.19.0#51f33bfa151f7471a7cedee7e89d688041f7ae05" +version = "0.19.1" +source = "git+https://github.com/Dastari/agql-auth.git?tag=v0.19.1#96bafbf21adbc7ad963729e1e981feaba5debe90" dependencies = [ "argon2", "async-graphql", diff --git a/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml b/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml index fcaa130..128b11c 100644 --- a/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml +++ b/crates/graphql-orm/tests/fixtures/backend-coexistence/auth-service/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -agql-auth = { git = "https://github.com/Dastari/agql-auth.git", tag = "v0.19.0", version = "0.19.0" } +agql-auth = { git = "https://github.com/Dastari/agql-auth.git", tag = "v0.19.1", version = "0.19.1" } async-graphql = { version = "7", features = ["dataloader", "uuid"] } graphql-orm = { path = "../../../../", default-features = false, features = [ "sqlite", diff --git a/docs/development/testing.md b/docs/development/testing.md index 8165690..e875df6 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -3,7 +3,7 @@ title: "Testing and verification" kind: reference status: active owner: workspace-maintainers -last_reviewed: 2026-08-13 +last_reviewed: 2026-09-15 review_by: 2027-02-01 supersedes: [] --- @@ -14,6 +14,12 @@ Run the narrowest package and backend lane that covers a change. Database backends are alternative configurations, so never use workspace `--all-features`. +Full release validation follows the stable compiler used by CI (Rust 1.98.1 +at the current release preparation). Compile-failure snapshots track that +compiler's diagnostics, and the locked S3/Azure SDK lane requires Rust 1.94.1 +or newer. Rust 1.90.0 is the separate router minimum-version lane; it is not +a workspace-wide compiler requirement. + ## Baseline ORM lane ```sh diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index dc37e28..9559208 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -30,7 +30,7 @@ changes. External Git dependency: -- `agql-auth` requirement `^0.19.0`, source `git+https://github.com/Dastari/agql-auth.git?tag=v0.19.0`, consumed by `graphql-orm`, `graphql-orm-ai`, `graphql-orm-router`. +- `agql-auth` requirement `^0.19.1`, source `git+https://github.com/Dastari/agql-auth.git?tag=v0.19.1`, consumed by `graphql-orm`, `graphql-orm-ai`, `graphql-orm-router`. From 802d19807a5642fa6d25176b55c52b9527f925f7 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 11:11:35 +1000 Subject: [PATCH 09/10] Package router third-party notices and source evidence --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 17 +- config/router-notice-review.v1.json | 393 ++++++++++++++++++++++++++++ docs/operations/release/process.md | 27 +- scripts/generate-router-notices.py | 185 +++++++++++++ scripts/test-router-notices.py | 142 ++++++++++ 6 files changed, 760 insertions(+), 5 deletions(-) create mode 100644 config/router-notice-review.v1.json create mode 100755 scripts/generate-router-notices.py create mode 100755 scripts/test-router-notices.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8111ca..038a7f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,6 +36,7 @@ jobs: run: | python3 scripts/check-release-state.py scripts/check-release-manifest.sh + python3 scripts/test-router-notices.py - name: Check PR documentation impact if: github.event_name == 'pull_request' env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a7de65..48fd394 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,6 +108,7 @@ jobs: python3 scripts/generate-workspace-inventory.py --check python3 scripts/check-release-state.py scripts/check-release-manifest.sh + python3 scripts/test-router-notices.py scripts/check-workspace-dependencies.sh cargo fmt --all -- --check @@ -393,22 +394,34 @@ jobs: SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) export SOURCE_DATE_EPOCH cargo install cargo-cyclonedx --version 0.5.9 --locked + rustup component add rust-docs cargo build -p graphql-orm-router --release --features auth-agql --locked cargo cyclonedx \ --manifest-path crates/graphql-orm-router/Cargo.toml \ --format json \ --spec-version 1.5 \ - --features auth-agql + --features auth-agql \ + --target x86_64-unknown-linux-gnu install -m 0755 target/release/graphql-orm-router release-dist/graphql-orm-router cp crates/graphql-orm-router/graphql-orm-router.cdx.json \ release-dist/graphql-orm-router.cdx.json + cargo metadata --format-version 1 --locked \ + --filter-platform x86_64-unknown-linux-gnu \ + --features graphql-orm-router/auth-agql > release-dist/router-metadata.json + python3 scripts/generate-router-notices.py \ + --metadata release-dist/router-metadata.json \ + --sbom release-dist/graphql-orm-router.cdx.json \ + --output release-dist/router-notices \ + --rust-sysroot "$(rustc --print sysroot)" + rm release-dist/router-metadata.json cp LICENSE release-dist/LICENSE printf '%s\n' "${ROUTER_APPROVAL}" > release-dist/router-distribution-approval.txt tar -C release-dist --sort=name --mtime="@${SOURCE_DATE_EPOCH}" \ --owner=0 --group=0 --numeric-owner \ -czf "release-dist/${RELEASE_ID}-graphql-orm-router-x86_64-unknown-linux-gnu.tar.gz" \ graphql-orm-router graphql-orm-router.cdx.json LICENSE \ - router-distribution-approval.txt + router-distribution-approval.txt router-notices + rm -r -- release-dist/router-notices rm release-dist/graphql-orm-router release-dist/graphql-orm-router.cdx.json \ release-dist/LICENSE release-dist/router-distribution-approval.txt - name: Create checksums diff --git a/config/router-notice-review.v1.json b/config/router-notice-review.v1.json new file mode 100644 index 0000000..9db0964 --- /dev/null +++ b/config/router-notice-review.v1.json @@ -0,0 +1,393 @@ +{ + "formatVersion": 1, + "cargoLockSha256": "57037f90a41b6a85def58b93c278959c60cc483d0a731d09cc636ea7f8391dd0", + "componentsSha256": "c27d4628abfb737a9a3c916e63cf12c174c57ef0cb2672403f06d05eb160a211", + "target": "x86_64-unknown-linux-gnu", + "features": [ + "auth-agql" + ], + "supplements": [ + { + "name": "Inflector", + "version": "0.11.4", + "notices": [ + { + "url": "https://raw.githubusercontent.com/whatisinternet/inflector/a4a95eac75043f4bffb127c7c8ec886b5b106053/LICENSE.md", + "sha256": "03d1bd5bfbee8d44651e7f57faf9e5b4eda4233f0d4eda36a716f2f0533d230b" + } + ] + }, + { + "name": "alloc-stdlib", + "version": "0.2.4", + "notices": [ + { + "url": "https://raw.githubusercontent.com/dropbox/rust-alloc-no-stdlib/ae42d22078b98549e987d2f03d12df7b984fde47/LICENSE", + "sha256": "c0c56f26d9c051cac4d200c34c84e7ae9aaa853e01a982a1df08b09931e518ae" + } + ] + }, + { + "name": "async-dropper-simple", + "version": "0.2.6", + "notices": [ + { + "url": "https://raw.githubusercontent.com/t3hmrman/async-dropper/a6ccaef55e3907dfc1376f70db517abeaf5c764f/LICENSE", + "sha256": "405a9a2b3ad77707786386959b65a8aaa41539b463eeaae28e8bdc971b15a82c" + } + ] + }, + { + "name": "base64-simd", + "version": "0.8.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/Nugine/simd/d74c030d9dc4f3cae02146d1f497ff62726ef09a/LICENSE", + "sha256": "71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d" + } + ] + }, + { + "name": "cynic-parser", + "version": "0.11.2", + "notices": [ + { + "url": "https://codeberg.org/obmarg/cynic/raw/commit/adc3685ebae599e713e49ee7fa39abebf8c9b676/LICENSE", + "sha256": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5" + } + ] + }, + { + "name": "cynic-parser-deser", + "version": "0.11.2", + "notices": [ + { + "url": "https://codeberg.org/obmarg/cynic/raw/commit/adc3685ebae599e713e49ee7fa39abebf8c9b676/LICENSE", + "sha256": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5" + } + ] + }, + { + "name": "cynic-parser-deser-macros", + "version": "0.11.2", + "notices": [ + { + "url": "https://codeberg.org/obmarg/cynic/raw/commit/adc3685ebae599e713e49ee7fa39abebf8c9b676/LICENSE", + "sha256": "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5" + } + ] + }, + { + "name": "defmt-parser", + "version": "1.0.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/knurling-rs/defmt/4a8cdb44891ed57b8ff5a023b6bec7137c48708f/LICENSE-APACHE", + "sha256": "8173d5c29b4f956d532781d2b86e4e30f83e6b7878dce18c919451d6ba707c90" + }, + { + "url": "https://raw.githubusercontent.com/knurling-rs/defmt/4a8cdb44891ed57b8ff5a023b6bec7137c48708f/LICENSE-MIT", + "sha256": "2710a622a896bba67356913d4d0492cab5465f61b2ecce6d880aeb483834fb50" + } + ] + }, + { + "name": "graphql-composition", + "version": "0.12.2", + "notices": [ + { + "url": "https://raw.githubusercontent.com/grafbase/grafbase/b6522774d6c2df4cda341c3e73e148ce0a00b86f/LICENSE", + "sha256": "3f3d9e0024b1921b067d6f7f88deb4a60cbe7a78e76c64e3f1d7fc3b779b9d04" + }, + { + "url": "https://raw.githubusercontent.com/grafbase/grafbase/b6522774d6c2df4cda341c3e73e148ce0a00b86f/crates/LICENSE", + "sha256": "86cf9656479f1edb82245b985f4a2cc0d503b945766ebeda4269c60b307699ae" + } + ] + }, + { + "name": "graphql-tools", + "version": "0.5.8", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/5aaa579ddf1a6618097b1925293305f84dc562d9/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "hive-console-sdk", + "version": "0.3.19", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/5aaa579ddf1a6618097b1925293305f84dc562d9/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "hive-router", + "version": "0.0.87", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/56cf846f15e960babb12585855b613f85048796a/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "hive-router-config", + "version": "0.1.10", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/56cf846f15e960babb12585855b613f85048796a/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "hive-router-internal", + "version": "0.0.40", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/56cf846f15e960babb12585855b613f85048796a/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "hive-router-plan-executor", + "version": "7.0.2", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/56cf846f15e960babb12585855b613f85048796a/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "hive-router-query-planner", + "version": "2.10.11", + "notices": [ + { + "url": "https://raw.githubusercontent.com/graphql-hive/router/5aaa579ddf1a6618097b1925293305f84dc562d9/LICENSE_MIT", + "sha256": "3cef4f573bae2133919c37f13739ba81e5a617ecab87b3d8a6494086f1ecb2ac" + } + ] + }, + { + "name": "nom-language", + "version": "0.1.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/rust-bakery/nom/2cec1b3e4c9ccac62c902d60c00de6d1549ccbe1/LICENSE", + "sha256": "4dbda04344456f09a7a588140455413a9ac59b6b26a1ef7cdf9c800c012d87f0" + } + ] + }, + { + "name": "opentelemetry", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-appender-tracing", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-http", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-jaeger-propagator", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-otlp", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-prometheus", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-proto", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-stdout", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry-zipkin", + "version": "0.32.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/ec289cb3c6f8260951699c51df968560943c1451/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "opentelemetry_sdk", + "version": "0.32.1", + "notices": [ + { + "url": "https://raw.githubusercontent.com/open-telemetry/opentelemetry-rust/284a37d93b3856e1975c2807ba3af1421ebd9b52/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "prost-reflect", + "version": "0.14.7", + "notices": [ + { + "url": "https://raw.githubusercontent.com/andrewhickman/prost-reflect/71c4c98d7565dfc30f76ebc7daaa139a443419e6/LICENSE-APACHE", + "sha256": "a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2" + }, + { + "url": "https://raw.githubusercontent.com/andrewhickman/prost-reflect/71c4c98d7565dfc30f76ebc7daaa139a443419e6/LICENSE-MIT", + "sha256": "23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3" + } + ] + }, + { + "name": "tonic-prost", + "version": "0.14.6", + "notices": [ + { + "url": "https://raw.githubusercontent.com/hyperium/tonic/6cb6056b5a748bc5a29bd48f4602dbc4e552bb7d/LICENSE", + "sha256": "e24a56698aa6feaf3a02272b3624f9dc255d982970c5ed97ac4525a95056a5b3" + } + ] + }, + { + "name": "typify", + "version": "0.7.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/oxidecomputer/typify/01153fa2fea45d660400e3060d91fa2e102976d8/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "typify-impl", + "version": "0.7.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/oxidecomputer/typify/01153fa2fea45d660400e3060d91fa2e102976d8/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "typify-macro", + "version": "0.7.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/oxidecomputer/typify/01153fa2fea45d660400e3060d91fa2e102976d8/LICENSE", + "sha256": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4" + } + ] + }, + { + "name": "uuid-simd", + "version": "0.8.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/Nugine/simd/d74c030d9dc4f3cae02146d1f497ff62726ef09a/LICENSE", + "sha256": "71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d" + } + ] + }, + { + "name": "vsimd", + "version": "0.8.0", + "notices": [ + { + "url": "https://raw.githubusercontent.com/Nugine/simd/d74c030d9dc4f3cae02146d1f497ff62726ef09a/LICENSE", + "sha256": "71674605ec4c087fe9eb534e3e4f9e26eb2e4aabcd76a29fd156c6a844d44b3d" + } + ] + } + ], + "sourceOnlyNoticePackages": [ + { + "name": "async-scoped", + "version": "0.9.0" + }, + { + "name": "community-id", + "version": "0.2.4" + }, + { + "name": "grafbase-workspace-hack", + "version": "0.1.0" + }, + { + "name": "graphql-wrapping-types", + "version": "0.4.0" + }, + { + "name": "http-serde", + "version": "2.1.1" + }, + { + "name": "influxdb-line-protocol", + "version": "2.0.0" + }, + { + "name": "seahash", + "version": "4.1.0" + } + ] +} diff --git a/docs/operations/release/process.md b/docs/operations/release/process.md index 49c512e..2f6a481 100644 --- a/docs/operations/release/process.md +++ b/docs/operations/release/process.md @@ -80,6 +80,7 @@ a side effect of this process. scripts/check-package-release-policy.sh scripts/check-semver.sh scripts/check-release-manifest.sh + python3 scripts/test-router-notices.py cargo fmt --all -- --check ``` @@ -159,9 +160,29 @@ delivery channel have a designated approval under ADR-0008. The binary lane is therefore opt-in and requires an evidence reference. It builds the explicit `auth-agql` feature profile for `x86_64-unknown-linux-gnu`, packages the binary with the workspace license, -CycloneDX inventory, and approval reference, and includes the archive in the -release checksums and provenance attestation. A later target or feature set -requires its own approval. +CycloneDX inventory, third-party notice/source evidence, and approval reference, +and includes the archive in the release checksums and provenance attestation. +A later target or feature set requires its own approval. + +`scripts/generate-router-notices.py` collects packaged and nested/native notice +files and supplemental upstream notices whose URLs and SHA-256 values were +reviewed at recorded source commits. It also includes exact registry source +archives for MPL components and the explicitly recorded packages whose +standalone notice files were unavailable. The archives must match Cargo.lock +checksums. The generated inventory preserves original license expressions, +source URLs, file hashes, and source-only notice dispositions; it contains no +builder-local source paths. The bundle also retains the matching Rust standard-library +copyright notices from the toolchain's `rust-docs` component. + +`config/router-notice-review.v1.json` binds these notice-source exceptions to +the lockfile hash, SBOM component set, target, and features. A dependency/profile +change requires a fresh review of those bindings and supplemental sources; +generation fails if the existing record does not match. This configuration is +technical evidence, not distribution approval. The designated owner must +review source-only notice dispositions, legacy license expressions, applicable +source obligations, and the actual delivery contents before providing the +workflow's `router_distribution_approval` reference. Do not treat a missing +notice as an automatically approved exception. External `agql-auth` dependencies may select a published `v` release tag. The manifest keeps its full locked commit in `externalGitDependencies[].revision` diff --git a/scripts/generate-router-notices.py b/scripts/generate-router-notices.py new file mode 100755 index 0000000..cee4956 --- /dev/null +++ b/scripts/generate-router-notices.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Package reviewed router notices and exact source archives for release review.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tomllib +import urllib.request + + +def sha256(data): + return hashlib.sha256(data).hexdigest() + + +def component_digest(components): + pairs = sorted((c['name'], c['version']) for c in components) + if len(pairs) != len(set(pairs)): + raise ValueError('duplicate SBOM package identity') + return sha256(json.dumps(pairs, separators=(',', ':')).encode()) + + +def notice_file(path): + return path.is_file() and ( + re.search(r'(?i)(^|[._-])(unlicen[cs]e|licen[cs]es?|copying|copyright|notice)([._-]|$)', path.name) + or any(part.lower() in ('licenses', 'licences') for part in path.parts) + ) and path.suffix.lower() not in ('.rs', '.c', '.h', '.toml', '.json', '.py') + + +def package_name(name, version): + if not re.fullmatch(r'[A-Za-z0-9_-]+', name) or not re.fullmatch(r'[0-9A-Za-z.+_-]+', version): + raise ValueError('unsafe package identity') + return f'{name}-{version}' + + +def checked_download(url, expected): + if not url.startswith('https://'): + raise ValueError('notice/source URL must use HTTPS') + with urllib.request.urlopen(url, timeout=60) as response: + data = response.read(64 * 1024 * 1024 + 1) + if len(data) > 64 * 1024 * 1024: + raise ValueError(f'notice/source download exceeds 64 MiB: {url}') + if sha256(data) != expected: + raise ValueError(f'download digest mismatch: {url}') + return data + + +def generate(root, metadata, sbom, review, output, cargo_home): + if review.get('formatVersion') != 1: + raise ValueError('unsupported router notice review format') + if review.get('target') != 'x86_64-unknown-linux-gnu' or review.get('features') != ['auth-agql']: + raise ValueError('unreviewed router artifact profile') + lock_bytes = (root / 'Cargo.lock').read_bytes() + if sha256(lock_bytes) != review['cargoLockSha256']: + raise ValueError('router notice review does not cover this Cargo.lock') + components = sbom['components'] + if component_digest(components) != review['componentsSha256']: + raise ValueError('router notice review does not cover this SBOM component set') + packages = {} + for package in metadata['packages']: + key = (package['name'], package['version']) + if key in packages: + raise ValueError(f'ambiguous metadata package: {key}') + packages[key] = package + lock = tomllib.loads(lock_bytes.decode()) + checksums = {(p['name'], p['version']): p.get('checksum') for p in lock['package']} + supplements = {(p['name'], p['version']): p for p in review['supplements']} + source_only = {(p['name'], p['version']) for p in review['sourceOnlyNoticePackages']} + output.mkdir(parents=True, exist_ok=False) + rows = [] + downloads = {} + for component in sorted(components, key=lambda p: (p['name'], p['version'])): + key = (component['name'], component['version']) + p = packages[key] + if not p.get('license'): + raise ValueError(f'missing license declaration: {key}') + label = package_name(*key) + folder = Path(p['manifest_path']).parent.resolve() + files = {f for f in folder.rglob('*') if notice_file(f)} + if p.get('license_file'): + files.add(folder / p['license_file']) + notices = [] + for f in sorted(files): + resolved = f.resolve() + if not resolved.is_relative_to(folder) or f.is_symlink(): + raise ValueError(f'notice escapes package source: {f}') + destination = output / 'notices' / label / f.relative_to(folder) + destination.parent.mkdir(parents=True, exist_ok=True) + data = f.read_bytes() + destination.write_bytes(data) + notices.append({'path': str(destination.relative_to(output)), 'sha256': sha256(data)}) + if p['source'] is None: + if not folder.is_relative_to(root.resolve()): + raise ValueError('unexpected local package outside the workspace') + data = (root / 'LICENSE').read_bytes() + destination = output / 'notices' / label / 'WORKSPACE-LICENSE' + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(data) + notices.append({'path': str(destination.relative_to(output)), 'sha256': sha256(data)}) + for extra in supplements.get(key, {}).get('notices', []): + cache_key = (extra['url'], extra['sha256']) + if cache_key not in downloads: + downloads[cache_key] = checked_download(*cache_key) + destination = output / 'notices' / label / ('UPSTREAM-' + extra['sha256'] + '.txt') + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(downloads[cache_key]) + notices.append({'path': str(destination.relative_to(output)), **extra}) + if not notices and key not in source_only: + raise ValueError(f'unreviewed package without notice files: {key}') + row = {'name': key[0], 'version': key[1], 'declaredLicense': p['license'], + 'source': p['source'] or 'workspace', 'notices': notices} + if not notices: + row['noticeDisposition'] = 'source-only evidence; designated owner review required' + if 'MPL-2.0' in p['license'] or key in source_only: + checksum = checksums.get(key) + if not checksum or not (p['source'] or '').startswith('registry+'): + raise ValueError(f'no locked registry archive checksum: {key}') + filename = label + '.crate' + data = None + for cached in sorted((cargo_home / 'registry/cache').glob('*/' + filename)): + candidate = cached.read_bytes() + if sha256(candidate) == checksum: + data = candidate + break + url = f'https://static.crates.io/crates/{key[0]}/{filename}' + if data is None: + data = checked_download(url, checksum) + destination = output / 'sources' / filename + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(data) + row['sourceArchive'] = {'path': str(destination.relative_to(output)), 'url': url, 'sha256': checksum} + rows.append(row) + inventory = {'cargoLockSha256': review['cargoLockSha256'], 'componentsSha256': review['componentsSha256'], + 'target': review['target'], 'features': review['features'], 'packages': rows} + (output / 'inventory.json').write_text(json.dumps(inventory, indent=2) + '\n') + (output / 'README.txt').write_text( + 'Router third-party notice and source evidence\n\n' + 'The inventory retains upstream license declarations, including legacy slash expressions.\n' + 'The notices directory includes packaged, nested/native and reviewed upstream notice files.\n' + 'The sources directory includes exact Cargo.lock-checksummed published archives for MPL\n' + 'components and reviewed packages whose standalone notice files were unavailable.\n' + 'MPL source archives also retain embedded notices, including the Cynic lexer MIT notice.\n' + 'The toolchain directory retains the matching Rust standard-library copyright notices.\n' + 'Source-only notice dispositions require the designated release owner review recorded\n' + 'in the enclosing router-distribution-approval.txt; generation is not that approval.\n' + ) + return inventory + + + +def collect_rust_notices(sysroot, output, version): + source = sysroot / 'share/doc/rust/COPYRIGHT-library.html' + if not source.is_file(): + raise ValueError('Rust standard-library notices missing; install the matching rust-docs component') + destination = output / 'toolchain/COPYRIGHT-library.html' + destination.parent.mkdir(parents=True, exist_ok=True) + data = source.read_bytes() + destination.write_bytes(data) + (destination.parent / 'rustc-version.txt').write_text(version) + return {'compiler': version.strip(), 'notices': [{'path': str(destination.relative_to(output)), 'sha256': sha256(data)}]} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--metadata', type=Path, required=True) + parser.add_argument('--sbom', type=Path, required=True) + parser.add_argument('--output', type=Path, required=True) + parser.add_argument('--rust-sysroot', type=Path, required=True) + parser.add_argument('--root', type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument('--review', type=Path) + args = parser.parse_args() + review_path = args.review or args.root / 'config/router-notice-review.v1.json' + result = generate(args.root, json.loads(args.metadata.read_text()), json.loads(args.sbom.read_text()), + json.loads(review_path.read_text()), args.output, + Path(os.environ.get('CARGO_HOME', str(Path.home() / '.cargo')))) + version = subprocess.check_output([str(args.rust_sysroot / 'bin/rustc'), '--version', '--verbose'], text=True) + result['rustToolchain'] = collect_rust_notices(args.rust_sysroot, args.output, version) + (args.output / 'inventory.json').write_text(json.dumps(result, indent=2) + '\n') + print(f"Packaged notice/source evidence for {len(result['packages'])} router dependency components") + + +if __name__ == '__main__': + main() diff --git a/scripts/test-router-notices.py b/scripts/test-router-notices.py new file mode 100755 index 0000000..1a5debb --- /dev/null +++ b/scripts/test-router-notices.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Regression tests for router notice review bindings and safe collection.""" +import hashlib +import importlib.util +import io +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location('router_notices', Path(__file__).with_name('generate-router-notices.py')) +notices = importlib.util.module_from_spec(spec) +spec.loader.exec_module(notices) + + +class RouterNoticeTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) + self.crate = self.root / 'fixture' + self.crate.mkdir() + (self.crate / 'Cargo.toml').write_text('[package]\nname="fixture"\nversion="1.0.0"\n') + (self.crate / 'LICENSE').write_text('Exact upstream license fixture\n') + self.archive = b'exact locked registry archive fixture' + self.archive_sha = hashlib.sha256(self.archive).hexdigest() + (self.root / 'Cargo.lock').write_text( + 'version=4\n[[package]]\nname="fixture"\nversion="1.0.0"\n' + 'source="registry+https://github.com/rust-lang/crates.io-index"\n' + f'checksum="{self.archive_sha}"\n') + self.cargo = self.root / 'cargo' + cache = self.cargo / 'registry/cache/test' + cache.mkdir(parents=True) + (cache / 'fixture-1.0.0.crate').write_bytes(self.archive) + self.metadata = {'packages': [{'name': 'fixture', 'version': '1.0.0', 'license': 'MIT', + 'source': 'registry+https://github.com/rust-lang/crates.io-index', + 'manifest_path': str(self.crate / 'Cargo.toml')}]} + self.sbom = {'components': [{'name': 'fixture', 'version': '1.0.0'}]} + self.review = {'formatVersion': 1, 'cargoLockSha256': notices.sha256((self.root / 'Cargo.lock').read_bytes()), + 'componentsSha256': notices.component_digest(self.sbom['components']), + 'target': 'x86_64-unknown-linux-gnu', 'features': ['auth-agql'], + 'supplements': [], 'sourceOnlyNoticePackages': []} + self.output = self.root / 'output' + + def generate(self): + return notices.generate(self.root, self.metadata, self.sbom, self.review, self.output, self.cargo) + + def test_collects_exact_notice_with_portable_inventory(self): + result = self.generate() + record = result['packages'][0]['notices'][0] + self.assertEqual((self.output / record['path']).read_bytes(), (self.crate / 'LICENSE').read_bytes()) + self.assertNotIn(str(self.root), json.dumps(result)) + + def test_rejects_changed_lock_before_collection(self): + (self.root / 'Cargo.lock').write_text('changed') + with self.assertRaisesRegex(ValueError, 'Cargo.lock'): + self.generate() + self.assertFalse(self.output.exists()) + + def test_rejects_wrong_sbom_component_set(self): + self.sbom['components'].append({'name': 'extra', 'version': '1.0.0'}) + with self.assertRaisesRegex(ValueError, 'component set'): + self.generate() + + def test_rejects_duplicate_sbom_identity(self): + with self.assertRaisesRegex(ValueError, 'duplicate'): + notices.component_digest(self.sbom['components'] * 2) + + def test_missing_notice_requires_reviewed_source_only_entry(self): + (self.crate / 'LICENSE').unlink() + with self.assertRaisesRegex(ValueError, 'without notice'): + self.generate() + + def test_source_only_entry_includes_exact_archive_and_disposition(self): + (self.crate / 'LICENSE').unlink() + self.review['sourceOnlyNoticePackages'] = [{'name': 'fixture', 'version': '1.0.0'}] + row = self.generate()['packages'][0] + self.assertEqual((self.output / row['sourceArchive']['path']).read_bytes(), self.archive) + self.assertIn('owner review required', row['noticeDisposition']) + + def test_mpl_always_includes_locked_source_archive(self): + self.metadata['packages'][0]['license'] = 'MPL-2.0' + row = self.generate()['packages'][0] + self.assertTrue(row['notices']) + self.assertEqual(row['sourceArchive']['sha256'], self.archive_sha) + + def test_tampered_cache_cannot_replace_locked_archive(self): + self.metadata['packages'][0]['license'] = 'MPL-2.0' + (self.cargo / 'registry/cache/test/fixture-1.0.0.crate').write_bytes(b'tampered') + with patch.object(notices, 'checked_download', return_value=self.archive) as download: + row = self.generate()['packages'][0] + download.assert_called_once_with('https://static.crates.io/crates/fixture/fixture-1.0.0.crate', self.archive_sha) + self.assertEqual((self.output / row['sourceArchive']['path']).read_bytes(), self.archive) + + def test_download_requires_exact_hash_and_https(self): + data = b'upstream notice' + with patch.object(notices.urllib.request, 'urlopen', return_value=io.BytesIO(data)): + self.assertEqual(notices.checked_download('https://example.invalid/LICENSE', notices.sha256(data)), data) + with patch.object(notices.urllib.request, 'urlopen', return_value=io.BytesIO(data)): + with self.assertRaisesRegex(ValueError, 'digest mismatch'): + notices.checked_download('https://example.invalid/LICENSE', '0' * 64) + with self.assertRaisesRegex(ValueError, 'HTTPS'): + notices.checked_download('http://example.invalid/LICENSE', '0' * 64) + + def test_rejects_notice_symlink_outside_package(self): + (self.crate / 'LICENSE').unlink() + outside = self.root / 'outside' + outside.write_text('must not copy') + (self.crate / 'LICENSE').symlink_to(outside) + with self.assertRaisesRegex(ValueError, 'escapes'): + self.generate() + + def test_rejects_missing_license_declaration(self): + self.metadata['packages'][0]['license'] = None + with self.assertRaisesRegex(ValueError, 'license declaration'): + self.generate() + + def test_rust_standard_library_notices_are_copied_from_matching_sysroot(self): + sysroot = self.root / 'rust' + source = sysroot / 'share/doc/rust/COPYRIGHT-library.html' + source.parent.mkdir(parents=True) + source.write_text('exact Rust library notice fixture') + self.output.mkdir() + result = notices.collect_rust_notices(sysroot, self.output, 'rustc fixture\n') + self.assertEqual((self.output / result['notices'][0]['path']).read_bytes(), source.read_bytes()) + self.assertEqual(result['compiler'], 'rustc fixture') + source.unlink() + with self.assertRaisesRegex(ValueError, 'rust-docs'): + notices.collect_rust_notices(sysroot, self.output, 'rustc fixture\n') + + def test_rejects_unsafe_names_and_unreviewed_profiles(self): + for name, version in [('../escape', '1.0.0'), ('fixture', '../escape')]: + with self.assertRaises(ValueError): + notices.package_name(name, version) + self.review['features'] = [] + with self.assertRaisesRegex(ValueError, 'artifact profile'): + self.generate() + + +if __name__ == '__main__': + unittest.main() From be29f48c22dc6c555ac1d4dfc0a05cc89a7e285c Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Tue, 15 Sep 2026 11:16:19 +1000 Subject: [PATCH 10/10] Use compiler-bundled Rust library notices --- .github/workflows/release.yml | 1 - docs/operations/release/process.md | 2 +- scripts/generate-router-notices.py | 2 +- scripts/test-router-notices.py | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 48fd394..0aac887 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -394,7 +394,6 @@ jobs: SOURCE_DATE_EPOCH=$(git show -s --format=%ct HEAD) export SOURCE_DATE_EPOCH cargo install cargo-cyclonedx --version 0.5.9 --locked - rustup component add rust-docs cargo build -p graphql-orm-router --release --features auth-agql --locked cargo cyclonedx \ --manifest-path crates/graphql-orm-router/Cargo.toml \ diff --git a/docs/operations/release/process.md b/docs/operations/release/process.md index 2f6a481..2f0c704 100644 --- a/docs/operations/release/process.md +++ b/docs/operations/release/process.md @@ -172,7 +172,7 @@ standalone notice files were unavailable. The archives must match Cargo.lock checksums. The generated inventory preserves original license expressions, source URLs, file hashes, and source-only notice dispositions; it contains no builder-local source paths. The bundle also retains the matching Rust standard-library -copyright notices from the toolchain's `rust-docs` component. +copyright notices shipped with the matching Rust compiler installation. `config/router-notice-review.v1.json` binds these notice-source exceptions to the lockfile hash, SBOM component set, target, and features. A dependency/profile diff --git a/scripts/generate-router-notices.py b/scripts/generate-router-notices.py index cee4956..83ae8d2 100755 --- a/scripts/generate-router-notices.py +++ b/scripts/generate-router-notices.py @@ -153,7 +153,7 @@ def generate(root, metadata, sbom, review, output, cargo_home): def collect_rust_notices(sysroot, output, version): source = sysroot / 'share/doc/rust/COPYRIGHT-library.html' if not source.is_file(): - raise ValueError('Rust standard-library notices missing; install the matching rust-docs component') + raise ValueError('matching Rust compiler installation lacks standard-library notices') destination = output / 'toolchain/COPYRIGHT-library.html' destination.parent.mkdir(parents=True, exist_ok=True) data = source.read_bytes() diff --git a/scripts/test-router-notices.py b/scripts/test-router-notices.py index 1a5debb..09388c8 100755 --- a/scripts/test-router-notices.py +++ b/scripts/test-router-notices.py @@ -126,7 +126,7 @@ def test_rust_standard_library_notices_are_copied_from_matching_sysroot(self): self.assertEqual((self.output / result['notices'][0]['path']).read_bytes(), source.read_bytes()) self.assertEqual(result['compiler'], 'rustc fixture') source.unlink() - with self.assertRaisesRegex(ValueError, 'rust-docs'): + with self.assertRaisesRegex(ValueError, 'compiler installation'): notices.collect_rust_notices(sysroot, self.output, 'rustc fixture\n') def test_rejects_unsafe_names_and_unreviewed_profiles(self):