From 0ebe7f739ea3dbad1b627599d3d717f19e07f469 Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Tue, 22 Sep 2026 16:49:22 -0400 Subject: [PATCH 1/2] feat: align proposal with CompleteTech Skills package contract --- .github/workflows/package-contract.yml | 24 ++++ CONTRIBUTING.md | 32 +++++ ONBOARDING.md | 64 ++++++++++ README.md | 2 + scripts/validate_package.py | 164 +++++++++++++++++++++++++ skill-package.json | 11 ++ tests/test_package_contract.py | 146 ++++++++++++++++++++++ 7 files changed, 443 insertions(+) create mode 100644 .github/workflows/package-contract.yml create mode 100644 CONTRIBUTING.md create mode 100644 ONBOARDING.md create mode 100644 scripts/validate_package.py create mode 100644 skill-package.json create mode 100644 tests/test_package_contract.py diff --git a/.github/workflows/package-contract.yml b/.github/workflows/package-contract.yml new file mode 100644 index 0000000..07c7bbb --- /dev/null +++ b/.github/workflows/package-contract.yml @@ -0,0 +1,24 @@ +name: Package contract + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + validate-package: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Test the shared contract + run: python -m unittest discover -s tests -p 'test_package_*.py' -v + - name: Validate this skill checkout + run: python scripts/validate_package.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f0afc98 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,32 @@ +# Contributing to CompleteTech LLC Skills + +[Start here](ONBOARDING.md) · [Agent instructions](SKILL.md) · [Brand asset policy](BRAND_ASSETS.md) + +## Scope and compatibility + +Keep each skill independently usable. Preserve its skill key, CLI entry points, specialist templates, approval gates and documented network boundaries. The usage ledger retains its standard-library runtime; generator skills retain their root `generate_*.py` entry points. Do not move these solely to make directory trees identical. + +Use a focused branch and PR. Describe the affected skill, user-visible change, compatibility impact, tests and any unverified behavior. Do not bypass branch protection, required review or failed checks. + +## Package checks + +From a full GitHub checkout, with Python 3.12 (the CI baseline): + +```bash +python -m unittest discover -s tests -p test_package_contract.py -v +python scripts/validate_package.py +``` + +The package contract is standard-library-only and read-only: it checks metadata, navigation, local paths and the existing logo. It does not execute generators or certify document content. Also run this repository's existing quality and regression commands documented in [README.md](README.md). Where present, `scripts/validate_quality.py` remains an independent gate; do not remove or weaken it to pass package validation. + +## Documentation and branding + +Keep [ONBOARDING.md](ONBOARDING.md), `skill-package.json` and the actual entry points synchronized. Use **CompleteTech LLC Skills** for the family navigation and the existing `assets/logo.png`; do not substitute unrelated artwork. Keep outputs and temporary experiments under `output/` or a temporary directory, not over committed previews. + +Code, templates and documentation retain their existing [MIT license](LICENSE). Names, logos, seals and other brand assets remain subject to [BRAND_ASSETS.md](BRAND_ASSETS.md); this package work grants no additional rights. Do not copy private assets or redistribute bundled fonts as part of a public package update. + +## Evidence and privacy + +Use synthetic fixtures. Never commit credentials, completion keys, personal logs, client documents, private account mappings or unapproved quotes. A successful render is not legal approval, payment authorization, verified attendance, security signoff, client acceptance or permission to publish/send. + +Shared contract changes should be coordinated through the orchestrator's skill-library catalog and applied consistently to the affected siblings. `schema_version` versions the package contract, not a ClawHub release. Registry publication and repository visibility changes require separate explicit authorization. diff --git a/ONBOARDING.md b/ONBOARDING.md new file mode 100644 index 0000000..24e6c08 --- /dev/null +++ b/ONBOARDING.md @@ -0,0 +1,64 @@ +# Start here: agentic-proposal-skill + +

CompleteTech LLC logo

