From e5e91c20f965f2a613f2474ba15c9298f0d8fc0f Mon Sep 17 00:00:00 2001 From: Wei Meng <20290571+m5i-work@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:08:11 +0800 Subject: [PATCH 1/2] Fix uv policy to keep sample metadata optional --- .../check-hosted-agent-python-requirements.py | 44 +------------------ ..._check_hosted_agent_python_requirements.py | 28 +++++------- .../python/hosted-agents/AGENTS-template.md | 11 ++--- samples/python/hosted-agents/AGENTS.md | 4 +- .../python/hosted-agents/DEPENDENCY_POLICY.md | 4 +- samples/python/hosted-agents/README.md | 2 +- 6 files changed, 24 insertions(+), 69 deletions(-) diff --git a/.github/scripts/check-hosted-agent-python-requirements.py b/.github/scripts/check-hosted-agent-python-requirements.py index c92f47383..dad2872d2 100644 --- a/.github/scripts/check-hosted-agent-python-requirements.py +++ b/.github/scripts/check-hosted-agent-python-requirements.py @@ -47,7 +47,6 @@ HASH_OPTION = re.compile(r"(?:^|\s)--hash(?:=|\s+)\S+") INLINE_COMMENT = re.compile(r"\s+#.*$") VCS_PREFIXES = ("git+", "hg+", "svn+", "bzr+") -PINNED_UV = re.compile(r"(? list[Finding]: - sample_yaml = service.manifest.parent / "sample.yaml" - if sample_yaml not in files: - return [ - Finding( - "PYREQ013", - "uv-native runtime requires sample.yaml build validation with a pinned uv version and uv sync --frozen", - service.path, - trigger, - sample_yaml, - ) - ] - try: - sample = yaml.safe_load(read_tree_file(repo, revision, sample_yaml)) or {} - except (yaml.YAMLError, UnicodeDecodeError) as exc: - return [ - Finding( - "PYREQ013", - f"cannot validate uv-native sample.yaml build contract: {exc}", - service.path, - trigger, - sample_yaml, - ) - ] - build = sample.get("build") if isinstance(sample, Mapping) else None - if ( - not isinstance(build, str) - or not PINNED_UV.search(build) - or not FROZEN_UV_SYNC.search(build) - ): - return [ - Finding( - "PYREQ013", - "uv-native sample.yaml build must install an exact uv version and run uv sync --frozen", - service.path, - trigger, - sample_yaml, - ) - ] - manifest = yaml.safe_load(read_tree_file(repo, revision, service.manifest)) or {} services = manifest.get("services", {}) if isinstance(manifest, Mapping) else {} declaration = ( @@ -965,7 +925,7 @@ def collect_findings( else: service = head_services.get(root) if service is not None: - contract_findings = validate_uv_runtime_contract( + contract_findings = validate_uv_deployment_contract( repo, head, head_files, service, trigger ) findings.extend(contract_findings) diff --git a/.github/scripts/test/test_check_hosted_agent_python_requirements.py b/.github/scripts/test/test_check_hosted_agent_python_requirements.py index 5021f14a9..a4f8cef15 100644 --- a/.github/scripts/test/test_check_hosted_agent_python_requirements.py +++ b/.github/scripts/test/test_check_hosted_agent_python_requirements.py @@ -103,11 +103,6 @@ def add_uv_service( " codeConfiguration:\n" " dependencyResolution: remote_build\n", ) - self.repo.write( - f"{sample}/sample.yaml", - "name: uv example\n" - 'build: python -m pip install "uv==0.11.7" && uv sync --frozen\n', - ) self.repo.write( f"{root}/pyproject.toml", '[project]\nname = "example"\nversion = "0.1.0"\n' @@ -159,9 +154,11 @@ def test_new_service_without_requirements_fails_and_points_at_runtime(self) -> N self.assertEqual(["PYREQ001"], [finding.code for finding in findings]) self.assertEqual(PurePosixPath(root), findings[0].root) - def test_new_service_with_uv_project_and_lock_passes(self) -> None: - self.add_uv_service() + def test_new_service_with_uv_project_and_lock_needs_no_sample_yaml(self) -> None: + root = self.add_uv_service() + sample = Path(root).parents[1].as_posix() self.repo.commit("add uv sample") + self.assertFalse((self.repo.path / sample / "sample.yaml").exists()) self.assertEqual([], self.findings()) def test_incomplete_uv_pair_fails(self) -> None: @@ -223,13 +220,6 @@ def test_uv_config_change_requires_updated_lock(self) -> None: self.repo.commit("change uv configuration") self.assertEqual(["PYREQ006"], [finding.code for finding in self.findings()]) - def test_uv_project_requires_pinned_frozen_validation(self) -> None: - root = self.add_uv_service() - sample = Path(root).parents[1].as_posix() - self.repo.write(f"{sample}/sample.yaml", "name: uv example\nbuild: uv sync\n") - self.repo.commit("add invalid uv sample") - self.assertEqual(["PYREQ013"], [finding.code for finding in self.findings()]) - def test_uv_project_requires_lock_aware_deployment(self) -> None: root = self.add_uv_service() sample = Path(root).parents[1].as_posix() @@ -257,15 +247,19 @@ def test_uv_project_accepts_lock_aware_docker_deployment(self) -> None: self.repo.commit("add containerized uv sample") self.assertEqual([], self.findings()) - def test_adopting_uv_requires_the_validation_and_deployment_contract(self) -> None: + def test_adopting_uv_requires_a_lock_aware_deployment(self) -> None: root = self.add_uv_service() sample = Path(root).parents[1].as_posix() self.repo.write(f"{root}/requirements.txt", "six==1.16.0\n") - self.repo.write(f"{sample}/sample.yaml", "name: uv example\nbuild: uv sync\n") + self.repo.write( + f"{sample}/azure.yaml", + "name: example\nservices:\n example:\n language: python\n" + " project: src/example\n", + ) self.repo.commit("add pip sample with supplemental uv files") self.base = self.repo.run("rev-parse", "HEAD") self.repo.remove(f"{root}/requirements.txt") - self.repo.commit("adopt uv without complete runtime contract") + self.repo.commit("adopt uv without lock-aware deployment") self.assertEqual(["PYREQ013"], [finding.code for finding in self.findings()]) def test_uv_resolution_is_wired_through_repository_check(self) -> None: diff --git a/samples/python/hosted-agents/AGENTS-template.md b/samples/python/hosted-agents/AGENTS-template.md index 1fcd9c5c5..d5341bfde 100644 --- a/samples/python/hosted-agents/AGENTS-template.md +++ b/samples/python/hosted-agents/AGENTS-template.md @@ -47,11 +47,12 @@ Follow the shared Python Hosted Agent dependency policy at Use `pyproject.toml` and `uv.lock` by default. A fully resolved `requirements.txt` remains accepted as a backward-compatible fallback. The selected artifact must pin the complete direct and transitive runtime graph; if -both forms exist, the uv pair is authoritative. A uv-native sample must use a -pinned uv version and `uv sync --frozen` in its validation and deployment paths. -Update the selected artifact whenever a dependency input changes. Run the policy -checker, including its `--resolve` closure check when network access is available, -before completing dependency changes. +both forms exist, the uv pair is authoritative. A uv-native sample's deployment must consume the lock through remote dependency +resolution or a Dockerfile with pinned uv and `uv sync --frozen`. `sample.yaml` +remains optional metadata for separate build-readiness validation. Update the +selected artifact whenever a dependency input changes. Run the policy checker, +including its `--resolve` closure check when network access is available, before +completing dependency changes. ## Runtime logging diff --git a/samples/python/hosted-agents/AGENTS.md b/samples/python/hosted-agents/AGENTS.md index 6b0157c39..833193165 100644 --- a/samples/python/hosted-agents/AGENTS.md +++ b/samples/python/hosted-agents/AGENTS.md @@ -43,10 +43,10 @@ Python Hosted Agent runtime or change its dependencies. - Before finishing a dependency change, run the checker documented in `DEPENDENCY_POLICY.md`, including `--resolve` when network access is available. -A uv-native sample may omit `requirements.txt` when `sample.yaml` installs a -pinned uv version and runs `uv sync --frozen`. Its deployment must consume the +A uv-native sample may omit `requirements.txt`. Its deployment must consume the lock through `dependencyResolution: remote_build`, or through a Dockerfile that uses a versioned `ghcr.io/astral-sh/uv` image and runs `uv sync --frozen`. +`sample.yaml` remains optional metadata for the separate build-readiness system. ## Cloud E2E contract for new samples diff --git a/samples/python/hosted-agents/DEPENDENCY_POLICY.md b/samples/python/hosted-agents/DEPENDENCY_POLICY.md index ce645e505..b3317223e 100644 --- a/samples/python/hosted-agents/DEPENDENCY_POLICY.md +++ b/samples/python/hosted-agents/DEPENDENCY_POLICY.md @@ -29,7 +29,7 @@ sample/ └── uv.lock ``` -CI validates that the lock matches the project and exports a complete runtime graph. The sample's `sample.yaml` build command must install a pinned uv version and use `uv sync --frozen`. Deployment must use Hosted Agent remote dependency resolution or a Dockerfile that installs a pinned uv version and uses `uv sync --frozen`. Consumers use uv directly; a duplicate `requirements.txt` export is not required. +CI uses a pinned uv version to verify that the lock matches the project and to export a complete runtime graph without updating the lock. Deployment must consume the lock through Hosted Agent remote dependency resolution or a Dockerfile that installs a pinned uv version and uses `uv sync --frozen`. Consumers use uv directly; a duplicate `requirements.txt` export and `sample.yaml` validation manifest are not required by this dependency policy. ### Backward-compatible fallback: requirements.txt @@ -210,4 +210,4 @@ Common failures: | `PYREQ010` | A requirement or uv TOML document is syntactically invalid. | | `PYREQ011` | A direct URL dependency is present without an approved exception. | | `PYREQ012` | Pip or uv could not validate the committed artifact. | -| `PYREQ013` | A uv-native runtime does not declare pinned, frozen uv validation and a lock-aware deployment path. | +| `PYREQ013` | A uv-native runtime does not declare a lock-aware remote-build or container deployment path. | diff --git a/samples/python/hosted-agents/README.md b/samples/python/hosted-agents/README.md index aa6dd588f..d7bd0faa5 100644 --- a/samples/python/hosted-agents/README.md +++ b/samples/python/hosted-agents/README.md @@ -205,7 +205,7 @@ Already built an agent with CrewAI or your own code? The protocol SDKs (`azure-a ## Python dependencies -New Python Hosted Agent runtimes use `pyproject.toml` with `uv.lock` by default. A fully resolved `requirements.txt` remains accepted as a backward-compatible fallback. The selected artifact must capture the complete dependency graph; uv-native validation and deployment use a pinned uv version with `uv sync --frozen`. +New Python Hosted Agent runtimes use `pyproject.toml` with `uv.lock` by default. A fully resolved `requirements.txt` remains accepted as a backward-compatible fallback. The selected artifact must capture the complete dependency graph; CI validates uv locks with a pinned uv version, and deployment must consume the lock through remote dependency resolution or a frozen uv container build. See the [Python Hosted Agent dependency policy](DEPENDENCY_POLICY.md) for pinning rules, authoring-tool examples, validation commands, ratchet behavior, and exceptions. From 7c1cf042feb4517d3f27ae50ae0a1fed4d7ee0c2 Mon Sep 17 00:00:00 2001 From: Wei Meng <20290571+m5i-work@users.noreply.github.com> Date: Mon, 21 Sep 2026 15:37:39 +0800 Subject: [PATCH 2/2] Keep dependency policy focused on lock reproducibility --- .../check-hosted-agent-python-requirements.py | 55 +------------------ ..._check_hosted_agent_python_requirements.py | 55 +------------------ .../python/hosted-agents/AGENTS-template.md | 10 ++-- samples/python/hosted-agents/AGENTS.md | 7 +-- .../python/hosted-agents/DEPENDENCY_POLICY.md | 3 +- samples/python/hosted-agents/README.md | 2 +- 6 files changed, 13 insertions(+), 119 deletions(-) diff --git a/.github/scripts/check-hosted-agent-python-requirements.py b/.github/scripts/check-hosted-agent-python-requirements.py index dad2872d2..c0f16efb6 100644 --- a/.github/scripts/check-hosted-agent-python-requirements.py +++ b/.github/scripts/check-hosted-agent-python-requirements.py @@ -47,8 +47,7 @@ HASH_OPTION = re.compile(r"(?:^|\s)--hash(?:=|\s+)\S+") INLINE_COMMENT = re.compile(r"\s+#.*$") VCS_PREFIXES = ("git+", "hg+", "svn+", "bzr+") -FROZEN_UV_SYNC = re.compile(r"\buv\s+sync\b[^;&\n]*\s--frozen(?:\s|$)") -VALID_CODES = {f"PYREQ{number:03d}" for number in range(1, 14)} +VALID_CODES = {f"PYREQ{number:03d}" for number in range(1, 13)} @dataclasses.dataclass(frozen=True) @@ -698,48 +697,6 @@ def validate_uv_resolution( return [] -def validate_uv_deployment_contract( - repo: Path, - revision: str, - files: set[PurePosixPath], - service: ServiceRoot, - trigger: PurePosixPath, -) -> list[Finding]: - manifest = yaml.safe_load(read_tree_file(repo, revision, service.manifest)) or {} - services = manifest.get("services", {}) if isinstance(manifest, Mapping) else {} - declaration = ( - services.get(service.service, {}) if isinstance(services, Mapping) else {} - ) - code_configuration = ( - declaration.get("codeConfiguration", {}) - if isinstance(declaration, Mapping) - else {} - ) - remote_build = ( - isinstance(code_configuration, Mapping) - and code_configuration.get("dependencyResolution") == "remote_build" - ) - dockerfile = service.path / "Dockerfile" - docker_build = False - if dockerfile in files: - content = read_tree_file(repo, revision, dockerfile).decode("utf-8") - docker_build = bool( - re.search(r"ghcr\.io/astral-sh/uv:\d+\.\d+\.\d+", content) - and FROZEN_UV_SYNC.search(content) - ) - if not remote_build and not docker_build: - return [ - Finding( - "PYREQ013", - "uv-native deployment must use remote dependency resolution or a Dockerfile with pinned uv and uv sync --frozen", - service.path, - trigger, - service.manifest, - ) - ] - return [] - - def load_exceptions(path: Path) -> list[ExceptionRule]: if not path.exists(): return [] @@ -923,21 +880,13 @@ def collect_findings( ) ) else: - service = head_services.get(root) - if service is not None: - contract_findings = validate_uv_deployment_contract( - repo, head, head_files, service, trigger - ) - findings.extend(contract_findings) - else: - contract_findings = [] lock_content = read_tree_file(repo, head, uv_source).decode("utf-8") project_content = read_tree_file(repo, head, uv_manifest).decode("utf-8") uv_findings = parse_uv_lock( lock_content, project_content, root, trigger, uv_source ) findings.extend(uv_findings) - if resolve and not contract_findings and not uv_findings: + if resolve and not uv_findings: findings.extend( validate_uv_resolution(repo, head, head_files, root, trigger, uv) ) diff --git a/.github/scripts/test/test_check_hosted_agent_python_requirements.py b/.github/scripts/test/test_check_hosted_agent_python_requirements.py index a4f8cef15..35e22b006 100644 --- a/.github/scripts/test/test_check_hosted_agent_python_requirements.py +++ b/.github/scripts/test/test_check_hosted_agent_python_requirements.py @@ -96,13 +96,6 @@ def add_uv_service( service=service, requirements=None, ) - self.repo.write( - f"{sample}/azure.yaml", - "name: example\nservices:\n example:\n language: python\n" - f" project: {service}\n" - " codeConfiguration:\n" - " dependencyResolution: remote_build\n", - ) self.repo.write( f"{root}/pyproject.toml", '[project]\nname = "example"\nversion = "0.1.0"\n' @@ -154,11 +147,9 @@ def test_new_service_without_requirements_fails_and_points_at_runtime(self) -> N self.assertEqual(["PYREQ001"], [finding.code for finding in findings]) self.assertEqual(PurePosixPath(root), findings[0].root) - def test_new_service_with_uv_project_and_lock_needs_no_sample_yaml(self) -> None: - root = self.add_uv_service() - sample = Path(root).parents[1].as_posix() + def test_new_service_with_uv_project_and_lock_passes(self) -> None: + self.add_uv_service() self.repo.commit("add uv sample") - self.assertFalse((self.repo.path / sample / "sample.yaml").exists()) self.assertEqual([], self.findings()) def test_incomplete_uv_pair_fails(self) -> None: @@ -220,48 +211,6 @@ def test_uv_config_change_requires_updated_lock(self) -> None: self.repo.commit("change uv configuration") self.assertEqual(["PYREQ006"], [finding.code for finding in self.findings()]) - def test_uv_project_requires_lock_aware_deployment(self) -> None: - root = self.add_uv_service() - sample = Path(root).parents[1].as_posix() - self.repo.write( - f"{sample}/azure.yaml", - "name: example\nservices:\n example:\n language: python\n" - " project: src/example\n", - ) - self.repo.commit("add uv sample without lock-aware deployment") - self.assertEqual(["PYREQ013"], [finding.code for finding in self.findings()]) - - def test_uv_project_accepts_lock_aware_docker_deployment(self) -> None: - root = self.add_uv_service() - sample = Path(root).parents[1].as_posix() - self.repo.write( - f"{sample}/azure.yaml", - "name: example\nservices:\n example:\n language: python\n" - " project: src/example\n", - ) - self.repo.write( - f"{root}/Dockerfile", - "FROM ghcr.io/astral-sh/uv:0.11.7 AS uv\n" - "RUN uv sync --frozen --no-dev\n", - ) - self.repo.commit("add containerized uv sample") - self.assertEqual([], self.findings()) - - def test_adopting_uv_requires_a_lock_aware_deployment(self) -> None: - root = self.add_uv_service() - sample = Path(root).parents[1].as_posix() - self.repo.write(f"{root}/requirements.txt", "six==1.16.0\n") - self.repo.write( - f"{sample}/azure.yaml", - "name: example\nservices:\n example:\n language: python\n" - " project: src/example\n", - ) - self.repo.commit("add pip sample with supplemental uv files") - self.base = self.repo.run("rev-parse", "HEAD") - self.repo.remove(f"{root}/requirements.txt") - self.repo.commit("adopt uv without lock-aware deployment") - self.assertEqual(["PYREQ013"], [finding.code for finding in self.findings()]) - def test_uv_resolution_is_wired_through_repository_check(self) -> None: root = self.add_uv_service() self.repo.commit("add uv sample") diff --git a/samples/python/hosted-agents/AGENTS-template.md b/samples/python/hosted-agents/AGENTS-template.md index d5341bfde..611f9c196 100644 --- a/samples/python/hosted-agents/AGENTS-template.md +++ b/samples/python/hosted-agents/AGENTS-template.md @@ -47,12 +47,10 @@ Follow the shared Python Hosted Agent dependency policy at Use `pyproject.toml` and `uv.lock` by default. A fully resolved `requirements.txt` remains accepted as a backward-compatible fallback. The selected artifact must pin the complete direct and transitive runtime graph; if -both forms exist, the uv pair is authoritative. A uv-native sample's deployment must consume the lock through remote dependency -resolution or a Dockerfile with pinned uv and `uv sync --frozen`. `sample.yaml` -remains optional metadata for separate build-readiness validation. Update the -selected artifact whenever a dependency input changes. Run the policy checker, -including its `--resolve` closure check when network access is available, before -completing dependency changes. +both forms exist, the uv pair is authoritative. Update the selected artifact +whenever a dependency input changes. Run the policy checker, including its +`--resolve` closure check when network access is available, before completing +dependency changes. ## Runtime logging diff --git a/samples/python/hosted-agents/AGENTS.md b/samples/python/hosted-agents/AGENTS.md index 833193165..57920b2ee 100644 --- a/samples/python/hosted-agents/AGENTS.md +++ b/samples/python/hosted-agents/AGENTS.md @@ -43,10 +43,9 @@ Python Hosted Agent runtime or change its dependencies. - Before finishing a dependency change, run the checker documented in `DEPENDENCY_POLICY.md`, including `--resolve` when network access is available. -A uv-native sample may omit `requirements.txt`. Its deployment must consume the -lock through `dependencyResolution: remote_build`, or through a Dockerfile that -uses a versioned `ghcr.io/astral-sh/uv` image and runs `uv sync --frozen`. -`sample.yaml` remains optional metadata for the separate build-readiness system. +A uv-native sample may omit `requirements.txt`. Keep `pyproject.toml` and +`uv.lock` synchronized, and run the frozen resolution check before submitting +dependency changes. ## Cloud E2E contract for new samples diff --git a/samples/python/hosted-agents/DEPENDENCY_POLICY.md b/samples/python/hosted-agents/DEPENDENCY_POLICY.md index b3317223e..3bf2cd503 100644 --- a/samples/python/hosted-agents/DEPENDENCY_POLICY.md +++ b/samples/python/hosted-agents/DEPENDENCY_POLICY.md @@ -29,7 +29,7 @@ sample/ └── uv.lock ``` -CI uses a pinned uv version to verify that the lock matches the project and to export a complete runtime graph without updating the lock. Deployment must consume the lock through Hosted Agent remote dependency resolution or a Dockerfile that installs a pinned uv version and uses `uv sync --frozen`. Consumers use uv directly; a duplicate `requirements.txt` export and `sample.yaml` validation manifest are not required by this dependency policy. +CI uses a pinned uv version to verify that the lock matches the project and to export a complete runtime graph without updating the lock. Consumers use uv directly; a duplicate `requirements.txt` export is not required. ### Backward-compatible fallback: requirements.txt @@ -210,4 +210,3 @@ Common failures: | `PYREQ010` | A requirement or uv TOML document is syntactically invalid. | | `PYREQ011` | A direct URL dependency is present without an approved exception. | | `PYREQ012` | Pip or uv could not validate the committed artifact. | -| `PYREQ013` | A uv-native runtime does not declare a lock-aware remote-build or container deployment path. | diff --git a/samples/python/hosted-agents/README.md b/samples/python/hosted-agents/README.md index d7bd0faa5..14d83b04b 100644 --- a/samples/python/hosted-agents/README.md +++ b/samples/python/hosted-agents/README.md @@ -205,7 +205,7 @@ Already built an agent with CrewAI or your own code? The protocol SDKs (`azure-a ## Python dependencies -New Python Hosted Agent runtimes use `pyproject.toml` with `uv.lock` by default. A fully resolved `requirements.txt` remains accepted as a backward-compatible fallback. The selected artifact must capture the complete dependency graph; CI validates uv locks with a pinned uv version, and deployment must consume the lock through remote dependency resolution or a frozen uv container build. +New Python Hosted Agent runtimes use `pyproject.toml` with `uv.lock` by default. A fully resolved `requirements.txt` remains accepted as a backward-compatible fallback. The selected artifact must capture the complete dependency graph; CI validates uv locks with a pinned uv version and a frozen production export. See the [Python Hosted Agent dependency policy](DEPENDENCY_POLICY.md) for pinning rules, authoring-tool examples, validation commands, ratchet behavior, and exceptions.