From a51a67109a35c9e236d18fc2c95c8d1cae63357f Mon Sep 17 00:00:00 2001 From: klickd-agent Date: Tue, 2 Jun 2026 11:21:57 +0000 Subject: [PATCH] feat(supply-chain): add tool-backed audit-trail index + determinism record First real automation stage of the x.klickd supply-chain protocol. Adds a stdlib-only, offline generator that collects the 42 verifiable v4.1 candidate skill packs (+ manifest), enforces the loaded+sha256_matches_manifest gate, and writes two re-checkable artefacts: - .internal-skills/supply-chain/audit/audit_trail_index.json - .internal-skills/supply-chain/audit/determinism_record.json deterministic_run_id is derived only from inputs (timestamps quarantined in a non_deterministic_zone, excluded from every hash), so identical inputs yield an identical id across runs and hosts. A `check` subcommand verifies on-disk artefacts are in sync and exits non-zero on drift or on banned-claim/secret content. validation_results is left empty by design: the generator records but does not run the validation commands, so it asserts no outcomes it did not observe (anti-mirage). Only stages labelled `tool` are automated; everything else stays `planned` / `partial` / `manual` per the stage_automation map. Not a v4.1 GA release. No publish/deploy/merge/tag. Co-Authored-By: Claude Opus 4.7 --- .internal-skills/supply-chain/audit/README.md | 70 +++ .../supply-chain/audit/audit_trail_index.json | 52 ++ .../audit/determinism_record.json | 252 +++++++++ scripts/generate_supply_chain_audit.py | 522 ++++++++++++++++++ tests/test_supply_chain_audit.py | 158 ++++++ 5 files changed, 1054 insertions(+) create mode 100644 .internal-skills/supply-chain/audit/README.md create mode 100644 .internal-skills/supply-chain/audit/audit_trail_index.json create mode 100644 .internal-skills/supply-chain/audit/determinism_record.json create mode 100644 scripts/generate_supply_chain_audit.py create mode 100644 tests/test_supply_chain_audit.py diff --git a/.internal-skills/supply-chain/audit/README.md b/.internal-skills/supply-chain/audit/README.md new file mode 100644 index 0000000..20f6a5a --- /dev/null +++ b/.internal-skills/supply-chain/audit/README.md @@ -0,0 +1,70 @@ +# x.klickd supply-chain — audit-trail index + determinism record + +**Status:** NON-NORMATIVE. Not a v4.1 GA release artefact. No publish / deploy / +merge / tag / release performed by this stage. + +This directory holds the **first tool-backed automation stage** of the x.klickd +supply-chain protocol. It does **not** automate the full pipeline. It turns two +traceability elements from spec into artefacts that are actually generated, +hashed, and re-checkable by a script: + +| File | What it is | +|---|---| +| `audit_trail_index.json` | A consultable index of the verifiable artifacts the supply chain operates on, the declared validation commands, an append-style event list, and a per-stage automation map. | +| `determinism_record.json` | Input file hashes, output file hashes, and a `deterministic_run_id` derived **only** from inputs, so identical inputs yield an identical id across runs and hosts. | + +## Generate / re-check + +```bash +# Write (or refresh) both artefacts: +python scripts/generate_supply_chain_audit.py + +# Verify the on-disk artefacts are still in sync with current inputs (no write): +python scripts/generate_supply_chain_audit.py check +``` + +`generate` exits non-zero if a critical invariant fails (missing or changed +input, hash mismatch against the manifest, banned public-claim string, or an +obvious secret/PII pattern in the generated output). `check` exits non-zero on +any drift in the deterministic core. + +## Determinism + +- The inputs are the 42 NON-NORMATIVE x.klickd v4.1 candidate skill packs plus + their manifest under `examples/v4.1/x-klickd-skills/` (43 inputs total). +- An input is counted **only** when its bytes exist on disk **and** hash-match + the manifest — the same `artifact_loaded` + `sha256_matches_manifest` gate + enforced by `scripts/verify_xklickd_skill_packs.py`. A catalogue entry alone + is not a loaded skill. +- `deterministic_run_id` and `checked_artifacts_hash_summary` are computed over + `(relative_path, sha256)` pairs only. They do **not** depend on timestamps, + host, or run order. +- The only non-deterministic field, `generated_at`, is quarantined under + `non_deterministic_zone` and is **excluded** from every hash. + +## What is real vs. planned + +`stage_automation` in `audit_trail_index.json` labels each pipeline stage: + +- `tool` — backed by shipped, runnable automation (audit-trail index, + determinism record, reproducibility check, pack hash verification, candidate + mapping validation). +- `partial` — a tripwire, not a full implementation (the PII/secrets scan here + guards only this stage's own generated output). +- `planned` — spec-only; no automation yet (diff report, threat model, license + check, source-freshness check, private/public boundary check, context-graph + generation, candidate-skill generation). +- `manual` — human/agent premium pass. + +`validation_results` is intentionally **empty**: this generator records the +declared validation commands but does not run them, so it does not assert their +outcomes. Pre-filled "pass" values would be a mirage. The operator runs the +commands; the audit / CI captures the outcomes. + +## Relation to the supply-chain spec + +The full 18-stage build-process specification is documented separately in the +supply-chain RFC under `docs/rfcs/` (the docs-only spec PR; not merged here). +This stage is the narrow, executable slice of stage **15 (determinism / +reproducibility)** and the **audit-trail index** from that spec. Everything else +in the pipeline remains `planned` until separately implemented. diff --git a/.internal-skills/supply-chain/audit/audit_trail_index.json b/.internal-skills/supply-chain/audit/audit_trail_index.json new file mode 100644 index 0000000..304b870 --- /dev/null +++ b/.internal-skills/supply-chain/audit/audit_trail_index.json @@ -0,0 +1,52 @@ +{ + "build_or_audit_events": [ + { + "automation": "tool", + "event": "audit_trail_index_generated", + "inputs_hash_summary": "10fa77ec74ebfa2b7daa51a5787607b1dc9eb654608477f478c7850ab5a09b85", + "source_commit_sha": "b73858cb2d9c6915195361e9ed34ed1b02a39ea4", + "stage": "audit_trail_index" + } + ], + "checked_artifacts_count": 43, + "checked_artifacts_hash_summary": "10fa77ec74ebfa2b7daa51a5787607b1dc9eb654608477f478c7850ab5a09b85", + "deterministic_run_id": "sha256:10fa77ec74ebfa2b7daa51a5787607b1dc9eb654608477f478c7850ab5a09b85", + "kind": "x_klickd_supply_chain_audit_trail_index", + "non_deterministic_zone": { + "comment": "Fields here are excluded from deterministic_run_id and checked_artifacts_hash_summary.", + "generated_at": "2026-06-02T11:20:28Z" + }, + "non_normative": true, + "notes": [ + "NON-NORMATIVE. Not a v4.1 GA release artefact.", + "Only the stages marked 'tool' are backed by shipped automation; 'planned' stages are spec-only; 'partial' is a tripwire, not a full scanner; 'manual' is human/agent premium work.", + "An artifact is counted only when its bytes exist on disk and hash-match the manifest (loaded + sha256_matches_manifest).", + "validation_results is empty by design: this generator does not run the validation commands, so it does not assert their outcomes.", + "Timestamps are excluded from deterministic_run_id; see determinism_record.json non_deterministic_zone." + ], + "repo": "Davincc77/klickdskill", + "schema_version": "0.1.0", + "source_commit_sha": "b73858cb2d9c6915195361e9ed34ed1b02a39ea4", + "stage_automation": { + "audit_trail_index": "tool", + "candidate_mapping_validation": "tool", + "candidate_skill_generation": "planned", + "context_graph_generation": "planned", + "determinism_record": "tool", + "diff_report": "planned", + "license_check": "planned", + "pack_hash_verification": "tool", + "pii_secrets_scan": "partial", + "premium_pass": "manual", + "private_public_boundary_check": "planned", + "reproducibility_check": "tool", + "source_freshness_check": "planned", + "threat_model": "planned" + }, + "validation_commands": [ + "python scripts/verify_xklickd_skill_packs.py verify", + "python scripts/validate_v4_1_candidate_mapping.py", + "pytest tests/test_supply_chain_audit.py" + ], + "validation_results": [] +} diff --git a/.internal-skills/supply-chain/audit/determinism_record.json b/.internal-skills/supply-chain/audit/determinism_record.json new file mode 100644 index 0000000..52b3bb6 --- /dev/null +++ b/.internal-skills/supply-chain/audit/determinism_record.json @@ -0,0 +1,252 @@ +{ + "deterministic_run_id": "sha256:10fa77ec74ebfa2b7daa51a5787607b1dc9eb654608477f478c7850ab5a09b85", + "hash_algo": "sha256", + "input_files": [ + { + "bytes": 9308, + "relative_path": "examples/v4.1/x-klickd-skills/lite/artist.klickd", + "sha256": "56dbd966942475354e4f2daac48c3aeabfedbf3ff48f6248c443cdf8fe82b5c8" + }, + { + "bytes": 9353, + "relative_path": "examples/v4.1/x-klickd-skills/lite/consumer-rights.klickd", + "sha256": "16f77c9b7cb0801f198407f063c1b8395b98f6f729a14597ede8a15ae766ebbc" + }, + { + "bytes": 9325, + "relative_path": "examples/v4.1/x-klickd-skills/lite/game-literacy.klickd", + "sha256": "6e3158a2bde1024f54db53d149e525b820c4c167a71124416c8f4e8df38a3632" + }, + { + "bytes": 9719, + "relative_path": "examples/v4.1/x-klickd-skills/lite/media-planner.klickd", + "sha256": "a399ef56eb140d5adf272ffe3578448085f082154647811853e3d52e58b3a33e" + }, + { + "bytes": 10149, + "relative_path": "examples/v4.1/x-klickd-skills/lite/parent-gaming.klickd", + "sha256": "f2016892ac731f284d37d207a9464dcb93551b6934ff86f09d05fcd9d1a0ce52" + }, + { + "bytes": 9745, + "relative_path": "examples/v4.1/x-klickd-skills/lite/social-literacy.klickd", + "sha256": "5da46abae45a40adf562d1119bd4240c60d14b7c2b798e8e47aed4122834d9dc" + }, + { + "bytes": 9332, + "relative_path": "examples/v4.1/x-klickd-skills/lite/streaming-creator.klickd", + "sha256": "83a47a7370650c9870d28566344a11e52298a9e0a581003dc0ea8009558ac8ce" + }, + { + "bytes": 10312, + "relative_path": "examples/v4.1/x-klickd-skills/lite/work-assistant.klickd", + "sha256": "7918922d04e406c406f1d8a1a6aeaa3f82fb2a9c5697a547f001cfac85063bcb" + }, + { + "bytes": 15656, + "relative_path": "examples/v4.1/x-klickd-skills/manifest.json", + "sha256": "4f54e35ae1469dc0519f7ff97de0b69395f360e167220bc204525cfcb9c7c55f" + }, + { + "bytes": 11371, + "relative_path": "examples/v4.1/x-klickd-skills/pro/accounting-operator.klickd", + "sha256": "0464b9d8bd284e7c498a60064f8f923e148f2ac3e0e39fd7c0f479174c8ad243" + }, + { + "bytes": 11707, + "relative_path": "examples/v4.1/x-klickd-skills/pro/api-integrator.klickd", + "sha256": "875536c647ff1905f2e54a7997977b7d4087b7153b8b9a7c3270aef301af6dab" + }, + { + "bytes": 10615, + "relative_path": "examples/v4.1/x-klickd-skills/pro/contract-review.klickd", + "sha256": "4be316d9bbb758e9d98dcf81ac25ef1fc9a8e24d9cb6462055d66660f104ba41" + }, + { + "bytes": 12349, + "relative_path": "examples/v4.1/x-klickd-skills/pro/customer-support-operator.klickd", + "sha256": "9e6c755f8d975110bd8fd5bfdb1d87773f1a571968bf27933357b7bcc13516e9" + }, + { + "bytes": 12175, + "relative_path": "examples/v4.1/x-klickd-skills/pro/data-analyst.klickd", + "sha256": "8ae8fe5e3b55d4402e0da93738e97d23c41d3daeb93c6bdc41301486a2f6cd4f" + }, + { + "bytes": 12201, + "relative_path": "examples/v4.1/x-klickd-skills/pro/devops-operator.klickd", + "sha256": "bfad80b1110461f683c7000b9af1104ccdcd33a10c3059e3898dabea3f1d6fb4" + }, + { + "bytes": 10450, + "relative_path": "examples/v4.1/x-klickd-skills/pro/drone-operator.klickd", + "sha256": "d4017d93c8b8ac765e59914b61116bd90bd68944c4a2b47e031dafc7d562ec23" + }, + { + "bytes": 12705, + "relative_path": "examples/v4.1/x-klickd-skills/pro/edge-ai-operator.klickd", + "sha256": "295cd879168320e5af7d19a0c82a1bd0f8be52fbf9f16ec80c9db7f33b3cef6e" + }, + { + "bytes": 10507, + "relative_path": "examples/v4.1/x-klickd-skills/pro/eu-ai-act.klickd", + "sha256": "4df66de2930e3f4d726c08a1e0d15e64c6929f4cbbd6a593d69b9f88c5a9de93" + }, + { + "bytes": 11765, + "relative_path": "examples/v4.1/x-klickd-skills/pro/evidence-desk.klickd", + "sha256": "a1cc35277960a95ebbe12ca12c00d9d70ce71dc58cae99b642653835b90d14dd" + }, + { + "bytes": 11950, + "relative_path": "examples/v4.1/x-klickd-skills/pro/finance-analyst.klickd", + "sha256": "dbf02eac4910f73b35f818bd20545cd41f6a391ca06adf59f9470aa421afb326" + }, + { + "bytes": 12342, + "relative_path": "examples/v4.1/x-klickd-skills/pro/game-design.klickd", + "sha256": "0894cec1f3f54b770a16fd3f07e16817ee57e574bdb2ca0837b8653362f3abb3" + }, + { + "bytes": 10671, + "relative_path": "examples/v4.1/x-klickd-skills/pro/gdpr-readiness.klickd", + "sha256": "d5c4cb7b707fc3f028424f34149cb95300fae583f9a261dfc161914da43bbc40" + }, + { + "bytes": 12293, + "relative_path": "examples/v4.1/x-klickd-skills/pro/healthcare-ai-safety-reviewer.klickd", + "sha256": "183965179ebcdb7ef0f67866aa9c7364aaad1bd02996b4d0c696580ee2c6f789" + }, + { + "bytes": 12310, + "relative_path": "examples/v4.1/x-klickd-skills/pro/identity-access-management.klickd", + "sha256": "d9112c7f82733e77bc382c4ca314c2bf3112c63ca03455a7efd037b6c33cd74d" + }, + { + "bytes": 12206, + "relative_path": "examples/v4.1/x-klickd-skills/pro/learning-designer.klickd", + "sha256": "f7b1f5d39ee4f05261b4735a32475dee2583556681124c59b0250335729188f9" + }, + { + "bytes": 10906, + "relative_path": "examples/v4.1/x-klickd-skills/pro/literature-review.klickd", + "sha256": "104865d2750605d9261fd27208b0e19e2908861e9e5672ed453d383701a770f1" + }, + { + "bytes": 12164, + "relative_path": "examples/v4.1/x-klickd-skills/pro/llm-agent-engineering.klickd", + "sha256": "1649234c2859b8309c404f71f5c0db714805655cdbb714b7bdb9394644fb49f8" + }, + { + "bytes": 13319, + "relative_path": "examples/v4.1/x-klickd-skills/pro/llm-agent-security.klickd", + "sha256": "baed596a642f68086cabc1a76d09ba6d987231a057c8fb66f1aabb84dd137098" + }, + { + "bytes": 11861, + "relative_path": "examples/v4.1/x-klickd-skills/pro/mission-control.klickd", + "sha256": "2c2d02b4ecec40f022c4c7da0cdb3f082e588dfe11d3f14d1692dbbdba7e8ba2" + }, + { + "bytes": 11275, + "relative_path": "examples/v4.1/x-klickd-skills/pro/policy-analyst.klickd", + "sha256": "f348f1d1a73797979a51005586dcd9f1069a17fab173dab7df742290127c73a7" + }, + { + "bytes": 10728, + "relative_path": "examples/v4.1/x-klickd-skills/pro/privacy-product.klickd", + "sha256": "5a35d4a214a98f945f048c6647528df5ce2188aaa3d0913b7c6a1af2305eff00" + }, + { + "bytes": 12231, + "relative_path": "examples/v4.1/x-klickd-skills/pro/product-manager.klickd", + "sha256": "dbf2365508df3ae9acbbb6e002e3c44fb2a81e9166d0003cacb94a3714fdaeda" + }, + { + "bytes": 11164, + "relative_path": "examples/v4.1/x-klickd-skills/pro/project-operator.klickd", + "sha256": "5d019adfc08bcf518780bbe3e18a428682374397475a8bf6282d1e474ae6d5ce" + }, + { + "bytes": 12865, + "relative_path": "examples/v4.1/x-klickd-skills/pro/release-engineer.klickd", + "sha256": "c96329cff482f39d52a13b628bd027cd5854f15d40ce58c945fb58aec52aef9e" + }, + { + "bytes": 10347, + "relative_path": "examples/v4.1/x-klickd-skills/pro/rights-guard.klickd", + "sha256": "3efa2982479b6ba3544092d848fb52b91dc7687031c4a481181547d9728703b3" + }, + { + "bytes": 12186, + "relative_path": "examples/v4.1/x-klickd-skills/pro/sales-operator.klickd", + "sha256": "1bdc2c5895e07b909dda9bd04040917004727bed00198962c006a43a0be13060" + }, + { + "bytes": 11153, + "relative_path": "examples/v4.1/x-klickd-skills/pro/second-brain.klickd", + "sha256": "06504aa5dddc1b53634310210fff4374e68e0a2b959ae9dfa191b29995153bdb" + }, + { + "bytes": 12682, + "relative_path": "examples/v4.1/x-klickd-skills/pro/security-incident-response.klickd", + "sha256": "1315833cf73b3d04e00fe73006e985b0a7af7d266c457e18f61204cca09dcc7a" + }, + { + "bytes": 12667, + "relative_path": "examples/v4.1/x-klickd-skills/pro/sustainability-analyst.klickd", + "sha256": "445a7f1111e6474ca87bc717d0f50d6af5016863fe3fd87083502d77eeffcf1f" + }, + { + "bytes": 12150, + "relative_path": "examples/v4.1/x-klickd-skills/pro/technical-writer.klickd", + "sha256": "473c44c67a889c783872494f262feb16bdb1377a74dd5b4d605fe340ecebecc9" + }, + { + "bytes": 11317, + "relative_path": "examples/v4.1/x-klickd-skills/pro/trust-evidence.klickd", + "sha256": "57f4ac349438d3c3305e03ecf9a09a44d20e289550f8afa292267666bcc150ad" + }, + { + "bytes": 12109, + "relative_path": "examples/v4.1/x-klickd-skills/pro/ux-researcher.klickd", + "sha256": "d91afbf120c6175c4d23d175265697b7a0a7bd4f11dd60fc08bb053c8113e13e" + }, + { + "bytes": 16908, + "relative_path": "examples/v4.1/x-klickd-skills/pro/video-production-pipeline.klickd", + "sha256": "d6da8f7569953f7c6fb4f0ce02cb2649187bcbf52ae3f4d32a35c8fe885498a8" + } + ], + "inputs_hash_summary": "10fa77ec74ebfa2b7daa51a5787607b1dc9eb654608477f478c7850ab5a09b85", + "kind": "x_klickd_supply_chain_determinism_record", + "non_deterministic_zone": { + "comment": "Excluded from deterministic_run_id and from the output determinism hashes.", + "generated_at": "2026-06-02T11:20:28Z" + }, + "non_normative": true, + "output_files": [ + { + "deterministic_core_sha256": "8443e75756ffbbf1e138efec221cf117c70d76b15ea7d198822c5a46975a1ab6", + "relative_path": ".internal-skills/supply-chain/audit/audit_trail_index.json" + }, + { + "deterministic_core_sha256": "37a5891642a59ff26078e70bccea5e387eb8465ff0ca212b564ea63e728e3bee", + "relative_path": ".internal-skills/supply-chain/audit/determinism_record.json" + } + ], + "repeatability": { + "deterministic_fields": [ + "deterministic_run_id", + "inputs_hash_summary", + "input_files[*].sha256" + ], + "instructions": "Re-run `python scripts/generate_supply_chain_audit.py`. If inputs are unchanged, deterministic_run_id and inputs_hash_summary are identical across runs and hosts.", + "non_deterministic_fields_excluded": [ + "non_deterministic_zone.generated_at", + "source_commit_sha (provenance, not part of the hash)" + ] + }, + "repo": "Davincc77/klickdskill", + "schema_version": "0.1.0" +} diff --git a/scripts/generate_supply_chain_audit.py b/scripts/generate_supply_chain_audit.py new file mode 100644 index 0000000..e3f92ed --- /dev/null +++ b/scripts/generate_supply_chain_audit.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +"""Generate the x.klickd supply-chain audit-trail index + determinism record. + +This is the FIRST real (tool-backed) automation stage of the supply-chain +protocol described in the supply-chain RFC under docs/rfcs/ (docs-only spec PR, +not merged) and summarised in .internal-skills/supply-chain/audit/README.md. It +does NOT automate the full pipeline. It turns two traceability elements from +spec into artefacts that are actually produced, hashed, and re-checkable: + + 1. audit_trail_index.json -- a consultable index of the verifiable + artifacts the supply chain operates on, the + validation commands run against them, and an + append-style event list. + 2. determinism_record.json -- input file hashes, output file hashes, and a + deterministic_run_id derived only from inputs, + so two runs over identical inputs produce an + identical id (timestamps are quarantined in a + documented non-deterministic zone and are NOT + part of the hash). + +Inputs are the 42 NON-NORMATIVE x.klickd v4.1 candidate skill packs and their +manifest under examples/v4.1/x-klickd-skills/. A pack is only treated as a real +artifact here because its bytes exist on disk and hash-match the manifest -- +the same loaded + sha256_matches_manifest gate enforced by +scripts/verify_xklickd_skill_packs.py. A catalogue entry alone is NOT a loaded +skill. + +Stdlib-only. Offline. No network, no provider calls, no paid resources. No +release, tag, merge, publish, or deploy. Does not touch the private repo. + +CLI: + + python scripts/generate_supply_chain_audit.py # write artefacts + python scripts/generate_supply_chain_audit.py generate # (explicit) write + python scripts/generate_supply_chain_audit.py check # verify on-disk + # artefacts are + # in sync; no write + +Exit codes: + 0 success (write succeeded, or check found no drift) + 1 a critical invariant failed (missing/changed input, hash mismatch, + banned claim, obvious secret/PII), or `check` found drift + 2 usage / I-O error +""" + +from __future__ import annotations + +import datetime as _dt +import hashlib +import json +import re +import sys +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parents[1] +PACK_DIR = REPO_ROOT / "examples" / "v4.1" / "x-klickd-skills" +MANIFEST_PATH = PACK_DIR / "manifest.json" + +AUDIT_DIR = REPO_ROOT / ".internal-skills" / "supply-chain" / "audit" +AUDIT_INDEX_PATH = AUDIT_DIR / "audit_trail_index.json" +DETERMINISM_PATH = AUDIT_DIR / "determinism_record.json" + +SCHEMA_VERSION = "0.1.0" +REPO_NAME = "Davincc77/klickdskill" + +# Validation commands this stage records as the supply-chain's current +# tool-backed checks. They are recorded as declared commands; this generator +# does not silently run them (anti-mirage: the operator runs and audits them). +VALIDATION_COMMANDS = [ + "python scripts/verify_xklickd_skill_packs.py verify", + "python scripts/validate_v4_1_candidate_mapping.py", + "pytest tests/test_supply_chain_audit.py", +] + +# Substrings that must never appear in the generated public-facing artefacts. +# Two classes: internal codename leak, and banned unbounded public claims. +BANNED_SUBSTRINGS = ( + "chimera", + "universal standard", + "automatic gdpr", + "automatic eu ai act", + "benchmark superiority", + "proven benchmark", +) + +# Coarse secret / PII signatures. This is a tripwire on our OWN generated +# output, not a general scanner -- the inputs are public artifacts, but we +# refuse to emit anything that looks like a credential or personal contact. +_SECRET_PATTERNS = ( + re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"), + re.compile(r"\bAKIA[0-9A-Z]{16}\b"), + re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), + re.compile(r"\bghp_[A-Za-z0-9]{36}\b"), + re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"), +) + + +class InvariantError(RuntimeError): + """Raised when a critical supply-chain invariant fails.""" + + +def _rel(path: Path) -> str: + """Repo-relative path when possible, else the bare name. + + The bare-name fallback keeps the output stable and the record self-describing + when artefacts are written outside the repo (e.g. a temp dir under test). + """ + try: + return str(path.relative_to(REPO_ROOT)) + except ValueError: + return path.name + + +def _sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _sha256_file(path: Path) -> str: + return _sha256_bytes(path.read_bytes()) + + +def _load_manifest() -> dict[str, Any]: + if not MANIFEST_PATH.exists(): + raise InvariantError(f"manifest not found at {MANIFEST_PATH}") + return json.loads(MANIFEST_PATH.read_text(encoding="utf-8")) + + +def _pack_path(entry: dict[str, Any]) -> Path: + rel = entry.get("relative_path") + if rel: + return REPO_ROOT / rel + return PACK_DIR / entry["tier"] / entry["file"] + + +def _git_commit_sha() -> str | None: + """Best-effort source commit, read from .git without invoking git. + + Returns None when not in a usable git checkout (the artefact then records + null rather than a guessed value -- never fabricate provenance). + """ + head = REPO_ROOT / ".git" / "HEAD" + if not head.exists(): + return None + try: + ref = head.read_text(encoding="utf-8").strip() + except OSError: + return None + if ref.startswith("ref:"): + ref_path = REPO_ROOT / ".git" / ref.split(" ", 1)[1].strip() + if ref_path.exists(): + return ref_path.read_text(encoding="utf-8").strip() or None + # packed-refs fallback + packed = REPO_ROOT / ".git" / "packed-refs" + target = ref.split(" ", 1)[1].strip() + if packed.exists(): + for line in packed.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line or line.startswith(("#", "^")): + continue + sha, _, name = line.partition(" ") + if name == target: + return sha or None + return None + return ref or None + + +def _collect_inputs() -> list[dict[str, Any]]: + """Collect the verifiable supply-chain inputs with deterministic ordering. + + Each input must (a) exist on disk and (b) hash-match the manifest, mirroring + the loaded + sha256_matches_manifest gate. A mismatch is a critical + invariant failure -- we do NOT silently paper over it. + """ + manifest = _load_manifest() + packs = manifest.get("packs", []) + if manifest.get("total_count") != 42 or len(packs) != 42: + raise InvariantError( + f"manifest must report 42 packs, got " + f"total_count={manifest.get('total_count')} entries={len(packs)}" + ) + + inputs: list[dict[str, Any]] = [] + # The manifest itself is an input. + inputs.append( + { + "role": "manifest", + "relative_path": str(MANIFEST_PATH.relative_to(REPO_ROOT)), + "bytes": MANIFEST_PATH.stat().st_size, + "sha256": _sha256_file(MANIFEST_PATH), + } + ) + + for entry in packs: + path = _pack_path(entry) + label = entry.get("file", "") + if not path.exists(): + raise InvariantError(f"{label}: missing input file at {path}") + data = path.read_bytes() + sha = _sha256_bytes(data) + expected = entry.get("sha256_file") + if sha != expected: + raise InvariantError( + f"{label}: sha256 {sha} != manifest {expected} " + "(artifact not in a loaded+verified state)" + ) + if len(data) != entry.get("bytes"): + raise InvariantError( + f"{label}: byte length {len(data)} != manifest {entry.get('bytes')}" + ) + inputs.append( + { + "role": "pack", + "pack": entry.get("pack"), + "tier": entry.get("tier"), + "relative_path": entry.get("relative_path"), + "bytes": len(data), + "sha256": sha, + } + ) + + # Stable ordering by relative_path so the derived id is order-independent + # w.r.t. manifest layout changes that do not change content. + inputs.sort(key=lambda x: x["relative_path"]) + return inputs + + +def _hash_summary(inputs: list[dict[str, Any]]) -> str: + """A single deterministic digest over (relative_path, sha256) pairs. + + Depends only on input content + identity -- not on timestamps, host, or run + order -- so it is the reproducibility anchor. + """ + h = hashlib.sha256() + for item in inputs: + h.update(item["relative_path"].encode("utf-8")) + h.update(b"\0") + h.update(item["sha256"].encode("utf-8")) + h.update(b"\n") + return h.hexdigest() + + +def _scan_banned(text: str) -> list[str]: + low = text.lower() + return [s for s in BANNED_SUBSTRINGS if s in low] + + +def _scan_secrets(text: str) -> list[str]: + hits: list[str] = [] + for pat in _SECRET_PATTERNS: + if pat.search(text): + hits.append(pat.pattern) + return hits + + +def build_records() -> tuple[dict[str, Any], dict[str, Any]]: + """Build (audit_index, determinism_record) as plain dicts. + + The deterministic core of both records excludes the timestamp, which lives + only under `non_deterministic_zone`. + """ + inputs = _collect_inputs() + inputs_hash_summary = _hash_summary(inputs) + commit_sha = _git_commit_sha() + now = _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # deterministic_run_id is derived ONLY from inputs -> identical inputs give + # an identical id across runs / hosts / clocks. + deterministic_run_id = "sha256:" + inputs_hash_summary + + audit_index: dict[str, Any] = { + "schema_version": SCHEMA_VERSION, + "kind": "x_klickd_supply_chain_audit_trail_index", + "non_normative": True, + "repo": REPO_NAME, + "source_commit_sha": commit_sha, + "deterministic_run_id": deterministic_run_id, + "checked_artifacts_count": len(inputs), + "checked_artifacts_hash_summary": inputs_hash_summary, + "validation_commands": list(VALIDATION_COMMANDS), + # validation_results is intentionally empty here: this generator records + # the declared commands but does NOT run them, so it cannot honestly + # assert their results. The operator runs them and the audit/CI captures + # outcomes. Pre-filled "pass" values would be a mirage. + "validation_results": [], + "build_or_audit_events": [ + { + "event": "audit_trail_index_generated", + "stage": "audit_trail_index", + "automation": "tool", + "inputs_hash_summary": inputs_hash_summary, + "source_commit_sha": commit_sha, + } + ], + "stage_automation": { + "audit_trail_index": "tool", + "determinism_record": "tool", + "reproducibility_check": "tool", + "pack_hash_verification": "tool", + "candidate_mapping_validation": "tool", + "diff_report": "planned", + "threat_model": "planned", + "license_check": "planned", + "source_freshness_check": "planned", + "pii_secrets_scan": "partial", + "private_public_boundary_check": "planned", + "context_graph_generation": "planned", + "candidate_skill_generation": "planned", + "premium_pass": "manual", + }, + "notes": [ + "NON-NORMATIVE. Not a v4.1 GA release artefact.", + "Only the stages marked 'tool' are backed by shipped automation; " + "'planned' stages are spec-only; 'partial' is a tripwire, not a " + "full scanner; 'manual' is human/agent premium work.", + "An artifact is counted only when its bytes exist on disk and " + "hash-match the manifest (loaded + sha256_matches_manifest).", + "validation_results is empty by design: this generator does not run " + "the validation commands, so it does not assert their outcomes.", + "Timestamps are excluded from deterministic_run_id; see " + "determinism_record.json non_deterministic_zone.", + ], + "non_deterministic_zone": { + "generated_at": now, + "comment": "Fields here are excluded from deterministic_run_id and " + "checked_artifacts_hash_summary.", + }, + } + + determinism_record: dict[str, Any] = { + "schema_version": SCHEMA_VERSION, + "kind": "x_klickd_supply_chain_determinism_record", + "non_normative": True, + "repo": REPO_NAME, + "hash_algo": "sha256", + "deterministic_run_id": deterministic_run_id, + "input_files": [ + {"relative_path": i["relative_path"], "sha256": i["sha256"], "bytes": i["bytes"]} + for i in inputs + ], + "inputs_hash_summary": inputs_hash_summary, + # output_files hashes are computed over the deterministic core of each + # output (with non_deterministic_zone stripped), so the record is + # self-consistent across runs. See verify_outputs(). + "output_files": [ + {"relative_path": _rel(AUDIT_INDEX_PATH)}, + {"relative_path": _rel(DETERMINISM_PATH)}, + ], + "repeatability": { + "instructions": "Re-run `python scripts/generate_supply_chain_audit.py`. " + "If inputs are unchanged, deterministic_run_id and " + "inputs_hash_summary are identical across runs and hosts.", + "deterministic_fields": [ + "deterministic_run_id", + "inputs_hash_summary", + "input_files[*].sha256", + ], + "non_deterministic_fields_excluded": [ + "non_deterministic_zone.generated_at", + "source_commit_sha (provenance, not part of the hash)", + ], + }, + "non_deterministic_zone": { + "generated_at": now, + "comment": "Excluded from deterministic_run_id and from the output " + "determinism hashes.", + }, + } + return audit_index, determinism_record + + +def _deterministic_core(record: dict[str, Any]) -> dict[str, Any]: + """Return a copy of `record` with the non-deterministic zone removed. + + Used to hash outputs in a clock-independent way. + """ + core = dict(record) + core.pop("non_deterministic_zone", None) + core.pop("source_commit_sha", None) + if "build_or_audit_events" in core: + core["build_or_audit_events"] = [ + {k: v for k, v in ev.items() if k != "source_commit_sha"} + for ev in core["build_or_audit_events"] + ] + return core + + +def _canonical_json(obj: Any) -> str: + return json.dumps(obj, indent=2, ensure_ascii=False, sort_keys=True) + "\n" + + +def _serialize(record: dict[str, Any]) -> str: + # Stable, human-diffable serialization. sort_keys keeps the on-disk bytes + # deterministic regardless of dict construction order. + return _canonical_json(record) + + +def _guard_output(name: str, text: str) -> None: + banned = _scan_banned(text) + if banned: + raise InvariantError(f"{name}: banned substring(s) present: {banned}") + secrets = _scan_secrets(text) + if secrets: + raise InvariantError(f"{name}: possible secret/PII pattern(s): {secrets}") + + +def cmd_generate() -> int: + audit_index, determinism_record = build_records() + + # Stamp the deterministic-core hashes of each output into the determinism + # record so the record describes the bytes it ships next to. + audit_core_hash = _sha256_bytes( + _canonical_json(_deterministic_core(audit_index)).encode("utf-8") + ) + det_core_hash = _sha256_bytes( + _canonical_json(_deterministic_core(determinism_record)).encode("utf-8") + ) + for out in determinism_record["output_files"]: + if out["relative_path"].endswith("audit_trail_index.json"): + out["deterministic_core_sha256"] = audit_core_hash + else: + out["deterministic_core_sha256"] = det_core_hash + + audit_text = _serialize(audit_index) + det_text = _serialize(determinism_record) + + _guard_output("audit_trail_index.json", audit_text) + _guard_output("determinism_record.json", det_text) + + AUDIT_DIR.mkdir(parents=True, exist_ok=True) + AUDIT_INDEX_PATH.write_text(audit_text, encoding="utf-8") + DETERMINISM_PATH.write_text(det_text, encoding="utf-8") + + print( + f"OK: wrote audit-trail index + determinism record " + f"({audit_index['checked_artifacts_count']} artifacts, " + f"run_id {audit_index['deterministic_run_id']})." + ) + print(f" - {_rel(AUDIT_INDEX_PATH)}") + print(f" - {_rel(DETERMINISM_PATH)}") + return 0 + + +def cmd_check() -> int: + """Verify on-disk artefacts are in sync with current inputs (no write). + + Compares the deterministic core of the freshly-built records against the + deterministic core of the on-disk records. Drift in the time-quarantined + zone is ignored; drift anywhere else (or missing files) is a failure. + """ + if not AUDIT_INDEX_PATH.exists() or not DETERMINISM_PATH.exists(): + print("FAIL: audit artefacts missing; run generate.", file=sys.stderr) + return 1 + + audit_index, determinism_record = build_records() + + disk_audit = json.loads(AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + disk_det = json.loads(DETERMINISM_PATH.read_text(encoding="utf-8")) + + problems: list[str] = [] + if _deterministic_core(audit_index) != _deterministic_core(disk_audit): + problems.append("audit_trail_index.json out of sync with current inputs") + # output_files carry computed hashes; rebuild them before comparing. + audit_core_hash = _sha256_bytes( + _canonical_json(_deterministic_core(audit_index)).encode("utf-8") + ) + det_core_hash = _sha256_bytes( + _canonical_json(_deterministic_core(determinism_record)).encode("utf-8") + ) + for out in determinism_record["output_files"]: + out["deterministic_core_sha256"] = ( + audit_core_hash + if out["relative_path"].endswith("audit_trail_index.json") + else det_core_hash + ) + if _deterministic_core(determinism_record) != _deterministic_core(disk_det): + problems.append("determinism_record.json out of sync with current inputs") + + # Re-guard on-disk bytes for banned/secret content. + for name, text in ( + ("audit_trail_index.json", AUDIT_INDEX_PATH.read_text(encoding="utf-8")), + ("determinism_record.json", DETERMINISM_PATH.read_text(encoding="utf-8")), + ): + try: + _guard_output(name, text) + except InvariantError as exc: + problems.append(str(exc)) + + if problems: + print(f"FAIL: {len(problems)} problem(s):", file=sys.stderr) + for p in problems: + print(f" - {p}", file=sys.stderr) + print("Run `python scripts/generate_supply_chain_audit.py` to refresh.", file=sys.stderr) + return 1 + + print( + f"OK: audit artefacts in sync (run_id {audit_index['deterministic_run_id']}, " + f"{audit_index['checked_artifacts_count']} artifacts)." + ) + return 0 + + +def main(argv: list[str]) -> int: + args = argv[1:] + cmd = args[0] if args else "generate" + try: + if cmd == "generate": + return cmd_generate() + if cmd == "check": + return cmd_check() + except InvariantError as exc: + print(f"FAIL (invariant): {exc}", file=sys.stderr) + return 1 + except OSError as exc: + print(f"FAIL (io): {exc}", file=sys.stderr) + return 2 + print(f"unknown command: {cmd!r} (generate|check)", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) diff --git a/tests/test_supply_chain_audit.py b/tests/test_supply_chain_audit.py new file mode 100644 index 0000000..5dd24b1 --- /dev/null +++ b/tests/test_supply_chain_audit.py @@ -0,0 +1,158 @@ +"""Tests for the supply-chain audit-trail index + determinism record stage. + +Exercises scripts/generate_supply_chain_audit.py directly (stdlib-only, offline) +against a temporary output directory so the committed artefacts are not touched. + +Covers: + - artefacts are generable and parse as JSON; + - required fields present in both records; + - deterministic_run_id / hash summary stable across two runs (same inputs); + - only the timestamp differs between runs (it lives in non_deterministic_zone); + - no obvious secret/PII in generated artefacts; + - no banned public-claim / codename string in generated artefacts; + - `check` reports in-sync, and detects tampering as drift. +""" +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPT = REPO_ROOT / "scripts" / "generate_supply_chain_audit.py" + + +def _load_module(tmp_path: Path): + """Import the generator with its output paths redirected into tmp_path.""" + spec = importlib.util.spec_from_file_location("gen_sc_audit", SCRIPT) + assert spec and spec.loader + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + mod.AUDIT_DIR = tmp_path / "audit" + mod.AUDIT_INDEX_PATH = mod.AUDIT_DIR / "audit_trail_index.json" + mod.DETERMINISM_PATH = mod.AUDIT_DIR / "determinism_record.json" + return mod + + +@pytest.fixture() +def gen(tmp_path): + return _load_module(tmp_path) + + +def test_generate_succeeds_and_parses(gen): + assert gen.cmd_generate() == 0 + audit = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + det = json.loads(gen.DETERMINISM_PATH.read_text(encoding="utf-8")) + assert isinstance(audit, dict) and isinstance(det, dict) + + +def test_audit_index_required_fields(gen): + gen.cmd_generate() + audit = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + for field in ( + "schema_version", + "repo", + "source_commit_sha", + "deterministic_run_id", + "checked_artifacts_count", + "checked_artifacts_hash_summary", + "validation_commands", + "validation_results", + "build_or_audit_events", + "stage_automation", + "notes", + "non_deterministic_zone", + ): + assert field in audit, f"missing {field}" + assert audit["repo"] == "Davincc77/klickdskill" + assert audit["checked_artifacts_count"] == 43 # 42 packs + manifest + assert isinstance(audit["build_or_audit_events"], list) + assert audit["build_or_audit_events"] + # validation_results must be empty by design (generator does not run them). + assert audit["validation_results"] == [] + + +def test_determinism_record_required_fields(gen): + gen.cmd_generate() + det = json.loads(gen.DETERMINISM_PATH.read_text(encoding="utf-8")) + for field in ( + "schema_version", + "hash_algo", + "deterministic_run_id", + "input_files", + "inputs_hash_summary", + "output_files", + "repeatability", + "non_deterministic_zone", + ): + assert field in det, f"missing {field}" + assert det["hash_algo"] == "sha256" + assert len(det["input_files"]) == 43 + for item in det["input_files"]: + assert len(item["sha256"]) == 64 + + +def test_deterministic_run_id_stable_across_runs(gen, tmp_path): + gen.cmd_generate() + first_audit = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + first_det = json.loads(gen.DETERMINISM_PATH.read_text(encoding="utf-8")) + + gen.cmd_generate() # second run, identical inputs + second_audit = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + second_det = json.loads(gen.DETERMINISM_PATH.read_text(encoding="utf-8")) + + assert first_audit["deterministic_run_id"] == second_audit["deterministic_run_id"] + assert first_det["deterministic_run_id"] == second_det["deterministic_run_id"] + assert ( + first_audit["checked_artifacts_hash_summary"] + == second_audit["checked_artifacts_hash_summary"] + ) + + +def test_only_timestamp_is_non_deterministic(gen): + """Two runs must agree on everything outside non_deterministic_zone.""" + gen.cmd_generate() + first = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + gen.cmd_generate() + second = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + first.pop("non_deterministic_zone") + second.pop("non_deterministic_zone") + assert first == second + + +def test_no_banned_claims_or_codename(gen): + gen.cmd_generate() + for path in (gen.AUDIT_INDEX_PATH, gen.DETERMINISM_PATH): + text = path.read_text(encoding="utf-8").lower() + for banned in gen.BANNED_SUBSTRINGS: + assert banned not in text, f"{path.name} contains banned {banned!r}" + + +def test_no_obvious_secret_or_pii(gen): + gen.cmd_generate() + for path in (gen.AUDIT_INDEX_PATH, gen.DETERMINISM_PATH): + text = path.read_text(encoding="utf-8") + assert gen._scan_secrets(text) == [], f"{path.name} has secret/PII pattern" + + +def test_check_reports_in_sync_after_generate(gen): + gen.cmd_generate() + assert gen.cmd_check() == 0 + + +def test_check_detects_tampering(gen): + gen.cmd_generate() + audit = json.loads(gen.AUDIT_INDEX_PATH.read_text(encoding="utf-8")) + audit["checked_artifacts_count"] = 999 # tamper inside deterministic core + gen.AUDIT_INDEX_PATH.write_text(json.dumps(audit, indent=2), encoding="utf-8") + assert gen.cmd_check() == 1 + + +def test_check_detects_banned_string_injection(gen): + gen.cmd_generate() + det = json.loads(gen.DETERMINISM_PATH.read_text(encoding="utf-8")) + det["non_deterministic_zone"]["comment"] = "universal standard" # banned + gen.DETERMINISM_PATH.write_text(json.dumps(det, indent=2), encoding="utf-8") + assert gen.cmd_check() == 1