From 4b6da9842bdac710b2cca176597c565fd9c53a1c Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Mon, 19 Oct 2026 16:10:19 +0800 Subject: [PATCH] Read public fleet workflow metadata without private credentials Fleet setup checks were sending the private monitoring token to public central workflow repositories. Organization token-lifetime policies could then reject the scan even when the same public data was available anonymously. Use a separate cached unauthenticated client for the pinned central contract, toolchain and reusable workflows. Keep authenticated reads for inventory, monitored project files and Actions state. Anonymous failures remain unverified without retrying the private credential. Document the read boundary and current public inventory. Regression coverage reproduces token-policy rejection, retained private read authorization, anonymous failure and snapshot reuse. All 1,097 public tests, lint, formatting and runtime types pass. --- docs/node-fleet-setup.md | 13 +++++--- node/runtime/fleet.py | 4 ++- node/tests/test_fleet_setup.py | 55 ++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/docs/node-fleet-setup.md b/docs/node-fleet-setup.md index 3843d08..7aa9818 100644 --- a/docs/node-fleet-setup.md +++ b/docs/node-fleet-setup.md @@ -46,9 +46,9 @@ Use a branch ref for live defaults or a full commit SHA for fixed policy. The inventory ref used to fetch this list is independent of the defaults ref inside it. See [shared defaults](shared-defaults.md) for inheritance rules. -This repository's [inventory](../defaults/node-repositories.json) lists ten public -repositories: Hydra, OmegaConf, and eight personal Node projects. Repositories -that are not installed yet remain listed so their missing setup stays visible. +This repository's [inventory](../defaults/node-repositories.json) lists Hydra and +OmegaConf. Repositories that are not installed yet can also remain in an +inventory so their missing setup stays visible. Other maintainers can keep their own inventories in their own repositories; no registration here is required. @@ -107,7 +107,12 @@ secrets: The optional token goes only to the scanner. The reporter uses the host's built-in token. Shared defaults sources remain public, as required by the -maintenance resolver. No private key or secret content is inspected. +maintenance resolver. Pinned central workflow repositories must also be public: +the scanner reads their metadata and files without authentication. The optional +token remains in use for the inventory, listed repositories, and their Actions +status. This avoids applying the central organization's token policies to a +credential intended for your private repositories. No private key or secret +content is inspected. ## 3. Run and inspect results diff --git a/node/runtime/fleet.py b/node/runtime/fleet.py index e51b500..b0f55b7 100644 --- a/node/runtime/fleet.py +++ b/node/runtime/fleet.py @@ -66,6 +66,8 @@ def __init__(self, token=""): self.defaults_lock = Lock() # Defaults remain public and unauthenticated, as in the individual audit. self.cached_defaults = functools.cache(CONFIGURATION["public_read"]) + # Public workflow metadata must not inherit private repository credentials. + self.public_github = GitHub() if token else self def read_defaults(self, url, accept="application/json"): # Resolve each shared ref once, even when several repositories need it. @@ -442,7 +444,7 @@ def check_caller(root, config, selected, github): raise ValueError( "Caller must serialize runs with concurrency and cancel-in-progress: false" ) - source = github.snapshot(match[1], match[2]) + source = github.public_github.snapshot(match[1], match[2]) contract = CONFIGURATION["json_object"]( source.read("node/contract.json"), "pinned Node contract" ) diff --git a/node/tests/test_fleet_setup.py b/node/tests/test_fleet_setup.py index 25207f1..edacdd7 100644 --- a/node/tests/test_fleet_setup.py +++ b/node/tests/test_fleet_setup.py @@ -3,9 +3,12 @@ import base64 import copy import hashlib +import io import json import runpy import subprocess +import urllib.error +import urllib.request from concurrent.futures import ThreadPoolExecutor from pathlib import Path from threading import Barrier, Event, Lock @@ -884,6 +887,58 @@ def opener(handler): assert observed[0].get_header("Authorization") == "Bearer test-token" +@pytest.mark.parametrize("central_error", [None, 403]) +def test_public_workflow_reads_do_not_inherit_private_monitoring_token( + monkeypatch, central_error +): + server = GitHub("npm") + observed = [] + + class Opener: + def open(self, request, timeout): + path = urlparse(request.full_url).path.lstrip("/") + if urlparse(request.full_url).query: + path += "?" + urlparse(request.full_url).query + authorization = request.get_header("Authorization") + observed.append((path, authorization)) + if path.startswith("repos/owner/actions"): + # Reproduce an organization rejecting an otherwise valid PAT. + if authorization or central_error: + raise urllib.error.HTTPError( + request.full_url, 403, "organization policy", {}, None + ) + else: + # Inventory, project files and Actions state still need the PAT. + assert authorization == "Bearer test-token" + return io.BytesIO(json.dumps(server.get(path)).encode()) + + monkeypatch.setattr(urllib.request, "build_opener", lambda handler: Opener()) + github = FLEET["GitHub"]("test-token") + github.read_defaults = server.public + source = { + "repository": "owner/settings", + "path": "defaults/repos.json", + "ref": "main", + } + report = FLEET["check_fleet"](github, source) + result = report["results"][0] + assert result["status"] == ("unverified" if central_error else "ok") + central_reads = [ + item for item in observed if item[0].startswith("repos/owner/actions") + ] + assert central_reads and all(auth is None for _, auth in central_reads) + if central_error: + assert "HTTP 403" in result["diagnostic"] + assert len(central_reads) == 1 # No retry with the private credential. + else: + assert any("git/blobs/" in path for path, _ in central_reads) + assert any("actions/workflows/" in path for path, _ in observed) + # Repeated installations share the unauthenticated pinned snapshot. + before = len(observed) + assert FLEET["check_fleet"](github, source)["status"] == "ok" + assert len(observed) == before + + def test_pinned_workflow_must_support_the_resolved_toolchain(): github = GitHub() support = json.loads(github.repos["owner/actions"]["node/runtime/toolchain.json"])