diff --git a/.github/workflows/architecture-cli-boundary-regression.yml b/.github/workflows/architecture-cli-boundary-regression.yml new file mode 100644 index 000000000..784fb27dd --- /dev/null +++ b/.github/workflows/architecture-cli-boundary-regression.yml @@ -0,0 +1,135 @@ +name: Architecture CLI boundary diagnostic + +'on': + push: + branches: + - codex/architecture-cli-boundary-red-20261016 + pull_request: + paths: + - architecture/artifact_index.py + - architecture/graph_artifact_readback.py + - tools/artifact_index_export.py + - tools/architecture_graph_artifact_readback.py + - tests/test_architecture_artifact_index.py + - tests/test_architecture_graph_artifact_readback.py + - tests/test_architecture_cli_boundaries.py + - .github/workflows/architecture-cli-boundary-regression.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: architecture-cli-boundary-${{ github.run_id }}-${{ github.run_attempt }} + cancel-in-progress: false + +jobs: + cli-boundaries: + name: architecture-cli-boundary-${{ matrix.mode }} + runs-on: ubuntu-24.04 + timeout-minutes: 6 + strategy: + fail-fast: false + matrix: + mode: [normal, optimized] + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + timeout-minutes: 1 + with: + persist-credentials: false + fetch-depth: 1 + + - name: Record diagnostic native context before running controls + timeout-minutes: 1 + env: + DIAGNOSTIC_MODE: ${{ matrix.mode }} + run: | + set -euo pipefail + evidence="$RUNNER_TEMP/architecture-cli-boundary-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$DIAGNOSTIC_MODE" + mkdir -m 700 "$evidence" + git rev-parse HEAD > "$evidence/checked-commit.txt" + python3 -I -B -S - "$evidence" <<'PY' + from datetime import datetime, timezone + import hashlib + import json + import os + from pathlib import Path + import platform + import sys + root = Path(os.environ['GITHUB_WORKSPACE']) + destination = Path(sys.argv[1]) + paths = ( + '.github/workflows/architecture-cli-boundary-regression.yml', + 'tests/test_architecture_cli_boundaries.py', + 'tests/test_architecture_artifact_index.py', + 'tests/test_architecture_graph_artifact_readback.py', + 'tools/artifact_index_export.py', + 'tools/architecture_graph_artifact_readback.py', + 'architecture/artifact_index.py', + 'architecture/graph_artifact_readback.py', + 'docs/site/dependency-source/GRAPH.json', + ) + sources = {} + for name in paths: + raw = (root / name).read_bytes() + sources[name] = {'bytes': len(raw), 'sha256': hashlib.sha256(raw).hexdigest()} + context = { + 'schema': 'architecture-cli-boundary-diagnostic/v1', + 'recorded_at': datetime.now(timezone.utc).isoformat(), + 'repository': os.environ['GITHUB_REPOSITORY'], + 'event_name': os.environ['GITHUB_EVENT_NAME'], + 'workflow_ref': os.environ['GITHUB_WORKFLOW_REF'], + 'workflow_sha': os.environ['GITHUB_WORKFLOW_SHA'], + 'event_sha': os.environ['GITHUB_SHA'], + 'checked_commit': (destination / 'checked-commit.txt').read_text().strip(), + 'run_id': os.environ['GITHUB_RUN_ID'], + 'run_attempt': os.environ['GITHUB_RUN_ATTEMPT'], + 'mode': os.environ['DIAGNOSTIC_MODE'], + 'python': sys.version, + 'platform': platform.platform(), + 'source_identities': sources, + 'scope': 'four CLI boundary methods only; required full suite remains separate', + 'scientific_effect': 'NONE', + 'scientific_status_authority': False, + } + (destination / 'native-context.json').write_text( + json.dumps(context, sort_keys=True, indent=2) + '\n', encoding='utf-8') + PY + + - name: Run only the four external CLI boundary controls + timeout-minutes: 3 + env: + DIAGNOSTIC_MODE: ${{ matrix.mode }} + ARCHITECTURE_CLI_BOUNDARY_REPORT: ${{ runner.temp }}/architecture-cli-boundary-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.mode }}/child-results.jsonl + run: | + set -euo pipefail + evidence="$RUNNER_TEMP/architecture-cli-boundary-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$DIAGNOSTIC_MODE" + flags=(-I -B -S) + if [ "$DIAGNOSTIC_MODE" = optimized ]; then + flags+=(-O) + fi + printf '%q ' timeout --signal=TERM --kill-after=5s 150s \ + python3 "${flags[@]}" -m unittest discover -s tests -p test_architecture_cli_boundaries.py -v \ + > "$evidence/test-command.txt" + printf '\n' >> "$evidence/test-command.txt" + set +e + timeout --signal=TERM --kill-after=5s 150s \ + python3 "${flags[@]}" -m unittest discover -s tests -p test_architecture_cli_boundaries.py -v \ + > "$evidence/tests.stdout" 2> "$evidence/tests.stderr" + status=$? + set -e + printf '%s\n' "$status" > "$evidence/test-exit-status.txt" + cat "$evidence/tests.stdout" + cat "$evidence/tests.stderr" >&2 + exit "$status" + + - name: Retain separate diagnostic originals on every conclusion + if: always() + timeout-minutes: 1 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 + with: + name: architecture-cli-boundary-${{ matrix.mode }}-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/architecture-cli-boundary-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.mode }}/ + if-no-files-found: error + retention-days: 90 + compression-level: 6 diff --git a/.github/workflows/navigation.yml b/.github/workflows/navigation.yml index 586d2a722..c4e157293 100644 --- a/.github/workflows/navigation.yml +++ b/.github/workflows/navigation.yml @@ -10,7 +10,7 @@ permissions: jobs: navigation: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 15 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 diff --git a/.github/workflows/research-architecture.yml b/.github/workflows/research-architecture.yml index 4b53e755e..8739e26fd 100644 --- a/.github/workflows/research-architecture.yml +++ b/.github/workflows/research-architecture.yml @@ -36,8 +36,8 @@ jobs: graph: name: research-architecture runs-on: ubuntu-24.04 - # Complete stage ceiling: 19 preparation + 11 tests + fourteen 1-minute stages. - timeout-minutes: 44 + # Complete stage ceiling: 19 preparation + 16 tests + fourteen 1-minute stages. + timeout-minutes: 49 outputs: checked_commit: ${{ steps.accepted.outputs.checked_commit }} repository: ${{ steps.accepted.outputs.repository }} @@ -311,17 +311,17 @@ jobs: printf '%s\n' "$GITHUB_RUN_ATTEMPT" > "$RUNNER_TEMP/architecture-publish/run-attempt.txt" - name: Run controls and export in ephemeral restricted container id: architecture_controls - timeout-minutes: 11 + timeout-minutes: 16 env: IMAGE: python:3.11.14-slim-bookworm@sha256:83f339c1be6340ae1096010fdccf6552ac932d8f410d45d206014916bdf37e48 run: | set -euo pipefail set +e - timeout --signal=TERM --kill-after=10s 600s docker run --pull=never --rm --log-driver none --name architecture-controls \ + timeout --signal=TERM --kill-after=10s 900s docker run --pull=never --rm --log-driver none --name architecture-controls \ --network none --read-only --user 65532:65532 --cpus 2 --memory 1g --pids-limit 128 \ --cap-drop ALL --security-opt no-new-privileges \ --env ARCHITECTURE_SANDBOX=1 \ - --tmpfs /tmp:rw,noexec,nosuid,size=64m \ + --tmpfs /tmp:rw,noexec,nosuid,size=128m \ --mount type=bind,source="$GITHUB_WORKSPACE",target=/source,readonly \ --mount type=bind,source="$RUNNER_TEMP/architecture-evidence/container",target=/output \ --workdir /source "$IMAGE" sh -c ' diff --git a/architecture/artifact_index.py b/architecture/artifact_index.py new file mode 100644 index 000000000..7cb920cf6 --- /dev/null +++ b/architecture/artifact_index.py @@ -0,0 +1,325 @@ +"""Project a pinned native Artifact Index capture into fixed public fields. + +This module treats captures as inert JSON data. It performs no I/O, interprets +no cell content, and authenticates no external custody or scientific status. +""" +from __future__ import annotations + +import base64 +from datetime import datetime +import hashlib +import json +import math +import re + + +STDIN_LIMIT = 16 * 1024 * 1024 +CAPTURE_LIMIT = 4 * 1024 * 1024 +DEPTH_LIMIT = 64 +COLLECTION_LIMIT = 4096 +CELL_LIMIT = 8192 +GRID_ROW_LIMIT = 10_000_000 + +HEADER = ( + "Artifact ID", "Title", "Org", "Class", "Priority", "Topics / object tags", + "Status", "Authority / canonical impact", "Dependencies or supersession", + "Modified UTC", "Source", "Notes", +) +PUBLIC_COLUMNS = ( + "Artifact ID", "Title", "Org", "Class", "Status", + "Authority / canonical impact", "Dependencies or supersession", "Modified UTC", +) +PUBLIC_INDICES = tuple(HEADER.index(column) for column in PUBLIC_COLUMNS) +ROLES = frozenset(("source", "review", "kernel", "computation", "alignment", "teaching-source")) +PIN_KEYS = frozenset(( + "capture_sha256", "metadata_sha256", "policy_sha256", + "bindings_sha256", "graph_nodes_sha256", +)) +IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.:-]{0,191}") +SHA256 = re.compile(r"[0-9a-f]{64}") +UTC_SECOND = re.compile(r"([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z") + + +class ArtifactIndexRefused(ValueError): + """An opaque refusal whose message never includes caller data.""" + + def __init__(self): + super().__init__("ARTIFACT_INDEX_REFUSED") + + +def _require(condition): + if not condition: + raise ArtifactIndexRefused() + + +def _validate_json_value(value, depth=0): + """Validate JSON types, finite numbers, Unicode, and container depth.""" + kind = type(value) + if kind is dict: + _require(depth < DEPTH_LIMIT) + for key, child in value.items(): + _require(type(key) is str) + key.encode("utf-8", errors="strict") + _validate_json_value(child, depth + 1) + elif kind is list: + _require(depth < DEPTH_LIMIT) + for child in value: + _validate_json_value(child, depth + 1) + elif kind is str: + value.encode("utf-8", errors="strict") + elif kind is float: + _require(math.isfinite(value)) + else: + _require(kind in (int, bool, type(None))) + + +def _object_pairs(pairs): + result = {} + for key, value in pairs: + _require(key not in result) + result[key] = value + return result + + +def _nonfinite_constant(_token): + raise ArtifactIndexRefused() + + +def _finite_float(token): + value = float(token) + _require(math.isfinite(value)) + return value + + +def _strict_json(raw, limit): + try: + _require(type(raw) is bytes and 0 < len(raw) <= limit) + value = json.loads( + raw.decode("utf-8", errors="strict"), + object_pairs_hook=_object_pairs, + parse_constant=_nonfinite_constant, + parse_float=_finite_float, + ) + _validate_json_value(value) + return value + except Exception: + raise ArtifactIndexRefused() from None + + +def parse_packet(raw): + """Read one bounded strict JSON packet from caller-supplied bytes.""" + return _strict_json(raw, STDIN_LIMIT) + + +def canonical_json(value): + """Return deterministic ASCII JSON bytes, preserving array order/types.""" + try: + _validate_json_value(value) + return json.dumps( + value, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=False, + ).encode("ascii") + except Exception: + raise ArtifactIndexRefused() from None + + +def _object(value, keys=None): + _require(type(value) is dict) + if keys is not None: + _require(value.keys() == keys) + return value + + +def _integer(value, minimum=0, maximum=None): + _require(type(value) is int and value >= minimum) + if maximum is not None: + _require(value <= maximum) + return value + + +def _identifier(value): + _require(type(value) is str and IDENTIFIER.fullmatch(value) is not None) + return value + + +def _bounded_list(value, minimum=0, maximum=COLLECTION_LIMIT): + _require(type(value) is list and minimum <= len(value) <= maximum) + return value + + +def _unique_identifiers(value, minimum=0): + values = _bounded_list(value, minimum) + seen = set() + for item in values: + _identifier(item) + _require(item not in seen) + seen.add(item) + return seen + + +def _expected_pins(expected): + _object(expected, PIN_KEYS) + for value in expected.values(): + _require(type(value) is str and SHA256.fullmatch(value) is not None) + + +def _capture_metadata(capture): + _object(capture, {"capture_id", "captured_at", "source", "raw_base64"}) + _identifier(capture["capture_id"]) + timestamp = capture["captured_at"] + _require(type(timestamp) is str) + match = UTC_SECOND.fullmatch(timestamp) + _require(match is not None) + # A numeric constructor checks calendar validity without parsing cell text. + datetime(*(int(part) for part in match.groups())) + source = _object(capture["source"], { + "spreadsheet_id", "worksheet_id", "title", "ranges", "cell_fields", + }) + _identifier(source["spreadsheet_id"]) + _integer(source["worksheet_id"]) + title = source["title"] + _require(type(title) is str and 0 < len(title.encode("utf-8")) <= 256) + _require("'" not in title and not any(ord(char) < 32 or 127 <= ord(char) <= 159 for char in title)) + _require(source["cell_fields"] == "formattedValue" and type(source["cell_fields"]) is str) + ranges = _bounded_list(source["ranges"], 1, COLLECTION_LIMIT + 1) + range_pattern = re.compile(r"'" + re.escape(title) + r"'!A([1-9][0-9]*):L([1-9][0-9]*)") + intervals = [] + prior_end = 0 + for declared in ranges: + _require(type(declared) is str) + match = range_pattern.fullmatch(declared) + _require(match is not None) + # Bounds precede integer conversion, including enormous decimal text. + _require(all(len(part) <= 8 for part in match.groups())) + first, last = (int(part) for part in match.groups()) + _require(prior_end < first <= last <= GRID_ROW_LIMIT) + intervals.append((first - 1, last)) + prior_end = last + _require(sum(stop - start for start, stop in intervals) <= COLLECTION_LIMIT + 1) + return source, intervals + + +def _capture_bytes(encoded): + _require(type(encoded) is str and 0 < len(encoded) <= 4 * ((CAPTURE_LIMIT + 2) // 3)) + raw = base64.b64decode(encoded.encode("ascii"), validate=True) + _require(0 < len(raw) <= CAPTURE_LIMIT) + _require(base64.b64encode(raw).decode("ascii") == encoded) + return raw + + +def _native_rows(native, source, intervals): + _object(native) + _require(type(native.get("spreadsheetId")) is str and native["spreadsheetId"] == source["spreadsheet_id"]) + sheets = _bounded_list(native.get("sheets"), 1, 1) + sheet = _object(sheets[0]) + properties = _object(sheet.get("properties")) + _integer(properties.get("sheetId")) + _require(properties["sheetId"] == source["worksheet_id"]) + _require(type(properties.get("title")) is str and properties["title"] == source["title"]) + grid = _object(properties.get("gridProperties")) + row_count = _integer(grid.get("rowCount"), 1, GRID_ROW_LIMIT) + _require(_integer(grid.get("columnCount"), 1) == len(HEADER)) + blocks = _bounded_list(sheet.get("data"), len(intervals), len(intervals)) + rows_by_id = {} + header_count = 0 + for block, (start, stop) in zip(blocks, intervals): + _object(block) + _require(stop <= row_count) + _require(_integer(block.get("startRow", 0)) == start) + _require(_integer(block.get("startColumn", 0)) == 0) + rows = _bounded_list(block.get("rowData"), stop - start, stop - start) + for offset, row in enumerate(rows): + _object(row) + cells = _bounded_list(row.get("values"), len(HEADER), len(HEADER)) + values = [] + for cell in cells: + _object(cell) + value = cell.get("formattedValue", "") + _require(type(value) is str and len(value.encode("utf-8")) <= CELL_LIMIT) + values.append(value) + position = start + offset + if position == 0: + _require(values == list(HEADER)) + header_count += 1 + else: + artifact_id = _identifier(values[0]) + _require(artifact_id not in rows_by_id) + rows_by_id[artifact_id] = (position + 1, values) + _require(len(rows_by_id) <= COLLECTION_LIMIT) + _require(header_count == 1) + return rows_by_id + + +def _project(packet, expected): + _validate_json_value(packet) + _expected_pins(expected) + _object(packet, {"schema_version", "capture", "policy", "bindings", "graph_nodes"}) + _require(_integer(packet["schema_version"]) == 1) + capture = packet["capture"] + source, intervals = _capture_metadata(capture) + raw = _capture_bytes(capture["raw_base64"]) + metadata = {key: value for key, value in capture.items() if key != "raw_base64"} + identities = { + "capture_sha256": hashlib.sha256(raw).hexdigest(), + "metadata_sha256": hashlib.sha256(canonical_json(metadata)).hexdigest(), + "policy_sha256": hashlib.sha256(canonical_json(packet["policy"])).hexdigest(), + "bindings_sha256": hashlib.sha256(canonical_json(packet["bindings"])).hexdigest(), + "graph_nodes_sha256": hashlib.sha256(canonical_json(packet["graph_nodes"])).hexdigest(), + } + _require(identities == expected) + policy = _object(packet["policy"], {"schema_version", "artifact_ids", "columns"}) + _require(_integer(policy["schema_version"]) == 1) + _require(type(policy["columns"]) is list and policy["columns"] == list(PUBLIC_COLUMNS)) + selected = _unique_identifiers(policy["artifact_ids"], 1) + graph_nodes = _unique_identifiers(packet["graph_nodes"]) + bindings = [] + pairs = set() + bound_nodes = set() + for binding in _bounded_list(packet["bindings"]): + _object(binding, {"node_id", "artifact_id", "roles"}) + node = _identifier(binding["node_id"]) + artifact = _identifier(binding["artifact_id"]) + _require(node in graph_nodes and artifact in selected) + roles = _bounded_list(binding["roles"], 1, len(ROLES)) + _require(all(type(role) is str and role in ROLES for role in roles)) + _require(len(set(roles)) == len(roles)) + pair = (node, artifact) + _require(pair not in pairs) + pairs.add(pair) + bound_nodes.add(node) + bindings.append({"node_id": node, "artifact_id": artifact, "roles": sorted(roles)}) + native = _strict_json(raw, CAPTURE_LIMIT) + native_rows = _native_rows(native, source, intervals) + _require(selected <= native_rows.keys()) + rows = [] + for artifact in sorted(selected): + source_row, values = native_rows[artifact] + rows.append({ + "artifact_id": artifact, "source_row": source_row, + "fields": {column: values[index] for column, index in zip(PUBLIC_COLUMNS, PUBLIC_INDICES)}, + }) + return { + "schema_version": 1, "scientific_effect": "NONE", + "scientific_status_authority": False, "custody": "unknown", + "source": { + "capture_id": capture["capture_id"], "captured_at": capture["captured_at"], + "spreadsheet_id": source["spreadsheet_id"], "worksheet_id": source["worksheet_id"], + "worksheet_title": source["title"], "ranges": list(source["ranges"]), + "cell_fields": source["cell_fields"], "representation": "native-api-formattedValue-json", + }, + "identities": identities, "columns": list(PUBLIC_COLUMNS), "rows": rows, + "bindings": sorted(bindings, key=lambda item: (item["node_id"], item["artifact_id"])), + "unmapped_nodes": sorted(graph_nodes - bound_nodes), + } + + +def project_artifact_index(packet, expected): + """Return the exact public projection, or raise an opaque refusal. + + Independent expected pins are mandatory; no expectation is learned from + the packet. Input objects are not mutated and private data is not returned. + """ + try: + return _project(packet, expected) + except Exception: + raise ArtifactIndexRefused() from None diff --git a/architecture/graph_artifact_readback.py b/architecture/graph_artifact_readback.py new file mode 100644 index 000000000..c39ffd0fd --- /dev/null +++ b/architecture/graph_artifact_readback.py @@ -0,0 +1,531 @@ +"""Strict byte-only readback of a bounded architecture artifact. + +Caller pins establish declared comparison context, never authenticated custody. +No captured code, producer helper, gate, or test module is imported or executed. +""" +from __future__ import annotations + +import hashlib +import json +import math +import re +import stat +import struct +import zlib +from typing import Any + + +MIB = 1024 * 1024 +RAW_LIMIT = 64 * MIB +INFLATED_LIMIT = 64 * MIB +OUTPUT_LIMIT = 16 * MIB +MAX_DEPTH = 64 +SOURCE_BYTES = 38753 +GRAPH_SOURCE = { + "repository": "d6g8k5htny-coder/Math-", + "commit": "7858329974e28be79f29b22644370084ff43da4f", + "captured_at": "2026-10-03T15:07:03Z", + "graph_sha256": "8822e9618678321a342d69cd0b8ae6552de1b5d578c331de5072b2892ee9dd09", + "gate_sha256": "a78f3e25f3b0cfe113e618a4c31a7a25d7f22af638c46dec1ecba221fa333ac8", +} +IMAGE_DIGEST = "python@sha256:83f339c1be6340ae1096010fdccf6552ac932d8f410d45d206014916bdf37e48" +MEMBER_LIMITS = { + "ARTIFACT_SHA256SUMS": 16 * 1024, + "checked-commit.txt": 128, + "run-id.txt": 128, + "run-attempt.txt": 128, + "container-exit-status.txt": 128, + "exporter.stdout": 64 * 1024, + "exporter.stderr": 64 * 1024, + "runtime-image.json": MIB, + "generated/graph.json": 4 * MIB, + "architecture-run-receipt.json": 4 * MIB, + "console.log": 4 * MIB, + "tests-normal.log": 16 * MIB, + "tests-optimized.log": 16 * MIB, +} +EVIDENCE_MEMBERS = frozenset({ + "generated/graph.json", "checked-commit.txt", "run-id.txt", + "run-attempt.txt", "container-exit-status.txt", "tests-normal.log", + "tests-optimized.log", "runtime-image.json", +}) +BINDING_KEYS = frozenset({ + "checked_commit", "repository", "run_id", "run_attempt", + "receipt_sha256", "artifact_id", "artifact_sha256", "artifact_name", +}) +REVIEW_FIELDS = ( + "review_source", "review_issue", "review_basis", "review_provider", + "review_providers", "review_disposition", +) +HEX40 = re.compile(r"[0-9a-f]{40}") +HEX64 = re.compile(r"[0-9a-f]{64}") +POSITIVE_DECIMAL = re.compile(r"[1-9][0-9]*") +REPOSITORY = re.compile(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+") +IMAGE_ID = re.compile(r"sha256:[0-9a-f]{64}") +TEST_ID = re.compile(r"test_architecture_[A-Za-z0-9_]+\.[A-Za-z_][A-Za-z0-9_]*\.test[A-Za-z0-9_]*") +TEST_RESULT = re.compile(r"(\S+) \(([^()]*)\) \.\.\. (.+)") +TEST_RESULT_LIKE = re.compile(r"^\s*test|\([^()\n]*\.test[^()\n]*\)|\)\s*\.\.\.", re.IGNORECASE) +TEST_SUMMARY = re.compile(r"Ran ([1-9][0-9]*) tests in ([0-9]+(?:\.[0-9]+)?)s") +REQUIRED_IDS = frozenset(""" +test_architecture_binding.ArchitectureBindingContract.test_artifact_id_requires_positive_ascii_decimal_without_leading_zero +test_architecture_binding.ArchitectureBindingContract.test_artifact_name_is_exactly_bound_to_current_run_and_attempt +test_architecture_binding.ArchitectureBindingContract.test_attempt_ten_is_a_positive_decimal_not_a_single_digit +test_architecture_binding.ArchitectureBindingContract.test_commit_format_cannot_be_authorized_by_an_equally_invalid_native_commit +test_architecture_binding.ArchitectureBindingContract.test_deeply_nested_json_is_refused_without_a_success_envelope +test_architecture_binding.ArchitectureBindingContract.test_duplicate_top_level_keys_are_refused_even_when_last_value_is_valid +test_architecture_binding.ArchitectureBindingContract.test_each_current_native_argument_is_used_for_identity_binding +test_architecture_binding.ArchitectureBindingContract.test_each_duplicate_output_key_is_refused_even_when_last_value_is_valid +test_architecture_binding.ArchitectureBindingContract.test_each_expected_native_identity_argument_is_required +test_architecture_binding.ArchitectureBindingContract.test_each_non_success_result_is_refused +test_architecture_binding.ArchitectureBindingContract.test_each_of_the_eight_outputs_is_required +test_architecture_binding.ArchitectureBindingContract.test_each_output_rejects_an_empty_string +test_architecture_binding.ArchitectureBindingContract.test_each_output_rejects_non_string_identities_without_coercion +test_architecture_binding.ArchitectureBindingContract.test_each_top_level_field_is_required +test_architecture_binding.ArchitectureBindingContract.test_extra_output_fields_are_refused +test_architecture_binding.ArchitectureBindingContract.test_extra_top_level_fields_are_refused +test_architecture_binding.ArchitectureBindingContract.test_invalid_or_multiple_json_documents_are_refused +test_architecture_binding.ArchitectureBindingContract.test_non_finite_json_numbers_are_refused +test_architecture_binding.ArchitectureBindingContract.test_non_object_top_level_values_are_refused +test_architecture_binding.ArchitectureBindingContract.test_other_valid_native_identities_are_accepted_and_preserved +test_architecture_binding.ArchitectureBindingContract.test_outputs_must_be_an_object +test_architecture_binding.ArchitectureBindingContract.test_receipt_and_artifact_sha256_require_lowercase_64_hex_digits +test_architecture_binding.ArchitectureBindingContract.test_repository_substitution_and_case_drift_are_refused +test_architecture_binding.ArchitectureBindingContract.test_run_and_attempt_decimal_format_cannot_be_authorized_by_invalid_native_args +test_architecture_binding.ArchitectureBindingContract.test_stale_attempt_is_refused_even_with_a_matching_stale_artifact_name +test_architecture_binding.ArchitectureBindingContract.test_stale_checked_commit_is_refused +test_architecture_binding.ArchitectureBindingContract.test_stale_run_is_refused_even_with_a_matching_stale_artifact_name +test_architecture_binding.ArchitectureBindingContract.test_valid_current_run_has_exact_stable_non_scientific_output +test_architecture_binding.ArchitectureBindingContract.test_valid_json_at_exact_stdin_byte_limit_is_accepted +test_architecture_binding.ArchitectureBindingContract.test_valid_json_over_stdin_byte_limit_is_refused +test_architecture_graph_export.ArchitectureGraphExportContract.test_caller_pinned_provenance_cannot_authorize_different_gate_code +test_architecture_graph_export.ArchitectureGraphExportContract.test_classification_and_proof_review_text_cannot_manufacture_evidence +test_architecture_graph_export.ArchitectureGraphExportContract.test_context_only_cycle_is_allowed_without_changing_gate_semantics +test_architecture_graph_export.ArchitectureGraphExportContract.test_default_trust_rejects_self_consistent_changed_graph_and_provenance +test_architecture_graph_export.ArchitectureGraphExportContract.test_dimensions_describe_only_explicit_recorded_metadata +test_architecture_graph_export.ArchitectureGraphExportContract.test_duplicate_graph_json_keys_are_rejected_after_identity_validation +test_architecture_graph_export.ArchitectureGraphExportContract.test_duplicate_provenance_json_keys_are_rejected_before_gate_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_exact_source_bound_hard_gate_validator_is_called +test_architecture_graph_export.ArchitectureGraphExportContract.test_failed_export_preserves_an_earlier_valid_export +test_architecture_graph_export.ArchitectureGraphExportContract.test_gate_identity_is_checked_before_untrusted_gate_executes +test_architecture_graph_export.ArchitectureGraphExportContract.test_graph_identity_is_checked_before_gate_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_missing_required_dependency_is_rejected_by_pinned_hard_gate +test_architecture_graph_export.ArchitectureGraphExportContract.test_pinned_49_node_55_edge_export_is_deterministic_and_preserves_records +test_architecture_graph_export.ArchitectureGraphExportContract.test_required_dependency_cycle_is_rejected_by_pinned_hard_gate +test_architecture_graph_export.ArchitectureGraphExportContract.test_same_size_gate_change_is_rejected_by_sha_before_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_source_byte_counts_are_validated_even_when_sha_matches +test_architecture_graph_export.ArchitectureGraphExportContract.test_symlink_source_directory_is_rejected +test_architecture_graph_export.ArchitectureGraphExportContract.test_symlink_source_files_are_rejected_before_gate_import +test_architecture_receipt.ArchitectureReceiptContract.test_attempt_ten_and_other_native_repository_are_preserved +test_architecture_receipt.ArchitectureReceiptContract.test_changed_node_or_edge_is_refused_even_when_graph_source_pins_are_unchanged +test_architecture_receipt.ArchitectureReceiptContract.test_directory_or_fifo_member_is_refused_without_waiting_for_a_writer +test_architecture_receipt.ArchitectureReceiptContract.test_duplicate_json_keys_are_refused_even_when_last_value_is_valid +test_architecture_receipt.ArchitectureReceiptContract.test_duplicate_test_identity_or_multiple_runs_are_refused +test_architecture_receipt.ArchitectureReceiptContract.test_each_expected_host_identity_argument_is_used_and_required +test_architecture_receipt.ArchitectureReceiptContract.test_each_native_identity_file_must_match_current_host_arguments +test_architecture_receipt.ArchitectureReceiptContract.test_each_required_sandbox_control_rejects_skipped_failed_or_error_status +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_is_required +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_must_be_regular_and_cannot_be_a_symlink +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_over_16_mib_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_empty_or_zero_discovered_test_logs_are_refused_in_each_mode +test_architecture_receipt.ArchitectureReceiptContract.test_every_graph_source_pin_is_required_and_cannot_be_substituted +test_architecture_receipt.ArchitectureReceiptContract.test_every_required_identity_is_needed_even_when_53_other_tests_are_ok +test_architecture_receipt.ArchitectureReceiptContract.test_evidence_directory_or_generated_parent_symlink_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_graph_and_runtime_inspect_require_strict_json +test_architecture_receipt.ArchitectureReceiptContract.test_graph_root_schema_and_non_scientific_flags_are_exact +test_architecture_receipt.ArchitectureReceiptContract.test_invalid_matching_host_identity_does_not_authorize_bad_commit_or_decimal +test_architecture_receipt.ArchitectureReceiptContract.test_later_all_ok_test_methods_are_counted_and_bound +test_architecture_receipt.ArchitectureReceiptContract.test_malformed_extra_result_lines_cannot_be_ignored_before_the_success_footer +test_architecture_receipt.ArchitectureReceiptContract.test_malformed_foreign_or_shortname_mismatched_test_identities_are_refused +test_architecture_receipt.ArchitectureReceiptContract.test_missing_extra_or_forged_dimension_cannot_manufacture_scientific_evidence +test_architecture_receipt.ArchitectureReceiptContract.test_native_repository_argument_must_have_owner_and_repository_components +test_architecture_receipt.ArchitectureReceiptContract.test_nested_record_types_cannot_change_even_when_python_values_compare_equal +test_architecture_receipt.ArchitectureReceiptContract.test_non_finite_numbers_are_refused_even_in_additional_inspect_fields +test_architecture_receipt.ArchitectureReceiptContract.test_nonunderscore_unittest_names_are_accepted_as_additional_controls +test_architecture_receipt.ArchitectureReceiptContract.test_nonunderscore_unittest_prefix_malformed_results_are_not_diagnostics +test_architecture_receipt.ArchitectureReceiptContract.test_nonzero_or_malformed_container_exit_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_normal_and_optimized_must_discover_the_same_additional_passing_tests +test_architecture_receipt.ArchitectureReceiptContract.test_runtime_inspect_is_one_linux_amd64_image_with_the_fixed_digest +test_architecture_receipt.ArchitectureReceiptContract.test_skip_or_failure_in_additional_discovered_test_is_also_refused +test_architecture_receipt.ArchitectureReceiptContract.test_summary_must_report_exact_discovered_count_and_terminal_ok +test_architecture_receipt.ArchitectureReceiptContract.test_valid_53_test_receipt_is_exact_deterministic_and_preserves_staged_bytes +test_architecture_receipt.ArchitectureReceiptContract.test_valid_inspected_image_id_is_preserved_instead_of_hardcoded +test_architecture_receipt.ArchitectureReceiptContract.test_valid_log_at_exact_16_mib_limit_is_accepted +test_architecture_receipt.ArchitectureReceiptContract.test_valid_log_larger_than_64_kib_is_accepted_and_raw_bytes_are_hashed +test_architecture_sandbox.ArchitectureSandbox.test_kernel_enforces_cpu_memory_and_process_limits +test_architecture_sandbox.ArchitectureSandbox.test_network_namespace_has_no_external_interface_or_route +test_architecture_sandbox.ArchitectureSandbox.test_nonroot_and_no_new_privileges +test_architecture_sandbox.ArchitectureSandbox.test_output_quota_stops_disk_exhaustion_and_cleanup_recovers_space +test_architecture_sandbox.ArchitectureSandbox.test_source_mount_is_read_only_and_environment_has_no_publishing_secret +""".split()) + + +def _require(condition: object) -> None: + if not condition: + raise ValueError("architecture graph readback refused") + + +def _sha(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def _canonical(value: Any) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False) + + +def _encoded(value: Any) -> bytes: + return (_canonical(value) + "\n").encode("ascii") + + +def _object(value: Any, fields: set[str] | frozenset[str]) -> dict: + _require(type(value) is dict and set(value) == fields) + return value + + +def _science(document: dict) -> None: + _require(type(document["schema_version"]) is int and document["schema_version"] == 1) + _require(document["scientific_effect"] == "NONE" and document["scientific_status_authority"] is False) + + +def _json_types(value: Any, depth: int = 0) -> None: + kind = type(value) + if kind in (dict, list): + depth += 1 + _require(depth <= MAX_DEPTH) + if kind is dict: + for key, child in value.items(): + _require(type(key) is str) + key.encode("utf-8") + _json_types(child, depth) + else: + for child in value: + _json_types(child, depth) + elif kind is str: + value.encode("utf-8") + elif kind is float: + _require(math.isfinite(value)) + else: + _require(value is None or kind in (bool, int)) + + +def _json(raw: bytes) -> Any: + def unique(pairs): + value = {} + for key, child in pairs: + _require(key not in value) + value[key] = child + return value + + def no_constant(_value): + raise ValueError("architecture graph readback refused") + + def finite_float(value): + number = float(value) + _require(math.isfinite(number)) + return number + + try: + value = json.loads(raw.decode("utf-8"), object_pairs_hook=unique, + parse_constant=no_constant, parse_float=finite_float) + _json_types(value) + return value + except (UnicodeError, RecursionError, OverflowError) as exc: + raise ValueError("architecture graph readback refused") from exc + + +def validate_expected(expected: dict[str, str]) -> dict[str, str]: + _object(expected, BINDING_KEYS) + _require(all(type(value) is str for value in expected.values())) + _require(sum(len(value) for value in expected.values()) <= OUTPUT_LIMIT) + for field, pattern in ( + ("checked_commit", HEX40), ("repository", REPOSITORY), + ("run_id", POSITIVE_DECIMAL), ("run_attempt", POSITIVE_DECIMAL), + ("receipt_sha256", HEX64), ("artifact_id", POSITIVE_DECIMAL), + ("artifact_sha256", HEX64), + ): + _require(pattern.fullmatch(expected[field]) is not None) + _require(all(part not in (".", "..") for part in expected["repository"].split("/"))) + _require(expected["artifact_name"] == "research-architecture-" + expected["run_id"] + "-" + expected["run_attempt"]) + return dict(expected) + + +def _fields(layout: str, raw: bytes, offset: int, boundary: int) -> tuple: + _require(0 <= offset and offset + struct.calcsize(layout) <= boundary <= len(raw)) + return struct.unpack_from(layout, raw, offset) + + +def _archive_geometry(raw: bytes) -> list[dict[str, Any]]: + """Inspect the complete closed inventory and every span before inflation.""" + footer_start = len(raw) - 22 + footer = _fields(" 0) + name_bytes = raw[position + 46:record_end] + try: + name = name_bytes.decode("ascii") + except UnicodeError as exc: + raise ValueError("architecture graph readback refused") from exc + _require(name.encode("ascii") == name_bytes and name in MEMBER_LIMITS) + _require(name not in names and name.casefold() not in folded) + names.add(name) + folded.add(name.casefold()) + file_type = stat.S_IFMT(attributes >> 16) + _require(file_type in (0, stat.S_IFREG) and not attributes & (0x08 | 0x10 | 0x40)) + if file_type == 0: + _require(creator >> 8 == 0) + _require(expanded <= MEMBER_LIMITS[name]) + declared_total += expanded + _require(declared_total <= INFLATED_LIMIT) + rows.append({"name": name, "name_bytes": name_bytes, "needed": needed, + "flags": flags, "method": method, "crc": crc, "compressed": compressed, + "expanded": expanded, "local_offset": local_offset}) + position = record_end + _require(position == footer_start and names == set(MEMBER_LIMITS)) + rows.sort(key=lambda row: row["local_offset"]) + cursor = 0 + for index, row in enumerate(rows): + _require(row["local_offset"] == cursor) + local = _fields(" bytes: + limit = min(MEMBER_LIMITS[row["name"]], remaining) + _require(row["expanded"] <= limit) + parts = [] + total = 0 + crc = 0 + decoder = zlib.decompressobj(-15) if row["method"] == 8 else None + start, end = row["data_start"], row["data_end"] + for position in range(start, end, 64 * 1024): + block = memoryview(raw)[position:min(position + 64 * 1024, end)] + if decoder is None: + part = bytes(block) + else: + try: + part = decoder.decompress(block, min(limit, row["expanded"]) - total + 1) + except zlib.error as exc: + raise ValueError("architecture graph readback refused") from exc + _require(not decoder.unconsumed_tail and not decoder.unused_data) + total += len(part) + _require(total <= limit and total <= row["expanded"]) + parts.append(part) + crc = zlib.crc32(part, crc) + if decoder is not None and decoder.eof: + _require(position + len(block) == end) + if decoder is not None: + _require(decoder.eof and not decoder.unused_data and not decoder.unconsumed_tail) + _require(total == row["expanded"] and crc & 0xFFFFFFFF == row["crc"]) + return b"".join(parts) + + +def _members(raw: bytes) -> dict[str, bytes]: + rows = _archive_geometry(raw) + members = {} + total = 0 + for row in rows: + member = _member(raw, row, INFLATED_LIMIT - total) + total += len(member) + _require(total <= INFLATED_LIMIT) + members[row["name"]] = member + return members + + +def _manifest(members: dict[str, bytes]) -> None: + raw = members["ARTIFACT_SHA256SUMS"] + _require(raw.endswith(b"\n")) + lines = raw[:-1].split(b"\n") + _require(len(lines) == 12) + expected_names = set(MEMBER_LIMITS) - {"ARTIFACT_SHA256SUMS"} + seen = set() + for line in lines: + _require(re.fullmatch(rb"[0-9a-f]{64} [A-Za-z0-9_./-]+", line) is not None) + digest, name_bytes = line.split(b" ", 1) + name = name_bytes.decode("ascii") + _require(name in expected_names and name not in seen) + _require(digest.decode("ascii") == _sha(members[name])) + seen.add(name) + _require(seen == expected_names) + + +def _source_graph(raw: bytes) -> dict: + _require(type(raw) is bytes and len(raw) == SOURCE_BYTES and _sha(raw) == GRAPH_SOURCE["graph_sha256"]) + source = _json(raw) + _require(type(source) is dict and type(source.get("nodes")) is dict and len(source["nodes"]) == 49) + _require(type(source.get("edges")) is list and len(source["edges"]) == 55) + return source + + +def _graph(raw: bytes, source: dict) -> dict: + graph = _object(_json(raw), { + "schema_version", "source", "nodes", "edges", "dimensions", + "scientific_effect", "scientific_status_authority", + }) + _science(graph) + _require(_canonical(graph["source"]) == _canonical(GRAPH_SOURCE)) + for field in ("nodes", "edges"): + _require(_canonical(graph[field]) == _canonical(source[field])) + dimensions = { + identity: { + "source": "recorded" if node.get("source") else "not_recorded", + "review": "recorded" if any(node.get(field) for field in REVIEW_FIELDS) else "not_recorded", + "kernel": "not_recorded", "computation": "not_recorded", "alignment": "not_recorded", + } for identity, node in source["nodes"].items() + } + _require(_canonical(graph["dimensions"]) == _canonical(dimensions)) + return graph + + +def _image(raw: bytes) -> dict[str, str]: + inspected = _json(raw) + _require(type(inspected) is list and len(inspected) == 1 and type(inspected[0]) is dict) + image = inspected[0] + identity = image.get("Id") + _require(type(identity) is str and IMAGE_ID.fullmatch(identity) is not None) + _require(_canonical(image.get("RepoDigests")) == _canonical([IMAGE_DIGEST])) + _require(image.get("Os") == "linux" and image.get("Architecture") == "amd64") + return {"id": identity, "repo_digest": IMAGE_DIGEST} + + +def _log(raw: bytes, mode: str) -> dict[str, Any]: + text = raw.decode("utf-8") + _require(text.endswith("\n") and "\r" not in text and "\x00" not in text) + lines = text.split("\n") + nonempty = [line for line in lines if line.strip()] + _require(bool(nonempty) and nonempty[-1] == "OK" and lines.count("OK") == 1) + identities = set() + footer_index = None + reported_count = None + last_result = -1 + for index, line in enumerate(lines): + if line.lstrip().casefold().startswith("ran "): + summary = TEST_SUMMARY.fullmatch(line) + _require(summary is not None and footer_index is None) + reported_count = int(summary.group(1)) + duration = float(summary.group(2)) + _require(math.isfinite(duration) and duration >= 0 and reported_count <= 4096) + footer_index = index + elif " ... " in line or TEST_RESULT_LIKE.search(line): + result = TEST_RESULT.fullmatch(line) + _require(result is not None) + short, identity, status = result.groups() + _require(TEST_ID.fullmatch(identity) is not None and short == identity.rsplit(".", 1)[1]) + _require(status == "ok" and identity not in identities and len(identities) < 4096) + identities.add(identity) + last_result = index + elif (re.match(r"(?:FAIL|ERROR|FAILED)(?:$|[:\s(])", line.lstrip(), re.IGNORECASE) + or line.lstrip().upper().startswith("OK (")): + raise ValueError("architecture graph readback refused") + _require(footer_index is not None and last_result < footer_index) + _require(REQUIRED_IDS <= identities and reported_count == len(identities)) + return {"mode": mode, "test_count": len(identities), "test_ids": sorted(identities)} + + +def _receipt(members: dict[str, bytes], expected: dict[str, str], image: dict[str, str]) -> dict: + raw = members["architecture-run-receipt.json"] + _require(_sha(raw) == expected["receipt_sha256"]) + receipt = _object(_json(raw), { + "schema_version", "scientific_effect", "scientific_status_authority", "checked_commit", + "repository", "run_id", "run_attempt", "runtime_image", "test_modes", "graph_source", + "evidence_file_sha256", + }) + _science(receipt) + for field in ("checked_commit", "repository", "run_id", "run_attempt"): + _require(type(receipt[field]) is str and receipt[field] == expected[field]) + _require(_canonical(receipt["runtime_image"]) == _canonical(image)) + _require(_canonical(receipt["graph_source"]) == _canonical(GRAPH_SOURCE)) + hashes = _object(receipt["evidence_file_sha256"], EVIDENCE_MEMBERS) + for name, digest in hashes.items(): + _require(type(digest) is str and HEX64.fullmatch(digest) is not None and digest == _sha(members[name])) + modes = [_log(members["tests-" + mode + ".log"], mode) for mode in ("normal", "optimized")] + _require(modes[0]["test_ids"] == modes[1]["test_ids"]) + _require(_canonical(receipt["test_modes"]) == _canonical(modes)) + return receipt + + +def read_graph_archive(raw_zip: bytes, expected: dict[str, str], source_graph_bytes: bytes) -> dict: + """Compare declared pins and preserve validated original records; perform no I/O.""" + binding = validate_expected(expected) + _require(type(raw_zip) is bytes and 22 <= len(raw_zip) <= RAW_LIMIT) + _require(_sha(raw_zip) == binding["artifact_sha256"]) + source = _source_graph(source_graph_bytes) + members = _members(raw_zip) + _manifest(members) + for name, field in ( + ("checked-commit.txt", "checked_commit"), ("run-id.txt", "run_id"), + ("run-attempt.txt", "run_attempt"), + ): + _require(members[name] == (binding[field] + "\n").encode("ascii")) + _require(members["container-exit-status.txt"] == b"0\n") + _require(members["exporter.stderr"] == b"" and members["exporter.stdout"] == b"/output/generated/graph.json\n") + graph = _graph(members["generated/graph.json"], source) + producer = _receipt(members, binding, _image(members["runtime-image.json"])) + metadata = {"schema_version": 1, "scientific_effect": "NONE", + "scientific_status_authority": False, "custody": "unknown"} + unit = { + **metadata, "graph": graph, + "receipt": { + **metadata, "verification_scope": "declared-native-pins-and-archive-content", + "binding": binding, + "archive": { + "sha256": _sha(raw_zip), "bytes": len(raw_zip), + "members": [{"path": name, "bytes": len(members[name]), "sha256": _sha(members[name])} + for name in sorted(members)], + }, + "graph_member": {"path": "generated/graph.json", "bytes": len(members["generated/graph.json"]), + "sha256": _sha(members["generated/graph.json"])}, + "producer_receipt_sha256": _sha(members["architecture-run-receipt.json"]), + "producer_receipt": producer, + }, + } + _require(len(_encoded(unit)) <= OUTPUT_LIMIT) + return unit diff --git a/architecture/public-artifact-index-bindings.json b/architecture/public-artifact-index-bindings.json new file mode 100644 index 000000000..fe51488c7 --- /dev/null +++ b/architecture/public-artifact-index-bindings.json @@ -0,0 +1 @@ +[] diff --git a/architecture/public-artifact-index-policy.json b/architecture/public-artifact-index-policy.json new file mode 100644 index 000000000..3dcf1909c --- /dev/null +++ b/architecture/public-artifact-index-policy.json @@ -0,0 +1,18 @@ +{ + "schema_version": 1, + "artifact_ids": [ + "C151-MAIN285-LOG-BINDINGS-20261007-v1", + "C152-MAIN286-FACTUAL-REVIEW-20261007-v1", + "C215-GRAPH-ALIAS-AUDIT-20261008-v1" + ], + "columns": [ + "Artifact ID", + "Title", + "Org", + "Class", + "Status", + "Authority / canonical impact", + "Dependencies or supersession", + "Modified UTC" + ] +} diff --git a/docs/plans/2026-10-08-architecture-interface.md b/docs/plans/2026-10-08-architecture-interface.md new file mode 100644 index 000000000..aad7b7d2f --- /dev/null +++ b/docs/plans/2026-10-08-architecture-interface.md @@ -0,0 +1,99 @@ +# Architecture Interface and Canvas Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish an additive Astro reader of pinned graph and explicitly public evidence inputs, with source-bound cubic and finite elder teaching views and a verifiable immutable build identity. + +**Architecture:** A trusted capture/projection stage produces a public-only input bundle. A hosted offline static build consumes that bundle once; browser tests and publication consume the same assembled output artifact. Original graph and native evidence records retain their distinct scope and authority; the reader is a derived view. + +**Tech Stack:** Stdlib Python capture/projection controls; exact Astro/Node/npm candidates and lockfile after hosted verification; vanilla ES modules and Canvas; existing hosted Chrome tooling; GitHub Pages. + +**Spec:** [Research architecture design, sections 1/4 and 7](../superpowers/specs/2026-10-08-research-architecture-design.md); [global implementation plan, Tasks 3 and 7](../superpowers/plans/2026-10-08-research-architecture-plan.md); [evidence adapter contract](2026-10-08-architecture-evidence-adapter.md). + +## Global Constraints + +- Scientific effect is always `NONE`; scientific status authority is exact boolean `false`. Build success, recorded review, source-relative consistency and teaching output do not promote science. +- Preserve source, review, kernel, computation and alignment as five separate axes. Preserve original classifications, records, qualifiers and unknown/stale/unavailable states. The stdin evidence adapter's custody remains `unknown`; the reader cannot convert its `current` applicability into authenticated kernel verification or declaration existence. +- The public graph may display all 49 currently pinned nodes without any guessed Artifact Index or Lean join. Artifact/node/teaching joins require exact reviewed IDs and roles; absence remains unresolved. +- Keep the complete private Artifact Index/source capture outside the public repository and publication bundle. Publish only explicitly allowed rows and columns. No private original packet, raw table, credential or unlisted field is copied into generated assets, logs or failure artifacts. +- Root retained the actual R17 Artifact Index capture and froze its observed metadata, column names, record schema, size limits and public projection boundary in the first-unit contract. Synthetic TEST fixtures exercise that boundary; they do not substitute for the retained real capture or actual public projection delivery. +- Root owns the isolated interface branch and the precise first-unit and runtime-preparation paths recorded in the pickup and amendment. Tests precede implementation, with hosted RED required before product authoring. The earlier source-only checkpoint is superseded operationally by this scoped execution claim. Standing owner authorization creates no additional user-permission checkpoint. +- No project execution or installation on the laptop. Preparation, RED/GREEN, two clean builds and browser execution occur in disposable hosted environments; network is denied during the actual build/test stage. +- PR #317 owns shared site/brand styles, release-key changes and the existing browser harness/tests. Existing `docs/site` generators and release inventory stay owner-controlled until reconciliation after that PR lands. Root currently owns the architecture producer/required-check workflows. +- New public routes use `/main/reader/`. Preserve the complete existing `/docs` publication namespace and `/main/site/*.html` routes. Pages Actions migration and navigation integration require explicit technical coordination with those owners, not renewed owner permission. +- True 500-token retrieval, lease services, routing and broader execution migration remain separate increments. All eight original requirements remain active until their actual end-to-end completion evidence exists. + +## Review Focus + +- A public-column whitelist applied after serializing the full private capture still leaks data; Task 1 must prove private canaries never enter any output or retained failure artifact. +- Matching caller-provided digests do not establish archive custody; Task 1 must bind original ZIP bytes to the native producer outputs and reject contradictory/stale archive identities. +- Generated graph schema differs from the old source viewer schema; Task 2 must consume the exporter directly without inventing `terminal_classifications` or other missing semantics. +- A disjoint-set parent is not the mathematical elder, and neutral events are not deaths; Task 3 must independently verify those distinctions and interval endpoints. +- A green build rebuilt during deployment can publish different inputs; Task 4 must deploy and read back the exact tested output artifact while preserving legacy URLs. + +--- + +## Task 1: Private capture, public projection and native graph custody + +**Create:** `architecture/artifact_index.py`, `tools/artifact_index_export.py`, `tools/interface_artifact_readback.py`, `tools/interface_build_inputs.py`, `tests/test_architecture_artifact_index.py`, `tests/test_interface_artifact_readback.py`, `tests/test_interface_build_inputs.py`. + +**Interfaces:** `project_artifact_index(packet, expected) -> public_projection` follows the [frozen first-unit contract](2026-10-08-artifact-index-contract.md). The packet carries the native capture, explicit public policy, exact bindings and graph-node inventory; `expected` carries five independently supplied identities. The first CLI reads one packet from stdin and writes one admitted canonical public JSON object to stdout. It opens no caller-selected files, performs no network access, and uses the exact opaque refusal line `ARTIFACT_INDEX_REFUSED` on stderr. Policy selects explicit stable artifact IDs and the fixed eight public columns; bindings contain exactly `{node_id, artifact_id, roles}`. Roles are explicit source/review/kernel/computation/alignment/teaching-source associations, never acceptance. A later separately tested trusted collector reads the retained outside-repository private capture and writes the admitted stdout projection to `public-index.json`; its file and archive custody is outside this first CLI unit. + +`interface_artifact_readback.py` consumes a previously downloaded original ZIP plus expected native repository/checked-commit/run/attempt/artifact-ID/name/archive-digest/host-receipt-digest arguments. Expectations come from the current trusted producer job, not ZIP members. It produces a bounded regular-file staging directory and an original-byte readback receipt. `interface_build_inputs.py` consumes that staged graph, public projection, allowed evidence/teaching/brand inputs and renderer identities and emits the public bundle below. + +- [ ] Root freezes the real capture's metadata and parser ABI, exact public row/column allowlist, explicit ID joins, positive public fixture and refusal fixture. Preserve the full private capture in its existing private evidence location; record only its identity/digest publicly when allowed. +- [ ] Write external CLI controls: valid public projection; absent mapping leaves unresolved; unknown/duplicate IDs and contradictory joins refuse; unlisted columns/rows and private canaries remain absent from all public outputs and failure artifacts; mutable references stay labeled; raw capture/policy/binding hash changes refuse. Observe hosted RED before implementation. +- [ ] Write custody controls: native archive identity/digest mismatch; wrong attempt/checked commit/receipt; duplicate ZIP members; absolute/traversing paths; symlink/special members; expanded-size limits; changed graph member; failed or missing producer evidence. Preserve original failure evidence without producing an accepted input bundle. +- [ ] Implement format-specific projection and archive verification without fetching caller-selected paths/URLs or executing captured code. Validate generated graph's exact seven-key schema and source identity, and match its raw hash to the original host receipt. Keep its dated Math source commit separate from the current executing main commit. +- [ ] Run hosted normal/optimized Python controls, inspect public/failure artifacts for the private canaries, and obtain nonauthor source/custody review. Freeze this unit through root before the renderer consumes it. + +**Public bundle boundary:** `inputs.json` has schema version, exact non-scientific fields, checked repository/commit, graph producer binding and raw graph/receipt identities, public index capture/projection/policy/binding identities, public evidence projection identity, teaching/brand file identities, renderer source/lockfile/runtime identities. Each member is an explicitly listed regular file with byte count and SHA-256. The exact public-index record fields are frozen in the first-unit contract before test/code authoring. The full private capture and evidence adapter `original_packet` are never bundle members. + +## Task 2: Offline Astro proof pages and immutable build receipt + +**Create:** `interface/package.json`, `interface/package-lock.json`, `interface/astro.config.mjs`, `interface/src/lib/load-inputs.mjs`, `interface/src/layouts/Reader.astro`, `interface/src/components/EvidenceDimensions.astro`, `interface/src/components/DependencyTable.astro`, `interface/src/pages/index.astro`, `interface/src/pages/nodes/[id].astro`, `interface/tests/inputs.test.mjs`, `interface/tests/pages.test.mjs`, `tools/interface_build_receipt.py`, `tests/test_interface_build_receipt.py`, `.github/workflows/research-interface.yml`. + +**Interfaces:** `loadPublicInputs(inputDirectory, expectedManifestSha256) -> admittedInputs` reads only the already admitted public bundle; it performs no network fetch. `nodePageData(nodeId, admittedInputs) -> pageData` retains exact original node/edge data and explicitly joined evidence, including unresolved joins. `interface_build_receipt.py` binds the identity core and complete sorted output-file inventory. + +- [ ] In a root-owned trusted hosted preparation stage, verify candidates Astro `7.3.8`, Node `24.21.0` and npm `11.19.0`; retain exact package/runtime bytes and identities, install from a committed integrity-bearing lockfile, and test compatibility. These versions were observed during reconnaissance and are candidates until preparation verifies them. Use no new theme, external fonts, client framework or graph library in this bounded unit. +- [ ] Write controls for missing/tampered bundle members, duplicate/nonfinite/incorrectly typed JSON, mixed build inputs, escaped IDs/URLs, unknown roles, all graph nodes without joins, unavailable evidence and native metadata preservation. Observe hosted RED; a setup/import failure or zero discovery is not RED evidence. +- [ ] Implement static routes with `site: 'https://d6g8k5htny-coder.github.io'`, `base: '/main/reader'`, `output: 'static'`, and `build.inlineStylesheets: 'never'`. Keep scripts/styles as local external assets under the existing self-only CSP. Reuse an owner-approved, byte-pinned brand capture without modifying shared brand/release files. +- [ ] Render an accessible dependency table/view and one page per exact graph ID. Display original classification as a recorded source label; render all five evidence axes independently with custody and as-of identity. Escape retained text; never execute embedded HTML/code or synthesize formal mappings from titles/classifications. +- [ ] Define `build_id = SHA256(canonical identity_core)` binding checked commit, graph/projection/evidence/teaching/brand input hashes, source tree, lockfile and runtime identity. Exclude the build ID itself and fresh wall-clock timestamps. Emit `build-receipt.json` with that identity and sorted output path/byte/SHA inventory; external archive identity binds the receipt itself. Every page links to its build receipt. +- [ ] From two clean hosted work directories, with network denied and identical admitted inputs, run `npm ci --offline` then `npm run build` using the prepared dependency cache. Compare complete output membership and bytes; require identical build IDs and no private canary. Retain both manifests/logs and the single tested output archive. Review source and byte identities before freezing. + +## Task 3: Source-bound cubic and finite elder Canvas views + +**Create:** `interface/src/pages/teach/cubic.astro`, `interface/src/pages/teach/elder.astro`, `interface/src/scripts/cubic-model.mjs`, `interface/src/scripts/elder-model.mjs`, `interface/src/scripts/canvas-reader.mjs`, `interface/tests/cubic.test.mjs`, `interface/tests/elder.test.mjs`, `interface/tests/reader-controls.test.mjs`. Public teaching fixtures enter only through Task 1's explicit source policy. + +**Interfaces:** `cubicModel({r, k, b}) -> scales` implements the pinned educational `gap = k*r^3` definition. `finiteElderTrace({values, n, scale}) -> trace` returns finite-filtration threshold/component/elder/merge/neutral events and positive/zero/essential intervals. `renderTeachingView(host, admittedTeachingData)` connects the same data to Canvas and equivalent DOM tables/controls. Source-node links exist only through explicit admitted mappings. + +- [ ] Pin source bytes for the current cubic definition and `experiments/periodic_h0/{EXACT_H0.md,exact_h0.py,test_exact_h0.py}`. Keep their finite/educational scope and source dates visible. No synthetic source identity, continuum sample, Lean declaration or source-to-component certificate is invented. +- [ ] Write independent controls for positive cubic scales, halving `r` dividing gap by eight, normalization and nonrepresentable/invalid values. Write finite elder controls for `[5,1,4,2,3,0]` repeated six times: positive pairs `[[3,2],[4,1]]`, essential birth `5`, zero count `33`; plateau ties prefer the smaller row-major vertex ID. Include periodic seam and no-diagonal controls and a BFS connectivity oracle distinct from the sweep. +- [ ] Pin superlevel `(d/S,b/S]`, lifetime `(b-d)/S`, zero pairs excluded from the positive diagram, essential class separate, and no longest finite bar removal. Test that disjoint-set parents cannot overrule mathematical elders and same-class edges create neutral events rather than deaths. Missing mappings/certificates render unresolved, never a positive source claim. Observe hosted RED before implementing. +- [ ] Implement exact model/event data separately from approximate drawing coordinates. Label the cubic arithmetic as teaching; label the elder scene as a specified finite periodic filtration. Keep immediate mathematical merge relation, global class equality, essential bars and finite conventions distinct from data-structure pointers. +- [ ] Implement keyboard-operable ordinary DOM controls, event/value tables and source disclosures usable without Canvas. Respect reduced motion; avoid mandatory animation. Source absence/tamper leaves readable unavailable state with no inferred result. Run model and DOM controls hosted and obtain mathematical-scope/source review. + +## Task 4: Browser verification, existing namespace preservation and exact-artifact deployment + +**Create:** `tools/interface_browser_check.py`, `tests/test_interface_browser_contract.py`; extend only the new `research-interface.yml` under root's claim. **Later coordinated paths:** assembled Pages workflow/configuration and existing navigation/docs; exact modifications follow PR #317 integration and a refreshed owner claim. + +**Interfaces:** The browser harness serves Task 2/3's assembled artifact, not a fresh build. Publication consumes the same artifact ID/digest and build receipt accepted by tests. The assembled Pages root includes the complete verified existing `/docs` namespace plus the additive `reader/` subtree. + +- [ ] Test the actual built pages in hosted Chrome at desktop/mobile widths, light/dark schemes, keyboard-only interaction, reduced motion, Canvas unavailable, source/index refusal and no-JavaScript DOM fallback. Check graph-node routes, five-axis qualifiers, focus/overflow and source/build identities; retain exact runtime/report/screenshots and failures. Existing PR #317 browser findings remain separate work to reconcile. +- [ ] Freeze a preservation manifest for every legacy `/docs` file/route from the actual integrated source. Assemble that namespace unchanged with the new reader. Detect output collisions and missing legacy routes before publication; exercise existing reader links and release checks under their owner-controlled hosted lane. +- [ ] After PR #317 and current producer scopes are reconciled, root claims the precise navigation and Pages migration paths. Switch to GitHub Actions publication only through available authorized administration capability and retained readback. This is technical integration work, not another owner approval queue. +- [ ] Use reviewed, commit-pinned Pages upload/deploy actions and scoped publication credentials only in the trusted host stage. Deploy the verified assembled artifact; do not rerun Astro, regenerate graph/index data or fetch mutable cloud inputs during deployment. Keep a recoverable prior artifact and source/configuration record. +- [ ] Read back live build receipt, representative graph/source/teaching pages and asset bytes; verify their hashes/build ID and actual reader interactions. Retain native deployment identity and exact source-bound receipts in existing governance surfaces. Mark this increment deployed only after that evidence exists. + +## Handoff and Completion Boundary + +The initial source-only checkpoint is preserved in the root checkpoint below; its operational wait is superseded by root's retained capture, frozen contract and scoped execution claim. First-unit tests are now authored and under nonauthor review; hosted RED and product implementation remain pending. The build does not authenticate mathematical records, establish unknown declarations, repair missing mappings or complete semantic retrieval. Goals 1 and 7 require tested deployed pages/Canvas; the every-PR producer/SSG join and all remaining original goals retain their explicit incomplete scope until separately verified. + +Primary references: [Astro Pages deployment](https://docs.astro.build/en/guides/deploy/github/), [Astro configuration](https://docs.astro.build/en/reference/configuration-reference/), [Astro package metadata](https://registry.npmjs.org/astro/latest), [Node distribution metadata](https://nodejs.org/dist/index.json), [GitHub publishing-source configuration](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site). Package aliases/docs are mutable research inputs; exact preparation receipts and the committed lockfile govern execution. + +## Root capture and first-unit checkpoint + +The source-only handoff above is the earlier checkpoint. Root now owns the new isolated interface worktree/branch based main879fb4f5 under pickup6070431864, with exact initial path scope published on main307. The actual native formattedValue capture is9273 UTF-8 bytes, SHA25603baf67b92c7b31390fd497431abf120204a6ccbde7d8b00395d1d40288f6ebf, captured22:18:57UTC8October; full private bytes remain outside Git. The [frozen first-unit contract](2026-10-08-artifact-index-contract.md) records its observed12-column header, worksheet200004, three ranges, typed packet, five independent pins and exact public output. The explicit policy selects three dated rows and eight public columns; actual bindings are empty. No title-based graph/Lean join is inferred. + +The first CLI is stdout-only with stdin private bytes and fixed refusal codes. A later trusted collector writes its admitted public JSON to the bundle; that later wrapper's filesystem/archive custody is separate work. External tests use synthetic TEST capture data and private canaries and precede implementation. Hosted absent-CLI RED, implementation, normal/optimized GREEN and nonauthor privacy review remain pending. Existing namespace, Astro/model/browser and exact-artifact deployment work remains unchanged in scope and unfinished. diff --git a/docs/plans/2026-10-08-artifact-index-contract.md b/docs/plans/2026-10-08-artifact-index-contract.md new file mode 100644 index 000000000..c1e95087a --- /dev/null +++ b/docs/plans/2026-10-08-artifact-index-contract.md @@ -0,0 +1,47 @@ +# Artifact Index public projection v1 contract + +Root owns this new boundary under main307 pickup6070431864 through00:00UTC9October. Base main879fb4f5. This is the first unit of the architecture interface plan. Standing owner authorization applies; no additional permission checkpoint is introduced. Product execution, RED/GREEN and installation occur only in disposable hosted environments. Full private captures stay outside Git and publication. + +## Actual source and public policy + +The real R17 source is spreadsheet `1O6x8ivmaVUxYqKmCOXmToIHpMXqDI362ibqBl8HY8no`, worksheet ID200004, title `Artifact Index`, native12-column formatted-cell representation. The2026-10-08T22:18:57Z response capture is9273 UTF-8 bytes with SHA256 `03baf67b92c7b31390fd497431abf120204a6ccbde7d8b00395d1d40288f6ebf`. This is serialized native API formattedValue data, not raw Sheet-file bytes or an export. It has one sheet and three grid-data ranges: row0 header, rows935–936, and row1000, all starting at column0. Capture ranges are `'Artifact Index'!A1:L1`, `'Artifact Index'!A936:L937`, and `'Artifact Index'!A1001:L1001`, cell_fields `formattedValue`. No native Sheet write is part of this unit. + +Header exact order: Artifact ID; Title; Org; Class; Priority; Topics / object tags; Status; Authority / canonical impact; Dependencies or supersession; Modified UTC; Source; Notes. + +Public column policy exact order: Artifact ID; Title; Org; Class; Status; Authority / canonical impact; Dependencies or supersession; Modified UTC. Only these eight native values may appear in public row fields. Source and Notes are prohibited even under a caller-forged policy; Priority and Topics are excluded. Do not serialize the original capture into any output or failure artifact. The real policy selects only C151-MAIN285-LOG-BINDINGS-20261007-v1, C152-MAIN286-FACTUAL-REVIEW-20261007-v1, C215-GRAPH-ALIAS-AUDIT-20261008-v1. These are dated source records with their original negative/scope/independence labels, not current acceptance. Actual initial bindings are empty because there is no exact verified Artifact Index-to-graph mapping. All49 graph IDs remain unmapped. Synthetic tests use TEST IDs, synthetic spreadsheet IDs, and private canaries distinct from the real source; synthetic fixtures are not delivery evidence. + +## CLI and typed packet + +`python -B -S tools/artifact_index_export.py` reads one strict JSON document from stdin, emits exactly one public JSON document on stdout on success, and exits0. On every refusal, including CLI arguments, it exits nonzero, emits no stdout and exactly the public opaque ASCII constant `ARTIFACT_INDEX_REFUSED` plus one newline on stderr. Private values, base64, cells, paths and exception text cannot appear in diagnostics. It performs no network fetch, URL/path read, imports of captured code or filesystem writes. Module API is `project_artifact_index(packet, expected) -> projection`. The output is a projection, not an authenticated table or current status claim. + +All five independent flags are required exactly once, with lower-case64-hex values: `--expected-capture-sha256`, `--expected-metadata-sha256`, `--expected-policy-sha256`, `--expected-bindings-sha256`, `--expected-graph-nodes-sha256`. No abbreviation or duplicate arguments. Expectations are supplied from trusted root capture/policy inputs, not inferred from stdin. They prove consistency with declared pins only, not external custody. Canonical identity JSON uses sorted object keys, compact separators, ensure_ascii=true and allow_nan=false, preserving typed values and array order. Capture SHA covers exact decoded raw bytes; metadata SHA covers the capture object with only raw_base64 removed; remaining SHA values cover policy, bindings and graph_nodes respectively. + +Packet exact keys: +- schema_version: exact integer1 (bool refuses). +- capture: exactly {capture_id, captured_at, source, raw_base64}. +- policy: exactly {schema_version, artifact_ids, columns}. +- bindings: array of exactly {node_id, artifact_id, roles}. +- graph_nodes: array of explicit node IDs. + +Capture ID is a nonempty ASCII identifier matching `[A-Za-z0-9][A-Za-z0-9_.:-]{0,191}`. captured_at is a valid UTC second timestamp YYYY-MM-DDTHH:MM:SSZ. Source exact keys are {spreadsheet_id, worksheet_id, title, ranges, cell_fields}. spreadsheet_id is a nonempty ASCII identifier of at most192 characters; worksheet_id exact nonnegative integer; title nonempty string, at most256 UTF-8 bytes, no controls; ranges is a unique nonempty list of exact quoted title A-to-L ranges `'TITLE'!A:L`, nonreversed and ordered by starting row; title containing a quote is unsupported. cell_fields must be exactly formattedValue. Native metadata must agree with the declared spreadsheet ID, worksheet ID and title. Metadata is declared as-of, not immutable native revision authentication. + +Stdin limit16MiB, decoded capture limit4MiB, JSON nesting at most64 container levels (a scalar leaf does not add a level), at most4096 nonheader native data rows plus the one header and4096 graph IDs/bindings/policy IDs, each formatted cell at most8192 UTF-8 bytes. Artifact and graph IDs use the same ASCII identifier pattern and192-character bound as capture_id. Reject duplicate object keys, nonfinite numbers, multiple JSON documents, malformed UTF-8/base64/noncanonical base64, booleans used as integers, overflow, empty or unknown envelope fields, and mismatching pins. Unknown native annotations stay private and inert; no unknown native field is projected. + +Native response is an object containing spreadsheetId and one sheet. The sheet properties must identify the declared worksheet/title. Grid properties must declare rowCount positive <=10000000 and columnCount exactly12. Its data list must have exactly one grid block for each declared range in the same order. Missing startRow/startColumn means0; supplied offsets are exact nonnegative integers and must equal the range's0-based position/column0. Every block has exactly its declared number of rowData rows, every row exactly12 values. Each cell is an object; missing formattedValue means empty string, supplied formattedValue must be a string within the bound. Unknown annotations remain inert. Blocks may not overlap and must be inside declared grid bounds. Exactly one row0 contains the exact12-column header; all other rows have a nonempty valid artifact ID, with no duplicate ID even on unselected rows. Nonselected row values and private columns never enter output. Selected IDs must each exist exactly once; unknown/duplicate/invalid policy IDs refuse. + +Policy columns must equal the fixed eight public columns above, in that exact order. Policy schema_version exact1. artifact_ids is a nonempty unique list of valid IDs; its order is preserved for identity hashing but output rows sort by artifact ID. graph_nodes are valid unique IDs. Bindings require known graph ID and selected artifact ID. Roles are a nonempty unique list drawn from source, review, kernel, computation, alignment, teaching-source; no inferred acceptance or title join. Duplicate node/artifact pairs refuse, even with different roles. Explicit valid bindings sort by node_id, artifact_id; their roles sort lexically. Unknown mappings refuse; absent mappings remain unmapped. + +## Exact public output + +Projection exact root fields: +`{schema_version:1,scientific_effect:"NONE",scientific_status_authority:false,custody:"unknown",source,identities,columns,rows,bindings,unmapped_nodes}`. + +source exactly `{capture_id,captured_at,spreadsheet_id,worksheet_id,worksheet_title,ranges,cell_fields,representation:"native-api-formattedValue-json"}`; preserve declared metadata/range order only. It contains no source URL, original packet, native annotations or raw capture bytes. identities exactly `{capture_sha256,metadata_sha256,policy_sha256,bindings_sha256,graph_nodes_sha256}`, matching the independently supplied pins. columns equals the eight public column names. + +Every row exactly `{artifact_id,source_row,fields}`: source_row is original1-based native row, fields is an object with exactly the eight column names and original formatted strings. Do not interpret status, authority, review prose, URLs or scope into another evidence axis. Empty fields stay empty. Every binding exactly {node_id,artifact_id,roles} with explicit canonical sorting. unmapped_nodes is the sorted graph IDs with no binding. Success JSON is deterministic ASCII with sorted object keys and one terminal newline. No output timestamp, original private input, accepted marker or inferred formal join. + +## External controls and completion + +Tests launch the CLI as a subprocess with -B -S and propagate the active optimized mode. Before implementation, actual hosted RED must identify the missing CLI, not merely fail discovery/import/setup. Cover valid deterministic preservation and typing; multiple native ranges/default offsets/blank cells; every independently pinned identity; each required/duplicate/invalid flag; duplicate/nonfinite/deep/oversized JSON and canonical base64; native source/header/range/row/grid/type contradictions; unknown/duplicate artifact IDs and bindings; six explicit roles; unknown graph IDs; missing mappings; forged columns; private Source/Notes and unselected-row canaries absent from public stdout and refusal stderr; inert code/URL strings never fetched or executed. Positive selected public strings may themselves contain URLs/code as inert escaped data; omission is determined by explicit field/row policy. + +Keep fixtures synthetic and compact. Do not copy any real private capture into tests or the public repository. Complete this unit only after exact candidate hosted normal/optimized controls pass with full discovery, original failure/success archives and nonauthor source/privacy review. Real selected source projection/readback follows that tested parser; the capture SHA alone does not authenticate a Sheet revision. Native graph archive readback, evidence-public projection, Astro/build/browser/deployment and all other architecture outcomes remain separate work. diff --git a/docs/plans/2026-10-08-interface-graph-custody-contract.md b/docs/plans/2026-10-08-interface-graph-custody-contract.md new file mode 100644 index 000000000..ec17f2d08 --- /dev/null +++ b/docs/plans/2026-10-08-interface-graph-custody-contract.md @@ -0,0 +1,209 @@ +# Interface graph archive readback v1 contract + +Root pickup 6070431864/amendment 6070796606 owns this bounded next graph-to-SSG unit. This document is the only current write. Proposed later files are `architecture/graph_artifact_readback.py`, `tools/architecture_graph_artifact_readback.py`, and `tests/test_architecture_graph_artifact_readback.py`; root claims and assigns those separately after contract review. Standing owner authorization applies. No local project execution, implementation, tests, deployment, or private Artifact Index capture access is part of this design task. + +## Observed retained data and the custody boundary + +Source-only ZIP reads and hashes on 8 October 2026 inspected `work/evidence/architecture-casefold-required-main00bcd-37856787382.zip`, outside the public Git tree. Original bytes are 19001 bytes, SHA256 `7c5891445d327b88f1fb7a82966ebf681643286a64179b2389a5bcb3da41e8b9`. Root supplies the native artifact identity 11584985628, name `research-architecture-37856787382-1`, producer repository `d6g8k5htny-coder/main`, run 37856787382/attempt 1, checked commit `dcb30554f9cc9e60aeb9f6b4b8efa3b5c9287a3b`, and original receipt SHA256 `4d32e7d72b999c74c13726f665cf57632959ae797541a22da458ade3b41ca5d2`. The 19001-byte ZIP has 13 regular DEFLATED members, no archive comment, classic single-disk central directory, and signed 32-bit data descriptors. This inspection read data; it did not rerun the exporter, gate, tests, or receipt helper. + +The original graph member is 48932 bytes with SHA256 `30cdcedabbdd3d9906a6cf13c838a23424ca065225d179c112db3ed7f5b3aa7f`; data inspection counted 49 dictionary node IDs and 55 edges. The original receipt is 22627 bytes. Both full logs record 89 passing qualified IDs and terminal `OK`; the sorted IDs joined with one LF after each ID have SHA256 `b51eaf6cfaede42105ad377c066c4b18789e320ccf542b6817a6850c350fc9ef`. These are original retained-data observations, not a new execution or scientific acceptance. + +Independently supplied native expectations establish comparison context. The reader can prove that bytes match those declared pins and that archive contents satisfy this contract; it cannot authenticate GitHub, an artifact ID, a producer conclusion, or an archive download through caller strings. Root's separate native producer/job/artifact readback establishes that external association. A synthetically regenerated archive with supplied matching pins remains synthetic. Every output retains `custody: "unknown"`, scientific effect `NONE`, and exact false scientific-status authority. No capture-authentication override is accepted. + +The producer checked commit above is distinct from the dated Math source commit below and from a later executing reader/SSG build commit. The reader never rewrites one identity into another and never requires an old producer commit to equal its own current checkout. The later immutable build manifest binds its executing commit, this output unit's SHA256, the separately governed public Artifact Index projection, and emitted assets. That build/deployment unit remains separate. + +## Exact CLI, module, and output ABI + +The external CLI is `python -B -S tools/architecture_graph_artifact_readback.py`. It requires exactly once, with no abbreviation, `--archive FILE` and these eight flags: + +| Flag | Binding field | Exact value type | +|---|---|---| +| `--expected-commit` | checked_commit | lowercase 40-hex string | +| `--expected-repository` | repository | ASCII `[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+`, neither component `.` or `..` | +| `--expected-run-id` | run_id | ASCII positive decimal, no leading zero | +| `--expected-run-attempt` | run_attempt | ASCII positive decimal, no leading zero | +| `--expected-receipt-sha256` | receipt_sha256 | lowercase 64-hex string | +| `--expected-artifact-id` | artifact_id | ASCII positive decimal, no leading zero | +| `--expected-artifact-sha256` | artifact_sha256 | lowercase 64-hex string | +| `--expected-artifact-name` | artifact_name | exactly `research-architecture--` | + +The expectations cannot be learned from stdin, the ZIP, its manifest, receipt, log, or filename. No stdin packet, output-path flag, optional trust pin, URL input, alternate graph source, extraction, network fetch, or captured-code import is supported. Any missing/duplicate/unknown/abbreviated argument or invalid input refuses nonzero with empty stdout and exactly `ARCHITECTURE_GRAPH_READBACK_REFUSED\n` on stderr. Diagnostics never echo paths, members, archive bytes, logs, or exception text. Success exits 0, stderr is empty, and stdout is one deterministic ASCII JSON document with sorted object keys, compact separators, preserved JSON types/array order, and one terminal LF. + +The pure stdlib module API is `read_graph_archive(raw_zip: bytes, expected: dict[str, str], source_graph_bytes: bytes) -> dict`. The expected dictionary has exactly the eight binding keys above, all exact strings. The source bytes must match the independently fixed source SHA below, even if callers provide another internally coherent source. The module performs no I/O. The CLI reads only the archive and the bundled fixed `docs/site/dependency-source/GRAPH.json`; it imports only trusted implementation/stdlib code and never `hard_gate.py`, the producer receipt helper, tests, or captured members. Suppress bytecode writes. Reuse the current helper's documented data rules independently, not its executable acceptance result. + +Success root exact keys are `{schema_version, scientific_effect, scientific_status_authority, custody, graph, receipt}` with schema_version exact integer 1, scientific_effect `NONE`, authority exact false, custody `unknown`, and graph the original parsed seven-field export with every typed record preserved. Receipt exact keys are `{schema_version, scientific_effect, scientific_status_authority, custody, verification_scope, binding, archive, graph_member, producer_receipt_sha256, producer_receipt}`. The same exact non-scientific constants apply; verification_scope is `declared-native-pins-and-archive-content`, binding preserves the eight externally supplied strings, and producer_receipt is the validated original parsed producer receipt without added/mutated fields. + +`archive` is exactly `{sha256, bytes, members}`: original raw ZIP SHA and byte count, then 13 entries sorted by canonical path, each exactly `{path, bytes, sha256}` derived from fully read original member bytes, including `ARTIFACT_SHA256SUMS`. `graph_member` is exactly `{path: "generated/graph.json", bytes, sha256}`. producer_receipt_sha256 is the original member SHA matching the external receipt pin. No raw ZIP/base64, log/console text, exporter output, inspected-image annotations, filesystem path, validation timestamp, inferred graph join, accepted badge, kernel result, or source-custody assertion is projected. + +Validation completes before stdout serialization. The complete encoded output is bounded at 16 MiB. The CLI writes no files; a trusted caller retains stdout in a temporary file and promotes the one unit atomically only after exit 0 and successful complete-output validation. Refusal preserves any previously published caller-owned unit. A broken/partial output transport is discarded by that caller; it is never a valid unit merely because some JSON bytes appeared. + +## File and ZIP admission + +Archive raw bytes are bounded at 64 MiB. The CLI anchors every absolute parent component with `O_DIRECTORY|O_NOFOLLOW`, resolving a relative input against the trusted current directory without erasing symlink components. Reject `..` components; never call resolve/abspath in a way that collapses an unchecked component. The leaf uses `O_NOFOLLOW|O_NONBLOCK`; fstat must establish a regular file before reading. Read at most 64 MiB + 1 once through that descriptor, check byte count and pre/post descriptor identity/size/change metadata, and compare the SHA of that exact buffer to expected artifact_sha256 before archive parsing. Directory, FIFO, device, symlink leaf/ancestor, unavailable, changing, and oversized files refuse without waiting for a writer. The bundled source graph uses the same anchored regular-file policy with a 16 MiB cap and its fixed byte/SHA check. + +V1 admits the observed classic single-disk ZIP subset, with STORED or DEFLATED entries and optional ordinary 32-bit data descriptors. Allowed general-purpose flag bits are 3 (data descriptor), 11 (UTF-8 names), and, for DEFLATED entries only, 1/2 (compression option); all other set bits refuse. ZIP64, multidisk, encrypted/strong-encrypted, unsupported compression/flags, archive/file comments, and extra fields refuse; these are explicit unsupported formats, not claims that all general ZIPs are unsafe. The initial local header begins at byte 0, the end-of-central-directory record ends exactly at EOF, central-directory count/size/offsets agree, and every local entry belongs to exactly one central entry. Local names/flags/methods and declared CRC/sizes or its 12/16-byte descriptor must agree with the central record. Local header/data/descriptor spans cannot overlap, share an offset, contain unreferenced gaps/entries, or extend into the central directory. No self-extracting prefix, concatenated/trailing archive, truncation, hidden duplicate, or unsigned range wrap is accepted. + +Inspect every entry before reading any content. Raw/decoded/original filenames must agree and be ASCII exact canonical names; reject NUL/control characters, backslashes, absolute/drive paths, empty/dot/dot-dot components, directories, trailing separators, casefold aliases, and duplicates even if a library would return the last entry. Unix type bits must be regular when present; absent type bits are admitted only for a nondirectory DOS regular entry. Symlink, FIFO, socket, device, and other special attributes refuse. Permission timestamps/order may vary and are not projected. Both file order and manifest line order may vary; the member-name set may not vary in v1. + +The exact 13 members are the 12 data members in the next section plus `ARTIFACT_SHA256SUMS`. KiB/MiB mean 1024/1048576 bytes. Total inflated member bytes are bounded at 64 MiB; the smaller member limits are stronger for this closed inventory, while independent aggregate accounting remains required. Member limits are: manifest 16 KiB; checked-commit/run-id/run-attempt/container-status each 128 bytes; exporter.stdout and exporter.stderr each 64 KiB; runtime-image.json 1 MiB; generated/graph.json, architecture-run-receipt.json and console.log each 4 MiB; normal/optimized logs each 16 MiB. Declared sizes must satisfy limits before decompression; bounded streaming must enforce each actual count and aggregate count independently. Read every member to its complete end and verify CRC; for DEFLATED data also require one complete deflate stream with no unused compressed tail. A high compression ratio alone is not rejected: valid repetitive logs remain admissible within the actual byte limits. A compression bomb that exceeds a declaration/budget, never ends, or fails structure/CRC refuses before publication. No entry is extracted, evaluated, executed, or imported. + +## Manifest, producer receipt, and native consistency + +`ARTIFACT_SHA256SUMS` is strict ASCII: exactly 12 nonempty lines, each lowercase 64-hex, two spaces, one canonical name from this exact set, and a final LF. No duplicate/case alias, escaped filename, binary marker, blank line, self-entry, unknown/missing member, or trailing text is allowed. Every listed SHA is compared with independently recomputed full original member bytes; never accept the manifest as its own proof. + +```text +runtime-image.json +checked-commit.txt +run-id.txt +run-attempt.txt +container-exit-status.txt +console.log +tests-normal.log +tests-optimized.log +exporter.stdout +exporter.stderr +generated/graph.json +architecture-run-receipt.json +``` + +The receipt's eight evidence_file_sha256 keys are exactly generated/graph.json, checked-commit.txt, run-id.txt, run-attempt.txt, container-exit-status.txt, tests-normal.log, tests-optimized.log, runtime-image.json; every lowercase 64-hex value must independently match those eight raw members. The manifest additionally verifies the receipt and all three diagnostics. Manifest SHA and receipt hashes remain internal integrity until matched with the separate externally supplied raw ZIP and receipt pins; regenerating a manifest does not authorize changed context or source records. + +Producer receipt exact root keys are `{schema_version, scientific_effect, scientific_status_authority, checked_commit, repository, run_id, run_attempt, runtime_image, test_modes, graph_source, evidence_file_sha256}`. Require integer 1, `NONE`, exact false, and all four native strings exactly equal external commit/repository/run/attempt. checked-commit.txt, run-id.txt and run-attempt.txt equal the corresponding ASCII string plus exactly one LF; the repository exists in the receipt, not a fabricated repository.txt member. container-exit-status.txt equals exactly `0\n`. exporter.stderr must be empty and exporter.stdout must equal exactly `/output/generated/graph.json\n`, the current fixed producer invocation. console.log may contain bounded inert diagnostics and is hash-verified but is never interpreted or projected. + +runtime-image.json is strict JSON, a one-element list containing an object. Native extra inspected-image fields are admitted as inert data, but Id must be a lowercase `sha256:<64hex>` string, RepoDigests exactly `["python@sha256:83f339c1be6340ae1096010fdccf6552ac932d8f410d45d206014916bdf37e48"]`, Os `linux`, Architecture `amd64`. The receipt runtime_image is exactly `{id, repo_digest}` and must equal those checked values; do not hardcode a synthetic/current image ID. All JSON members and bundled source refuse duplicate keys at any depth, invalid UTF-8/surrogates, nonfinite constants/overflow, multiple documents, malformed types, and more than 64 container levels (root container counts; scalar leaves do not). Canonical typed comparisons distinguish false/0 and integer/float, without coercion. + +## Complete test inventory and exact graph policy + +Derive both inventories from the full raw normal/optimized logs independently of receipt claims. Logs must be strict UTF-8 text with LF line boundaries and no CR/NUL. Accept only complete lines ` () ... ok`, with no leading whitespace, exact delimiter, short name equal the qualified last component, and identity matching ASCII `test_architecture_[A-Za-z0-9_]+\.[A-Za-z_][A-Za-z0-9_]*\.test[A-Za-z0-9_]*`. `test1` and `testExtra` remain valid prefixes. Every ID occurs exactly once per mode, all 89 immutable IDs in the appendix must occur, and later valid unique passing IDs are allowed, up to 4096 per mode. Both discovered inventories must be identical. A count-only 89 substitute is forbidden. + +Every result-looking line must match the complete passing grammar: detect case-insensitive test prefixes after optional whitespace, parenthesized qualified test markers, ellipses after a right parenthesis, and the ordinary result delimiter. Malformed/foreign/shortname-mismatched results, FAIL/ERROR/FAILED, skips, expected failures, unexpected successes, or duplicate/partial runs refuse. Exactly one `Ran tests in s` footer follows all test results, its count equals discovered IDs, and exactly one terminal `OK` is the last nonempty line. Diagnostic blank lines, separators and ordinary non-result text may remain inert. Receipt test_modes is exactly `[normal, optimized]`; each entry has exactly `{mode, test_count, test_ids}` with an exact integer count and sorted complete IDs equal the derived inventory. Do not discover expected IDs by executing/importing tests or learning a new floor from the input receipt. + +Graph root exact keys are `{schema_version, source, nodes, edges, dimensions, scientific_effect, scientific_status_authority}` with integer 1, `NONE`, exact false. Source exact five fields are repository `d6g8k5htny-coder/Math-`, commit `7858329974e28be79f29b22644370084ff43da4f`, captured_at `2026-10-03T15:07:03Z`, graph_sha256 `8822e9618678321a342d69cd0b8ae6552de1b5d578c331de5072b2892ee9dd09`, gate_sha256 `a78f3e25f3b0cfe113e618a4c31a7a25d7f22af638c46dec1ecba221fa333ac8`. Receipt graph_source must equal those original fields exactly. Bundled GRAPH.json has 38753 original bytes and the graph_sha256 above. Verify its byte count/SHA, strict parse, then canonical typed equality of exported nodes/edges with this independent source. Preserve all 49 dictionary IDs, 55 ordered original edges, records, review qualifiers, classifications and scopes; no conversion to a node array, case aliasing, rewriting, or guessed relation to Lean or Artifact Index IDs. + +Dimensions must have exactly those 49 IDs. Each value has exactly source/review/kernel/computation/alignment. Source is `recorded` iff the original node's explicit source value is truthy; review is `recorded` iff any explicit review_source, review_issue, review_basis, review_provider, review_providers or review_disposition value is truthy; otherwise each is `not_recorded`. Kernel, computation and alignment are always `not_recorded` in this dated export. Classification/proof prose and passing engineering tests cannot manufacture evidence. Raw native formal receipts and the separate evidence adapter remain distinct inputs with unknown graph-to-target joins; no actual Lean elaboration, declaration existence, per-statement proof, or accepted alignment is inferred here. + +The current helper source inspected is tools/architecture_run_receipt.py SHA256 `a4bb66325f64422398ff726bc1c448afb284498275e5894a7d1af6b25fba9890`; its staged receipt validates the original eight members and 53 baseline IDs. This ZIP reader independently repeats those data invariants, strengthens the required floor to the observed complete 89 controls, verifies the producer receipt itself and all 12 manifest entries, and adds bounded archive admission. It never calls receipt() on extracted data or accepts its own newly synthesized receipt in place of the original member. + +## Independent external controls and acceptance + +Freeze reviewed contract before assigning tests. Target about 30 external CLI methods, with an explicit CLI-exists assertion before every child so missing implementation yields genuine hosted RED. Child interpreters retain -B/-S and active -O/-OO. Synthetic ZIP fixtures use TEST native identities and clearly labeled synthetic logs; graph source data may be read inertly from the already-public fixed source. Do not copy the private Artifact Index capture or mistake a synthetic passing log for a new native run. A separately root-supplied real-archive readback control uses the original external eight bindings above. + +Meaningful control groups are: deterministic real-format positive; alternative valid native context and attempt 10; original typed graph/receipt retention; additional valid test1/testExtra controls; each eight expected pin/context substitution; all argument absence/duplicates/format errors; raw and member byte boundaries and aggregate accounting; regular/nofollow/symlink-parent/FIFO refusal; duplicate/casefold/path/NUL members; Unix special/DOS-directory types; encrypted/unsupported/multidisk/ZIP64 cases; central/local disagreement/overlap/truncation/prefix/tail; CRC and bounded expansion corruption; every missing/extra ZIP member; each 12 manifest hash and malformed/duplicate/self-row; each receipt-eight hash; receipt raw pin despite regenerated manifest; nonzero native status and each native identity mismatch; strict JSON/depth/type contrasts; all graph pins and typed node/edge/dimension mutation with coherently regenerated internal hashes; missing/substituted one of 89 IDs despite matching count; skipped/failed/expected-negative additional tests; malformed result-like extras before valid footer; unequal additional suites; receipt inventory mismatch; runtime inspect/receipt contradictions; exporter diagnostics contradictions; and inert captured strings with calibrated I/O/network observers, no extraction or writes. + +Retain first exact tests-only hosted missing-CLI RED, then implementation candidate normal/optimized full discovery and original failure/success archives. Include exact-limit positives plus one-over negatives; mutations must recompute unrelated internal/external synthetic pins where needed so another mismatch does not mask the intended control. Include an independently reviewed count-only/circular-rehash mutant refusal and a meaningful positive so reject-all cannot pass. Nonauthor source review verifies the parser/privacy/IO boundary, with test-author and organizational-independence relationships disclosed. Root checks native job conclusions, original archive bytes/member identities, and executing revisions before reporting this unit implemented. No passing receipt alone promotes science, confirms custody, completes the reader, or authorizes a guessed graph mapping. + +At this plan's source freeze: contract SOURCE_PREPARED; test implementation NOT_WRITTEN; hosted RED/GREEN NOT_RUN; reader/SSG/browser/deployment NOT_RUN. Existing Artifact Index tests at SHA256 `8d6fb4abc139e37f0163f344c7c53f263e2dd6a0c2f440a29643ac01c33d7736` remain unchanged. + +## Immutable required 89 inventory + +The literal fully qualified IDs below are independent v1 expectations, not parsed from an incoming receipt. Their sorted LF-terminated byte inventory has the b51eaf6c SHA above. Additional IDs never replace any of these 89. + +```text +test_architecture_binding.ArchitectureBindingContract.test_artifact_id_requires_positive_ascii_decimal_without_leading_zero +test_architecture_binding.ArchitectureBindingContract.test_artifact_name_is_exactly_bound_to_current_run_and_attempt +test_architecture_binding.ArchitectureBindingContract.test_attempt_ten_is_a_positive_decimal_not_a_single_digit +test_architecture_binding.ArchitectureBindingContract.test_commit_format_cannot_be_authorized_by_an_equally_invalid_native_commit +test_architecture_binding.ArchitectureBindingContract.test_deeply_nested_json_is_refused_without_a_success_envelope +test_architecture_binding.ArchitectureBindingContract.test_duplicate_top_level_keys_are_refused_even_when_last_value_is_valid +test_architecture_binding.ArchitectureBindingContract.test_each_current_native_argument_is_used_for_identity_binding +test_architecture_binding.ArchitectureBindingContract.test_each_duplicate_output_key_is_refused_even_when_last_value_is_valid +test_architecture_binding.ArchitectureBindingContract.test_each_expected_native_identity_argument_is_required +test_architecture_binding.ArchitectureBindingContract.test_each_non_success_result_is_refused +test_architecture_binding.ArchitectureBindingContract.test_each_of_the_eight_outputs_is_required +test_architecture_binding.ArchitectureBindingContract.test_each_output_rejects_an_empty_string +test_architecture_binding.ArchitectureBindingContract.test_each_output_rejects_non_string_identities_without_coercion +test_architecture_binding.ArchitectureBindingContract.test_each_top_level_field_is_required +test_architecture_binding.ArchitectureBindingContract.test_extra_output_fields_are_refused +test_architecture_binding.ArchitectureBindingContract.test_extra_top_level_fields_are_refused +test_architecture_binding.ArchitectureBindingContract.test_invalid_or_multiple_json_documents_are_refused +test_architecture_binding.ArchitectureBindingContract.test_non_finite_json_numbers_are_refused +test_architecture_binding.ArchitectureBindingContract.test_non_object_top_level_values_are_refused +test_architecture_binding.ArchitectureBindingContract.test_other_valid_native_identities_are_accepted_and_preserved +test_architecture_binding.ArchitectureBindingContract.test_outputs_must_be_an_object +test_architecture_binding.ArchitectureBindingContract.test_receipt_and_artifact_sha256_require_lowercase_64_hex_digits +test_architecture_binding.ArchitectureBindingContract.test_repository_substitution_and_case_drift_are_refused +test_architecture_binding.ArchitectureBindingContract.test_run_and_attempt_decimal_format_cannot_be_authorized_by_invalid_native_args +test_architecture_binding.ArchitectureBindingContract.test_stale_attempt_is_refused_even_with_a_matching_stale_artifact_name +test_architecture_binding.ArchitectureBindingContract.test_stale_checked_commit_is_refused +test_architecture_binding.ArchitectureBindingContract.test_stale_run_is_refused_even_with_a_matching_stale_artifact_name +test_architecture_binding.ArchitectureBindingContract.test_valid_current_run_has_exact_stable_non_scientific_output +test_architecture_binding.ArchitectureBindingContract.test_valid_json_at_exact_stdin_byte_limit_is_accepted +test_architecture_binding.ArchitectureBindingContract.test_valid_json_over_stdin_byte_limit_is_refused +test_architecture_graph_export.ArchitectureGraphExportContract.test_caller_pinned_provenance_cannot_authorize_different_gate_code +test_architecture_graph_export.ArchitectureGraphExportContract.test_classification_and_proof_review_text_cannot_manufacture_evidence +test_architecture_graph_export.ArchitectureGraphExportContract.test_context_only_cycle_is_allowed_without_changing_gate_semantics +test_architecture_graph_export.ArchitectureGraphExportContract.test_default_trust_rejects_self_consistent_changed_graph_and_provenance +test_architecture_graph_export.ArchitectureGraphExportContract.test_dimensions_describe_only_explicit_recorded_metadata +test_architecture_graph_export.ArchitectureGraphExportContract.test_duplicate_graph_json_keys_are_rejected_after_identity_validation +test_architecture_graph_export.ArchitectureGraphExportContract.test_duplicate_provenance_json_keys_are_rejected_before_gate_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_exact_source_bound_hard_gate_validator_is_called +test_architecture_graph_export.ArchitectureGraphExportContract.test_failed_export_preserves_an_earlier_valid_export +test_architecture_graph_export.ArchitectureGraphExportContract.test_gate_identity_is_checked_before_untrusted_gate_executes +test_architecture_graph_export.ArchitectureGraphExportContract.test_graph_identity_is_checked_before_gate_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_missing_required_dependency_is_rejected_by_pinned_hard_gate +test_architecture_graph_export.ArchitectureGraphExportContract.test_pinned_49_node_55_edge_export_is_deterministic_and_preserves_records +test_architecture_graph_export.ArchitectureGraphExportContract.test_required_dependency_cycle_is_rejected_by_pinned_hard_gate +test_architecture_graph_export.ArchitectureGraphExportContract.test_same_size_gate_change_is_rejected_by_sha_before_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_source_byte_counts_are_validated_even_when_sha_matches +test_architecture_graph_export.ArchitectureGraphExportContract.test_symlink_source_directory_is_rejected +test_architecture_graph_export.ArchitectureGraphExportContract.test_symlink_source_files_are_rejected_before_gate_import +test_architecture_receipt.ArchitectureReceiptContract.test_attempt_ten_and_other_native_repository_are_preserved +test_architecture_receipt.ArchitectureReceiptContract.test_changed_node_or_edge_is_refused_even_when_graph_source_pins_are_unchanged +test_architecture_receipt.ArchitectureReceiptContract.test_directory_or_fifo_member_is_refused_without_waiting_for_a_writer +test_architecture_receipt.ArchitectureReceiptContract.test_duplicate_json_keys_are_refused_even_when_last_value_is_valid +test_architecture_receipt.ArchitectureReceiptContract.test_duplicate_test_identity_or_multiple_runs_are_refused +test_architecture_receipt.ArchitectureReceiptContract.test_each_expected_host_identity_argument_is_used_and_required +test_architecture_receipt.ArchitectureReceiptContract.test_each_native_identity_file_must_match_current_host_arguments +test_architecture_receipt.ArchitectureReceiptContract.test_each_required_sandbox_control_rejects_skipped_failed_or_error_status +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_is_required +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_must_be_regular_and_cannot_be_a_symlink +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_over_16_mib_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_empty_or_zero_discovered_test_logs_are_refused_in_each_mode +test_architecture_receipt.ArchitectureReceiptContract.test_every_graph_source_pin_is_required_and_cannot_be_substituted +test_architecture_receipt.ArchitectureReceiptContract.test_every_required_identity_is_needed_even_when_53_other_tests_are_ok +test_architecture_receipt.ArchitectureReceiptContract.test_evidence_directory_or_generated_parent_symlink_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_graph_and_runtime_inspect_require_strict_json +test_architecture_receipt.ArchitectureReceiptContract.test_graph_root_schema_and_non_scientific_flags_are_exact +test_architecture_receipt.ArchitectureReceiptContract.test_invalid_matching_host_identity_does_not_authorize_bad_commit_or_decimal +test_architecture_receipt.ArchitectureReceiptContract.test_later_all_ok_test_methods_are_counted_and_bound +test_architecture_receipt.ArchitectureReceiptContract.test_malformed_extra_result_lines_cannot_be_ignored_before_the_success_footer +test_architecture_receipt.ArchitectureReceiptContract.test_malformed_foreign_or_shortname_mismatched_test_identities_are_refused +test_architecture_receipt.ArchitectureReceiptContract.test_missing_extra_or_forged_dimension_cannot_manufacture_scientific_evidence +test_architecture_receipt.ArchitectureReceiptContract.test_native_repository_argument_must_have_owner_and_repository_components +test_architecture_receipt.ArchitectureReceiptContract.test_nested_record_types_cannot_change_even_when_python_values_compare_equal +test_architecture_receipt.ArchitectureReceiptContract.test_non_finite_numbers_are_refused_even_in_additional_inspect_fields +test_architecture_receipt.ArchitectureReceiptContract.test_nonunderscore_unittest_names_are_accepted_as_additional_controls +test_architecture_receipt.ArchitectureReceiptContract.test_nonunderscore_unittest_prefix_malformed_results_are_not_diagnostics +test_architecture_receipt.ArchitectureReceiptContract.test_nonzero_or_malformed_container_exit_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_normal_and_optimized_must_discover_the_same_additional_passing_tests +test_architecture_receipt.ArchitectureReceiptContract.test_runtime_inspect_is_one_linux_amd64_image_with_the_fixed_digest +test_architecture_receipt.ArchitectureReceiptContract.test_skip_or_failure_in_additional_discovered_test_is_also_refused +test_architecture_receipt.ArchitectureReceiptContract.test_summary_must_report_exact_discovered_count_and_terminal_ok +test_architecture_receipt.ArchitectureReceiptContract.test_valid_53_test_receipt_is_exact_deterministic_and_preserves_staged_bytes +test_architecture_receipt.ArchitectureReceiptContract.test_valid_inspected_image_id_is_preserved_instead_of_hardcoded +test_architecture_receipt.ArchitectureReceiptContract.test_valid_log_at_exact_16_mib_limit_is_accepted +test_architecture_receipt.ArchitectureReceiptContract.test_valid_log_larger_than_64_kib_is_accepted_and_raw_bytes_are_hashed +test_architecture_sandbox.ArchitectureSandbox.test_kernel_enforces_cpu_memory_and_process_limits +test_architecture_sandbox.ArchitectureSandbox.test_network_namespace_has_no_external_interface_or_route +test_architecture_sandbox.ArchitectureSandbox.test_nonroot_and_no_new_privileges +test_architecture_sandbox.ArchitectureSandbox.test_output_quota_stops_disk_exhaustion_and_cleanup_recovers_space +test_architecture_sandbox.ArchitectureSandbox.test_source_mount_is_read_only_and_environment_has_no_publishing_secret +``` + +## First implementation run and bounded observer-launcher correction + +The original source-freeze checkpoint above is preserved as history. Reviewed tests at SHA256 `204056347967bbe58e7e13aab1015da51b2754b2f20a8313606af9bf985eae0d` contain 37 external methods; the graph reader module and CLI remain frozen at SHA256 `663dd06f0e65e48f864fbc2143b86d7e09229157b018e84cfea1fefb2995c632` and `0ec793ccfd419d6bb9c83fe419af042b617b53f3a20ccb7993b5ee799084dc98`. The independently retained missing-CLI RED preceded those implementation bytes. This checkpoint does not mark the reader or its integration complete. + +The first actual implementation run, [37863465487/attempt 1](https://github.com/d6g8k5htny-coder/main/actions/runs/37863465487), ran architecture job [113604483110](https://github.com/d6g8k5htny-coder/main/actions/runs/37863465487/job/113604483110) for head `bb92abe5595f97d4defd14b1403980ae15f38987`. The native checkout and original checked-commit member are merge `f21e0fa0796602e0e57f1e35a6e36741d2c127f4`, merging that head into `0be54aa227d7015b0abda00feb5e847a3fbdbcdb`. Formal succeeded; architecture failed, landing was skipped and verify failed. Disposal, regular-output staging, original failure upload and strict unmount succeeded; receipt acceptance and accepted identity emission were skipped. + +The complete original normal log ran 160 methods in 182.623 seconds: all 123 prior methods passed; 36 graph-reader methods passed and the calibrated inert-observer method failed. The optimized log started 99 methods: 97 passed, the same observer method failed, and the typed-node/edge/dimension method remained unfinished. That partial log contains 64 passing prior methods and 35 started graph-reader methods, of which 33 passed. Both retained logs report the exact 64MiB positive and one-byte-over control as passing. Neither optimized completion nor a full two-mode GREEN is inferred. + +The observer failure occurred in the trusted launcher's `runpy.run_path` bootstrap before any CLI frame. Python 3.11.14 runpy.py line 280 imports pkgutil's get_importer; importlib then filled a fixture-CWD directory cache, and the active private-fixture list observer correctly refused that attempt. The correction preloads only trusted pkgutil support before observer installation/calibration and adds `-I` only to the audit-launcher child so the synthetic private CWD is not an import search root. Candidate CLI instructions still run under the active observer; calibration, network/read/write/list observations, normal/-O/-OO propagation and all 37 method identities and bodies remain unchanged. There is no blanket list/read exception, candidate import before activation or private-input access allowance. + +The outer command began at 00:11:26.174 UTC on 9 October and was killed at 00:16:36.186 UTC after approximately 310.012 seconds: the configured 300-second timeout plus its 10-second kill grace. The original container-exit-status member is 137. Normal mode alone took 182.623 seconds; two comparable modes would take approximately 365.246 seconds before exporter overhead. Claim amendment 6071777153 therefore changes only the total ephemeral timeout from 300 to 420 seconds for the next bounded hosted experiment. The 420-second allowance is an estimate, not a measured optimized completion or resource margin. The 20-second child bound, CPU2, 1GiB memory, PID128, 64MiB output filesystem, 128MiB /tmp under the same aggregate memory limit, network-none, nonroot, read-only source/root and strict disposal/collection/release controls remain unchanged. No native peak memory/CPU telemetry or Docker OOM flag was retained, so exit137 is not repurposed as an OOM diagnosis or proof of resource headroom. + +Root retained the completed original failure archive outside Git at `work/evidence/architecture-graph-reader-first-failure-37863465487.zip`: native artifact 11587317831, name `research-architecture-37863465487-1`, 11258 raw bytes, SHA256 `ed8c5afda7da6855ffcbb39696aa205f8849aea3757c26b5ad16ce17201c9139`. It has nine original entries, including ARTIFACT_SHA256SUMS and eight payloads; root verified every payload hash. The archive retains the two original complete/partial logs and original status/context/runtime bytes. No producer graph, exporter output or accepted receipt was manufactured for the failed run, and no raw native ZIP or private capture is added to Git. + +The bootstrap/workflow/history correction is SOURCE_PREPARED under amendments 6071777153 and 6071792491 for a fresh implementation-nonauthor source review before publication or hosted execution. Its author reviewed the earlier controls and authored the graph implementation, so exposure is source/test-exposed and organizational independence remains 0. No local Python, Node, test, YAML, download or Git execution occurred; corrected hosted execution, actual resource measurements, full optimized completion, accepted original graph/archive readback and reader/SSG/browser/deployment remain NOT_RUN. Earlier successful individual controls and this failure remain retained without rewriting the run as GREEN. + +Fresh actual root source assessment now ACCEPT_SCOPED_SOURCE for testSHA256dd3c5b3f2011ae3f7582514eae0fb79f25257d5bf54f40a99585d21dff65116e and workflow9cd51af45dc7892f665756a95c42872cac19297d062877aeb3b70616a6868c97. Root read the full183-line workflow, complete changed launcher and invocation context, and all16 appended history lines; exact whole-source comparison against the retained12-path publication proves only two test-line replacements and one timeout-line replacement. All37 method identities/bodies and both implementation files are unchanged, while the original191-line contract prefix is byte-identical. The trusted launcher imports pkgutil before observer installation, and only its audited child receives -I; calibration and every actual candidate instruction stay observed. This is nonauthor source/test-exposed review with organization-independence0, distinct from correction author /root/graph_tests_review. Corrected160-method two-mode runtime, original archive acceptance and real reader/SSG integration remain NOT_RUN until the new hosted attempt is read back. diff --git a/tests/test_architecture_artifact_index.py b/tests/test_architecture_artifact_index.py new file mode 100644 index 000000000..46fc4f736 --- /dev/null +++ b/tests/test_architecture_artifact_index.py @@ -0,0 +1,945 @@ +"""External contract for the inert, explicitly pinned Artifact Index projection. + +Every fixture below is synthetic TEST data. It models formattedValue API JSON, +not a real Sheet capture, native revision authentication, or delivered evidence. +""" + +import base64 +import copy +import hashlib +import http.server +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import threading +import urllib.request +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +CLI = ROOT / "tools" / "artifact_index_export.py" +HEADER = [ + "Artifact ID", "Title", "Org", "Class", "Priority", "Topics / object tags", + "Status", "Authority / canonical impact", "Dependencies or supersession", + "Modified UTC", "Source", "Notes", +] +PUBLIC_COLUMNS = [ + "Artifact ID", "Title", "Org", "Class", "Status", + "Authority / canonical impact", "Dependencies or supersession", "Modified UTC", +] +PIN_FLAGS = { + "capture_sha256": "--expected-capture-sha256", + "metadata_sha256": "--expected-metadata-sha256", + "policy_sha256": "--expected-policy-sha256", + "bindings_sha256": "--expected-bindings-sha256", + "graph_nodes_sha256": "--expected-graph-nodes-sha256", +} +ROLES = ["source", "review", "kernel", "computation", "alignment", "teaching-source"] +TITLE = "TEST Artifact Index" +RANGES = [ + "'TEST Artifact Index'!A1:L1", + "'TEST Artifact Index'!A936:L937", + "'TEST Artifact Index'!A1001:L1001", +] + +# Trusted test launcher, executed only in the external child. Its Python audit +# observer covers the fixture paths; it is not an operating-system sandbox. +AUDIT_LAUNCHER = r''' +import json +import os +import runpy +import sys + +cli_path, report_number, fixture_path, secret_path = sys.argv[1:5] +cli_arguments = sys.argv[5:] +report_fd = int(report_number) +fixture_root = os.path.normcase(os.path.abspath(fixture_path)) +labels = ("read", "write", "mkdir", "remove", "rename", "rmdir") +counts = dict.fromkeys(labels, 0) +calibration = dict(counts) +guard = False + +def fixture_target(path, dir_fd=None): + if isinstance(path, int): + # Standard streams and the trusted report descriptor are allowed. + # Other opaque descriptors conservatively count as fixture attempts. + return path not in (0, 1, 2, report_fd) + if path is None: + path = os.getcwd() + path = os.fsdecode(path) + if not os.path.isabs(path) and dir_fd not in (None, -1): + # A relative dir_fd target cannot safely be assigned to another path + # without extra I/O, so observe/guard it conservatively. + return True + path = os.path.normcase(os.path.abspath(path)) + return path == fixture_root or path.startswith(fixture_root + os.sep) + +def audit(event, arguments): + selected = [] + if event == "open" and fixture_target(arguments[0]): + flags = arguments[2] + if flags & (os.O_WRONLY | os.O_RDWR | os.O_APPEND | os.O_CREAT | os.O_TRUNC): + selected.append("write") + if not flags & os.O_WRONLY: + selected.append("read") + elif event in ("os.listdir", "os.scandir") and fixture_target(arguments[0]): + selected.append("read") + elif event == "os.mkdir" and fixture_target(arguments[0], arguments[2]): + selected.append("mkdir") + elif event in ("os.remove", "os.rmdir") and fixture_target(arguments[0], arguments[1]): + selected.append("remove" if event == "os.remove" else "rmdir") + elif event == "os.rename" and ( + fixture_target(arguments[0], arguments[2]) or fixture_target(arguments[1], arguments[3]) + ): + selected.append("rename") + for label in selected: + counts[label] += 1 + if selected and guard: + raise RuntimeError("TEST_FIXTURE_IO_GUARD") + +sys.addaudithook(audit) +try: + # Positive calibration uses trusted paths, never captured code or values. + with open(secret_path, "rb") as stream: + stream.read() + temporary = os.path.join(fixture_root, "TEST-audit-temporary") + renamed = os.path.join(fixture_root, "TEST-audit-renamed") + directory = os.path.join(fixture_root, "TEST-audit-directory") + with open(temporary, "wb") as stream: + stream.write(b"TEST calibration") + os.rename(temporary, renamed) + os.remove(renamed) + os.mkdir(directory) + os.rmdir(directory) + os.listdir(fixture_root) + calibration = dict(counts) + counts = dict.fromkeys(labels, 0) + guard = True + sys.argv = [cli_path, *cli_arguments] + sys.path[0] = os.path.dirname(cli_path) + runpy.run_path(cli_path, run_name="__main__") +finally: + report = {"calibration": calibration, "attempts": counts, "optimize": sys.flags.optimize} + encoded = (json.dumps(report, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n").encode("ascii") + os.write(report_fd, encoded) + os.close(report_fd) +''' + + +def canonical(value): + return json.dumps(value, sort_keys=True, separators=(",", ":"), + ensure_ascii=True, allow_nan=False).encode("ascii") + + +def digest(raw): + return hashlib.sha256(raw).hexdigest() + + +def native_bytes(value): + return (json.dumps(value, separators=(",", ":"), ensure_ascii=False, + allow_nan=False) + "\n").encode("utf-8") + + +def row(values): + return {"values": [{"formattedValue": value} for value in values]} + + +def selected_values(artifact_id, label): + return [ + artifact_id, label + " — α; TEST.node.a is only title text", "TEST Org", + "dated review record", "PRIVATE_PRIORITY_" + artifact_id, + "PRIVATE_TOPICS_" + artifact_id, "HISTORICAL / NOT ACCEPTED", + "independence=0; scope=historical only; as of 2026-10-01", + "supersedes TEST-OLDER only within the stated scope", "2026-10-01T00:00:00Z", + "PRIVATE_SOURCE_" + artifact_id, "PRIVATE_NOTES_" + artifact_id, + ] + + +def synthetic_native(): + unselected = ["TEST-UNSELECTED"] + ["PRIVATE_UNSELECTED_COL_%02d" % i + for i in range(1, 12)] + return { + "spreadsheetId": "TEST-R17-SPREADSHEET", + "privateAnnotation": "PRIVATE_NATIVE_ANNOTATION", + "sheets": [{ + "properties": { + "sheetId": 4242, "title": TITLE, + "gridProperties": {"rowCount": 1002, "columnCount": 12}, + }, + "data": [ + {"startRow": 0, "startColumn": 0, "rowData": [row(HEADER)]}, + {"startRow": 935, "startColumn": 0, "rowData": [ + row(selected_values("TEST-ZETA", "Zeta title")), + row(selected_values("TEST-ALPHA", "Alpha title")), + ]}, + {"startRow": 1000, "startColumn": 0, "rowData": [row(unselected)]}, + ], + }], + } + + +def synthetic_packet(): + return { + "schema_version": 1, + "capture": { + "capture_id": "TEST-capture-1", "captured_at": "2026-10-01T12:34:56Z", + "source": { + "spreadsheet_id": "TEST-R17-SPREADSHEET", "worksheet_id": 4242, + "title": TITLE, "ranges": list(RANGES), "cell_fields": "formattedValue", + }, + "raw_base64": base64.b64encode(native_bytes(synthetic_native())).decode("ascii"), + }, + "policy": {"schema_version": 1, "artifact_ids": ["TEST-ZETA", "TEST-ALPHA"], + "columns": list(PUBLIC_COLUMNS)}, + "bindings": [], + "graph_nodes": ["TEST.node.z", "TEST.node.a", "TEST.node.m"], + } + + +def pins_for(packet): + capture = packet["capture"] + raw = base64.b64decode(capture["raw_base64"], validate=True) + metadata = {key: value for key, value in capture.items() if key != "raw_base64"} + return { + "capture_sha256": digest(raw), "metadata_sha256": digest(canonical(metadata)), + "policy_sha256": digest(canonical(packet["policy"])), + "bindings_sha256": digest(canonical(packet["bindings"])), + "graph_nodes_sha256": digest(canonical(packet["graph_nodes"])), + } + + +def replace_raw(packet, raw): + packet["capture"]["raw_base64"] = base64.b64encode(raw).decode("ascii") + + +def get_native(packet): + return json.loads(base64.b64decode(packet["capture"]["raw_base64"])) + + +def replace_native(packet, value): + replace_raw(packet, native_bytes(value)) + + +class ArtifactIndexProjectionContractTests(unittest.TestCase): + def setUp(self): + self.packet = synthetic_packet() + self.pins = pins_for(self.packet) + self.temp = tempfile.TemporaryDirectory(prefix="TEST-artifact-index-") + self.addCleanup(self.temp.cleanup) + self.workdir = Path(self.temp.name) + self.private_values = ["PRIVATE_NATIVE_ANNOTATION"] + for block in get_native(self.packet)["sheets"][0]["data"][1:]: + for native_row in block["rowData"]: + values = [cell["formattedValue"] for cell in native_row["values"]] + if values[0] == "TEST-UNSELECTED": + self.private_values.extend(values) + else: + self.private_values.extend(values[index] for index in (4, 5, 10, 11)) + + def flag_args(self, pins): + return [argument for name, flag in PIN_FLAGS.items() for argument in (flag, pins[name])] + + def invoke(self, packet=None, *, expected=None, raw=None, args=None, audit=None): + # Missing exporters must fail every control before launching a child. + self.assertTrue(CLI.is_file(), "missing required external CLI: tools/artifact_index_export.py") + packet = self.packet if packet is None else packet + expected = pins_for(packet) if expected is None else expected + raw = canonical(packet) + b"\n" if raw is None else raw + flags = ["-B", "-S"] + if sys.flags.optimize: + flags.append("-" + "O" * sys.flags.optimize) + env = dict(os.environ) + env.pop("PYTHONOPTIMIZE", None) + arguments = self.flag_args(expected) if args is None else args + + def launch(command, pass_fds=()): + return subprocess.run( + command, input=raw, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + cwd=self.workdir, env=env, timeout=20, check=False, pass_fds=pass_fds, + ) + + if audit is None: + return launch([sys.executable, *flags, str(CLI), *arguments]) + with tempfile.TemporaryDirectory(prefix="TEST-audit-report-") as report_directory: + report_root = Path(report_directory) + launcher = report_root / "trusted_launcher.py" + report_path = report_root / "counts.json" + launcher.write_text(AUDIT_LAUNCHER, encoding="utf-8") + report_fd = os.open(report_path, os.O_CREAT | os.O_RDWR | os.O_TRUNC, 0o600) + try: + result = launch([ + sys.executable, *flags, str(launcher), str(CLI), str(report_fd), + str(self.workdir), str(audit["secret"]), *arguments, + ], pass_fds=(report_fd,)) + finally: + os.close(report_fd) + result.audit_report = json.loads(report_path.read_bytes()) + return result + + def assert_private_absent(self, result): + combined = result.stdout + result.stderr + for value in self.private_values: + self.assertNotIn(value.encode("utf-8"), combined) + self.assertNotIn(self.packet["capture"]["raw_base64"].encode("ascii"), combined) + + def expected_projection(self, packet, expected): + source = packet["capture"]["source"] + selected = set(packet["policy"]["artifact_ids"]) + projected_rows = [] + for block in get_native(packet)["sheets"][0]["data"]: + start = block.get("startRow", 0) + for offset, native_row in enumerate(block["rowData"]): + values = [cell.get("formattedValue", "") for cell in native_row["values"]] + if values[0] in selected: + projected_rows.append({ + "artifact_id": values[0], "source_row": start + offset + 1, + "fields": {name: values[HEADER.index(name)] for name in PUBLIC_COLUMNS}, + }) + bindings = [dict(binding, roles=sorted(binding["roles"])) for binding in packet["bindings"]] + bindings.sort(key=lambda binding: (binding["node_id"], binding["artifact_id"])) + bound_nodes = {binding["node_id"] for binding in bindings} + return { + "schema_version": 1, "scientific_effect": "NONE", "scientific_status_authority": False, + "custody": "unknown", + "source": { + "capture_id": packet["capture"]["capture_id"], + "captured_at": packet["capture"]["captured_at"], + "spreadsheet_id": source["spreadsheet_id"], "worksheet_id": source["worksheet_id"], + "worksheet_title": source["title"], "ranges": source["ranges"], + "cell_fields": source["cell_fields"], "representation": "native-api-formattedValue-json", + }, + "identities": expected, "columns": list(PUBLIC_COLUMNS), + "rows": sorted(projected_rows, key=lambda value: value["artifact_id"]), + "bindings": bindings, "unmapped_nodes": sorted(set(packet["graph_nodes"]) - bound_nodes), + } + + def accept(self, packet=None, *, expected=None, raw=None, audit=None): + packet = self.packet if packet is None else packet + expected = pins_for(packet) if expected is None else expected + result = self.invoke(packet, expected=expected, raw=raw, audit=audit) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, b"") + self.assertTrue(result.stdout.isascii()) + projected = json.loads(result.stdout) + self.assertEqual(projected, self.expected_projection(packet, expected)) + self.assertIs(type(projected["schema_version"]), int) + self.assertIs(projected["scientific_status_authority"], False) + self.assertIs(type(projected["source"]["worksheet_id"]), int) + for value in projected["rows"]: + self.assertIs(type(value["source_row"]), int) + self.assertEqual(result.stdout, canonical(projected) + b"\n") + self.assert_private_absent(result) + return result + + def refuse(self, packet=None, *, expected=None, raw=None, args=None): + result = self.invoke(packet, expected=expected, raw=raw, args=args) + self.assertNotEqual(result.returncode, 0) + self.assertEqual(result.stdout, b"") + self.assertEqual(result.stderr, b"ARTIFACT_INDEX_REFUSED\n") + self.assert_private_absent(result) + return result + + def test_exact_public_projection_is_stable_and_retains_original_row_positions(self): + before = sorted(path.name for path in self.workdir.iterdir()) + first = self.accept() + self.assertEqual(self.accept().stdout, first.stdout) + projected = json.loads(first.stdout) + self.assertEqual([(item["artifact_id"], item["source_row"]) for item in projected["rows"]], + [("TEST-ALPHA", 937), ("TEST-ZETA", 936)]) + self.assertEqual(projected["unmapped_nodes"], ["TEST.node.a", "TEST.node.m", "TEST.node.z"]) + self.assertEqual(sorted(path.name for path in self.workdir.iterdir()), before) + + def test_all_49_explicit_graph_nodes_remain_unmapped_without_a_binding(self): + packet = copy.deepcopy(self.packet) + packet["graph_nodes"] = ["TEST.node.%02d" % index for index in reversed(range(49))] + projected = json.loads(self.accept(packet).stdout) + self.assertEqual(len(projected["unmapped_nodes"]), 49) + self.assertEqual(projected["bindings"], []) + + def test_public_unicode_empty_cells_and_historical_scope_are_preserved_without_promotion(self): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + values = native["sheets"][0]["data"][1]["rowData"][1]["values"] + values[1]["formattedValue"] = "Unicode α / résumé / 雪; declared text only" + values[2] = {"unknownAnnotation": "PRIVATE_EMPTY_CELL_ANNOTATION"} + values[6]["formattedValue"] = "FAILED; skipped; as of 2026-09-30" + values[7]["formattedValue"] = "independence=0; historical scope only" + replace_native(packet, native) + result = self.accept(packet) + fields = json.loads(result.stdout)["rows"][0]["fields"] + self.assertEqual(fields["Org"], "") + self.assertEqual(fields["Status"], "FAILED; skipped; as of 2026-09-30") + self.assertNotIn(b"PRIVATE_EMPTY_CELL_ANNOTATION", result.stdout) + + def test_default_zero_offsets_and_unknown_native_annotations_are_private_and_inert(self): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + header = native["sheets"][0]["data"][0] + del header["startRow"] + for block in native["sheets"][0]["data"]: + del block["startColumn"] + block["annotation"] = {"raw": "PRIVATE_BLOCK_ANNOTATION", "nested": [True, None, 1.5]} + native["sheets"][0]["properties"]["annotation"] = "PRIVATE_PROPERTIES_ANNOTATION" + replace_native(packet, native) + result = self.accept(packet) + self.assertNotIn(b"PRIVATE_BLOCK_ANNOTATION", result.stdout) + self.assertNotIn(b"PRIVATE_PROPERTIES_ANNOTATION", result.stdout) + + def test_explicit_six_roles_and_bindings_sort_without_inferred_acceptance(self): + packet = copy.deepcopy(self.packet) + packet["bindings"] = [ + {"node_id": "TEST.node.z", "artifact_id": "TEST-ZETA", "roles": list(ROLES)}, + {"node_id": "TEST.node.a", "artifact_id": "TEST-ZETA", "roles": ["teaching-source"]}, + {"node_id": "TEST.node.a", "artifact_id": "TEST-ALPHA", "roles": ["review", "source"]}, + ] + projected = json.loads(self.accept(packet).stdout) + self.assertEqual(projected["bindings"][2]["roles"], sorted(ROLES)) + self.assertEqual(projected["unmapped_nodes"], ["TEST.node.m"]) + self.assertEqual(projected["custody"], "unknown") + + def test_policy_array_order_is_pinned_even_when_public_rows_have_the_same_order(self): + packet = copy.deepcopy(self.packet) + packet["policy"]["artifact_ids"].reverse() + original = json.loads(self.accept().stdout) + reordered = json.loads(self.accept(packet).stdout) + self.assertEqual(original["rows"], reordered["rows"]) + self.assertNotEqual(original["identities"]["policy_sha256"], reordered["identities"]["policy_sha256"]) + + def test_each_of_the_five_independent_expected_pins_must_match(self): + for name in PIN_FLAGS: + with self.subTest(pin=name): + expected = dict(self.pins) + expected[name] = "0" * 64 if expected[name] != "0" * 64 else "1" * 64 + self.refuse(expected=expected) + + def test_changed_input_components_cannot_replace_independently_supplied_pins(self): + for component in ("capture", "metadata", "policy", "bindings", "graph_nodes"): + with self.subTest(component=component): + packet = copy.deepcopy(self.packet) + if component == "capture": + replace_raw(packet, base64.b64decode(packet["capture"]["raw_base64"]) + b" ") + elif component == "metadata": + packet["capture"]["captured_at"] = "2026-10-02T12:34:56Z" + elif component == "policy": + packet["policy"]["artifact_ids"].reverse() + elif component == "bindings": + packet["bindings"] = [{"node_id": "TEST.node.a", "artifact_id": "TEST-ALPHA", "roles": ["source"]}] + else: + packet["graph_nodes"].reverse() + self.refuse(packet, expected=self.pins) + + def test_all_cli_flags_are_required_once_and_only_full_names_are_accepted(self): + arguments = self.flag_args(self.pins) + for index, (name, flag) in enumerate(PIN_FLAGS.items()): + with self.subTest(flag=flag, fault="missing"): + self.refuse(args=arguments[:2 * index] + arguments[2 * index + 2:]) + with self.subTest(flag=flag, fault="duplicate"): + self.refuse(args=arguments + [flag, self.pins[name]]) + with self.subTest(flag=flag, fault="abbreviated"): + amended = list(arguments) + amended[2 * index] = flag[:-1] + self.refuse(args=amended) + + def test_invalid_cli_digests_and_private_argument_values_are_never_echoed(self): + for name in PIN_FLAGS: + for value in ("", "a" * 63, "a" * 65, "A" * 64, "g" * 64, "../PRIVATE_ARG_PATH"): + with self.subTest(pin=name, value=value): + expected = dict(self.pins) + expected[name] = value + result = self.refuse(expected=expected) + self.assertNotIn(b"PRIVATE_ARG_PATH", result.stderr) + for extra in (["--PRIVATE_UNKNOWN_FLAG", "PRIVATE_ARGUMENT_VALUE"], ["/PRIVATE_POSITIONAL_PATH"]): + with self.subTest(extra=extra): + result = self.refuse(args=self.flag_args(self.pins) + extra) + self.assertNotIn(b"PRIVATE_", result.stderr) + + def test_packet_wrappers_are_closed_and_schema_versions_have_exact_integer_types(self): + wrapped = copy.deepcopy(self.packet) + wrapped["bindings"] = [{"node_id": "TEST.node.a", "artifact_id": "TEST-ALPHA", "roles": ["source"]}] + + def remaining_pins(packet): + # Pin every remaining component, so an unrelated mismatch cannot + # mask the missing/extra-key control. A wholly absent component has + # no hash to supply and retains the positive fixture's expectation. + expected = pins_for(wrapped) + for name, key in (("policy_sha256", "policy"), ("bindings_sha256", "bindings"), + ("graph_nodes_sha256", "graph_nodes")): + if key in packet: + expected[name] = digest(canonical(packet[key])) + if "capture" in packet: + capture = packet["capture"] + expected["metadata_sha256"] = digest(canonical({ + key: value for key, value in capture.items() if key != "raw_base64" + })) + if "raw_base64" in capture: + expected["capture_sha256"] = digest(base64.b64decode(capture["raw_base64"], validate=True)) + return expected + + paths = [(), ("capture",), ("capture", "source"), ("policy",), ("bindings", 0)] + for path in paths: + target = wrapped + for key in path: + target = target[key] + for key in list(target): + with self.subTest(path=path, missing=key): + packet = copy.deepcopy(wrapped) + changed = packet + for part in path: + changed = changed[part] + del changed[key] + self.refuse(packet, expected=remaining_pins(packet)) + with self.subTest(path=path, extra=True): + packet = copy.deepcopy(wrapped) + changed = packet + for part in path: + changed = changed[part] + changed["PRIVATE_UNRECOGNIZED_WRAPPER"] = "PRIVATE_WRAPPER_VALUE" + result = self.refuse(packet, expected=remaining_pins(packet)) + self.assertNotIn(b"PRIVATE_UNRECOGNIZED_WRAPPER", result.stderr) + self.assertNotIn(b"PRIVATE_WRAPPER_VALUE", result.stderr) + for path in ((), ("policy",)): + for value in (True, 1.0, "1", 0, 2, None): + with self.subTest(path=path, schema=value): + packet = copy.deepcopy(self.packet) + target = packet if not path else packet["policy"] + target["schema_version"] = value + self.refuse(packet) + + def test_capture_identity_timestamp_and_declared_source_are_strictly_typed(self): + cases = { + "capture_id": ("", "x" * 193, " space", "TEST\nID", "é", True, 1, None), + "captured_at": ("2026-02-30T12:00:00Z", "2026-10-01T24:00:00Z", "2026-10-01T12:34:56+00:00", + "2026-10-01T12:34:56.000Z", "2026-10-01", True, None), + } + for key, values in cases.items(): + for value in values: + with self.subTest(field=key, value=value): + packet = copy.deepcopy(self.packet) + packet["capture"][key] = value + self.refuse(packet) + source_cases = { + "spreadsheet_id": ("", "x" * 193, "é", True, None), + "worksheet_id": (True, 4242.0, -1, "4242", None), + "title": ("", "λ" * 129, "TEST\nTitle", "TEST'Quote", True, None), + "cell_fields": ("", "formattedValue,userEnteredValue", ["formattedValue"], None), + } + for key, values in source_cases.items(): + for value in values: + with self.subTest(source_field=key, value=value): + packet = copy.deepcopy(self.packet) + packet["capture"]["source"][key] = value + self.refuse(packet) + + def test_native_spreadsheet_worksheet_and_title_must_match_declared_metadata(self): + for field, value in (("spreadsheetId", "TEST-OTHER-SHEET"), ("sheetId", 4243), + ("sheetId", True), ("sheetId", 4242.0), ("title", "TEST Other Title")): + with self.subTest(field=field, value=value): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + target = native if field == "spreadsheetId" else native["sheets"][0]["properties"] + target[field] = value + replace_native(packet, native) + self.refuse(packet) + + def test_exact_header_order_names_types_and_single_header_are_required(self): + for fault in ("rename", "swap", "empty", "nonstring", "second_header"): + with self.subTest(fault=fault): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + header = native["sheets"][0]["data"][0]["rowData"][0]["values"] + if fault == "rename": + header[10]["formattedValue"] = "Sources" + elif fault == "swap": + header[0], header[1] = header[1], header[0] + elif fault == "empty": + header[11] = {} + elif fault == "nonstring": + header[0]["formattedValue"] = 1 + else: + native["sheets"][0]["data"][2]["rowData"][0] = row(HEADER) + replace_native(packet, native) + self.refuse(packet) + + def test_ranges_require_exact_title_a_to_l_syntax_order_and_nonoverlap(self): + cases = [[], RANGES + [RANGES[1]], list(reversed(RANGES)), + [RANGES[0], "'TEST Artifact Index'!A936:L937", "'TEST Artifact Index'!A937:L1001"], + ["TEST Artifact Index!A1:L1", *RANGES[1:]], + ["'TEST Other Title'!A1:L1", *RANGES[1:]], + ["'TEST Artifact Index'!A0:L1", *RANGES[1:]], + ["'TEST Artifact Index'!A01:L1", *RANGES[1:]], + ["'TEST Artifact Index'!B1:L1", *RANGES[1:]], + ["'TEST Artifact Index'!A1:K1", *RANGES[1:]], + [RANGES[0], "'TEST Artifact Index'!A937:L936", RANGES[2]], + [RANGES[0], True, RANGES[2]], "PRIVATE_RANGES_STRING"] + for value in cases: + with self.subTest(ranges=value): + packet = copy.deepcopy(self.packet) + packet["capture"]["source"]["ranges"] = value + self.refuse(packet) + + def test_grid_dimensions_are_exact_integers_and_bound_all_declared_blocks(self): + for key, value in (("rowCount", 0), ("rowCount", -1), ("rowCount", True), + ("rowCount", 1002.0), ("rowCount", 1000), ("rowCount", 10000001), + ("columnCount", 11), ("columnCount", 13), ("columnCount", 12.0), + ("columnCount", True)): + with self.subTest(field=key, value=value): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + native["sheets"][0]["properties"]["gridProperties"][key] = value + replace_native(packet, native) + self.refuse(packet) + + def test_native_offsets_match_range_positions_and_refuse_boolean_float_or_missing_nonzero(self): + for block_index, key, value in ((0, "startRow", 1), (1, "startRow", 934), + (1, "startRow", True), (1, "startRow", 935.0), + (1, "startRow", -1), (1, "startRow", "935"), + (1, "startRow", None), (2, "startColumn", 1), + (2, "startColumn", False), (2, "startColumn", 0.0)): + with self.subTest(block=block_index, offset=key, value=value): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + block = native["sheets"][0]["data"][block_index] + if value is None: + del block[key] + else: + block[key] = value + replace_native(packet, native) + self.refuse(packet) + + def test_native_sheet_block_row_and_cell_shapes_and_cardinalities_are_enforced(self): + for fault in ("root_array", "no_sheet", "two_sheets", "no_block", "extra_block", "block_order", + "short_rows", "extra_rows", "short_cells", "extra_cells", "cell_not_object", + "row_not_object", "block_not_object", "missing_properties", "missing_grid"): + with self.subTest(fault=fault): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + sheet = native["sheets"][0] + blocks = sheet["data"] + if fault == "root_array": + native = [native] + elif fault == "no_sheet": + native["sheets"] = [] + elif fault == "two_sheets": + native["sheets"].append(copy.deepcopy(sheet)) + elif fault == "no_block": + blocks.pop() + elif fault == "extra_block": + blocks.append(copy.deepcopy(blocks[2])) + elif fault == "block_order": + blocks[1], blocks[2] = blocks[2], blocks[1] + elif fault == "short_rows": + blocks[1]["rowData"].pop() + elif fault == "extra_rows": + blocks[1]["rowData"].append(copy.deepcopy(blocks[1]["rowData"][0])) + elif fault == "short_cells": + blocks[2]["rowData"][0]["values"].pop() + elif fault == "extra_cells": + blocks[2]["rowData"][0]["values"].append({}) + elif fault == "cell_not_object": + blocks[2]["rowData"][0]["values"][10] = "PRIVATE_BAD_CELL" + elif fault == "row_not_object": + blocks[2]["rowData"][0] = [] + elif fault == "block_not_object": + blocks[2] = [] + elif fault == "missing_properties": + del sheet["properties"] + else: + del sheet["properties"]["gridProperties"] + replace_native(packet, native) + result = self.refuse(packet) + self.assertNotIn(b"PRIVATE_BAD_CELL", result.stderr) + + def test_all_formatted_cells_are_strings_including_private_and_unselected_cells(self): + for block_index, row_index, column in ((1, 0, 1), (1, 1, 10), (1, 1, 11), (2, 0, 3)): + for value in (None, True, 3, 3.0, ["PRIVATE_ARRAY_CELL"], {"PRIVATE_OBJECT_CELL": "value"}): + with self.subTest(block=block_index, row=row_index, column=column, value=value): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + native["sheets"][0]["data"][block_index]["rowData"][row_index]["values"][column]["formattedValue"] = value + replace_native(packet, native) + result = self.refuse(packet) + self.assertNotIn(b"PRIVATE_ARRAY_CELL", result.stderr) + self.assertNotIn(b"PRIVATE_OBJECT_CELL", result.stderr) + + def test_formatted_cell_limit_counts_utf8_bytes_in_public_private_and_unselected_rows(self): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + native["sheets"][0]["data"][1]["rowData"][1]["values"][1]["formattedValue"] = "λ" * 4096 + replace_native(packet, native) + self.accept(packet) + for block_index, row_index, column in ((1, 1, 1), (1, 0, 10), (2, 0, 11)): + with self.subTest(block=block_index, row=row_index, column=column): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + native["sheets"][0]["data"][block_index]["rowData"][row_index]["values"][column]["formattedValue"] = "λ" * 4096 + "x" + replace_native(packet, native) + self.refuse(packet) + + def test_forged_column_policies_cannot_publish_source_notes_priority_or_topics(self): + cases = [PUBLIC_COLUMNS + [name] for name in ("Source", "Notes", "Priority", "Topics / object tags")] + cases.extend([list(reversed(PUBLIC_COLUMNS)), PUBLIC_COLUMNS[:-1], + PUBLIC_COLUMNS[:-1] + ["Source"], PUBLIC_COLUMNS[:-1] + [PUBLIC_COLUMNS[0]], + PUBLIC_COLUMNS[:-1] + [True], "PRIVATE_COLUMNS_STRING"]) + for columns in cases: + with self.subTest(columns=columns): + packet = copy.deepcopy(self.packet) + packet["policy"]["columns"] = columns + self.refuse(packet) + + def test_policy_ids_are_nonempty_unique_valid_and_present_in_native_rows(self): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + boundary_id = "T" + "x" * 191 + native["sheets"][0]["data"][1]["rowData"][1]["values"][0]["formattedValue"] = boundary_id + packet["policy"]["artifact_ids"] = ["TEST-ZETA", boundary_id] + replace_native(packet, native) + self.accept(packet) + for ids in ([], ["TEST-UNKNOWN"], ["TEST-ALPHA", "TEST-ALPHA"], [""], [True], [1], + ["TEST\nID"], [" TEST-ALPHA"], "TEST-ALPHA", None): + with self.subTest(ids=ids): + packet = copy.deepcopy(self.packet) + packet["policy"]["artifact_ids"] = ids + self.refuse(packet) + + def test_artifact_ids_are_globally_unique_and_valid_even_on_unselected_native_rows(self): + for value in ("TEST-ALPHA", "TEST-ZETA", "", True, 1, " TEST-UNSELECTED", "TEST\nUNSELECTED", + "T" + "x" * 192, "TEST-é", "TEST/UNSELECTED"): + with self.subTest(unselected_id=value): + packet = copy.deepcopy(self.packet) + native = get_native(packet) + native["sheets"][0]["data"][2]["rowData"][0]["values"][0]["formattedValue"] = value + replace_native(packet, native) + self.refuse(packet) + + def test_graph_ids_and_bindings_require_explicit_known_unique_identities(self): + packet = copy.deepcopy(self.packet) + packet["graph_nodes"] = ["T" + "x" * 191] + self.accept(packet) + for nodes in (["TEST.node.a", "TEST.node.a"], [""], [True], [1], ["TEST\nnode"], + [" TEST.node.a"], ["T" + "x" * 192], ["TEST.é"], ["TEST/node"], "TEST.node.a", None): + with self.subTest(nodes=nodes): + packet = copy.deepcopy(self.packet) + packet["graph_nodes"] = nodes + self.refuse(packet) + cases = [ + [{"node_id": "TEST.unknown", "artifact_id": "TEST-ALPHA", "roles": ["source"]}], + [{"node_id": "TEST.node.a", "artifact_id": "TEST-UNSELECTED", "roles": ["source"]}], + [{"node_id": "TEST.node.a", "artifact_id": "TEST-UNKNOWN", "roles": ["source"]}], + [{"node_id": True, "artifact_id": "TEST-ALPHA", "roles": ["source"]}], + [{"node_id": "TEST.node.a", "artifact_id": True, "roles": ["source"]}], + [{"node_id": "TEST.node.a", "artifact_id": "TEST-ALPHA", "roles": ["source"]}, + {"node_id": "TEST.node.a", "artifact_id": "TEST-ALPHA", "roles": ["review"]}], + [True], {}, None, + ] + for bindings in cases: + with self.subTest(bindings=bindings): + packet = copy.deepcopy(self.packet) + packet["bindings"] = bindings + self.refuse(packet) + + def test_binding_roles_are_nonempty_unique_strings_from_the_exact_six_role_set(self): + for roles in ([], ["accepted"], ["Source"], ["source", "source"], ["kernel", True], + [1], "source", None): + with self.subTest(roles=roles): + packet = copy.deepcopy(self.packet) + packet["bindings"] = [{"node_id": "TEST.node.a", "artifact_id": "TEST-ALPHA", "roles": roles}] + self.refuse(packet) + + def test_collection_limits_accept_4096_and_refuse_4097_without_duplicate_shortcuts(self): + packet = copy.deepcopy(self.packet) + packet["graph_nodes"] = ["TEST.node.%04d" % index for index in range(4096)] + packet["bindings"] = [{"node_id": node, "artifact_id": "TEST-ALPHA", "roles": ["source"]} + for node in packet["graph_nodes"]] + self.accept(packet) + too_many = copy.deepcopy(packet) + too_many["graph_nodes"].append("TEST.node.4096") + self.refuse(too_many) + too_many = copy.deepcopy(packet) + too_many["bindings"].append({"node_id": "TEST.node.0000", "artifact_id": "TEST-ZETA", "roles": ["review"]}) + self.refuse(too_many) + for count in (4096, 4097): + with self.subTest(native_rows_and_policy_ids=count): + many = copy.deepcopy(self.packet) + ids = ["TEST-ROW-%04d" % index for index in range(count)] + native = synthetic_native() + native["sheets"][0]["properties"]["gridProperties"]["rowCount"] = count + 1 + native["sheets"][0]["data"] = [ + {"rowData": [row(HEADER)]}, + {"startRow": 1, "rowData": [row([artifact_id] + ["TEST value"] * 9 + + ["PRIVATE_MANY_SOURCE", "PRIVATE_MANY_NOTES"]) + for artifact_id in ids]}, + ] + many["capture"]["source"]["ranges"] = [RANGES[0], "'TEST Artifact Index'!A2:L%d" % (count + 1)] + many["policy"]["artifact_ids"] = ids + replace_native(many, native) + result = self.accept(many) if count == 4096 else self.refuse(many) + self.assertNotIn(b"PRIVATE_MANY_", result.stdout + result.stderr) + + def test_stdin_limit_is_16_mib_of_bytes_including_legal_json_whitespace(self): + raw = canonical(self.packet) + bounded = raw + b" " * (16 * 1024 * 1024 - len(raw)) + self.accept(raw=bounded) + self.refuse(raw=bounded + b" ") + + def test_decoded_capture_limit_is_4_mib_of_original_bytes_not_base64_length(self): + packet = copy.deepcopy(self.packet) + raw = base64.b64decode(packet["capture"]["raw_base64"]) + bounded = raw + b" " * (4 * 1024 * 1024 - len(raw)) + replace_raw(packet, bounded) + self.accept(packet) + replace_raw(packet, bounded + b" ") + self.refuse(packet) + + def test_duplicate_keys_refuse_in_packet_wrappers_and_native_private_annotations(self): + raw = canonical(self.packet) + mutations = [ + raw[:-1] + b',"schema_version":1}', + raw.replace(b'"schema_version":1', b'"schema_version":1,"schema_version":1', 1), + raw.replace(b'"capture_id":"TEST-capture-1"', b'"capture_id":"TEST-capture-1","capture_id":"TEST-capture-1"', 1), + raw.replace(b'"artifact_ids":[', b'"artifact_ids":[],"artifact_ids":[', 1), + ] + for index, changed in enumerate(mutations): + with self.subTest(packet_duplicate=index): + self.refuse(raw=changed, expected=self.pins) + for marker, replacement in ( + (b'"spreadsheetId":"TEST-R17-SPREADSHEET"', b'"spreadsheetId":"TEST-R17-SPREADSHEET","spreadsheetId":"TEST-R17-SPREADSHEET"'), + (b'"formattedValue":"TEST-ZETA"', b'"formattedValue":"TEST-ZETA","formattedValue":"TEST-ZETA"'), + (b'"privateAnnotation":"PRIVATE_NATIVE_ANNOTATION"', b'"privateAnnotation":{"x":1,"x":1}'), + ): + with self.subTest(native_duplicate=marker): + packet = copy.deepcopy(self.packet) + native_raw = base64.b64decode(packet["capture"]["raw_base64"]) + self.assertIn(marker, native_raw) + replace_raw(packet, native_raw.replace(marker, replacement, 1)) + self.refuse(packet) + + def test_nonfinite_numbers_and_numeric_overflow_refuse_even_in_unknown_native_annotations(self): + for token in (b"NaN", b"Infinity", b"-Infinity", b"1e9999"): + with self.subTest(token=token, location="native_annotation"): + packet = copy.deepcopy(self.packet) + raw = base64.b64decode(packet["capture"]["raw_base64"]) + raw = raw.replace(b'"privateAnnotation":"PRIVATE_NATIVE_ANNOTATION"', b'"privateAnnotation":' + token, 1) + replace_raw(packet, raw) + self.refuse(packet) + with self.subTest(token=token, location="packet_schema"): + raw = canonical(self.packet).replace(b'"schema_version":1', b'"schema_version":' + token, 1) + self.refuse(raw=raw, expected=self.pins) + + def test_json_nesting_is_bounded_in_unknown_native_annotations_without_schema_masking(self): + packet = copy.deepcopy(self.packet) + raw = base64.b64decode(packet["capture"]["raw_base64"]) + marker = b'"privateAnnotation":"PRIVATE_NATIVE_ANNOTATION"' + self.assertIn(marker, raw) + # The native root object plus 63 nested arrays occupies 64 levels. + bounded = b"[" * 63 + b"0" + b"]" * 63 + replace_raw(packet, raw.replace(marker, b'"privateAnnotation":' + bounded, 1)) + self.accept(packet) + # One root object plus 64 arrays is exactly one level over the limit. + deep = b"[" * 64 + b"0" + b"]" * 64 + replace_raw(packet, raw.replace(marker, b'"privateAnnotation":' + deep, 1)) + self.refuse(packet) + + def test_malformed_utf8_json_and_multiple_documents_refuse_at_both_input_layers(self): + for raw in (b"", b"{", b"[]", b"null", canonical(self.packet) + b"{}", b"\xff", + canonical(self.packet).replace(b"TEST-capture-1", b"TEST-\xff-capture", 1)): + with self.subTest(packet_raw=raw[:20]): + self.refuse(raw=raw, expected=self.pins) + for raw in (b"", b"{", b"null", b"\xff", native_bytes(synthetic_native()) + b"{}", + native_bytes(synthetic_native()).replace(b"PRIVATE_NATIVE_ANNOTATION", b"PRIVATE_\xff_NATIVE", 1)): + with self.subTest(native_raw=raw[:20]): + packet = copy.deepcopy(self.packet) + replace_raw(packet, raw) + self.refuse(packet) + + def test_base64_is_strict_canonical_and_not_merely_decodable_to_the_expected_bytes(self): + encoded = self.packet["capture"]["raw_base64"] + for value in ("", "!!PRIVATE_BAD_BASE64!!", encoded + "=", encoded + "\n", " " + encoded, + encoded[:-1], True, None): + with self.subTest(base64_type=type(value).__name__, value_prefix=str(value)[:20]): + packet = copy.deepcopy(self.packet) + packet["capture"]["raw_base64"] = value + result = self.refuse(packet, expected=self.pins) + self.assertNotIn(b"PRIVATE_BAD_BASE64", result.stderr) + packet = copy.deepcopy(self.packet) + raw = base64.b64decode(encoded) + raw += b" " * ((1 - len(raw)) % 3) + replace_raw(packet, raw) + canonical_encoded = packet["capture"]["raw_base64"] + self.assertTrue(canonical_encoded.endswith("==")) + alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + index = alphabet.index(canonical_encoded[-3]) + alternate = canonical_encoded[:-3] + alphabet[index + 1] + "==" + self.assertEqual(base64.b64decode(alternate, validate=True), raw) + expected = pins_for(packet) + self.accept(packet) + packet["capture"]["raw_base64"] = alternate + self.refuse(packet, expected=expected) + + def test_urls_paths_and_code_are_inert_data_without_fetch_execution_or_filesystem_writes(self): + requests = [] + + class Handler(http.server.BaseHTTPRequestHandler): + def do_GET(self): + requests.append(self.path) + self.send_response(200) + self.end_headers() + self.wfile.write(b"PRIVATE_FETCH_RESULT") + + def log_message(self, *args): + pass + + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + marker = self.workdir / "PRIVATE_EXECUTION_MARKER" + secret = self.workdir / "PRIVATE_LOCAL_SOURCE" + secret.write_text("PRIVATE_LOCAL_FILE_CONTENT", encoding="utf-8") + packet = copy.deepcopy(self.packet) + native = get_native(packet) + code = "__import__('pathlib').Path(%r).write_text('executed')" % str(marker) + url = "http://127.0.0.1:%d/PRIVATE_URL_PATH" % server.server_port + probe = "http://127.0.0.1:%d/TEST_OBSERVER_PROBE" % server.server_port + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + with opener.open(probe, timeout=5) as response: + self.assertEqual(response.read(), b"PRIVATE_FETCH_RESULT") + self.assertEqual(requests, ["/TEST_OBSERVER_PROBE"]) + requests.clear() + public = "inert URL %s; inert code %s; path %s" % (url, code, secret) + values = native["sheets"][0]["data"][1]["rowData"][1]["values"] + values[1]["formattedValue"] = public + values[10]["formattedValue"] = url + values[11]["formattedValue"] = code + native["privateAnnotation"] = {"file": str(secret), "code": code, "url": url} + replace_native(packet, native) + before = sorted(path.name for path in self.workdir.iterdir()) + result = self.accept(packet, audit={"secret": secret}) + report = result.audit_report + labels = {"read", "write", "mkdir", "remove", "rename", "rmdir"} + self.assertEqual(set(report), {"calibration", "attempts", "optimize"}) + self.assertIs(type(report["optimize"]), int) + self.assertEqual(report["optimize"], sys.flags.optimize) + self.assertEqual(set(report["calibration"]), labels) + for count in report["calibration"].values(): + self.assertIs(type(count), int) + self.assertGreater(count, 0) + self.assertEqual(report["attempts"], dict.fromkeys(labels, 0)) + for count in report["attempts"].values(): + self.assertIs(type(count), int) + projected = json.loads(result.stdout) + self.assertEqual(projected["rows"][0]["fields"]["Title"], public) + self.assertEqual(requests, []) + self.assertFalse(marker.exists()) + self.assertEqual(secret.read_text(encoding="utf-8"), "PRIVATE_LOCAL_FILE_CONTENT") + self.assertEqual(sorted(path.name for path in self.workdir.iterdir()), before) + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_architecture_cli_boundaries.py b/tests/test_architecture_cli_boundaries.py new file mode 100644 index 000000000..9b526cae5 --- /dev/null +++ b/tests/test_architecture_cli_boundaries.py @@ -0,0 +1,225 @@ +"""External CLI boundary regressions; execute only in hosted isolation. + +The sibling package and its private-input canary are trusted, synthetic TEST +instruments. They never refer to an actual private capture or user file. Existing +fixture modules are imported as modules, so their TestCases are not rediscovered +or inherited by these four controls. A passing diagnostic is not the full suite. +""" + +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +import test_architecture_artifact_index as artifact_fixtures +import test_architecture_graph_artifact_readback as graph_fixtures + + +ROOT = Path(__file__).resolve().parents[1] +ARTIFACT_CLI = ROOT / "tools/artifact_index_export.py" +GRAPH_CLI = ROOT / "tools/architecture_graph_artifact_readback.py" +CHILD_DEADLINE_SECONDS = 20 +PRIVATE_CANARY = b"TEST_ONLY_SYNTHETIC_PRIVATE_INPUT_CANARY" +# Trusted test instrument: the real stdout descriptor receives a real Python +# buffered stream. No exception, captured value or product result is fabricated. +BUFFERED_STDOUT_LAUNCHER = r''' +import io +import runpy +import sys +capacity, cli = int(sys.argv[1]), sys.argv[2] +sys.stdout = io.TextIOWrapper( + io.BufferedWriter(io.FileIO(1, mode="wb", closefd=False), buffer_size=capacity), + encoding="utf-8", newline="\n") +sys.argv = [cli, *sys.argv[3:]] +runpy.run_path(cli, run_name="__main__") +''' + + +class ArchitectureCliBoundaryContract(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="TEST-cli-boundary-") + self.addCleanup(temporary.cleanup) + self.cwd = Path(temporary.name) + self.environment = dict(os.environ) + for name in ("PYTHONPATH", "PYTHONHOME", "PYTHONOPTIMIZE", "PYTHONUNBUFFERED"): + self.environment.pop(name, None) + + def python_flags(self): + flags = ["-I", "-B", "-S"] + if sys.flags.optimize: + flags.append("-" + "O" * sys.flags.optimize) + return flags + + def invoke(self, command, raw=b"", *, closed_stdout=False): + """Launch the real child, optionally with a pipe that has no reader. + + The reader is closed before Popen, so no child write or shutdown flush + can race a parent read/close. Parent and child each close their writer. + """ + writer = None + if closed_stdout: + reader, writer = os.pipe() + os.close(reader) + try: + process = subprocess.Popen( + command, stdin=subprocess.PIPE, + stdout=writer if closed_stdout else subprocess.PIPE, + stderr=subprocess.PIPE, cwd=self.cwd, env=self.environment, + ) + finally: + if writer is not None: + os.close(writer) + try: + stdout, stderr = process.communicate(raw, timeout=CHILD_DEADLINE_SECONDS) + except subprocess.TimeoutExpired: + process.kill() + # Killing the child closes its pipes; keep cleanup bounded as well. + process.communicate(timeout=5) + self.fail("CLI child exceeded its 20-second deadline") + return subprocess.CompletedProcess(command, process.returncode, stdout or b"", stderr) + + def artifact_case(self): + packet = artifact_fixtures.synthetic_packet() + expected = artifact_fixtures.pins_for(packet) + arguments = [part for name, flag in artifact_fixtures.PIN_FLAGS.items() + for part in (flag, expected[name])] + return ARTIFACT_CLI, arguments, artifact_fixtures.canonical(packet) + b"\n" + + def graph_case(self): + # Reuse the existing fixture builder and its source-pin assertions, + # without inheriting or executing any of its test methods. + fixture = graph_fixtures.ArchitectureGraphArtifactReadbackContract() + self.addCleanup(fixture.doCleanups) + fixture.setUp() + raw = graph_fixtures.classic_zip(fixture.entries()) + archive = fixture.cwd / "TEST-cli-boundary-input.zip" + archive.write_bytes(raw) + return GRAPH_CLI, fixture.arguments(archive, fixture.expected(raw)), b"" + + def command(self, cli, arguments): + self.assertTrue(cli.is_file(), "required external CLI is absent") + return [sys.executable, *self.python_flags(), str(cli), *arguments] + + def accepted_baseline(self, case): + cli, arguments, raw = case + result = self.invoke(self.command(cli, arguments), raw) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, b"") + document = json.loads(result.stdout) + self.assertEqual(document["scientific_effect"], "NONE") + self.assertIs(document["scientific_status_authority"], False) + self.assertEqual(document["custody"], "unknown") + return result.stdout + + def symlink_does_not_select_sibling_package(self, case): + cli, arguments, raw = case + baseline = self.accepted_baseline(case) + attack_root = self.cwd / "TEST-attacker-source-root" + tools = attack_root / "tools" + package = attack_root / "architecture" + tools.mkdir(parents=True) + package.mkdir() + secret = self.cwd / "TEST-synthetic-private-input" + marker = self.cwd / "TEST-sibling-package-read-marker" + secret.write_bytes(PRIVATE_CANARY) + observed = PRIVATE_CANARY + b"\nOPTIMIZE=" + str(sys.flags.optimize).encode("ascii") + package.joinpath("__init__.py").write_text( + "from pathlib import Path\n" + "import sys\n" + "Path(" + repr(str(marker)) + ").write_bytes(\n" + " Path(" + repr(str(secret)) + ").read_bytes()\n" + " + b'\\nOPTIMIZE=' + str(sys.flags.optimize).encode('ascii'))\n" + "raise RuntimeError('TEST_SYNTHETIC_SIBLING_PACKAGE_EXECUTED')\n", + encoding="utf-8", + ) + linked_cli = tools / cli.name + linked_cli.symlink_to(cli) + + # Positive calibration deliberately imports this trusted TEST package. + # It proves the marker detects a synthetic private read in this mode. + calibration = self.invoke([ + sys.executable, *self.python_flags(), "-c", + "import sys; sys.path.insert(0, sys.argv[1]); import architecture", + str(attack_root), + ]) + self.record_diagnostic_child("synthetic-sibling-package-calibration", calibration) + self.assertNotEqual(calibration.returncode, 0) + self.assertIn(b"TEST_SYNTHETIC_SIBLING_PACKAGE_EXECUTED", calibration.stderr) + self.assertEqual(marker.read_bytes(), observed, "sibling-package observer was not live") + marker.unlink() + + result = self.invoke(self.command(linked_cli, arguments), raw) + self.record_diagnostic_child("symlinked-cli-with-synthetic-sibling", result) + self.assertEqual(secret.read_bytes(), PRIVATE_CANARY) + self.assertFalse(marker.exists(), "symlink-selected sibling package read the synthetic private input") + self.assertNotIn(PRIVATE_CANARY, result.stdout + result.stderr) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, b"") + self.assertEqual(result.stdout, baseline, "symlink invocation changed the validated public unit") + + def record_diagnostic_child(self, variant, result): + # Only the focused hosted workflow sets this path. Do not print between + # unittest's progress and result tokens in the required full-suite log. + report = self.environment.get("ARCHITECTURE_CLI_BOUNDARY_REPORT") + if report is not None: + record = { + "test_id": self.id(), "variant": variant, + "optimize": sys.flags.optimize, "returncode": result.returncode, + "stdout_bytes": len(result.stdout), + "stderr": result.stderr.decode("utf-8", errors="backslashreplace"), + } + with open(report, "ab") as stream: + stream.write((json.dumps(record, sort_keys=True) + "\n").encode("utf-8")) + + def assert_opaque_refusal(self, result, refusal): + self.assertEqual(result.returncode, 1, result.stderr) + self.assertEqual(result.stdout, b"") + self.assertEqual(result.stderr, refusal) + + def broken_stdout_has_opaque_refusal(self, case, refusal, *, buffered_flush=False): + # This positive run ensures an unrelated argument/fixture refusal cannot + # satisfy the closed-reader protocol assertion below. + baseline = self.accepted_baseline(case) + cli, arguments, raw = case + with self.subTest(stdout_boundary="direct CLI"): + result = self.invoke(self.command(cli, arguments), raw, closed_stdout=True) + self.record_diagnostic_child("direct-closed-reader", result) + self.assert_opaque_refusal(result, refusal) + if buffered_flush: + # The graph unit can exceed Python's default buffer and fail during + # direct write, leaving no bytes for shutdown to flush. A larger + # real buffer exercises flush-time failure instead. Its capacity is + # derived from the already accepted bounded synthetic unit only. + capacity = len(baseline) + 1 + self.assertLessEqual(capacity, 16 * 1024 * 1024 + 1) + command = [sys.executable, *self.python_flags(), "-c", BUFFERED_STDOUT_LAUNCHER, + str(capacity), str(cli), *arguments] + calibration = self.invoke(command, raw) + self.record_diagnostic_child("buffered-open-reader-calibration", calibration) + self.assertEqual(calibration.returncode, 0, calibration.stderr) + self.assertEqual(calibration.stderr, b"") + self.assertEqual(calibration.stdout, baseline) + with self.subTest(stdout_boundary="real buffered flush"): + result = self.invoke(command, raw, closed_stdout=True) + self.record_diagnostic_child("buffered-closed-reader", result) + self.assert_opaque_refusal(result, refusal) + + def test_artifact_index_symlink_cannot_read_synthetic_private_input(self): + self.symlink_does_not_select_sibling_package(self.artifact_case()) + + def test_graph_readback_symlink_cannot_read_synthetic_private_input(self): + self.symlink_does_not_select_sibling_package(self.graph_case()) + + def test_artifact_index_broken_stdout_has_one_opaque_refusal_and_exit_one(self): + self.broken_stdout_has_opaque_refusal(self.artifact_case(), b"ARTIFACT_INDEX_REFUSED\n") + + def test_graph_readback_broken_stdout_has_one_opaque_refusal_and_exit_one(self): + self.broken_stdout_has_opaque_refusal( + self.graph_case(), b"ARCHITECTURE_GRAPH_READBACK_REFUSED\n", buffered_flush=True) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_architecture_graph_artifact_readback.py b/tests/test_architecture_graph_artifact_readback.py new file mode 100644 index 000000000..5ae0aa7e4 --- /dev/null +++ b/tests/test_architecture_graph_artifact_readback.py @@ -0,0 +1,1185 @@ +"""External CLI controls for graph archive readback; run only in hosted isolation. + +All native identities, logs, console text and runtime-image declarations below +are synthetic TEST fixtures. No fixture log establishes a native run or custody. +The graph records come from the already-public, independently pinned source. +This targeted audit observer is a test instrument, not an operating-system sandbox. +""" + +import copy +import hashlib +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import json +import os +from pathlib import Path +import stat +import struct +import subprocess +import sys +import tempfile +import threading +import unittest +from urllib.request import urlopen +import zlib + + +ROOT = Path(__file__).resolve().parents[1] +CLI = ROOT / "tools/architecture_graph_artifact_readback.py" +SOURCE = ROOT / "docs/site/dependency-source/GRAPH.json" +SOURCE_SHA = "8822e9618678321a342d69cd0b8ae6552de1b5d578c331de5072b2892ee9dd09" +SOURCE_BYTES = 38753 +IMAGE_DIGEST = "python@sha256:83f339c1be6340ae1096010fdccf6552ac932d8f410d45d206014916bdf37e48" +REFUSAL = b"ARCHITECTURE_GRAPH_READBACK_REFUSED\n" +MIB = 1024 * 1024 +GRAPH_SOURCE = { + "repository": "d6g8k5htny-coder/Math-", + "commit": "7858329974e28be79f29b22644370084ff43da4f", + "captured_at": "2026-10-03T15:07:03Z", + "graph_sha256": SOURCE_SHA, + "gate_sha256": "a78f3e25f3b0cfe113e618a4c31a7a25d7f22af638c46dec1ecba221fa333ac8", +} +NATIVE = {"checked_commit": "a" * 40, "repository": "TEST-owner/TEST-repository", + "run_id": "123", "run_attempt": "1", "artifact_id": "456"} +MEMBERS = ( + "runtime-image.json", "checked-commit.txt", "run-id.txt", "run-attempt.txt", + "container-exit-status.txt", "console.log", "tests-normal.log", "tests-optimized.log", + "exporter.stdout", "exporter.stderr", "generated/graph.json", "architecture-run-receipt.json", +) +EVIDENCE = ( + "generated/graph.json", "checked-commit.txt", "run-id.txt", "run-attempt.txt", + "container-exit-status.txt", "tests-normal.log", "tests-optimized.log", "runtime-image.json", +) +REQUIRED_IDS = tuple(""" +test_architecture_binding.ArchitectureBindingContract.test_artifact_id_requires_positive_ascii_decimal_without_leading_zero +test_architecture_binding.ArchitectureBindingContract.test_artifact_name_is_exactly_bound_to_current_run_and_attempt +test_architecture_binding.ArchitectureBindingContract.test_attempt_ten_is_a_positive_decimal_not_a_single_digit +test_architecture_binding.ArchitectureBindingContract.test_commit_format_cannot_be_authorized_by_an_equally_invalid_native_commit +test_architecture_binding.ArchitectureBindingContract.test_deeply_nested_json_is_refused_without_a_success_envelope +test_architecture_binding.ArchitectureBindingContract.test_duplicate_top_level_keys_are_refused_even_when_last_value_is_valid +test_architecture_binding.ArchitectureBindingContract.test_each_current_native_argument_is_used_for_identity_binding +test_architecture_binding.ArchitectureBindingContract.test_each_duplicate_output_key_is_refused_even_when_last_value_is_valid +test_architecture_binding.ArchitectureBindingContract.test_each_expected_native_identity_argument_is_required +test_architecture_binding.ArchitectureBindingContract.test_each_non_success_result_is_refused +test_architecture_binding.ArchitectureBindingContract.test_each_of_the_eight_outputs_is_required +test_architecture_binding.ArchitectureBindingContract.test_each_output_rejects_an_empty_string +test_architecture_binding.ArchitectureBindingContract.test_each_output_rejects_non_string_identities_without_coercion +test_architecture_binding.ArchitectureBindingContract.test_each_top_level_field_is_required +test_architecture_binding.ArchitectureBindingContract.test_extra_output_fields_are_refused +test_architecture_binding.ArchitectureBindingContract.test_extra_top_level_fields_are_refused +test_architecture_binding.ArchitectureBindingContract.test_invalid_or_multiple_json_documents_are_refused +test_architecture_binding.ArchitectureBindingContract.test_non_finite_json_numbers_are_refused +test_architecture_binding.ArchitectureBindingContract.test_non_object_top_level_values_are_refused +test_architecture_binding.ArchitectureBindingContract.test_other_valid_native_identities_are_accepted_and_preserved +test_architecture_binding.ArchitectureBindingContract.test_outputs_must_be_an_object +test_architecture_binding.ArchitectureBindingContract.test_receipt_and_artifact_sha256_require_lowercase_64_hex_digits +test_architecture_binding.ArchitectureBindingContract.test_repository_substitution_and_case_drift_are_refused +test_architecture_binding.ArchitectureBindingContract.test_run_and_attempt_decimal_format_cannot_be_authorized_by_invalid_native_args +test_architecture_binding.ArchitectureBindingContract.test_stale_attempt_is_refused_even_with_a_matching_stale_artifact_name +test_architecture_binding.ArchitectureBindingContract.test_stale_checked_commit_is_refused +test_architecture_binding.ArchitectureBindingContract.test_stale_run_is_refused_even_with_a_matching_stale_artifact_name +test_architecture_binding.ArchitectureBindingContract.test_valid_current_run_has_exact_stable_non_scientific_output +test_architecture_binding.ArchitectureBindingContract.test_valid_json_at_exact_stdin_byte_limit_is_accepted +test_architecture_binding.ArchitectureBindingContract.test_valid_json_over_stdin_byte_limit_is_refused +test_architecture_graph_export.ArchitectureGraphExportContract.test_caller_pinned_provenance_cannot_authorize_different_gate_code +test_architecture_graph_export.ArchitectureGraphExportContract.test_classification_and_proof_review_text_cannot_manufacture_evidence +test_architecture_graph_export.ArchitectureGraphExportContract.test_context_only_cycle_is_allowed_without_changing_gate_semantics +test_architecture_graph_export.ArchitectureGraphExportContract.test_default_trust_rejects_self_consistent_changed_graph_and_provenance +test_architecture_graph_export.ArchitectureGraphExportContract.test_dimensions_describe_only_explicit_recorded_metadata +test_architecture_graph_export.ArchitectureGraphExportContract.test_duplicate_graph_json_keys_are_rejected_after_identity_validation +test_architecture_graph_export.ArchitectureGraphExportContract.test_duplicate_provenance_json_keys_are_rejected_before_gate_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_exact_source_bound_hard_gate_validator_is_called +test_architecture_graph_export.ArchitectureGraphExportContract.test_failed_export_preserves_an_earlier_valid_export +test_architecture_graph_export.ArchitectureGraphExportContract.test_gate_identity_is_checked_before_untrusted_gate_executes +test_architecture_graph_export.ArchitectureGraphExportContract.test_graph_identity_is_checked_before_gate_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_missing_required_dependency_is_rejected_by_pinned_hard_gate +test_architecture_graph_export.ArchitectureGraphExportContract.test_pinned_49_node_55_edge_export_is_deterministic_and_preserves_records +test_architecture_graph_export.ArchitectureGraphExportContract.test_required_dependency_cycle_is_rejected_by_pinned_hard_gate +test_architecture_graph_export.ArchitectureGraphExportContract.test_same_size_gate_change_is_rejected_by_sha_before_import +test_architecture_graph_export.ArchitectureGraphExportContract.test_source_byte_counts_are_validated_even_when_sha_matches +test_architecture_graph_export.ArchitectureGraphExportContract.test_symlink_source_directory_is_rejected +test_architecture_graph_export.ArchitectureGraphExportContract.test_symlink_source_files_are_rejected_before_gate_import +test_architecture_receipt.ArchitectureReceiptContract.test_attempt_ten_and_other_native_repository_are_preserved +test_architecture_receipt.ArchitectureReceiptContract.test_changed_node_or_edge_is_refused_even_when_graph_source_pins_are_unchanged +test_architecture_receipt.ArchitectureReceiptContract.test_directory_or_fifo_member_is_refused_without_waiting_for_a_writer +test_architecture_receipt.ArchitectureReceiptContract.test_duplicate_json_keys_are_refused_even_when_last_value_is_valid +test_architecture_receipt.ArchitectureReceiptContract.test_duplicate_test_identity_or_multiple_runs_are_refused +test_architecture_receipt.ArchitectureReceiptContract.test_each_expected_host_identity_argument_is_used_and_required +test_architecture_receipt.ArchitectureReceiptContract.test_each_native_identity_file_must_match_current_host_arguments +test_architecture_receipt.ArchitectureReceiptContract.test_each_required_sandbox_control_rejects_skipped_failed_or_error_status +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_is_required +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_must_be_regular_and_cannot_be_a_symlink +test_architecture_receipt.ArchitectureReceiptContract.test_each_staged_member_over_16_mib_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_empty_or_zero_discovered_test_logs_are_refused_in_each_mode +test_architecture_receipt.ArchitectureReceiptContract.test_every_graph_source_pin_is_required_and_cannot_be_substituted +test_architecture_receipt.ArchitectureReceiptContract.test_every_required_identity_is_needed_even_when_53_other_tests_are_ok +test_architecture_receipt.ArchitectureReceiptContract.test_evidence_directory_or_generated_parent_symlink_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_graph_and_runtime_inspect_require_strict_json +test_architecture_receipt.ArchitectureReceiptContract.test_graph_root_schema_and_non_scientific_flags_are_exact +test_architecture_receipt.ArchitectureReceiptContract.test_invalid_matching_host_identity_does_not_authorize_bad_commit_or_decimal +test_architecture_receipt.ArchitectureReceiptContract.test_later_all_ok_test_methods_are_counted_and_bound +test_architecture_receipt.ArchitectureReceiptContract.test_malformed_extra_result_lines_cannot_be_ignored_before_the_success_footer +test_architecture_receipt.ArchitectureReceiptContract.test_malformed_foreign_or_shortname_mismatched_test_identities_are_refused +test_architecture_receipt.ArchitectureReceiptContract.test_missing_extra_or_forged_dimension_cannot_manufacture_scientific_evidence +test_architecture_receipt.ArchitectureReceiptContract.test_native_repository_argument_must_have_owner_and_repository_components +test_architecture_receipt.ArchitectureReceiptContract.test_nested_record_types_cannot_change_even_when_python_values_compare_equal +test_architecture_receipt.ArchitectureReceiptContract.test_non_finite_numbers_are_refused_even_in_additional_inspect_fields +test_architecture_receipt.ArchitectureReceiptContract.test_nonunderscore_unittest_names_are_accepted_as_additional_controls +test_architecture_receipt.ArchitectureReceiptContract.test_nonunderscore_unittest_prefix_malformed_results_are_not_diagnostics +test_architecture_receipt.ArchitectureReceiptContract.test_nonzero_or_malformed_container_exit_is_refused +test_architecture_receipt.ArchitectureReceiptContract.test_normal_and_optimized_must_discover_the_same_additional_passing_tests +test_architecture_receipt.ArchitectureReceiptContract.test_runtime_inspect_is_one_linux_amd64_image_with_the_fixed_digest +test_architecture_receipt.ArchitectureReceiptContract.test_skip_or_failure_in_additional_discovered_test_is_also_refused +test_architecture_receipt.ArchitectureReceiptContract.test_summary_must_report_exact_discovered_count_and_terminal_ok +test_architecture_receipt.ArchitectureReceiptContract.test_valid_53_test_receipt_is_exact_deterministic_and_preserves_staged_bytes +test_architecture_receipt.ArchitectureReceiptContract.test_valid_inspected_image_id_is_preserved_instead_of_hardcoded +test_architecture_receipt.ArchitectureReceiptContract.test_valid_log_at_exact_16_mib_limit_is_accepted +test_architecture_receipt.ArchitectureReceiptContract.test_valid_log_larger_than_64_kib_is_accepted_and_raw_bytes_are_hashed +test_architecture_sandbox.ArchitectureSandbox.test_kernel_enforces_cpu_memory_and_process_limits +test_architecture_sandbox.ArchitectureSandbox.test_network_namespace_has_no_external_interface_or_route +test_architecture_sandbox.ArchitectureSandbox.test_nonroot_and_no_new_privileges +test_architecture_sandbox.ArchitectureSandbox.test_output_quota_stops_disk_exhaustion_and_cleanup_recovers_space +test_architecture_sandbox.ArchitectureSandbox.test_source_mount_is_read_only_and_environment_has_no_publishing_secret +""".split()) +REQUIRED_IDS_SHA = "b51eaf6cfaede42105ad377c066c4b18789e320ccf542b6817a6850c350fc9ef" + + +def sha(raw): + return hashlib.sha256(raw).hexdigest() + + +def encoded(value): + return (json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, + allow_nan=False) + "\n").encode("ascii") + + +def document(raw): + return json.loads(raw) + + +def synthetic_log(ids=REQUIRED_IDS): + rows = ["Synthetic TEST fixture only; no executed tests or authenticated custody."] + rows.extend(identity.rsplit(".", 1)[1] + " (" + identity + ") ... ok" for identity in ids) + rows.extend(["", "----------------------------------------------------------------------", + "Ran " + str(len(ids)) + " tests in 0.001s", "", "OK", ""]) + return "\n".join(rows).encode("ascii") + + +def padded_log(raw, length): + footer = raw.index(b"\n----------------------------------------------------------------------\n") + padding = length - len(raw) + if padding < 0: + raise ValueError("TEST requested padding below fixture length") + return raw[:footer] + b"\n" + b"x" * max(0, padding - 1) + raw[footer:] if padding else raw + + +def classic_zip(entries, options=None, *, comment=b"", prefix=b"", trailing=b"", zip64=False, + disk=0): + """Construct classic ZIP fixtures independently; this is not an acceptance parser.""" + options = {} if options is None else options + body, central = bytearray(prefix), bytearray() + for index, (name, raw) in enumerate(entries): + option = options.get(index, {}) + filename = option.get("name", name) + filename = filename.encode("utf-8") if isinstance(filename, str) else filename + method, flags = option.get("method", 8), option.get("flags", 0) + compressor = zlib.compressobj(level=6, wbits=-15) + compressed = raw if method == 0 else compressor.compress(raw) + compressor.flush() + compressed = option.get("compressed", compressed) + crc, size, expanded = zlib.crc32(raw) & 0xffffffff, len(compressed), len(raw) + crc, size, expanded = option.get("crc", crc), option.get("size", size), option.get("expanded", expanded) + extra, member_comment = option.get("extra", b""), option.get("comment", b"") + body.extend(option.get("before", b"")) + offset = len(body) + local = {"flags": flags, "method": method, "crc": 0 if flags & 8 else crc, + "size": 0 if flags & 8 else size, "expanded": 0 if flags & 8 else expanded} + local.update(option.get("local", {})) + local_name = option.get("local_name", filename) + body.extend(struct.pack(" 1 else None): + observed(label) + elif event in ("os.rename", "os.replace"): + if fixture_path(args[0], args[2] if len(args) > 2 else None) or fixture_path(args[1], args[3] if len(args) > 3 else None): + observed("rename") + elif event == "socket.connect": + observed("network") +sys.addaudithook(audit) +try: + with open(secret, "rb") as stream: + stream.read() + calibration = os.path.join(cwd, "TEST-calibration") + with open(calibration, "wb") as stream: + stream.write(b"TEST calibration only") + moved = calibration + "-moved" + os.rename(calibration, moved) + os.remove(moved) + os.mkdir(calibration) + os.listdir(cwd) + os.rmdir(calibration) + with socket.socket() as probe: + probe.settimeout(5) + probe.connect(("127.0.0.1", observer_port)) + calibration_counts = dict(counts) + counts = dict.fromkeys(labels, 0) + active = True + sys.argv = [cli, *arguments] + sys.path.insert(0, os.path.dirname(cli)) + runpy.run_path(cli, run_name="__main__") +finally: + raw = (json.dumps({"calibration": calibration_counts, "attempts": counts, + "optimize": sys.flags.optimize}, sort_keys=True) + "\n").encode("ascii") + os.write(report_fd, raw) + os.close(report_fd) +''' + + +class ArchitectureGraphArtifactReadbackContract(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.cwd = Path(temporary.name) + source_raw = SOURCE.read_bytes() + self.assertEqual(len(source_raw), SOURCE_BYTES) + self.assertEqual(sha(source_raw), SOURCE_SHA, "public source fixture changed") + self.assertEqual(len(REQUIRED_IDS), 89) + self.assertEqual(sha(("\n".join(sorted(REQUIRED_IDS)) + "\n").encode("ascii")), REQUIRED_IDS_SHA) + source = document(source_raw) + self.graph = { + "schema_version": 1, "source": GRAPH_SOURCE, "nodes": source["nodes"], "edges": source["edges"], + "dimensions": {identity: { + "source": "recorded" if node.get("source") else "not_recorded", + "review": "recorded" if any(node.get(field) for field in ( + "review_source", "review_issue", "review_basis", "review_provider", + "review_providers", "review_disposition")) else "not_recorded", + "kernel": "not_recorded", "computation": "not_recorded", "alignment": "not_recorded", + } for identity, node in source["nodes"].items()}, + "scientific_effect": "NONE", "scientific_status_authority": False, + } + self.base = self.fixture() + + def fixture(self, context=None, ids=REQUIRED_IDS): + context = NATIVE if context is None else context + image = {"Id": "sha256:" + "f" * 64, "RepoDigests": [IMAGE_DIGEST], + "Os": "linux", "Architecture": "amd64", "TEST_annotation": "synthetic image declaration"} + members = { + "generated/graph.json": encoded(self.graph), + "checked-commit.txt": (context["checked_commit"] + "\n").encode("ascii"), + "run-id.txt": (context["run_id"] + "\n").encode("ascii"), + "run-attempt.txt": (context["run_attempt"] + "\n").encode("ascii"), + "container-exit-status.txt": b"0\n", "runtime-image.json": encoded([image]), + "tests-normal.log": synthetic_log(ids), "tests-optimized.log": synthetic_log(ids), + "console.log": b"Synthetic TEST fixture, inert diagnostics; no executed native run.\n", + "exporter.stdout": b"/output/generated/graph.json\n", "exporter.stderr": b"", + } + receipt = {"schema_version": 1, "scientific_effect": "NONE", "scientific_status_authority": False, + **{key: context[key] for key in ("checked_commit", "repository", "run_id", "run_attempt")}, + "runtime_image": {"id": image["Id"], "repo_digest": IMAGE_DIGEST}, + "test_modes": [{"mode": mode, "test_count": len(ids), "test_ids": sorted(ids)} + for mode in ("normal", "optimized")], + "graph_source": GRAPH_SOURCE, + "evidence_file_sha256": {name: sha(members[name]) for name in EVIDENCE}} + members["architecture-run-receipt.json"] = encoded(receipt) + return members + + def rebind(self, members): + receipt = document(members["architecture-run-receipt.json"]) + receipt["evidence_file_sha256"] = {name: sha(members[name]) for name in EVIDENCE} + members["architecture-run-receipt.json"] = encoded(receipt) + + def change_json(self, members, name, change, *, rebind=True): + value = document(members[name]) + change(value) + members[name] = encoded(value) + if rebind: + self.rebind(members) + + def entries(self, members=None, manifest=None, order=None): + members = self.base if members is None else members + if manifest is None: + manifest = "".join(sha(members[name]) + " " + name + "\n" for name in sorted(members)).encode("ascii") + names = list(members) if order is None else order + return [(name, members[name]) for name in names] + [("ARTIFACT_SHA256SUMS", manifest)] + + def expected(self, raw, members=None, context=None): + members, context = (self.base if members is None else members), (NATIVE if context is None else context) + return {**context, "receipt_sha256": sha(members.get("architecture-run-receipt.json", self.base["architecture-run-receipt.json"])), + "artifact_sha256": sha(raw), "artifact_name": "research-architecture-" + context["run_id"] + "-" + context["run_attempt"]} + + def arguments(self, path, expected): + aliases = {"checked_commit": "commit", "repository": "repository", "run_id": "run-id", + "run_attempt": "run-attempt", "receipt_sha256": "receipt-sha256", "artifact_id": "artifact-id", + "artifact_sha256": "artifact-sha256", "artifact_name": "artifact-name"} + return ["--archive", str(path), *[part for field, flag in aliases.items() + for part in ("--expected-" + flag, expected[field])]] + + def invoke(self, raw=None, *, members=None, expected=None, arguments=None, path=None, + audit=False, observer_port=None): + self.assertTrue(CLI.is_file(), "required graph readback CLI tools/architecture_graph_artifact_readback.py is absent") + members = self.base if members is None else members + raw = classic_zip(self.entries(members)) if raw is None else raw + path = self.cwd / "TEST-input.zip" if path is None else path + if arguments is None and not path.exists() and not path.is_symlink(): + path.write_bytes(raw) + elif arguments is None and path.is_file() and not path.is_symlink(): + path.write_bytes(raw) + expected = self.expected(raw, members) if expected is None else expected + arguments = self.arguments(path, expected) if arguments is None else arguments + flags = ["-B", "-S"] + if sys.flags.optimize: + flags.append("-" + "O" * sys.flags.optimize) + command = [sys.executable, *flags, str(CLI), *arguments] + if audit: + command = [sys.executable, *flags, "-I", "-c", AUDIT_LAUNCHER, str(self.cwd), str(path), + str(self.cwd / "TEST-private-secret"), str(self.cwd / "TEST-observer.json"), str(CLI), + str(observer_port), *arguments] + environment = os.environ.copy() + environment.pop("PYTHONOPTIMIZE", None) + try: + return subprocess.run(command, cwd=self.cwd, env=environment, capture_output=True, + timeout=20, check=False) + except subprocess.TimeoutExpired: + self.fail("graph archive CLI exceeded the bounded 20-second fixture run") + + def accepted(self, raw=None, *, manifest=None, **kwargs): + members = kwargs.get("members", self.base) + manifest = self.entries(members)[-1][1] if manifest is None else manifest + raw = classic_zip(self.entries(members)) if raw is None else raw + expected = kwargs.get("expected", self.expected(raw, members)) + result = self.invoke(raw, **kwargs) + self.assertEqual(result.returncode, 0, result.stdout[-2000:] + result.stderr) + self.assertEqual(result.stderr, b"") + self.assertLessEqual(len(result.stdout), 16 * MIB) + report = document(result.stdout) + self.assertEqual(set(report), {"schema_version", "scientific_effect", "scientific_status_authority", + "custody", "graph", "receipt"}) + self.assertIs(type(report["schema_version"]), int) + self.assertEqual(report["schema_version"], 1) + self.assertEqual(report["scientific_effect"], "NONE") + self.assertIs(report["scientific_status_authority"], False) + self.assertEqual(report["custody"], "unknown") + self.assertEqual(encoded(report["graph"]), encoded(document(members["generated/graph.json"]))) + receipt = report["receipt"] + self.assertEqual(set(receipt), {"schema_version", "scientific_effect", "scientific_status_authority", + "custody", "verification_scope", "binding", "archive", "graph_member", + "producer_receipt_sha256", "producer_receipt"}) + self.assertIs(type(receipt["schema_version"]), int) + self.assertEqual(receipt["schema_version"], 1) + self.assertEqual(receipt["scientific_effect"], "NONE") + self.assertIs(receipt["scientific_status_authority"], False) + self.assertEqual(receipt["custody"], "unknown") + self.assertEqual(receipt["verification_scope"], "declared-native-pins-and-archive-content") + self.assertEqual(receipt["binding"], expected) + self.assertEqual(receipt["producer_receipt_sha256"], sha(members["architecture-run-receipt.json"])) + self.assertEqual(encoded(receipt["producer_receipt"]), encoded(document(members["architecture-run-receipt.json"]))) + archive = receipt["archive"] + self.assertEqual(set(archive), {"sha256", "bytes", "members"}) + self.assertEqual(archive["sha256"], sha(raw)) + self.assertIs(type(archive["bytes"]), int) + self.assertEqual(archive["bytes"], len(raw)) + rows = archive["members"] + self.assertEqual(len(rows), 13) + self.assertEqual([row["path"] for row in rows], sorted([*members, "ARTIFACT_SHA256SUMS"])) + for row in rows: + self.assertEqual(set(row), {"path", "bytes", "sha256"}) + self.assertIs(type(row["bytes"]), int) + raw_member = manifest if row["path"] == "ARTIFACT_SHA256SUMS" else members[row["path"]] + self.assertEqual(row, {"path": row["path"], "bytes": len(raw_member), "sha256": sha(raw_member)}) + self.assertIs(type(receipt["graph_member"]["bytes"]), int) + self.assertEqual(receipt["graph_member"], {"path": "generated/graph.json", + "bytes": len(members["generated/graph.json"]), "sha256": sha(members["generated/graph.json"])}) + self.assertEqual(result.stdout, encoded(report), "stdout is not one canonical ASCII/LF unit") + return report, result + + def refused(self, raw=None, **kwargs): + result = self.invoke(raw, **kwargs) + self.assertNotEqual(result.returncode, 0, "invalid archive was accepted") + self.assertEqual(result.stdout, b"", "refusal published partial or success JSON") + self.assertEqual(result.stderr, REFUSAL, "refusal exposed paths/member data or variable diagnostics") + + def test_valid_archive_is_one_stable_non_scientific_unit_with_original_receipt(self): + first, one = self.accepted() + _, two = self.accepted(path=self.cwd / "TEST-second-input.zip") + self.assertEqual(one.stdout, two.stdout) + self.assertNotIn("kernel_verified", first) + self.assertNotIn("lemma_exists", first) + self.assertNotIn(str(self.cwd).encode(), one.stdout) + manifest = self.entries()[-1][1] + self.assertEqual(next(row for row in first["receipt"]["archive"]["members"] + if row["path"] == "ARTIFACT_SHA256SUMS"), + {"path": "ARTIFACT_SHA256SUMS", "bytes": len(manifest), "sha256": sha(manifest)}) + + def test_other_native_context_attempt_ten_and_declared_artifact_id_are_preserved(self): + context = {**NATIVE, "checked_commit": "d" * 40, "repository": "TEST-other/TEST-repo", + "run_id": "987", "run_attempt": "10", "artifact_id": "7654321"} + members = self.fixture(context) + raw = classic_zip(self.entries(members)) + self.accepted(raw, members=members, expected=self.expected(raw, members, context)) + expected = self.expected(classic_zip(self.entries())) + expected["artifact_id"] = "999999" + report, _ = self.accepted(expected=expected) + self.assertEqual(report["receipt"]["binding"]["artifact_id"], "999999") + self.assertEqual(report["custody"], "unknown", "an artifact label became authenticated custody") + + def test_stored_deflated_descriptors_and_member_or_manifest_order_are_valid(self): + reversed_rows = b"".join(reversed(self.entries()[-1][1].splitlines(keepends=True))) + entries = self.entries(manifest=reversed_rows, order=list(reversed(list(self.base)))) + for mode in ("stored", "deflated", "signed_descriptors", "unsigned_descriptors", "dos_regular"): + with self.subTest(mode=mode): + options = {} + for index, (_, raw) in enumerate(entries): + if mode == "stored": + options[index] = {"method": 0} + elif mode == "signed_descriptors": + options[index] = {"flags": 8} + elif mode == "unsigned_descriptors": + compressed = zlib.compressobj(level=6, wbits=-15) + payload = compressed.compress(raw) + compressed.flush() + options[index] = {"flags": 8, "descriptor": struct.pack( + "