diff --git a/ARCHIVE-CUSTODY.md b/ARCHIVE-CUSTODY.md index aa9d5e5..898268a 100644 --- a/ARCHIVE-CUSTODY.md +++ b/ARCHIVE-CUSTODY.md @@ -6,11 +6,11 @@ CanopyOps keeps one canonical source tree and several independently useful relea |---|---|---| | Complete Augment | `release-assets/v0.1.5/CanopyOps-v0.1.5.zip` | Portable full CanopyOps capability tree | | Codex plugin | `release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5.zip` | Branded Codex plugin, manifest, assets, and bundled skill | +| OpenAI skills-only submission | `release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip` | Deterministic portal upload with the portal-accepted minimal interface | | Standalone skill | `release-assets/v0.1.5/Skill-canopyops--CanopyOps-v0.1.5.zip` | Direct skill installation and subskill custody | | Claude.ai upload | `claude-ai/canopyops-v0.1.5.zip` | One-top-level-folder Claude.ai upload package | | Source repository | Git tag `v0.1.5` and its GitHub source archives | Versioned source, documentation, tests, and provenance | -`release-assets/v0.1.5/archive-custody.json` records exact hashes, sizes, member counts, source-tree digests, and extraction-parity results for the governed archives. GitHub release assets and the latest-only convenience backup shelf must match those hashes. Canonical release assets are copied, never moved, to the backup shelf. Older same-family convenience copies may be removed only after the new copies match; unrelated products are untouched. +`release-assets/v0.1.5/archive-custody.json` records exact hashes, sizes, member counts, source-tree digests, and extraction-parity results for the installable governed archives. `release-assets/v0.1.5/openai-submission-custody.json` separately records the portal derivative's archive hash, source and transformed manifest hashes, member count, and POSIX path requirement. The portal ZIP is not a replacement for the installable plugin. GitHub release assets and the latest-only convenience backup shelf must match the applicable custody records. Canonical release assets are copied, never moved, to the backup shelf. Older same-family convenience copies may be removed only after the new copies match; unrelated products are untouched. The standalone skill and Claude.ai upload intentionally carry the same skill content under host-appropriate names. Archive equality does not establish live-host activation. - diff --git a/BUILD-WEEK.md b/BUILD-WEEK.md index 07b54b3..b0a4906 100644 --- a/BUILD-WEEK.md +++ b/BUILD-WEEK.md @@ -81,7 +81,7 @@ Codex did not independently authorize publication, legal posture, operational ac The public repository provides three levels of evidence: -1. **Deterministic:** run **python -m unittest discover -s tests -v**. The 17 tests cover calculations, invalid inputs, unit and timestamp normalization, schema validation, template linting, source freshness, required package surfaces, schema parsing, version custody, distribution parity, customer-document reachability, archive topology, and release-manifest integrity. +1. **Deterministic:** run **python -m unittest discover -s tests -v**. The 18 tests cover calculations, invalid inputs, unit and timestamp normalization, schema validation, template linting, source freshness, required package surfaces, schema parsing, version custody, distribution parity, customer-document reachability, archive topology, release-manifest integrity, and deterministic OpenAI submission custody. 2. **Installable:** install the branded plugin directly from this repository using the two commands in [INSTALL.md](INSTALL.md). 3. **Behavioral:** run the fictional late-flower incident in [JUDGE-QUICKSTART.md](JUDGE-QUICKSTART.md) and inspect whether CanopyOps preserves alternatives, authority, reversible containment, and an auditable record. diff --git a/PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md b/PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md index 3df2536..841808a 100644 --- a/PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md +++ b/PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md @@ -1,27 +1,41 @@ # CanopyOps Plugins Directory submission packet -This packet maps the released CanopyOps plugin to the current OpenAI Plugins Directory form. It prepares a **Skills only** submission; it does not assert that a publisher identity is verified, that policy attestations have been made, or that OpenAI has reviewed or published the plugin. +This packet maps the released CanopyOps plugin to the current OpenAI Plugins Directory form and records the created **Skills only** draft. Draft creation and publisher-identity selection are observed; owner policy attestations, submission for review, approval, publication, and discoverability remain separate states. ## Released object - Plugin and Augment version: `0.1.5` -- Upload: `release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5.zip` -- SHA-256: `6caf7ecf8f7016a9c5efd07a2844cb5d2e783f147b59badcbff6695019fa7778` +- Installable plugin: `release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5.zip` +- Installable plugin SHA-256: `6caf7ecf8f7016a9c5efd07a2844cb5d2e783f147b59badcbff6695019fa7778` +- OpenAI submission upload: `release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip` +- Submission upload SHA-256: `0c6297a89b3b8081fdc23b00c1474f8f35cbdd658e5334a088cab11e1cf8c179` +- Submission custody: `release-assets/v0.1.5/openai-submission-custody.json` +- Draft-upload comparison: `release-assets/v0.1.5/portal-draft-upload-evidence.json` - Public release: - Submission type: **Skills only** +The installable plugin keeps its full local and marketplace `interface`. The governed portal upload is a deterministic channel derivative of the same 76-file plugin tree; only the archived manifest is transformed to retain `composerIcon` and `logo`. Every ZIP member path uses `/`; UTF-8 text is canonicalized to LF without a BOM; and members use stored ZIP entries so the archive is byte-identical across supported Python runtimes. The draft was created from a preflight ZIP with the same member names; 75 members are byte-identical and the Ella Greenfield persona is identical after CRLF/LF normalization. Its ZIP container metadata was not deterministic. The live portal accepted this shape. + +## Created draft + +- Plugin ID: `plugins_6a5e101a0c0481918dd2604b5c25d969` +- Submission ID: `appsub_6a5e101a2b3c81919fc93b7fc00e8a77` +- Draft URL: +- Observed draft state on 2026-07-20: listing, three prompts, one skill, and capability tags saved; the skill passed automated scanning; policy attestations unchecked; review submission not executed. + ## Info - Plugin name: **CanopyOps** -- Short description: **Cannabis crop plans, diagnostics, and operating records.** +- Portal subtitle: **Cannabis crop operations** (24 characters; the live form caps this field at 30) - Long description: **Turn facility data, crop observations, logs, and constraints into evidence-bounded plans, incident workups, calculations, harvest reviews, and auditable records for legal cannabis cultivation.** -- Developer identity: select the verified **Collaborative Dynamics** identity in the portal; the accountable owner must confirm the identity and organization match before submission. +- Developer identity: **Business — Collaborative Dynamics Inc** selected in the portal; the listing developer name is **Collaborative Dynamics Inc**. - Category: **Productivity** - Logo: `plugins/canopyops/assets/canopyops-icon-v0.1.5.png` - Website: - Support: - Privacy: - Terms: +- Capabilities: separate **Interactive**, **Read**, and **Write** tags. ## Starter prompts @@ -97,3 +111,5 @@ Initial public submission of the free CanopyOps skills-only plugin. CanopyOps pr ## Accountable-owner gate Before selecting **Submit for Review**, the publisher must personally confirm the verified developer or business identity, organization and Apps Management authority, country availability, public URLs, and every policy attestation. Submission begins OpenAI review; approval, publisher release, and public discoverability are later observable states. + +The live 2026-07-20 skills-only form did not expose the documentation page's separate Testing, Global, or release-notes panels. The exact five positive cases, three negative cases, availability decision, and release notes remain preserved above for reviewer follow-up or a later portal revision; their repository presence does not mean they were transmitted in the current draft. diff --git a/README.md b/README.md index 331ed16..cf72338 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It is the reasoning-and-record layer between “something looks wrong” and an CanopyOps was conceived and built on July 17, 2026, during the OpenAI Build Week submission period. Stun supplied compact product intent, source material, the Ella Greenfield persona, domain and authority boundaries, and release judgment. Codex with GPT-5.6 turned that direction into the routed SKILL, deterministic utilities, schemas, templates, evaluations, host adapters, documentation, licensing surfaces, plugin packaging, verification, and public release. -The working Augment emerged in roughly an hour; public packaging, branding, licensing, hardening, and publication continued afterward. This repository now includes a 17-test deterministic suite and automatic verification so judges and users can inspect the machinery rather than taking the claim on faith. +The working Augment emerged in roughly an hour; public packaging, branding, licensing, hardening, and publication continued afterward. This repository now includes an 18-test deterministic suite and automatic verification so judges and users can inspect the machinery rather than taking the claim on faith. Read [BUILD-WEEK.md](BUILD-WEEK.md) for the architecture, provenance, human/AI responsibility split, and evidence. Judges can use [JUDGE-QUICKSTART.md](JUDGE-QUICKSTART.md) to install and test CanopyOps with fictional data in about five minutes. @@ -83,7 +83,7 @@ Read [SAFETY-AND-SCOPE.md](SAFETY-AND-SCOPE.md) before operational use. In parti The v0.1.5 package passes the current Augment Builder Codex and Claude profiles plus the repository's deterministic release tests. The standalone, plugin, and Claude archive skill trees are byte-identical. A reviewed, context-only three-case safety/scope smoke completed 3/3 selected episodes in v0.1.2; that result remains inherited behavioral evidence because v0.1.5 changes documentation, release custody, listing identity, and regression checks rather than the operating kernel. It is evidence under those exact recorded conditions, not field validation or a reliability guarantee. -Field use, broader behavioral consistency, current jurisdiction coverage, live Claude.ai or Claude Code execution, equipment integration, and official OpenAI Plugins Directory appearance remain unverified or outside v0.1.5. See [RELEASE-NOTES-v0.1.5.md](RELEASE-NOTES-v0.1.5.md). +Field use, broader behavioral consistency, current jurisdiction coverage, live Claude.ai or Claude Code execution, equipment integration, and official OpenAI Plugins Directory appearance remain unverified or outside v0.1.5. An OpenAI skills-only draft has been created and populated, and its skill passed automated scanning; accountable-owner attestations, review submission, approval, and publication remain separate states. See [PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md](PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md) and [RELEASE-NOTES-v0.1.5.md](RELEASE-NOTES-v0.1.5.md). The exact v0.1.5 checks and their evidence boundaries are recorded in [VERIFICATION-v0.1.5.md](VERIFICATION-v0.1.5.md). @@ -111,4 +111,5 @@ CanopyOps is a Collaborative Dynamics Augment created by Sam Walker (stunspot), - [Terms of use](TERMS-OF-USE.md) - [Current release notes](RELEASE-NOTES-v0.1.5.md) - [Archive custody](ARCHIVE-CUSTODY.md) +- [OpenAI draft and submission custody](PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md) - [Previous release notes](RELEASE-NOTES-v0.1.3.md) diff --git a/RELEASE-NOTES-v0.1.5.md b/RELEASE-NOTES-v0.1.5.md index d3a0d84..ca8e1f6 100644 --- a/RELEASE-NOTES-v0.1.5.md +++ b/RELEASE-NOTES-v0.1.5.md @@ -11,8 +11,9 @@ CanopyOps v0.1.5 is the plugin-identity and archive-custody release. It preserve - Added canonical archives for the complete Augment, Codex plugin, and standalone CanopyOps skill. - Added a custody ledger with source-tree and archive SHA-256 values, sizes, member counts, and extraction-parity evidence. - Documented the distinction among canonical release assets, GitHub release assets, Claude.ai upload package, and latest-only convenience backups. +- Added a deterministic OpenAI skills-only submission ZIP and its separate custody record without weakening the full installable plugin manifest. +- Created and populated the OpenAI plugin draft; the skill passed automated scanning, while owner policy attestations and review submission remain pending. ## Boundary -The operating skill is unchanged, so prior reviewed behavioral evidence is inherited rather than rerun. Package validation, archive parity, listing-asset inspection, and documentation review do not establish cultivation-field fitness, jurisdictional currency, live Claude behavior, official OpenAI Plugins Directory approval, accessibility conformance, or customer outcomes. - +The operating skill is unchanged, so prior reviewed behavioral evidence is inherited rather than rerun. Package validation, archive parity, listing-asset inspection, portal draft creation, and documentation review do not establish cultivation-field fitness, jurisdictional currency, live Claude behavior, OpenAI review approval or publication, accessibility conformance, or customer outcomes. diff --git a/VERIFICATION-v0.1.5.md b/VERIFICATION-v0.1.5.md index 6513a8e..a1c6bf5 100644 --- a/VERIFICATION-v0.1.5.md +++ b/VERIFICATION-v0.1.5.md @@ -8,19 +8,21 @@ This record distinguishes checks that were actually run from behavior that remai | Check | Result | What it establishes | |---|---|---| -| Repository deterministic suite | 17 of 17 tests passed on the frozen release candidate | Calculations, record validation, version custody, canonical/plugin/Claude parity, customer-document reachability, and release-manifest hashes behaved as asserted. | +| Repository deterministic suite | 18 of 18 tests passed on the frozen release candidate | Calculations, record validation, version custody, canonical/plugin/Claude parity, customer-document reachability, release-manifest hashes, and OpenAI submission reproducibility behaved as asserted. | | Augment Builder profiles | Bundle, canonical Codex, plugin-bundled Codex, and Claude profiles passed | Required structure, metadata, contained resources, JSON, and private-path rules passed the current static profiles. | | Plugin-readiness audit | 1 skill; 0 errors; 0 warnings | The v0.1.5 manifest, HTTPS customer links, listing metadata, skill entry point, and version-bound PNG assets satisfy the deterministic publication preflight. | | Distribution parity | Canonical, plugin-bundled, and Claude.ai skill trees matched | Every supported distribution carries the same CanopyOps operating skill. This does not prove host activation. | | Governed release archives | Complete Augment, Codex plugin, and standalone skill archives built and extracted byte-for-byte | `release-assets/v0.1.5/archive-custody.json` records source-tree and archive hashes, byte sizes, member counts, and extraction parity. | +| OpenAI submission archive | Deterministic 76-member skills-only ZIP reproduced byte-for-byte | `release-assets/v0.1.5/openai-submission-custody.json` records the accepted channel transform, archive hash, manifest hashes, and POSIX member paths. | +| OpenAI draft creation | Listing, three prompts, one skill, and three capability tags saved under the verified Collaborative Dynamics Inc business identity; the skill passed automated scanning | Draft creation and scan success are observed. Owner attestations, review submission, approval, publication, and discoverability are not established. | | Listing-asset inspection | New canopy-grid icon inspected at 1024 and 32 pixels | The mark remains recognizable at listing size and uses shape and contrast, rather than color alone, to distinguish canopy nodes, grid, and decision point. This is visual inspection, not accessibility conformance testing. | -| Customer-document checks | 20 declared customer documents and all tracked repository-local Markdown links passed deterministic validation | The release identity and local navigation are coherent. External destinations, representative users, and assistive technologies were not tested by this check. | +| Customer-document checks | 21 declared customer documents and all tracked repository-local Markdown links passed deterministic validation | The release identity and local navigation are coherent. External destinations, representative users, and assistive technologies were not tested by this check. | ## Fresh documentation review custody This page records checks performed against other release artifacts; it is not the evidence source for its own statements. The repository test output, Builder profile results, plugin-readiness audit, release manifest, archive-custody ledger, and retained reviewer records provide the corresponding evidence outside this prose summary. -Because this page is also part of the reviewed customer corpus, it does not predeclare or self-certify its documentation-review result. Immutable fresh-context responses, exact input custody, runtime metadata, and validation records are retained under `verification/evidence/`. The machine-readable release summary names the authoritative run and records its resulting disposition after that evidence exists. +Because this page is also part of the reviewed customer corpus, it does not predeclare or self-certify its documentation-review result. Immutable fresh-context responses, exact input custody, runtime metadata, and validation records are retained under `verification/evidence/`. That independent review covered the 20-document pre-draft corpus. The subsequently added Plugins Directory packet is included in the current 21-document deterministic inventory but has not received a new independent accessibility review. ## Inherited behavioral evidence @@ -36,4 +38,4 @@ This release does not establish: - current jurisdiction coverage, legal advice, regulatory approval, or compliance certification; - equipment integration, direct control, batch-release authority, or customer outcomes; - browser, keyboard, screen-reader, localization, representative-user, or formal accessibility-conformance testing; -- official OpenAI Plugins Directory appearance or accountable publication approval. +- owner policy attestations, submission for review, approval, public Directory appearance, or discoverability. diff --git a/documentation-manifest.json b/documentation-manifest.json index 1efc988..791d48f 100644 --- a/documentation-manifest.json +++ b/documentation-manifest.json @@ -21,7 +21,8 @@ "BUILD-WEEK.md", "RELEASE-NOTES-v0.1.5.md", "VERIFICATION-v0.1.5.md", - "ARCHIVE-CUSTODY.md" + "ARCHIVE-CUSTODY.md", + "PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md" ], "moments": { "orientation": ["README.md", "START-HERE.md"], @@ -30,7 +31,7 @@ "normal_use": ["FAQ.md", "SAFETY-AND-SCOPE.md"], "recovery": ["INSTALL.md", "SUPPORT.md"], "privacy_security": ["DATA-AND-PRIVACY.md", "TERMS-OF-USE.md", "SECURITY.md"], - "support_maintenance": ["SUPPORT.md", "RELEASE-NOTES-v0.1.5.md", "ARCHIVE-CUSTODY.md"], + "support_maintenance": ["SUPPORT.md", "RELEASE-NOTES-v0.1.5.md", "ARCHIVE-CUSTODY.md", "PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md"], "provenance": ["ATTRIBUTION.md", "LICENSE.md", "TRADEMARKS.md", "NOTICE.md", "CONTRIBUTING.md", "BUILD-WEEK.md"], "evidence_limits": ["VERIFICATION-v0.1.5.md"] } diff --git a/release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip b/release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip new file mode 100644 index 0000000..7c04d2f Binary files /dev/null and b/release-assets/v0.1.5/Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip differ diff --git a/release-assets/v0.1.5/openai-submission-custody.json b/release-assets/v0.1.5/openai-submission-custody.json new file mode 100644 index 0000000..4f25bcd --- /dev/null +++ b/release-assets/v0.1.5/openai-submission-custody.json @@ -0,0 +1,38 @@ +{ + "schema_version": "cd-openai-plugin-submission-custody/v1", + "plugin": { + "name": "canopyops", + "version": "0.1.5" + }, + "source_plugin_root": "canopyops", + "top_level": "canopyops-plugin", + "archive_name": "Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip", + "archive_sha256": "0c6297a89b3b8081fdc23b00c1474f8f35cbdd658e5334a088cab11e1cf8c179", + "bytes": 289426, + "member_count": 76, + "source_manifest_sha256": "a6e62e60b33438f3395ffd987a331688837363c3d42df55cba47d2e747a0915b", + "submission_manifest_sha256": "11167dc921515c40f94d23f675a6251264d7e31486eded62fe867eed605ba0bd", + "text_canonicalization": "UTF-8 text is stored as LF without a BOM; binary and non-UTF-8 bytes are preserved", + "normalized_text_member_count": 0, + "zip_compression": "stored", + "manifest_transform": { + "kept_interface_fields": [ + "composerIcon", + "logo" + ], + "omitted_interface_fields": [ + "brandColor", + "capabilities", + "category", + "defaultPrompt", + "developerName", + "displayName", + "longDescription", + "privacyPolicyURL", + "shortDescription", + "termsOfServiceURL", + "websiteURL" + ] + }, + "archive_paths_use_forward_slashes": true +} diff --git a/release-assets/v0.1.5/portal-draft-upload-evidence.json b/release-assets/v0.1.5/portal-draft-upload-evidence.json new file mode 100644 index 0000000..e58b881 --- /dev/null +++ b/release-assets/v0.1.5/portal-draft-upload-evidence.json @@ -0,0 +1,29 @@ +{ + "schema_version": "cd-openai-plugin-draft-upload-evidence/v1", + "observed_on": "2026-07-20", + "plugin_id": "plugins_6a5e101a0c0481918dd2604b5c25d969", + "submission_id": "appsub_6a5e101a2b3c81919fc93b7fc00e8a77", + "skill_scan_result": "canopyops passed", + "owner_gate": "four policy attestations unchecked; review submission not executed", + "accepted_preflight_archive": { + "name": "Plugin-CanopyOps-v0.1.5-submission.zip", + "sha256": "93d6ccc8d3ac9f8eae1595c74414d0f584c964d32f9a0e296da8cc61a9590adc", + "bytes": 214115, + "member_count": 76 + }, + "governed_release_archive": { + "name": "Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip", + "sha256": "0c6297a89b3b8081fdc23b00c1474f8f35cbdd658e5334a088cab11e1cf8c179", + "bytes": 289426, + "member_count": 76 + }, + "comparison": { + "member_names_match": true, + "raw_member_hashes_matching": 75, + "raw_member_hashes_differing": 1, + "canonical_text_member_hashes_match": true, + "archive_bytes_match": false, + "line_ending_only_member": "canopyops-plugin/skills/canopyops/personas/ella-greenfield-v2.md", + "explanation": "The accepted preflight ZIP and governed release ZIP have identical member names. Seventy-five members are byte-identical; the remaining UTF-8 text is identical after CRLF/LF normalization. ZIP timestamps and container metadata also differ. Only the governed release ZIP is deterministic." + } +} diff --git a/release-manifest.json b/release-manifest.json index 50b6ebe..6dc1162 100644 --- a/release-manifest.json +++ b/release-manifest.json @@ -47,8 +47,8 @@ }, { "path": "ARCHIVE-CUSTODY.md", - "bytes": 1578, - "sha256": "575913e3ee6f91ce992c30704fd8d0bea2d98f4da8b5155d380b73a73d90dfe7" + "bytes": 2044, + "sha256": "9afbdcaf1a3f55ef32d48bcbb7b19809d3acdc072a5ba2b5c038de066ec798b1" }, { "path": "archive-plan.json", @@ -62,8 +62,8 @@ }, { "path": "BUILD-WEEK.md", - "bytes": 7370, - "sha256": "fb80c0f8d1c512886df84dd104a490135788c12a9a1fa4d958bf41167b71f29a" + "bytes": 7411, + "sha256": "e382a2779408a82654efbbae3dd02c96682a4a57040dcde0b439ccc2ffd9efad" }, { "path": "canopyops/adapters/bot-runtime.md", @@ -432,8 +432,8 @@ }, { "path": "documentation-manifest.json", - "bytes": 1158, - "sha256": "57acb0e0ee188317d3f1d3065d8b69609a4915edb3d84e7b4b523844ff7b9085" + "bytes": 1244, + "sha256": "9e7c1f60ab4a03e8f02332759c98560bf9c7bbdba31b181e249fa91382715f31" }, { "path": "documentation-review.json", @@ -507,8 +507,8 @@ }, { "path": "PLUGIN-DIRECTORY-SUBMISSION-v0.1.5.md", - "bytes": 8628, - "sha256": "6aaf39da14c57d076f543fe8c81737d6ec01027bd851e601e6d4fa69f33e9873" + "bytes": 10661, + "sha256": "bd8cd23268efc91403c4bedd142fe044bebecb319c7d68488161d417c48f2209" }, { "path": "plugins/canopyops/.codex-plugin/plugin.json", @@ -892,8 +892,8 @@ }, { "path": "README.md", - "bytes": 8662, - "sha256": "34d309563e950de94d2f557e70fd6646cc1b7db87283ff610939840a4b1faf1c" + "bytes": 9027, + "sha256": "62528a2575808fc71851d41ac2efda15d09174d0a28a02898bdfb435fd471b43" }, { "path": "RELEASE-NOTES-v0.1.2.md", @@ -912,8 +912,8 @@ }, { "path": "RELEASE-NOTES-v0.1.5.md", - "bytes": 1365, - "sha256": "961abb05f63f25c60a53542921feb865139a4d000e9c60580753854acc3134a6" + "bytes": 1684, + "sha256": "25c3df458350051982605799fa2b4e8d69ee5fd6b50b9ab8ff01e0e181f9993f" }, { "path": "SAFETY-AND-SCOPE.md", @@ -945,10 +945,20 @@ "bytes": 617, "sha256": "ee636201f4c62be964a4b3561eeb0e249e0cdcecfd6ed0816f1d7574dd3f17ef" }, + { + "path": "tests/test_openai_submission.py", + "bytes": 1884, + "sha256": "523657c4cf70ce6fc8236dfe5931546843f039f18722637703f6790845b7e885" + }, { "path": "tests/test_scripts.py", "bytes": 8099, - "sha256": "dda953c155e43022bc373088ae68cc61be2848d09b9f38c4d3f3d280cf30c3f8" + "sha256": "7ba78a643ac701a4da34160d20c33d27e0ee35d457436c8b89cd7d1731a9bdc9" + }, + { + "path": "tools/build_openai_submission_archive.py", + "bytes": 7174, + "sha256": "af5dace6b88303b244233fc47191cce381cd250dc39b4806d348a3e9589bf3f7" }, { "path": "TRADEMARKS.md", @@ -962,8 +972,8 @@ }, { "path": "VERIFICATION-v0.1.5.md", - "bytes": 4038, - "sha256": "6ecf90f691d8522833ac652cf98dcd4b289a7e01859a0b23e492e46490571b4f" + "bytes": 4803, + "sha256": "27247526383001edcd000089cb81f99503b948b0ec767541f5627b86ff4ee3ba" }, { "path": "verification/evidence/hesperos-doc-review-v0.1.5-api-20260720-run1/api-response.json", @@ -1362,8 +1372,8 @@ }, { "path": "verification/release-summary-v0.1.5.json", - "bytes": 3268, - "sha256": "e8ca47265ab1cec5f56a5b05cb97fa0cd4cf5d4768760e87787b619cf32ce81f" + "bytes": 4365, + "sha256": "0e34adec7e16d40bbc96b409956990bf63362276eb74c10f33a4d1f12e78fa08" } ] } diff --git a/tests/test_openai_submission.py b/tests/test_openai_submission.py new file mode 100644 index 0000000..78359cc --- /dev/null +++ b/tests/test_openai_submission.py @@ -0,0 +1,59 @@ +import json +import subprocess +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PLUGIN = ROOT / "plugins" / "canopyops" +ARCHIVE = ( + ROOT + / "release-assets" + / "v0.1.5" + / "Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip" +) +CUSTODY = ROOT / "release-assets" / "v0.1.5" / "openai-submission-custody.json" + + +class OpenAISubmissionTests(unittest.TestCase): + def test_submission_archive_is_reproducible_and_portal_shaped(self): + with tempfile.TemporaryDirectory() as temporary: + output = Path(temporary) / ARCHIVE.name + custody = Path(temporary) / CUSTODY.name + subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "build_openai_submission_archive.py"), + str(PLUGIN), + "--output", + str(output), + "--json-output", + str(custody), + ], + check=True, + capture_output=True, + text=True, + ) + self.assertEqual(ARCHIVE.read_bytes(), output.read_bytes()) + self.assertEqual( + json.loads(CUSTODY.read_text(encoding="utf-8")), + json.loads(custody.read_text(encoding="utf-8")), + ) + + with zipfile.ZipFile(ARCHIVE) as archive: + names = archive.namelist() + self.assertTrue(names) + self.assertTrue(all("\\" not in name for name in names)) + manifest = json.loads( + archive.read("canopyops-plugin/.codex-plugin/plugin.json").decode( + "utf-8" + ) + ) + self.assertEqual({"composerIcon", "logo"}, set(manifest["interface"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_scripts.py b/tests/test_scripts.py index 585951f..6e229eb 100644 --- a/tests/test_scripts.py +++ b/tests/test_scripts.py @@ -144,7 +144,7 @@ def test_release_manifest_hashes_and_inventory(self): def test_customer_document_manifest_and_local_links(self): manifest = json.loads((REPO_ROOT / "documentation-manifest.json").read_text(encoding="utf-8")) documents = manifest["customer_docs"] - self.assertEqual(20, len(documents)) + self.assertEqual(21, len(documents)) self.assertEqual(len(documents), len(set(documents))) declared = set(documents) for paths in manifest["moments"].values(): diff --git a/tools/build_openai_submission_archive.py b/tools/build_openai_submission_archive.py new file mode 100644 index 0000000..018b7a2 --- /dev/null +++ b/tools/build_openai_submission_archive.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Build a deterministic OpenAI skills-only submission ZIP from a Codex plugin.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path, PurePosixPath +import struct +import zipfile + + +FIXED_TIME = (2026, 1, 1, 0, 0, 0) +EXCLUDED_PARTS = {".git", "__pycache__", ".pytest_cache"} +MANIFEST_PATH = PurePosixPath(".codex-plugin/plugin.json") + + +def sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def canonical_archive_bytes(path: Path) -> tuple[bytes, bool]: + """Return cross-platform bytes while preserving non-UTF-8 and binary files.""" + data = path.read_bytes() + if b"\x00" in data: + return data, False + try: + text = data.decode("utf-8-sig") + except UnicodeDecodeError: + return data, False + canonical = text.replace("\r\n", "\n").replace("\r", "\n").encode("utf-8") + return canonical, canonical != data + + +def regular_files(root: Path) -> list[Path]: + return sorted( + ( + path + for path in root.rglob("*") + if path.is_file() and not EXCLUDED_PARTS.intersection(path.relative_to(root).parts) + ), + key=lambda path: ( + path.relative_to(root).as_posix().casefold(), + path.relative_to(root).as_posix(), + ), + ) + + +def resolve_asset(root: Path, value: object, field: str) -> Path: + if not isinstance(value, str) or not value.startswith("./"): + raise ValueError(f"interface.{field} must be a ./-relative path") + target = (root / value.removeprefix("./")).resolve() + if root.resolve() not in target.parents: + raise ValueError(f"interface.{field} escapes the plugin root") + if not target.is_file(): + raise ValueError(f"interface.{field} does not exist: {value}") + return target + + +def png_dimensions(path: Path) -> tuple[int, int]: + data = path.read_bytes() + if data[:8] != b"\x89PNG\r\n\x1a\n" or data[12:16] != b"IHDR": + raise ValueError(f"not a PNG: {path}") + return struct.unpack(">II", data[16:24]) + + +def submission_manifest(plugin_root: Path) -> tuple[dict[str, object], dict[str, object]]: + manifest_path = plugin_root / MANIFEST_PATH + manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig")) + if not isinstance(manifest, dict): + raise ValueError("plugin manifest must be an object") + interface = manifest.get("interface") + if not isinstance(interface, dict): + raise ValueError("plugin interface must be an object") + for field, minimum in (("composerIcon", 48), ("logo", 256)): + asset = resolve_asset(plugin_root, interface.get(field), field) + width, height = png_dimensions(asset) + if width != height or width < minimum: + raise ValueError( + f"interface.{field} must be a square PNG at least {minimum}x{minimum}; " + f"found {width}x{height}" + ) + + transformed = dict(manifest) + transformed["interface"] = { + "composerIcon": interface["composerIcon"], + "logo": interface["logo"], + } + metadata = { + "kept_interface_fields": ["composerIcon", "logo"], + "omitted_interface_fields": sorted(set(interface) - {"composerIcon", "logo"}), + } + return transformed, metadata + + +def build(plugin_root: Path, output: Path, top_level: str) -> dict[str, object]: + plugin_root = plugin_root.resolve() + if not plugin_root.is_dir(): + raise ValueError(f"plugin root does not exist: {plugin_root}") + manifest, transform = submission_manifest(plugin_root) + rendered_manifest = (json.dumps(manifest, indent=2, ensure_ascii=False) + "\n").encode("utf-8") + source_manifest, _ = canonical_archive_bytes(plugin_root / MANIFEST_PATH) + files = regular_files(plugin_root) + normalized_text_members: list[str] = [] + output = output.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile( + output, + "w", + compression=zipfile.ZIP_STORED, + ) as archive: + for path in files: + relative = PurePosixPath(path.relative_to(plugin_root).as_posix()) + name = PurePosixPath(top_level, relative).as_posix() + info = zipfile.ZipInfo(name, FIXED_TIME) + info.create_system = 0 + info.compress_type = zipfile.ZIP_STORED + info.external_attr = 0o644 << 16 + if relative == MANIFEST_PATH: + data = rendered_manifest + else: + data, normalized = canonical_archive_bytes(path) + if normalized: + normalized_text_members.append(relative.as_posix()) + archive.writestr(info, data) + + with zipfile.ZipFile(output) as archive: + names = archive.namelist() + if any("\\" in name for name in names): + raise RuntimeError("submission archive contains a backslash path") + archived_manifest = archive.read(f"{top_level}/{MANIFEST_PATH.as_posix()}") + observed = json.loads(archived_manifest.decode("utf-8")) + if observed.get("interface") != manifest["interface"]: + raise RuntimeError("submission manifest round trip failed") + + return { + "schema_version": "cd-openai-plugin-submission-custody/v1", + "plugin": {"name": manifest.get("name", ""), "version": manifest.get("version", "")}, + "source_plugin_root": plugin_root.name, + "top_level": top_level, + "archive_name": output.name, + "archive_sha256": sha256_file(output), + "bytes": output.stat().st_size, + "member_count": len(files), + "source_manifest_sha256": sha256_bytes(source_manifest), + "submission_manifest_sha256": sha256_bytes(rendered_manifest), + "text_canonicalization": "UTF-8 text is stored as LF without a BOM; binary and non-UTF-8 bytes are preserved", + "normalized_text_member_count": len(normalized_text_members), + "zip_compression": "stored", + "manifest_transform": transform, + "archive_paths_use_forward_slashes": True, + } + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("plugin_root", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--json-output", type=Path) + parser.add_argument("--top-level") + args = parser.parse_args() + + manifest = json.loads( + (args.plugin_root / MANIFEST_PATH).read_text(encoding="utf-8-sig") + ) + top_level = args.top_level or f"{manifest.get('name', args.plugin_root.name)}-plugin" + report = build(args.plugin_root, args.output, top_level) + rendered = json.dumps(report, indent=2) + "\n" + if args.json_output: + args.json_output.parent.mkdir(parents=True, exist_ok=True) + args.json_output.write_text(rendered, encoding="utf-8") + print(rendered, end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/verification/release-summary-v0.1.5.json b/verification/release-summary-v0.1.5.json index aa4f328..96bcf16 100644 --- a/verification/release-summary-v0.1.5.json +++ b/verification/release-summary-v0.1.5.json @@ -4,10 +4,10 @@ "version": "0.1.5", "review_date": "2026-07-20", "review_mode": "verification-reviewer and documentation-accessibility-reviewer challenge after final local rebuild", - "candidate_scope": "Public repository candidate, complete CanopyOps Augment, Codex plugin, standalone skill, and Claude.ai upload archive", + "candidate_scope": "Public repository candidate, complete CanopyOps Augment, installable Codex plugin, OpenAI skills-only submission, standalone skill, and Claude.ai upload archive", "evidence": { "deterministic_tests": { - "passed": 17, + "passed": 18, "failed": 0 }, "builder_profiles": [ @@ -20,9 +20,16 @@ "plugin_audit_result": "1 skill; zero errors and zero warnings", "documentation_review": "documentation-review.json", "documentation_fingerprint": "329236b3f9ea9ac054d0a88923635c4f8168ba9c1be9c61bacce1ea6dd2b68f3", - "documentation_result": "REVIEW_PASS_WITH_CONDITIONS; zero unresolved material findings", + "documentation_result": "REVIEW_PASS_WITH_CONDITIONS for the 20-document pre-draft corpus; zero unresolved material findings; the later Plugins Directory packet passed deterministic inventory/link checks but was not independently re-reviewed", "archive_custody": "release-assets/v0.1.5/archive-custody.json", - "release_manifest_validation": "17-test repository inventory, plugin parity, Claude archive parity, and customer-document link checks passed", + "openai_submission_custody": "release-assets/v0.1.5/openai-submission-custody.json", + "portal_draft_upload_evidence": "release-assets/v0.1.5/portal-draft-upload-evidence.json", + "openai_draft": { + "plugin_id": "plugins_6a5e101a0c0481918dd2604b5c25d969", + "submission_id": "appsub_6a5e101a2b3c81919fc93b7fc00e8a77", + "state": "draft created and populated; skill passed automated scanning; owner attestations unchecked; review submission not executed" + }, + "release_manifest_validation": "18-test repository inventory, plugin parity, Claude archive parity, OpenAI submission reproducibility, and 21-document customer link checks passed", "icon_inspection": "1024-pixel source and 32-pixel rendering inspected; the canopy-grid mark remained recognizable without text" }, "archives": [ @@ -38,6 +45,12 @@ "bytes": 215285, "members": 76 }, + { + "name": "Plugin-CanopyOps-v0.1.5-OpenAI-Submission.zip", + "sha256": "0c6297a89b3b8081fdc23b00c1474f8f35cbdd658e5334a088cab11e1cf8c179", + "bytes": 289426, + "members": 76 + }, { "name": "Skill-canopyops--CanopyOps-v0.1.5.zip", "sha256": "cff21580bd064604ad25a8e04ada0670d17bcbfbbd19903b79fccae95ae66875", @@ -49,7 +62,7 @@ "version_and_manifest_drift": "not found", "missing_skill_or_archive": "not found", "plugin_listing_or_asset_breakage": "not found; version-bound raster icon and bundled skill entry point exist", - "customer_path_or_recovery_dead_end": "not found in the declared 20-document corpus", + "customer_path_or_recovery_dead_end": "not found in the independently reviewed 20-document corpus; the later portal packet passed deterministic inventory and link checks only", "unsupported_readiness_claim": "not found; package verification is separated from cultivation-field, live-host, directory, accessibility, legal, and outcome claims", "private_path_or_cache_debris": "not found by package checks" }, @@ -57,7 +70,7 @@ "conditions": [ "Fresh Codex marketplace installation, discovery, activation, runtime-resource access, and representative first success remain publication-stage checks.", "Live Claude upload and activation, broad repeated-model behavior, cultivation-field fitness, jurisdictional currency, equipment integration, representative-user and assistive-technology testing, accessibility conformance, legal review, production outcomes, and customer outcomes remain unexecuted.", - "GitHub publication and official OpenAI Plugins Directory approval are separate observable states and are not established by this pre-publication review." + "GitHub publication is established separately. The OpenAI draft exists and its skill passed automated scanning, but owner policy attestations, review submission, approval, publication, and discoverability remain separate observable states." ], "disposition": "REVIEW_PASS_WITH_CONDITIONS", "release_decision": "READY_WITH_RESIDUAL_RISK"