+ +**CompleteTech LLC Skills** — Proposals and statements of work. + +[Overview](README.md) · [Agent instructions](SKILL.md) · [Contributing](CONTRIBUTING.md) · [Skill library](https://github.com/CompleteTech-LLC/agentic-services-orchestrator-skill/blob/main/references/skill-family.md) + +## 1. Choose the right skill + +This is a `catalog-renderer`. Its install/activation key is **`agentic-proposal-skill`**. Use the orchestrator when work spans multiple specialists; install only the specialists the engagement needs. Install the complete skill directory, including `SKILL.md`, references, scripts and assets, in the directory supported by your agent. Do not install only the Markdown file. Keep this skill separate from its siblings. + +## 2. Get a full checkout and prepare Python + +Python 3.12 is the shared CI baseline. Git is needed only for cloning. The full GitHub checkout includes the existing brand assets; text-only registry packages can omit binary logos/previews and are not the target of the checkout validator. + +```bash +git clone https://github.com/CompleteTech-LLC/agentic-proposal-skill.git agentic-proposal-skill +cd agentic-proposal-skill +python3 -m venv .venv +. .venv/bin/activate +python -m pip install -r requirements.txt +python scripts/validate_package.py +``` + +On Windows PowerShell, create the environment with `py -3 -m venv .venv`, then use `.\.venv\Scripts\python.exe` instead of `python` in subsequent commands. Activation is optional; do not change machine execution policy for this skill. For an existing checkout, skip `git clone` and start at environment setup. + +## 3. Run a safe demonstration + +Run from the skill root. The commands below use bundled synthetic/example inputs, not client data. They can replace an earlier demonstration at the same output path; choose a new filename when retaining prior output. + +```bash +python scripts/render_proposal.py --list +python scripts/render_pdf.py --markdown assets/examples/example.md --out output/onboarding-demo.pdf --logo assets/logo.png --title "Onboarding demonstration" +``` + +The catalog lists available templates; the second command renders the bundled demonstration Markdown to `output/onboarding-demo.pdf`. This is a rendering smoke test, not approval of its illustrative facts. + +Open the generated output and inspect page breaks, tables, logo, text and any remaining placeholders. Package validation checks structure; it does not certify visual quality or factual correctness. Committed previews in `assets/examples/` are references, not destinations for your first run. + +## 4. Use the skill with your agent + +Read [SKILL.md](SKILL.md), select `agentic-proposal-skill` using your agent's skill activation mechanism, and provide verified inputs, the requested artifact, destination and required approval owners. Agent discovery paths differ by product; follow that product's installation documentation rather than assuming one global path. Do not grant broader permissions simply because multiple skills are installed together. + +## Branding and handoff + +Use the existing CompleteTech LLC logo and the practical, evidence-led voice described in [README.md](README.md). The code license does not grant logo, seal or signature rights: follow [BRAND_ASSETS.md](BRAND_ASSETS.md). Do not replace or republish private artwork or fonts. + +For a multi-skill handoff, preserve the source facts, artifact paths, approval owner/status, blockers and next specialist in the orchestrator's existing `project_state` schema. Do not treat a generated artifact, anonymization or successful check as permission to send, publish, bill, sign, award or launch. + +## Safety and permissions + +The documented generation workflow is local. Installation may contact GitHub and package indexes, but generation does not send messages, move money, publish documents, grant approval or install background services. Review the specialist boundaries in [SKILL.md](SKILL.md) before using real inputs. + +## Troubleshooting and verification + +A missing module usually means the active interpreter differs from the one used for `pip`; use `python -m pip` in the same environment and this repository's `requirements.txt`. Missing logos or references usually indicate a partial/text-only installation or the wrong working directory; use a full checkout and run from its root. Do not download replacement branding from unrelated sources. + +```bash +python -m unittest discover -s tests -p test_package_contract.py -v +python scripts/validate_package.py +``` + +These checks use the standard library and do not execute the listed generators, scan accounts, contact services or install schedules. Follow [CONTRIBUTING.md](CONTRIBUTING.md) and the repository's existing quality instructions for the additional runtime, rendering, parser and diagram checks. diff --git a/README.md b/README.md index b4a166f..62d01df 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +**CompleteTech LLC Skills** · [Start here](ONBOARDING.md) · [Agent instructions](SKILL.md) · [Skill library](https://github.com/CompleteTech-LLC/agentic-services-orchestrator-skill/blob/main/references/skill-family.md) · [Contributing](CONTRIBUTING.md) + # Agentic Proposal Skill

diff --git a/scripts/validate_package.py b/scripts/validate_package.py new file mode 100644 index 0000000..d712c2a --- /dev/null +++ b/scripts/validate_package.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Validate the CompleteTech LLC Skills checkout contract; no execution or network.""" +from __future__ import annotations + +import argparse +import json +import re +import struct +import zlib +from pathlib import Path + +FAMILY = "completetech-skills" +FIELDS = {"schema_version", "family", "repository", "skill_name", "kind", "entrypoints", "example_inputs", "network_mode", "private"} +FILES = ("README.md", "SKILL.md", "LICENSE", "BRAND_ASSETS.md", "ONBOARDING.md", "CONTRIBUTING.md", "requirements.txt", "agents/openai.yaml") +KINDS = {"catalog-renderer", "config-generator", "orchestrator", "usage-ledger"} +NETWORK_MODES = {"local", "operator-selected-hosts", "optional-receipts"} +SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") +NAVIGATION = ("CompleteTech LLC Skills", "[Start here](ONBOARDING.md)", "[Contributing](CONTRIBUTING.md)", "assets/logo.png") + + +def unique_object(pairs: list[tuple[str, object]]) -> dict: + result: dict = {} + for key, value in pairs: + if key in result: + raise ValueError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +def local_file(root: Path, value: object) -> Path: + if not isinstance(value, str) or not value or "\\" in value or ":" in value: + raise ValueError(f"expected a relative POSIX file path: {value!r}") + path = Path(value) + if path.is_absolute() or ".." in path.parts or value.startswith("."): + raise ValueError(f"unsafe package path: {value!r}") + resolved = (root / path).resolve() + if not resolved.is_relative_to(root.resolve()): + raise ValueError(f"package path escapes checkout: {value!r}") + if not resolved.is_file(): + raise ValueError(f"missing package file: {value}") + return resolved + + +def validate_png(path: Path) -> None: + """Check a bounded PNG chunk envelope and CRCs, not pixel decoding or artwork.""" + limit = 16 * 1024 * 1024 + with path.open("rb") as handle: + data = handle.read(limit + 1) + if len(data) > limit or not data.startswith(b"\x89PNG\r\n\x1a\n"): + raise ValueError("logo must be a PNG of at most 16 MiB") + offset = 8 + seen_header = False + seen_data = False + while offset + 12 <= len(data): + length = struct.unpack_from(">I", data, offset)[0] + kind = data[offset + 4:offset + 8] + end = offset + 12 + length + if end > len(data): + raise ValueError("truncated PNG chunk") + payload = data[offset + 8:end - 4] + crc = struct.unpack_from(">I", data, end - 4)[0] + if zlib.crc32(kind + payload) != crc: + raise ValueError("PNG chunk checksum mismatch") + if not seen_header and kind != b"IHDR": + raise ValueError("PNG must start with IHDR") + if kind == b"IHDR": + if seen_header or length != 13: + raise ValueError("invalid PNG header") + width, height, depth, color, compression, filtering, interlace = struct.unpack(">IIBBBBB", payload) + depths = {0: (1, 2, 4, 8, 16), 2: (8, 16), 3: (1, 2, 4, 8), 4: (8, 16), 6: (8, 16)} + if not width or not height or depth not in depths.get(color, ()) or compression or filtering or interlace not in (0, 1): + raise ValueError("invalid PNG image parameters") + seen_header = True + elif kind == b"IDAT" and length: + seen_data = True + elif kind == b"IEND": + if length or not seen_data or end != len(data): + raise ValueError("invalid PNG end or missing image data") + return + offset = end + raise ValueError("PNG missing complete IHDR/IDAT/IEND structure") + + +def validate(root: Path) -> list[str]: + errors: list[str] = [] + root = root.resolve() + try: + manifest_path = local_file(root, "skill-package.json") + manifest = json.loads(manifest_path.read_text(encoding="utf-8"), object_pairs_hook=unique_object) + except (OSError, UnicodeError, ValueError) as exc: + return [f"manifest: {exc}"] + if not isinstance(manifest, dict): + return ["manifest must be a JSON object"] + if set(manifest) != FIELDS: + errors.append(f"manifest fields mismatch: missing={sorted(FIELDS - set(manifest))}, unknown={sorted(set(manifest) - FIELDS)}") + if type(manifest.get("schema_version")) is not int or manifest.get("schema_version") != 1: + errors.append("schema_version must be integer 1") + if manifest.get("family") != FAMILY: + errors.append(f"family must be {FAMILY}") + name = manifest.get("skill_name") + if not isinstance(name, str) or not SLUG.fullmatch(name) or len(name) > 64: + errors.append("skill_name must be a lowercase hyphenated slug of at most 64 characters") + repo = manifest.get("repository") + if not isinstance(repo, str) or not re.fullmatch(r"CompleteTech-LLC/[a-z0-9]+(?:-[a-z0-9]+)*", repo): + errors.append("repository must be CompleteTech-LLC/") + elif isinstance(name, str) and repo.split("/")[1] not in (name, name + "-skill"): + errors.append("repository and skill_name do not describe the same skill") + if manifest.get("kind") not in tuple(KINDS): + errors.append("unsupported kind") + if manifest.get("network_mode") not in tuple(NETWORK_MODES): + errors.append("unsupported network_mode") + if type(manifest.get("private")) is not bool: + errors.append("private must be a JSON boolean") + paths = list(FILES) + ["assets/logo.png"] + for field in ("entrypoints", "example_inputs"): + values = manifest.get(field) + if not isinstance(values, list) or not values: + errors.append(f"{field} must be a non-empty array") + continue + if all(isinstance(value, str) for value in values) and len(set(values)) != len(values): + errors.append(f"{field} contains duplicate paths") + paths.extend(values) + for value in paths: + try: + local_file(root, value) + except (OSError, ValueError) as exc: + errors.append(str(exc)) + try: + skill = local_file(root, "SKILL.md").read_text(encoding="utf-8") + header = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|\Z)", skill, re.S) + declared = re.search(r'''(?m)^name:\s*(['"]?)([a-z0-9-]+)\1[ \t]*(?:\#.*)?$''', header.group(1)) if header else None + if not declared or declared.group(2) != name: + errors.append("skill_name must match the scalar name in SKILL.md frontmatter") + readme = local_file(root, "README.md").read_text(encoding="utf-8") + for marker in NAVIGATION: + if marker not in readme: + errors.append(f"README.md missing family navigation: {marker}") + validate_png(local_file(root, "assets/logo.png")) + for document in ("ONBOARDING.md", "CONTRIBUTING.md"): + text = local_file(root, document).read_text(encoding="utf-8") + for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", text): + if target.startswith(("https://", "http://", "#")): + continue + local_file(root, target.split("#", 1)[0]) + except (OSError, UnicodeError, ValueError) as exc: + errors.append(str(exc)) + return errors + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) + args = parser.parse_args() + errors = validate(args.root) + for error in errors: + print(f"ERROR: {error}") + if errors: + return 1 + print("CompleteTech LLC Skills package contract OK (full checkout; no commands executed)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skill-package.json b/skill-package.json new file mode 100644 index 0000000..1359c29 --- /dev/null +++ b/skill-package.json @@ -0,0 +1,11 @@ +{ + "schema_version": 1, + "family": "completetech-skills", + "repository": "CompleteTech-LLC/agentic-proposal-skill", + "skill_name": "agentic-proposal-skill", + "kind": "catalog-renderer", + "entrypoints": ["scripts/render_proposal.py", "scripts/render_pdf.py"], + "example_inputs": ["assets/examples/example.md"], + "network_mode": "local", + "private": false +} diff --git a/tests/test_package_contract.py b/tests/test_package_contract.py new file mode 100644 index 0000000..0a68c78 --- /dev/null +++ b/tests/test_package_contract.py @@ -0,0 +1,146 @@ +"""Offline regression tests for the shared package contract, using synthetic files.""" +from __future__ import annotations + +import importlib.util +import json +import tempfile +import struct +import zlib +import unittest +from pathlib import Path + +MODULE = Path(__file__).resolve().parents[1] / "scripts" / "validate_package.py" +SPEC = importlib.util.spec_from_file_location("validate_package", MODULE) +assert SPEC is not None and SPEC.loader is not None +package = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(package) + + +def png_fixture() -> bytes: + def chunk(kind: bytes, payload: bytes) -> bytes: + return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", zlib.crc32(kind + payload)) + return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)) + + chunk(b"IDAT", zlib.compress(b"\x00\x00\x00\x00")) + chunk(b"IEND", b"")) + + +class PackageContractTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.root = Path(self.temp.name) / "checkout" + self.root.mkdir() + self.manifest = dict(schema_version=1, family=package.FAMILY, + repository="CompleteTech-LLC/example-skill", skill_name="example-skill", + kind="catalog-renderer", entrypoints=["scripts/render.py"], + example_inputs=["examples/input.md"], network_mode="local", private=False) + for value in (*package.FILES, "scripts/render.py", "examples/input.md"): + path = self.root / value + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("demo\n", encoding="utf-8") + (self.root / "SKILL.md").write_text("---\nname: example-skill\ndescription: Demo\n---\n", encoding="utf-8") + (self.root / "README.md").write_text("\n".join(package.NAVIGATION), encoding="utf-8") + (self.root / "assets").mkdir() + (self.root / "assets/logo.png").write_bytes(png_fixture()) + self.save() + + def save(self): + (self.root / "skill-package.json").write_text(json.dumps(self.manifest), encoding="utf-8") + + def test_valid(self): + self.assertEqual(package.validate(self.root), []) + + def test_ledger_repository_name_differs_from_install_name(self): + self.manifest.update(repository="CompleteTech-LLC/ai-usage-ledger-skill", skill_name="ai-usage-ledger", kind="usage-ledger", network_mode="operator-selected-hosts") + (self.root / "SKILL.md").write_text("---\nname: ai-usage-ledger\n---\n", encoding="utf-8") + self.save() + self.assertEqual(package.validate(self.root), []) + + def test_schema_rejects_bad_types_and_unknown_values(self): + for key, value in (("schema_version", True), ("family", "other"), ("kind", []), ("network_mode", {}), ("private", "false"), ("repository", "other/example"), ("skill_name", "../example"), ("entrypoints", []), ("example_inputs", "input.md")): + with self.subTest(key=key): + original = self.manifest[key] + self.manifest[key] = value + self.save() + self.assertTrue(package.validate(self.root)) + self.manifest[key] = original + self.save() + + def test_unknown_and_missing_fields(self): + self.manifest["typo"] = True + del self.manifest["private"] + self.save() + self.assertTrue(package.validate(self.root)) + + def test_duplicate_json_keys(self): + (self.root / "skill-package.json").write_text('{"family":"a","family":"b"}', encoding="utf-8") + self.assertIn("duplicate JSON key", package.validate(self.root)[0]) + + def test_invalid_or_non_object_json(self): + for text in ("{", "[]", "null"): + (self.root / "skill-package.json").write_text(text, encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_unsafe_and_missing_paths(self): + for value in ("../outside", "/tmp/outside", "C:/outside", "scripts\\render.py", "missing.py", None, {}, ""): + with self.subTest(path=value): + self.manifest["entrypoints"] = [value] + self.save() + self.assertTrue(package.validate(self.root)) + + def test_duplicate_paths(self): + self.manifest["entrypoints"] *= 2 + self.save() + self.assertTrue(package.validate(self.root)) + + def test_symlink_escape(self): + outside = Path(self.temp.name) / "outside.py" + outside.write_text("private", encoding="utf-8") + path = self.root / "scripts/render.py" + path.unlink() + try: + path.symlink_to(outside) + except OSError as exc: + self.skipTest(str(exc)) + self.assertTrue(package.validate(self.root)) + + def test_missing_brand_asset_and_invalid_png(self): + logo = self.root / "assets/logo.png" + logo.write_text("not an image", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + logo.unlink() + self.assertTrue(package.validate(self.root)) + + def test_truncated_or_corrupted_png_rejected(self): + valid = png_fixture() + for data in (valid[:8], valid[:-12], valid + b"junk", valid[:-1] + bytes([valid[-1] ^ 1])): + with self.subTest(length=len(data)): + (self.root / "assets/logo.png").write_bytes(data) + self.assertTrue(package.validate(self.root)) + + def test_frontmatter_mismatch(self): + (self.root / "SKILL.md").write_text("---\nname: another-skill\n---\n", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_quoted_frontmatter_and_crlf(self): + (self.root / "SKILL.md").write_bytes(b'---\r\nname: "example-skill"\r\n---\r\n') + self.assertEqual(package.validate(self.root), []) + + def test_missing_navigation(self): + (self.root / "README.md").write_text("demo", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_broken_onboarding_link(self): + (self.root / "ONBOARDING.md").write_text("[Missing](missing.md)", encoding="utf-8") + self.assertTrue(package.validate(self.root)) + + def test_valid_local_external_and_anchor_links(self): + (self.root / "ONBOARDING.md").write_text("[Readme](README.md#demo) [Web](https://example.com) [Here](#demo)", encoding="utf-8") + self.assertEqual(package.validate(self.root), []) + + def test_never_executes_declared_entrypoints(self): + (self.root / "scripts/render.py").write_text("raise RuntimeError('must not execute')", encoding="utf-8") + self.assertEqual(package.validate(self.root), []) + + +if __name__ == "__main__": + unittest.main() From 604b97c0053e911f0a0bac26444db5688263533a Mon Sep 17 00:00:00 2001 From: Timothy Wayne Gregg Date: Tue, 22 Sep 2026 17:55:01 -0400 Subject: [PATCH 2/2] feat: align branding, handoffs and cross-platform package checks --- .editorconfig | 10 ++ .github/PULL_REQUEST_TEMPLATE.md | 15 +++ .github/workflows/package-contract.yml | 19 ++- AGENTS.md | 9 ++ BRANDING.md | 30 +++++ CONTRIBUTING.md | 28 ++-- scripts/validate_package.py | 180 +++++++++++-------------- tests/test_package_contract.py | 155 +++++++++------------ 8 files changed, 223 insertions(+), 223 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 AGENTS.md create mode 100644 BRANDING.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6e4cda6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2df1088 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Change and affected skills + +Describe the user-visible change, sibling dependencies and compatibility impact. + +## Evidence + +Record commands, results and exact tested commit. Distinguish structural checks, runtime tests and visual review. Name checks not run and why. + +## Boundaries + +Confirm that activation keys, consent, private assets, approval gates and specialist entry points remain intact. Identify any intentional exception. No registry release, visibility change or live external action is implied. + +## Family consistency + +For shared changes, identify affected sibling PRs and the orchestrator drift-audit result. Do not claim coordinated merge until each PR is confirmed merged. diff --git a/.github/workflows/package-contract.yml b/.github/workflows/package-contract.yml index 07c7bbb..c115ce6 100644 --- a/.github/workflows/package-contract.yml +++ b/.github/workflows/package-contract.yml @@ -1,15 +1,14 @@ name: Package contract - -on: - push: - pull_request: - +on: [push, pull_request] permissions: contents: read - jobs: validate-package: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} timeout-minutes: 5 steps: - uses: actions/checkout@v4 @@ -17,8 +16,8 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: "3.12" - - name: Test the shared contract + python-version: '3.12' + - name: Test the package contract run: python -m unittest discover -s tests -p 'test_package_*.py' -v - - name: Validate this skill checkout + - name: Validate the full checkout run: python scripts/validate_package.py diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d95b554 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# CompleteTech LLC Skills: maintainer instructions + +Read [SKILL.md](SKILL.md) for specialist behavior, [ONBOARDING.md](ONBOARDING.md) for first use, [BRANDING.md](BRANDING.md) for presentation and handoffs, and [CONTRIBUTING.md](CONTRIBUTING.md) before changing this checkout. + +Preserve the activation key, existing CLI paths, templates, dependencies, approval gates and consent boundaries. A common package does not require identical generators. Keep private assets private and use synthetic fixtures. Never scan personal logs, submit certificate data, install schedules, send messages or publish artifacts merely to test alignment. + +Run `python -m unittest discover -s tests -p 'test_package_*.py' -v` and `python scripts/validate_package.py`, then the existing Quality checks. Do not disable failing checks or replace verified evidence with successful rendering. Describe any checks not run. + +Shared files are maintained together through the orchestrator's catalog and drift audit. Update affected siblings in focused PRs; review exact heads and checks before merging. Registry publishing and visibility changes are separate actions. Never claim a PR exists or merged without its successful GitHub response. diff --git a/BRANDING.md b/BRANDING.md new file mode 100644 index 0000000..97df1de --- /dev/null +++ b/BRANDING.md @@ -0,0 +1,30 @@ +# CompleteTech LLC Skills: branding and handoffs + +[Start here](ONBOARDING.md) · [Asset rights](BRAND_ASSETS.md) · [Specialist instructions](SKILL.md) + +## Resolve identity before rendering + +An approved engagement brand takes precedence over a starter example. Preserve the operator's explicit settings; ask about unresolved identity instead of silently applying company branding. The ledger remains neutral by default. CompleteTech examples identify this library, not permission to represent another business. + +Use the existing local `assets/logo.png` for authorized CompleteTech output. Do not synthesize replacement marks, fetch remote artwork, redistribute fonts, or transfer private seals/signatures to public repositories. The MIT code license does not replace the asset policy. + +## Starter palette + +These established library colors are a reference, not new runtime defaults: + +| Token | Value | +|---|---| +| Accent | `#1E3A8A` | +| Ink | `#0F172A` | +| Soft accent | `#EEF2FF` | +| Muted text | `#64748B` | +| Border | `#E2E8F0` | +| Alternating row | `#F8FAFC` | + +Keep specialist layouts: invoices, envelopes, certificates, proposals and dashboards have different constraints. Use existing renderer options, generator configuration or the ledger's explicit branding preset, as documented in this skill's README and `--help`. This document does not introduce a universal brand-import flag. + +## Handoff to the next skill + +Carry verified facts and their sources, artifact path/version, approved identity and local logo path, palette overrides, confidentiality, intended audience, approval owner/status, blockers and next specialist. Preserve these in the orchestrator's existing `project_state`; do not replace its workflow schema with an incompatible parallel state format. + +Missing approval stays unknown or draft. Rendering, anonymization, a clean package check or a reused brand does not authorize sending, publication, billing, signing, attendance claims or launch. Review the actual output for clipping, contrast, placeholders and factual accuracy before approved use. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0afc98..62909d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,32 +1,24 @@ # Contributing to CompleteTech LLC Skills -[Start here](ONBOARDING.md) · [Agent instructions](SKILL.md) · [Brand asset policy](BRAND_ASSETS.md) +[Start here](ONBOARDING.md) · [Branding and handoffs](BRANDING.md) · [Maintainer instructions](AGENTS.md) · [Specialist instructions](SKILL.md) -## Scope and compatibility +## Compatibility -Keep each skill independently usable. Preserve its skill key, CLI entry points, specialist templates, approval gates and documented network boundaries. The usage ledger retains its standard-library runtime; generator skills retain their root `generate_*.py` entry points. Do not move these solely to make directory trees identical. +Keep each skill independently usable. Preserve activation keys, root generator CLIs, templates, runtime dependencies, consent and approval gates. The ledger's standard-library core and neutral branding are intentional differences, not gaps to remove. -Use a focused branch and PR. Describe the affected skill, user-visible change, compatibility impact, tests and any unverified behavior. Do not bypass branch protection, required review or failed checks. +## Verification -## Package checks - -From a full GitHub checkout, with Python 3.12 (the CI baseline): +From a full checkout using Python 3.12, the package CI baseline: ```bash -python -m unittest discover -s tests -p test_package_contract.py -v +python -m unittest discover -s tests -p 'test_package_*.py' -v python scripts/validate_package.py ``` -The package contract is standard-library-only and read-only: it checks metadata, navigation, local paths and the existing logo. It does not execute generators or certify document content. Also run this repository's existing quality and regression commands documented in [README.md](README.md). Where present, `scripts/validate_quality.py` remains an independent gate; do not remove or weaken it to pass package validation. - -## Documentation and branding - -Keep [ONBOARDING.md](ONBOARDING.md), `skill-package.json` and the actual entry points synchronized. Use **CompleteTech LLC Skills** for the family navigation and the existing `assets/logo.png`; do not substitute unrelated artwork. Keep outputs and temporary experiments under `output/` or a temporary directory, not over committed previews. - -Code, templates and documentation retain their existing [MIT license](LICENSE). Names, logos, seals and other brand assets remain subject to [BRAND_ASSETS.md](BRAND_ASSETS.md); this package work grants no additional rights. Do not copy private assets or redistribute bundled fonts as part of a public package update. +Also run the existing quality and regression commands in [README.md](README.md). Package checks are read-only and do not execute specialist generators. They validate metadata, required files, local documentation links and PNG chunk integrity, not decoded artwork or document truth. Do not weaken original Quality checks to obtain a green result. -## Evidence and privacy +## Coordinated changes -Use synthetic fixtures. Never commit credentials, completion keys, personal logs, client documents, private account mappings or unapproved quotes. A successful render is not legal approval, payment authorization, verified attendance, security signoff, client acceptance or permission to publish/send. +Keep `skill-package.json`, onboarding and actual entry points synchronized. Shared validator, tests, CI, maintainer instructions and branding guidance must match the orchestrator's shared-file audit. Use focused PRs and report exact checks, failures and limitations. Respect branch protection, review requirements and changed heads. -Shared contract changes should be coordinated through the orchestrator's skill-library catalog and applied consistently to the affected siblings. `schema_version` versions the package contract, not a ClawHub release. Registry publication and repository visibility changes require separate explicit authorization. +Use synthetic fixtures and new output paths instead of overwriting committed previews. Never commit secrets, personal logs, real account mappings, client documents or unapproved quotes. Code remains under [LICENSE](LICENSE); branding remains under [BRAND_ASSETS.md](BRAND_ASSETS.md). Do not redistribute private assets or fonts. Registry releases and repository visibility changes require separate authorization. diff --git a/scripts/validate_package.py b/scripts/validate_package.py index d712c2a..e7dbf4c 100644 --- a/scripts/validate_package.py +++ b/scripts/validate_package.py @@ -1,21 +1,20 @@ #!/usr/bin/env python3 -"""Validate the CompleteTech LLC Skills checkout contract; no execution or network.""" +"""Read-only, standard-library validation of a full CompleteTech skill checkout.""" from __future__ import annotations - import argparse import json import re import struct import zlib from pathlib import Path +from urllib.parse import unquote, urlsplit FAMILY = "completetech-skills" -FIELDS = {"schema_version", "family", "repository", "skill_name", "kind", "entrypoints", "example_inputs", "network_mode", "private"} -FILES = ("README.md", "SKILL.md", "LICENSE", "BRAND_ASSETS.md", "ONBOARDING.md", "CONTRIBUTING.md", "requirements.txt", "agents/openai.yaml") -KINDS = {"catalog-renderer", "config-generator", "orchestrator", "usage-ledger"} -NETWORK_MODES = {"local", "operator-selected-hosts", "optional-receipts"} -SLUG = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*") +FIELDS = set("schema_version family repository skill_name kind entrypoints example_inputs network_mode private".split()) +FILES = ("README.md", "SKILL.md", "LICENSE", "BRAND_ASSETS.md", "ONBOARDING.md", "CONTRIBUTING.md", "BRANDING.md", "AGENTS.md", "requirements.txt", "agents/openai.yaml") NAVIGATION = ("CompleteTech LLC Skills", "[Start here](ONBOARDING.md)", "[Contributing](CONTRIBUTING.md)", "assets/logo.png") +KINDS = ("catalog-renderer", "config-generator", "orchestrator", "usage-ledger") +NETWORK_MODES = ("local", "operator-selected-hosts", "optional-receipts") def unique_object(pairs: list[tuple[str, object]]) -> dict: @@ -28,137 +27,108 @@ def unique_object(pairs: list[tuple[str, object]]) -> dict: def local_file(root: Path, value: object) -> Path: - if not isinstance(value, str) or not value or "\\" in value or ":" in value: - raise ValueError(f"expected a relative POSIX file path: {value!r}") - path = Path(value) - if path.is_absolute() or ".." in path.parts or value.startswith("."): + if not isinstance(value, str) or not value or any(ord(c) < 32 for c in value): + raise ValueError(f"invalid package path: {value!r}") + if "\\" in value or ":" in value or value.startswith("/") or any(p in ("", ".", "..") for p in value.split("/")): raise ValueError(f"unsafe package path: {value!r}") - resolved = (root / path).resolve() - if not resolved.is_relative_to(root.resolve()): - raise ValueError(f"package path escapes checkout: {value!r}") - if not resolved.is_file(): - raise ValueError(f"missing package file: {value}") - return resolved + path = (root / value).resolve() + if not path.is_relative_to(root.resolve()) or not path.is_file(): + raise ValueError(f"missing or escaping package file: {value}") + return path def validate_png(path: Path) -> None: - """Check a bounded PNG chunk envelope and CRCs, not pixel decoding or artwork.""" - limit = 16 * 1024 * 1024 + """Check bounded chunks and CRCs; decoding and visual review remain separate.""" with path.open("rb") as handle: - data = handle.read(limit + 1) - if len(data) > limit or not data.startswith(b"\x89PNG\r\n\x1a\n"): - raise ValueError("logo must be a PNG of at most 16 MiB") - offset = 8 - seen_header = False - seen_data = False + data = handle.read(16 * 1024 * 1024 + 1) + if len(data) > 16 * 1024 * 1024 or data[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("invalid or oversized PNG") + offset, header, image = 8, False, False while offset + 12 <= len(data): length = struct.unpack_from(">I", data, offset)[0] kind = data[offset + 4:offset + 8] - end = offset + 12 + length + end = offset + length + 12 if end > len(data): raise ValueError("truncated PNG chunk") payload = data[offset + 8:end - 4] - crc = struct.unpack_from(">I", data, end - 4)[0] - if zlib.crc32(kind + payload) != crc: - raise ValueError("PNG chunk checksum mismatch") - if not seen_header and kind != b"IHDR": + if zlib.crc32(kind + payload) != struct.unpack_from(">I", data, end - 4)[0]: + raise ValueError("PNG checksum mismatch") + if not header and kind != b"IHDR": raise ValueError("PNG must start with IHDR") if kind == b"IHDR": - if seen_header or length != 13: + if header or length != 13: raise ValueError("invalid PNG header") width, height, depth, color, compression, filtering, interlace = struct.unpack(">IIBBBBB", payload) depths = {0: (1, 2, 4, 8, 16), 2: (8, 16), 3: (1, 2, 4, 8), 4: (8, 16), 6: (8, 16)} - if not width or not height or depth not in depths.get(color, ()) or compression or filtering or interlace not in (0, 1): - raise ValueError("invalid PNG image parameters") - seen_header = True - elif kind == b"IDAT" and length: - seen_data = True + if not 0 < width < 2**31 or not 0 < height < 2**31 or depth not in depths.get(color, ()) or compression or filtering or interlace not in (0, 1): + raise ValueError("invalid PNG parameters") + header = True + elif kind == b"IDAT": + image = image or bool(length) elif kind == b"IEND": - if length or not seen_data or end != len(data): - raise ValueError("invalid PNG end or missing image data") + if length or not image or end != len(data): + raise ValueError("invalid PNG end") return offset = end - raise ValueError("PNG missing complete IHDR/IDAT/IEND structure") + raise ValueError("incomplete PNG") def validate(root: Path) -> list[str]: errors: list[str] = [] - root = root.resolve() - try: - manifest_path = local_file(root, "skill-package.json") - manifest = json.loads(manifest_path.read_text(encoding="utf-8"), object_pairs_hook=unique_object) - except (OSError, UnicodeError, ValueError) as exc: - return [f"manifest: {exc}"] - if not isinstance(manifest, dict): - return ["manifest must be a JSON object"] - if set(manifest) != FIELDS: - errors.append(f"manifest fields mismatch: missing={sorted(FIELDS - set(manifest))}, unknown={sorted(set(manifest) - FIELDS)}") - if type(manifest.get("schema_version")) is not int or manifest.get("schema_version") != 1: - errors.append("schema_version must be integer 1") - if manifest.get("family") != FAMILY: - errors.append(f"family must be {FAMILY}") - name = manifest.get("skill_name") - if not isinstance(name, str) or not SLUG.fullmatch(name) or len(name) > 64: - errors.append("skill_name must be a lowercase hyphenated slug of at most 64 characters") - repo = manifest.get("repository") - if not isinstance(repo, str) or not re.fullmatch(r"CompleteTech-LLC/[a-z0-9]+(?:-[a-z0-9]+)*", repo): - errors.append("repository must be CompleteTech-LLC/") - elif isinstance(name, str) and repo.split("/")[1] not in (name, name + "-skill"): - errors.append("repository and skill_name do not describe the same skill") - if manifest.get("kind") not in tuple(KINDS): - errors.append("unsupported kind") - if manifest.get("network_mode") not in tuple(NETWORK_MODES): - errors.append("unsupported network_mode") - if type(manifest.get("private")) is not bool: - errors.append("private must be a JSON boolean") - paths = list(FILES) + ["assets/logo.png"] - for field in ("entrypoints", "example_inputs"): - values = manifest.get(field) - if not isinstance(values, list) or not values: - errors.append(f"{field} must be a non-empty array") - continue - if all(isinstance(value, str) for value in values) and len(set(values)) != len(values): - errors.append(f"{field} contains duplicate paths") - paths.extend(values) - for value in paths: - try: - local_file(root, value) - except (OSError, ValueError) as exc: - errors.append(str(exc)) try: + manifest = json.loads(local_file(root, "skill-package.json").read_text(encoding="utf-8"), object_pairs_hook=unique_object) + if not isinstance(manifest, dict) or set(manifest) != FIELDS: + raise ValueError("manifest fields must match schema 1") + name, repo = manifest["skill_name"], manifest["repository"] + if type(manifest["schema_version"]) is not int or manifest["schema_version"] != 1 or manifest["family"] != FAMILY: + errors.append("invalid schema_version or family") + if not isinstance(name, str) or not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name) or len(name) > 64: + errors.append("invalid skill_name") + if not isinstance(repo, str) or repo not in (f"CompleteTech-LLC/{name}", f"CompleteTech-LLC/{name}-skill"): + errors.append("repository does not match skill_name") + if manifest["kind"] not in KINDS or manifest["network_mode"] not in NETWORK_MODES or type(manifest["private"]) is not bool: + errors.append("invalid kind, network_mode or private") + paths: list[object] = [*FILES, "assets/logo.png"] + if manifest["kind"] == "config-generator": + paths.append("config.ini") + for field in ("entrypoints", "example_inputs"): + values = manifest[field] + if not isinstance(values, list) or not values or not all(isinstance(v, str) for v in values): + errors.append(f"{field} must be a nonempty string array") + else: + if len(set(values)) != len(values): + errors.append(f"duplicate {field}") + paths.extend(values) + for value in paths: + try: + local_file(root, value) + except (OSError, ValueError, RuntimeError) as exc: + errors.append(str(exc)) skill = local_file(root, "SKILL.md").read_text(encoding="utf-8") - header = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|\Z)", skill, re.S) - declared = re.search(r'''(?m)^name:\s*(['"]?)([a-z0-9-]+)\1[ \t]*(?:\#.*)?$''', header.group(1)) if header else None - if not declared or declared.group(2) != name: - errors.append("skill_name must match the scalar name in SKILL.md frontmatter") + header = re.match(r"\A---\n(.*?)\n---(?:\n|\Z)", skill, re.S) + names = re.findall(r'''(?m)^name:\s*['"]?([a-z0-9-]+)['"]?\s*$''', header.group(1)) if header else [] + if names != [name]: + errors.append("SKILL.md name must match manifest") readme = local_file(root, "README.md").read_text(encoding="utf-8") - for marker in NAVIGATION: - if marker not in readme: - errors.append(f"README.md missing family navigation: {marker}") + errors.extend(f"missing README navigation: {item}" for item in NAVIGATION if item not in readme) validate_png(local_file(root, "assets/logo.png")) - for document in ("ONBOARDING.md", "CONTRIBUTING.md"): + for document in ("ONBOARDING.md", "CONTRIBUTING.md", "BRANDING.md", "AGENTS.md"): text = local_file(root, document).read_text(encoding="utf-8") for target in re.findall(r"\[[^\]]+\]\(([^)]+)\)", text): - if target.startswith(("https://", "http://", "#")): + parsed = urlsplit(target) + if target.startswith("#") or parsed.scheme in ("https", "http", "mailto"): continue - local_file(root, target.split("#", 1)[0]) - except (OSError, UnicodeError, ValueError) as exc: + if parsed.scheme or parsed.netloc: + raise ValueError(f"unsupported documentation link: {target}") + local_file(root, unquote(parsed.path)) + except (OSError, UnicodeError, ValueError, RuntimeError, RecursionError) as exc: errors.append(str(exc)) return errors -def main() -> int: +if __name__ == "__main__": parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1]) - args = parser.parse_args() - errors = validate(args.root) - for error in errors: - print(f"ERROR: {error}") - if errors: - return 1 - print("CompleteTech LLC Skills package contract OK (full checkout; no commands executed)") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) + issues = validate(parser.parse_args().root) + print("\n".join(issues) if issues else "Package contract OK; no specialist commands executed") + raise SystemExit(bool(issues)) diff --git a/tests/test_package_contract.py b/tests/test_package_contract.py index 0a68c78..41b1c54 100644 --- a/tests/test_package_contract.py +++ b/tests/test_package_contract.py @@ -1,144 +1,119 @@ -"""Offline regression tests for the shared package contract, using synthetic files.""" -from __future__ import annotations - +"""Synthetic package regressions; no specialist code or personal data is used.""" import importlib.util import json -import tempfile import struct -import zlib +import tempfile import unittest +import zlib from pathlib import Path -MODULE = Path(__file__).resolve().parents[1] / "scripts" / "validate_package.py" -SPEC = importlib.util.spec_from_file_location("validate_package", MODULE) -assert SPEC is not None and SPEC.loader is not None +SPEC = importlib.util.spec_from_file_location("package", Path(__file__).resolve().parents[1] / "scripts/validate_package.py") +assert SPEC and SPEC.loader package = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(package) -def png_fixture() -> bytes: - def chunk(kind: bytes, payload: bytes) -> bytes: - return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", zlib.crc32(kind + payload)) - return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)) - + chunk(b"IDAT", zlib.compress(b"\x00\x00\x00\x00")) + chunk(b"IEND", b"")) +def png_fixture(): + def chunk(kind, data): + return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data)) + return b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(b"\0\0\0\0")) + chunk(b"IEND", b"") class PackageContractTests(unittest.TestCase): def setUp(self): - self.temp = tempfile.TemporaryDirectory() - self.addCleanup(self.temp.cleanup) - self.root = Path(self.temp.name) / "checkout" - self.root.mkdir() - self.manifest = dict(schema_version=1, family=package.FAMILY, - repository="CompleteTech-LLC/example-skill", skill_name="example-skill", - kind="catalog-renderer", entrypoints=["scripts/render.py"], - example_inputs=["examples/input.md"], network_mode="local", private=False) - for value in (*package.FILES, "scripts/render.py", "examples/input.md"): - path = self.root / value + temp = tempfile.TemporaryDirectory() + self.addCleanup(temp.cleanup) + self.root = Path(temp.name) / "checkout" + self.manifest: dict[str, object] = dict(schema_version=1, family=package.FAMILY, repository="CompleteTech-LLC/demo-skill", skill_name="demo-skill", kind="catalog-renderer", entrypoints=["scripts/render.py"], example_inputs=["example.md"], network_mode="local", private=False) + for name in (*package.FILES, "scripts/render.py", "example.md"): + path = self.root / name path.parent.mkdir(parents=True, exist_ok=True) path.write_text("demo\n", encoding="utf-8") - (self.root / "SKILL.md").write_text("---\nname: example-skill\ndescription: Demo\n---\n", encoding="utf-8") - (self.root / "README.md").write_text("\n".join(package.NAVIGATION), encoding="utf-8") (self.root / "assets").mkdir() (self.root / "assets/logo.png").write_bytes(png_fixture()) + (self.root / "SKILL.md").write_text("---\nname: demo-skill\n---\n", encoding="utf-8") + (self.root / "README.md").write_text("\n".join(package.NAVIGATION), encoding="utf-8") self.save() def save(self): (self.root / "skill-package.json").write_text(json.dumps(self.manifest), encoding="utf-8") - def test_valid(self): + def test_valid_and_no_execution(self): + (self.root / "scripts/render.py").write_text("raise RuntimeError('do not execute')", encoding="utf-8") self.assertEqual(package.validate(self.root), []) - def test_ledger_repository_name_differs_from_install_name(self): - self.manifest.update(repository="CompleteTech-LLC/ai-usage-ledger-skill", skill_name="ai-usage-ledger", kind="usage-ledger", network_mode="operator-selected-hosts") - (self.root / "SKILL.md").write_text("---\nname: ai-usage-ledger\n---\n", encoding="utf-8") - self.save() - self.assertEqual(package.validate(self.root), []) - - def test_schema_rejects_bad_types_and_unknown_values(self): - for key, value in (("schema_version", True), ("family", "other"), ("kind", []), ("network_mode", {}), ("private", "false"), ("repository", "other/example"), ("skill_name", "../example"), ("entrypoints", []), ("example_inputs", "input.md")): - with self.subTest(key=key): - original = self.manifest[key] - self.manifest[key] = value + def test_bad_values(self): + for field, value in (("schema_version", True), ("family", "other"), ("skill_name", "../escape"), ("repository", "other/demo"), ("kind", []), ("network_mode", {}), ("private", "false"), ("entrypoints", []), ("entrypoints", [None]), ("example_inputs", "example.md"), ("example_inputs", ["example.md", "example.md"])): + with self.subTest(field=field, value=value): + original = self.manifest[field] + self.manifest[field] = value self.save() self.assertTrue(package.validate(self.root)) - self.manifest[key] = original - self.save() - - def test_unknown_and_missing_fields(self): - self.manifest["typo"] = True - del self.manifest["private"] - self.save() - self.assertTrue(package.validate(self.root)) + self.manifest[field] = original - def test_duplicate_json_keys(self): - (self.root / "skill-package.json").write_text('{"family":"a","family":"b"}', encoding="utf-8") - self.assertIn("duplicate JSON key", package.validate(self.root)[0]) - - def test_invalid_or_non_object_json(self): - for text in ("{", "[]", "null"): + def test_json_and_missing_fields(self): + for text in ("{", "[]", "null", "{}", '{"private":true,"private":false}'): (self.root / "skill-package.json").write_text(text, encoding="utf-8") self.assertTrue(package.validate(self.root)) - def test_unsafe_and_missing_paths(self): - for value in ("../outside", "/tmp/outside", "C:/outside", "scripts\\render.py", "missing.py", None, {}, ""): + def test_paths_and_links(self): + for value in ("../escape", "/escape", "C:/escape", "a\\b", "./example.md", "a//b", "bad\npath", "missing.md"): with self.subTest(path=value): self.manifest["entrypoints"] = [value] self.save() self.assertTrue(package.validate(self.root)) - - def test_duplicate_paths(self): - self.manifest["entrypoints"] *= 2 + self.manifest["entrypoints"] = ["scripts/render.py"] self.save() - self.assertTrue(package.validate(self.root)) + for target in ("missing.md", "%2e%2e/outside", "//outside/file", "file:///etc/passwd", "file:example.md"): + (self.root / "ONBOARDING.md").write_text(f"[Bad]({target})", encoding="utf-8") + self.assertTrue(package.validate(self.root)) - def test_symlink_escape(self): - outside = Path(self.temp.name) / "outside.py" - outside.write_text("private", encoding="utf-8") - path = self.root / "scripts/render.py" - path.unlink() + def test_escaping_symlink(self): + target = self.root / "example.md" + outside = self.root.parent / "outside.md" + outside.write_text("outside", encoding="utf-8") + target.unlink() try: - path.symlink_to(outside) + target.symlink_to(outside) except OSError as exc: self.skipTest(str(exc)) self.assertTrue(package.validate(self.root)) - def test_missing_brand_asset_and_invalid_png(self): - logo = self.root / "assets/logo.png" - logo.write_text("not an image", encoding="utf-8") - self.assertTrue(package.validate(self.root)) - logo.unlink() - self.assertTrue(package.validate(self.root)) + def test_png_corruption(self): + image = png_fixture() + for data in (b"not png", image[:8], image[:-12], image + b"junk", image[:-1] + bytes([image[-1] ^ 1])): + (self.root / "assets/logo.png").write_bytes(data) + self.assertTrue(package.validate(self.root)) - def test_truncated_or_corrupted_png_rejected(self): - valid = png_fixture() - for data in (valid[:8], valid[:-12], valid + b"junk", valid[:-1] + bytes([valid[-1] ^ 1])): - with self.subTest(length=len(data)): - (self.root / "assets/logo.png").write_bytes(data) + def test_missing_required_files(self): + for name in (*package.FILES, "assets/logo.png"): + with self.subTest(path=name): + path = self.root / name + data = path.read_bytes() + path.unlink() self.assertTrue(package.validate(self.root)) + path.write_bytes(data) - def test_frontmatter_mismatch(self): - (self.root / "SKILL.md").write_text("---\nname: another-skill\n---\n", encoding="utf-8") + def test_config_generator_requires_base(self): + self.manifest["kind"] = "config-generator" + self.save() self.assertTrue(package.validate(self.root)) - - def test_quoted_frontmatter_and_crlf(self): - (self.root / "SKILL.md").write_bytes(b'---\r\nname: "example-skill"\r\n---\r\n') + (self.root / "config.ini").write_text("[demo]\n", encoding="utf-8") self.assertEqual(package.validate(self.root), []) - def test_missing_navigation(self): - (self.root / "README.md").write_text("demo", encoding="utf-8") - self.assertTrue(package.validate(self.root)) - - def test_broken_onboarding_link(self): - (self.root / "ONBOARDING.md").write_text("[Missing](missing.md)", encoding="utf-8") + def test_names_and_navigation(self): + (self.root / "SKILL.md").write_text("---\nname: wrong\n---\n", encoding="utf-8") self.assertTrue(package.validate(self.root)) - - def test_valid_local_external_and_anchor_links(self): - (self.root / "ONBOARDING.md").write_text("[Readme](README.md#demo) [Web](https://example.com) [Here](#demo)", encoding="utf-8") + (self.root / "SKILL.md").write_bytes(b'---\r\nname: "demo-skill"\r\n---\r\n') self.assertEqual(package.validate(self.root), []) + (self.root / "README.md").write_text("no navigation", encoding="utf-8") + self.assertTrue(package.validate(self.root)) - def test_never_executes_declared_entrypoints(self): - (self.root / "scripts/render.py").write_text("raise RuntimeError('must not execute')", encoding="utf-8") + def test_ledger_install_name_and_links(self): + self.manifest.update(skill_name="ai-usage-ledger", repository="CompleteTech-LLC/ai-usage-ledger-skill", kind="usage-ledger", network_mode="operator-selected-hosts") + self.save() + (self.root / "SKILL.md").write_text("---\nname: ai-usage-ledger\n---\n", encoding="utf-8") + (self.root / "ONBOARDING.md").write_text("[Read](README.md#demo) [Web](https://example.com) [Here](#here)", encoding="utf-8") self.assertEqual(package.validate(self.root), [])