From 19aa4e09d7774e14d919e9e558979b512b16a369 Mon Sep 17 00:00:00 2001
From: lozenge0 <7510861+lozenge0@users.noreply.github.com>
Date: Tue, 22 Sep 2026 18:03:24 +0100
Subject: [PATCH] Harden publication checks and sanitize public reports
---
.githooks/commit-msg | 2 +
.githooks/pre-commit | 2 +
.githooks/pre-push | 2 +
.github/workflows/validate.yml | 12 +-
.gitignore | 5 +
CHANGELOG.md | 6 +
CONTRIBUTING.md | 44 +++-
docs/MAINTAINER.md | 60 +++++-
docs/RELEASE-REVIEW.md | 12 +-
docs/VALIDATION.md | 35 ++-
docs/reports/CPU-RETEST-20260912.md | 18 +-
docs/reports/FIRST-RUN-FINDINGS.md | 13 +-
docs/reports/RECREATION-TEST.md | 20 +-
docs/reports/SHORT-TEXT-INVESTIGATION.md | 7 +-
docs/reports/UI-TEST-PREFLIGHT.md | 30 ++-
docs/reports/UPDATE-ROLLBACK-TEST.md | 25 +--
docs/reports/USER-IDENTITY-TESTS.md | 24 +--
scripts/check_publication.py | 258 +++++++++++++++++++++++
tests/test_ci.py | 6 +
tests/test_publication.py | 34 +--
tests/test_publication_guard.py | 237 +++++++++++++++++++++
21 files changed, 708 insertions(+), 144 deletions(-)
create mode 100755 .githooks/commit-msg
create mode 100755 .githooks/pre-commit
create mode 100755 .githooks/pre-push
create mode 100644 scripts/check_publication.py
create mode 100644 tests/test_publication_guard.py
diff --git a/.githooks/commit-msg b/.githooks/commit-msg
new file mode 100755
index 0000000..b15ed09
--- /dev/null
+++ b/.githooks/commit-msg
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec python3 scripts/check_publication.py --message "$1"
diff --git a/.githooks/pre-commit b/.githooks/pre-commit
new file mode 100755
index 0000000..c30ab25
--- /dev/null
+++ b/.githooks/pre-commit
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec python3 scripts/check_publication.py --staged
diff --git a/.githooks/pre-push b/.githooks/pre-push
new file mode 100755
index 0000000..ed1dcdd
--- /dev/null
+++ b/.githooks/pre-push
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec python3 scripts/check_publication.py --pre-push --remote "$2"
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 3e5779e..fc27ee0 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -23,9 +23,19 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
+ fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.12'
- name: Run offline checks
- run: python -m unittest discover -s tests -v
+ env:
+ PUBLICATION_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
+ PUBLICATION_TIP: ${{ github.event.pull_request.head.sha || github.sha }}
+ run: |
+ python -m unittest discover -s tests -v
+ if [ -n "$PUBLICATION_BASE" ]; then
+ python scripts/check_publication.py --since "$PUBLICATION_BASE" --tip "$PUBLICATION_TIP"
+ else
+ python scripts/check_publication.py --commit HEAD
+ fi
diff --git a/.gitignore b/.gitignore
index 84f3020..78072f5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,8 @@ __pycache__/
*.pyc
.pytest_cache/
.validation-output/
+.serena/
+.env
+.env.*
+*.pem
+*.key
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5e77490..31fb10b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,10 @@ Targeting `v0.1.0`, the first beta release.
### Added
+- Local publication hooks and broader content/commit-identity checks, including
+ outgoing history, tests and embedded SVG provenance.
+- History checks reject shallow clones. Push checks use the destination's
+ advertised history; manual CI checks the selected commit and complete tree.
- Community Apps template for audio.cpp with CPU, NVIDIA CUDA 12 and NVIDIA
CUDA 13 variants, using unmodified upstream Docker images.
- Model storage, host port and NVIDIA GPU selection fields.
@@ -25,6 +29,8 @@ Targeting `v0.1.0`, the first beta release.
### Changed
+- Public test reports omit unnecessary server operational details. Original
+ technical evidence and test conclusions are preserved.
- New installations default to host port `6969`. Existing installations keep
their configured port.
- Every variant runs as Docker user `99:100`, which matches the ownership of
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1e13907..50bc564 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,12 +17,54 @@ Before proposing a change:
1. Explain its purpose and keep changes scoped to the integration.
2. Run `python3 -m unittest discover -s tests -v` from the repository root.
3. If adding a file, review its publication safety and add it to the explicit
- inventory in `tests/test_publication.py`.
+ inventory in `scripts/check_publication.py`.
4. If behavior changes, update setup guidance and the changelog. If the change
affects installed users, add an entry to the template's `Changes` field.
Record runtime tests in a dated report under `docs/reports/`. CI has no
Unraid server or GPU.
+Before committing, configure this checkout to use your **public GitHub handle**
+as `user.name` and the matching GitHub noreply address as `user.email`. Copy the
+address from your GitHub email settings; do not use a personal mailbox. Both
+the name and email are published in commits, including merges and tags.
+Use repository-local settings, leaving other projects unchanged:
+
+```sh
+git config --local user.name PUBLIC_HANDLE
+git config --local user.email YOUR_GITHUB_NOREPLY_ADDRESS
+git config --local user.useConfigOnly true
+git config --local core.hooksPath .githooks
+```
+
+Replace the uppercase placeholders with your own public identity. If you already
+use custom Git hooks, integrate these checks with them before changing hooksPath.
+Hooks are not activated automatically by cloning the repository.
+
+The hooks check the staged snapshot, commit message and every outgoing commit
+before a push. Tests receive the same content checks as other files. A file
+removed in a later commit can still block publication because its earlier
+contents would be uploaded. The push hook queries the actual destination's
+advertised branch tips and excludes history already reachable there, including
+when creating a branch. It does not trust local remote-tracking refs. An empty
+destination requires checking from the root; annotated tags also have their
+messages and tagger identities checked. Shallow clones must fetch complete
+history before these checks can pass. If the destination cannot be queried,
+the push is blocked.
+Diagnostics omit matched values and unapproved filenames. Do not bypass a failed
+check or paste raw Git output into a public issue.
+
+GitHub's browser can create commits/merges with a different author name from
+your local configuration. Until that identity has been verified to use your
+public handle, create commits and merges locally with these hooks enabled.
+CI provides a second check after upload; it cannot prevent an initial leak.
+
+Public reports should retain test versions, relevant hardware, image/model
+digests, methods, results and limitations. Omit personal service inventories,
+storage availability, private artifact locations, operational schedules and
+unnecessary server state. Privacy amendments to dated reports must be labelled
+without changing their original test conclusions. Do not upload raw audio,
+screenshots or logs as evidence by default.
+
Pull requests run read-only CI on GitHub-hosted runners. Passing CI does not prove
hardware compatibility or CA acceptance. Action updates are proposed by Dependabot
and need maintainer review; they are not automatically merged. Do not replace
diff --git a/docs/MAINTAINER.md b/docs/MAINTAINER.md
index 5de1c46..e0013d5 100644
--- a/docs/MAINTAINER.md
+++ b/docs/MAINTAINER.md
@@ -56,8 +56,9 @@ repository. Nothing here changes an existing personal installation.
- `docs/RELEASE-REVIEW.md`: current status, evidence and remaining gates.
- `docs/VALIDATION.md`: acceptance checklist and evidence requirements.
- `docs/reports/`: dated experiment reports. They record what was known at
- the time and are not updated later.
+ the time. Label privacy amendments; preserve original test conclusions.
- `tests/`: maintainer-only structural checks, never shipped into the container.
+- `scripts/check_publication.py` and `.githooks/`: publication checks.
- `.github/`: read-only CI checks and Dependabot updates for the CI actions only.
The project is called **audio.cpp for Unraid**. The template and container name
@@ -74,12 +75,49 @@ python3 -m unittest discover -s tests -v
These checks validate local structural invariants, not the CA parser, live
registry availability, hardware, browser workflows or installation success.
-The publication test holds the exact list of files that belong in the
-repository. If you add a file, review its publication safety first, then add
-it to that list.
+The publication test uses the exact inventory in `scripts/check_publication.py`.
+If you add a file, review its publication safety first, then add it to that list.
+
+Follow the identity and hook setup in [Contributing](../CONTRIBUTING.md) for each
+checkout. The pre-commit hook inspects the index, not unstaged working files.
+The pre-push hook inspects the commits actually being sent, including intermediate
+trees and commit/tag identities. It fails closed when history cannot be read.
+History checks reject shallow clones. Fetch the full history before retrying.
+The push hook queries the actual push destination and excludes history reachable
+from its advertised branch tips that can be resolved locally. Missing local
+objects exclude nothing; stale remote-tracking refs are never used as evidence
+of publication. A destination query failure blocks the push. Other checks run
+offline.
+Local tool state is ignored, and ignored files are still rejected if force-added.
+
+For a read-only audit of all history reachable from HEAD, run:
-GitHub Actions runs the same tests on pushes, pull requests and manual
-dispatch. The workflow uses GitHub-hosted Ubuntu, a read-only repository token,
+```sh
+python3 scripts/check_publication.py --since ''
+```
+
+Use `--since BASE_COMMIT` to inspect only subsequent commits, optionally ending
+at `--tip COMMIT` instead of HEAD. Pull-request CI audits the actual proposed
+commits from base to head, excluding GitHub's synthetic test-merge identity;
+structural tests still run on the merged tree. Push CI uses the pre-push branch
+tip as its base. Manual dispatch checks the selected commit
+and its full tree with `--commit HEAD`; it is not a full-history audit.
+Historical privacy findings can therefore fail a full audit even after current
+files are sanitized. Do not suppress them to claim the history has been cleaned.
+Existing published history has identity, message-email and historical-inventory
+findings under the new policy. Excluding already-published ancestors from a push
+check does not remediate those findings or certify that history as clean.
+GitHub browser merges also need a public author name; local configuration cannot
+control that separate identity.
+
+Checks cover selected credential formats, private addresses, home paths and
+email addresses across all approved files, including tests. SVG C2PA provenance
+is decoded and scanned; PNG metadata is restricted. This is not exhaustive
+secret detection or proof of anonymity. Review prose and other encoded content
+manually, and retain GitHub secret scanning and push protection.
+
+GitHub Actions runs the tests and commit publication checks on pushes, pull
+requests and manual dispatch. The workflow uses GitHub-hosted Ubuntu, a read-only repository token,
commit-pinned official actions and no persisted checkout credentials. There are
no application builds, deployments, artifact uploads, scheduled server tasks,
custom secrets or access to Unraid. Do not add an Unraid SSH key or API token to
@@ -118,9 +156,17 @@ See [contribution guidance](../CONTRIBUTING.md) and
Never publish the surrounding audio.cpp checkout. If the repository is ever
restaged, start from a new directory and make sure that
`git rev-parse --show-toplevel` points at it. Copy only the files in the
-publication list. Use the GitHub noreply commit identity and a fresh root
+publication list. Use a public handle and matching GitHub noreply commit identity, with a fresh root
commit. Never force-push.
+Removing data from current files or changing the local identity does not erase
+published commits. History remediation requires a separately reviewed rewrite
+in an isolated copy, verification of all affected refs, and explicit approval
+before any protected-branch exception or remote replacement. Rewritten commits
+lose their old signatures and IDs. Forks, existing clones and cached pull-request
+views can retain earlier objects; follow
+[GitHub's sensitive-data removal guidance](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/removing-sensitive-data-from-a-repository).
+
## Ongoing maintenance
Upstream images update independently. Monitor changes to launch arguments,
diff --git a/docs/RELEASE-REVIEW.md b/docs/RELEASE-REVIEW.md
index e3b7cb1..73fc876 100644
--- a/docs/RELEASE-REVIEW.md
+++ b/docs/RELEASE-REVIEW.md
@@ -2,7 +2,7 @@
## Status
-Updated 2026-09-13 (Europe/London). Target: **audio.cpp for Unraid v0.1.0**.
+Updated 2026-09-13. Target: **audio.cpp for Unraid v0.1.0**.
This section is the single source for project status. Other documents link
here instead of repeating it.
@@ -107,8 +107,8 @@ then Validate/Scan.
## Repository contents
-The publication test in `tests/test_publication.py` holds the exact list of
-files that belong in the repository and compares it with Git's file inventory.
+The publication test uses the exact list in `scripts/check_publication.py`
+and compares it with Git's file inventory.
Add a new file to that list only after a publication-safety review.
Never include the parent checkout/history, `speak.sh`, `.devops/unraid`, SSH/API
@@ -128,7 +128,7 @@ pull request. No image build, custom CI credentials or server access is required
## GitHub publication record
-Published September 13 (Europe/London).
+Published September 13.
- Public repository: [lozenge0/audio-cpp-unraid](https://github.com/lozenge0/audio-cpp-unraid),
default branch `main`, Issues enabled.
@@ -146,6 +146,6 @@ Published September 13 (Europe/London).
README/template/profile/icon hashes matched local files. This does not validate
the CA UI or public branch picker.
-The first commit and CI timestamps are September 12 in UTC (after midnight
-September 13 locally). Dated runtime evidence has not been rerun for publication.
+The first commit and CI timestamps are September 12 in UTC.
+Dated runtime evidence has not been rerun for publication.
No Unraid operations were performed.
diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md
index 3a68a60..6b0a364 100644
--- a/docs/VALIDATION.md
+++ b/docs/VALIDATION.md
@@ -1,5 +1,8 @@
# Validation and release acceptance
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
This integration is under development. Source inspection and XML checks do not
establish that its installation flows work. Record the date, Unraid and
Community Applications versions, image digest, hardware, model package and
@@ -40,17 +43,12 @@ The specific short-text crash gate is cleared for this tested image/model/host;
full release acceptance is not implied. Current Studio offers Alba for Pocket TTS;
the former male demo voice passed API/streaming tests separately.
-The separate CPU retest is now running on port 18081. The earlier CUDA 13 test
-was stopped and retained to free that port; the personal container remains untouched.
-
### Controlled CUDA 13 update/rollback — 2026-09-10
The independently reviewed isolated image update and retained-container rollback
passed short/long API speech, Studio generation, CUDA computation, configuration
-and model-data checks. See [full results](reports/UPDATE-ROLLBACK-TEST.md). The original
-pinned CUDA 13 test was restored and running at that test's completion; it was
-later stopped for the September 12 CPU retest. This is not CA scheduled-updater
-or full DockerMan-update acceptance.
+and model-data checks. See [full results](reports/UPDATE-ROLLBACK-TEST.md).
+This is not CA scheduled-updater or full DockerMan-update acceptance.
### CUDA 12 owner-operated UI test — 2026-09-09
@@ -88,11 +86,9 @@ healthy CUDA backend, loaded Pocket TTS, unchanged GGUF checksum and user 99:100
This covers owner-operated restart and model reuse, not automatic restoration
of dynamic model registrations or image replacement.
-After independent CUDA 13 preflight review, only the verified CUDA 12 test
-container was stopped to release port 18081. It exited cleanly (0); its container
-and downloaded files were retained. The personal service remained stopped and
-unchanged. The staged CUDA 13 template matched its reviewed checksum, its model
-path was absent, and its retained pinned image matched revision `b075757`.
+After independent CUDA 13 preflight review, an unused test port and fresh model
+path were confirmed. The staged template matched its reviewed checksum and the
+pinned image matched revision `b075757`.
CUDA 13 UI installation then proceeded as recorded below.
### CUDA 13 owner-operated UI test — 2026-09-09
@@ -101,7 +97,7 @@ After screenshot review, the owner installed the private CUDA 13 test through
Unraid's form. Inspection confirmed pinned upstream digest
`sha256:f89dfcdccd5d54755fde3e670abd2a29d1ddc83df7aedc27a4bef83c9bffd45d`,
revision `b075757`, user 99:100, NVIDIA runtime, the selected RTX 3060 UUID and
-`compute,utility` capabilities. Port 18081 and the fresh dedicated CUDA 13 model
+`compute,utility` capabilities. The isolated port and fresh dedicated CUDA 13 model
mount matched the reviewed test settings; privileged mode was disabled.
The owner reports successful native Pocket TTS GGUF Q8 download and playback
@@ -202,16 +198,9 @@ the protocol below is not a claim that every clause has been completed.
## Existing evidence and its limits
-The earlier personal deployment on Unraid 7.3.2 used an RTX 3060 with 12 GiB of
-VRAM, an explicitly pinned CUDA 12 image and a prepared server configuration.
-That deployment produced speech using CUDA and was checked for restart
-persistence and idle unloading. Its appdata ownership was prepared explicitly.
-
-Those results support the feasibility of running audio.cpp on this server.
-They do not validate this integration's branch selector, empty-appdata setup,
-CPU image, CUDA 13 image, or future image updates. All variants of the new
-template need the checks below. Do not mark a new check complete solely because
-the personal deployment passed a similar check.
+Results from preconfigured deployments do not validate this integration's branch
+selector, empty-appdata setup or future image updates. All variants need the
+checks below; a similar result elsewhere does not complete an acceptance check.
## 1. Template and branch checks
diff --git a/docs/reports/CPU-RETEST-20260912.md b/docs/reports/CPU-RETEST-20260912.md
index 9abb73c..4a8af31 100644
--- a/docs/reports/CPU-RETEST-20260912.md
+++ b/docs/reports/CPU-RETEST-20260912.md
@@ -1,11 +1,14 @@
# Official CPU image regression retest — 2026-09-12
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
## Scope and image
Retest Pocket TTS English GGUF Q8 short-text speech on Unraid 7.3.2 with a
Ryzen 7 3700X. The previous official CPU image, revision `b075757`, exited 139
with `GGML_ASSERT(tensor->buffer == NULL)` after the owner's `hello` request.
-The original failed container and logs were retained; it was not rerun today.
+The previous failing image was not rerun in this retest.
The official `full-cpu` tag resolved on September 12 to:
@@ -25,10 +28,9 @@ claim that all possible short-text failures have been fixed.
## Isolation
A separately named CPU retest container uses a copy of the old test's model
-directory, with hashes and ownership checked before deployment. Existing CPU,
-CUDA 12 and personal containers/data are preserved. Only the isolated CUDA 13
-test was stopped to release the shared test port. No existing container was
-deleted or renamed, and no saved Unraid template or updater setting was edited.
+directory, with hashes and ownership checked before deployment. An unused test
+port and dedicated storage kept the retest isolated from unrelated services.
+No saved Unraid template or updater setting was edited.
The deployment harness and actual image/configuration snapshot were independently
reviewed before creation/start.
@@ -71,10 +73,8 @@ this is not automatic registration persistence.
Final checks confirmed the original and copied model files, UID/GID and modes
were unchanged. All pre-existing audio.cpp container configurations and saved
-Unraid XML files matched the before snapshot. The new pinned CPU retest remains
-running on host port 18081; CUDA 13 is retained stopped, and the personal service
-and old failed CPU container remain untouched. No files, containers or images
-were deleted.
+Unraid XML files matched the before snapshot. Unrelated services and data
+were unchanged.
Independent final review verified all nine successful WAVs and six streaming
PCM outputs were non-silent, the before/after model manifests matched, and logs
diff --git a/docs/reports/FIRST-RUN-FINDINGS.md b/docs/reports/FIRST-RUN-FINDINGS.md
index 3a2308c..3b67c60 100644
--- a/docs/reports/FIRST-RUN-FINDINGS.md
+++ b/docs/reports/FIRST-RUN-FINDINGS.md
@@ -1,5 +1,8 @@
# First-run validation: model-directory permissions
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
Follow-up: the isolated native Docker `--user=99:100` experiment subsequently
passed model download/inference checks for CPU, CUDA 12 and CUDA 13, with lifecycle
checks. See [user-identity test results](USER-IDENTITY-TESTS.md). The original
@@ -40,9 +43,8 @@ inside that mount. Independent source review confirmed this matches the observed
failed write check and browser error.
The actual container command matched the reviewed CPU rendering except for using
-the pulled immutable image digest during this diagnostic session. The host has
-global container auto-updates enabled, so a fixed digest prevented a rolling image
-change during testing. The public template still follows the moving upstream tag.
+the pulled immutable image digest during this diagnostic session. A fixed digest
+prevented image drift during testing. The public template follows the moving tag.
This test did not exercise the real CA branch-selector UI or its update workflow.
- Upstream tag pulled: `ghcr.io/0xshug0/audio.cpp:full-cpu`
@@ -77,7 +79,4 @@ Do not silently run the application as root, add a wrapper, recursively loosen
appdata permissions, or change an existing shared directory.
The temporary test container was stopped and removed after evidence collection.
-Its small harness/fresh appdata and pulled CPU image were retained; no model
-weights were downloaded. The production service remained running and healthy.
-No driver, host Docker settings, global updater settings or production appdata
-were changed.
+No model weights were downloaded. Unrelated services and data were unchanged.
diff --git a/docs/reports/RECREATION-TEST.md b/docs/reports/RECREATION-TEST.md
index 915360c..7548718 100644
--- a/docs/reports/RECREATION-TEST.md
+++ b/docs/reports/RECREATION-TEST.md
@@ -1,5 +1,8 @@
# CUDA 13 same-image recreation — 2026-09-09
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
## Outcome
The isolated CUDA 13 container was stopped, removed without deleting volumes,
@@ -10,7 +13,7 @@ rollback, scheduled update or the CA public branch selector.
The owner approved this exact test. Independent subagent review preceded the
destructive step and reviewed the one Docker default normalization described
-below. The personal service and other test containers remained unchanged.
+below. Unrelated services and data were unchanged.
## Image, settings and preservation
@@ -24,9 +27,8 @@ below. The personal service and other test containers remained unchanged.
retained, not added to public template defaults.
- Before/after manifests matched for every model file's SHA256, file/directory
sizes, owners and modes, including package metadata and the Alba embedding.
-- The saved Unraid user template remained byte-identical. Config, HostConfig
- and State of the personal service and two other audio.cpp tests were checked
- against their snapshots; none were changed by this operation.
+- The saved Unraid user template remained byte-identical. Isolation checks
+ confirmed that unrelated services were unchanged.
- No image pull, model download, ownership change or global updater change was
made. Original container logs and configuration were preserved locally before
removal; only the replaced container's disposable writable layer was removed.
@@ -71,15 +73,7 @@ successful results do not imply all similar warnings are harmless.
## Evidence and remaining work
-Private snapshots, request harness, logs, checksums and replacement payload are
-under `build/unraid-ca-validation/recreate-cuda13-20260909/` in the surrounding
-development checkout. WAVs and the Studio screenshot use the
-`cuda13-recreated-*-20260909` prefix in its parent directory. These are testing
-artifacts, not code or personal configuration shipped by the community app.
-
-The recreated CUDA 13 test remains running on the isolated test port. The old
-container object no longer exists; saved settings, unchanged official image
-and retained model storage allow another recreation if necessary.
+Raw test artifacts are excluded from this public repository.
Next lifecycle gate: distinct official-image update and rollback, separately
reviewed and authorized. Do not reuse this same-image payload for upgrades: it
diff --git a/docs/reports/SHORT-TEXT-INVESTIGATION.md b/docs/reports/SHORT-TEXT-INVESTIGATION.md
index 888f89e..1d07db0 100644
--- a/docs/reports/SHORT-TEXT-INVESTIGATION.md
+++ b/docs/reports/SHORT-TEXT-INVESTIGATION.md
@@ -1,5 +1,8 @@
# Short-text CPU crash: investigation plan
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
Prepared 2026-09-08. Follow-up: [official CPU image retest](CPU-RETEST-20260912.md)
records the September 12 results. The historical plan below is not a claim that
every proposed comparison was executed. Execution and
@@ -26,7 +29,7 @@ an image/build difference, process identity, request mode, or integration settin
## Isolation rules
-Use one isolated test container at a time, port 18081, and dedicated test storage.
+Use one isolated test container at a time, an unused port and dedicated test storage.
Recheck current server state before starting; do not assume yesterday's state.
Never stop, start, edit or reuse the personal container's configuration/data.
Check full container identity and mappings before every lifecycle operation.
@@ -53,7 +56,7 @@ default male voice. Confirm the expected `demo_1_man` ID from the request itself
Replay the same request and required setup directly against the isolated API.
If API replay crashes identically, the browser is not necessary to trigger it.
If only Studio fails, compare request sequencing, cancellation and overlap before
-blaming Brave or changing the inference backend. Distinguish HTTP response
+blaming the browser or changing the inference backend. Distinguish HTTP response
streaming from an application-level incremental audio-generation setting.
## 2. Establish a repeatable CPU baseline
diff --git a/docs/reports/UI-TEST-PREFLIGHT.md b/docs/reports/UI-TEST-PREFLIGHT.md
index b7dbbfb..3020d3a 100644
--- a/docs/reports/UI-TEST-PREFLIGHT.md
+++ b/docs/reports/UI-TEST-PREFLIGHT.md
@@ -1,13 +1,13 @@
# Unraid UI validation preflight — 2026-09-07
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
## Result
Read-only checks passed on Unraid 7.3.2 with Community Applications 2026.07.21.
-The existing personal service remained running and healthy with its container
-identity unchanged. No isolated test container existed, and the selected test
-port was free. Docker image storage had approximately 67 GiB available; cache
-storage approximately 816 GiB. The retained official CPU/CUDA images and test
-models were available. No deployment or server configuration writes were made.
+Test isolation and sufficient storage were checked before proceeding.
+No deployment or server configuration writes were made.
Two prerequisites affect the next UI tests:
@@ -48,8 +48,8 @@ cache to represent a supported installation test.
digests for controlled runtime tests. Record that difference in the evidence.
- Run one test container at a time. No production mappings, global updater
changes, driver changes or test autostart entries.
-- The host has global container updates enabled; do not invoke a global update
- job to test one container. Pin controlled experiments and review the specific
+- Do not invoke a global update job to test one container. Pin controlled
+ experiments and review the specific
single-container update/recreation path before use.
- Test distinct upstream image versions and rollback against isolated data,
preserving the Docker user/group and selected GPU settings. Same-image
@@ -65,18 +65,14 @@ test was completed during this preflight.
## Private entries staged — 2026-09-07
-After the owner logged into Apps in their own Brave browser, an independently
-reviewed generator produced three pre-expanded test copies outside this public
-repository candidate. Copies were staged only in the new private repository
-directory `/boot/config/plugins/community.applications/private/audio-cpp-ca-ui-test/`.
+After authenticated browser access, an independently reviewed generator produced
+three pre-expanded test copies outside this public repository candidate.
+Copies were staged in a dedicated CA private-app directory.
Local and server SHA256 checksums matched for all three files.
-The test names are `audio-cpp-test-cpu`, `audio-cpp-test-cuda12` and
-`audio-cpp-test-cuda13`. They share test port 18081 (only one may run at a time),
-with fresh per-variant model paths under the separate UI-test storage root.
-No model directories or containers were created by staging. The existing
-personal container remained healthy and unchanged. No global updater settings
-or existing templates were modified.
+Each variant used an isolated name, unused port and fresh model path.
+No model directories or containers were created by staging.
+No existing templates or unrelated services were modified.
Next owner action: return to Apps Home, open Private Apps and choose the CPU
test's Install action, stopping before Apply. Confirm the actual form's name,
diff --git a/docs/reports/UPDATE-ROLLBACK-TEST.md b/docs/reports/UPDATE-ROLLBACK-TEST.md
index b30f5f7..74ad7cd 100644
--- a/docs/reports/UPDATE-ROLLBACK-TEST.md
+++ b/docs/reports/UPDATE-ROLLBACK-TEST.md
@@ -1,5 +1,8 @@
# Controlled CUDA 13 image update and rollback
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
Prepared 2026-09-09; executed and verified 2026-09-10.
## Outcome and scope
@@ -47,16 +50,14 @@ settings were carried over. Old CUDA-library environment variables and old OCI
image labels were not copied into the new image.
The test preserved UID/GID 99:100, NVIDIA runtime, the selected RTX 3060,
-`compute,utility`, bridge networking, host port 18081 and the sole dedicated
+`compute,utility`, bridge networking, an isolated host port and the sole dedicated
model mount. Privileged mode and automatic restart remained disabled. A strict
pre-start check paused on Docker's `OomKillDisable` null-to-false normalization;
only that known default-equivalent difference was independently reviewed and
accepted. All other compared HostConfig settings had to match.
-The personal service, CPU test and CUDA 12 test retained their original IDs,
-configuration and stopped state. Their snapshots and the unchanged Unraid user
-template were checked throughout. Global updater settings were not changed and
-no global update job was invoked.
+Isolation checks confirmed that unrelated services and the saved Unraid user
+template were unchanged. No global update job was invoked.
## Speech and persistence results
@@ -94,16 +95,10 @@ Because model data remained unchanged, backup restoration was unnecessary;
the test would have paused before rollback if data differed.
The original container was renamed back and started. Its original ID, image,
-Config and HostConfig were verified. The final isolated test is running on the
-**original** pinned CUDA 13 image, not the candidate. No rollback-named container
-remains. The approximately 128 MiB model backup and both official images are
-retained; neither was deleted or pruned.
-
-Private evidence remains outside this public repository candidate under
-`build/unraid-ca-validation/update-cuda13-20260909/`. Generated WAVs/screenshots
-are in its parent directory with `cuda13-upgraded-` or `cuda13-rollback-` prefixes
-and the date `20260910`. The harness and personal test settings are not shipped
-as community-app runtime code.
+Config and HostConfig were verified. Rollback restored the **original** pinned
+CUDA 13 image and its settings.
+
+Raw test artifacts are excluded from this public repository.
Remaining gates include the fixed CPU image retest, optional persistent JSON,
public CA branch-selector validation, and the actual Unraid updater workflow.
diff --git a/docs/reports/USER-IDENTITY-TESTS.md b/docs/reports/USER-IDENTITY-TESTS.md
index a912f32..171c49d 100644
--- a/docs/reports/USER-IDENTITY-TESTS.md
+++ b/docs/reports/USER-IDENTITY-TESTS.md
@@ -1,5 +1,8 @@
# Native Docker user/group override: validation results
+> Privacy edit, 2026-09-20: unnecessary operational details have been omitted;
+> original test results and limitations are preserved.
+
## Outcome — 2026-09-06
The isolated `--user=99:100` experiment succeeded for **CPU, CUDA 12 and CUDA 13**
@@ -9,8 +12,7 @@ running as root, adding a startup wrapper, or changing directory ownership/modes
The local public-template draft adopted the override on 2026-09-07, with matching
structural checks and documentation. That local adoption is not a new runtime
-test or a claim of complete Community Apps acceptance. The existing personal
-deployment was not modified.
+test or a claim of complete Community Apps acceptance.
## Controlled change
@@ -25,8 +27,8 @@ No `chown`, `chmod`, extra supplementary group, privileged mode or root executio
was added. Temporary-directory write checks passed for CPU and CUDA 13; actual
native downloads/inference exercised the temporary path for all three variants.
-The test used immutable upstream image digests to prevent the host's global
-auto-updater from changing the experiment. The public draft retains moving tags.
+The test used immutable upstream image digests to prevent image drift.
+The public draft retains moving tags.
No global update/autostart settings were changed; the temporary container was
removed after testing and was never added to Unraid autostart.
@@ -118,13 +120,9 @@ registration across restarts still requires the documented upstream JSON setup.
## Cleanup and retained evidence
-Only the isolated test container was stopped/removed. The three sets of downloaded
-stock test-model files, small harness and official images were retained for
-reproducibility; test appdata totals approximately 384 MiB and can be reused for
-the remaining tests. All three downloaded model GGUF files have SHA256
-`0315406421d515d9ffbde49ed998832ff2962562ef8abde440c85fa0a27d8b2a`. No production
-container, appdata, driver or host Docker configuration was changed.
+Only the isolated test container was stopped/removed. All three downloaded model
+GGUF files have SHA256
+`0315406421d515d9ffbde49ed998832ff2962562ef8abde440c85fa0a27d8b2a`.
+Unrelated services and data were unchanged.
-Screenshots, browser automation and validated WAV samples are local test artifacts
-under `build/unraid-ca-validation/` in the development workspace, outside this
-standalone public repository candidate.
+Raw test artifacts are excluded from this public repository.
diff --git a/scripts/check_publication.py b/scripts/check_publication.py
new file mode 100644
index 0000000..8a5577d
--- /dev/null
+++ b/scripts/check_publication.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+"""Publication checks. Diagnostics never echo matched content.
+
+These checks detect selected data formats, not arbitrary personal information.
+Human review and GitHub secret protection remain necessary.
+"""
+import argparse
+import base64
+import binascii
+from pathlib import Path
+import re
+import struct
+import subprocess
+import sys
+import xml.etree.ElementTree as ET
+import zlib
+
+
+ROOT = Path(__file__).resolve().parents[1]
+PUBLIC_FILES = {
+ '.github/dependabot.yml', '.github/workflows/validate.yml',
+ '.gitignore', 'CHANGELOG.md', 'LICENSE', 'README.md',
+ 'CONTRIBUTING.md', 'SECURITY.md',
+ 'assets/README.md', 'assets/icon.svg', 'assets/icon.png', 'ca_profile.xml',
+ 'docs/CONFIGURATION.md', 'docs/MAINTAINER.md', 'docs/RELEASE-REVIEW.md',
+ 'docs/VALIDATION.md',
+ 'docs/reports/CPU-RETEST-20260912.md', 'docs/reports/FIRST-RUN-FINDINGS.md',
+ 'docs/reports/RECREATION-TEST.md', 'docs/reports/SHORT-TEXT-INVESTIGATION.md',
+ 'docs/reports/UI-TEST-PREFLIGHT.md', 'docs/reports/UPDATE-ROLLBACK-TEST.md',
+ 'docs/reports/USER-IDENTITY-TESTS.md', 'templates/audio-cpp.xml',
+ 'tests/test_ci.py', 'tests/test_publication.py', 'tests/test_template.py',
+ 'tests/test_publication_guard.py', 'scripts/check_publication.py',
+ '.githooks/pre-commit', '.githooks/pre-push', '.githooks/commit-msg',
+}
+PATTERNS = {
+ 'home directory': rb'(?:/(?:Users|home)/[A-Za-z0-9_.-]+|[A-Za-z]:\\Users\\[A-Za-z0-9_.-]+)',
+ 'private network address': rb'\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\b',
+ 'private IPv6 address': rb'\b(?:fc|fd)[0-9a-fA-F]{2}:[0-9a-fA-F:]+',
+ 'GPU identifier': rb'GPU-[0-9a-fA-F]{8}-[0-9a-fA-F-]{20,}',
+ 'private key': rb'-----BEGIN (?:OPENSSH |RSA |EC |DSA |ENCRYPTED )?PRIVATE KEY-----',
+ 'GitHub token': rb'\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{30,})\b',
+ 'provider token': rb'\b(?:hf_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9_-]{30,}|xox[baprs]-[A-Za-z0-9-]{20,}|AKIA[0-9A-Z]{16})\b',
+ 'credential in URL': rb'https?://[^\s/:<>]+:[^\s/@<>]+@',
+}
+EMAIL = re.compile(rb'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b')
+NOREPLY = re.compile(r'(?:\d+\+)?([A-Za-z0-9-]+(?:\[bot\])?)@users\.noreply\.github\.com')
+
+
+def git(*args, cwd=ROOT):
+ return subprocess.run(['git', *args], cwd=cwd, check=True,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout
+
+
+def sensitive_markers(data):
+ findings = {label for label, pattern in PATTERNS.items() if re.search(pattern, data)}
+ for match in EMAIL.findall(data):
+ address = match.decode('ascii')
+ domain = address.rsplit('@', 1)[1].lower()
+ if not (NOREPLY.fullmatch(address) or address == 'noreply@github.com'
+ or domain in ('example.com', 'example.org', 'example.net')
+ or domain.endswith(('.example', '.invalid', '.test'))):
+ findings.add('non-public email address')
+ return findings
+
+
+def scan_content(name, data):
+ findings = sensitive_markers(data)
+ if name.endswith('.svg'):
+ try:
+ root = ET.fromstring(data)
+ for node in root.iter('{http://c2pa.org/manifest}manifest'):
+ decoded = base64.b64decode(''.join((node.text or '').split()), validate=True)
+ if not decoded:
+ findings.add('empty SVG provenance')
+ findings.update('SVG provenance: ' + label for label in sensitive_markers(decoded))
+ except (ET.ParseError, ValueError, binascii.Error):
+ findings.add('unreadable SVG provenance')
+ elif name.endswith('.png'):
+ try:
+ if data[:8] != b'\x89PNG\r\n\x1a\n':
+ raise ValueError()
+ offset, chunks = 8, []
+ while offset < len(data):
+ size = struct.unpack('>I', data[offset:offset + 4])[0]
+ kind = data[offset + 4:offset + 8]
+ payload = data[offset + 8:offset + 8 + size]
+ crc = struct.unpack('>I', data[offset + 8 + size:offset + 12 + size])[0]
+ if kind not in (b'IHDR', b'bKGD', b'IDAT', b'IEND') or zlib.crc32(kind + payload) != crc:
+ raise ValueError()
+ chunks.append(kind)
+ offset += size + 12
+ if offset != len(data) or not chunks or chunks[0] != b'IHDR' or chunks[-1] != b'IEND':
+ raise ValueError()
+ except (ValueError, struct.error):
+ findings.add('unexpected PNG structure or metadata')
+ else:
+ try:
+ data.decode('utf-8')
+ if b'\0' in data:
+ findings.add('unexpected binary content')
+ except UnicodeDecodeError:
+ findings.add('unexpected binary content')
+ return findings
+
+
+def public_identity(name, email, role):
+ if role == 'committer' and (name, email) == ('GitHub', 'noreply@github.com'):
+ return True
+ match = NOREPLY.fullmatch(email)
+ return bool(match and name == match.group(1))
+
+
+def check_index(cwd=ROOT):
+ errors = []
+ for entry in git('ls-files', '--stage', '-z', cwd=cwd).split(b'\0'):
+ if not entry:
+ continue
+ metadata, name = entry.split(b'\t', 1)
+ mode, oid, stage = metadata.decode().split()
+ name = name.decode('utf-8')
+ errors += check_blob(name, mode, oid, cwd)
+ if stage != '0':
+ errors.append('Unresolved index entry (path withheld).')
+ for role in ('author', 'committer'):
+ identity = git('var', 'GIT_' + role.upper() + '_IDENT', cwd=cwd).decode()
+ match = re.fullmatch(r'(.*?) <([^<>]+)> \d+ [+-]\d{4}\n?', identity)
+ if not match or not public_identity(*match.groups(), role):
+ errors.append('Configured ' + role + ' must use the public handle and matching GitHub noreply email.')
+ return errors
+
+
+def check_blob(name, mode, oid, cwd):
+ if name not in PUBLIC_FILES:
+ return ['Unapproved publication path (name withheld).']
+ if mode not in ('100644', '100755'):
+ return [name + ': symlink/submodule or unsupported file mode.']
+ data = git('cat-file', 'blob', oid, cwd=cwd)
+ return [name + ': ' + label for label in sorted(scan_content(name, data))]
+
+
+def check_commits(commits, cwd=ROOT):
+ errors, seen = [], set()
+ for commit in commits:
+ raw = git('show', '-s', '--format=%an%x00%ae%x00%cn%x00%ce%x00%B', commit, cwd=cwd)
+ author, author_email, committer, committer_email, message = raw.split(b'\0', 4)
+ for role, name, email in (('author', author, author_email), ('committer', committer, committer_email)):
+ if not public_identity(name.decode('utf-8'), email.decode('utf-8'), role):
+ errors.append(commit[:12] + ': non-public ' + role + ' identity (values withheld).')
+ errors += [commit[:12] + ': commit message: ' + label for label in sorted(sensitive_markers(message))]
+ for entry in git('ls-tree', '-rz', commit, cwd=cwd).split(b'\0'):
+ if not entry:
+ continue
+ metadata, name = entry.split(b'\t', 1)
+ mode, kind, oid = metadata.decode().split()
+ key = (name, mode, oid)
+ if key in seen:
+ continue
+ seen.add(key)
+ errors += [commit[:12] + ': ' + error for error in check_blob(name.decode('utf-8'), mode, oid, cwd)]
+ return errors
+
+
+class IncompleteHistory(ValueError):
+ pass
+
+
+def resolve_commit(revision, cwd=ROOT):
+ return git('rev-parse', '--verify', '--end-of-options', revision + '^{commit}', cwd=cwd).decode().strip()
+
+
+def commits_between(base, tip, cwd=ROOT, published=()):
+ if git('rev-parse', '--is-shallow-repository', cwd=cwd).strip() != b'false':
+ raise IncompleteHistory('History checks require a complete clone; fetch the full history first.')
+ # Resolve user/event values as revisions before handing them to rev-list.
+ tip = resolve_commit(tip, cwd)
+ args = [tip]
+ if base and set(base) != {'0'}:
+ base = resolve_commit(base, cwd)
+ args.append('^' + base)
+ args.extend('^' + resolve_commit(oid, cwd) for oid in published)
+ return git('rev-list', *args, cwd=cwd).decode().splitlines()
+
+
+def remote_commit_tips(remote, cwd=ROOT):
+ # Query the actual push destination; stale local tracking refs are not proof
+ # that a commit is already published there. Unknown tips exclude nothing.
+ tips = set()
+ for line in git('ls-remote', '--heads', '--', remote, cwd=cwd).splitlines():
+ oid, _ = line.decode().split('\t', 1)
+ try:
+ tips.add(resolve_commit(oid, cwd))
+ except subprocess.CalledProcessError:
+ continue
+ return tips
+
+
+def check_push(lines, cwd=ROOT, remote=None):
+ commits, errors = set(), []
+ published = None
+ for line in lines:
+ local_ref, local_oid, remote_ref, remote_oid = line.split()
+ if set(local_oid) == {'0'}:
+ continue # Deleting a ref publishes no new objects.
+ if published is None:
+ published = remote_commit_tips(remote, cwd) if remote else ()
+ # Scan tagger identity and message too, including nested annotated tags.
+ target = local_oid
+ while git('cat-file', '-t', target, cwd=cwd).strip() == b'tag':
+ raw = git('cat-file', 'tag', target, cwd=cwd)
+ headers, _, message = raw.partition(b'\n\n')
+ tagger = re.search(rb'^tagger (.*?) <([^<>]+)> \d+ [+-]\d{4}$', headers, re.MULTILINE)
+ if not tagger or not public_identity(*(value.decode() for value in tagger.groups()), 'tagger'):
+ errors.append('Tag has a non-public tagger identity (values withheld).')
+ errors += ['Tag: ' + label for label in sorted(sensitive_markers(raw))]
+ target = headers.splitlines()[0].removeprefix(b'object ').decode()
+ if git('cat-file', '-t', target, cwd=cwd).strip() != b'commit':
+ errors.append('Only commits and tags referring to commits may be published.')
+ continue
+ commits.update(commits_between(remote_oid, local_oid, cwd, published))
+ return errors + check_commits(sorted(commits), cwd)
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument('--staged', action='store_true')
+ group.add_argument('--pre-push', action='store_true')
+ group.add_argument('--message', type=Path)
+ group.add_argument('--since', help='Check commits after this base through --tip; empty means full history')
+ group.add_argument('--commit', help='Check one commit and its complete tree, without auditing ancestors')
+ parser.add_argument('--tip', default='HEAD', help='History tip for --since (default: HEAD)')
+ parser.add_argument('--remote', help='Actual push destination supplied by the pre-push hook')
+ args = parser.parse_args()
+ try:
+ if args.staged:
+ errors = check_index()
+ elif args.pre_push:
+ errors = check_push(sys.stdin, remote=args.remote)
+ elif args.message:
+ errors = ['Commit message: ' + label for label in sorted(sensitive_markers(args.message.read_bytes()))]
+ elif args.commit:
+ errors = check_commits([resolve_commit(args.commit)])
+ else:
+ errors = check_commits(commits_between(args.since, args.tip))
+ except IncompleteHistory as error:
+ errors = [str(error)]
+ except (subprocess.CalledProcessError, OSError, ValueError, UnicodeError):
+ # Git errors can include local paths, identities or credentials in URLs.
+ errors = ['Publication check could not complete; no sensitive diagnostics printed.']
+ for error in errors:
+ print(error, file=sys.stderr)
+ if errors:
+ print('Publication blocked. Review locally; do not post raw diagnostics or bypass the check.', file=sys.stderr)
+ return bool(errors)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/tests/test_ci.py b/tests/test_ci.py
index 83fbe17..ced27a5 100644
--- a/tests/test_ci.py
+++ b/tests/test_ci.py
@@ -24,6 +24,12 @@ def test_read_only_hosted_validation(self):
'permissions:\n contents: read\n',
'persist-credentials: false', 'runs-on: ubuntu-24.04',
'timeout-minutes: 5', 'python -m unittest discover -s tests -v',
+ 'fetch-depth: 0',
+ 'python scripts/check_publication.py --since "$PUBLICATION_BASE" --tip "$PUBLICATION_TIP"',
+ 'PUBLICATION_BASE: ${{ github.event.pull_request.base.sha || github.event.before }}',
+ 'PUBLICATION_TIP: ${{ github.event.pull_request.head.sha || github.sha }}',
+ 'if [ -n "$PUBLICATION_BASE" ]; then',
+ 'python scripts/check_publication.py --commit HEAD',
):
self.assertIn(required, self.workflow)
for forbidden in ('pull_request_target', 'self-hosted', 'secrets.',
diff --git a/tests/test_publication.py b/tests/test_publication.py
index 3b5e3e5..ed2f8c1 100644
--- a/tests/test_publication.py
+++ b/tests/test_publication.py
@@ -3,6 +3,7 @@
import re
import subprocess
import unittest
+from scripts.check_publication import PUBLIC_FILES, scan_content
ROOT = Path(__file__).resolve().parents[1]
@@ -17,20 +18,7 @@ def candidate_files():
['git', 'ls-files', '--cached', '--others', '--exclude-standard', '-z'],
cwd=ROOT, check=True, capture_output=True, text=True).stdout
return {name for name in output.split('\0') if name}
-EXPECTED = {
- '.github/dependabot.yml', '.github/workflows/validate.yml',
- '.gitignore', 'CHANGELOG.md', 'LICENSE', 'README.md',
- 'CONTRIBUTING.md', 'SECURITY.md',
- 'assets/README.md', 'assets/icon.svg', 'assets/icon.png', 'ca_profile.xml',
- 'docs/CONFIGURATION.md', 'docs/MAINTAINER.md', 'docs/RELEASE-REVIEW.md',
- 'docs/VALIDATION.md',
- 'docs/reports/CPU-RETEST-20260912.md', 'docs/reports/FIRST-RUN-FINDINGS.md',
- 'docs/reports/RECREATION-TEST.md', 'docs/reports/SHORT-TEXT-INVESTIGATION.md',
- 'docs/reports/UI-TEST-PREFLIGHT.md', 'docs/reports/UPDATE-ROLLBACK-TEST.md',
- 'docs/reports/USER-IDENTITY-TESTS.md',
- 'templates/audio-cpp.xml',
- 'tests/test_ci.py', 'tests/test_publication.py', 'tests/test_template.py',
-}
+EXPECTED = PUBLIC_FILES
class PublicationTests(unittest.TestCase):
@@ -83,23 +71,9 @@ def test_relative_markdown_links_stay_inside_candidate(self):
self.assertTrue(target.is_file())
def test_no_selected_private_data_patterns_in_public_content(self):
- # Tests themselves contain negative examples. SVG metadata is preserved;
- # this plain-text check does not decode/clear provenance metadata.
- patterns = (
- r'/Users/', r'192\.168\.\d{1,3}\.\d{1,3}',
- r'GPU-[0-9a-fA-F]{8}-',
- r'-----BEGIN (?:OPENSSH |RSA |EC )?PRIVATE KEY-----',
- r'\bgh[pousr]_[A-Za-z0-9]{30,}\b',
- r'\bgithub_pat_[A-Za-z0-9_]{30,}\b',
- )
for name in sorted(EXPECTED):
- if name.startswith('tests/') or name == 'assets/icon.png':
- # PNG structure/chunk allowlist is checked by test_template.
- continue
- text = (ROOT / name).read_text()
- for pattern in patterns:
- with self.subTest(file=name, pattern=pattern):
- self.assertIsNone(re.search(pattern, text))
+ with self.subTest(file=name):
+ self.assertEqual(scan_content(name, (ROOT / name).read_bytes()), set())
if __name__ == '__main__':
diff --git a/tests/test_publication_guard.py b/tests/test_publication_guard.py
new file mode 100644
index 0000000..7101da5
--- /dev/null
+++ b/tests/test_publication_guard.py
@@ -0,0 +1,237 @@
+"""Exercise publication boundaries with synthetic data in disposable Git repos."""
+import base64
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+from unittest.mock import patch
+
+from scripts.check_publication import (
+ ROOT, IncompleteHistory, check_commits, check_index, check_push, commits_between,
+ public_identity, scan_content, sensitive_markers,
+)
+
+
+class ContentTests(unittest.TestCase):
+ def test_sensitive_formats_and_no_test_file_exemption(self):
+ samples = [
+ b'10.' + b'20.30.40', b'172.' + b'20.30.40', b'192.' + b'168.2.3',
+ b'/Users/' + b'synthetic/input.wav', b'/home/' + b'synthetic/config',
+ b'C:\\Users\\' + b'synthetic\\input.wav',
+ b'ghp_' + b'x' * 36, b'hf_' + b'x' * 36,
+ b'-----BEGIN ' + b'PRIVATE KEY-----',
+ b'person@' + b'private-mail.invalid-domain.com',
+ b'https://' + b'person:secret@example.com',
+ ]
+ for sample in samples:
+ with self.subTest(kind=samples.index(sample)):
+ self.assertTrue(scan_content('tests/test_template.py', sample))
+
+ def test_public_examples_and_hashes_are_allowed(self):
+ self.assertFalse(sensitive_markers(
+ b'contact@example.com reviewer@users.noreply.github.com sha256:' + b'a' * 64))
+
+ def test_svg_provenance_is_decoded(self):
+ payload = base64.b64encode(b'ghp_' + b'x' * 36)
+ svg = b''
+ self.assertIn('SVG provenance: GitHub token', scan_content('assets/icon.svg', svg))
+ self.assertIn('unreadable SVG provenance', scan_content('assets/icon.svg', b'