Compare commits

...
18 Commits
Author SHA1 Message Date
james.maes 4e150dd61a Merge pull request 'ci: use ci-workflows@v1 again' (#16) from ci/pin-v1 into main
node / node (push) Successful in 37s
build / node (push) Successful in 37s
2026-11-08 14:27:46 +00:00
james.maesandClaude Opus 5.5 92b78bc996 ci: use ci-workflows@v1 again
node / node (push) Successful in 36s
build / node (push) Successful in 36s
v1 now contains node.yaml publishing (ci-workflows 0e0f145).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 14:25:28 +00:00
james.maes a3819b469c Merge pull request 'ci: Gitea CI publishes the release from the tag; 1.1.1-rc.1' (#15) from ci/gitea-release into main
node / node (push) Successful in 42s
build / node (push) Successful in 42s
2026-11-08 13:37:58 +00:00
james.maesandClaude Opus 5.5 09fdb0663c ci: Gitea CI publishes the release from the tag; 1.1.1-rc.1
node / node (push) Successful in 1m15s
build / node (push) Successful in 1m15s
build.yaml sets publish: true with release-files dist/* on the shared node.yaml
(@main until ci-workflows v1 moves): a v<package.json version> tag reruns every
check, bundles, and creates the Gitea release with the archive, SHA256SUMS and
the CHANGELOG section. Replaces the retired GitHub release + attestation path
(Gitea has no attestation equivalent). 1.1.1-rc.1 is the pipeline proof, with
no contract change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:27:55 -06:00
james.maes 39e4c3eee7 Merge pull request 'ci: first run on ci-workflows v1' (#14) from ci/first-run-v1 into main
node / node (push) Successful in 1m10s
build / node (push) Successful in 1m10s
2026-11-07 22:41:46 +00:00
James MaesandClaude Opus 5.5 80e859534e ci: first run on ci-workflows v1 (node.yaml)
node / node (push) Successful in 40s
build / node (push) Successful in 40s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:37:56 -06:00
james.maes eee42e951d Merge pull request 'ci: build on the shared ci-workflows node.yaml, remove GitHub workflows' (#13) from ci/ci-workflows into main 2026-11-07 22:16:00 +00:00
james.maesandClaude Opus 5.5 177db4b06b ci: pin ci-workflows node.yaml to @v1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:15:36 -06:00
james.maesandClaude Opus 5.5 57670e9891 ci: build on the shared ci-workflows node.yaml, remove GitHub workflows
node / node (push) Successful in 1m13s
build / node (push) Successful in 1m13s
Gitea CI runs the uv script-quality and source-header checks, typecheck,
lint, check and bundle through KofTwentyTwo/ci-workflows node.yaml (Node 24,
pinned uv). Removes the retired GitHub Actions workflows (ci, codeql,
codeql-analysis, pr-gates, security) and the CodeQL-only scripts. The EX-0015
uri-js guard keeps its lock/integrity/license pins; its check of the
pr-gates.yml allowlist entry goes with that file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:14:09 -06:00
james.maes 220cd529f1 Merge pull request 'docs(release): record protocol 1.1.0 publication and consumer fixture pins' (#12) from docs/release-1.1.0-20261105 into main
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (push) Successful in 14s
QRun-IO/ci-policy: Shared Node CI / verify (push) Successful in 41s
QRun-IO/ci-policy: Shared Node CI / gate (push) Successful in 13s
QRun-IO/ci-policy: ci / node-8 (push) Successful in 55s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 5s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (push) Successful in 10s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
2026-11-06 14:15:02 +00:00
james.maesandClaude Fable 5.1 fde9cd8183 docs(versions): 1.2.1 client in the negotiation example
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 47s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Successful in 13s
QRun-IO/ci-policy: ci / node-8 (pull_request) Successful in 1m4s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 16s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:55:33 -06:00
james.maesandClaude Fable 5.1 920dd57035 docs: the paired Windows connector is 1.2.1 (1.2.0 was never published)
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / ci-result (pull_request) Canceled after 0s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Canceled after 0s
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 49s
QRun-IO/ci-policy: ci / node-8 (pull_request) Canceled after 50s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Canceled after 0s
QRun-IO/ci-policy: native / native-result (pull_request) Canceled after 0s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:55:04 -06:00
james.maesandClaude Fable 5.1 54c02db3a0 docs(readme): published distribution 1.1.0 and the 2.4.0 cohort
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Canceled after 0s
QRun-IO/ci-policy: ci / ci-result (pull_request) Canceled after 0s
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Canceled after 38s
QRun-IO/ci-policy: ci / node-8 (pull_request) Canceled after 38s
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1e25bb5a243dfdcd95d4b9a4f780200ff8c8778a] (pull_request) Successful in 7s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 11s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 6s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-06 07:54:15 -06:00
james.maesandClaude Fable 5.1 a4ab8a4fa1 docs(release): record protocol 1.1.0 publication and consumer fixture pins
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow d20a578c7d818bc7ca32a2ac2c675da4e4f27385] (pull_request) Failing after 2s
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Waiting to run
QRun-IO/ci-policy: native / native-result (pull_request) Failing after 1s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-05 17:09:15 -06:00
james.maes 5005f3c9e2 Merge pull request 'feat(realtime): printer-approval-v1 and the PRINTERS frame for extension 2.4.0' (#11) from feat/printer-approval-20261105 into main
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Failing after 2s
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (push) Successful in 11s
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (push) Failing after 1s
QRun-IO/ci-policy: Shared Node CI / gate (push) Failing after 1s
QRun-IO/ci-policy: ci / node-8 (push) Failing after 1m26s
QRun-IO/ci-policy: ci / ci-result (push) Failing after 1s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (push) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (push) Failing after 1s
2026-11-05 22:22:14 +00:00
james.maesandClaude Fable 5.1 ac835f8b93 feat(realtime): add printer-approval-v1 and the PRINTERS frame for extension 2.4.0
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / Scoped workflow [workflow 1b3648d9421543350067e95e54959dfb471b26d5] (pull_request) Successful in 5s
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 33s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Successful in 9s
QRun-IO/ci-policy: ci / node-8 (pull_request) Successful in 44s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 3s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: ci / shared-pipeline (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 11s
Add the printer-approval-v1 HELLO capability (maxItems 5), extend
ControlCapability and the historical control-poll/management capability
arrays to four entries, and define RealtimePrinters, RealtimePrintersPayload
and PrinterApprovalState with the union entry. The server sends PRINTERS only
on connections that advertised the capability, after each accepted INVENTORY
and on every approval change; it is not acknowledged and carries no job data.

Add the PRINTERS, five-capability hello and 2.4.0 discovery fixtures, cover
the new frame in the directional and negative verifier checks, document the
frame, the 2.4.0 registry row and the negotiation rule, and record
distribution 1.1.0 as unreleased. Wire payload v1, the subprotocol and every
2.3.0 fixture are unchanged. 79 definitions compile and 82 fixtures pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
2026-11-05 15:39:39 -06:00
james.maes afc0c145ac Merge pull request 'docs: finalize stable printing protocol guidance' (#10) from docs/printing-release-closeout-20261104 into main
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (push) Successful in 11s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
QRun-IO/ci-policy: native / native-result (push) Successful in 5s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 4s
QRun-IO/ci-policy: Shared Node CI / gate (push) Successful in 10s
QRun-IO/ci-policy: ci / node-8 (push) Successful in 1m31s
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (push) Successful in 53s
2026-11-04 15:44:44 +00:00
james.maesandCodex a80b619636 docs: finalize stable printing protocol guidance
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / original-business (pull_request) Skipped
QRun-IO/ci-policy: ci / original-mcp (pull_request) Skipped
QRun-IO/ci-policy: ci / original-voyage (pull_request) Skipped
QRun-IO/ci-policy: ci / original-cli (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Successful in 31s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Successful in 8s
QRun-IO/ci-policy: ci / node-8 (pull_request) Successful in 42s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 4s
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / android-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 10s
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
Document the published 1.0 cohort, Gitea primary release path, realtime transport and immutable fixture provenance. Preserve historical compatibility and acceptance evidence.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
2026-11-04 09:38:47 -06:00
33 changed files with 458 additions and 632 deletions

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: build
on:
push:
jobs:
node:
# @main until the owner moves ci-workflows v1 onto the publishing node.yaml; then back to @v1.
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/node.yaml@v1
with:
node-version: '24'
install: npm ci --ignore-scripts
uv: true
commands: |
uv sync --locked
uv run --locked --no-sync python .github/scripts/test-script-quality.py
uv run --locked --no-sync python .github/scripts/check-script-quality.py
uv run --locked --no-sync python .github/scripts/test-source-headers.py
uv run --locked --no-sync python .github/scripts/check-source-headers.py
npm run typecheck
npm run lint
npm run check
npm run bundle
# A v<package.json version> tag creates the Gitea release with the bundle, SHA256SUMS and the
# CHANGELOG section (-rc.N -> prerelease). The package is private: nothing goes to npm.
publish: true
npm-publish-dirs: ''
release-files: dist/*
-108
View File
@@ -1,108 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
"""Reject missing/malformed CodeQL evidence before enforcing the severity gate."""
import json
import math
import os
from pathlib import Path
from typing import TypedDict
RuleProperties = TypedDict("RuleProperties", {"security-severity": str | float | None}, total=False)
class Rule(TypedDict):
"""Keep the rule identity and optional severity metadata used by the gate."""
id: str
properties: RuleProperties
class Component(TypedDict, total=False):
"""Describe CodeQL driver or extension rule metadata."""
name: str
rules: list[Rule]
class Tool(TypedDict, total=False):
"""Group the driver and optional extension rule catalogs."""
driver: Component
extensions: list[Component]
class Result(TypedDict, total=False):
"""Describe only the result fields consumed by the severity gate."""
ruleId: str
rule: dict[str, str]
suppressions: list[object]
class Run(TypedDict, total=False):
"""Retain invocation success and findings for one analyzed language."""
tool: Tool
results: list[Result]
invocations: list[dict[str, bool]]
class Report(TypedDict, total=False):
"""Represent SARIF fields while retaining the explicit runtime validation."""
version: str
runs: list[Run]
MAX_SEVERITY = 10
def check(directory: str | Path, language: str, threshold: float) -> bool: # noqa: PLR0912 -- Keep the ordered evidence checks together; none may be skipped.
"""Validate complete CodeQL evidence and report findings at the threshold."""
if not math.isfinite(threshold) or not 0 <= threshold <= MAX_SEVERITY:
raise ValueError("Invalid security severity threshold")
canonical = {"java-kotlin": "java", "javascript-typescript": "javascript", "c-cpp": "cpp"}.get(language, language)
files = sorted(Path(directory).glob("*.sarif"))
if not canonical or not any(path.name == canonical + ".sarif" for path in files):
raise ValueError("Missing expected CodeQL SARIF report for " + language)
blocking: list[tuple[float, str]] = []
for path in files:
report: Report = json.loads(path.read_text(encoding="utf-8"))
runs = report.get("runs")
if report.get("version") != "2.1.0" or not isinstance(runs, list) or not runs:
raise ValueError("Invalid or empty SARIF runs: " + path.name)
for run in runs:
driver = run.get("tool", {}).get("driver", {})
results = run.get("results")
if driver.get("name") != "CodeQL" or not isinstance(driver.get("rules"), list) or not isinstance(results, list):
raise ValueError("Missing CodeQL tool/rules/results evidence: " + path.name)
rules: dict[str, Rule] = {}
for component in [driver, *run.get("tool", {}).get("extensions", [])]:
for rule in component.get("rules", []):
rules[rule["id"]] = rule
if not rules:
raise ValueError("CodeQL report has no analyzed rule metadata: " + path.name)
for invocation in run.get("invocations", []):
if invocation.get("executionSuccessful") is False:
raise ValueError("CodeQL invocation failed: " + path.name)
for result in results:
rule_id = result.get("ruleId") or result.get("rule", {}).get("id")
if rule_id not in rules:
raise ValueError("Result references missing rule metadata: " + path.name)
if result.get("suppressions"):
continue
severity = float(rules[rule_id].get("properties", {}).get("security-severity", 0) or 0)
if not math.isfinite(severity) or not 0 <= severity <= MAX_SEVERITY:
raise ValueError("Invalid rule severity: " + rule_id)
if severity >= threshold:
blocking.append((severity, rule_id))
for severity, rule_id in sorted(blocking, reverse=True):
print(f"::error title=CodeQL security finding::{rule_id} ({severity})")
print(f"Validated {len(files)} SARIF report(s); {len(blocking)} blocking result(s)")
return bool(blocking)
if __name__ == "__main__":
raise SystemExit(1 if check(os.environ["SARIF_DIR"], os.environ["EXPECTED_LANGUAGE"], float(os.environ["THRESHOLD"])) else 0)
+1 -8
View File
@@ -5,7 +5,6 @@
import hashlib
import json
import re
import shutil
import subprocess
import sys
@@ -16,12 +15,10 @@ PYTHON_SUFFIXES = (".py", ".pyi", ".pyw")
SCRIPT_PATTERN = ".github/scripts/*.py"
PYRIGHT_CONFIG = ".github"
JAVASCRIPT_SUFFIXES = (".js", ".cjs", ".mjs", ".jsx", ".ts", ".mts", ".cts", ".tsx")
# EX-0015: the only npm package whose license classification the PR gate may exempt (the matcher ignores versions, so this guard pins it).
URI_JS_PURL = "pkg:npm/uri-js"
# EX-0015: the only npm package with a license exception; this guard pins the reviewed copy.
URI_JS_VERSION = "4.4.1"
URI_JS_INTEGRITY = "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="
URI_JS_LICENSE_SHA256 = "0af366eff4c01ec147c9c61ea9e8ffad64a4294754c9d79355f3fd1b97cb2fb9"
PR_GATES = ".github/workflows/pr-gates.yml"
# Microsoft's official npm Pyright, restored from package-lock.json by `npm ci --ignore-scripts`.
PYRIGHT_ENTRY = Path(__file__).resolve().parents[2] / "node_modules" / "pyright" / "index.js"
@@ -116,10 +113,6 @@ def check_uri_js_exception(root: Path) -> None:
digest = hashlib.sha256((root / "node_modules" / "uri-js" / "LICENSE").read_bytes()).hexdigest()
if digest != URI_JS_LICENSE_SHA256:
raise ValueError(f"Installed uri-js LICENSE bytes differ from the reviewed EX-0015 copy ({digest})")
caller = [line for line in (root / PR_GATES).read_text(encoding="utf-8").splitlines() if not line.lstrip().startswith("#")]
listed = re.findall(r"pkg:npm/[^\s,]+", "\n".join(caller))
if listed != [URI_JS_PURL]:
raise ValueError(f"EX-0015 permits only {URI_JS_PURL} in allow-dependencies-licenses, found {listed}")
def main() -> None:
-107
View File
@@ -1,107 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
"""Exercise the exact CI guards using synthetic evidence only."""
import contextlib
import copy
import importlib.util
import io
import json
import sys
import tempfile
import unittest
from pathlib import Path
from types import ModuleType
from typing import Any, override
sys.dont_write_bytecode = True
def load(name: str) -> ModuleType:
"""Load the real sibling guard for synthetic evidence tests."""
spec = importlib.util.spec_from_file_location(name, Path(__file__).with_name(name + ".py"))
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
codeql = load("check-codeql-sarif")
class SarifTests(unittest.TestCase):
"""Reject incomplete SARIF evidence and preserve severity enforcement."""
@override
def setUp(self) -> None:
"""Create isolated synthetic evidence with cleanup after each test."""
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.directory = Path(self.temporary.name)
self.report: dict[str, Any] = {"version": "2.1.0", "runs": [{"tool": {"driver": {"name": "CodeQL", "rules": [{"id": "synthetic", "properties": {"security-severity": "8.0"}}]}}, "results": []}]}
def save(self, report: object) -> None:
"""Write the test body as SARIF without validating it first."""
(self.directory / "java.sarif").write_text(json.dumps(report), encoding="utf-8")
def test_clean_report_with_zero_findings_passes(self) -> None:
"""Verify clean report with zero findings passes."""
self.save(self.report)
self.assertFalse(codeql.check(self.directory, "java-kotlin", 7))
def test_missing_report_or_wrong_language_fails(self) -> None:
"""Verify missing report or wrong language fails."""
with self.assertRaises(ValueError):
codeql.check(self.directory, "java-kotlin", 7)
self.save(self.report)
with self.assertRaises(ValueError):
codeql.check(self.directory, "csharp", 7)
def test_empty_or_malformed_report_fails(self) -> None:
"""Verify empty or malformed report fails."""
for text in ["", "{", "null", "[]"]:
with self.subTest(text=text):
(self.directory / "java.sarif").write_text(text)
with self.assertRaises((ValueError, AttributeError)):
codeql.check(self.directory, "java-kotlin", 7)
def test_missing_runs_tool_rules_or_results_fails(self) -> None:
"""Verify missing runs tool rules or results fails."""
variants: list[dict[str, Any]] = [{"version": "2.1.0"}, {"version": "2.1.0", "runs": []}]
for field in ["tool", "results"]:
bad = copy.deepcopy(self.report)
del bad["runs"][0][field]
variants.append(bad)
bad = copy.deepcopy(self.report)
bad["runs"][0]["tool"]["driver"]["rules"] = []
variants.append(bad)
for bad in variants:
with self.subTest(report=bad):
self.save(bad)
with self.assertRaises(ValueError):
codeql.check(self.directory, "java-kotlin", 7)
def test_high_finding_blocks_and_unknown_rule_fails(self) -> None:
"""Verify high finding blocks and unknown rule fails."""
self.report["runs"][0]["results"] = [{"ruleId": "synthetic"}]
self.save(self.report)
with contextlib.redirect_stdout(io.StringIO()):
self.assertTrue(codeql.check(self.directory, "java-kotlin", 7))
self.report["runs"][0]["results"] = [{"ruleId": "missing"}]
self.save(self.report)
with self.assertRaises(ValueError):
codeql.check(self.directory, "java-kotlin", 7)
def test_failed_invocation_and_invalid_threshold_fail(self) -> None:
"""Verify failed invocation and invalid threshold fail."""
self.report["runs"][0]["invocations"] = [{"executionSuccessful": False}]
self.save(self.report)
with self.assertRaises(ValueError):
codeql.check(self.directory, "java-kotlin", 7)
with self.assertRaises(ValueError):
codeql.check(self.directory, "java-kotlin", float("nan"))
if __name__ == "__main__":
unittest.main()
+8 -24
View File
@@ -234,37 +234,29 @@ class JavaScriptProbeTests(unittest.TestCase):
self.assertIn("TS2322", bad.stdout)
URI_GATES = "jobs:\n pr:\n with:\n allow-dependencies-licenses: pkg:npm/uri-js\n"
URI_MIXED_GATES = "jobs:\n pr:\n with:\n allow-dependencies-licenses: >-\n pkg:nuget/Microsoft.WindowsAppSDK,\n pkg:npm/uri-js\n"
def uri_lock(version: str = "4.4.1", integrity: str | None = None, dev: bool = True, direct: bool = False, consumer_dev: bool = True) -> dict[str, Any]:
"""Build a minimal package-lock shape with uri-js under an ESLint-like consumer."""
root_entry: dict[str, Any] = {"devDependencies": {"uri-js": version} if direct else {"eslint": "10.14.0"}}
return {"packages": {"": root_entry, "node_modules/uri-js": {"version": version, "integrity": integrity or guard.URI_JS_INTEGRITY, "dev": dev}, "node_modules/eslint": {"dev": consumer_dev, "dependencies": {"uri-js": "^4.2.2"}}}}
def uri_project(root: Path, lock: dict[str, Any], gates: str = URI_GATES, license_bytes: bytes | None = None) -> None:
"""Write a private project with the lock, installed LICENSE and PR caller that the guard inspects."""
def uri_project(root: Path, lock: dict[str, Any], license_bytes: bytes | None = None) -> None:
"""Write a private project with the lock and installed LICENSE that the guard inspects."""
(root / "package-lock.json").write_text(json.dumps(lock), encoding="utf-8")
installed = root / "node_modules" / "uri-js"
installed.mkdir(parents=True)
installed.joinpath("LICENSE").write_bytes((ROOT / "node_modules" / "uri-js" / "LICENSE").read_bytes() if license_bytes is None else license_bytes)
workflows = root / ".github" / "workflows"
workflows.mkdir(parents=True)
(workflows / "pr-gates.yml").write_text("# EX-0015 pkg:npm/uri-js is cited here\n" + gates, encoding="utf-8")
class UriJsExceptionTests(unittest.TestCase):
"""Allow the EX-0015 license exception only for the reviewed, unmodified, development-only uri-js."""
def test_reviewed_uri_js_with_exact_caller_is_accepted(self) -> None:
"""Accept the reviewed package, installed license bytes and an exact caller list, mixed with NuGet entries."""
for gates in (URI_GATES, URI_MIXED_GATES):
with tempfile.TemporaryDirectory() as name, self.subTest(gates=gates):
root = Path(name)
uri_project(root, uri_lock(), gates)
guard.check_uri_js_exception(root)
def test_reviewed_uri_js_is_accepted(self) -> None:
"""Accept the reviewed package with the reviewed installed license bytes."""
with tempfile.TemporaryDirectory() as name:
root = Path(name)
uri_project(root, uri_lock())
guard.check_uri_js_exception(root)
def test_other_version_integrity_scope_or_consumer_is_rejected(self) -> None:
"""Reject another version, integrity, non-dev or direct use, a non-dev consumer and a second copy."""
@@ -284,14 +276,6 @@ class UriJsExceptionTests(unittest.TestCase):
with self.assertRaisesRegex(ValueError, "LICENSE bytes differ"):
guard.check_uri_js_exception(root)
def test_caller_must_list_only_uri_js(self) -> None:
"""Reject a caller that lists no npm exception, another package, a versioned form or a duplicate."""
for gates in ("jobs:\n pr:\n uses: shared\n", URI_GATES.rstrip() + ", pkg:npm/other\n", "jobs:\n pr:\n with:\n allow-dependencies-licenses: pkg:npm/other\n", URI_GATES.rstrip() + "@4.4.1\n", URI_GATES.rstrip() + ", pkg:npm/uri-js\n"):
with tempfile.TemporaryDirectory() as name, self.subTest(gates=gates), self.assertRaisesRegex(ValueError, "permits only"):
root = Path(name)
uri_project(root, uri_lock(), gates)
guard.check_uri_js_exception(root)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -3,6 +3,7 @@
"includes": [
".github/*.yml",
".github/*.yaml",
".gitea/*.yaml",
".github/scripts/*.py",
"tools/*.mjs"
],
-100
View File
@@ -1,100 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: Protocol CI and release
on:
pull_request:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
permissions:
contents: read
jobs:
verify:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '24'
package-manager-cache: false
- run: npm ci --ignore-scripts
- name: Set up uv
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: '0.13.6'
enable-cache: false
- name: Restore locked Python tools
run: uv sync --locked
- name: Verify script quality gates and rejection paths
run: |
uv run --locked --no-sync python .github/scripts/test-script-quality.py
uv run --locked --no-sync python .github/scripts/check-script-quality.py
npm run typecheck
npm run lint
- name: Verify owned source notices and rejection paths
run: |
python3 .github/scripts/test-source-headers.py
python3 .github/scripts/check-source-headers.py
- run: npm run check
- run: npm run bundle
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: protocol-bundle
path: dist/*
if-no-files-found: error
release-security:
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/security.yml
permissions:
contents: read
security-events: write
release-codeql:
if: startsWith(github.ref, 'refs/tags/v')
uses: ./.github/workflows/codeql.yml
permissions:
contents: read
security-events: write
release:
if: startsWith(github.ref, 'refs/tags/v')
needs: [verify, release-security, release-codeql]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: protocol-bundle
path: dist
- name: Verify packaged checksums
working-directory: dist
run: sha256sum --check --strict SHA256SUMS
- name: Attest verified release artifacts
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: dist/*
- name: Publish versioned GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
prerelease=()
if [[ "$GITHUB_REF_NAME" == *-* ]]; then prerelease+=(--prerelease); fi
gh release create "$GITHUB_REF_NAME" dist/*.tar.gz dist/SHA256SUMS \
--verify-tag --title "$GITHUB_REF_NAME" --notes-file CHANGELOG.md "${prerelease[@]}"
-128
View File
@@ -1,128 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
# Local copy of KofTwentyTwo/standards at fdf5ed029603c938615a32b1beca3628b8787945 (v0.1.2).
# Pins, inputs and analysis checks preserved; report validation now fails closed.
# Static analysis (K22-CI-22): CodeQL with the security-extended suite, failing on
# alerts of high severity or above. Called as:
#
# jobs:
# codeql:
# uses: KofTwentyTwo/standards/.github/workflows/codeql.yml@<sha> # vX.Y.Z
# permissions:
# contents: read
# security-events: write
# with:
# languages: '["csharp", "actions"]'
# build-mode: manual
# build-command: dotnet build MySolution.slnx -p:Platform=x64
# runs-on: windows-latest
#
# producing `codeql / analyze (<language>)` for each language. The CodeQL action
# uploads results but never fails a build on them, so the last step reads the SARIF
# and fails on any result whose rule has security-severity >= the threshold.
# Suppress a false positive with a query filter in a CodeQL config file passed as
# `config-file`, citing the exception record in a comment.
name: codeql
on:
workflow_call:
inputs:
languages:
description: JSON array of CodeQL languages, e.g. '["csharp","actions"]'.
type: string
required: true
build-mode:
description: CodeQL build mode for compiled languages (none, autobuild, manual). Interpreted languages always use none.
type: string
default: autobuild
build-command:
description: Command run for build-mode manual.
type: string
default: ""
runs-on:
description: Runner label; Windows-only builds (WinUI, WPF) need windows-latest.
type: string
default: ubuntu-24.04
setup-dotnet:
description: Install the .NET SDK from global.json before building.
type: boolean
default: false
config-file:
description: Optional path to a CodeQL configuration file in the calling repository.
type: string
default: ""
fail-on-security-severity:
description: Minimum CVSS-style security-severity that fails the job (7.0 = high).
type: string
default: "7.0"
permissions: {}
jobs:
analyze:
name: analyze (${{ matrix.language }})
runs-on: ${{ inputs.runs-on }}
timeout-minutes: 60
permissions:
contents: read
security-events: write # upload SARIF results to code scanning
strategy:
fail-fast: false
matrix:
language: ${{ fromJSON(inputs.languages) }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Test scan guard rejection paths
shell: python
run: |
import runpy
runpy.run_path(".github/scripts/test-scan-gates.py", run_name="__main__")
- name: Set up .NET SDK
if: inputs.setup-dotnet
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
global-json-file: global.json
- name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language) && 'none' || inputs.build-mode }}
queries: security-extended
config-file: ${{ inputs.config-file }}
- name: Build (manual mode)
if: inputs.build-mode == 'manual' && !contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language)
shell: bash
env:
BUILD_COMMAND: ${{ inputs.build-command }}
run: |
if [[ -z "$BUILD_COMMAND" ]]; then
echo "::error::build-mode is manual but build-command is empty"
exit 1
fi
eval "$BUILD_COMMAND"
- name: Analyze
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: /language:${{ matrix.language }}
output: codeql-sarif
# Fork pull requests have a read-only token; analyze still runs and gates.
upload: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'never' || 'always' }}
- name: Gate on security severity
shell: python
env:
SARIF_DIR: codeql-sarif
THRESHOLD: ${{ inputs.fail-on-security-severity }}
EXPECTED_LANGUAGE: ${{ matrix.language }}
run: |
import runpy
runpy.run_path(".github/scripts/check-codeql-sarif.py", run_name="__main__")
-21
View File
@@ -1,21 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: codeql
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '41 6 * * 1'
workflow_call:
permissions: {}
jobs:
codeql:
uses: ./.github/workflows/codeql-analysis.yml
permissions:
contents: read
security-events: write
with:
languages: '["javascript-typescript", "actions"]'
build-mode: none
-29
View File
@@ -1,29 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
# Pull request gates (K22-CI-01): Conventional Commit title, DCO sign-off, and
# dependency review, from the shared KofTwentyTwo/standards workflow pinned by SHA
# (K22-CI-11). Dependabot's github-actions ecosystem keeps the pin current.
name: pr-gates
on:
pull_request:
permissions: {}
concurrency:
group: pr-gates-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
pr:
uses: KofTwentyTwo/standards/.github/workflows/pr.yml@fdf5ed029603c938615a32b1beca3628b8787945 # v0.1.2
with:
# EX-0015: development-only uri-js 4.4.1 (ESLint transitive; BSD-2-Clause plus an authors' views sentence).
# https://git.kof22.com/KofTwentyTwo/standards/src/commit/a18179e90f8e6d35ba9f5e583249885c066e52b8/exceptions/register.md#ex-0015
# The matcher ignores versions; check-script-quality.py pins the version, lock integrity and installed
# license bytes and rejects any other pkg:npm entry here.
allow-dependencies-licenses: pkg:npm/uri-js
permissions:
contents: read # read the commits being merged
pull-requests: read # read the pull request title and dependency diff
-26
View File
@@ -1,26 +0,0 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: security
on:
pull_request:
push:
branches: [main]
schedule:
- cron: '23 6 * * 1'
workflow_dispatch:
workflow_call:
permissions: {}
concurrency:
group: security-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
security:
uses: KofTwentyTwo/standards/.github/workflows/security.yml@fdf5ed029603c938615a32b1beca3628b8787945 # v0.1.2
permissions:
contents: read
security-events: write
+25
View File
@@ -1,5 +1,30 @@
# Changelog
## 1.1.1-rc.1
Release-pipeline candidate: no schema, fixture or wire change from 1.1.0. The
first release built and published by Gitea CI from the tag (bundle, `SHA256SUMS`
and these notes), replacing the hand-published Gitea release and the retired
GitHub release with attestation.
## 1.1.0
Additive contract for printing semantic extension **2.4.0**. Wire payload stays
**v1** and the subprotocol stays `qqq-printing.v1`; every 2.3.0 fixture is unchanged.
- Add the `printer-approval-v1` capability to `RealtimeHelloPayload` (`maxItems` 5)
and `ControlCapability`; the historical control poll and management
`Device.control` accept four non-realtime capabilities.
- Add the server → client `PRINTERS` frame (`RealtimePrinters`,
`RealtimePrintersPayload`, `PrinterApprovalState`) carrying the complete approval
state of the device's latest accepted inventory; sent only to connections that
advertised `printer-approval-v1`, after each accepted INVENTORY and on every
approval change. Not acknowledged, not durable, no job or document data.
- Add `realtime-printers.message.json`, a five-capability hello and a 2.4.0
discovery fixture; the verifiers cover the directional frame, bounds and
required fields. See [REALTIME.md](docs/REALTIME.md#printer-approval) and the
[version registry](docs/VERSIONS.md).
## 1.0.0
Distribution version 1.0.0 of the contract and fixtures from 0.4.0-rc.1. The wire
+5 -5
View File
@@ -2,13 +2,13 @@
Use Node.js 24 or newer. Install the locked development dependencies with `npm ci`, then run `npm run check` (`npm test` is the standard entry point for the same check). The runtime protocol has no JavaScript dependency; Ajv and ajv-formats are development-only JSON Schema validators.
Make changes through a pull request. Describe the client/server behavior affected and update README, schema, fixtures and changelog together. Keep credentials, customer documents, printer addresses and machine-specific data out of fixtures. Samples must remain synthetic.
Make changes through a pull request to `main` on the [primary Gitea repository](https://git.kof22.com/QRun-IO/qqq-print-protocol). GitHub is a readable shadow; do not open ordinary development PRs or publish releases there. Describe the client/server behavior affected and update README, schema, fixtures and changelog together. Keep credentials, customer documents, printer addresses and machine-specific data out of fixtures. Samples must remain synthetic.
Run both the Java and .NET consumers' protocol tests for a contract change. Check out sibling `qbit-printing`, `qqq-print-agent` and this repository; see [integration notes](docs/INTEGRATION.md). Coordinate consumer changes before merging a breaking change.
Keep wire names stable within `/printing/v1`. Optional response fields may be added; clients ignore unknown response fields. Removing/renaming a field, changing its type or changing an existing field's meaning requires a new protocol version. A prerelease package version is not permission to silently break an already deployed connector.
Keep wire-v1 payload names stable for native `/qqq/v1/qbit/printing` and explicit legacy `/printing/v1` routes. Optional response fields may be added; clients ignore unknown response fields. Removing/renaming a field, changing its type or changing an existing field's meaning requires explicit versioned compatibility design. Package, printing extension, wire and WebSocket subprotocol versions are independent; see [the version registry](docs/VERSIONS.md). A package version is not permission to silently break an already deployed connector.
`npm run bundle` produces the release archive and checksum in `dist/`. Do not hand-edit generated checksums. Release tags must match the version in package.json, prefixed with `v`. Commits and releases are made by maintainers after review.
`npm run bundle` produces the release archive and checksum in `dist/`. Do not hand-edit generated checksums. Release tags must match the version in package.json, prefixed with `v`. A `v<version>` tag on the reviewed merge commit makes Gitea CI rerun every check, build the bundle and create the [Gitea release](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases) with the archive, a fresh `SHA256SUMS` (the bundle's own is checked first) and the matching CHANGELOG section as notes (a `-rc.N` version is a prerelease). Gitea has no equivalent of the retired GitHub artifact attestation: verify downloads against `SHA256SUMS` and the tag. GitHub Actions for this repository were retired on 2026-11-04; retained `.github/scripts` remain local/shared-CI tooling. See [current release provenance](docs/INTEGRATION.md#stable-printing-release) before updating consumer fixture pins.
## Script quality
@@ -21,6 +21,6 @@ npm run typecheck
npm run lint
```
`npm run quality` runs the repository's Python sequence in one step (`uv sync --locked`, both script-quality commands and both source-header commands); the shared Node CI calls it as its `quality-script`. It needs `uv` on `PATH` and `npm ci` first.
`npm run quality` runs the repository's Python sequence in one step (`uv sync --locked`, both script-quality commands and both source-header commands); Gitea CI (`.gitea/workflows/build.yaml`, the shared ci-workflows `node.yaml`) runs the same commands individually, then typecheck, lint, check and bundle. It needs `uv` on `PATH` and `npm ci` first.
`check-script-quality.py` fails for any tracked Python outside `.github/scripts/` or JavaScript outside the `tsconfig.json` include list, and requires Pyright's analyzed-file count to equal the selected scripts. Pyright must run as `pyright -p .github`: it silently skips dot directories from a root config. Documented exceptions: CLI `print`/`console.log` only in `check-*.py` and `tools/`; synthetic test assertions and boundary constants; narrow inline `noqa` comments for fixed no-shell Git/interpreter calls and ordered fail-closed validators. Do not loosen a rule or add a type waiver to pass. `package.json` overrides `minimatch` to 11.0.1, the patched release for [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (affected before 11.0.1); keep it until ESLint's own dependency range includes a patched release. The only npm license exception, [EX-0015](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0015) for development-only `uri-js` 4.4.1, is enforced by `check-script-quality.py` (version, lock integrity, installed license bytes and the exact `pkg:npm/uri-js` caller entry); changing that package or the entry needs renewed approval.
`check-script-quality.py` fails for any tracked Python outside `.github/scripts/` or JavaScript outside the `tsconfig.json` include list, and requires Pyright's analyzed-file count to equal the selected scripts. Pyright must run as `pyright -p .github`: it silently skips dot directories from a root config. Documented exceptions: CLI `print`/`console.log` only in `check-*.py` and `tools/`; synthetic test assertions and boundary constants; narrow inline `noqa` comments for fixed no-shell Git/interpreter calls and ordered fail-closed validators. Do not loosen a rule or add a type waiver to pass. `package.json` overrides `minimatch` to 11.0.1, the patched release for [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (affected before 11.0.1); keep it until ESLint's own dependency range includes a patched release. The only npm license exception, [EX-0015](https://github.com/KofTwentyTwo/standards/blob/main/exceptions/register.md#ex-0015) for development-only `uri-js` 4.4.1, is enforced by `check-script-quality.py` (version, lock integrity and installed license bytes); changing that package needs renewed approval.
+38 -15
View File
@@ -1,17 +1,40 @@
# QQQ Print Protocol v1
Shared contract for the [QQQ 4.1+ printing QBit](https://github.com/QRun-IO/qbit-printing) and [Windows AppKit connector](https://github.com/QRun-IO/qqq-print-agent). The coordinated [realtime extension 2.0](docs/REALTIME.md) uses a client-initiated authenticated WebSocket for controls, receipts and job notifications, with separate streamed HTTPS documents and no polling fallback. This contract is authoritative for wire names; language types may use their idiomatic naming.
Shared contract for the [QQQ 4.1+ printing QBit](https://git.kof22.com/QRun-IO/qbit-printing) and [Windows AppKit connector](https://git.kof22.com/QRun-IO/qqq-print-agent). The [realtime transport](docs/REALTIME.md) uses a client-initiated authenticated two-way WebSocket for controls, receipts and job notifications, with separate streamed HTTPS documents and no polling fallback. No inbound connection to the facility is required. This contract is authoritative for wire names; language types may use their idiomatic naming.
Distribution version: **1.0.0**. Earlier release candidates: [0.4.0-rc.1](https://github.com/QRun-IO/qqq-print-protocol/releases/tag/v0.4.0-rc.1) and [0.3.0-rc.1](https://github.com/QRun-IO/qqq-print-protocol/releases/tag/v0.3.0-rc.1). Wire protocol: **v1**. This repository supplies specification, JSON Schema and synthetic compatibility fixtures; install the Windows connector and integrate the QBit from their own repositories to print documents.
Published distribution: [**1.1.0**](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.1.0). Wire protocol: **v1**. The stable cohort pairs [Windows 1.2.1](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.2.1) with [QBit 1.1.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.0), printing semantic API **2.4.0**, and QQQ **4.1.0-RC.21**; the [1.0.0 cohort](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.0.0) (Windows 1.0.1, QBit 1.0.0, API 2.3.0) stays compatible. See [release and fixture provenance](docs/INTEGRATION.md#stable-printing-release). This repository supplies specification, JSON Schema and synthetic compatibility fixtures; install the Windows connector and integrate the QBit from their own repositories to print documents.
Version **0.4.0-rc.1** added the [printer media and operator
The stable release includes the [printer media and operator
contract](docs/PRINTER-MEDIA.md) for printing semantic extension **2.3.0** (see the
[extension version registry](docs/VERSIONS.md); 2.2 is rendering only), which 1.0.0
carries unchanged. Media profiles
[extension version registry](docs/VERSIONS.md); 2.2 is rendering only). Media profiles
represent configured Windows queue defaults with exact revision binding;
operator access uses normal current-user and tenant authorization without
granting device management. The installed 0.3.2 client does not support these
profiles. Wire version 1 and the client-initiated realtime transport stay the same.
granting device management. Use the stable cohort above for these profiles;
older 0.3.2 clients do not support them. Wire version 1 and the client-initiated
realtime transport stay the same.
Distribution **1.1.0** adds printing semantic extension **2.4.0**:
the `printer-approval-v1` HELLO capability and the server → client `PRINTERS`
frame, which reports each exposed printer's server approval over the
[realtime channel](docs/REALTIME.md#printer-approval). Clients advertise it only
after discovering a stable 2.4+ extension; a 2.3.0 server or a client without the
capability keeps the 2.3.0 wire unchanged.
## Current connection and delivery
1. Configure the QQQ server origin in the Windows app. Discover its UI/UX API and sign in using the server's configured authentication method.
2. Enroll the computer under that user and select which local Windows queues to expose. Approve the intended queues in QQQ.
3. The client opens the authenticated WebSocket, synchronizes settings and desired state, then listens for committed job and control events.
4. A job notification starts an authenticated HTTPS claim and streamed document download. Controls remain responsive during large transfers.
5. The client verifies the document, journals submission and sends durable status receipts over the socket. A lost connection automatically reconnects and reconciles pending work without blindly printing it again.
QQQ user, tenant, table, process and record permissions remain authoritative.
Document limits are configurable: the realtime defaults are 100 MiB for PDF and
20 MiB for ZPL. See [authentication](docs/QQQ-AUTHENTICATION.md),
[operational settings](docs/REALTIME.md#operational-settings-and-client-ceilings),
and [printer media selection](docs/PRINTER-MEDIA.md).
## Development
Use Node.js 24+ to run the development checks:
@@ -40,7 +63,7 @@ Use `Content-Type: application/json` except the content-download response. The e
The host application authenticates management requests and determines authorized tenant/facility scope. A standalone local test host may use a configured administrative bearer credential. Device ownership, tenant and facility are taken from server records, not trusted request fields.
## Endpoints
## Historical compatibility endpoints
| Method/path | Request | Successful response |
| --- | --- | --- |
@@ -58,14 +81,14 @@ The host application authenticates management requests and determines authorized
| GET `/admin/jobs` | none | 200 `{ "jobs": [PrintJob] }` |
| GET `/admin/jobs/{jobId}` | none | 200 `PrintJob` |
Device lists and job lists are scoped by host authorization. Listing pagination and destination/profile CRUD are beyond this first wire contract. Native application code may submit through the QBit service rather than the management HTTP API.
Device lists and job lists are scoped by host authorization. Bounded job/printer pagination is described below; destination/profile CRUD is outside this wire contract. Native application code may submit through the QBit service or the authorized [operator API](docs/PRINTER-MEDIA.md).
The additive [device-control contract](docs/DEVICE-CONTROLS.md) defines capability
discovery, dispatch pause/resume, operational disconnect/reconnect, permanent
credential revocation, inventory refresh, queued-job clearing, and exact owned
Windows spool cancellation. It uses durable request identities, bounded command
polling, and explicit receipts. These additions are included in the
`0.2.0-rc.1` distribution, with 42 definitions and 54 validated fixtures.
Windows spool cancellation. It uses durable request identities and explicit
receipts over the realtime channel. Historical command-polling DTOs remain for
explicit compatibility; they are not the stable client's transport.
## DTOs
@@ -97,7 +120,7 @@ Formats: `PDF` (`application/pdf`) and `ZPL` (`application/vnd.zebra-zpl`). Cont
6. Connector persists the job locally, reports RECEIVED, downloads `/device/jobs/{jobId}/content`, verifies byte length/hash, and validates the format/printer mapping.
7. Connector records SUBMITTING locally and reports it before invoking Windows. After success it records and reports SPOOLED, including a spooler reference when available. Submission must not be presented as physical print confirmation.
Content downloads derive authorization from the authenticated device and job assignment. They never accept arbitrary URLs. Default decoded document limit is 20 MiB. Copies range from 1 to 100. Reject empty documents, unsupported formats and unsupported printer capabilities before queuing.
Content downloads derive authorization from the authenticated device and job assignment. They never accept arbitrary URLs. The historical HTTP defaults were 20 MiB and 100 copies; current native limits are the configurable policy in [REALTIME.md](docs/REALTIME.md). Reject empty documents, unsupported formats and unsupported printer capabilities before queuing.
## Minimal delivery rules
@@ -157,11 +180,11 @@ Error body: `{ "code": "ERROR_CODE", "message": "Human-readable explanation" }`.
`npm run check` compiles every JSON Schema 2020-12 definition with Ajv and validates all shared fixtures, then checks ZPL bytes/hash/base64 and representative invalid inputs. The two pinned development dependencies are solely for standards-compliant schema validation; they do not enter either printing implementation's runtime.
`node tools/verify-fixtures.mjs` remains a dependency-free subset check. Java and .NET implementations should consume these exact fixtures in protocol tests, pinned to a reviewed immutable protocol commit. The Java HTTP round trip and .NET fixture compatibility checks passed during initial development. CI configuration is included; a local check does not imply that GitHub CI has run.
`node tools/verify-fixtures.mjs` remains a dependency-free subset check. Java and .NET implementations consume shared fixtures pinned to a reviewed immutable protocol commit. The Java HTTP round trip and .NET fixture compatibility checks passed during initial development. Gitea is the primary CI and release service; the protocol repository's GitHub Actions were retired on 2026-11-04. See [release and fixture provenance](docs/INTEGRATION.md#stable-printing-release) for the retained Windows fixture bridge and the scope of release evidence.
## Branded test-document profiles
`DeviceTestPrintRequest` optionally includes `testProfile`. PDF accepts `LETTER`, `A4`, or a `LABEL_4X6_*`, `LABEL_4X2_*` or `LABEL_4X1_*` profile (all profiles of one height produce the same PDF: 101.6 × 152.4, 50.8 or 25.4 mm). ZPL accepts those label profiles with `_203`, `_300` or `_600`; the suffix selects the printer resolution in dots per inch. For the 4 × 2 and 4 × 1 PDFs the server also submits `options.paperWidthMm`/`paperHeightMm` (101.6 × 50.8 or 25.4) so the client sets actual-size job paper; they are never combined with `mediaProfile`. The 4 × 6 PDF and all ZPL labels carry no job paper. These profiles require an agent and server from the same unreleased 0.4 cohort; older agents reject job paper. Use possible-value selectors in operator interfaces and match the loaded stock and device resolution. The profile defines document layout; it does not reconfigure Windows paper stock.
`DeviceTestPrintRequest` optionally includes `testProfile`. PDF accepts `LETTER`, `A4`, or a `LABEL_4X6_*`, `LABEL_4X2_*` or `LABEL_4X1_*` profile (all profiles of one height produce the same PDF: 101.6 × 152.4, 50.8 or 25.4 mm). ZPL accepts those label profiles with `_203`, `_300` or `_600`; the suffix selects the printer resolution in dots per inch. For the 4 × 2 and 4 × 1 PDFs the server also submits `options.paperWidthMm`/`paperHeightMm` (101.6 × 50.8 or 25.4) so the client sets actual-size job paper; they are never combined with `mediaProfile`. The 4 × 6 PDF and all ZPL labels carry no job paper. These profiles are included in the stable cohort above; older agents may reject job paper. Use possible-value selectors in operator interfaces and match the loaded stock and device resolution. The profile defines document layout; it does not reconfigure Windows paper stock.
Requests that omit the field retain compatible defaults: PDF uses Letter, ZPL uses 4 × 6 at 203 dpi. An explicit null, unknown profile, or Letter/A4 with ZPL is invalid. A stable requestId protects transport retries; changing document media is a new operator request. Each action queues one test on each explicitly selected destination, subject to the existing exposure and approval checks. No discovery or connection event automatically prints.
+2 -2
View File
@@ -2,6 +2,6 @@
This repository specifies the QQQ printing wire boundary. Implementations must authenticate every device operation, bind jobs and content to the enrolled device, restrict administration separately, and avoid automatically reprinting uncertain submissions.
Report vulnerabilities privately using [GitHub private vulnerability reporting](https://github.com/QRun-IO/qqq-print-protocol/security/advisories/new). If that channel is unavailable, contact `contact@kingsrook.com`. Do not publish working device tokens, enrollment codes, customer print content or exploit details in a public issue.
Report vulnerabilities privately using [GitHub private vulnerability reporting](https://github.com/QRun-IO/qqq-print-protocol/security/advisories/new). If that channel is unavailable, contact `contact@kingsrook.com`. This private reporting channel remains available while [Gitea](https://git.kof22.com/QRun-IO/qqq-print-protocol) is primary for source, pull requests and releases. Do not publish working device tokens, enrollment codes, customer print content or exploit details in a public issue on either forge.
Fixes target the current prerelease and protocol v1 consumers; there is no separate legacy support branch. TLS, token storage, host permissions and printer-command restrictions are implemented and assessed in the server and Windows repositories.
Fixes target stable protocol 1.0.0 and supported wire-v1 consumers; there is no separate legacy support branch. TLS, token storage, host permissions and printer-command restrictions are implemented and assessed in the [server](https://git.kof22.com/QRun-IO/qbit-printing) and [Windows](https://git.kof22.com/QRun-IO/qqq-print-agent) repositories. Native printing requires the current QQQ user session plus secondary device proof; a transport failure never permits an authentication or polling fallback.
+5
View File
@@ -1,5 +1,10 @@
# Workflow compatibility — 2026-10-30
Historical decision retained unchanged below. Protocol GitHub Actions were
retired on 2026-11-04 at 01:11 UTC; Gitea is now the primary CI and release
service. This snapshot does not describe current upstream tool versions or a
new hosted result. See [current publication and fixture provenance](INTEGRATION.md#stable-printing-release).
The `zizmor/self-repository` advisory recommends GitHub's newer `$/` syntax. This repository currently uses `./.github/workflows/...` **only for job-level reusable workflow calls**. GitHub documents that both forms select the same commit as the calling workflow. These are not step-level local actions that resolve from a potentially different workspace checkout.
The latest released actionlint, **1.7.12**, rejects `$/` reusable workflow calls. This was reproduced locally with the pinned version; the unchanged `./` calls pass. The upstream compatibility issue is still open. Retaining the supported job-level form preserves full actionlint validation of the called workflow, including its inputs and permissions, without suppressing the syntax rule or disabling any security check.
+17 -14
View File
@@ -1,6 +1,6 @@
# Device controls — additive wire v1
For the coordinated printing extension 2.0 implementation, these domain state,
For the stable printing cohort (native extension 2.3.0), these domain state,
command and receipt semantics use [direct realtime frames](REALTIME.md).
The HTTP polling routes below describe the earlier 1.0/explicit compatibility
transport and are never a fallback for a realtime client.
@@ -8,13 +8,14 @@ transport and are never a fallback for a realtime client.
This extension provides server dispatch controls and two bounded Windows agent
commands. Its additive payloads preserve existing print job states and old
request bodies. These domain controls were published in package `0.2.0-rc.1`;
their realtime transport is part of release candidate `0.3.0-rc.1`. This contract does
not establish deployment or production readiness.
their realtime transport was introduced in `0.3.0-rc.1` and is included in
[stable protocol 1.0.0](INTEGRATION.md#stable-printing-release). Deployment and physical output evidence
belong to the implementation repositories.
## Discovery and routes
The primary transport is the discovered QQQ semantic extension at
`/qqq/v1/qbit/printing`, extension version `1.0.0`, using channel
`/qqq/v1/qbit/printing`, extension version `>=2.0.0 <3.0.0`, using channel
`api/qqq-print-agent`. Standard QQQ session authentication and current user,
tenant, table, process and record permissions apply. Device calls additionally
send `X-QQQ-Device-Id` and `X-QQQ-Device-Token`; that secondary proof does not
@@ -51,7 +52,7 @@ a missing capability or 404 is a failure, never permission to bypass saved state
Native printing extension `1.0.0` includes these controls; a native-route 404 is
always a configuration or compatibility failure, never legacy discovery.
The new poll reports `controlVersion:1`, `agentVersion`, `capabilities`, and
The historical compatibility poll reports `controlVersion:1`, `agentVersion`, `capabilities`, and
`lastAppliedRevision`. The two capability names are `REFRESH_INVENTORY` and
`CANCEL_SPOOL`; an empty list is valid for a state-only agent. A successful poll
establishes state-control support independently of these command capabilities.
@@ -73,19 +74,20 @@ and `CONNECTED`. Changes are committed atomically with their action record and
advance the revision. Revisions are nonnegative JSON-safe integers, at most
9007199254740991. A device reports a revision only after applying and durably
retaining that state; it cannot acknowledge a future server revision. The agent
must not replace newer persisted state with an older poll response.
must not replace newer persisted state with an older received revision.
The control loop runs independently of printing. Pausing or disconnecting gates
new claims and dispatch. It does not cancel CLAIMED or later work, erase local
journals, withdraw exposure selections, or discard in-flight job events. Claims
and server state changes are serialized so work claimed before the change remains
in flight. Before first synchronization or after restart, the agent retains the
last state and withholds new claims until valid state or explicit legacy support
has been established.
last state and withholds new claims until authenticated realtime synchronization
has completed. Historical clients instead used explicit legacy support discovery.
`DISCONNECTED` stops ordinary heartbeat, inventory publication, and new claims.
Authenticated control polling, command receipts, and in-flight job events remain
available, so `RECONNECT` can reach the device. `REFRESH_INVENTORY` waits for
The authenticated socket, command receipts, and in-flight job events remain
available, so `RECONNECT` can reach the device; historical clients used control
polling for this purpose. `REFRESH_INVENTORY` waits for
`CONNECTED` or expires; it must not report success without publishing inventory.
Explicit `CANCEL_SPOOL` remains available over the control path while disconnected.
Reconnecting does not clear `dispatchPaused`; resuming does not change connection
@@ -150,10 +152,11 @@ history or document retention.
Remote actions initially return `PENDING`. `DeviceCommand` carries stable
`commandId`, `requestId`, `action`, `createdAt`, `expiresAt`, and, for spool cancel,
`jobId`. Server-created expiry is 15 minutes after creation. One poll returns at
most 20 commands and `serverTime`, `pollIntervalSeconds` (1–300), and the full
desired state. Pending commands may be delivered repeatedly until a receipt or
expiry; they are not new commands on every poll. Do not deliver expired commands.
`jobId`. Server-created expiry is 15 minutes after creation. The realtime server
delivers `COMMAND` frames and replays unexpired pending commands after reconnect.
The historical poll returns at most 20 commands and `serverTime`,
`pollIntervalSeconds` (1–300), and the full desired state. Repeated delivery does
not create a new command. Do not deliver expired commands.
The agent journals command identity and execution state before a side effect.
Use the current enrollment/connection scope. It checks expiry before beginning,
+65 -7
View File
@@ -1,14 +1,72 @@
# Consumer integration
## Stable printing release
The current primary release is [protocol 1.1.0](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.1.0) (annotated tag `v1.1.0` at source `5005f3c9e27053b6c327bb923695012832c0fdcc`, bundle `qqq-print-protocol-1.1.0.tar.gz` SHA-256 `541f0f3054cbfe9ce16e7dea58fad01b55a7f0e9246ade8f30df1bb826940703`), adding printing extension 2.4.0's `printer-approval-v1` capability and `PRINTERS` frame. It pairs with [QBit 1.1.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.1.0) and the Windows connector 1.2.1; the 1.0.0 cohort below stays valid for 2.3 peers.
The primary [protocol 1.0.0 release](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.0.0)
is published on Gitea (release record 1344). This is the specification and fixture
bundle, not an application installer.
| Component | Stable cohort |
| --- | --- |
| Protocol distribution | [1.0.0](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.0.0) |
| Windows Print Agent | [1.0.1](https://git.kof22.com/QRun-IO/qqq-print-agent/releases/tag/v1.0.1) |
| Printing QBit | [1.0.0](https://git.kof22.com/QRun-IO/qbit-printing/releases/tag/v1.0.0) |
| QQQ dependency baseline | 4.1.0-RC.21; printing targets QQQ 4.1+ |
| Native printing semantic API | 2.3.0 |
| Wire payload / WebSocket subprotocol | 1 / `qqq-printing.v1` |
Upgrade the server before the matching Windows connector. The connector opens an
authenticated two-way WebSocket from the facility to QQQ. Job notifications,
controls and receipts use that channel; documents stream separately over
same-origin HTTPS. Reconnection is automatic and preserves durable job/receipt
identity. There is no job/control polling fallback. Standard QQQ authentication
and current-user permissions apply throughout.
### Source and fixtures
The annotated `v1.0.0` tag resolves to protocol source commit
`d652fba1317aec7f25c5a464bac15e97d6094e67`. The release changes distribution
versioning, not the wire schema.
The existing consumer fixture pin is
`42c0d249f81395f00408ec0f92638dcbbf5deb03`. This reviewed immutable fixture source
is distinct from the stable release source commit. Retain the pin until a reviewed
consumer change updates it; do not substitute a moving branch or imply the two
commits are identical. The readable GitHub shadow retains that immutable commit
for the Windows fixture bridge.
### Publication and verification scope
Use [Gitea](https://git.kof22.com/QRun-IO/qqq-print-protocol) for development, pull
requests, CI and releases. Protocol GitHub Actions were retired on 2026-11-04 at
01:11 UTC; the shadow remains readable. Historical workflow files, compatibility
decisions and test evidence are retained as history. Their presence is not an
active GitHub publishing path.
Download the release archive and `SHA256SUMS` from the primary release and verify
the archive before extracting it. No npm registry publication is performed.
This documentation closeout introduces no new build, hosted attestation or
hardware-acceptance claim. Schema fixtures establish message shape; installation,
authentication deployment and physical output evidence belong to the consumer
repositories. See [authentication](QQQ-AUTHENTICATION.md),
[realtime transport](REALTIME.md), and [media selection](PRINTER-MEDIA.md).
## Consumer checkout
The coordinated repositories are:
- [qbit-printing](https://github.com/QRun-IO/qbit-printing): QQQ 4.1+ server metadata, durable queue and HTTP endpoints.
- [qqq-print-agent](https://github.com/QRun-IO/qqq-print-agent): Windows AppKit companion, service and printer adapters.
- [qqq-print-protocol](https://github.com/QRun-IO/qqq-print-protocol): this specification and compatibility fixtures.
- [qbit-printing](https://git.kof22.com/QRun-IO/qbit-printing): QQQ 4.1+ server metadata, durable queue, native API and realtime WebSocket endpoint.
- [qqq-print-agent](https://git.kof22.com/QRun-IO/qqq-print-agent): Windows AppKit companion, service and printer adapters.
- [qqq-print-protocol](https://git.kof22.com/QRun-IO/qqq-print-protocol): this specification and compatibility fixtures.
The [stable cohort](INTEGRATION.md#stable-printing-release) is protocol 1.0.0, Windows 1.0.1 and QBit 1.0.0,
with native printing API 2.3.0 and QQQ 4.1.0-RC.21 as its dependency baseline.
For local development, clone them as siblings. The Java HTTP test defaults to `../qqq-print-protocol/fixtures` and accepts `-Dprinting.protocol.fixtures=<directory>`. The Windows tests link shared fixtures from the sibling repository. See each consumer README for its build and platform requirements.
CI must check out this repository at an immutable commit reviewed with the consumer change. A checkout path of `qqq-print-protocol` beside `qbit-printing` or `qqq-print-agent` preserves the local layout. Do not pin a moving branch for release qualification. Maintainers update the pinned commit when adopting a new compatible contract release.
CI must check out this repository at an immutable commit reviewed with the consumer change. A checkout path of `qqq-print-protocol` beside `qbit-printing` or `qqq-print-agent` preserves the local layout. Do not pin a moving branch for release qualification. The current consumer fixture pins are `ac835f8b9396b641b525669249fdb4a1e2a50c99` (Windows, the PR 11 commit) and `5005f3c9e27053b6c327bb923695012832c0fdcc` (QBit, the 1.1.0 source); both carry the 1.1.0 fixtures. Maintainers update the pin through a reviewed consumer change; the readable GitHub shadow retains it for the Windows fixture bridge.
If a consumer builds outside a multi-checkout workspace, download the release archive, verify `SHA256SUMS`, extract it and point its tests at `fixtures/`. Consume `schemas/protocol.schema.json#/$defs/<Type>` with a JSON Schema 2020-12 validator.
@@ -18,7 +76,7 @@ The coordinated realtime client requires printing semantic extension
`>=2.0.0 <3.0.0` and the authenticated `qqq-printing.v1` WebSocket subprotocol.
Wire payload version remains 1. [REALTIME.md](REALTIME.md) defines the new frames,
configurable policy, streaming limits and runtime acceptance expectations.
The release candidate is [0.3.0-rc.1](https://github.com/QRun-IO/qqq-print-protocol/releases/tag/v0.3.0-rc.1).
This transport is included in [stable protocol 1.0.0](INTEGRATION.md#stable-printing-release).
Upgrade the server before the Windows connector; fixtures do not establish installed behavior.
Historical HTTP wire protocol v1 is selected by the `/printing/v1` path and enrollment response `protocolVersion: 1`. Published distribution version `0.2.0-rc.1` identifies the specification/fixture bundle; it does not change that wire version. The Windows and QBit release numbers are independent of both. Native clients use the discovered printing extension semantic version; the realtime contract above adds its explicit version/capability gate without redefining core discovery.
@@ -37,11 +95,11 @@ Claim acknowledgment means local receipt; SPOOLED means Windows accepted the doc
## Checks and artifacts
```sh
npm ci
npm ci --ignore-scripts
npm run check
npm run bundle
```
The check runs full draft-2020-12 schema compilation and fixture validation with Ajv, plus independent content/hash/base64 and invalid-input checks. `node tools/verify-fixtures.mjs` remains available without installing dependencies for a quick subset check.
The bundle includes specification, schema, fixtures, tools, locked validator dependencies and license metadata, excluding installed dependencies. Consumers do not need Node.js to use the schema or fixture JSON. Maintainers publish the bundle as a GitHub release asset; no npm registry publication is performed.
The bundle includes specification, schema, fixtures, tools, dependency manifests and lockfile, and license metadata, excluding installed dependencies. Consumers do not need Node.js to use the schema or fixture JSON. Gitea CI publishes the bundle and checksum on [Gitea releases](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases) from a `v<version>` tag; no npm registry publication is performed and there is no artifact attestation (Gitea has no equivalent). Protocol GitHub Actions were retired on 2026-11-04; retained files and historical results do not imply a new hosted build or attestation.
+4 -5
View File
@@ -1,10 +1,9 @@
# Printer media contract
This additive contract is under implementation for the Echo integration. It is
not advertised by the installed 0.3.2 client. Existing wire version 1 and the
This additive contract is included in [protocol 1.0.0, Windows 1.0.1 and QBit 1.0.0](INTEGRATION.md#stable-printing-release).
Older 0.3.2 clients do not advertise it. Existing wire version 1 and the
WebSocket subprotocol remain unchanged. The feature minimum is printing extension
2.3.0 (see [VERSIONS.md](VERSIONS.md)); immutable consumer artifacts must be
qualified together before publication. A client must offer `printer-media-v1` only
2.3.0 (see [VERSIONS.md](VERSIONS.md)). A client must offer `printer-media-v1` only
after discovering a stable 2.3+ extension within major two, so a 2.2 (rendering)
server keeps it a three-capability peer. Older 2.x deployments do not promise the
operator routes.
@@ -51,7 +50,7 @@ length, initiating actor, request and destination with the durable acceptance
receipt. Acceptance means a committed queue entry; it does not prove paper output.
Lost responses reconcile the same request key rather than issuing a new request.
The draft native routes below are relative to the discovered printing extension
The native routes below are relative to the discovered printing extension
path, behind the standard QQQ authentication and authorization layers:
| Operation | Contract |
+7 -6
View File
@@ -1,10 +1,11 @@
# Standard QQQ authentication
The new Windows client uses the QQQ semantic UI/UX API. Package versions, wire
The Windows client uses the QQQ semantic UI/UX API. Package versions, wire
payload version `1` and the printing extension semantic version are separate
contracts. The standard-session contract was published in `0.2.0-rc.1`; realtime
transport is part of release candidate `0.3.0-rc.1`. This document does not establish an
accepted Authentik deployment.
contracts. The standard-session contract was introduced in `0.2.0-rc.1` and
realtime transport in `0.3.0-rc.1`; both are included in the [stable cohort](INTEGRATION.md#stable-printing-release).
Provider configuration and server session policy still determine the supported
sign-in and renewal behavior of each deployment.
## Discovery and sign-in
@@ -36,11 +37,11 @@ Missing native grants require server configuration, not a fallback local login.
The desktop callback must match the shared grant and installer registration.
Windows uses the exact private callback
`io.qrun.qqq-print-agent:/oauth/callback`; current central candidate policy rejects
`io.qrun.qqq-print-agent:/oauth/callback`; the shared native grant rejects
HTTP callbacks. The installer registers the callback to a non-elevated forwarding
helper, which delivers it only to an active sign-in attempt in the same Windows
user/session. State and PKCE validation remain mandatory. The implementation,
shared grant and provider deployment must be qualified before Authentik acceptance.
shared grant and provider configuration must agree for Authentik sign-in to work.
## User and device authority
+27 -3
View File
@@ -1,6 +1,7 @@
# Realtime printing — semantic extension 2.0
This is the contract for the `0.3.0-rc.1` realtime release candidate.
This transport was introduced in `0.3.0-rc.1` and is included in the
[stable 1.0.0 contract](INTEGRATION.md#stable-printing-release), whose paired server advertises printing 2.3.0.
Contract publication does not establish installed acceptance. Existing job states, command outcomes,
current-user authorization and durable idempotency rules remain authoritative.
@@ -8,7 +9,7 @@ Native printing extension **2.0.0** makes an authenticated synchronized WebSocke
a prerequisite for claiming jobs. Published extension 1.0 clients could claim
over HTTP alone, so this changes existing behavior and requires a major version
under [Semantic Versioning](https://semver.org/#spec-item-8). A coordinated upgrade
does not make that change backward compatible. Product `0.3.0-rc.1`, framework
does not make that change backward compatible. Product releases, framework
`/qqq/v1`, wire payload v1 and subprotocol `qqq-printing.v1` remain independent.
## Discovery and connection
@@ -31,7 +32,9 @@ The client opens one persistent WebSocket to the configured QQQ origin:
`X-QQQ-Device-Id`, `X-QQQ-Device-Token`, `X-QQQ-Client: api/qqq-print-agent`
and the canonical same-origin `Origin`.
- Capability: `realtime-v1` is mandatory in `HELLO`. Optional command capabilities
are the existing `REFRESH_INVENTORY` and `CANCEL_SPOOL`.
are the existing `REFRESH_INVENTORY` and `CANCEL_SPOOL`; optional feature
capabilities are `printer-media-v1` (extension 2.3.0) and `printer-approval-v1`
(extension 2.4.0), each gated by [discovery](VERSIONS.md#negotiation-rules).
Do not add credentials to URLs, subprotocols, logs or frame payloads. Refuse
cross-origin redirects and arbitrary content URLs. The existing session is
@@ -67,6 +70,7 @@ compression that could bypass decoded-size limits are not supported.
| Server → client | DESIRED_STATE | Existing `DeviceDesiredState` directly |
| Server → client | SETTINGS | `RealtimeSettings` directly |
| Server → client | JOB_AVAILABLE | Empty object; a coalescible wake-up, not a job or claim |
| Server → client | PRINTERS | `printers`: complete `printerId`/`enabled` approval of the latest accepted inventory; only after `printer-approval-v1` |
| Server → client | COMMAND | Existing `DeviceCommand` directly |
| Client → server | JOB_EVENT | `jobId`, existing `JobEvent` as `event` |
| Client → server | COMMAND_RECEIPT | `commandId`, existing `DeviceCommandReceipt` as `receipt` |
@@ -96,6 +100,26 @@ refreshing inventory or querying jobs/controls. Only one serialized writer owns
socket sends; the receive/control path must not await document download,
rendering, printer submission or UI work.
### Printer approval
Extension 2.4.0 adds `printer-approval-v1`. A client advertises it only after
discovering a stable 2.4+ extension within major two. The server then sends
PRINTERS (`RealtimePrinters`) on that connection, through the same serialized
writer, after it has processed an accepted INVENTORY from that connection
(after that INVENTORY's ACK) and whenever the approval of any printer belonging
to the device changes by any server-side path. Nothing else triggers it; there
is no timer and the client does not acknowledge it.
The payload lists every printer in the device's latest accepted inventory, for
this device only, as `printerId` plus boolean `enabled`, at most 200 entries.
`enabled:false` means the server knows the printer and has not approved it. A
printer the device no longer exposes is not listed; an empty inventory yields
`{ "printers": [] }`. Each frame replaces the previous one entirely, so frames
are idempotent and coalescible like JOB_AVAILABLE. The frame is not a durable
event and carries no job or document data. A client that did not advertise the
capability never receives it and keeps reading its existing authorized catalog
for approval; a server never sends it to a connection that did not advertise it.
## Ordering, synchronization and replay
1. Authenticate the connection, then require HELLO before other application work.
+24 -2
View File
@@ -1,6 +1,28 @@
# Session state — 2026-11-01
# Session state — 2026-11-04
## Release-cohort integration (branch `release/printing-cohort-20261101`)
## Current stable release
Protocol [1.0.0](https://git.kof22.com/QRun-IO/qqq-print-protocol/releases/tag/v1.0.0)
is published on primary Gitea (release 1344), from tagged source
`d652fba1317aec7f25c5a464bac15e97d6094e67`. It pairs with Windows 1.0.1 and QBit 1.0.0,
native printing API 2.3.0, wire v1 and QQQ dependency baseline 4.1.0-RC.21.
Consumer fixtures remain pinned to `42c0d249f81395f00408ec0f92638dcbbf5deb03`,
a separate reviewed source commit. See [release details](INTEGRATION.md#stable-printing-release).
The current transport is client-initiated authenticated two-way WebSocket with
independent streamed HTTPS documents and automatic reconnect; no polling fallback.
Protocol GitHub Actions were retired at 2026-11-04 01:11 UTC. The readable shadow
and immutable fixture commit remain for the Windows fixture bridge.
This documentation-only closeout updates release guidance, primary links,
authentication/media wording and historical status labels. No schema, fixture,
runtime or workflow changed; no new build, hosted attestation or hardware test is
claimed. Earlier checkpoint statements below retain their original dates and
statuses and are not the current backlog.
## Historical checkpoints
### Release-cohort integration — 2026-11-01 (branch `release/printing-cohort-20261101`)
Cohort: protocol package `1.0.0` (source prepared, not yet published; `0.4.0-rc.1` is the
latest published), wire v1, native printing extension 2.3.0. This branch integrates the script-quality tooling (locked Ruff/Pyright,
+21 -5
View File
@@ -1,18 +1,34 @@
# TODO
## Script quality gates
## Current protocol closeout — 2026-11-04
- [x] Publish stable protocol 1.0.0 on primary Gitea (release 1344).
- [x] Document the Windows 1.0.1 / QBit 1.0.0 cohort, native API 2.3.0, wire v1,
current realtime/authentication/media behavior and separate immutable fixture pin.
- [x] Retire protocol GitHub Actions; retain the readable shadow and Windows fixture bridge.
No protocol implementation or publication item remains for this stable release.
Consumer installation and hardware evidence remain in their repositories.
Documentation review and publication are coordinated by the release owner.
## Historical task ledger
The original checkpoint statuses below are preserved as history, not as open
tasks for the published stable release. See [current release details](INTEGRATION.md#stable-printing-release).
### Script quality gates
- [x] Pin Ruff, strict Pyright, ESLint/Stylistic and strict checkJs with locked restores; reject untracked-scope Python/JavaScript and zero-file Pyright runs; negative tool probes pass locally.
- [ ] Observe the new `verify` job steps pass on hosted CI before relying on them.
## Release cohort integration (0.4.0-rc.1, unreleased)
### Release cohort integration (0.4.0-rc.1, unreleased at this checkpoint)
- [x] Integrate short-label (4 × 2 / 4 × 1) test profiles, fixture and job-paper assertions with the strictly typed tools in source.
- [x] Pass the merged-tree `npm run check` (76 definitions / 79 fixtures), `npm run typecheck`, `npm run lint` and `npm run bundle`.
- [ ] Pin the final protocol commit in both consumers, then observe hosted Gitea CI.
- [ ] Complete installed client and physical short-label acceptance separately; schema checks do not establish them.
## Echo media integration
### Echo media integration
- [x] Define bounded queue-default profile and exact revision selection schemas.
- [x] Validate positive fixtures and reject invalid/ambiguous options.
@@ -21,7 +37,7 @@
- [ ] Qualify atomic retained-source acceptance and lost-response reconciliation.
- [ ] Publish the reviewed immutable contract and compatible consumer tuple.
## Realtime 0.3.0-rc.1 release candidate
### Realtime 0.3.0-rc.1 release candidate
- [x] Freeze printing extension 2.0, authenticated realtime frames and independent
streamed HTTPS documents with no polling fallback.
@@ -35,7 +51,7 @@
authorization acceptance before release; schema checks do not establish them.
- [ ] Complete installed UI and physical output acceptance separately.
## Historical 0.2 and earlier implementation checkpoints
### Historical 0.2 and earlier implementation checkpoints
The entries below retain their original checkpoint status. They do not describe
the current realtime release's completion or publication.
+18 -8
View File
@@ -1,9 +1,11 @@
# Extension version registry
Three version numbers are independent. Do not compare them.
Distribution, native API, wire and subprotocol versions are independent. Do not
compare them as if they were one version sequence. See the [stable cohort](INTEGRATION.md#stable-printing-release).
| Number | Meaning | Current |
| --- | --- | --- |
| Protocol distribution | Version of the specification and fixture archive | 1.1.0 (unreleased; 1.0.0 published) |
| Wire payload version | Shape of the JSON messages in this repository | 1 |
| WebSocket subprotocol | Framing and message envelope | `qqq-printing.v1` |
| Printing semantic extension | Server feature level discovered at `/qqq/v1/version` | see below |
@@ -18,24 +20,32 @@ additive and keeps the earlier behavior.
| 2.0.0 | Authenticated realtime channel is required for native claims; no polling fallback | [REALTIME.md](REALTIME.md) |
| 2.1.0 | Record, table-list and text print processes | server documentation |
| 2.2.0 | Render-to-PDF process, header and footer values, content sections. No media negotiation | server documentation |
| 2.3.0 (unreleased, reserved) | Least-privilege operator catalog, submission and request lookup; reported queue-default media and `printer-media-v1`; opt-in retained-source facade | [PRINTER-MEDIA.md](PRINTER-MEDIA.md) |
| 2.3.0 | Least-privilege operator catalog, submission and request lookup; reported queue-default media and `printer-media-v1`; opt-in retained-source facade | [PRINTER-MEDIA.md](PRINTER-MEDIA.md) |
| 2.4.0 | `printer-approval-v1` and the server → client `PRINTERS` frame reporting each exposed printer's approval | [REALTIME.md](REALTIME.md#printer-approval) |
## Negotiation rules
- A client advertises a capability only when it supports it **and** the discovered
extension is a stable `major.minor.patch` version in major 2 at or above the
feature minimum. `printer-media-v1` has minimum 2.3.0.
feature minimum. `printer-media-v1` has minimum 2.3.0; `printer-approval-v1`
has minimum 2.4.0.
- A client facing 2.0, 2.1 or 2.2 stays a three-capability peer (`realtime-v1`,
`REFRESH_INVENTORY`, `CANCEL_SPOOL`) and sends no media inventory.
`REFRESH_INVENTORY`, `CANCEL_SPOOL`) and sends no media inventory. A client
facing 2.3 is at most a four-capability peer and receives no `PRINTERS` frame.
- A server sends `PRINTERS` only on a connection whose HELLO advertised
`printer-approval-v1`, whatever extension version it serves; a client that did
not advertise it keeps its own approval fallback.
- A server must not let a client that did not advertise `printer-media-v1` claim a
job that carries a media selection, whatever extension version that client
discovered.
- Capabilities are re-evaluated on each new socket, so a server upgrade from 2.0
to 2.3, or a downgrade from 2.3 to 2.2, takes effect at the next reconnect
to 2.4, or a downgrade from 2.4 to 2.2, takes effect at the next reconnect
without a polling schedule.
- Pre-release, ambiguous or malformed versions (`2.3.0-rc.1`, `2.3`, `02.3.0`,
four-part values) never enable a feature.
The 2.3.0 number is reserved for the unreleased operator, media and retained-source
feature set. It is not a published release until consumer artifacts are qualified
together.
QBit 1.0.0 advertises native printing extension 2.3.0. Windows 1.0.1 is the paired
stable client; their product versions do not replace capability discovery.
QBit 1.1.0 advertises 2.4.0 and pairs with Windows 1.2.1; a 2.4.0 server still
serves 1.0.1/1.1.x clients as four-capability peers, and a 1.2.1 client facing
2.3.0 does not advertise `printer-approval-v1`.
@@ -0,0 +1,5 @@
{
"name": "printing",
"version": "2.4.0",
"pathPrefix": "/qbit/printing"
}
@@ -0,0 +1,27 @@
{
"type": "HELLO",
"messageId": "hello-connection-1",
"payload": {
"agentVersion": "1.2.0",
"capabilities": [
"realtime-v1",
"REFRESH_INVENTORY",
"CANCEL_SPOOL",
"printer-media-v1",
"printer-approval-v1"
],
"lastAppliedRevision": 0,
"lastSettingsRevision": 0,
"limits": {
"maxPdfBytes": 536870912,
"maxZplBytes": 104857600,
"maxFrameBytes": 16777216,
"maxQueuedFrames": 4096,
"maxQueuedFrameBytes": 67108864,
"maxConcurrentDownloads": 16,
"maxConcurrentPrints": 16,
"maxStagedBytes": 4294967296,
"maxCopies": 32767
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"type": "PRINTERS",
"messageId": "printers-connection-1",
"payload": {
"printers": [
{
"printerId": "laser-example",
"enabled": true
},
{
"printerId": "label-example",
"enabled": false
}
]
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@qrun-io/qqq-print-protocol",
"version": "1.0.0",
"version": "1.1.1-rc.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qrun-io/qqq-print-protocol",
"version": "1.0.0",
"version": "1.1.1-rc.1",
"license": "Apache-2.0",
"devDependencies": {
"@eslint/js": "10.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qrun-io/qqq-print-protocol",
"version": "1.0.0",
"version": "1.1.1-rc.1",
"private": true,
"description": "Versioned wire contract and compatibility fixtures for QQQ cloud printing.",
"license": "Apache-2.0",
+65 -5
View File
@@ -993,7 +993,8 @@
"enum": [
"REFRESH_INVENTORY",
"CANCEL_SPOOL",
"printer-media-v1"
"printer-media-v1",
"printer-approval-v1"
]
},
"ProtocolVersionResponse": {
@@ -1060,7 +1061,7 @@
"items": {
"$ref": "#/$defs/ControlCapability"
},
"maxItems": 3,
"maxItems": 4,
"uniqueItems": true
},
"lastPollAt": {
@@ -1099,7 +1100,7 @@
"items": {
"$ref": "#/$defs/ControlCapability"
},
"maxItems": 3,
"maxItems": 4,
"uniqueItems": true
},
"lastAppliedRevision": {
@@ -1718,11 +1719,12 @@
"realtime-v1",
"REFRESH_INVENTORY",
"CANCEL_SPOOL",
"printer-media-v1"
"printer-media-v1",
"printer-approval-v1"
]
},
"minItems": 1,
"maxItems": 4,
"maxItems": 5,
"uniqueItems": true,
"contains": {
"const": "realtime-v1"
@@ -2068,6 +2070,61 @@
},
"additionalProperties": false
},
"PrinterApprovalState": {
"description": "Server approval of one printer in the device's latest accepted inventory; enabled:false means known but not approved.",
"type": "object",
"required": [
"printerId",
"enabled"
],
"properties": {
"printerId": {
"$ref": "#/$defs/Id"
},
"enabled": {
"type": "boolean"
}
},
"additionalProperties": true
},
"RealtimePrintersPayload": {
"description": "Complete approval state of every printer the device currently exposes; each frame replaces the previous one.",
"type": "object",
"required": [
"printers"
],
"properties": {
"printers": {
"type": "array",
"items": {
"$ref": "#/$defs/PrinterApprovalState"
},
"maxItems": 200
}
},
"additionalProperties": true
},
"RealtimePrinters": {
"description": "Sent only on connections whose HELLO advertised printer-approval-v1; not acknowledged.",
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "PRINTERS"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimePrintersPayload"
}
},
"additionalProperties": false
},
"RealtimeCommand": {
"type": "object",
"required": [
@@ -2184,6 +2241,9 @@
{
"$ref": "#/$defs/RealtimeJobAvailable"
},
{
"$ref": "#/$defs/RealtimePrinters"
},
{
"$ref": "#/$defs/RealtimeCommand"
},
+3
View File
@@ -4,6 +4,7 @@
// Shared correspondence between wire examples and schema definitions.
export const fixtureTypes = {
"printing-realtime-extension.discovery.json": "PrintingRealtimeExtensionDiscovery",
"printing-realtime-extension-240.discovery.json": "PrintingRealtimeExtensionDiscovery",
"version-legacy.response.json": "ProtocolVersionResponse",
"version-controls.response.json": "ProtocolVersionResponse",
"control-poll.request.json": "DeviceControlPollRequest",
@@ -82,4 +83,6 @@ export const fixtureTypes = {
"operator-printers.response.json": "OperatorPrinterList",
"operator-submit.request.json": "OperatorSubmitJobRequest",
"realtime-media-hello.message.json": "RealtimeHello",
"realtime-approval-hello.message.json": "RealtimeHello",
"realtime-printers.message.json": "RealtimePrinters",
};
+40 -1
View File
@@ -27,6 +27,10 @@ for(const version of ["1.0.0", "1.1.0", "3.0.0", "2.0", "2.01.0", "2.0.0-rc.1",
}
assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, name: "another-extension" }));
assert(!valid("PrintingRealtimeExtensionDiscovery", { ...extension, pathPrefix: "/printing/v1" }));
const approvalExtension = fixture("printing-realtime-extension-240.discovery.json");
assert(valid("PrintingRealtimeExtensionDiscovery", approvalExtension), ajv.errorsText());
assert.equal(approvalExtension["version"], "2.4.0", "printer-approval-v1 has feature minimum 2.4.0.");
assert.deepEqual({ ...approvalExtension, version: extension["version"] }, extension, "2.4.0 keeps the 2.x descriptor name and path prefix.");
/*******************************************************************************
** Cross-field policy relationships require semantic checks beyond JSON Schema.
@@ -91,8 +95,23 @@ assert(!valid("RealtimeHello", { ...hello, messageId: "hello:unsafe-prefix" }));
assert(!valid("RealtimeHello", { ...hello, token: "not-a-real-token" }));
assert(!valid("RealtimeClientMessage", { type: "RUN_SHELL", messageId: "unsupported-1", payload: {} }));
/*******************************************************************************
** Capability hellos: the 2.3.0 four-capability and 2.4.0 five-capability peers
** both remain valid; the list is bounded, unique and closed.
*******************************************************************************/
const mediaHello = /** @type {typeof hello} */ (/** @type {unknown} */ (fixture("realtime-media-hello.message.json")));
const approvalHello = /** @type {typeof import("../fixtures/realtime-approval-hello.message.json")} */ (/** @type {unknown} */ (fixture("realtime-approval-hello.message.json")));
assert(valid("RealtimeHello", mediaHello), ajv.errorsText());
assert(valid("RealtimeHello", approvalHello), ajv.errorsText());
assert.equal(approvalHello.payload.capabilities.length, 5);
assert(approvalHello.payload.capabilities.includes("printer-approval-v1"));
assert.deepEqual(approvalHello.payload.limits, hello.payload.limits, "Approval negotiation does not change declared ceilings.");
assert(!valid("RealtimeHello", { ...approvalHello, payload: { ...approvalHello.payload, capabilities: [...approvalHello.payload.capabilities, "printer-approval-v1"] } }), "Duplicate capability accepted");
assert(!valid("RealtimeHello", { ...approvalHello, payload: { ...approvalHello.payload, capabilities: [...approvalHello.payload.capabilities, "printer-approval-v2"] } }), "Unknown sixth capability accepted");
assert(!valid("RealtimeHello", { ...hello, payload: { ...hello.payload, capabilities: ["printer-approval-v1"] } }), "realtime-v1 remains mandatory");
const clientFixtures = ["hello", "state-applied", "job-event", "command-receipt", "heartbeat", "inventory", "ping"];
const serverFixtures = ["welcome", "desired-state", "job-available", "command", "settings", "ack", "error", "pong"];
const serverFixtures = ["welcome", "desired-state", "job-available", "printers", "command", "settings", "ack", "error", "pong"];
for(const [files, accepted, rejected] of /** @type {[string[],string,string][]} */ ([
[clientFixtures, "RealtimeClientMessage", "RealtimeServerMessage"],
[serverFixtures, "RealtimeServerMessage", "RealtimeClientMessage"],
@@ -110,6 +129,26 @@ for(const [files, accepted, rejected] of /** @type {[string[],string,string][]}
assert(!valid(accepted, { ...message, contentUrl: "https://unrelated.invalid/document" }));
}
}
/*******************************************************************************
** PRINTERS carries the complete approval state of the latest accepted inventory;
** entries are bounded, both fields are required and the envelope stays closed.
*******************************************************************************/
const printers = /** @type {typeof import("../fixtures/realtime-printers.message.json")} */ (/** @type {unknown} */ (fixture("realtime-printers.message.json")));
assert.equal(printers.payload.printers.length, 2);
assert.deepEqual(printers.payload.printers.map(printer => printer.enabled), [true, false], "Fixture shows one approved and one awaiting printer.");
assert(valid("RealtimePrinters", { ...printers, payload: { printers: [] } }), "An empty inventory yields an empty approval list.");
assert(valid("RealtimePrinters", { ...printers, payload: { printers: Array(200).fill(printers.payload.printers[0]) } }));
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: Array(201).fill(printers.payload.printers[0]) } }));
assert(valid("RealtimePrinters", { ...printers, payload: { ...printers.payload, futureField: true } }), "Additive payload fields remain compatible.");
assert(valid("RealtimePrinters", { ...printers, payload: { printers: [{ ...printers.payload.printers[0], futureField: true }] } }));
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ printerId: "laser-example" }] } }), "enabled is required");
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ enabled: true }] } }), "printerId is required");
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ printerId: "laser-example", enabled: "true" }] } }), "enabled must be boolean");
assert(!valid("RealtimePrinters", { ...printers, payload: { printers: [{ printerId: "", enabled: true }] } }));
assert(!valid("RealtimePrinters", { ...printers, payload: {} }), "printers list is required");
assert(!valid("RealtimePrinters", { ...printers, type: "JOB_AVAILABLE" }));
assert(!valid("RealtimeJobAvailable", { ...printers, type: "JOB_AVAILABLE" }), "PRINTERS payload is not the empty wake-up");
const event = fixture("realtime-job-event.message.json");
assert.deepEqual(fixture("realtime-job-event-replay.message.json"), event, "Lost ACK replay preserves the exact envelope.");
assert.equal((/** @type {typeof import("../fixtures/realtime-ack.message.json")} */ (/** @type {unknown} */ (fixture("realtime-ack.message.json")))).payload.acknowledgedMessageId, event["messageId"]);
+2
View File
@@ -77,6 +77,8 @@ const poll = control("control-poll.request.json");
assert(!validControl("DeviceControlPollRequest", { ...poll, controlVersion: 2 }));
assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["CANCEL_SPOOL", "CANCEL_SPOOL"] }));
assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["RESTART_SERVICE"] }));
assert(validControl("DeviceControlPollRequest", { ...poll, capabilities: ["REFRESH_INVENTORY", "CANCEL_SPOOL", "printer-media-v1", "printer-approval-v1"] }), "Historical control poll accepts every non-realtime capability.");
assert(!validControl("DeviceControlPollRequest", { ...poll, capabilities: ["REFRESH_INVENTORY", "CANCEL_SPOOL", "printer-media-v1", "printer-approval-v1", "printer-approval-v1"] }));
assert(!validControl("DeviceControlPollRequest", { ...poll, lastAppliedRevision: -1 }));
assert(!validControl("DeviceControlPollRequest", { ...poll, lastAppliedRevision: 9007199254740992 }));
const pollResult = /** @type {typeof import("../fixtures/control-poll.response.json")} */ (/** @type {unknown} */ (control("control-poll.response.json")));