Compare commits

...
Author SHA1 Message Date
forge-recovery e5af4b3028 Merge pull request 'Import existing pre-cutoff GitHub work into Gitea' (#7) from migration/cutoff-40cb6fec6dc4-b21637 into dependabot/github_actions/github/codeql-action/init-4.39.1 2026-11-02 00:34:42 +00:00
dependabot[bot] 40cb6fec6d chore(deps): bump github/codeql-action/init from 4.38.3 to 4.39.1
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: Shared Node CI / verify (pull_request) Failing after 10s
QRun-IO/ci-policy: Shared Node CI / secrets (pull_request) Successful in 11s
QRun-IO/ci-policy: Shared Node CI / gate (pull_request) Failing after 10s
QRun-IO/ci-policy: ci / node-4 (pull_request) Failing after 2m11s
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 4s
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.38.3 to 4.39.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-11-01 18:25:43 +00:00
james.maesandCodex eea7f23556 chore: enforce owned source copyright notices (#13)
* chore(standards): preserve owned-source copyright notices

Add accurate repository copyright and license comments to 14 owned source/build/configuration files. Preserve existing holders, upstream wrappers, legal payloads and schema/data exemptions. Exact inverse-byte reconstruction and applicable parser/token/AST checks prove comment-only changes; existing relevant regressions pass.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

* chore: enforce owned source copyright notices

Check real leading comments against explicit repository provenance and retain existing native style gates.

Co-Authored-By: Codex GPT-6 <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>

---------

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex (GPT-6) <noreply@openai.com>
2026-11-01 12:22:58 -06:00
james.maes f3cbc02e9b Merge pull request #12 from QRun-IO/feat/realtime-printing-20261031
Protocol CI and release / verify (push) Failing after 24s
security / workflows (push) Canceled after 0s
security / sca (push) Canceled after 0s
security / secrets (push) Canceled after 0s
security / security (push) Canceled after 0s
Protocol CI and release / release-security (push) Canceled after 0s
codeql / analyze (javascript-typescript) (push) Canceled after 0s
codeql / analyze (actions) (push) Canceled after 0s
codeql / codeql (push) Canceled after 0s
Protocol CI and release / release-codeql (push) Canceled after 0s
Protocol CI and release / release (push) Canceled after 0s
feat: define real-time printing API 2.0
2026-10-31 18:03:46 -05:00
james.maesandCodex b33c7c4cf3 docs(release): prepare realtime API 2.0 candidate notes
Document the coordinated 0.3.0-rc.1 release, server-first upgrade and wire-v1
compatibility without claiming installed or physical acceptance.

Validation: changed Markdown and diff checks pass; protocol schema and fixtures
are unchanged.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 17:59:31 -05:00
james.maesandCodex df3729ac63 fix(protocol): use major version for mandatory realtime claims
Advertise printing extension 2.0.0 because requiring a synchronized channel
breaks the published HTTP-only native claim contract. Preserve product0.3,
framework /qqq/v1 and wire/subprotocolv1. Add a strict standard extension
entry fixture and compatibility checks without redefining core discovery.

Validation: 71 schema definitions and73 fixtures, bundle and Markdown gates pass.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 16:27:50 -05:00
james.maesandCodex 67be986cb7 feat(protocol): define realtime printing transport and configurable limits
Specify authenticated outbound WebSockets, direct state and command delivery,
durable receipt replay and independent streamed document transfers. Add the
1.1 contract, strict fixtures and configurable operating policy for the
coordinated 0.3.0 prerelease.

Validation: 70 schema definitions and 72 fixtures; bundle and changed Markdown checks pass.

Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 16:08:17 -05:00
james.maes a35c083db7 Merge pull request #11 from QRun-IO/docs/published-020-20261031
docs: document the published 0.2.0 prerelease
2026-10-31 11:37:53 -05:00
james.maesandCodex 45ca5483b2 docs: document the published 0.2.0 prerelease
Co-Authored-By: Codex (GPT-6) <noreply@openai.com>
Signed-off-by: James Maes <james@kof22.com>
2026-10-31 11:29:17 -05:00
dependabot[bot] b4e5b5304a chore(deps): bump github/codeql-action/init from 4.38.3 to 4.39.1
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.38.3 to 4.39.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.39.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-31 01:11:54 +00:00
48 changed files with 2030 additions and 25 deletions

No files matched your search

+3
View File
@@ -1,3 +1,6 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
version: 2
updates:
- package-ecosystem: npm
+3
View File
@@ -1,3 +1,6 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
"""Reject missing/malformed CodeQL evidence before enforcing the severity gate."""
import json
import math
+128
View File
@@ -0,0 +1,128 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
"""Check leading notices in explicitly owned, Git-tracked source families."""
import fnmatch
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
def selected(name: str, policy: dict[str, Any]) -> bool:
"""Select owned paths, honoring documented exact/pattern exclusions first."""
if any(fnmatch.fnmatchcase(name, item["pattern"]) for item in policy["exclusions"]):
return False
return any(fnmatch.fnmatchcase(name, pattern) for pattern in policy["includes"])
def leading_comments(text: str, style: str) -> str:
"""Read comments only, preserving required interpreter/XML directive ordering."""
text = text.removeprefix("\ufeff")
if text.startswith("<?xml"):
declaration = re.match(r"<\?xml[^?]*\?>", text)
if style != "xml" or declaration is None:
raise ValueError("Invalid XML declaration")
text = text[declaration.end():]
elif text.startswith("#!"):
if style not in ("hash", "powershell", "slash"):
raise ValueError("Unexpected interpreter directive")
text = text.partition("\n")[2]
while re.match(r"#Requires\b", text, re.IGNORECASE):
if style != "powershell":
raise ValueError("Unexpected PowerShell directive")
text = text.partition("\n")[2]
comments = []
while text.strip():
text = text.lstrip()
if text.startswith("<?xml") or re.match(r"#!|#Requires\b", text, re.IGNORECASE):
raise ValueError("Required declaration must precede the notice")
if style in ("hash", "powershell") and text.startswith("#"):
line, _, text = text.partition("\n")
comments.append(line[1:])
elif style == "slash" and text.startswith("//"):
line, _, text = text.partition("\n")
comments.append(line[2:])
elif ((style == "slash" and text.startswith("/*"))
or (style == "xml" and text.startswith("<!--"))
or (style == "powershell" and text.startswith("<#"))):
opening, closing = {"slash": ("/*", "*/"), "xml": ("<!--", "-->"), "powershell": ("<#", "#>")}[style]
body, separator, rest = text[len(opening):].partition(closing)
if not separator:
raise ValueError("Unterminated leading comment")
comments.append(body)
text = rest
else:
break
return "\n".join(re.sub(r"^\s*[*~]\s?", "", line).strip() for line in "\n".join(comments).splitlines())
def check_notice(name: str, text: str, policy: dict[str, Any]) -> None:
"""Require an approved holder/license pair in real leading comment syntax."""
suffix = Path(name).suffix
style = policy["styles"].get(suffix)
if Path(name).name.startswith("Dockerfile."):
style = "hash"
if style is None:
raise ValueError("Selected path has no configured comment syntax")
comments = leading_comments(text, style)
approved = [item for item in policy["notices"] if any(fnmatch.fnmatchcase(name, pattern) for pattern in item["paths"])]
for notice in approved:
holder = re.escape(notice["holder"])
copyright_pattern = rf"(?m)^Copyright\s+(?:\([cC]\)\s*)?\d{{4}}(?:[-–]\d{{4}})?\.?\s+{holder}(?:\s+\(KofTwentyTwo\))?\.?(?:\s+Licensed under the MIT License \(see LICENSE\)\.)?$"
if re.search(copyright_pattern, comments) is None:
continue
spdx = re.findall(r"(?m)^SPDX-License-Identifier:\s*(\S+)\s*$", comments)
if spdx and any(value != notice["license"] for value in spdx):
continue
apache = all(phrase in comments for phrase in (
"Licensed under the Apache License, Version 2.0",
"you may not use this file except in compliance with the License.",
"https://www.apache.org/licenses/LICENSE-2.0",
"WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND",
"limitations under the License.",
))
if suffix == ".java" and not apache:
continue
if notice["license"] in spdx or (notice["license"] == "Apache-2.0" and apache):
return
raise ValueError("Missing or mismatched leading copyright/license notice")
def check_repository(root: Path, policy: dict[str, Any]) -> int:
"""Check tracked sources without scanning build outputs or executing source."""
tracked = subprocess.run(["git", "-C", str(root), "ls-files", "-z"], check=True, capture_output=True).stdout.decode("utf-8").split("\u0000")
failures = []
count = 0
for name in tracked:
if not name or not selected(name, policy):
continue
count += 1
try:
check_notice(name, (root / name).read_text(encoding="utf-8-sig"), policy)
except (ValueError, OSError) as error:
failures.append(f"{name}: {error}")
if count == 0:
raise ValueError("No owned source files selected")
if failures:
raise ValueError("\n".join(failures))
return count
def main() -> None:
"""Load the repository-specific reviewed scope and fail CI on a bad notice."""
root = Path(__file__).resolve().parents[2]
policy = json.loads((root / ".github/source-headers.json").read_text(encoding="utf-8"))
count = check_repository(root, policy)
print(f"Leading source notices verified: {count} tracked files")
if __name__ == "__main__":
try:
main()
except (ValueError, OSError, subprocess.CalledProcessError) as error:
print(str(error), file=sys.stderr)
sys.exit(1)
+3
View File
@@ -1,3 +1,6 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
"""Exercise the exact CI guards using synthetic evidence only."""
import contextlib
import copy
+156
View File
@@ -0,0 +1,156 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
"""Exercise notice parsing, provenance, scope and real Git inventory rejection."""
import importlib.util
import json
import subprocess
import tempfile
import unittest
from pathlib import Path
from typing import Any
SPEC = importlib.util.spec_from_file_location("headers", Path(__file__).with_name("check-source-headers.py"))
assert SPEC is not None and SPEC.loader is not None
HEADERS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(HEADERS)
POLICY = json.loads(Path(__file__).parents[1].joinpath("source-headers.json").read_text(encoding="utf-8"))
MIT = "Copyright (c) 2026 James Maes\nSPDX-License-Identifier: MIT"
APACHE = """Copyright (C) 2021-2025. Kingsrook, LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License."""
def fixture_policy(holder: str = "James Maes", license_id: str = "MIT") -> dict[str, Any]:
"""Use the production styles with a small explicit owned fixture scope."""
return {"includes": ["src/*"], "exclusions": [], "styles": POLICY["styles"],
"notices": [{"holder": holder, "license": license_id, "paths": ["src/*"]}]}
class HeaderTests(unittest.TestCase):
"""Ensure policy failures cannot pass through text-only copyright searches."""
def test_each_real_comment_syntax(self) -> None:
"""Recognize hash, slash, block and XML notices in their own languages."""
for name, text in (
("src/a.py", "# " + MIT.replace("\n", "\n# ") + "\nprint(1)"),
("src/a.cs", "// " + MIT.replace("\n", "\n// ") + "\nclass A {}"),
("src/a.mjs", "/*\n * " + MIT.replace("\n", "\n * ") + "\n */\nexport {};"),
("src/a.xml", "<!--\n" + MIT + "\n-->\n<root/>"),
("src/a.ps1", "<#\n" + MIT + "\n#>\nWrite-Output 1"),
):
with self.subTest(name=name):
HEADERS.check_notice(name, text, fixture_policy())
def test_required_first_lines_and_bom(self) -> None:
"""Accept shebangs, PowerShell requirements and XML declarations first."""
for name, prefix, body in (
("src/a.sh", "#!/bin/sh\n", "# " + MIT.replace("\n", "\n# ")),
("src/a.ps1", "#Requires -Version 7.4\n", "# " + MIT.replace("\n", "\n# ")),
("src/a.xml", "<?xml version=\"1.0\"?>\n", "<!--" + MIT + "--><root/>"),
("src/a.mjs", "#!/usr/bin/env node\n", "/*" + MIT + "*/"),
):
with self.subTest(name=name):
HEADERS.check_notice(name, "\ufeff" + prefix + body, fixture_policy())
def test_misordered_required_first_lines_fail(self) -> None:
"""A notice must not displace an interpreter or XML declaration."""
for name, text in (
("src/a.sh", "# " + MIT.replace("\n", "\n# ") + "\n#!/bin/sh"),
("src/a.ps1", "# " + MIT.replace("\n", "\n# ") + "\n#Requires -Version 7.4"),
("src/a.xml", "<!--" + MIT + "--><?xml version=\"1.0\"?><root/>"),
):
with self.subTest(name=name), self.assertRaises(ValueError):
HEADERS.check_notice(name, text, fixture_policy())
def test_code_strings_and_late_comments_do_not_count(self) -> None:
"""Reject docstrings and comments after executable declarations."""
for text in ("notice = \"" + MIT.replace("\n", "\\n") + "\"",
"\"\"\"" + MIT + "\"\"\"", "import os\n# " + MIT.replace("\n", "\n# ")):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.py", text, fixture_policy())
def test_wrong_syntax_and_unterminated_comments_fail(self) -> None:
"""Notice text alone is insufficient without a complete language comment."""
for text in ("# " + MIT, "/*" + MIT, "string text = \"" + MIT + "\";"):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.cs", text, fixture_policy())
with self.assertRaises(ValueError):
HEADERS.check_notice("src/a.py", "<#" + MIT + "#>", fixture_policy())
def test_wrong_holder_or_license_and_conflicting_spdx_fail(self) -> None:
"""Do not conflate repository licenses or accept conflicting identifiers."""
for text in (MIT.replace("James Maes", "Someone Else"), MIT.replace("MIT", "Apache-2.0"),
MIT + "\nSPDX-License-Identifier: Apache-2.0", MIT.splitlines()[0]):
with self.subTest(text=text), self.assertRaises(ValueError):
HEADERS.check_notice("src/a.cs", "/*" + text + "*/", fixture_policy())
def test_complete_legacy_apache_without_spdx(self) -> None:
"""Retain valid legacy year ranges and complete Apache notices."""
policy = fixture_policy("Kingsrook, LLC", "Apache-2.0")
HEADERS.check_notice("src/A.java", "/*" + APACHE + "*/\nclass A {}", policy)
HEADERS.check_notice("src/a.xml", "<!--" + APACHE + "--><root/>", policy)
def test_java_requires_full_apache_block(self) -> None:
"""An abbreviated SPDX-only Java notice cannot replace scoped convention."""
policy = fixture_policy("Kingsrook, LLC", "Apache-2.0")
for notice in ("Copyright 2026 Kingsrook, LLC\nSPDX-License-Identifier: Apache-2.0",
APACHE.replace("limitations under the License.", "")):
with self.subTest(notice=notice), self.assertRaises(ValueError):
HEADERS.check_notice("src/A.java", "/*" + notice + "*/", policy)
def test_virtual_agent_provenance_is_path_scoped(self) -> None:
"""Only the owned virtual-agent family may use its existing QRun.IO holder."""
policy = fixture_policy()
policy["notices"].append({"holder": "QRun.IO, Inc.", "license": "MIT", "paths": ["tools/virtual-agent/*"]})
text = "/*Copyright 2026 QRun.IO, Inc. Licensed under the MIT License (see LICENSE).\nSPDX-License-Identifier: MIT*/"
HEADERS.check_notice("tools/virtual-agent/a.mjs", text, policy)
with self.assertRaises(ValueError):
HEADERS.check_notice("src/a.mjs", text, policy)
def test_current_scope_and_documented_exclusions(self) -> None:
"""New scripts enter the guard; data, generated and upstream files do not."""
self.assertTrue(HEADERS.selected(".github/scripts/new-tool.py", POLICY))
self.assertTrue(HEADERS.selected(".github/workflows/new-check.yml", POLICY))
for name in (".mvn/wrapper/MavenWrapperDownloader.java", "mvnw", "schemas/new.json",
"src/main/resources/openapi/new.yaml", "src/test/resources/openapi/old.yaml",
"src/generated/Example.cs", "src/Example/obj/Generated.cs", "packages.lock.json"):
with self.subTest(name=name):
self.assertFalse(HEADERS.selected(name, POLICY))
self.assertTrue(all(item["reason"].strip() for item in POLICY["exclusions"]))
def test_guard_sources_match_this_repository_provenance(self) -> None:
"""Check copied guard files against local policy even before Git staging."""
for filename in ("check-source-headers.py", "test-source-headers.py"):
path = Path(__file__).with_name(filename)
name = ".github/scripts/" + filename
self.assertTrue(HEADERS.selected(name, POLICY))
HEADERS.check_notice(name, path.read_text(encoding="utf-8"), POLICY)
def test_real_git_inventory_rejects_missing_tracked_header(self) -> None:
"""A tracked new source fails while untracked generated output is ignored."""
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
subprocess.run(["git", "init", "-q", str(root)], check=True)
root.joinpath("src").mkdir()
source = root / "src/a.py"
source.write_text("print(1)", encoding="utf-8")
subprocess.run(["git", "-C", str(root), "add", "--", "src/a.py"], check=True)
with self.assertRaisesRegex(ValueError, "src/a.py"):
HEADERS.check_repository(root, fixture_policy())
source.write_text("# " + MIT.replace("\n", "\n# ") + "\nprint(1)", encoding="utf-8")
root.joinpath("src/untracked.py").write_text("print(2)", encoding="utf-8")
self.assertEqual(1, HEADERS.check_repository(root, fixture_policy()))
if __name__ == "__main__":
unittest.main()
+98
View File
@@ -0,0 +1,98 @@
{
"description": "Scope of the reviewed 2026-11-01 owned-source notice pass. Python fnmatch patterns match slash-containing relative paths. Native C# IDE0073 and full Java Apache conventions remain in force.",
"includes": [
".github/*.yml",
".github/*.yaml",
".github/scripts/*.py",
"tools/*.mjs"
],
"exclusions": [
{
"pattern": "*.json",
"reason": "Schema, fixtures, manifests and lock data: repository LICENSE/NOTICE, no comments."
},
{
"pattern": "*.lock",
"reason": "Dependency lock data, not owned executable source."
},
{
"pattern": "*/generated/*",
"reason": "Explicit generated-code directory."
},
{
"pattern": "*/bin/*",
"reason": "Generated build output."
},
{
"pattern": "*/obj/*",
"reason": "Generated build output."
},
{
"pattern": "*/node_modules/*",
"reason": "Third-party dependency output."
},
{
"pattern": ".mvn/*",
"reason": "Upstream Maven wrapper provenance."
},
{
"pattern": "mvnw*",
"reason": "Upstream Maven wrapper provenance."
},
{
"pattern": "Assets/Legal/*",
"reason": "Byte-exact third-party legal files."
},
{
"pattern": "src/main/resources/*",
"reason": "Bundled font/legal assets and OpenAPI schema data."
},
{
"pattern": "src/test/resources/openapi/*",
"reason": "Historical contract schema fixtures."
},
{
"pattern": "*.slnx",
"reason": "Solution metadata outside the completed source-header pass; root LICENSE/NOTICE applies."
},
{
"pattern": "*.manifest",
"reason": "Application activation metadata outside this pass; root LICENSE/NOTICE applies."
},
{
"pattern": "*.runsettings",
"reason": "Test runner metadata outside this pass; root LICENSE/NOTICE applies."
},
{
"pattern": ".editorconfig",
"reason": "Style configuration outside this pass; root LICENSE/NOTICE applies."
}
],
"styles": {
".py": "hash",
".ps1": "powershell",
".psd1": "powershell",
".sh": "hash",
".yml": "hash",
".yaml": "hash",
".cs": "slash",
".java": "slash",
".mjs": "slash",
".js": "slash",
".xml": "xml",
".props": "xml",
".csproj": "xml",
".wxs": "xml",
".wixproj": "xml",
".xaml": "xml"
},
"notices": [
{
"paths": [
"*"
],
"license": "Apache-2.0",
"holder": "Kingsrook, LLC"
}
]
}
+7
View File
@@ -1,3 +1,6 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: Protocol CI and release
on:
@@ -23,6 +26,10 @@ jobs:
node-version: '24'
package-manager-cache: false
- run: npm ci --ignore-scripts
- 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
+4 -1
View File
@@ -1,3 +1,6 @@
# 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
@@ -87,7 +90,7 @@ jobs:
global-json-file: global.json
- name: Initialize CodeQL
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ contains(fromJSON('["actions","javascript-typescript","python","ruby"]'), matrix.language) && 'none' || inputs.build-mode }}
+3
View File
@@ -1,3 +1,6 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: codeql
on:
pull_request:
+3
View File
@@ -1,3 +1,6 @@
# 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.
+3
View File
@@ -1,3 +1,6 @@
# Copyright 2026 Kingsrook, LLC
# SPDX-License-Identifier: Apache-2.0
name: security
on:
+18
View File
@@ -1,5 +1,23 @@
# Changelog
## 0.3.0-rc.1 — 2026-10-31
Upgrade the server before the matching Windows connector. Native printing API
2.0 requires synchronized authenticated WSS; the wire payload version remains 1.
- Define printing semantic extension 2.0 with an authenticated outbound WebSocket
for direct controls, events, receipts and job notifications; prohibit polling
fallback while retaining existing wire-v1 domain payloads.
- Mark the mandatory channel prerequisite for native claims as a breaking API
change from extension 1.0; framework `/qqq/v1` and the v1 subprotocol stay unchanged.
- Specify durable reconnect reconciliation, exact ACK replay and independent
same-origin HTTPS streaming for large documents.
- Add validated administrator settings and declared client ceilings for document
sizes, copies, queues, concurrency, bandwidth, liveness and timeouts, including
the native submission deadline.
- Add strict frame/settings schemas, synthetic fixtures and negative validation.
Consumer transport, installed UI and physical output acceptance remain separate.
## 0.2.0-rc.1 — 2026-10-31
- Add optional wire-v1 device-control discovery and independent control polling.
+9 -7
View File
@@ -1,8 +1,8 @@
# 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 first implementation uses outbound HTTPS polling. WebSockets are unnecessary for this happy path. This contract is authoritative for wire names; language types may use their idiomatic naming.
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.
Published source distribution: [**0.1.4-rc.1**](https://github.com/QRun-IO/qqq-print-protocol/releases/tag/v0.1.4-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.
Current release candidate: [**0.3.0-rc.1**](https://github.com/QRun-IO/qqq-print-protocol/releases/tag/v0.3.0-rc.1). Previous distribution: [**0.2.0-rc.1**](https://github.com/QRun-IO/qqq-print-protocol/releases/tag/v0.2.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.
Use Node.js 24+ to run the development checks:
@@ -23,9 +23,11 @@ The release archive and `SHA256SUMS` are written to `dist/`. Schema/fixture cons
## Conventions
Primary new-client base: `/qqq/v1/qbit/printing`, discovered as QQQ semantic extension `printing` version `1.0.0`, using standard QQQ user authentication and secondary device proof. See [QQQ authentication](docs/QQQ-AUTHENTICATION.md). The routes below describe explicit legacy compatibility at `/printing/v1`; native authentication failure never silently selects that transport. JSON uses UTF-8, camelCase properties, uppercase enum strings, and RFC 3339 UTC timestamps. IDs are opaque case-sensitive strings using URL-safe characters. Consumers ignore unknown response fields for compatible additions. Breaking changes require a new major contract. Configure the complete server origin; production requires HTTPS. Explicit localhost-only development may use HTTP.
The realtime implementation requires printing semantic extension `>=2.0.0 <3.0.0`; wire payload version remains `1`. Its settings, frames, replay rules and required consumer acceptance are in [REALTIME.md](docs/REALTIME.md). Upgrade the server before the matching Windows connector; installed and physical-output acceptance remain separate from this contract release. The HTTP polling sequence below documents the earlier 1.0/explicit compatibility transport, not a fallback for a realtime client.
Use `Content-Type: application/json` except the content-download response. All authenticated endpoints require `Authorization: Bearer <token>`. Device tokens and admin credentials are distinct; a device token must never authorize `/admin` operations. Enrollment uses only the single-use code in its request body. Never put credentials in query strings or log them.
Primary native base: `/qqq/v1/qbit/printing`, discovered as QQQ semantic extension `printing`, using standard QQQ user authentication and secondary device proof. Realtime requires extension `2.0.0`; the earlier HTTP transport uses `1.0.0`. See [QQQ authentication](docs/QQQ-AUTHENTICATION.md). The routes below describe explicit legacy compatibility at `/printing/v1`; native authentication failure never silently selects that transport. JSON uses UTF-8, camelCase properties, uppercase enum strings, and RFC 3339 UTC timestamps. IDs are opaque case-sensitive strings using URL-safe characters. Consumers ignore unknown response fields for compatible additions. Breaking changes require a new major contract. Configure the complete server origin; production requires HTTPS. Explicit localhost-only development may use HTTP.
Use `Content-Type: application/json` except the content-download response. The explicit legacy endpoints below use `Authorization: Bearer <token>`. Device tokens and admin credentials are distinct; a device token must never authorize `/admin` operations. Legacy enrollment uses only the single-use code in its request body. Never put credentials in query strings or log them.
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.
@@ -53,8 +55,8 @@ The additive [device-control contract](docs/DEVICE-CONTROLS.md) defines capabili
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 prepared for the
`0.2.0-rc.1` distribution; the published version above remains unchanged.
polling, and explicit receipts. These additions are included in the
`0.2.0-rc.1` distribution, with 42 definitions and 54 validated fixtures.
## DTOs
@@ -76,7 +78,7 @@ polling, and explicit receipts. These additions are prepared for the
Formats: `PDF` (`application/pdf`) and `ZPL` (`application/vnd.zebra-zpl`). Content downloads may use `application/octet-stream`; the authenticated job's format is authoritative. ZPL content is UTF-8 printer-ready data. Printer status is `READY`, `OFFLINE`, `PAUSED`, `ERROR`, or `UNKNOWN`. It is a driver observation, not proof that hardware is ready. Formats are advertised adapter capabilities; do not infer ZPL support for every Windows queue.
## Happy-path sequence
## Historical HTTP happy-path sequence
1. Administrator creates an enrollment and enters its code plus server URL in the connector.
2. Connector exchanges the code once and securely retains the device identity/token.
+8 -2
View File
@@ -1,9 +1,15 @@
# Device controls — additive wire v1
For the coordinated printing extension 2.0 implementation, 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.
This extension provides server dispatch controls and two bounded Windows agent
commands. Its additive payloads preserve existing print job states and old
request bodies. Package version `0.2.0-rc.1` is the proposed coordinated release;
this document does not claim publication, deployment, or production readiness.
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.
## Discovery and routes
+10 -3
View File
@@ -14,15 +14,22 @@ If a consumer builds outside a multi-checkout workspace, download the release ar
## Versioning
Wire protocol v1 is selected by the `/printing/v1` path and enrollment response `protocolVersion: 1`. Published distribution version `0.1.4-rc.1` identifies the specification/fixture bundle; it does not change that wire version. The Windows and QBit release numbers are independent of both. Current clients validate the wire major at enrollment; runtime contract minor/patch discovery, feature negotiation and a served OpenAPI definition are not implemented.
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).
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.
Additive optional response fields can remain v1. Required new inputs on existing routes, renamed fields, removed enum values or changed semantics require explicit compatibility design and usually a new path version. Consumers must not assume unadvertised capabilities.
The proposed `0.2.0-rc.1` extension adds explicit `device-control-v1` discovery and
The published `0.2.0-rc.1` extension adds explicit `device-control-v1` discovery and
new control routes without adding required fields to existing requests. See the
[device-control contract](DEVICE-CONTROLS.md) for state, commands, receipts,
timestamped idempotency, and legacy behavior. Source fixtures do not establish
consumer implementation or installation. Pin the eventual reviewed protocol
consumer implementation or installation. Pin the reviewed protocol
commit with matching server and Windows compatibility tests before shipping.
Claim acknowledgment means local receipt; SPOOLED means Windows accepted the document. Neither proves physical output. Cross-language compatibility tests should assert these distinctions as well as spelling, JSON casing, formats, option defaults and content integrity.
+13 -3
View File
@@ -2,14 +2,24 @@
The new Windows client uses the QQQ semantic UI/UX API. Package versions, wire
payload version `1` and the printing extension semantic version are separate
contracts. This document describes implementation in progress, not an accepted
Authentik deployment or a published feature release.
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.
## Discovery and sign-in
Require printing extension `>=2.0.0 <3.0.0` before opening the realtime channel
or dispatching work. Standard discovery and sign-in may accept compatible 1.x
and 2.x extensions; a successful 1.x login does not permit realtime dispatch or
polling fallback. Published 0.2 clients used native printing extension 1.0 and
cannot satisfy the new mandatory channel prerequisite. The standard descriptor
is unchanged; `realtime-v1` is mandatory in WebSocket HELLO. See
[realtime authentication and transport](REALTIME.md).
Read public `/qqq/v1/version` before sending credentials. Require core semantic
API version `>=1.8.0 <2.0.0`, the `api` channel and printing extension version
`>=1.0.0 <2.0.0`.
`>=1.0.0 <3.0.0` for the shared authentication client. Realtime operation still
requires the stricter 2.x gate above.
Use client channel `api/qqq-print-agent` and read
`/qqq/v1/metaData/authentication`. Do not infer server authentication from a URL,
brand, a saved token or a previous deployment.
+291
View File
@@ -0,0 +1,291 @@
# Realtime printing — semantic extension 2.0
This is the contract for the `0.3.0-rc.1` realtime release candidate.
Contract publication does not establish installed acceptance. Existing job states, command outcomes,
current-user authorization and durable idempotency rules remain authoritative.
Native printing extension **2.0.0** makes an authenticated synchronized WebSocket
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
`/qqq/v1`, wire payload v1 and subprotocol `qqq-printing.v1` remain independent.
## Discovery and connection
Require QQQ printing semantic extension `>=2.0.0 <3.0.0`, discovered through
`/qqq/v1/version`, and the existing core API/channel/authentication gates.
A 1.x server is upgrade-required for this client. Do not infer support from a
socket accepting a connection, a saved version, or a prior enrollment.
`printing-realtime-extension.discovery.json` shows the existing core `extensions`
entry for printing 2.0.0, not a replacement schema for the core discovery document.
Its descriptor uses the unchanged name `printing` and path prefix `/qbit/printing`.
The client opens one persistent WebSocket to the configured QQQ origin:
- Route: `/qqq/v1/qbit/printing/device/realtime`.
- Subprotocol: `qqq-printing.v1`; the server must select this exact value.
- Scheme: `wss`; `ws` is allowed only for explicitly configured loopback development.
- Upgrade authentication: standard QQQ session cookie,
`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`.
Do not add credentials to URLs, subprotocols, logs or frame payloads. Refuse
cross-origin redirects and arbitrary content URLs. The existing session is
primary authority; device proof never replaces it. See [QQQ authentication](QQQ-AUTHENTICATION.md).
Authenticate the upgrade and revalidate the current session, principal/device
binding, tenant and operation-specific permissions for each incoming operation
and outgoing delivery. Revalidate on liveness traffic too; an idle connection
cannot preserve expired or withdrawn authority indefinitely. Use the normal QQQ
session-resume flow; provider refresh stays on the server. When the session
credential changes, reconnect with the current credential while retaining the
same authoritative device/journal scope. No private WebSocket token is created.
There is one active authenticated connection per device. A new connection
supersedes the old generation. A late close, ACK or callback from an older
generation cannot close or acknowledge the replacement. A connection ID and
message order identify transport ownership, never user authority.
## Frames
Only UTF-8 JSON text messages are accepted. Each complete message has
`{ "type": "...", "messageId": "...", "payload": { ... } }`.
Use the existing URL-safe `Id` alphabet and 200-character bound; use hyphens,
not colons, in generated prefixes. Bound the reassembled message, not merely an
individual WebSocket fragment, by `maxFrameBytes`. Binary/document messages and
compression that could bypass decoded-size limits are not supported.
| Direction | Type | Payload |
| --- | --- | --- |
| Client → server | HELLO | `agentVersion`, `capabilities`, `lastAppliedRevision`, `lastSettingsRevision`, `limits` |
| Server → client | WELCOME | `connectionId`, `serverTime`, `desiredState`, `settings` |
| Client → server | STATE_APPLIED | `lastAppliedRevision`, `lastSettingsRevision` |
| 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 | COMMAND | Existing `DeviceCommand` directly |
| Client → server | JOB_EVENT | `jobId`, existing `JobEvent` as `event` |
| Client → server | COMMAND_RECEIPT | `commandId`, existing `DeviceCommandReceipt` as `receipt` |
| Client → server | HEARTBEAT | Existing `HeartbeatRequest` directly |
| Client → server | INVENTORY | Existing `PrinterInventory` directly |
| Client → server | PING | Empty object; liveness only |
| Server → client | PONG | `serverTime` |
| Server → client | ACK | `acknowledgedMessageId` |
| Server → client | ERROR | `code`, safe `message`, `retryable` |
The settings payload schema is `RealtimeSettings`; its envelope schema is
`RealtimeSettingsMessage`. The directional unions are `RealtimeClientMessage`
and `RealtimeServerMessage`. An ERROR responding to a frame uses that frame's
`messageId`; connection-level errors use a fresh ID. Never treat an ERROR as
a successful ACK or erase the corresponding durable event/receipt.
Canonical error codes include AUTHORIZATION_REQUIRED, CONFIGURATION_REQUIRED,
INVALID_MESSAGE and OPERATION_FAILED. Preserve domain NOT_FOUND, EXPIRED and
CONFLICT distinctions for 404/410/409 outcomes, including the existing bounded
pruned-receipt retirement rule. Only transient failures set `retryable:true`;
unknown codes remain explicit errors, never permission to fall back or drop work.
HEARTBEAT is periodic small status/profile traffic over this socket. INVENTORY
runs on initial synchronization, actual local changes or an explicit refresh
command; it is not an HTTP polling loop. PING/PONG may maintain liveness without
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.
## Ordering, synchronization and replay
1. Authenticate the connection, then require HELLO before other application work.
Its revision values report previously persisted observations, not authority.
Subscribe to committed changes before taking the initial durable snapshot.
2. Send WELCOME with current settings and desired state. The client validates
policy and its declared ceilings, persists both revisions and values, applies
them, and sends STATE_APPLIED. Neither revision may roll back or exceed the
authoritative revision.
3. Acknowledge STATE_APPLIED only after recording that observation for the active
connection. New claims require that active authorized connection and both
current revisions. The client also waits for that matching ACK before claims.
Remember an early JOB_AVAILABLE until this gate is satisfied.
4. Deliver pending unexpired commands and a coalesced JOB_AVAILABLE for durable
queued work. Committed desired-state, settings, job and command changes trigger
delivery. Use a transaction/outbox or post-commit synchronization mechanism
that cannot lose a change between subscription, snapshot and the empty claim.
5. JOB_AVAILABLE triggers a finite authenticated HTTPS claim drain until empty
or policy/concurrency capacity is reached. Keep a pending wake while at capacity;
lane completion resumes that drain. Reaching empty atomically clears the wake
relative to later committed enqueues. Returning to eligible state after
resume/reconnect or settings application also wakes the drain.
6. Persist exact JOB_EVENT and COMMAND_RECEIPT envelopes before sending.
ACK means the same existing authorized service operation committed durably.
Replay identical message IDs, domain event/receipt IDs and payloads until ACK.
Lost ACKs must return success for the original committed result; changed
content for an existing identity conflicts.
7. After network loss, reconnect using configured bounded exponential backoff
and jitter, repeat HELLO/WELCOME/current-state reconciliation, and replay the
durable outbox. The server replays pending commands from its durable store;
notifications and per-connection ordering are not a durable work ledger.
No job, control, heartbeat or receipt HTTP timer is permitted in this transport.
No timeout, 404, socket failure, empty response or unsupported version enables
HTTP polling or bearer fallback. Existing compatibility routes remain explicit
diagnostics for eligible identities, not this client's recovery path.
An implementation that uses timed database polling as its notification source
does not satisfy this contract. Multi-node hosts need shared committed-change
delivery; an in-process event bus qualifies only its documented single-node scope.
Job claims/content stay on their existing authenticated HTTPS routes at the same
origin. A lost claim response must be reconciled with the authoritative assigned
job and local journal on reconnect before claiming unrelated work; it cannot
strand a CLAIMED job or authorize a second physical submission. Initial or
event-triggered bounded HTTPS reconciliation snapshots are allowed, never timers.
The existing command journal decides whether a repeated command is unexecuted,
in progress or terminal. Persist execution intent before a native cancellation;
after uncertain execution return OUTCOME_UNKNOWN, never blindly invoke it again.
The existing receipt-retention and pruned-history rules still apply. A missing
expired server receipt is not fabricated server acceptance.
DISCONNECTED and dispatch pause gate new claims. DISCONNECTED retains the socket,
desired-state updates, commands, receipts and in-flight job events, but suppresses
ordinary HEARTBEAT/inventory publication; PING/PONG still maintains liveness.
REFRESH_INVENTORY waits until CONNECTED or expires. RECONNECT does not clear pause,
and neither control restores revoked credentials. No command restarts Windows,
the service or a printer. A reported spool cancellation cannot recall paper.
## Operational settings and client ceilings
The server is authoritative. Administrators edit validated operational policy
through native QQQ permissions; agents cannot set it. All 23 properties below,
including the server-generated `revision`, are required in each complete settings
snapshot. Never infer an omitted value from a compiled default.
| Property | Default | Inclusive range |
| --- | --- | --- |
| `revision` | Server-generated | 0–9007199254740991 |
| `maxCopies` | 100 | 1–32767 |
| `maxPdfBytes` | 104857600 | 1–2147483647 |
| `maxZplBytes` | 20971520 | 1–2147483647 |
| `maxFrameBytes` | 1048576 | 8192–16777216 |
| `maxQueuedFrames` | 64 | 8–4096 |
| `maxQueuedFrameBytes` | 4194304 | 8192–67108864 |
| `maxConcurrentDownloads` | 1 | 1–16 |
| `maxConcurrentPrints` | 1 | 1–16 |
| `maxStagedBytes` | 536870912 | 1–9007199254740991 |
| `maxDownloadBytesPerSecond` | 0 | 0–2147483647 |
| `requestTimeoutMillis` | 30000 | 1000–300000 |
| `downloadConnectTimeoutMillis` | 30000 | 1000–300000 |
| `downloadIdleTimeoutMillis` | 30000 | 1000–3600000 |
| `downloadTimeoutMillis` | 600000 | 1000–86400000 |
| `webSocketHandshakeTimeoutMillis` | 30000 | 1000–300000 |
| `heartbeatIntervalSeconds` | 20 | 5–300 |
| `heartbeatTimeoutSeconds` | 60 | 10–900 |
| `reconnectMinDelayMillis` | 1000 | 100–60000 |
| `reconnectMaxDelayMillis` | 30000 | 1000–300000 |
| `reconnectJitterPercent` | 20 | 0–50 |
| `ackTimeoutMillis` | 15000 | 1000–300000 |
| `nativeSubmissionTimeoutMillis` | 120000 | 1000–86400000 |
Bytes are exact integers; 100 MiB PDF and 20 MiB ZPL are recommendations, not
immutable content limits. `maxDownloadBytesPerSecond:0` explicitly means
unlimited aggregate download bandwidth. Jitter is applied within the configured
minimum/maximum reconnect bounds, including at the cap. No setting authorizes
an arbitrary URL, OS operation or widening of a user's permissions.
Validate these relationships before committing or applying policy:
- `maxQueuedFrameBytes >= maxFrameBytes`.
- `maxStagedBytes >= max(maxPdfBytes, maxZplBytes)`.
- `downloadTimeoutMillis >= downloadConnectTimeoutMillis` and
`downloadTimeoutMillis >= downloadIdleTimeoutMillis`.
- `heartbeatTimeoutSeconds >= 2 * heartbeatIntervalSeconds`.
- `reconnectMaxDelayMillis >= reconnectMinDelayMillis`.
- Each corresponding setting is at most the declared client ceiling.
HELLO `limits` declares the client's actual supported maxima for
`maxCopies`, `maxPdfBytes`, `maxZplBytes`, `maxFrameBytes`, `maxQueuedFrames`,
`maxQueuedFrameBytes`, `maxConcurrentDownloads`, `maxConcurrentPrints`,
and `maxStagedBytes`. It is not a client request to lower server policy.
Reject incompatible policy with CONFIGURATION_REQUIRED; never silently clamp or
use a lower intersection. Concurrency must be an actual configurable bounded
pool, with separate per-printer serialization, not a permanently serial
implementation advertising a configurable pool.
A committed policy revision is pushed as SETTINGS. Apply and durably acknowledge
it before new work under that revision. Existing transfers retain their admitted
resource reservation and a consistent timeout/size snapshot; reducing limits
blocks further admission until usage fits, rather than discarding receipts or
reclassifying already submitted output. Queue/frame limits apply to both count
and bytes. Reserve capacity for acknowledgments/liveness; coalesce replaceable
state/settings/job notifications, but never drop durable events or receipts.
An overfull queue produces bounded backpressure/reconnect and durable replay.
Existing admission and retention policies keep their established names and
scopes; do not duplicate them as per-device transport defaults. Host/tenant
admission comprises `deviceOutstandingJobs`, `tenantOutstandingJobs`,
`deviceOutstandingBytes`, `tenantOutstandingBytes`, `deviceRetainedBytes`,
`tenantRetainedBytes`, `deviceJobsPerHour`, `tenantJobsPerHour`,
`devicePrinters` and `tenantPrinters`. Retention uses `enabled`, `days` and
`batchSize`. Their effective values and scope belong in authorized settings
metadata. Intrinsic protocol, renderer, pipe and operating-system ceilings must
be disclosed separately from editable operational policy. No implementation
may claim that this realtime settings object alone exposes every existing host
or Windows setting.
## Document transfer and failure boundaries
Large documents never enter WebSocket messages, JSON/base64 control buffers or
whole-file memory buffers. Stream the existing same-origin HTTPS content route
to a restricted temporary file with incremental SHA-256, actual byte counting,
format validation and a staging reservation. Validate declared length and format
against the effective PDF/ZPL policy before transfer; enforce actual bytes while
streaming, including absent or false Content-Length and decompression behavior.
Only verified complete content may be submitted to Windows.
Enforce configured connect, idle and total deadlines and aggregate bandwidth
across the bounded download pool. Dispose/cancel streams and remove only owned
temporary files on failure, retaining job identity/outcome evidence. A throttled
download must not consume the control reader, writer or command worker. Do not
start another download without its byte reservation or release reserved space
while a renderer still uses the file. No automatic retry may duplicate a
possibly submitted print.
Apply `nativeSubmissionTimeoutMillis` to the isolated native printer submission
using the admitted job's policy snapshot. A timeout does not prove that Windows
accepted no bytes; preserve uncertain output and never automatically submit it
again. A larger configured document limit must not leave a hidden fixed native
submission deadline in the consumer.
Authentication/permission failure blocks new work and preserves journal state.
A generic denial is not proof that the device was revoked. Protocol errors,
oversized messages and overload must fail explicitly; never treat them as an
empty queue. Close/reconnect transport without silently switching authentication.
Native HTTP calls continue to revalidate current user and device authority.
## Required consumer acceptance
Schema/fixture checks establish spelling, bounds and examples, not runtime or
hardware qualification. Both implementations must demonstrate these behaviors
with isolated real transports before release:
| Scenario | Required observation |
| --- | --- |
| Slow large PDF | Hold an HTTPS response mid-body; receive/apply a new desired-state revision and complete an unrelated owned command/receipt ACK before releasing the body. Liveness continues; resident memory does not scale with document length. |
| Concurrent transfers | Set download/print concurrency above one; observe bounded actual parallel transfers, aggregate bandwidth/staging limits and per-printer serialization. Restore the configured default of one without dropping admitted work. |
| Lost command receipt ACK | Commit receipt, drop ACK, reconnect and replay exact envelope; one native cancellation invocation, one durable result, replay ACK succeeds. |
| Interrupted cancellation | Lose the connection/process after durable execution intent but before a known result; preserve OUTCOME_UNKNOWN and do not issue the native operation twice. |
| Queued work while offline | Commit jobs and controls while the socket is absent; reconnect snapshot/pending-command replay and one job wake drain all eligible queued work without a timer or duplicate print. |
| Empty-claim/enqueue race | Enqueue after the last empty query while ending a drain; a committed wake is retained and the new job is claimed without polling. |
| Lost claim response | Disconnect after server claim commit; reconcile assigned work and the journal before further claims, without stranding or reprinting it. |
| Lost event ACK | Commit SPOOLED then drop ACK; the identical replay fixture is acknowledged without another spool submission. |
| Policy change | Push validated new settings during a stalled transfer; control remains responsive, new claims wait for both revisions, and existing reservations remain bounded. |
| Policy/ceiling refusal | Reject missing/fractional/out-of-range/cross-field-invalid values and a policy above declared client capability; no silent defaults or clamping. |
| Authorization loss | Revoke session/device or withdraw current permission while idle and busy; reject further delivery/claims, preserve pending evidence and never use bearer fallback. |
| Connection replacement | Replace a socket, then deliver old-generation ACK/close callbacks; replacement ownership and pending messages remain correct. |
| Inventory/frame bounds | Exercise the maximum configured inventory and fragmented/oversized control messages; fail explicitly before unbounded allocation, never interpret partial inventory as withdrawal. |
| Notification source | Demonstrate after-commit enqueue/control/settings wake-ups and reconnect reconciliation with no periodic HTTP or database job/control polling. |
Physical output still requires user/hardware confirmation. SPOOLED and
SPOOLER_COMPLETED retain their existing meanings.
+29 -2
View File
@@ -1,6 +1,33 @@
# Session state — 2026-10-31
## Standard-session primary transport
## Realtime contract implementation
Isolated branch `feat/realtime-printing-20261031` defines printing extension 2.0,
authenticated client-initiated WSS, direct controls/events/receipts and streamed
same-origin HTTPS documents without job/control polling or fallback. The frozen
settings include a default single configurable download lane, 100 MiB PDF,
20 MiB ZPL, 100 copies and a configurable 120-second native submission deadline,
with declared client ceilings and explicit validation.
Slow-transfer control delivery, lost-connection replay, claim reconciliation and
no-duplicate-submission checks are required consumer acceptance, not claims made
by schema validation. Release candidate `0.3.0-rc.1` is described in
[the complete contract](REALTIME.md); this source checkpoint records no live changes.
The native API major is 2 because an authenticated synchronized WebSocket is now
mandatory for claims that 1.0 clients could previously make over HTTP alone.
Product 0.3, framework `/qqq/v1`, wire payload v1 and the v1 subprotocol remain
unchanged. A new fixture covers the standard printing extension descriptor;
historical legacy discovery fixtures remain unchanged.
Locked offline dependency restore, `npm run check` and `npm run bundle` passed
for this source: 71 JSON Schema definitions and 73 synthetic fixtures, including
the final native submission deadline, all 23 required settings, invalid policy
bounds, directional frames and exact replay. The eight changed Markdown documents
passed Markdown lint; `git diff --check` passed. The local archive is preparation
only; both consumers pin reviewed protocol commit `df3729ac632d4a34a3fb8423acef465042a624fc`.
Tag publication and asset provenance must be checked on the release workflow.
## Historical 0.2 implementation checkpoint — standard-session transport
The new Windows client uses discovered semantic extension `printing` version
`1.0.0` at `/qqq/v1/qbit/printing`, channel `api/qqq-print-agent`, with standard
@@ -15,7 +42,7 @@ disables legacy enrollment by default in native hosts; only eligible unbound
identities retain explicit migration compatibility. This clarification changes
no schema, fixture or consumer pin.
## Device-control contract under review
## Historical 0.2 implementation checkpoint — device-control review
Isolated branch `feat/server-device-controls-20261031` adds the frozen wire-v1
control contract in `docs/DEVICE-CONTROLS.md`. Server and Windows owners received
+17
View File
@@ -0,0 +1,17 @@
# Owned source notice check
Run these commands from the repository root with the existing Python 3 interpreter:
```text
python3 .github/scripts/test-source-headers.py
python3 .github/scripts/check-source-headers.py
```
The standard-library checker enforces the scope of the reviewed 2026-11-01 header pass, not an estate-wide licensing or formatting audit. `.github/source-headers.json` names the owned source/config path families, comment syntax, accepted holder/license pairs and explicit exclusion reasons. Python `fnmatch` patterns match slash-containing relative Git paths; new files in those families are covered after they are tracked. Empty selections fail. The checker does not scan untracked build output or modify files.
Only leading language comments count. Copyright text in a string, Python docstring or comment after code is rejected. UTF-8 BOMs, initial shebangs, PowerShell `#Requires` directives and XML declarations are handled before the notice. These declarations cannot be displaced beneath a new notice. PowerShell block comments are recognized only for PowerShell files. Existing years and holders are not rewritten. Complete canonical Apache notices are valid without redundant SPDX; Java still requires the full Apache block. A contradictory SPDX identifier fails.
JSON/schema/fixture/lock data, explicit generated output directories, vendor legal assets and upstream Maven wrappers are excluded with reasons in the policy. Solution/activation/test-runner metadata and `.editorconfig` remain outside this completed pass; their applicable root LICENSE/NOTICE remains authoritative. No broad claim about third-party licensing follows from this check. New owned file types or provenance must be reviewed before changing the allowlist; do not add blanket exclusions to make a failure disappear.
The implementation and its negative tests are intentionally maintained copies across the three printing repositories, with each repository's own copyright/license header and independently reviewed scope. They do not fetch policy or execute source. CI invokes them in existing jobs using the runner's available Python, with no new package or workflow context. Existing native C# IDE0073 and Java Checkstyle conventions remain unchanged.
This repository accepts Kingsrook, LLC / Apache-2.0 for its reviewed source/build/config families. The existing CI verify job runs the guard.
+19
View File
@@ -1,5 +1,24 @@
# TODO
## 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.
- [x] Define all 23 required settings, including configurable native submission
timeout, client ceilings and explicit policy refusal.
- [x] Pass 71 schema definitions, 73 fixtures, local bundle creation, Markdown
lint and whitespace checks without changing runtime or publishing assets.
- [x] Review and commit the protocol, then pin that immutable commit in both
consumer CI workflows.
- [ ] Complete consumer transport/replay, slow-transfer control, concurrency and
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
The entries below retain their original checkpoint status. They do not describe
the current realtime release's completion or publication.
- [ ] Qualify semantic discovery, current-user authorization, device binding,
session renewal and the shared native PKCE callback in both consumers.
@@ -0,0 +1,5 @@
{
"name": "printing",
"version": "2.0.0",
"pathPrefix": "/qbit/printing"
}
+7
View File
@@ -0,0 +1,7 @@
{
"type": "ACK",
"messageId": "ack-event-spooled-1",
"payload": {
"acknowledgedMessageId": "event-spooled-1"
}
}
@@ -0,0 +1,12 @@
{
"type": "COMMAND_RECEIPT",
"messageId": "receipt-cancel-1",
"payload": {
"commandId": "command-cancel-1",
"receipt": {
"receiptId": "receipt-cancel-1",
"outcome": "CANCEL_REQUESTED",
"completedAt": "2026-10-31T22:00:01Z"
}
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"type": "COMMAND",
"messageId": "command-delivery-1",
"payload": {
"commandId": "command-cancel-1",
"requestId": "action-cancel-spool-1",
"action": "CANCEL_SPOOL",
"createdAt": "2026-10-31T22:00:00Z",
"expiresAt": "2026-10-31T22:15:00Z",
"jobId": "job-example-1"
}
}
@@ -0,0 +1,9 @@
{
"type": "DESIRED_STATE",
"messageId": "state-revision-1",
"payload": {
"revision": 1,
"dispatchPaused": true,
"connectionState": "CONNECTED"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"type": "ERROR",
"messageId": "event-spooled-1",
"payload": {
"code": "PERMISSION_DENIED",
"message": "Current printing permission is required.",
"retryable": false
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"type": "HEARTBEAT",
"messageId": "heartbeat-connection-1",
"payload": {
"agentVersion": "0.3.0-rc.1",
"profile": {
"name": "Example workstation",
"description": "Synthetic contract fixture",
"location": "Example facility"
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"type": "HELLO",
"messageId": "hello-connection-1",
"payload": {
"agentVersion": "0.3.0-rc.1",
"capabilities": [
"realtime-v1",
"REFRESH_INVENTORY",
"CANCEL_SPOOL"
],
"lastAppliedRevision": 0,
"lastSettingsRevision": 0,
"limits": {
"maxPdfBytes": 536870912,
"maxZplBytes": 104857600,
"maxFrameBytes": 16777216,
"maxQueuedFrames": 4096,
"maxQueuedFrameBytes": 67108864,
"maxConcurrentDownloads": 16,
"maxConcurrentPrints": 16,
"maxStagedBytes": 4294967296,
"maxCopies": 32767
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"type": "INVENTORY",
"messageId": "inventory-connection-1",
"payload": {
"printers": [
{
"printerId": "laser-example",
"name": "Local Laser",
"driverName": "Example Laser Driver",
"portName": "USB001",
"status": "READY",
"formats": [
"PDF"
]
}
]
}
}
@@ -0,0 +1,5 @@
{
"type": "JOB_AVAILABLE",
"messageId": "jobs-wake-1",
"payload": {}
}
@@ -0,0 +1,13 @@
{
"type": "JOB_EVENT",
"messageId": "event-spooled-1",
"payload": {
"jobId": "job-example-1",
"event": {
"eventId": "event-spooled-1",
"status": "SPOOLED",
"message": "Windows accepted the job; physical output is not confirmed.",
"spoolerJobId": "17"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"type": "JOB_EVENT",
"messageId": "event-spooled-1",
"payload": {
"jobId": "job-example-1",
"event": {
"eventId": "event-spooled-1",
"status": "SPOOLED",
"message": "Windows accepted the job; physical output is not confirmed.",
"spoolerJobId": "17"
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"jobId": "large-pdf-example",
"deviceId": "device-example",
"printerId": "laser-example",
"title": "Synthetic 100 MiB metadata example; no document included",
"format": "PDF",
"contentSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"contentLength": 104857600,
"options": {
"copies": 1,
"scale": "ACTUAL_SIZE"
},
"createdAt": "2026-10-31T22:00:00Z",
"status": "CLAIMED"
}
+5
View File
@@ -0,0 +1,5 @@
{
"type": "PING",
"messageId": "ping-connection-1",
"payload": {}
}
+7
View File
@@ -0,0 +1,7 @@
{
"type": "PONG",
"messageId": "pong-connection-1",
"payload": {
"serverTime": "2026-10-31T22:00:00Z"
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"type": "SETTINGS",
"messageId": "settings-revision-2",
"payload": {
"revision": 2,
"maxPdfBytes": 104857600,
"maxZplBytes": 20971520,
"maxFrameBytes": 1048576,
"maxQueuedFrames": 64,
"maxQueuedFrameBytes": 4194304,
"maxConcurrentDownloads": 2,
"maxConcurrentPrints": 1,
"maxStagedBytes": 536870912,
"maxDownloadBytesPerSecond": 0,
"requestTimeoutMillis": 30000,
"downloadConnectTimeoutMillis": 30000,
"downloadIdleTimeoutMillis": 30000,
"downloadTimeoutMillis": 600000,
"webSocketHandshakeTimeoutMillis": 30000,
"heartbeatIntervalSeconds": 20,
"heartbeatTimeoutSeconds": 60,
"reconnectMinDelayMillis": 1000,
"reconnectMaxDelayMillis": 30000,
"reconnectJitterPercent": 20,
"ackTimeoutMillis": 15000,
"maxCopies": 100,
"nativeSubmissionTimeoutMillis": 120000
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"revision": 1,
"maxPdfBytes": 104857600,
"maxZplBytes": 20971520,
"maxFrameBytes": 1048576,
"maxQueuedFrames": 64,
"maxQueuedFrameBytes": 4194304,
"maxConcurrentDownloads": 1,
"maxConcurrentPrints": 1,
"maxStagedBytes": 536870912,
"maxDownloadBytesPerSecond": 0,
"requestTimeoutMillis": 30000,
"downloadConnectTimeoutMillis": 30000,
"downloadIdleTimeoutMillis": 30000,
"downloadTimeoutMillis": 600000,
"webSocketHandshakeTimeoutMillis": 30000,
"heartbeatIntervalSeconds": 20,
"heartbeatTimeoutSeconds": 60,
"reconnectMinDelayMillis": 1000,
"reconnectMaxDelayMillis": 30000,
"reconnectJitterPercent": 20,
"ackTimeoutMillis": 15000,
"maxCopies": 100,
"nativeSubmissionTimeoutMillis": 120000
}
@@ -0,0 +1,8 @@
{
"type": "STATE_APPLIED",
"messageId": "applied-connection-1",
"payload": {
"lastAppliedRevision": 0,
"lastSettingsRevision": 1
}
}
+38
View File
@@ -0,0 +1,38 @@
{
"type": "WELCOME",
"messageId": "welcome-connection-1",
"payload": {
"connectionId": "connection-example-1",
"serverTime": "2026-10-31T22:00:00Z",
"desiredState": {
"revision": 0,
"dispatchPaused": false,
"connectionState": "CONNECTED"
},
"settings": {
"revision": 1,
"maxPdfBytes": 104857600,
"maxZplBytes": 20971520,
"maxFrameBytes": 1048576,
"maxQueuedFrames": 64,
"maxQueuedFrameBytes": 4194304,
"maxConcurrentDownloads": 1,
"maxConcurrentPrints": 1,
"maxStagedBytes": 536870912,
"maxDownloadBytesPerSecond": 0,
"requestTimeoutMillis": 30000,
"downloadConnectTimeoutMillis": 30000,
"downloadIdleTimeoutMillis": 30000,
"downloadTimeoutMillis": 600000,
"webSocketHandshakeTimeoutMillis": 30000,
"heartbeatIntervalSeconds": 20,
"heartbeatTimeoutSeconds": 60,
"reconnectMinDelayMillis": 1000,
"reconnectMaxDelayMillis": 30000,
"reconnectJitterPercent": 20,
"ackTimeoutMillis": 15000,
"maxCopies": 100,
"nativeSubmissionTimeoutMillis": 120000
}
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@qrun-io/qqq-print-protocol",
"version": "0.2.0-rc.1",
"version": "0.3.0-rc.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qrun-io/qqq-print-protocol",
"version": "0.2.0-rc.1",
"version": "0.3.0-rc.1",
"license": "Apache-2.0",
"devDependencies": {
"ajv": "8.20.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@qrun-io/qqq-print-protocol",
"version": "0.2.0-rc.1",
"version": "0.3.0-rc.1",
"private": true,
"description": "Versioned wire contract and compatibility fixtures for QQQ cloud printing.",
"license": "Apache-2.0",
+777 -2
View File
@@ -4,6 +4,17 @@
"title": "QQQ Print Protocol v1",
"description": "Select the DTO with #/$defs/Name.",
"$defs": {
"PrintingRealtimeExtensionDiscovery": {
"description": "The existing core extensions entry required by realtime clients; this does not redefine the core discovery document or its API version.",
"type": "object",
"required": ["name", "version", "pathPrefix"],
"properties": {
"name": { "type": "string", "const": "printing" },
"version": { "type": "string", "pattern": "^2\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$", "maxLength": 100 },
"pathPrefix": { "type": "string", "const": "/qbit/printing" }
},
"additionalProperties": true
},
"PrintingSessionIdentity": {
"type": "object",
"required": ["principalId", "tenantId"],
@@ -444,7 +455,7 @@
"copies": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"maximum": 32767,
"default": 1
},
"scale": {
@@ -544,7 +555,8 @@
"contentLength": {
"type": "integer",
"minimum": 1,
"maximum": 20971520
"maximum": 2147483647,
"description": "Syntactic ceiling; the discovered transport's effective format policy and declared client capability impose the actual limit."
},
"options": {
"$ref": "#/$defs/PrintOptions"
@@ -1248,6 +1260,769 @@
}
},
"additionalProperties": true
},
"RealtimeSettingsMessage": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "SETTINGS"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeSettings"
}
},
"additionalProperties": false
},
"RealtimeSettings": {
"description": "Authoritative operational settings. Cross-field and declared-client-ceiling validation in REALTIME.md is also required.",
"type": "object",
"required": [
"revision",
"maxCopies",
"maxPdfBytes",
"maxZplBytes",
"maxFrameBytes",
"maxQueuedFrames",
"maxQueuedFrameBytes",
"maxConcurrentDownloads",
"maxConcurrentPrints",
"maxStagedBytes",
"maxDownloadBytesPerSecond",
"requestTimeoutMillis",
"downloadConnectTimeoutMillis",
"downloadIdleTimeoutMillis",
"downloadTimeoutMillis",
"webSocketHandshakeTimeoutMillis",
"heartbeatIntervalSeconds",
"heartbeatTimeoutSeconds",
"reconnectMinDelayMillis",
"reconnectMaxDelayMillis",
"reconnectJitterPercent",
"ackTimeoutMillis",
"nativeSubmissionTimeoutMillis"
],
"properties": {
"revision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"maxCopies": {
"type": "integer",
"minimum": 1,
"maximum": 32767,
"default": 100
},
"maxPdfBytes": {
"type": "integer",
"minimum": 1,
"maximum": 2147483647,
"default": 104857600
},
"maxZplBytes": {
"type": "integer",
"minimum": 1,
"maximum": 2147483647,
"default": 20971520
},
"maxFrameBytes": {
"type": "integer",
"minimum": 8192,
"maximum": 16777216,
"default": 1048576
},
"maxQueuedFrames": {
"type": "integer",
"minimum": 8,
"maximum": 4096,
"default": 64
},
"maxQueuedFrameBytes": {
"type": "integer",
"minimum": 8192,
"maximum": 67108864,
"default": 4194304
},
"maxConcurrentDownloads": {
"type": "integer",
"minimum": 1,
"maximum": 16,
"default": 1
},
"maxConcurrentPrints": {
"type": "integer",
"minimum": 1,
"maximum": 16,
"default": 1
},
"maxStagedBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"default": 536870912
},
"maxDownloadBytesPerSecond": {
"type": "integer",
"minimum": 0,
"maximum": 2147483647,
"default": 0,
"description": "Aggregate content-download bandwidth; zero explicitly means unlimited."
},
"requestTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 300000,
"default": 30000
},
"downloadConnectTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 300000,
"default": 30000
},
"downloadIdleTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 3600000,
"default": 30000
},
"downloadTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 86400000,
"default": 600000
},
"webSocketHandshakeTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 300000,
"default": 30000
},
"heartbeatIntervalSeconds": {
"type": "integer",
"minimum": 5,
"maximum": 300,
"default": 20
},
"heartbeatTimeoutSeconds": {
"type": "integer",
"minimum": 10,
"maximum": 900,
"default": 60
},
"reconnectMinDelayMillis": {
"type": "integer",
"minimum": 100,
"maximum": 60000,
"default": 1000
},
"reconnectMaxDelayMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 300000,
"default": 30000
},
"reconnectJitterPercent": {
"type": "integer",
"minimum": 0,
"maximum": 50,
"default": 20
},
"ackTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 300000,
"default": 15000
},
"nativeSubmissionTimeoutMillis": {
"type": "integer",
"minimum": 1000,
"maximum": 86400000,
"default": 120000,
"description": "Deadline for the isolated native printer submission; timeout cannot establish that no output was submitted."
}
},
"additionalProperties": false
},
"RealtimeClientLimits": {
"type": "object",
"required": [
"maxCopies",
"maxPdfBytes",
"maxZplBytes",
"maxFrameBytes",
"maxQueuedFrames",
"maxQueuedFrameBytes",
"maxConcurrentDownloads",
"maxConcurrentPrints",
"maxStagedBytes"
],
"properties": {
"maxCopies": {
"type": "integer",
"minimum": 1,
"maximum": 32767
},
"maxPdfBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxZplBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxFrameBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxQueuedFrames": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxQueuedFrameBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxConcurrentDownloads": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxConcurrentPrints": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"maxStagedBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
}
},
"additionalProperties": false
},
"RealtimeEmpty": {
"type": "object",
"required": [],
"properties": {},
"additionalProperties": false
},
"RealtimeHelloPayload": {
"type": "object",
"required": [
"agentVersion",
"capabilities",
"lastAppliedRevision",
"lastSettingsRevision",
"limits"
],
"properties": {
"agentVersion": {
"type": "string",
"minLength": 1,
"maxLength": 200
},
"capabilities": {
"type": "array",
"items": {
"enum": [
"realtime-v1",
"REFRESH_INVENTORY",
"CANCEL_SPOOL"
]
},
"minItems": 1,
"maxItems": 3,
"uniqueItems": true,
"contains": {
"const": "realtime-v1"
}
},
"lastAppliedRevision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"lastSettingsRevision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"limits": {
"$ref": "#/$defs/RealtimeClientLimits"
}
},
"additionalProperties": false
},
"RealtimeWelcomePayload": {
"type": "object",
"required": [
"connectionId",
"serverTime",
"desiredState",
"settings"
],
"properties": {
"connectionId": {
"$ref": "#/$defs/Id"
},
"serverTime": {
"$ref": "#/$defs/ControlTimestamp"
},
"desiredState": {
"$ref": "#/$defs/DeviceDesiredState"
},
"settings": {
"$ref": "#/$defs/RealtimeSettings"
}
},
"additionalProperties": false
},
"RealtimeStateAppliedPayload": {
"type": "object",
"required": [
"lastAppliedRevision",
"lastSettingsRevision"
],
"properties": {
"lastAppliedRevision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"lastSettingsRevision": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
},
"additionalProperties": false
},
"RealtimeJobEventPayload": {
"type": "object",
"required": [
"jobId",
"event"
],
"properties": {
"jobId": {
"$ref": "#/$defs/Id"
},
"event": {
"$ref": "#/$defs/JobEvent"
}
},
"additionalProperties": false
},
"RealtimeCommandReceiptPayload": {
"type": "object",
"required": [
"commandId",
"receipt"
],
"properties": {
"commandId": {
"$ref": "#/$defs/Id"
},
"receipt": {
"$ref": "#/$defs/DeviceCommandReceipt"
}
},
"additionalProperties": false
},
"RealtimeAckPayload": {
"type": "object",
"required": [
"acknowledgedMessageId"
],
"properties": {
"acknowledgedMessageId": {
"$ref": "#/$defs/Id"
}
},
"additionalProperties": false
},
"RealtimeErrorPayload": {
"type": "object",
"required": [
"code",
"message",
"retryable"
],
"properties": {
"code": {
"type": "string",
"minLength": 1,
"maxLength": 128
},
"message": {
"type": "string",
"minLength": 1,
"maxLength": 2048
},
"retryable": {
"type": "boolean"
}
},
"additionalProperties": false
},
"RealtimePongPayload": {
"type": "object",
"required": [
"serverTime"
],
"properties": {
"serverTime": {
"$ref": "#/$defs/ControlTimestamp"
}
},
"additionalProperties": false
},
"RealtimeHello": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "HELLO"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeHelloPayload"
}
},
"additionalProperties": false
},
"RealtimeStateApplied": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "STATE_APPLIED"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeStateAppliedPayload"
}
},
"additionalProperties": false
},
"RealtimeJobEvent": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "JOB_EVENT"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeJobEventPayload"
}
},
"additionalProperties": false
},
"RealtimeCommandReceipt": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "COMMAND_RECEIPT"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeCommandReceiptPayload"
}
},
"additionalProperties": false
},
"RealtimeHeartbeat": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "HEARTBEAT"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/HeartbeatRequest"
}
},
"additionalProperties": false
},
"RealtimeInventory": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "INVENTORY"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/PrinterInventory"
}
},
"additionalProperties": false
},
"RealtimePing": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "PING"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeEmpty"
}
},
"additionalProperties": false
},
"RealtimeWelcome": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "WELCOME"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeWelcomePayload"
}
},
"additionalProperties": false
},
"RealtimeDesiredState": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "DESIRED_STATE"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/DeviceDesiredState"
}
},
"additionalProperties": false
},
"RealtimeJobAvailable": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "JOB_AVAILABLE"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeEmpty"
}
},
"additionalProperties": false
},
"RealtimeCommand": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "COMMAND"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/DeviceCommand"
}
},
"additionalProperties": false
},
"RealtimeAck": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "ACK"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeAckPayload"
}
},
"additionalProperties": false
},
"RealtimeError": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "ERROR"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimeErrorPayload"
}
},
"additionalProperties": false
},
"RealtimePong": {
"type": "object",
"required": [
"type",
"messageId",
"payload"
],
"properties": {
"type": {
"const": "PONG"
},
"messageId": {
"$ref": "#/$defs/Id"
},
"payload": {
"$ref": "#/$defs/RealtimePongPayload"
}
},
"additionalProperties": false
},
"RealtimeClientMessage": {
"anyOf": [
{
"$ref": "#/$defs/RealtimeHello"
},
{
"$ref": "#/$defs/RealtimeStateApplied"
},
{
"$ref": "#/$defs/RealtimeJobEvent"
},
{
"$ref": "#/$defs/RealtimeCommandReceipt"
},
{
"$ref": "#/$defs/RealtimeHeartbeat"
},
{
"$ref": "#/$defs/RealtimeInventory"
},
{
"$ref": "#/$defs/RealtimePing"
}
]
},
"RealtimeServerMessage": {
"anyOf": [
{
"$ref": "#/$defs/RealtimeWelcome"
},
{
"$ref": "#/$defs/RealtimeDesiredState"
},
{
"$ref": "#/$defs/RealtimeJobAvailable"
},
{
"$ref": "#/$defs/RealtimeCommand"
},
{
"$ref": "#/$defs/RealtimeSettingsMessage"
},
{
"$ref": "#/$defs/RealtimeAck"
},
{
"$ref": "#/$defs/RealtimeError"
},
{
"$ref": "#/$defs/RealtimePong"
}
]
}
}
}
+4 -1
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -13,7 +16,7 @@ if(process.env.GITHUB_REF_TYPE === 'tag')
}
mkdirSync(new URL('../dist/', import.meta.url), { recursive: true });
const name = `qqq-print-protocol-${manifest.version}.tar.gz`;
const files = ['README.md', 'LICENSE', 'NOTICE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md', 'package.json', 'package-lock.json', 'schemas', 'fixtures', 'tools', 'docs/INTEGRATION.md', 'docs/DEVICE-CONTROLS.md', 'docs/QQQ-AUTHENTICATION.md'];
const files = ['README.md', 'LICENSE', 'NOTICE', 'CHANGELOG.md', 'CONTRIBUTING.md', 'SECURITY.md', 'package.json', 'package-lock.json', 'schemas', 'fixtures', 'tools', 'docs/INTEGRATION.md', 'docs/DEVICE-CONTROLS.md', 'docs/QQQ-AUTHENTICATION.md', 'docs/REALTIME.md'];
const result = spawnSync('tar', ['-czf', `dist/${name}`, ...files], { cwd: root, stdio: 'inherit', shell: false });
if(result.error) throw result.error;
assert.equal(result.status, 0, 'Archive creation failed.');
+3
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import { createHash } from 'node:crypto';
import { writeFileSync } from 'node:fs';
+23 -1
View File
@@ -1,5 +1,9 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
// Shared correspondence between wire examples and schema definitions.
export const fixtureTypes = {
'printing-realtime-extension.discovery.json': 'PrintingRealtimeExtensionDiscovery',
'version-legacy.response.json': 'ProtocolVersionResponse',
'version-controls.response.json': 'ProtocolVersionResponse',
'control-poll.request.json': 'DeviceControlPollRequest',
@@ -53,5 +57,23 @@ export const fixtureTypes = {
'ack.response.json': 'Acknowledgment',
'error.response.json': 'Error',
'printing-session.response.json': 'PrintingSessionIdentity',
'printing-session-bound.response.json': 'PrintingSessionIdentity'
'printing-session-bound.response.json': 'PrintingSessionIdentity',
'realtime-settings.payload.json': 'RealtimeSettings',
'realtime-hello.message.json': 'RealtimeHello',
'realtime-welcome.message.json': 'RealtimeWelcome',
'realtime-state-applied.message.json': 'RealtimeStateApplied',
'realtime-desired-state.message.json': 'RealtimeDesiredState',
'realtime-job-available.message.json': 'RealtimeJobAvailable',
'realtime-command.message.json': 'RealtimeCommand',
'realtime-command-receipt.message.json': 'RealtimeCommandReceipt',
'realtime-job-event.message.json': 'RealtimeJobEvent',
'realtime-job-event-replay.message.json': 'RealtimeJobEvent',
'realtime-settings.message.json': 'RealtimeSettingsMessage',
'realtime-ack.message.json': 'RealtimeAck',
'realtime-error.message.json': 'RealtimeError',
'realtime-heartbeat.message.json': 'RealtimeHeartbeat',
'realtime-inventory.message.json': 'RealtimeInventory',
'realtime-ping.message.json': 'RealtimePing',
'realtime-pong.message.json': 'RealtimePong',
'realtime-large-pdf.response.json': 'PrintJob'
};
+11
View File
@@ -1,3 +1,6 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
@@ -52,6 +55,14 @@ function validate(value, rule, path)
if(rule.minItems !== undefined) assert(value.length >= rule.minItems, path);
if(rule.maxItems !== undefined) assert(value.length <= rule.maxItems, path);
if(rule.uniqueItems) assert.equal(new Set(value.map(item => JSON.stringify(item))).size, value.length, path);
if(rule.contains)
{
assert(value.some(item =>
{
try { validate(item, rule.contains, path); return true; }
catch { return false; }
}), `${path}: required array member missing`);
}
value.forEach((item, index) => validate(item, rule.items, `${path}[${index}]`));
}
if(rule.type === 'string')
+113
View File
@@ -0,0 +1,113 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
const schema = JSON.parse(readFileSync(new URL('../schemas/protocol.schema.json', import.meta.url), 'utf8'));
const ajv = new Ajv2020({ strict: true, allErrors: true });
addFormats(ajv);
ajv.addSchema(schema);
const fixture = name => JSON.parse(readFileSync(new URL(`../fixtures/${name}`, import.meta.url), 'utf8'));
const valid = (type, value) => ajv.validate(`${schema.$id}#/$defs/${type}`, value);
const settings = fixture('realtime-settings.payload.json');
const hello = fixture('realtime-hello.message.json');
const extension = fixture('printing-realtime-extension.discovery.json');
assert(valid('PrintingRealtimeExtensionDiscovery', extension), ajv.errorsText());
assert.equal(extension.version, '2.0.0');
assert(valid('PrintingRealtimeExtensionDiscovery', { ...extension, version: '2.7.3' }));
for(const version of ['1.0.0', '1.1.0', '3.0.0', '2.0', '2.01.0', '2.0.0-rc.1', null])
{
assert(!valid('PrintingRealtimeExtensionDiscovery', { ...extension, version }), 'Only the compatible stable native major is accepted.');
}
assert(!valid('PrintingRealtimeExtensionDiscovery', { ...extension, name: 'another-extension' }));
assert(!valid('PrintingRealtimeExtensionDiscovery', { ...extension, pathPrefix: '/printing/v1' }));
/*******************************************************************************
** Cross-field policy relationships require semantic checks beyond JSON Schema.
*******************************************************************************/
function assertPolicy(policy, limits)
{
assert(valid('RealtimeSettings', policy), ajv.errorsText());
assert(valid('RealtimeClientLimits', limits), ajv.errorsText());
assert(policy.maxQueuedFrameBytes >= policy.maxFrameBytes, 'Frame queue must hold one permitted message.');
assert(policy.maxStagedBytes >= Math.max(policy.maxPdfBytes, policy.maxZplBytes), 'Staging must admit one permitted document.');
assert(policy.downloadTimeoutMillis >= policy.downloadConnectTimeoutMillis);
assert(policy.downloadTimeoutMillis >= policy.downloadIdleTimeoutMillis);
assert(policy.heartbeatTimeoutSeconds >= 2 * policy.heartbeatIntervalSeconds);
assert(policy.reconnectMaxDelayMillis >= policy.reconnectMinDelayMillis);
for(const name of schema.$defs.RealtimeClientLimits.required)
{
assert(policy[name] <= limits[name], `${name}: policy exceeds declared client capability`);
}
}
assertPolicy(settings, hello.payload.limits);
assert.equal(settings.maxConcurrentDownloads, 1, 'Initial deployment defaults to one configurable download lane.');
assert.equal(settings.maxCopies, 100);
assert.equal(settings.nativeSubmissionTimeoutMillis, 120000);
for(const [name, rule] of Object.entries(schema.$defs.RealtimeSettings.properties))
{
if(rule.default !== undefined) assert.equal(settings[name], rule.default, `${name}: fixture/schema default mismatch`);
const missing = { ...settings };
delete missing[name];
assert(!valid('RealtimeSettings', missing), `${name}: missing value cannot inherit a default`);
for(const value of [rule.minimum - 1, rule.maximum + 1, 1.5, null])
{
assert(!valid('RealtimeSettings', { ...settings, [name]: value }), `${name}: invalid bound/type accepted`);
}
}
for(const change of [
{ maxQueuedFrameBytes: 8192 },
{ maxStagedBytes: 1 },
{ downloadTimeoutMillis: 1000 },
{ heartbeatTimeoutSeconds: 10 },
{ reconnectMinDelayMillis: 60000, reconnectMaxDelayMillis: 1000 }
])
{
assert.throws(() => assertPolicy({ ...settings, ...change }, hello.payload.limits));
}
for(const name of schema.$defs.RealtimeClientLimits.required)
{
assert.throws(() => assertPolicy(settings, { ...hello.payload.limits, [name]: settings[name] - 1 }), name);
}
assertPolicy({ ...settings, maxConcurrentDownloads: 16, maxConcurrentPrints: 16, maxCopies: 32767 }, hello.payload.limits);
assert(!valid('RealtimeHello', { ...hello, payload: { ...hello.payload, capabilities: [] } }));
assert(!valid('RealtimeHello', { ...hello, payload: { ...hello.payload, capabilities: ['CANCEL_SPOOL'] } }));
assert(!valid('RealtimeHello', { ...hello, payload: { ...hello.payload, limits: {} } }));
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: {} }));
const clientFixtures = ['hello', 'state-applied', 'job-event', 'command-receipt', 'heartbeat', 'inventory', 'ping'];
const serverFixtures = ['welcome', 'desired-state', 'job-available', 'command', 'settings', 'ack', 'error', 'pong'];
for(const [files, accepted, rejected] of [
[clientFixtures, 'RealtimeClientMessage', 'RealtimeServerMessage'],
[serverFixtures, 'RealtimeServerMessage', 'RealtimeClientMessage']
])
{
for(const name of files)
{
const message = fixture(`realtime-${name}.message.json`);
assert(valid(accepted, message), `${name}: ${ajv.errorsText()}`);
assert(!valid(rejected, message), `${name}: wrong-direction frame accepted`);
assert(Buffer.byteLength(JSON.stringify(message), 'utf8') <= settings.maxFrameBytes);
const missing = { ...message };
delete missing.payload;
assert(!valid(accepted, missing), `${name}: missing payload accepted`);
assert(!valid(accepted, { ...message, contentUrl: 'https://unrelated.invalid/document' }));
}
}
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(fixture('realtime-ack.message.json').payload.acknowledgedMessageId, event.messageId);
assert.equal(fixture('realtime-error.message.json').messageId, event.messageId, 'Frame errors retain request correlation.');
const largeJob = fixture('realtime-large-pdf.response.json');
assert(valid('PrintJob', largeJob), ajv.errorsText());
assert.equal(largeJob.contentLength, settings.maxPdfBytes);
assert(largeJob.contentLength > 20 * 1024 * 1024);
assert(valid('PrintOptions', { copies: 32767 }));
assert(!valid('PrintOptions', { copies: 32768 }));
console.log('Realtime directional frames, defaults/ranges, policy/ceiling refusal and exact replay fixtures passed; runtime concurrency acceptance remains consumer-owned.');
+4
View File
@@ -1,8 +1,12 @@
// Copyright 2026 Kingsrook, LLC
// SPDX-License-Identifier: Apache-2.0
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import Ajv2020 from 'ajv/dist/2020.js';
import addFormats from 'ajv-formats';
import { fixtureTypes } from './fixture-types.mjs';
import './verify-realtime.mjs';
const schema = JSON.parse(readFileSync(new URL('../schemas/protocol.schema.json', import.meta.url), 'utf8'));
const ajv = new Ajv2020({ strict: true, allErrors: true });