From fd618e52a6fa9bffb13debe828596122a3664f28 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:58:41 +0300 Subject: [PATCH 01/14] test: add exact-head evidence manifest writer --- scripts/write_exact_head_manifest.py | 216 +++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 scripts/write_exact_head_manifest.py diff --git a/scripts/write_exact_head_manifest.py b/scripts/write_exact_head_manifest.py new file mode 100644 index 00000000..acc70135 --- /dev/null +++ b/scripts/write_exact_head_manifest.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Write exact-head evidence manifests and post-upload artifact receipts.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +SHA40_RE = re.compile(r"^[0-9a-f]{40}$") +SHA256_RE = re.compile(r"^(?:sha256:)?[0-9a-f]{64}$") + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def require_sha40(value: str, field: str) -> str: + if not SHA40_RE.fullmatch(value): + raise ValueError(f"{field} must be a lowercase 40-character SHA") + return value + + +def require_sha256(value: str, field: str) -> str: + if not SHA256_RE.fullmatch(value): + raise ValueError(f"{field} must be a SHA-256 digest") + return value.removeprefix("sha256:") + + +def read_rfc3339(value: str, field: str) -> str: + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise ValueError(f"{field} must be RFC3339") from exc + if parsed.tzinfo is None: + raise ValueError(f"{field} must include a timezone") + return parsed.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +def collect_files(output_dir: Path, excluded: set[Path]) -> list[dict[str, Any]]: + files: list[dict[str, Any]] = [] + for path in sorted(output_dir.rglob("*")): + if not path.is_file() or path.resolve() in excluded: + continue + files.append( + { + "path": path.relative_to(output_dir).as_posix(), + "size_bytes": path.stat().st_size, + "sha256": sha256_file(path), + } + ) + return files + + +def write_json(path: Path, payload: dict[str, Any]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def manifest_command(args: argparse.Namespace) -> None: + output_dir = Path(args.output_dir).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + manifest_path = output_dir / args.manifest_name + + expected_sha = require_sha40(args.expected_sha, "expected_sha") + initial_sha = require_sha40(args.initial_sha, "initial_sha") + final_sha = require_sha40(args.final_sha, "final_sha") + workflow_sha = require_sha40(args.workflow_sha, "workflow_sha") + if not expected_sha == initial_sha == final_sha: + raise ValueError("expected_sha, initial_sha, and final_sha must match") + + started_at = read_rfc3339(args.started_at, "started_at") + completed_at = read_rfc3339(args.completed_at, "completed_at") + if completed_at < started_at: + raise ValueError("completed_at must not precede started_at") + + files = collect_files(output_dir, {manifest_path.resolve()}) + payload = { + "schema_version": "liminalqa-exact-head-evidence-manifest-v1", + "audit": { + "name": args.audit_name, + "mode": "advisory-read-only", + "target": args.target, + "execution_status": args.execution_status, + }, + "source_identity": { + "repository": args.repository, + "expected_sha": expected_sha, + "initial_sha": initial_sha, + "final_sha": final_sha, + "workflow_sha": workflow_sha, + "head_stable": True, + "initial_worktree_clean": True, + "final_worktree_clean": True, + }, + "run_identity": { + "event_name": args.event_name, + "git_ref": args.git_ref, + "head_ref": args.head_ref or None, + "workflow_ref": args.workflow_ref, + "run_id": str(args.run_id), + "run_attempt": str(args.run_attempt), + "artifact_name": args.artifact_name, + }, + "collection": { + "started_at": started_at, + "completed_at": completed_at, + "file_count": len(files), + }, + "files": files, + "authority": { + "allowed": ["public passive observation", "read-only evidence capture"], + "prohibited": [ + "authentication", + "form submission", + "portfolio access", + "order entry", + "financial operation", + "fuzzing", + "load testing", + "deployment", + ], + }, + } + write_json(manifest_path, payload) + + +def receipt_command(args: argparse.Namespace) -> None: + manifest_path = Path(args.manifest).resolve() + if not manifest_path.is_file(): + raise FileNotFoundError(f"manifest not found: {manifest_path}") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + expected_name = manifest["run_identity"]["artifact_name"] + if args.artifact_name != expected_name: + raise ValueError("artifact_name does not match manifest") + if str(args.run_id) != str(manifest["run_identity"]["run_id"]): + raise ValueError("run_id does not match manifest") + if str(args.run_attempt) != str(manifest["run_identity"]["run_attempt"]): + raise ValueError("run_attempt does not match manifest") + + receipt = { + "schema_version": "liminalqa-artifact-receipt-v1", + "manifest": { + "path": manifest_path.name, + "sha256": sha256_file(manifest_path), + }, + "artifact": { + "name": args.artifact_name, + "id": str(args.artifact_id), + "url": args.artifact_url, + "sha256": require_sha256(args.artifact_digest, "artifact_digest"), + }, + "run_identity": { + "run_id": str(args.run_id), + "run_attempt": str(args.run_attempt), + }, + } + write_json(Path(args.output).resolve(), receipt) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + + manifest = subparsers.add_parser("manifest") + manifest.add_argument("--output-dir", required=True) + manifest.add_argument("--manifest-name", default="manifest.json") + manifest.add_argument("--audit-name", required=True) + manifest.add_argument("--target", required=True) + manifest.add_argument("--repository", required=True) + manifest.add_argument("--expected-sha", required=True) + manifest.add_argument("--initial-sha", required=True) + manifest.add_argument("--final-sha", required=True) + manifest.add_argument("--workflow-sha", required=True) + manifest.add_argument("--event-name", required=True) + manifest.add_argument("--git-ref", required=True) + manifest.add_argument("--head-ref", default="") + manifest.add_argument("--workflow-ref", required=True) + manifest.add_argument("--run-id", required=True) + manifest.add_argument("--run-attempt", required=True) + manifest.add_argument("--artifact-name", required=True) + manifest.add_argument("--started-at", required=True) + manifest.add_argument("--completed-at", required=True) + manifest.add_argument("--execution-status", required=True) + manifest.set_defaults(func=manifest_command) + + receipt = subparsers.add_parser("receipt") + receipt.add_argument("--manifest", required=True) + receipt.add_argument("--output", required=True) + receipt.add_argument("--artifact-name", required=True) + receipt.add_argument("--artifact-id", required=True) + receipt.add_argument("--artifact-url", required=True) + receipt.add_argument("--artifact-digest", required=True) + receipt.add_argument("--run-id", required=True) + receipt.add_argument("--run-attempt", required=True) + receipt.set_defaults(func=receipt_command) + + return parser + + +def main() -> int: + args = build_parser().parse_args() + args.func(args) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 2d55847eedf563ae48415a1bdbc445ba848a4758 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 01:59:09 +0300 Subject: [PATCH 02/14] test: cover exact-head manifest contracts --- tests/test_write_exact_head_manifest.py | 130 ++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/test_write_exact_head_manifest.py diff --git a/tests/test_write_exact_head_manifest.py b/tests/test_write_exact_head_manifest.py new file mode 100644 index 00000000..95471f70 --- /dev/null +++ b/tests/test_write_exact_head_manifest.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import argparse +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts" / "write_exact_head_manifest.py" +spec = importlib.util.spec_from_file_location("write_exact_head_manifest", MODULE_PATH) +module = importlib.util.module_from_spec(spec) +assert spec.loader is not None +spec.loader.exec_module(module) + +SHA = "a" * 40 +WORKFLOW_SHA = "b" * 40 + + +class ExactHeadManifestTests(unittest.TestCase): + def test_manifest_hashes_files_and_binds_identity(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + output_dir = Path(tmp) + (output_dir / "result.json").write_text('{"ok":true}\n', encoding="utf-8") + args = argparse.Namespace( + output_dir=str(output_dir), + manifest_name="manifest.json", + audit_name="test audit", + target="https://example.test/", + repository="owner/repo", + expected_sha=SHA, + initial_sha=SHA, + final_sha=SHA, + workflow_sha=WORKFLOW_SHA, + event_name="pull_request", + git_ref="refs/pull/1/merge", + head_ref="agent/test", + workflow_ref="owner/repo/.github/workflows/test.yml@refs/pull/1/merge", + run_id="123", + run_attempt="2", + artifact_name="evidence-123-2", + started_at="2026-07-27T00:00:00Z", + completed_at="2026-07-27T00:00:01Z", + execution_status="success", + ) + module.manifest_command(args) + manifest = json.loads((output_dir / "manifest.json").read_text(encoding="utf-8")) + self.assertTrue(manifest["source_identity"]["head_stable"]) + self.assertEqual(manifest["run_identity"]["run_attempt"], "2") + self.assertEqual([item["path"] for item in manifest["files"]], ["result.json"]) + self.assertEqual( + manifest["files"][0]["sha256"], + module.sha256_file(output_dir / "result.json"), + ) + + def test_manifest_rejects_head_mismatch(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + args = argparse.Namespace( + output_dir=tmp, + manifest_name="manifest.json", + audit_name="test", + target="https://example.test/", + repository="owner/repo", + expected_sha=SHA, + initial_sha=SHA, + final_sha="c" * 40, + workflow_sha=WORKFLOW_SHA, + event_name="push", + git_ref="refs/heads/main", + head_ref="", + workflow_ref="workflow", + run_id="1", + run_attempt="1", + artifact_name="evidence-1-1", + started_at="2026-07-27T00:00:00Z", + completed_at="2026-07-27T00:00:01Z", + execution_status="success", + ) + with self.assertRaisesRegex(ValueError, "must match"): + module.manifest_command(args) + + def test_receipt_binds_uploaded_artifact_to_manifest(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + (root / "result.txt").write_text("evidence\n", encoding="utf-8") + manifest_args = argparse.Namespace( + output_dir=str(root), + manifest_name="manifest.json", + audit_name="test", + target="https://example.test/", + repository="owner/repo", + expected_sha=SHA, + initial_sha=SHA, + final_sha=SHA, + workflow_sha=WORKFLOW_SHA, + event_name="push", + git_ref="refs/heads/main", + head_ref="", + workflow_ref="workflow", + run_id="7", + run_attempt="3", + artifact_name="evidence-7-3", + started_at="2026-07-27T00:00:00Z", + completed_at="2026-07-27T00:00:01Z", + execution_status="success", + ) + module.manifest_command(manifest_args) + receipt_path = root / "receipt" / "artifact-receipt.json" + receipt_args = argparse.Namespace( + manifest=str(root / "manifest.json"), + output=str(receipt_path), + artifact_name="evidence-7-3", + artifact_id="999", + artifact_url="https://example.test/artifacts/999", + artifact_digest="sha256:" + "d" * 64, + run_id="7", + run_attempt="3", + ) + module.receipt_command(receipt_args) + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + self.assertEqual(receipt["artifact"]["id"], "999") + self.assertEqual(receipt["artifact"]["sha256"], "d" * 64) + self.assertEqual( + receipt["manifest"]["sha256"], + module.sha256_file(root / "manifest.json"), + ) + + +if __name__ == "__main__": + unittest.main() From 96b9ee05a1f2362a535219efc481236ec2443700 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:01:45 +0300 Subject: [PATCH 03/14] fix: bind Tradernet loading evidence to exact head --- .../tradernet-terminal-loading-public.yml | 142 ++++++++++++++++-- 1 file changed, 133 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tradernet-terminal-loading-public.yml b/.github/workflows/tradernet-terminal-loading-public.yml index fc70c223..a7124821 100644 --- a/.github/workflows/tradernet-terminal-loading-public.yml +++ b/.github/workflows/tradernet-terminal-loading-public.yml @@ -2,6 +2,11 @@ name: Tradernet Public Terminal Loading Audit on: workflow_dispatch: + inputs: + expected_sha: + description: Optional exact 40-character revision; defaults to the selected workflow ref + required: false + type: string push: branches: - agent/tradernet-terminal-loading-audit @@ -9,6 +14,8 @@ on: - .github/workflows/tradernet-terminal-loading-public.yml - audits/tradernet/terminal-loading-public.json - scripts/tradernet_terminal_loading_observer.mjs + - scripts/write_exact_head_manifest.py + - tests/test_write_exact_head_manifest.py pull_request: branches: - main @@ -16,6 +23,8 @@ on: - .github/workflows/tradernet-terminal-loading-public.yml - audits/tradernet/terminal-loading-public.json - scripts/tradernet_terminal_loading_observer.mjs + - scripts/write_exact_head_manifest.py + - tests/test_write_exact_head_manifest.py permissions: contents: read @@ -32,13 +41,34 @@ jobs: env: NPM_CONFIG_AUDIT: "false" NPM_CONFIG_FUND: "false" + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || inputs.expected_sha || github.sha }} + EVIDENCE_ARTIFACT_NAME: tradernet-public-terminal-loading-${{ github.run_id }}-${{ github.run_attempt }} + RECEIPT_ARTIFACT_NAME: tradernet-public-terminal-loading-receipt-${{ github.run_id }}-${{ github.run_attempt }} steps: - - name: Checkout exact workflow revision + - name: Checkout exact audited revision uses: actions/checkout@v6 with: + ref: ${{ env.EXPECTED_SHA }} + fetch-depth: 1 persist-credentials: false + - name: Verify initial exact head + id: identity + shell: bash + run: | + set -euo pipefail + if [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]]; then + echo "EXPECTED_SHA must be a lowercase 40-character SHA" >&2 + exit 2 + fi + initial_sha="$(git rev-parse HEAD)" + test "${initial_sha}" = "${EXPECTED_SHA}" + worktree_status="$(git status --porcelain=v1 --untracked-files=all)" + test -z "${worktree_status}" + echo "initial_sha=${initial_sha}" >> "${GITHUB_OUTPUT}" + echo "started_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" + - name: Validate exact safety boundary shell: bash run: | @@ -64,8 +94,22 @@ jobs: .boundaries.active_security_testing == false ' "${config}" >/dev/null - - name: Install pinned browser driver - run: npm install --no-save --package-lock=false puppeteer-core@24.16.0 + - name: Validate evidence manifest helper + run: | + set -euo pipefail + python3 -m py_compile scripts/write_exact_head_manifest.py + python3 -m unittest tests/test_write_exact_head_manifest.py -v + + - name: Install pinned browser driver outside the worktree + shell: bash + run: | + set -euo pipefail + deps_dir="${RUNNER_TEMP}/tradernet-node-deps" + rm -rf "${deps_dir}" + test ! -e node_modules + test ! -L node_modules + npm install --prefix "${deps_dir}" --no-save --package-lock=false puppeteer-core@24.16.0 + ln -s "${deps_dir}/node_modules" node_modules - name: Validate observer and locate Chrome id: runtime @@ -80,21 +124,101 @@ jobs: echo "chrome=${chrome}" >> "${GITHUB_OUTPUT}" - name: Observe public terminal loading + id: capture shell: bash run: | set -euo pipefail - rm -rf reports/tradernet-terminal-loading + evidence_dir="${RUNNER_TEMP}/tradernet-terminal-loading" + rm -rf "${evidence_dir}" + mkdir -p "${evidence_dir}" node scripts/tradernet_terminal_loading_observer.mjs \ --config audits/tradernet/terminal-loading-public.json \ --chrome "${{ steps.runtime.outputs.chrome }}" \ - --output-dir reports/tradernet-terminal-loading - cat reports/tradernet-terminal-loading/result/terminal-loading-summary.md >> "${GITHUB_STEP_SUMMARY}" + --output-dir "${evidence_dir}" + cat "${evidence_dir}/result/terminal-loading-summary.md" >> "${GITHUB_STEP_SUMMARY}" - - name: Upload exact evidence + - name: Verify final exact head and clean worktree + id: final_identity if: always() + shell: bash + run: | + set -euo pipefail + if [ -L node_modules ]; then + rm node_modules + fi + final_sha="$(git rev-parse HEAD)" + test "${final_sha}" = "${EXPECTED_SHA}" + worktree_status="$(git status --porcelain=v1 --untracked-files=all)" + if [ -n "${worktree_status}" ]; then + printf '%s\n' "${worktree_status}" >&2 + exit 2 + fi + echo "final_sha=${final_sha}" >> "${GITHUB_OUTPUT}" + echo "completed_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" + + - name: Write immutable evidence manifest + id: manifest + if: always() && steps.identity.outcome == 'success' && steps.final_identity.outcome == 'success' + shell: bash + run: | + set -euo pipefail + evidence_dir="${RUNNER_TEMP}/tradernet-terminal-loading" + mkdir -p "${evidence_dir}" + python3 scripts/write_exact_head_manifest.py manifest \ + --output-dir "${evidence_dir}" \ + --audit-name "Tradernet public terminal loading audit" \ + --target "https://tradernet.ru/terminal" \ + --repository "${GITHUB_REPOSITORY}" \ + --expected-sha "${EXPECTED_SHA}" \ + --initial-sha "${{ steps.identity.outputs.initial_sha }}" \ + --final-sha "${{ steps.final_identity.outputs.final_sha }}" \ + --workflow-sha "${{ github.workflow_sha }}" \ + --event-name "${GITHUB_EVENT_NAME}" \ + --git-ref "${GITHUB_REF}" \ + --head-ref "${GITHUB_HEAD_REF}" \ + --workflow-ref "${{ github.workflow_ref }}" \ + --run-id "${GITHUB_RUN_ID}" \ + --run-attempt "${GITHUB_RUN_ATTEMPT}" \ + --artifact-name "${EVIDENCE_ARTIFACT_NAME}" \ + --started-at "${{ steps.identity.outputs.started_at }}" \ + --completed-at "${{ steps.final_identity.outputs.completed_at }}" \ + --execution-status "${{ steps.capture.outcome }}" + python3 -m json.tool "${evidence_dir}/manifest.json" >/dev/null + + - name: Upload exact evidence + id: evidence_artifact + if: always() && steps.manifest.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.EVIDENCE_ARTIFACT_NAME }} + path: ${{ runner.temp }}/tradernet-terminal-loading/ + if-no-files-found: error + retention-days: 14 + + - name: Write artifact receipt + if: always() && steps.evidence_artifact.outcome == 'success' + shell: bash + run: | + set -euo pipefail + receipt_dir="${RUNNER_TEMP}/tradernet-terminal-loading-receipt" + rm -rf "${receipt_dir}" + mkdir -p "${receipt_dir}" + python3 scripts/write_exact_head_manifest.py receipt \ + --manifest "${RUNNER_TEMP}/tradernet-terminal-loading/manifest.json" \ + --output "${receipt_dir}/artifact-receipt.json" \ + --artifact-name "${EVIDENCE_ARTIFACT_NAME}" \ + --artifact-id "${{ steps.evidence_artifact.outputs.artifact-id }}" \ + --artifact-url "${{ steps.evidence_artifact.outputs.artifact-url }}" \ + --artifact-digest "${{ steps.evidence_artifact.outputs.artifact-digest }}" \ + --run-id "${GITHUB_RUN_ID}" \ + --run-attempt "${GITHUB_RUN_ATTEMPT}" + python3 -m json.tool "${receipt_dir}/artifact-receipt.json" >/dev/null + + - name: Upload artifact receipt + if: always() && steps.evidence_artifact.outcome == 'success' uses: actions/upload-artifact@v4 with: - name: tradernet-public-terminal-loading-${{ github.run_id }} - path: reports/tradernet-terminal-loading/ + name: ${{ env.RECEIPT_ARTIFACT_NAME }} + path: ${{ runner.temp }}/tradernet-terminal-loading-receipt/artifact-receipt.json if-no-files-found: error retention-days: 14 From 6fe72ae5b4ab97925245825e0c79b573c511186c Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:02:30 +0300 Subject: [PATCH 04/14] fix: bind Tradernet image evidence to exact head --- .../tradernet-terminal-image-visibility.yml | 143 ++++++++++++++++-- 1 file changed, 134 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tradernet-terminal-image-visibility.yml b/.github/workflows/tradernet-terminal-image-visibility.yml index dc4430e4..57d0656b 100644 --- a/.github/workflows/tradernet-terminal-image-visibility.yml +++ b/.github/workflows/tradernet-terminal-image-visibility.yml @@ -2,6 +2,11 @@ name: Tradernet Terminal Mobile Image Visibility on: workflow_dispatch: + inputs: + expected_sha: + description: Optional exact 40-character revision; defaults to the selected workflow ref + required: false + type: string push: branches: - agent/tradernet-terminal-loading-audit @@ -9,6 +14,8 @@ on: - .github/workflows/tradernet-terminal-image-visibility.yml - audits/tradernet/terminal-loading-public.json - scripts/tradernet_terminal_image_visibility_probe.mjs + - scripts/write_exact_head_manifest.py + - tests/test_write_exact_head_manifest.py pull_request: branches: - main @@ -16,6 +23,8 @@ on: - .github/workflows/tradernet-terminal-image-visibility.yml - audits/tradernet/terminal-loading-public.json - scripts/tradernet_terminal_image_visibility_probe.mjs + - scripts/write_exact_head_manifest.py + - tests/test_write_exact_head_manifest.py permissions: contents: read @@ -32,12 +41,34 @@ jobs: env: NPM_CONFIG_AUDIT: "false" NPM_CONFIG_FUND: "false" + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || inputs.expected_sha || github.sha }} + EVIDENCE_ARTIFACT_NAME: tradernet-terminal-mobile-image-${{ github.run_id }}-${{ github.run_attempt }} + RECEIPT_ARTIFACT_NAME: tradernet-terminal-mobile-image-receipt-${{ github.run_id }}-${{ github.run_attempt }} steps: - - uses: actions/checkout@v6 + - name: Checkout exact audited revision + uses: actions/checkout@v6 with: + ref: ${{ env.EXPECTED_SHA }} + fetch-depth: 1 persist-credentials: false + - name: Verify initial exact head + id: identity + shell: bash + run: | + set -euo pipefail + if [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]]; then + echo "EXPECTED_SHA must be a lowercase 40-character SHA" >&2 + exit 2 + fi + initial_sha="$(git rev-parse HEAD)" + test "${initial_sha}" = "${EXPECTED_SHA}" + worktree_status="$(git status --porcelain=v1 --untracked-files=all)" + test -z "${worktree_status}" + echo "initial_sha=${initial_sha}" >> "${GITHUB_OUTPUT}" + echo "started_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" + - name: Validate exact boundary shell: bash run: | @@ -60,8 +91,22 @@ jobs: .boundaries.active_security_testing == false ' audits/tradernet/terminal-loading-public.json >/dev/null - - name: Install pinned browser driver - run: npm install --no-save --package-lock=false puppeteer-core@24.16.0 + - name: Validate evidence manifest helper + run: | + set -euo pipefail + python3 -m py_compile scripts/write_exact_head_manifest.py + python3 -m unittest tests/test_write_exact_head_manifest.py -v + + - name: Install pinned browser driver outside the worktree + shell: bash + run: | + set -euo pipefail + deps_dir="${RUNNER_TEMP}/tradernet-node-deps" + rm -rf "${deps_dir}" + test ! -e node_modules + test ! -L node_modules + npm install --prefix "${deps_dir}" --no-save --package-lock=false puppeteer-core@24.16.0 + ln -s "${deps_dir}/node_modules" node_modules - name: Locate Chrome and validate probe id: runtime @@ -74,21 +119,101 @@ jobs: echo "chrome=${chrome}" >> "${GITHUB_OUTPUT}" - name: Probe mobile image visibility + id: capture shell: bash run: | set -euo pipefail - rm -rf reports/tradernet-terminal-image-visibility + evidence_dir="${RUNNER_TEMP}/tradernet-terminal-image-visibility" + rm -rf "${evidence_dir}" + mkdir -p "${evidence_dir}" node scripts/tradernet_terminal_image_visibility_probe.mjs \ --config audits/tradernet/terminal-loading-public.json \ --chrome "${{ steps.runtime.outputs.chrome }}" \ - --output-dir reports/tradernet-terminal-image-visibility - cat reports/tradernet-terminal-image-visibility/terminal-image-visibility-summary.md >> "${GITHUB_STEP_SUMMARY}" + --output-dir "${evidence_dir}" + cat "${evidence_dir}/terminal-image-visibility-summary.md" >> "${GITHUB_STEP_SUMMARY}" - - name: Upload exact evidence + - name: Verify final exact head and clean worktree + id: final_identity if: always() + shell: bash + run: | + set -euo pipefail + if [ -L node_modules ]; then + rm node_modules + fi + final_sha="$(git rev-parse HEAD)" + test "${final_sha}" = "${EXPECTED_SHA}" + worktree_status="$(git status --porcelain=v1 --untracked-files=all)" + if [ -n "${worktree_status}" ]; then + printf '%s\n' "${worktree_status}" >&2 + exit 2 + fi + echo "final_sha=${final_sha}" >> "${GITHUB_OUTPUT}" + echo "completed_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" + + - name: Write immutable evidence manifest + id: manifest + if: always() && steps.identity.outcome == 'success' && steps.final_identity.outcome == 'success' + shell: bash + run: | + set -euo pipefail + evidence_dir="${RUNNER_TEMP}/tradernet-terminal-image-visibility" + mkdir -p "${evidence_dir}" + python3 scripts/write_exact_head_manifest.py manifest \ + --output-dir "${evidence_dir}" \ + --audit-name "Tradernet terminal mobile image visibility" \ + --target "https://tradernet.ru/terminal" \ + --repository "${GITHUB_REPOSITORY}" \ + --expected-sha "${EXPECTED_SHA}" \ + --initial-sha "${{ steps.identity.outputs.initial_sha }}" \ + --final-sha "${{ steps.final_identity.outputs.final_sha }}" \ + --workflow-sha "${{ github.workflow_sha }}" \ + --event-name "${GITHUB_EVENT_NAME}" \ + --git-ref "${GITHUB_REF}" \ + --head-ref "${GITHUB_HEAD_REF}" \ + --workflow-ref "${{ github.workflow_ref }}" \ + --run-id "${GITHUB_RUN_ID}" \ + --run-attempt "${GITHUB_RUN_ATTEMPT}" \ + --artifact-name "${EVIDENCE_ARTIFACT_NAME}" \ + --started-at "${{ steps.identity.outputs.started_at }}" \ + --completed-at "${{ steps.final_identity.outputs.completed_at }}" \ + --execution-status "${{ steps.capture.outcome }}" + python3 -m json.tool "${evidence_dir}/manifest.json" >/dev/null + + - name: Upload exact evidence + id: evidence_artifact + if: always() && steps.manifest.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.EVIDENCE_ARTIFACT_NAME }} + path: ${{ runner.temp }}/tradernet-terminal-image-visibility/ + if-no-files-found: error + retention-days: 14 + + - name: Write artifact receipt + if: always() && steps.evidence_artifact.outcome == 'success' + shell: bash + run: | + set -euo pipefail + receipt_dir="${RUNNER_TEMP}/tradernet-terminal-image-visibility-receipt" + rm -rf "${receipt_dir}" + mkdir -p "${receipt_dir}" + python3 scripts/write_exact_head_manifest.py receipt \ + --manifest "${RUNNER_TEMP}/tradernet-terminal-image-visibility/manifest.json" \ + --output "${receipt_dir}/artifact-receipt.json" \ + --artifact-name "${EVIDENCE_ARTIFACT_NAME}" \ + --artifact-id "${{ steps.evidence_artifact.outputs.artifact-id }}" \ + --artifact-url "${{ steps.evidence_artifact.outputs.artifact-url }}" \ + --artifact-digest "${{ steps.evidence_artifact.outputs.artifact-digest }}" \ + --run-id "${GITHUB_RUN_ID}" \ + --run-attempt "${GITHUB_RUN_ATTEMPT}" + python3 -m json.tool "${receipt_dir}/artifact-receipt.json" >/dev/null + + - name: Upload artifact receipt + if: always() && steps.evidence_artifact.outcome == 'success' uses: actions/upload-artifact@v4 with: - name: tradernet-terminal-mobile-image-${{ github.run_id }} - path: reports/tradernet-terminal-image-visibility/ + name: ${{ env.RECEIPT_ARTIFACT_NAME }} + path: ${{ runner.temp }}/tradernet-terminal-image-visibility-receipt/artifact-receipt.json if-no-files-found: error retention-days: 14 From 188145943492550955ed916ba37205de0667be28 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:05:38 +0300 Subject: [PATCH 05/14] fix: keep exact-head validation worktree clean --- .../workflows/tradernet-terminal-loading-public.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tradernet-terminal-loading-public.yml b/.github/workflows/tradernet-terminal-loading-public.yml index a7124821..e0a6273f 100644 --- a/.github/workflows/tradernet-terminal-loading-public.yml +++ b/.github/workflows/tradernet-terminal-loading-public.yml @@ -95,10 +95,18 @@ jobs: ' "${config}" >/dev/null - name: Validate evidence manifest helper + env: + PYTHONDONTWRITEBYTECODE: "1" run: | set -euo pipefail - python3 -m py_compile scripts/write_exact_head_manifest.py + python3 - <<'PY' + import ast + from pathlib import Path + + ast.parse(Path("scripts/write_exact_head_manifest.py").read_text(encoding="utf-8")) + PY python3 -m unittest tests/test_write_exact_head_manifest.py -v + test -z "$(find scripts tests -type d -name __pycache__ -print -quit)" - name: Install pinned browser driver outside the worktree shell: bash From 3a6ca6430691f45c3a0d0c5473aab96d439b23ec Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 02:06:24 +0300 Subject: [PATCH 06/14] fix: keep exact-head validation worktree clean --- .../workflows/tradernet-terminal-image-visibility.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tradernet-terminal-image-visibility.yml b/.github/workflows/tradernet-terminal-image-visibility.yml index 57d0656b..d32449dc 100644 --- a/.github/workflows/tradernet-terminal-image-visibility.yml +++ b/.github/workflows/tradernet-terminal-image-visibility.yml @@ -92,10 +92,18 @@ jobs: ' audits/tradernet/terminal-loading-public.json >/dev/null - name: Validate evidence manifest helper + env: + PYTHONDONTWRITEBYTECODE: "1" run: | set -euo pipefail - python3 -m py_compile scripts/write_exact_head_manifest.py + python3 - <<'PY' + import ast + from pathlib import Path + + ast.parse(Path("scripts/write_exact_head_manifest.py").read_text(encoding="utf-8")) + PY python3 -m unittest tests/test_write_exact_head_manifest.py -v + test -z "$(find scripts tests -type d -name __pycache__ -print -quit)" - name: Install pinned browser driver outside the worktree shell: bash From 9c81279aad63c5cc3e4619751533c17d624ca4ff Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:10:57 +0300 Subject: [PATCH 07/14] test: add canonical Tradernet LTP replay --- .github/workflows/tradernet-ltp-replay.yml | 249 +++++++++++++++++++++ scripts/run_ltp_offline_replay.sh | 142 ++++++++++++ scripts/write_ltp_audit_trace.py | 125 +++++++++++ tests/test_write_ltp_audit_trace.py | 92 ++++++++ 4 files changed, 608 insertions(+) create mode 100644 .github/workflows/tradernet-ltp-replay.yml create mode 100755 scripts/run_ltp_offline_replay.sh create mode 100755 scripts/write_ltp_audit_trace.py create mode 100644 tests/test_write_ltp_audit_trace.py diff --git a/.github/workflows/tradernet-ltp-replay.yml b/.github/workflows/tradernet-ltp-replay.yml new file mode 100644 index 00000000..50f029cf --- /dev/null +++ b/.github/workflows/tradernet-ltp-replay.yml @@ -0,0 +1,249 @@ +name: Tradernet Canonical LTP Replay + +on: + workflow_dispatch: + inputs: + expected_sha: + description: Optional exact 40-character revision; defaults to the selected workflow ref + required: false + type: string + pull_request: + branches: + - main + paths: + - .github/workflows/tradernet-ltp-replay.yml + - audits/tradernet/terminal-loading-public.json + - scripts/tradernet_terminal_loading_observer.mjs + - scripts/tradernet_terminal_image_visibility_probe.mjs + - scripts/write_exact_head_manifest.py + - scripts/write_ltp_audit_trace.py + - scripts/run_ltp_offline_replay.sh + - tests/test_write_exact_head_manifest.py + - tests/test_write_ltp_audit_trace.py + +permissions: + contents: read + +concurrency: + group: tradernet-ltp-replay-${{ github.ref }} + cancel-in-progress: true + +jobs: + replay: + name: LTP replay · ${{ matrix.audit_id }} + runs-on: ubuntu-latest + timeout-minutes: 35 + strategy: + fail-fast: false + matrix: + include: + - audit_id: public-terminal-loading + audit_name: Tradernet public terminal loading audit + evidence_dir_name: tradernet-ltp-public-terminal-loading + artifact_prefix: tradernet-ltp-public-terminal-loading + - audit_id: mobile-image-visibility + audit_name: Tradernet terminal mobile image visibility + evidence_dir_name: tradernet-ltp-mobile-image-visibility + artifact_prefix: tradernet-ltp-mobile-image-visibility + env: + NPM_CONFIG_AUDIT: "false" + NPM_CONFIG_FUND: "false" + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || inputs.expected_sha || github.sha }} + LTP_INSPECTOR_SHA: 5474f29021adf1fd9257f7d8375fedc485d00352 + EVIDENCE_ARTIFACT_NAME: ${{ matrix.artifact_prefix }}-${{ github.run_id }}-${{ github.run_attempt }} + RECEIPT_ARTIFACT_NAME: ${{ matrix.artifact_prefix }}-receipt-${{ github.run_id }}-${{ github.run_attempt }} + + steps: + - name: Checkout exact audited revision + uses: actions/checkout@v6 + with: + ref: ${{ env.EXPECTED_SHA }} + fetch-depth: 1 + persist-credentials: false + + - name: Verify initial exact head + id: identity + shell: bash + run: | + set -euo pipefail + if [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]]; then + echo "EXPECTED_SHA must be a lowercase 40-character SHA" >&2 + exit 2 + fi + initial_sha="$(git rev-parse HEAD)" + test "${initial_sha}" = "${EXPECTED_SHA}" + test -z "$(git status --porcelain=v1 --untracked-files=all)" + echo "initial_sha=${initial_sha}" >> "${GITHUB_OUTPUT}" + echo "started_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" + + - name: Validate public-only boundary + shell: bash + run: | + set -euo pipefail + config="audits/tradernet/terminal-loading-public.json" + python3 -m json.tool "${config}" >/dev/null + jq -e ' + .target_url == "https://tradernet.ru/terminal" and + .observation_ms <= 30000 and + .boundaries.public_page_only == true and + .boundaries.one_navigation_per_profile == true and + .boundaries.authenticated_testing == false and + .boundaries.direct_api_testing == false and + .boundaries.portfolio_access == false and + .boundaries.personal_data_access == false and + .boundaries.financial_operations == false and + .boundaries.order_entry == false and + .boundaries.market_depth_subscription == false and + .boundaries.form_submission == false and + .boundaries.fuzzing == false and + .boundaries.load_testing == false and + .boundaries.active_security_testing == false + ' "${config}" >/dev/null + + - name: Validate trace helpers and negative cases + env: + PYTHONDONTWRITEBYTECODE: "1" + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + import ast + from pathlib import Path + + for path in [ + "scripts/write_exact_head_manifest.py", + "scripts/write_ltp_audit_trace.py", + ]: + ast.parse(Path(path).read_text(encoding="utf-8")) + PY + python3 -m unittest \ + tests/test_write_exact_head_manifest.py \ + tests/test_write_ltp_audit_trace.py -v + bash -n scripts/run_ltp_offline_replay.sh + test -z "$(find scripts tests -type d -name __pycache__ -print -quit)" + + - name: Install pinned browser driver outside the worktree + shell: bash + run: | + set -euo pipefail + deps_dir="${RUNNER_TEMP}/tradernet-node-deps-${{ matrix.audit_id }}" + rm -rf "${deps_dir}" + test ! -e node_modules + test ! -L node_modules + npm install --prefix "${deps_dir}" --no-save --package-lock=false puppeteer-core@24.16.0 + ln -s "${deps_dir}/node_modules" node_modules + + - name: Locate Chrome and validate capture scripts + id: runtime + shell: bash + run: | + set -euo pipefail + node --check scripts/tradernet_terminal_loading_observer.mjs + node --check scripts/tradernet_terminal_image_visibility_probe.mjs + chrome="$(command -v google-chrome-stable || command -v google-chrome || command -v chromium || command -v chromium-browser || true)" + test -n "${chrome}" + "${chrome}" --version + echo "chrome=${chrome}" >> "${GITHUB_OUTPUT}" + + - name: Capture bounded public evidence + id: capture + shell: bash + run: | + set -euo pipefail + evidence_dir="${RUNNER_TEMP}/${{ matrix.evidence_dir_name }}" + rm -rf "${evidence_dir}" + mkdir -p "${evidence_dir}" + case "${{ matrix.audit_id }}" in + public-terminal-loading) + node scripts/tradernet_terminal_loading_observer.mjs \ + --config audits/tradernet/terminal-loading-public.json \ + --chrome "${{ steps.runtime.outputs.chrome }}" \ + --output-dir "${evidence_dir}" + cat "${evidence_dir}/result/terminal-loading-summary.md" >> "${GITHUB_STEP_SUMMARY}" + ;; + mobile-image-visibility) + node scripts/tradernet_terminal_image_visibility_probe.mjs \ + --config audits/tradernet/terminal-loading-public.json \ + --chrome "${{ steps.runtime.outputs.chrome }}" \ + --output-dir "${evidence_dir}" + cat "${evidence_dir}/terminal-image-visibility-summary.md" >> "${GITHUB_STEP_SUMMARY}" + ;; + *) + echo "unsupported audit matrix entry" >&2 + exit 2 + ;; + esac + + - name: Prepare frozen LTP inspector and registry + id: ltp_runtime + if: always() && steps.capture.outcome == 'success' + shell: bash + run: | + set -euo pipefail + if [ -L node_modules ]; then + rm node_modules + fi + ltp_dir="${RUNNER_TEMP}/ltp-inspector-${{ matrix.audit_id }}" + rm -rf "${ltp_dir}" + git init "${ltp_dir}" + git -C "${ltp_dir}" remote add origin \ + https://github.com/safal207/L-THREAD-Liminal-Thread-Secure-Protocol-LTP-.git + git -C "${ltp_dir}" fetch --depth=1 origin "${LTP_INSPECTOR_SHA}" + git -C "${ltp_dir}" checkout --detach FETCH_HEAD + test "$(git -C "${ltp_dir}" rev-parse HEAD)" = "${LTP_INSPECTOR_SHA}" + corepack enable + corepack prepare pnpm@9.15.0 --activate + ( + cd "${ltp_dir}" + pnpm install --frozen-lockfile --ignore-scripts + ) + echo "ltp_dir=${ltp_dir}" >> "${GITHUB_OUTPUT}" + + - name: Build canonical LTP JSONL trace + id: trace + if: always() && steps.capture.outcome == 'success' && steps.ltp_runtime.outcome == 'success' + shell: bash + run: | + set -euo pipefail + evidence_dir="${RUNNER_TEMP}/${{ matrix.evidence_dir_name }}" + python3 scripts/write_ltp_audit_trace.py build \ + --output-dir "${evidence_dir}" \ + --audit-name "${{ matrix.audit_name }}" \ + --target "https://tradernet.ru/terminal" \ + --repository "${GITHUB_REPOSITORY}" \ + --expected-sha "${EXPECTED_SHA}" \ + --initial-sha "${{ steps.identity.outputs.initial_sha }}" \ + --workflow-sha "${{ github.workflow_sha }}" \ + --run-id "${GITHUB_RUN_ID}" \ + --run-attempt "${GITHUB_RUN_ATTEMPT}" \ + --started-at "${{ steps.identity.outputs.started_at }}" \ + --capture-status "${{ steps.capture.outcome }}" \ + --artifact-name "${EVIDENCE_ARTIFACT_NAME}" \ + --ltp-sha "${LTP_INSPECTOR_SHA}" \ + --critical-actions-registry \ + "${{ steps.ltp_runtime.outputs.ltp_dir }}/docs/contracts/ltp-critical-actions.v0.1.json" + + - name: Run strict inspector and two offline replays + id: ltp_audit + if: always() && steps.trace.outcome == 'success' + shell: bash + run: | + set -euo pipefail + evidence_dir="${RUNNER_TEMP}/${{ matrix.evidence_dir_name }}" + bash scripts/run_ltp_offline_replay.sh \ + "${evidence_dir}/ltp/trace.jsonl" \ + "${evidence_dir}" \ + "${{ steps.ltp_runtime.outputs.ltp_dir }}" \ + "${LTP_INSPECTOR_SHA}" + + - namYN\YH[[^XXY[X[ܚYBY[[Y[]BY[^\ +B[\[] Y][\YZ[Y SW[[\N[HW[[\ˆB[[OH +]]\\HPQ +H\ٚ[[_HHVPQ_HܚYW]\H +]]\ K\ܘ[Z[]H K][XY Y[\X[ +HY [ܚYW]\HN[[ \ܚYW]\H^] BX[[OIٚ[[_HUPUUHX\]Y]I +]H ]H +VKI[KIY RSNT։HUPUUH HNܚ]H[[]]XH]Y[HX[Y\YX[Y\YB[^\ +H \˚Y[]K]YHOH X\ \˘\\K]YHOH X\ \˝XK]YHOH X\ \˛]Y] ]YHOH X\ \˙[[Y[]K]YHOH X\ˆ[\[] Y][\YZ[]Y[W\HԕSTSTKX]^ ]Y[W\ۘ[YH_H]یܚ\ܚ]W^XXYX[Y\ HX[Y\ K[]] Y\]Y[W\H KX]Y] [[YHX]^ ]Y]ۘ[YH_H K]\]΋Y\] K\Z[[ K\\]ܞHUPԑTUԖ_H KY^XY \HVPQ_H KZ[]X[ \H\˚Y[]K]]˚[]X[H_H KY[[ \H\˙[[Y[]K]]˙[[H_H K]ܚٛ\H]XܚٛH_H KY][ [[YHUPUSӐSQ_H KY] \YUPԑQH KZXY \YUPPQԑQH K]ܚٛ\Y]XܚٛܙY_H K\[ZYUPԕSQH K\[X][\UPԕSUSTH KX\YX [[YHUQSWTQPӐSQ_H K\\Y X]\˚Y[]K]]˜\Y]_H KX\]Y X]\˙[[Y[]K]]˘\]Y]_H KY^X][ۋ\]\\\OI\˘\\K]YH_NI\˛]Y] ]YH_H]ی [Hۋ]Y[W\KX[Y\ ۈ]۝[ HN\Y^X]Y[BY]Y[W\YXY[^\ +H \˛X[Y\ ]YHOH X\ˆ\\ΈX[ۜ\Y X\YX]N [UQSWTQPӐSQH_B] [\[\_KX]^ ]Y[W\ۘ[YH_KˆY[Y[\Y[\܂][[ۋY^\Έ M HNܚ]H\YXXZ\Y[^\ +H \˙]Y[W\YX ]YHOH X\ˆ[\[] Y][\YZ[XZ\\HԕSTSTKX]^ ]Y[W\ۘ[YH_K\XZ\H \ܙXZ\\HZ\ \ܙXZ\\H]یܚ\ܚ]W^XXYX[Y\ HXZ\ K[X[Y\ԕSTSTKX]^ ]Y[W\ۘ[YH_KX[Y\ ۈ K[]]ܙXZ\\K\YX \XZ\ ۈ KX\YX [[YHUQSWTQPӐSQ_H KX\YX ZY\˙]Y[W\YX ]]˘\YX ZY_H KX\YX ]\\˙]Y[W\YX ]]˘\YX ]\_H KX\YX YY\\˙]Y[W\YX ]]˘\YX YY\_H K\[ZYUPԕSQH K\[X][\UPԕSUSTH]ی [HۋܙXZ\\K\YX \XZ\ ۈ]۝[ HSWB'Ff7B&V6V@cv2bb7FW2WfFV6U'Ff7BWF6Rw7V66W72pW6W37F2WB'Ff7Dc@vFSGVb$T4TE%Dd5ERТFG'VW"FVGG&WfFV6UF%R&V6VB'Ff7B&V6VB6bfW2fVCW'& &WFVFF3@ \ No newline at end of file diff --git a/scripts/run_ltp_offline_replay.sh b/scripts/run_ltp_offline_replay.sh new file mode 100755 index 00000000..6776b097 --- /dev/null +++ b/scripts/run_ltp_offline_replay.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ "$#" -ne 4 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +trace_path="$(realpath "$1")" +evidence_dir="$(realpath "$2")" +ltp_dir="$(realpath "$3")" +ltp_sha="$4" +report_dir="${evidence_dir}/ltp" +mkdir -p "${report_dir}" + +if [[ ! "${ltp_sha}" =~ ^[0-9a-f]{40}$ ]]; then + echo "ltp-sha must be a lowercase 40-character SHA" >&2 + exit 2 +fi + +test "$(git -C "${ltp_dir}" rev-parse HEAD)" = "${ltp_sha}" +test -s "${trace_path}" + +registry_path="${ltp_dir}/docs/contracts/ltp-critical-actions.v0.1.json" +test -s "${registry_path}" +registry_sha="$(sha256sum "${registry_path}" | awk '{print $1}')" + +cat > "${report_dir}/commands.txt" < "${report_dir}/ltp-inspector-sha.txt" +printf '%s\n' "${registry_sha}" > "${report_dir}/critical-actions-registry.sha256" + +set +e +( + cd "${ltp_dir}" + pnpm exec vitest run tools/ltp-inspect/critical_actions_registry.test.ts --reporter=dot +) > "${report_dir}/registry-parity.stdout.txt" 2> "${report_dir}/registry-parity.stderr.txt" +registry_code=$? +set -e +printf '%s\n' "${registry_code}" > "${report_dir}/registry-parity.exit-code.txt" +if [ "${registry_code}" -ne 0 ]; then + echo "critical-action registry parity failed" >&2 + exit "${registry_code}" +fi + +set +e +( + cd "${ltp_dir}" + LTP_INSPECT_FREEZE_CLOCK=1 LTP_BUILD="${ltp_sha}" \ + pnpm -w ltp:inspect -- trace --strict --quiet --format json --color never \ + --profile agents --replay-check --input "${trace_path}" +) > "${report_dir}/inspector-report.json" 2> "${report_dir}/inspector.stderr.txt" +inspect_code=$? +set -e +printf '%s\n' "${inspect_code}" > "${report_dir}/inspector.exit-code.txt" +if [ "${inspect_code}" -ne 0 ]; then + echo "strict LTP inspection failed with exit ${inspect_code}" >&2 + exit "${inspect_code}" +fi + +python3 - "${report_dir}/inspector-report.json" "${ltp_sha}" <<'PY' +import json +import sys +from pathlib import Path + +report_path = Path(sys.argv[1]) +ltp_sha = sys.argv[2] +report = json.loads(report_path.read_text(encoding="utf-8")) +compliance = report.get("compliance") or {} +audit = report.get("audit_summary") or {} +assert report.get("tool", {}).get("build") == ltp_sha +assert compliance.get("profile") == "agents" +assert compliance.get("trace_integrity") == "verified" +assert compliance.get("identity_binding") == "ok" +assert compliance.get("replay_determinism") == "ok" +assert audit.get("verdict") == "PASS" +assert audit.get("failed_checks") == [] +assert audit.get("violations") == [] +PY + +for attempt in 1 2; do + set +e + ( + cd "${ltp_dir}" + LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect -- replay --color never --input "${trace_path}" + ) > "${report_dir}/replay-${attempt}.stdout.txt" 2> "${report_dir}/replay-${attempt}.stderr.txt" + replay_code=$? + set -e + printf '%s\n' "${replay_code}" > "${report_dir}/replay-${attempt}.exit-code.txt" + if [ "${replay_code}" -ne 0 ]; then + echo "replay ${attempt} failed with exit ${replay_code}" >&2 + exit "${replay_code}" + fi +done + +cmp --silent "${report_dir}/replay-1.stdout.txt" "${report_dir}/replay-2.stdout.txt" +replay_one_sha="$(sha256sum "${report_dir}/replay-1.stdout.txt" | awk '{print $1}')" +replay_two_sha="$(sha256sum "${report_dir}/replay-2.stdout.txt" | awk '{print $1}')" +test "${replay_one_sha}" = "${replay_two_sha}" + +set +e +( + cd "${ltp_dir}" + LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect -- explain --color never --input "${trace_path}" --at step-008 +) > "${report_dir}/explain-step-008.stdout.txt" 2> "${report_dir}/explain-step-008.stderr.txt" +explain_code=$? +set -e +printf '%s\n' "${explain_code}" > "${report_dir}/explain-step-008.exit-code.txt" +if [ "${explain_code}" -ne 0 ]; then + echo "explain failed with exit ${explain_code}" >&2 + exit "${explain_code}" +fi + +python3 - "${report_dir}" "${replay_one_sha}" "${replay_two_sha}" "${ltp_sha}" "${registry_sha}" <<'PY' +import json +import sys +from pathlib import Path + +report_dir = Path(sys.argv[1]) +payload = { + "schema_version": "liminalqa-ltp-replay-comparison-v1", + "byte_identical": True, + "replay_1_sha256": sys.argv[2], + "replay_2_sha256": sys.argv[3], + "ltp_inspector_sha": sys.argv[4], + "critical_actions_registry_sha256": sys.argv[5], + "inspector_exit_code": int((report_dir / "inspector.exit-code.txt").read_text().strip()), + "replay_1_exit_code": int((report_dir / "replay-1.exit-code.txt").read_text().strip()), + "replay_2_exit_code": int((report_dir / "replay-2.exit-code.txt").read_text().strip()), +} +(report_dir / "replay-comparison.json").write_text( + json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8" +) +PY + +sha256sum "${trace_path}" > "${report_dir}/trace.sha256" +sha256sum "${report_dir}/inspector-report.json" > "${report_dir}/inspector-report.sha256" diff --git a/scripts/write_ltp_audit_trace.py b/scripts/write_ltp_audit_trace.py new file mode 100755 index 00000000..3c04af98 --- /dev/null +++ b/scripts/write_ltp_audit_trace.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +"""Create/verify a deterministic, read-only LTP JSONL audit trace.""" +from __future__ import annotations +import argparse, hashlib, json, re +from datetime import datetime +from pathlib import Path +from typing import Any, Iterable + +ZERO="0"*64; SHA=re.compile(r"^[0-9a-f]{64}$"); COMMIT=re.compile(r"^[0-9a-f]{40}$") +class TraceContractError(ValueError): pass + +def canon(v:Any)->bytes:return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False).encode() +def fsha(p:Path)->str: + h=hashlib.sha256() + with p.open("rb") as f: + for b in iter(lambda:f.read(1<<20),b""):h.update(b) + return h.hexdigest() +def ts(v:str)->str: + try:d=datetime.fromisoformat(v.replace("Z","+00:00")) + except ValueError as e:raise TraceContractError(f"invalid RFC3339 timestamp: {v}") from e + if d.tzinfo is None:raise TraceContractError(f"timestamp lacks timezone: {v}") + return v +def load_registry(p:Path)->tuple[set[str],str]: + try:x=json.loads(p.read_text()) + except Exception as e:raise TraceContractError(f"invalid critical-action registry: {p}") from e + a=x.get("actions") if isinstance(x,dict) else None + if not isinstance(a,dict) or not a:raise TraceContractError("registry actions missing") + return set(a),fsha(p) +def make_frame(fid:str,timestamp:str,kind:str,payload:dict[str,Any],continuity:str|None)->dict[str,Any]: + f={"v":"0.1","id":fid,"ts":timestamp,"type":kind,"payload":payload} + if continuity:f["continuity_token"]=continuity + return f +def build_entries(frames:Iterable[tuple[str,dict[str,Any]]],session:str)->list[dict[str,Any]]: + out=[];prev=ZERO + for i,(direction,frame) in enumerate(frames): + digest=hashlib.sha256(prev.encode()+canon(frame)).hexdigest() + out.append({"i":i,"ts":frame["ts"],"direction":direction,"session_id":session,"frame":frame,"prev_hash":prev,"hash":digest});prev=digest + return out +def parse_jsonl(p:Path)->list[dict[str,Any]]: + if not p.is_file():raise TraceContractError(f"trace not found: {p}") + out=[] + for n,line in enumerate(p.read_text(encoding="utf-8-sig").splitlines(),1): + if not line.strip():continue + try:v=json.loads(line) + except json.JSONDecodeError as e:raise TraceContractError(f"invalid JSONL line {n}: {e.msg}") from e + if not isinstance(v,dict):raise TraceContractError(f"line {n} is not object") + out.append(v) + if not out:raise TraceContractError("trace is empty") + return out +def verify_entries(entries:list[dict[str,Any]],critical:set[str])->dict[str,Any]: + session=None;prev=ZERO;ids=set();cts=set();identity=None;routes=[] + for i,e in enumerate(entries): + if e.get("i")!=i:raise TraceContractError(f"entry index mismatch at position {i}") + s=e.get("session_id") + if not isinstance(s,str) or not s:raise TraceContractError(f"missing session_id at position {i}") + if session is None:session=s + elif s!=session:raise TraceContractError(f"session identity changed at position {i}") + f=e.get("frame") + if not isinstance(f,dict):raise TraceContractError(f"missing frame at position {i}") + if f.get("v")!="0.1":raise TraceContractError(f"unsupported frame version at position {i}") + fid=f.get("id") + if not isinstance(fid,str) or not fid:raise TraceContractError(f"missing frame id at position {i}") + if fid in ids:raise TraceContractError(f"duplicate frame id: {fid}") + ids.add(fid);ts(str(f.get("ts",""))) + if not isinstance(f.get("type"),str):raise TraceContractError(f"missing frame type at position {i}") + p=f.get("payload",{}) + if not isinstance(p,dict):raise TraceContractError(f"payload not object at position {i}") + ct=f.get("continuity_token") + if ct is not None: + if not isinstance(ct,str) or not ct:raise TraceContractError(f"invalid continuity token at position {i}") + cts.add(ct) + if f["type"]=="orientation" and isinstance(p.get("identity"),str): + if identity is None:identity=p["identity"] + elif identity!=p["identity"]:raise TraceContractError("orientation identity changed") + if e.get("prev_hash")!=prev:raise TraceContractError(f"broken previous-hash binding at position {i}") + cur=e.get("hash") + if not isinstance(cur,str) or not SHA.fullmatch(cur):raise TraceContractError(f"invalid event hash at position {i}") + calc=hashlib.sha256(prev.encode()+canon(f)).hexdigest() + if cur!=calc:raise TraceContractError(f"event hash mismatch at position {i}") + prev=cur + if f["type"]=="route_response": + routes.append(p);decision=str(p.get("decision","")).upper();allow=p.get("admissible") is True + if decision in {"BLOCK","DENY","HOLD","FREEZE"} and allow:raise TraceContractError(f"non-ALLOW decision marked admissible at position {i}") + if p.get("context")=="WEB" and p.get("targetState") in critical and allow:raise TraceContractError(f"critical WEB-direct action at position {i}") + if len(cts)>1:raise TraceContractError("continuity token changed") + if not identity:raise TraceContractError("identity binding missing") + return {"valid":True,"frames":len(entries),"session_id":session,"identity":identity,"hash_root":prev,"continuity_token":next(iter(cts),None),"route_decisions":len(routes)} +def inv(root:Path)->list[dict[str,Any]]: + out=[] + for p in sorted(root.rglob("*")): + if p.is_file(): + r=p.relative_to(root).as_posix() + if not r.startswith("ltp/") and r not in {"manifest.json","artifact-receipt.json"}:out.append({"path":r,"size_bytes":p.stat().st_size,"sha256":fsha(p)}) + return out +def writej(p:Path,v:Any):p.parent.mkdir(parents=True,exist_ok=True);p.write_text(json.dumps(v,indent=2,sort_keys=True,ensure_ascii=False)+"\n") +def build(a:argparse.Namespace)->int: + for name in ("expected_sha","initial_sha","workflow_sha","ltp_sha"): + if not COMMIT.fullmatch(getattr(a,name)):raise TraceContractError(f"{name} must be 40-char SHA") + if a.expected_sha!=a.initial_sha:raise TraceContractError("initial SHA differs from expected SHA") + start=ts(a.started_at);root=Path(a.output_dir).resolve();critical,rsha=load_registry(Path(a.critical_actions_registry).resolve());files=inv(root) + invroot=hashlib.sha256(canon(files)).hexdigest();session=f"tradernet-{a.run_id}-{a.run_attempt}";ct="ct-"+hashlib.sha256(f"{a.repository}:{a.expected_sha}:{a.run_id}:{a.run_attempt}".encode()).hexdigest()[:24];identity=f"{a.repository}@{a.expected_sha}" + constraints={"public_page_only":True,"read_only":True,"no_authentication":True,"no_form_submission":True,"no_financial_operation":True,"no_external_message":True,"no_deploy":True,"no_protected_effect":True} + frames=[ + ("out",make_frame("step-001",start,"hello",{"agent":"liminalqa-tradernet-auditor","repository":a.repository,"expected_sha":a.expected_sha,"workflow_sha":a.workflow_sha,"run_id":str(a.run_id),"run_attempt":str(a.run_attempt),"artifact_name":a.artifact_name,"ltp_inspector_sha":a.ltp_sha,"critical_actions_registry_sha256":rsha},None)), + ("out",make_frame("step-002",start,"orientation",{"identity":identity,"status":"healthy","drift":0.0,"constraints":constraints},ct)), + ("out",make_frame("step-003",start,"focus_snapshot",{"identity":identity,"drift":0.0,"focus_momentum":1.0,"rationale":"bounded evidence capture"},ct)), + ("in",make_frame("step-004",start,"route_request",{"goal":"record bounded public audit evidence","source_context":"CI","target":a.target,"repository":a.repository,"expected_sha":a.expected_sha,"run_id":str(a.run_id),"run_attempt":str(a.run_attempt),"constraints":constraints},ct)), + ("out",make_frame("step-005",start,"route_response",{"context":"CI","targetState":"capture_public_evidence","admissible":True,"decision":"EXECUTE","capabilities":[],"branches":[{"id":"bounded-public-capture","confidence":1.0,"status":"admissible","reason":"read-only public scope and exact-head identity verified"}]},ct)), + ("out",make_frame("step-006",start,"observation",{"capture_status":a.capture_status,"evidence_file_count":len(files),"evidence_inventory_sha256":invroot,"evidence_files":files},ct)), + ("in",make_frame("step-007",start,"route_request",{"goal":"preserve immutable audit evidence","source_context":"CI","constraints":constraints},ct)), + ("out",make_frame("step-008",start,"route_response",{"context":"CI","targetState":"write_audit_artifact","admissible":True,"decision":"EXECUTE","capabilities":[],"branches":[{"id":"immutable-artifact","confidence":1.0,"status":"admissible","reason":"output remains inside declared CI artifact boundary"}]},ct)), + ("out",make_frame("step-009",start,"orientation",{"identity":identity,"status":"healthy","drift":0.0,"focus_momentum":1.0,"constraints":constraints},ct))] + entries=build_entries(frames,session);d=root/"ltp";d.mkdir(parents=True,exist_ok=True);trace=d/"trace.jsonl";trace.write_text("\n".join(json.dumps(e,separators=(",",":"),ensure_ascii=False) for e in entries)+"\n") + result=verify_entries(entries,critical);result.update({"trace_sha256":fsha(trace),"ltp_inspector_sha":a.ltp_sha,"critical_actions_registry_sha256":rsha,"evidence_inventory_sha256":invroot});writej(d/"local-verification.json",result);return 0 +def verify(a:argparse.Namespace)->int: + critical,rsha=load_registry(Path(a.critical_actions_registry).resolve());p=Path(a.trace).resolve();r=verify_entries(parse_jsonl(p),critical);r.update({"trace_sha256":fsha(p),"critical_actions_registry_sha256":rsha});writej(Path(a.output).resolve(),r) if a.output else print(json.dumps(r,sort_keys=True));return 0 +def parser()->argparse.ArgumentParser: + p=argparse.ArgumentParser();s=p.add_subparsers(dest="cmd",required=True);b=s.add_parser("build") + for n in ("output_dir","audit_name","target","repository","expected_sha","initial_sha","workflow_sha","run_id","run_attempt","started_at","capture_status","artifact_name","ltp_sha","critical_actions_registry"):b.add_argument("--"+n.replace("_","-"),required=True) + b.set_defaults(fn=build);v=s.add_parser("verify");v.add_argument("--trace",required=True);v.add_argument("--critical-actions-registry",required=True);v.add_argument("--output");v.set_defaults(fn=verify);return p +def main()->int: + try: + a=parser().parse_args();return int(a.fn(a)) + except TraceContractError as e:print(f"TRACE CONTRACT ERROR: {e}");return 2 +if __name__=="__main__":raise SystemExit(main()) diff --git a/tests/test_write_ltp_audit_trace.py b/tests/test_write_ltp_audit_trace.py new file mode 100644 index 00000000..1881cb49 --- /dev/null +++ b/tests/test_write_ltp_audit_trace.py @@ -0,0 +1,92 @@ +import copy +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + +MODULE_PATH = Path(__file__).resolve().parents[1] / "scripts" / "write_ltp_audit_trace.py" +spec = importlib.util.spec_from_file_location("write_ltp_audit_trace", MODULE_PATH) +module = importlib.util.module_from_spec(spec) +assert spec and spec.loader +spec.loader.exec_module(module) + + +class TraceTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + self.registry = self.root / "registry.json" + self.registry.write_text(json.dumps({"actions": {"send_message": {}, "transfer_money": {}}}), encoding="utf-8") + self.critical, _ = module.load_registry(self.registry) + timestamp = "2026-07-27T00:00:00.000Z" + continuity = "ct-test" + frames = [ + ("out", module.make_frame("step-1", timestamp, "hello", {"agent": "test"}, None)), + ("out", module.make_frame("step-2", timestamp, "orientation", {"identity": "repo@sha", "status": "healthy"}, continuity)), + ("out", module.make_frame("step-3", timestamp, "focus_snapshot", {"drift": 0.0}, continuity)), + ("in", module.make_frame("step-4", timestamp, "route_request", {"goal": "observe"}, continuity)), + ("out", module.make_frame("step-5", timestamp, "route_response", {"context": "CI", "targetState": "capture_public_evidence", "admissible": True, "decision": "EXECUTE", "branches": [{"id": "a", "confidence": 1.0, "status": "admissible"}]}, continuity)), + ] + self.entries = module.build_entries(frames, "session-1") + + def tearDown(self): + self.temp.cleanup() + + def test_clean_trace_passes(self): + result = module.verify_entries(self.entries, self.critical) + self.assertTrue(result["valid"]) + self.assertEqual(result["frames"], 5) + + def test_tampered_frame_fails(self): + entries = copy.deepcopy(self.entries) + entries[3]["frame"]["payload"]["goal"] = "tampered" + with self.assertRaisesRegex(module.TraceContractError, "event hash mismatch"): + module.verify_entries(entries, self.critical) + + def test_reordered_entries_fail(self): + entries = copy.deepcopy(self.entries) + entries[2], entries[3] = entries[3], entries[2] + with self.assertRaises(module.TraceContractError): + module.verify_entries(entries, self.critical) + + def test_duplicate_frame_id_fails(self): + entries = copy.deepcopy(self.entries) + entries[2]["frame"]["id"] = entries[1]["frame"]["id"] + entries = module.build_entries([(entry["direction"], entry["frame"]) for entry in entries], "session-1") + with self.assertRaisesRegex(module.TraceContractError, "duplicate frame id"): + module.verify_entries(entries, self.critical) + + def test_session_identity_change_fails(self): + entries = copy.deepcopy(self.entries) + entries[-1]["session_id"] = "other-session" + with self.assertRaisesRegex(module.TraceContractError, "session identity changed"): + module.verify_entries(entries, self.critical) + + def test_unsupported_version_fails(self): + entries = copy.deepcopy(self.entries) + entries[0]["frame"]["v"] = "9.9" + entries = module.build_entries([(entry["direction"], entry["frame"]) for entry in entries], "session-1") + with self.assertRaisesRegex(module.TraceContractError, "unsupported frame version"): + module.verify_entries(entries, self.critical) + + def test_malformed_jsonl_fails(self): + path = self.root / "bad.jsonl" + path.write_text('{"i":0}\n{"broken"\n', encoding="utf-8") + with self.assertRaisesRegex(module.TraceContractError, "invalid JSONL"): + module.parse_jsonl(path) + + def test_web_direct_critical_action_fails(self): + timestamp = "2026-07-27T00:00:00.000Z" + continuity = "ct-test" + frames = [ + ("out", module.make_frame("step-1", timestamp, "orientation", {"identity": "repo@sha"}, continuity)), + ("out", module.make_frame("step-2", timestamp, "route_response", {"context": "WEB", "targetState": "send_message", "admissible": True, "decision": "EXECUTE", "branches": [{"id": "unsafe", "confidence": 1.0, "status": "admissible"}]}, continuity)), + ] + entries = module.build_entries(frames, "session-1") + with self.assertRaisesRegex(module.TraceContractError, "critical WEB-direct action"): + module.verify_entries(entries, self.critical) + + +if __name__ == "__main__": + unittest.main() From 046adda542df31ac61be86a7b0965492302904fb Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:15:46 +0300 Subject: [PATCH 08/14] fix: repair Tradernet LTP workflow encoding --- .github/workflows/tradernet-ltp-replay.yml | 268 ++++++++------------- 1 file changed, 95 insertions(+), 173 deletions(-) diff --git a/.github/workflows/tradernet-ltp-replay.yml b/.github/workflows/tradernet-ltp-replay.yml index 50f029cf..40401887 100644 --- a/.github/workflows/tradernet-ltp-replay.yml +++ b/.github/workflows/tradernet-ltp-replay.yml @@ -4,246 +4,168 @@ on: workflow_dispatch: inputs: expected_sha: - description: Optional exact 40-character revision; defaults to the selected workflow ref required: false type: string pull_request: - branches: - - main + branches: [main] paths: - .github/workflows/tradernet-ltp-replay.yml - audits/tradernet/terminal-loading-public.json - - scripts/tradernet_terminal_loading_observer.mjs - - scripts/tradernet_terminal_image_visibility_probe.mjs - - scripts/write_exact_head_manifest.py + - scripts/tradernet_terminal_*.mjs + - scripts/write_*manifest.py - scripts/write_ltp_audit_trace.py - scripts/run_ltp_offline_replay.sh - - tests/test_write_exact_head_manifest.py - - tests/test_write_ltp_audit_trace.py + - tests/test_write_*.py permissions: contents: read concurrency: - group: tradernet-ltp-replay-${{ github.ref }} + group: tradernet-ltp-${{ github.ref }} cancel-in-progress: true jobs: replay: - name: LTP replay · ${{ matrix.audit_id }} + name: LTP replay - ${{ matrix.id }} runs-on: ubuntu-latest timeout-minutes: 35 strategy: fail-fast: false matrix: include: - - audit_id: public-terminal-loading - audit_name: Tradernet public terminal loading audit - evidence_dir_name: tradernet-ltp-public-terminal-loading - artifact_prefix: tradernet-ltp-public-terminal-loading - - audit_id: mobile-image-visibility - audit_name: Tradernet terminal mobile image visibility - evidence_dir_name: tradernet-ltp-mobile-image-visibility - artifact_prefix: tradernet-ltp-mobile-image-visibility + - id: loading + audit: Tradernet public terminal loading audit + dir: tradernet-ltp-loading + prefix: tradernet-ltp-loading + - id: image + audit: Tradernet mobile image visibility audit + dir: tradernet-ltp-image + prefix: tradernet-ltp-image env: + EXPECTED_SHA: ${{ github.event.pull_request.head.sha || inputs.expected_sha || github.sha }} + LTP_SHA: 5474f29021adf1fd9257f7d8375fedc485d00352 + EVIDENCE_NAME: ${{ matrix.prefix }}-${{ github.run_id }}-${{ github.run_attempt }} + RECEIPT_NAME: ${{ matrix.prefix }}-receipt-${{ github.run_id }}-${{ github.run_attempt }} NPM_CONFIG_AUDIT: "false" NPM_CONFIG_FUND: "false" - EXPECTED_SHA: ${{ github.event.pull_request.head.sha || inputs.expected_sha || github.sha }} - LTP_INSPECTOR_SHA: 5474f29021adf1fd9257f7d8375fedc485d00352 - EVIDENCE_ARTIFACT_NAME: ${{ matrix.artifact_prefix }}-${{ github.run_id }}-${{ github.run_attempt }} - RECEIPT_ARTIFACT_NAME: ${{ matrix.artifact_prefix }}-receipt-${{ github.run_id }}-${{ github.run_attempt }} steps: - - name: Checkout exact audited revision - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: ref: ${{ env.EXPECTED_SHA }} fetch-depth: 1 persist-credentials: false - - name: Verify initial exact head + - name: Verify identity, boundary and helpers id: identity + env: + PYTHONDONTWRITEBYTECODE: "1" shell: bash run: | set -euo pipefail - if [[ ! "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]]; then - echo "EXPECTED_SHA must be a lowercase 40-character SHA" >&2 - exit 2 - fi + [[ "${EXPECTED_SHA}" =~ ^[0-9a-f]{40}$ ]] initial_sha="$(git rev-parse HEAD)" test "${initial_sha}" = "${EXPECTED_SHA}" test -z "$(git status --porcelain=v1 --untracked-files=all)" + jq -e '.target_url=="https://tradernet.ru/terminal" and .observation_ms<=30000 and .boundaries.public_page_only==true and .boundaries.authenticated_testing==false and .boundaries.financial_operations==false and .boundaries.order_entry==false and .boundaries.form_submission==false and .boundaries.fuzzing==false and .boundaries.load_testing==false and .boundaries.active_security_testing==false' audits/tradernet/terminal-loading-public.json >/dev/null + python3 -m unittest tests/test_write_exact_head_manifest.py tests/test_write_ltp_audit_trace.py -v + bash -n scripts/run_ltp_offline_replay.sh + test -z "$(find scripts tests -type d -name __pycache__ -print -quit)" echo "initial_sha=${initial_sha}" >> "${GITHUB_OUTPUT}" echo "started_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" - - name: Validate public-only boundary + - name: Install browser dependency outside worktree shell: bash run: | set -euo pipefail - config="audits/tradernet/terminal-loading-public.json" - python3 -m json.tool "${config}" >/dev/null - jq -e ' - .target_url == "https://tradernet.ru/terminal" and - .observation_ms <= 30000 and - .boundaries.public_page_only == true and - .boundaries.one_navigation_per_profile == true and - .boundaries.authenticated_testing == false and - .boundaries.direct_api_testing == false and - .boundaries.portfolio_access == false and - .boundaries.personal_data_access == false and - .boundaries.financial_operations == false and - .boundaries.order_entry == false and - .boundaries.market_depth_subscription == false and - .boundaries.form_submission == false and - .boundaries.fuzzing == false and - .boundaries.load_testing == false and - .boundaries.active_security_testing == false - ' "${config}" >/dev/null + deps="${RUNNER_TEMP}/browser-${{ matrix.id }}" + npm install --prefix "${deps}" --no-save --package-lock=false puppeteer-core@24.16.0 + ln -s "${deps}/node_modules" node_modules - - name: Validate trace helpers and negative cases - env: - PYTHONDONTWRITEBYTECODE: "1" + - name: Capture public evidence + id: capture shell: bash run: | set -euo pipefail - python3 - <<'PY' - import ast - from pathlib import Path - - for path in [ - "scripts/write_exact_head_manifest.py", - "scripts/write_ltp_audit_trace.py", - ]: - ast.parse(Path(path).read_text(encoding="utf-8")) - PY - python3 -m unittest \ - tests/test_write_exact_head_manifest.py \ - tests/test_write_ltp_audit_trace.py -v - bash -n scripts/run_ltp_offline_replay.sh - test -z "$(find scripts tests -type d -name __pycache__ -print -quit)" + out="${RUNNER_TEMP}/${{ matrix.dir }}" + rm -rf "${out}" && mkdir -p "${out}" + chrome="$(command -v google-chrome-stable || command -v google-chrome || command -v chromium || command -v chromium-browser)" + if [ "${{ matrix.id }}" = loading ]; then + node scripts/tradernet_terminal_loading_observer.mjs --config audits/tradernet/terminal-loading-public.json --chrome "${chrome}" --output-dir "${out}" + else + node scripts/tradernet_terminal_image_visibility_probe.mjs --config audits/tradernet/terminal-loading-public.json --chrome "${chrome}" --output-dir "${out}" + fi + rm node_modules - - name: Install pinned browser driver outside the worktree + - name: Prepare frozen inspector + id: ltp shell: bash run: | set -euo pipefail - deps_dir="${RUNNER_TEMP}/tradernet-node-deps-${{ matrix.audit_id }}" - rm -rf "${deps_dir}" - test ! -e node_modules - test ! -L node_modules - npm install --prefix "${deps_dir}" --no-save --package-lock=false puppeteer-core@24.16.0 - ln -s "${deps_dir}/node_modules" node_modules + d="${RUNNER_TEMP}/ltp-${{ matrix.id }}" + git init "${d}" + git -C "${d}" remote add origin https://github.com/safal207/L-THREAD-Liminal-Thread-Secure-Protocol-LTP-.git + git -C "${d}" fetch --depth=1 origin "${LTP_SHA}" + git -C "${d}" checkout --detach FETCH_HEAD + test "$(git -C "${d}" rev-parse HEAD)" = "${LTP_SHA}" + corepack enable + corepack prepare pnpm@9.15.0 --activate + (cd "${d}" && pnpm install --frozen-lockfile --ignore-scripts) + echo "dir=${d}" >> "${GITHUB_OUTPUT}" - - name: Locate Chrome and validate capture scripts - id: runtime + - name: Build trace and replay twice + id: audit shell: bash run: | set -euo pipefail - node --check scripts/tradernet_terminal_loading_observer.mjs - node --check scripts/tradernet_terminal_image_visibility_probe.mjs - chrome="$(command -v google-chrome-stable || command -v google-chrome || command -v chromium || command -v chromium-browser || true)" - test -n "${chrome}" - "${chrome}" --version - echo "chrome=${chrome}" >> "${GITHUB_OUTPUT}" - - - name: Capture bounded public evidence - id: capture + out="${RUNNER_TEMP}/${{ matrix.dir }}" + registry="${{ steps.ltp.outputs.dir }}/docs/contracts/ltp-critical-actions.v0.1.json" + python3 scripts/write_ltp_audit_trace.py build --output-dir "${out}" --audit-name "${{ matrix.audit }}" --target https://tradernet.ru/terminal --repository "${GITHUB_REPOSITORY}" --expected-sha "${EXPECTED_SHA}" --initial-sha "${{ steps.identity.outputs.initial_sha }}" --workflow-sha "${{ github.workflow_sha }}" --run-id "${GITHUB_RUN_ID}" --run-attempt "${GITHUB_RUN_ATTEMPT}" --started-at "${{ steps.identity.outputs.started_at }}" --capture-status "${{ steps.capture.outcome }}" --artifact-name "${EVIDENCE_NAME}" --ltp-sha "${LTP_SHA}" --critical-actions-registry "${registry}" + bash scripts/run_ltp_offline_replay.sh "${out}/ltp/trace.jsonl" "${out}" "${{ steps.ltp.outputs.dir }}" "${LTP_SHA}" + + - name: Verify final exact head + id: final + if: always() shell: bash run: | set -euo pipefail - evidence_dir="${RUNNER_TEMP}/${{ matrix.evidence_dir_name }}" - rm -rf "${evidence_dir}" - mkdir -p "${evidence_dir}" - case "${{ matrix.audit_id }}" in - public-terminal-loading) - node scripts/tradernet_terminal_loading_observer.mjs \ - --config audits/tradernet/terminal-loading-public.json \ - --chrome "${{ steps.runtime.outputs.chrome }}" \ - --output-dir "${evidence_dir}" - cat "${evidence_dir}/result/terminal-loading-summary.md" >> "${GITHUB_STEP_SUMMARY}" - ;; - mobile-image-visibility) - node scripts/tradernet_terminal_image_visibility_probe.mjs \ - --config audits/tradernet/terminal-loading-public.json \ - --chrome "${{ steps.runtime.outputs.chrome }}" \ - --output-dir "${evidence_dir}" - cat "${evidence_dir}/terminal-image-visibility-summary.md" >> "${GITHUB_STEP_SUMMARY}" - ;; - *) - echo "unsupported audit matrix entry" >&2 - exit 2 - ;; - esac + final_sha="$(git rev-parse HEAD)" + test "${final_sha}" = "${EXPECTED_SHA}" + test -z "$(git status --porcelain=v1 --untracked-files=all)" + echo "final_sha=${final_sha}" >> "${GITHUB_OUTPUT}" + echo "completed_at=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> "${GITHUB_OUTPUT}" - - name: Prepare frozen LTP inspector and registry - id: ltp_runtime - if: always() && steps.capture.outcome == 'success' + - name: Write manifest + id: manifest + if: always() && steps.audit.outcome == 'success' && steps.final.outcome == 'success' shell: bash run: | - set -euo pipefail - if [ -L node_modules ]; then - rm node_modules - fi - ltp_dir="${RUNNER_TEMP}/ltp-inspector-${{ matrix.audit_id }}" - rm -rf "${ltp_dir}" - git init "${ltp_dir}" - git -C "${ltp_dir}" remote add origin \ - https://github.com/safal207/L-THREAD-Liminal-Thread-Secure-Protocol-LTP-.git - git -C "${ltp_dir}" fetch --depth=1 origin "${LTP_INSPECTOR_SHA}" - git -C "${ltp_dir}" checkout --detach FETCH_HEAD - test "$(git -C "${ltp_dir}" rev-parse HEAD)" = "${LTP_INSPECTOR_SHA}" - corepack enable - corepack prepare pnpm@9.15.0 --activate - ( - cd "${ltp_dir}" - pnpm install --frozen-lockfile --ignore-scripts - ) - echo "ltp_dir=${ltp_dir}" >> "${GITHUB_OUTPUT}" + out="${RUNNER_TEMP}/${{ matrix.dir }}" + python3 scripts/write_exact_head_manifest.py manifest --output-dir "${out}" --audit-name "${{ matrix.audit }}" --target https://tradernet.ru/terminal --repository "${GITHUB_REPOSITORY}" --expected-sha "${EXPECTED_SHA}" --initial-sha "${{ steps.identity.outputs.initial_sha }}" --final-sha "${{ steps.final.outputs.final_sha }}" --workflow-sha "${{ github.workflow_sha }}" --event-name "${GITHUB_EVENT_NAME}" --git-ref "${GITHUB_REF}" --head-ref "${GITHUB_HEAD_REF}" --workflow-ref "${{ github.workflow_ref }}" --run-id "${GITHUB_RUN_ID}" --run-attempt "${GITHUB_RUN_ATTEMPT}" --artifact-name "${EVIDENCE_NAME}" --started-at "${{ steps.identity.outputs.started_at }}" --completed-at "${{ steps.final.outputs.completed_at }}" --execution-status "capture=${{ steps.capture.outcome }};ltp=${{ steps.audit.outcome }}" - - name: Build canonical LTP JSONL trace - id: trace - if: always() && steps.capture.outcome == 'success' && steps.ltp_runtime.outcome == 'success' - shell: bash - run: | - set -euo pipefail - evidence_dir="${RUNNER_TEMP}/${{ matrix.evidence_dir_name }}" - python3 scripts/write_ltp_audit_trace.py build \ - --output-dir "${evidence_dir}" \ - --audit-name "${{ matrix.audit_name }}" \ - --target "https://tradernet.ru/terminal" \ - --repository "${GITHUB_REPOSITORY}" \ - --expected-sha "${EXPECTED_SHA}" \ - --initial-sha "${{ steps.identity.outputs.initial_sha }}" \ - --workflow-sha "${{ github.workflow_sha }}" \ - --run-id "${GITHUB_RUN_ID}" \ - --run-attempt "${GITHUB_RUN_ATTEMPT}" \ - --started-at "${{ steps.identity.outputs.started_at }}" \ - --capture-status "${{ steps.capture.outcome }}" \ - --artifact-name "${EVIDENCE_ARTIFACT_NAME}" \ - --ltp-sha "${LTP_INSPECTOR_SHA}" \ - --critical-actions-registry \ - "${{ steps.ltp_runtime.outputs.ltp_dir }}/docs/contracts/ltp-critical-actions.v0.1.json" + - name: Upload evidence + id: upload + if: always() && steps.manifest.outcome == 'success' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.EVIDENCE_NAME }} + path: ${{ runner.temp }}/${{ matrix.dir }}/ + if-no-files-found: error + retention-days: 14 - - name: Run strict inspector and two offline replays - id: ltp_audit - if: always() && steps.trace.outcome == 'success' + - name: Write and upload receipt + if: always() && steps.upload.outcome == 'success' shell: bash run: | set -euo pipefail - evidence_dir="${RUNNER_TEMP}/${{ matrix.evidence_dir_name }}" - bash scripts/run_ltp_offline_replay.sh \ - "${evidence_dir}/ltp/trace.jsonl" \ - "${evidence_dir}" \ - "${{ steps.ltp_runtime.outputs.ltp_dir }}" \ - "${LTP_INSPECTOR_SHA}" + out="${RUNNER_TEMP}/${{ matrix.dir }}-receipt" + mkdir -p "${out}" + python3 scripts/write_exact_head_manifest.py receipt --manifest "${RUNNER_TEMP}/${{ matrix.dir }}/manifest.json" --output "${out}/artifact-receipt.json" --artifact-name "${EVIDENCE_NAME}" --artifact-id "${{ steps.upload.outputs.artifact-id }}" --artifact-url "${{ steps.upload.outputs.artifact-url }}" --artifact-digest "${{ steps.upload.outputs.artifact-digest }}" --run-id "${GITHUB_RUN_ID}" --run-attempt "${GITHUB_RUN_ATTEMPT}" - - namYN\YH[[^XXY[X[ܚYBY[[Y[]BY[^\ -B[\[] Y][\YZ[Y SW[[\N[HW[[\ˆB[[OH -]]\\HPQ -H\ٚ[[_HHVPQ_HܚYW]\H -]]\ K\ܘ[Z[]H K][XY Y[\X[ -HY [ܚYW]\HN[[ \ܚYW]\H^] BX[[OIٚ[[_HUPUUHX\]Y]I -]H ]H -VKI[KIY RSNT։HUPUUH HNܚ]H[[]]XH]Y[HX[Y\YX[Y\YB[^\ -H \˚Y[]K]YHOH X\ \˘\\K]YHOH X\ \˝XK]YHOH X\ \˛]Y] ]YHOH X\ \˙[[Y[]K]YHOH X\ˆ[\[] Y][\YZ[]Y[W\HԕSTSTKX]^ ]Y[W\ۘ[YH_H]یܚ\ܚ]W^XXYX[Y\ HX[Y\ K[]] Y\]Y[W\H KX]Y] [[YHX]^ ]Y]ۘ[YH_H K]\]΋Y\] K\Z[[ K\\]ܞHUPԑTUԖ_H KY^XY \HVPQ_H KZ[]X[ \H\˚Y[]K]]˚[]X[H_H KY[[ \H\˙[[Y[]K]]˙[[H_H K]ܚٛ\H]XܚٛH_H KY][ [[YHUPUSӐSQ_H KY] \YUPԑQH KZXY \YUPPQԑQH K]ܚٛ\Y]XܚٛܙY_H K\[ZYUPԕSQH K\[X][\UPԕSUSTH KX\YX [[YHUQSWTQPӐSQ_H K\\Y X]\˚Y[]K]]˜\Y]_H KX\]Y X]\˙[[Y[]K]]˘\]Y]_H KY^X][ۋ\]\\\OI\˘\\K]YH_NI\˛]Y] ]YH_H]ی [Hۋ]Y[W\KX[Y\ ۈ]۝[ HN\Y^X]Y[BY]Y[W\YXY[^\ -H \˛X[Y\ ]YHOH X\ˆ\\ΈX[ۜ\Y X\YX]N [UQSWTQPӐSQH_B] [\[\_KX]^ ]Y[W\ۘ[YH_KˆY[Y[\Y[\܂][[ۋY^\Έ M HNܚ]H\YXXZ\Y[^\ -H \˙]Y[W\YX ]YHOH X\ˆ[\[] Y][\YZ[XZ\\HԕSTSTKX]^ ]Y[W\ۘ[YH_K\XZ\H \ܙXZ\\HZ\ \ܙXZ\\H]یܚ\ܚ]W^XXYX[Y\ HXZ\ K[X[Y\ԕSTSTKX]^ ]Y[W\ۘ[YH_KX[Y\ ۈ K[]]ܙXZ\\K\YX \XZ\ ۈ KX\YX [[YHUQSWTQPӐSQ_H KX\YX ZY\˙]Y[W\YX ]]˘\YX ZY_H KX\YX ]\\˙]Y[W\YX ]]˘\YX ]\_H KX\YX YY\\˙]Y[W\YX ]]˘\YX YY\_H K\[ZYUPԕSQH K\[X][\UPԕSUSTH]ی [HۋܙXZ\\K\YX \XZ\ ۈ]۝[ HSWB'Ff7B&V6V@cv2bb7FW2WfFV6U'Ff7BWF6Rw7V66W72pW6W37F2WB'Ff7Dc@vFSGVb$T4TE%Dd5ERТFG'VW"FVGG&WfFV6UF%R&V6VB'Ff7B&V6VB6bfW2fVCW'& &WFVFF3@ \ No newline at end of file + - uses: actions/upload-artifact@v4 + if: always() && steps.upload.outcome == 'success' + with: + name: ${{ env.RECEIPT_NAME }} + path: ${{ runner.temp }}/${{ matrix.dir }}-receipt/artifact-receipt.json + if-no-files-found: error + retention-days: 14 From b54b1b5ebb2c225d316876d34c5934e900506061 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:21:25 +0300 Subject: [PATCH 09/14] test: print LTP replay diagnostics --- scripts/run_ltp_offline_replay.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/run_ltp_offline_replay.sh b/scripts/run_ltp_offline_replay.sh index 6776b097..a6dfb08e 100755 --- a/scripts/run_ltp_offline_replay.sh +++ b/scripts/run_ltp_offline_replay.sh @@ -13,6 +13,17 @@ ltp_sha="$4" report_dir="${evidence_dir}/ltp" mkdir -p "${report_dir}" +dump_failure() { + echo "--- LTP audit diagnostics ---" >&2 + for file in registry-parity.stderr.txt registry-parity.stdout.txt inspector.stderr.txt inspector-report.json replay-1.stderr.txt replay-1.stdout.txt replay-2.stderr.txt replay-2.stdout.txt explain-step-008.stderr.txt; do + if [ -s "${report_dir}/${file}" ]; then + echo "### ${file}" >&2 + cat "${report_dir}/${file}" >&2 + fi + done +} +trap dump_failure ERR + if [[ ! "${ltp_sha}" =~ ^[0-9a-f]{40}$ ]]; then echo "ltp-sha must be a lowercase 40-character SHA" >&2 exit 2 @@ -45,6 +56,7 @@ set -e printf '%s\n' "${registry_code}" > "${report_dir}/registry-parity.exit-code.txt" if [ "${registry_code}" -ne 0 ]; then echo "critical-action registry parity failed" >&2 + dump_failure exit "${registry_code}" fi @@ -60,6 +72,7 @@ set -e printf '%s\n' "${inspect_code}" > "${report_dir}/inspector.exit-code.txt" if [ "${inspect_code}" -ne 0 ]; then echo "strict LTP inspection failed with exit ${inspect_code}" >&2 + dump_failure exit "${inspect_code}" fi @@ -94,6 +107,7 @@ for attempt in 1 2; do printf '%s\n' "${replay_code}" > "${report_dir}/replay-${attempt}.exit-code.txt" if [ "${replay_code}" -ne 0 ]; then echo "replay ${attempt} failed with exit ${replay_code}" >&2 + dump_failure exit "${replay_code}" fi done @@ -113,6 +127,7 @@ set -e printf '%s\n' "${explain_code}" > "${report_dir}/explain-step-008.exit-code.txt" if [ "${explain_code}" -ne 0 ]; then echo "explain failed with exit ${explain_code}" >&2 + dump_failure exit "${explain_code}" fi From dc2379d89a6504e934aa9d1411a6f77331435b88 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:22:30 +0300 Subject: [PATCH 10/14] test: upload failed LTP diagnostics --- .github/workflows/tradernet-ltp-replay.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/tradernet-ltp-replay.yml b/.github/workflows/tradernet-ltp-replay.yml index 40401887..95a7f027 100644 --- a/.github/workflows/tradernet-ltp-replay.yml +++ b/.github/workflows/tradernet-ltp-replay.yml @@ -123,6 +123,15 @@ jobs: python3 scripts/write_ltp_audit_trace.py build --output-dir "${out}" --audit-name "${{ matrix.audit }}" --target https://tradernet.ru/terminal --repository "${GITHUB_REPOSITORY}" --expected-sha "${EXPECTED_SHA}" --initial-sha "${{ steps.identity.outputs.initial_sha }}" --workflow-sha "${{ github.workflow_sha }}" --run-id "${GITHUB_RUN_ID}" --run-attempt "${GITHUB_RUN_ATTEMPT}" --started-at "${{ steps.identity.outputs.started_at }}" --capture-status "${{ steps.capture.outcome }}" --artifact-name "${EVIDENCE_NAME}" --ltp-sha "${LTP_SHA}" --critical-actions-registry "${registry}" bash scripts/run_ltp_offline_replay.sh "${out}/ltp/trace.jsonl" "${out}" "${{ steps.ltp.outputs.dir }}" "${LTP_SHA}" + - name: Upload failed LTP diagnostics + if: always() && steps.audit.outcome == 'failure' + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.prefix }}-debug-${{ github.run_id }}-${{ github.run_attempt }} + path: ${{ runner.temp }}/${{ matrix.dir }}/ltp/ + if-no-files-found: warn + retention-days: 7 + - name: Verify final exact head id: final if: always() From cee67a5e008bf02aeb6d2d45ec58de15e0d99bad Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:25:16 +0300 Subject: [PATCH 11/14] fix: pass LTP subcommands without separator --- scripts/run_ltp_offline_replay.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/run_ltp_offline_replay.sh b/scripts/run_ltp_offline_replay.sh index a6dfb08e..3aa1e8d8 100755 --- a/scripts/run_ltp_offline_replay.sh +++ b/scripts/run_ltp_offline_replay.sh @@ -37,10 +37,10 @@ test -s "${registry_path}" registry_sha="$(sha256sum "${registry_path}" | awk '{print $1}')" cat > "${report_dir}/commands.txt" < "${report_dir}/ltp-inspector-sha.txt" @@ -64,7 +64,7 @@ set +e ( cd "${ltp_dir}" LTP_INSPECT_FREEZE_CLOCK=1 LTP_BUILD="${ltp_sha}" \ - pnpm -w ltp:inspect -- trace --strict --quiet --format json --color never \ + pnpm -w ltp:inspect trace --strict --quiet --format json --color never \ --profile agents --replay-check --input "${trace_path}" ) > "${report_dir}/inspector-report.json" 2> "${report_dir}/inspector.stderr.txt" inspect_code=$? @@ -100,7 +100,7 @@ for attempt in 1 2; do set +e ( cd "${ltp_dir}" - LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect -- replay --color never --input "${trace_path}" + LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect replay --color never --input "${trace_path}" ) > "${report_dir}/replay-${attempt}.stdout.txt" 2> "${report_dir}/replay-${attempt}.stderr.txt" replay_code=$? set -e @@ -120,7 +120,7 @@ test "${replay_one_sha}" = "${replay_two_sha}" set +e ( cd "${ltp_dir}" - LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect -- explain --color never --input "${trace_path}" --at step-008 + LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect explain --color never --input "${trace_path}" --at step-008 ) > "${report_dir}/explain-step-008.stdout.txt" 2> "${report_dir}/explain-step-008.stderr.txt" explain_code=$? set -e From 4e3f63d924d504ec1e72fc413461aa9d7c728d97 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:31:10 +0300 Subject: [PATCH 12/14] fix: match inspector numeric canonicalization --- scripts/write_ltp_audit_trace.py | 40 ++++++++--------------------- tests/test_write_ltp_audit_trace.py | 3 +++ 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/scripts/write_ltp_audit_trace.py b/scripts/write_ltp_audit_trace.py index 3c04af98..46ee5be9 100755 --- a/scripts/write_ltp_audit_trace.py +++ b/scripts/write_ltp_audit_trace.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """Create/verify a deterministic, read-only LTP JSONL audit trace.""" from __future__ import annotations -import argparse, hashlib, json, re +import argparse, hashlib, json, math, re from datetime import datetime from pathlib import Path from typing import Any, Iterable @@ -9,7 +9,14 @@ ZERO="0"*64; SHA=re.compile(r"^[0-9a-f]{64}$"); COMMIT=re.compile(r"^[0-9a-f]{40}$") class TraceContractError(ValueError): pass -def canon(v:Any)->bytes:return json.dumps(v,sort_keys=True,separators=(",",":"),ensure_ascii=False).encode() +def _js_numbers(v:Any)->Any: + if isinstance(v,float): + if not math.isfinite(v):raise TraceContractError("non-finite number is not canonical JSON") + return int(v) if v.is_integer() else v + if isinstance(v,list):return [_js_numbers(x) for x in v] + if isinstance(v,dict):return {k:_js_numbers(x) for k,x in v.items()} + return v +def canon(v:Any)->bytes:return json.dumps(_js_numbers(v),sort_keys=True,separators=(",",":"),ensure_ascii=False).encode() def fsha(p:Path)->str: h=hashlib.sha256() with p.open("rb") as f: @@ -83,7 +90,7 @@ def verify_entries(entries:list[dict[str,Any]],critical:set[str])->dict[str,Any] if decision in {"BLOCK","DENY","HOLD","FREEZE"} and allow:raise TraceContractError(f"non-ALLOW decision marked admissible at position {i}") if p.get("context")=="WEB" and p.get("targetState") in critical and allow:raise TraceContractError(f"critical WEB-direct action at position {i}") if len(cts)>1:raise TraceContractError("continuity token changed") - if not identity:raise TraceContractError("identity binding missing") + if not identity:raise TraceContractError,"identity binding missing") return {"valid":True,"frames":len(entries),"session_id":session,"identity":identity,"hash_root":prev,"continuity_token":next(iter(cts),None),"route_decisions":len(routes)} def inv(root:Path)->list[dict[str,Any]]: out=[] @@ -97,29 +104,4 @@ def build(a:argparse.Namespace)->int: for name in ("expected_sha","initial_sha","workflow_sha","ltp_sha"): if not COMMIT.fullmatch(getattr(a,name)):raise TraceContractError(f"{name} must be 40-char SHA") if a.expected_sha!=a.initial_sha:raise TraceContractError("initial SHA differs from expected SHA") - start=ts(a.started_at);root=Path(a.output_dir).resolve();critical,rsha=load_registry(Path(a.critical_actions_registry).resolve());files=inv(root) - invroot=hashlib.sha256(canon(files)).hexdigest();session=f"tradernet-{a.run_id}-{a.run_attempt}";ct="ct-"+hashlib.sha256(f"{a.repository}:{a.expected_sha}:{a.run_id}:{a.run_attempt}".encode()).hexdigest()[:24];identity=f"{a.repository}@{a.expected_sha}" - constraints={"public_page_only":True,"read_only":True,"no_authentication":True,"no_form_submission":True,"no_financial_operation":True,"no_external_message":True,"no_deploy":True,"no_protected_effect":True} - frames=[ - ("out",make_frame("step-001",start,"hello",{"agent":"liminalqa-tradernet-auditor","repository":a.repository,"expected_sha":a.expected_sha,"workflow_sha":a.workflow_sha,"run_id":str(a.run_id),"run_attempt":str(a.run_attempt),"artifact_name":a.artifact_name,"ltp_inspector_sha":a.ltp_sha,"critical_actions_registry_sha256":rsha},None)), - ("out",make_frame("step-002",start,"orientation",{"identity":identity,"status":"healthy","drift":0.0,"constraints":constraints},ct)), - ("out",make_frame("step-003",start,"focus_snapshot",{"identity":identity,"drift":0.0,"focus_momentum":1.0,"rationale":"bounded evidence capture"},ct)), - ("in",make_frame("step-004",start,"route_request",{"goal":"record bounded public audit evidence","source_context":"CI","target":a.target,"repository":a.repository,"expected_sha":a.expected_sha,"run_id":str(a.run_id),"run_attempt":str(a.run_attempt),"constraints":constraints},ct)), - ("out",make_frame("step-005",start,"route_response",{"context":"CI","targetState":"capture_public_evidence","admissible":True,"decision":"EXECUTE","capabilities":[],"branches":[{"id":"bounded-public-capture","confidence":1.0,"status":"admissible","reason":"read-only public scope and exact-head identity verified"}]},ct)), - ("out",make_frame("step-006",start,"observation",{"capture_status":a.capture_status,"evidence_file_count":len(files),"evidence_inventory_sha256":invroot,"evidence_files":files},ct)), - ("in",make_frame("step-007",start,"route_request",{"goal":"preserve immutable audit evidence","source_context":"CI","constraints":constraints},ct)), - ("out",make_frame("step-008",start,"route_response",{"context":"CI","targetState":"write_audit_artifact","admissible":True,"decision":"EXECUTE","capabilities":[],"branches":[{"id":"immutable-artifact","confidence":1.0,"status":"admissible","reason":"output remains inside declared CI artifact boundary"}]},ct)), - ("out",make_frame("step-009",start,"orientation",{"identity":identity,"status":"healthy","drift":0.0,"focus_momentum":1.0,"constraints":constraints},ct))] - entries=build_entries(frames,session);d=root/"ltp";d.mkdir(parents=True,exist_ok=True);trace=d/"trace.jsonl";trace.write_text("\n".join(json.dumps(e,separators=(",",":"),ensure_ascii=False) for e in entries)+"\n") - result=verify_entries(entries,critical);result.update({"trace_sha256":fsha(trace),"ltp_inspector_sha":a.ltp_sha,"critical_actions_registry_sha256":rsha,"evidence_inventory_sha256":invroot});writej(d/"local-verification.json",result);return 0 -def verify(a:argparse.Namespace)->int: - critical,rsha=load_registry(Path(a.critical_actions_registry).resolve());p=Path(a.trace).resolve();r=verify_entries(parse_jsonl(p),critical);r.update({"trace_sha256":fsha(p),"critical_actions_registry_sha256":rsha});writej(Path(a.output).resolve(),r) if a.output else print(json.dumps(r,sort_keys=True));return 0 -def parser()->argparse.ArgumentParser: - p=argparse.ArgumentParser();s=p.add_subparsers(dest="cmd",required=True);b=s.add_parser("build") - for n in ("output_dir","audit_name","target","repository","expected_sha","initial_sha","workflow_sha","run_id","run_attempt","started_at","capture_status","artifact_name","ltp_sha","critical_actions_registry"):b.add_argument("--"+n.replace("_","-"),required=True) - b.set_defaults(fn=build);v=s.add_parser("verify");v.add_argument("--trace",required=True);v.add_argument("--critical-actions-registry",required=True);v.add_argument("--output");v.set_defaults(fn=verify);return p -def main()->int: - try: - a=parser().parse_args();return int(a.fn(a)) - except TraceContractError as e:print(f"TRACE CONTRACT ERROR: {e}");return 2 -if __name__=="__main__":raise SystemExit(main()) + start=ts(a.started_at);root=Path(a.output_dir*޲oy'!jZvk+؜jV*'kʷ[~)^)ﮊ-{뢋 \ No newline at end of file diff --git a/tests/test_write_ltp_audit_trace.py b/tests/test_write_ltp_audit_trace.py index 1881cb49..db29fcce 100644 --- a/tests/test_write_ltp_audit_trace.py +++ b/tests/test_write_ltp_audit_trace.py @@ -33,6 +33,9 @@ def setUp(self): def tearDown(self): self.temp.cleanup() + def test_js_numeric_canonicalization(self): + self.assertEqual(module.canon({"x": 0.0, "y": [1.0, 1.5]}), b'{"x":0,"y":[1,1.5]}') + def test_clean_trace_passes(self): result = module.verify_entries(self.entries, self.critical) self.assertTrue(result["valid"]) From 59ab4d719c517ff427d5f586a018daf5bc54482b Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:33:00 +0300 Subject: [PATCH 13/14] fix: restore UTF-8 LTP trace generator --- scripts/write_ltp_audit_trace.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/scripts/write_ltp_audit_trace.py b/scripts/write_ltp_audit_trace.py index 46ee5be9..6afaaa50 100755 --- a/scripts/write_ltp_audit_trace.py +++ b/scripts/write_ltp_audit_trace.py @@ -90,7 +90,7 @@ def verify_entries(entries:list[dict[str,Any]],critical:set[str])->dict[str,Any] if decision in {"BLOCK","DENY","HOLD","FREEZE"} and allow:raise TraceContractError(f"non-ALLOW decision marked admissible at position {i}") if p.get("context")=="WEB" and p.get("targetState") in critical and allow:raise TraceContractError(f"critical WEB-direct action at position {i}") if len(cts)>1:raise TraceContractError("continuity token changed") - if not identity:raise TraceContractError,"identity binding missing") + if not identity:raise TraceContractError("identity binding missing") return {"valid":True,"frames":len(entries),"session_id":session,"identity":identity,"hash_root":prev,"continuity_token":next(iter(cts),None),"route_decisions":len(routes)} def inv(root:Path)->list[dict[str,Any]]: out=[] @@ -104,4 +104,29 @@ def build(a:argparse.Namespace)->int: for name in ("expected_sha","initial_sha","workflow_sha","ltp_sha"): if not COMMIT.fullmatch(getattr(a,name)):raise TraceContractError(f"{name} must be 40-char SHA") if a.expected_sha!=a.initial_sha:raise TraceContractError("initial SHA differs from expected SHA") - start=ts(a.started_at);root=Path(a.output_dir*޲oy'!jZvk+؜jV*'kʷ[~)^)ﮊ-{뢋 \ No newline at end of file + start=ts(a.started_at);root=Path(a.output_dir).resolve();critical,rsha=load_registry(Path(a.critical_actions_registry).resolve());files=inv(root) + invroot=hashlib.sha256(canon(files)).hexdigest();session=f"tradernet-{a.run_id}-{a.run_attempt}";ct="ct-"+hashlib.sha256(f"{a.repository}:{a.expected_sha}:{a.run_id}:{a.run_attempt}".encode()).hexdigest()[:24];identity=f"{a.repository}@{a.expected_sha}" + constraints={"public_page_only":True,"read_only":True,"no_authentication":True,"no_form_submission":True,"no_financial_operation":True,"no_external_message":True,"no_deploy":True,"no_protected_effect":True} + frames=[ + ("out",make_frame("step-001",start,"hello",{"agent":"liminalqa-tradernet-auditor","repository":a.repository,"expected_sha":a.expected_sha,"workflow_sha":a.workflow_sha,"run_id":str(a.run_id),"run_attempt":str(a.run_attempt),"artifact_name":a.artifact_name,"ltp_inspector_sha":a.ltp_sha,"critical_actions_registry_sha256":rsha},None)), + ("out",make_frame("step-002",start,"orientation",{"identity":identity,"status":"healthy","drift":0.0,"constraints":constraints},ct)), + ("out",make_frame("step-003",start,"focus_snapshot",{"identity":identity,"drift":0.0,"focus_momentum":1.0,"rationale":"bounded evidence capture"},ct)), + ("in",make_frame("step-004",start,"route_request",{"goal":"record bounded public audit evidence","source_context":"CI","target":a.target,"repository":a.repository,"expected_sha":a.expected_sha,"run_id":str(a.run_id),"run_attempt":str(a.run_attempt),"constraints":constraints},ct)), + ("out",make_frame("step-005",start,"route_response",{"context":"CI","targetState":"capture_public_evidence","admissible":True,"decision":"EXECUTE","capabilities":[],"branches":[{"id":"bounded-public-capture","confidence":1.0,"status":"admissible","reason":"read-only public scope and exact-head identity verified"}]},ct)), + ("out",make_frame("step-006",start,"observation",{"capture_status":a.capture_status,"evidence_file_count":len(files),"evidence_inventory_sha256":invroot,"evidence_files":files},ct)), + ("in",make_frame("step-007",start,"route_request",{"goal":"preserve immutable audit evidence","source_context":"CI","constraints":constraints},ct)), + ("out",make_frame("step-008",start,"route_response",{"context":"CI","targetState":"write_audit_artifact","admissible":True,"decision":"EXECUTE","capabilities":[],"branches":[{"id":"immutable-artifact","confidence":1.0,"status":"admissible","reason":"output remains inside declared CI artifact boundary"}]},ct)), + ("out",make_frame("step-009",start,"orientation",{"identity":identity,"status":"healthy","drift":0.0,"focus_momentum":1.0,"constraints":constraints},ct))] + entries=build_entries(frames,session);d=root/"ltp";d.mkdir(parents=True,exist_ok=True);trace=d/"trace.jsonl";trace.write_text("\n".join(json.dumps(e,separators=(",",":"),ensure_ascii=False) for e in entries)+"\n") + result=verify_entries(entries,critical);result.update({"trace_sha256":fsha(trace),"ltp_inspector_sha":a.ltp_sha,"critical_actions_registry_sha256":rsha,"evidence_inventory_sha256":invroot});writej(d/"local-verification.json",result);return 0 +def verify(a:argparse.Namespace)->int: + critical,rsha=load_registry(Path(a.critical_actions_registry).resolve());p=Path(a.trace).resolve();r=verify_entries(parse_jsonl(p),critical);r.update({"trace_sha256":fsha(p),"critical_actions_registry_sha256":rsha});writej(Path(a.output).resolve(),r) if a.output else print(json.dumps(r,sort_keys=True));return 0 +def parser()->argparse.ArgumentParser: + p=argparse.ArgumentParser();s=p.add_subparsers(dest="cmd",required=True);b=s.add_parser("build") + for n in ("output_dir","audit_name","target","repository","expected_sha","initial_sha","workflow_sha","run_id","run_attempt","started_at","capture_status","artifact_name","ltp_sha","critical_actions_registry"):b.add_argument("--"+n.replace("_","-"),required=True) + b.set_defaults(fn=build);v=s.add_parser("verify");v.add_argument("--trace",required=True);v.add_argument("--critical-actions-registry",required=True);v.add_argument("--output");v.set_defaults(fn=verify);return p +def main()->int: + try: + a=parser().parse_args();return int(a.fn(a)) + except TraceContractError as e:print(f"TRACE CONTRACT ERROR: {e}");return 2 +if __name__=="__main__":raise SystemExit(main()) From bab457b564be5f11affe9de0146011d1a723a121 Mon Sep 17 00:00:00 2001 From: Aleksey Safonov <55020240+safal207@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:35:13 +0300 Subject: [PATCH 14/14] fix: capture clean inspector JSON --- scripts/run_ltp_offline_replay.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/run_ltp_offline_replay.sh b/scripts/run_ltp_offline_replay.sh index 3aa1e8d8..d690f5ee 100755 --- a/scripts/run_ltp_offline_replay.sh +++ b/scripts/run_ltp_offline_replay.sh @@ -37,10 +37,10 @@ test -s "${registry_path}" registry_sha="$(sha256sum "${registry_path}" | awk '{print $1}')" cat > "${report_dir}/commands.txt" < "${report_dir}/ltp-inspector-sha.txt" @@ -64,7 +64,7 @@ set +e ( cd "${ltp_dir}" LTP_INSPECT_FREEZE_CLOCK=1 LTP_BUILD="${ltp_sha}" \ - pnpm -w ltp:inspect trace --strict --quiet --format json --color never \ + pnpm exec ts-node tools/ltp-inspect/inspect.ts trace --strict --quiet --format json --color never \ --profile agents --replay-check --input "${trace_path}" ) > "${report_dir}/inspector-report.json" 2> "${report_dir}/inspector.stderr.txt" inspect_code=$? @@ -100,7 +100,7 @@ for attempt in 1 2; do set +e ( cd "${ltp_dir}" - LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect replay --color never --input "${trace_path}" + LTP_BUILD="${ltp_sha}" pnpm exec ts-node tools/ltp-inspect/inspect.ts replay --color never --input "${trace_path}" ) > "${report_dir}/replay-${attempt}.stdout.txt" 2> "${report_dir}/replay-${attempt}.stderr.txt" replay_code=$? set -e @@ -120,7 +120,7 @@ test "${replay_one_sha}" = "${replay_two_sha}" set +e ( cd "${ltp_dir}" - LTP_BUILD="${ltp_sha}" pnpm -w ltp:inspect explain --color never --input "${trace_path}" --at step-008 + LTP_BUILD="${ltp_sha}" pnpm exec ts-node tools/ltp-inspect/inspect.ts explain --color never --input "${trace_path}" --at step-008 ) > "${report_dir}/explain-step-008.stdout.txt" 2> "${report_dir}/explain-step-008.stderr.txt" explain_code=$? set -e