From 8d8d599a4d1fbae93874553f81f3b50443c580b4 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Fri, 30 Oct 2026 10:27:13 +0800 Subject: [PATCH 1/3] fix(contract): catch unlabeled provider tokens in the public boundary scan `loopx check --scan-path` reported ok for a file containing an unlabeled GitHub PAT, Slack, npm, PyPI, Stripe, Google or AWS token. Only values with a `token=`/`password=`/`Authorization:` label were caught, because the credential rule kept its own stale alternation instead of the canonical provider shapes in `loopx/public_safe_text.py`. That scan gates repository publication (also via `canary premerge`), so a rule that recognizes only some issuers certifies the rest as public-safe (Refs #5136, #7635). Reuse `PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE` from the canonical owner and extend the rule prefilter. Fixtures and the example smoke that deliberately contain provider-shaped literals now split them, the convention the source fixtures already follow, so no test stops exercising the scan. Signed-off-by: JasonBuildAI --- ...oal-artifact-lifecycle-projection-smoke.py | 10 +++-- loopx/contract.py | 25 +++++++++++ ...iability_diagnostics_public_safety_v0.json | 8 ++-- tests/test_chat_activity.py | 9 +++- ...t_contract_credential_pattern_precision.py | 44 +++++++++++++++++++ 5 files changed, 87 insertions(+), 9 deletions(-) diff --git a/examples/control_plane/goal-artifact-lifecycle-projection-smoke.py b/examples/control_plane/goal-artifact-lifecycle-projection-smoke.py index b4cd405408..71dfd648c6 100644 --- a/examples/control_plane/goal-artifact-lifecycle-projection-smoke.py +++ b/examples/control_plane/goal-artifact-lifecycle-projection-smoke.py @@ -31,6 +31,10 @@ GOAL_ID = "artifact-lifecycle-fixture" +# Split so the literal provider shape is not itself a credential hit for the +# public boundary scan, the same convention the source fixtures follow. +PROVIDER_TOKEN = "ghp_" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ012345" + # Substrings that must never appear in a public projection. FORBIDDEN_LEAKS = ( "/Users/", @@ -290,7 +294,7 @@ def assert_private_values_are_redacted_or_dropped() -> None: "delivery_batch_scale": "multi_surface", "evidence_ref": "/Users/private-owner/.ssh/id_rsa", "recommended_action": ( - "publish ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ012345 and " + f"publish {PROVIDER_TOKEN} and " "see /private/var/folders/secret/notes.md" ), } @@ -301,14 +305,14 @@ def assert_private_values_are_redacted_or_dropped() -> None: for leaked in ( "/Users/private-owner", "/private/var/folders", - "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ012345", + PROVIDER_TOKEN, ): assert leaked not in rendered, (leaked, rendered) # Unsafe references are withheld by the same validator as other status # projections; no local path is retained even as a truncated fragment. assert _compact_text("/Users/private-owner/notes.md") is None - assert _compact_text("token ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ012345") is None + assert _compact_text(f"token {PROVIDER_TOKEN}") is None assert _compact_text("/private/var/folders/x/secret.md") is None diff --git a/loopx/contract.py b/loopx/contract.py index f3f01f1fd2..f1e7134ca6 100644 --- a/loopx/contract.py +++ b/loopx/contract.py @@ -42,6 +42,7 @@ collect_history, ) from .paths import DEFAULT_RUNTIME_ROOT, LEGACY_RUNTIME_ROOT, rel_or_abs, resolve_runtime_root +from .public_safe_text import PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE from .registry import inspect_registry, inspect_registry_boundary, registry_goals, resolve_state_file from .state_projection import state_projection_gap_warning from .control_plane.todos.contract import ( @@ -111,6 +112,13 @@ def _prefilter_fold(text: str) -> str: r"(? str: "tok" + "en=", "pass" + "word=", "author" + "ization:", + "ghp_", + "gho_", + "ghu_", + "ghs_", + "ghr_", + "github_pat_", + "akia", + "asia", + "xox", + "aiza", + "sk_live_", + "sk_test_", + "rk_live_", + "rk_test_", + "npm_", + "pypi-", + "eyj", ), ), # The private roots take the host's own separator, so a Windows host diff --git a/tests/fixtures/control_plane/reliability_diagnostics_public_safety_v0.json b/tests/fixtures/control_plane/reliability_diagnostics_public_safety_v0.json index e37be4f885..3702d79305 100644 --- a/tests/fixtures/control_plane/reliability_diagnostics_public_safety_v0.json +++ b/tests/fixtures/control_plane/reliability_diagnostics_public_safety_v0.json @@ -2,8 +2,8 @@ "schema_version": "reliability_diagnostics_public_safety_counterfactuals_v0", "unsafe_summary_tokens": [ "sk-abcdefghijklmnop0123", - "ghp_abcdefghijklmnopqrstuvwx", - "eyJabcdefghijk.abcdefghijk.abcdefghijk", + "ghp_\u0061bcdefghijklmnopqrstuvwx", + "eyJ\u0061bcdefghijk.abcdefghijk.abcdefghijk", "C:/Users/alice/private.txt", "path:/Users/alice/private.txt", "file:///Users/alice/private.txt", @@ -11,8 +11,8 @@ ], "unsafe_identity_tokens": [ "sk-abcdefghijklmnop0123", - "ghp_abcdefghijklmnopqrstuvwx", - "eyJabcdefghijk.abcdefghijk.abcdefghijk" + "ghp_\u0061bcdefghijklmnopqrstuvwx", + "eyJ\u0061bcdefghijk.abcdefghijk.abcdefghijk" ], "invalid_source_ref_tokens": [ "C:/Users/alice/private.txt" diff --git a/tests/test_chat_activity.py b/tests/test_chat_activity.py index e93e069059..01ce39bd5f 100644 --- a/tests/test_chat_activity.py +++ b/tests/test_chat_activity.py @@ -30,6 +30,11 @@ def _command(command: str, actions: list[dict] | None = None, **extra) -> dict: "commandActions": actions or [{"type": "unknown", "command": command}], **extra} +# Split so the literal provider shape is not itself a credential hit for the +# public boundary scan, the same convention the source fixtures follow. +GITHUB_TOKEN = "ghp_" + "abcdefghijklmnop123" + + def test_command_step_names_the_intent_and_keeps_the_full_command_behind_details(): steps = CodexActivitySteps(protected_paths=["/work/project"]) item = _command( @@ -67,9 +72,9 @@ def test_failed_items_are_failed_steps_not_completions(): def test_credentials_in_commands_are_masked_before_replay(): title = CodexActivitySteps().started(_command( "curl -H 'Authorization: Bearer abc.def' --api-key=k123 https://x.test?q=1 " - "&& GH_TOKEN=ghp_abcdefghijklmnop123 gh pr list --password hunter2 sk-proj-abcdefghijklmnop" + f"&& GH_TOKEN={GITHUB_TOKEN} gh pr list --password hunter2 sk-proj-abcdefghijklmnop" ))["title"] - for secret in ("abc.def", "k123", "ghp_abcdefghijklmnop123", "hunter2", "sk-proj-abcdefghijklmnop"): + for secret in ("abc.def", "k123", GITHUB_TOKEN, "hunter2", "sk-proj-abcdefghijklmnop"): assert secret not in title assert "gh pr list" in title and "https://x.test" in title diff --git a/tests/test_contract_credential_pattern_precision.py b/tests/test_contract_credential_pattern_precision.py index 6315a775e3..982b04eb99 100644 --- a/tests/test_contract_credential_pattern_precision.py +++ b/tests/test_contract_credential_pattern_precision.py @@ -128,3 +128,47 @@ def test_private_lark_tenant_urls_remain_blocked(tmp_path: Path, line: str) -> N assert len(payload["hits"]) == 1 assert payload["hits"][0].endswith(": private_doc_url") + + +# A provider token carries its issuer in the prefix, so no label says what it +# is. The shapes come from the canonical public-safety owner instead of a +# second list here: this rule gates repository publication, and a list that +# recognized only some issuers certified the rest as public-safe. +PROVIDER_TOKEN_SAMPLES = [ + "ghp_" + "16C7e42F292c6912E7710c838347Ae178B4a", + "github_pat_" + "11ABCDEFG0123456789_abcdefghijklmnopqrstuvwxyz0123456789", + "xoxb-" + "123456789012-abcdefghijkl", + "sk_live_" + "abcdefghijklmnopqrst", + "npm_" + "abcdefghijklmnopqrstuvwxyz", + "pypi-" + "abcdefghijklmnopqrst", + "AIza" + "SyD-1234567890abcdefghijklmnopqrstu", + "eyJ" + "hbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.dozjgNryP4J3jVmNHl0w5N_XgL0n3", +] + + +@pytest.mark.parametrize("token", PROVIDER_TOKEN_SAMPLES) +def test_unlabeled_provider_tokens_are_credential_errors( + tmp_path: Path, + token: str, +) -> None: + payload = _scan_line(tmp_path, f"token {token}") + + assert len(payload["hits"]) == 1 + assert payload["hits"][0].endswith(": credential") + assert payload["credential_reference_hits"] == [] + + +def test_credential_rule_reuses_the_canonical_provider_shape_owner() -> None: + from loopx.public_safe_text import PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE + + assert ( + PROVIDER_TOKEN_SHAPE_PATTERN_SOURCE + in contract.LEAK_RULES["credential"].pattern.pattern + ) + + +def test_a_short_provider_lookalike_stays_clean(tmp_path: Path) -> None: + payload = _scan_line(tmp_path, "token gh_preview, not a credential") + + assert payload["hits"] == [] + assert payload["credential_reference_hits"] == [] From 214befe53b271edb13a65b9f376d29d4a313ae75 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Fri, 30 Oct 2026 10:53:21 +0800 Subject: [PATCH 2/3] test(contract): pin the provider prefilter and document split literals The credential rule is skipped unless a required prefilter literal appears in the folded text, so an issuer family without one is silently invisible even though the canonical pattern matches it. Cover every provider arm the rule reuses, including ASIA and the rk_ Stripe key, so a future shape cannot be added to the pattern alone. Record the split-literal convention next to the documented scan command: a fixture that must show a redaction keeps the value split instead of putting a raw provider token into the repository or the PR diff. Signed-off-by: JasonBuildAI --- CONTRIBUTING.md | 6 ++++++ tests/test_contract_public_boundary_prefilter.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef7cf0ea2f..c1c14b31ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,6 +105,12 @@ loopx check \ --scan-path examples/ ``` +The credential rule also rejects unlabeled provider token shapes (GitHub, Slack, +AWS, Google, Stripe, npm, PyPI, JWT) anywhere in a file. A fixture or example +that must demonstrate a redaction keeps the value split, for example +`"ghp_" + ""` in Python or a `\u` escape in JSON, so the test still +exercises the scan without a raw token entering the repository or the PR diff. + ## Local Development Use the [developer guide](docs/development/README.md) as the stable entry point. diff --git a/tests/test_contract_public_boundary_prefilter.py b/tests/test_contract_public_boundary_prefilter.py index 5682f0510b..02c3e9ec4b 100644 --- a/tests/test_contract_public_boundary_prefilter.py +++ b/tests/test_contract_public_boundary_prefilter.py @@ -68,6 +68,22 @@ def test_explicit_scan_file_keeps_unsupported_suffix(tmp_path: Path) -> None: ("credential", "tok" + "en=literal"), ("credential", "pass" + "word=literal"), ("credential", "Author" + "ization: literal"), + # Provider tokens carry their issuer in the prefix instead of a label. + # Every shape the canonical owner matches needs a prefilter literal, or + # the rule is silently skipped for that whole issuer family. + ("credential", "ghp_" + "16C7e42F292c6912E7710c838347Ae178B4a"), + ("credential", "ghs_" + "16C7e42F292c6912E7710c838347Ae178B4a"), + ("credential", "github_pat_" + "11ABCDEFG0123456789_abcdefghijklmnopqrstuvwxyz0123456789"), + ("credential", "AS" + "IA1234567890ABCDEF"), + ("credential", "xoxb-" + "123456789012-abcdefghijkl"), + ("credential", "AIza" + "SyD-1234567890abcdefghijklmnopqrstu"), + ("credential", "sk_live_" + "abcdefghijklmnopqrst"), + ("credential", "sk_test_" + "abcdefghijklmnopqrst"), + ("credential", "rk_live_" + "abcdefghijklmnopqrst"), + ("credential", "rk_test_" + "abcdefghijklmnopqrst"), + ("credential", "npm_" + "abcdefghijklmnopqrstuvwxyz"), + ("credential", "pypi-" + "abcdefghijklmnopqrst"), + ("credential", "eyJ" + "hbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.dozjgNryP4J3jVmNHl0w5N_XgL0n3"), ("local_private_path", "/" + "Users/alice/Documents/example.md"), ("local_private_path", "/" + "Users/alice/code-reading/example.md"), ("local_private_path", "/ext" + "_data/example.md"), From e17c66ac36dbab273f46e600edebc051e61aaf45 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Sun, 1 Nov 2026 15:39:08 +0800 Subject: [PATCH 3/3] fix(contract): require explicit examples for provider references Signed-off-by: JasonBuildAI --- loopx/contract.py | 7 +- ...t_contract_credential_pattern_precision.py | 69 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/loopx/contract.py b/loopx/contract.py index f1e7134ca6..117841bbb2 100644 --- a/loopx/contract.py +++ b/loopx/contract.py @@ -229,6 +229,11 @@ def _prefilter_fold(text: str) -> str: r"<[^>]{1,64}>|\{\{|example|dummy|placeholder|changeme|redacted|your[-_]", re.I, ) +# A provider token has no keyword that separates its value from surrounding +# text, so only the exact access-key id published in the AWS documentation may +# be treated as an example. A placeholder word inside an opaque token body is +# part of the value, not an external reference (Refs #7713 review). +CREDENTIAL_DOCUMENTED_EXAMPLE_PATTERN = re.compile(r"AKIAIOSFODNN7EXAMPLE") CREDENTIAL_HIGH_ENTROPY_VALUE_PATTERN = re.compile(r"[A-Za-z0-9._-]{20,}") CREDENTIAL_VARIABLE_REFERENCE_PATTERN = re.compile( r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*\s*[,)\]}]" @@ -250,7 +255,7 @@ def _credential_match_is_reference(line: str, match: re.Match[str]) -> bool: last_keyword = keyword.group(0) value = value[keyword.end() :].lstrip(" \t\"'`") if last_keyword is None: - return bool(CREDENTIAL_PLACEHOLDER_PATTERN.search(match.group(0))) + return bool(CREDENTIAL_DOCUMENTED_EXAMPLE_PATTERN.fullmatch(match.group(0))) if not value: return True if CREDENTIAL_ENVIRONMENT_REFERENCE_PATTERN.match(value): diff --git a/tests/test_contract_credential_pattern_precision.py b/tests/test_contract_credential_pattern_precision.py index 982b04eb99..7e229a9645 100644 --- a/tests/test_contract_credential_pattern_precision.py +++ b/tests/test_contract_credential_pattern_precision.py @@ -1,5 +1,9 @@ from __future__ import annotations +import json +import os +import subprocess +import sys from pathlib import Path import pytest @@ -14,6 +18,7 @@ PASSWORD_ASSIGN = "pass" + "word=" REAL_BEARER_VALUE = "a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f60718293a4b5c6d7e8f90" PRIVATE_LARK_TENANT_HOST = "tenant.lark" + "office.com" +ROOT = Path(__file__).resolve().parents[1] def _scan_line(tmp_path: Path, line: str) -> dict[str, list[str]]: @@ -76,7 +81,7 @@ def test_literal_credential_hits_stay_errors(tmp_path: Path, line: str) -> None: def test_a_literal_on_a_line_with_references_still_fails(tmp_path: Path) -> None: - line = f'{PASSWORD_ASSIGN}$SERVICE_SECRET {TOKEN_ASSIGN}{REAL_BEARER_VALUE}' + line = f"{PASSWORD_ASSIGN}$SERVICE_SECRET {TOKEN_ASSIGN}{REAL_BEARER_VALUE}" payload = _scan_line(tmp_path, line) @@ -172,3 +177,65 @@ def test_a_short_provider_lookalike_stays_clean(tmp_path: Path) -> None: assert payload["hits"] == [] assert payload["credential_reference_hits"] == [] + + +PROVIDER_TOKENS_WITH_PLACEHOLDER_BODY = [ + "ghp_" + "dummy" + "1234567890abcdefghijklmnop", + "npm_" + "example" + "1234567890abcdefghij", +] + + +@pytest.mark.parametrize("token", PROVIDER_TOKENS_WITH_PLACEHOLDER_BODY) +def test_provider_placeholder_words_inside_the_body_stay_literal_errors( + tmp_path: Path, + token: str, +) -> None: + payload = _scan_line(tmp_path, f"token {token}") + + assert payload["hits"] == ["sample.md:1: credential"] + assert payload["credential_reference_hits"] == [] + + +def _run_check_cli(tmp_path: Path, line: str) -> tuple[int, dict[str, object]]: + outgoing = tmp_path / "outgoing.md" + outgoing.write_text(f"{line}\n", encoding="utf-8") + result = subprocess.run( + [ + sys.executable, + "-m", + "loopx.cli", + "--format", + "json", + "--registry", + str(tmp_path / "registry.json"), + "--runtime-root", + str(tmp_path / "runtime"), + "check", + "--scan-path", + str(outgoing), + ], + cwd=ROOT, + capture_output=True, + text=True, + encoding="utf-8", + env={**os.environ, "PYTHONIOENCODING": "utf-8"}, + check=False, + ) + assert result.stdout, result.stderr + return result.returncode, json.loads(result.stdout) + + +@pytest.mark.parametrize("token", PROVIDER_TOKENS_WITH_PLACEHOLDER_BODY) +def test_real_check_cli_rejects_provider_placeholder_words_inside_the_body( + tmp_path: Path, + token: str, +) -> None: + returncode, payload = _run_check_cli(tmp_path, f"token {token}") + + assert returncode == 1 + assert payload["ok"] is False + assert payload["public_boundary_scan"]["ok"] is False + assert any( + row["code"] == "public_boundary_violation" + for row in payload["error_diagnostics"] + )