Skip to content

Commit 31956d8

Browse files
TMHSDigitalclaude
andcommitted
feat(distribution): Codex plugin manifest with site, privacy and terms pages
HOL's registry scan reports three medium findings for a plugin with no websiteURL, privacyPolicyURL or termsOfServiceURL, which hold its Security score at 59 and Installability at 64 (#511). Those fields live in a Codex manifest's interface block, so: - .codex-plugin/plugin.json: name, description, version, homepage, license and keywords matching the other manifests, skills from ./skills/, and the three URLs. It ships in the plugin-dist build and release.yml bumps its version with the others. - privacy/ and terms/ pages on the site, built from scripts/site/ (legal.html.j2 plus one template each) and listed in the sitemap. The pack collects no data; the terms summarize the existing licenses. - check_manifest_meta.py holds the Codex manifest to the others and its URLs to the pages build_site.py actually renders. Closes #511 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8884a1c commit 31956d8

9 files changed

Lines changed: 254 additions & 5 deletions

File tree

‎.codex-plugin/plugin.json‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "blender-developer-tools",
3+
"version": "0.155.0",
4+
"description": "Blender Python skills and rules for AI agents: correct bpy for Blender 5.2 LTS and 4.5 LTS (Slotted Actions, temp_override, Extensions, bmesh, Geometry Nodes, glTF/FBX export), backed by smoke-tested examples",
5+
"author": {
6+
"name": "TMHSDigital",
7+
"url": "https://github.com/TMHSDigital"
8+
},
9+
"homepage": "https://tmhsdigital.github.io/Blender-Developer-Tools/",
10+
"repository": "https://github.com/TMHSDigital/Blender-Developer-Tools",
11+
"license": "CC-BY-NC-ND-4.0 AND MIT",
12+
"keywords": [
13+
"claude-code",
14+
"developer-tools",
15+
"blender",
16+
"bpy",
17+
"blender-addon",
18+
"blender-python"
19+
],
20+
"skills": "./skills/",
21+
"interface": {
22+
"displayName": "Blender Developer Tools",
23+
"shortDescription": "Correct Blender Python for 5.2 LTS and 4.5 LTS",
24+
"longDescription": "Skills, rules, snippets and templates that keep AI coding agents writing correct Blender Python for 5.2 LTS and 4.5 LTS, backed by runnable examples that are smoke-tested on both versions.",
25+
"developerName": "TMHSDigital",
26+
"category": "Developer Tools",
27+
"websiteURL": "https://tmhsdigital.github.io/Blender-Developer-Tools/",
28+
"privacyPolicyURL": "https://tmhsdigital.github.io/Blender-Developer-Tools/privacy/",
29+
"termsOfServiceURL": "https://tmhsdigital.github.io/Blender-Developer-Tools/terms/",
30+
"defaultPrompt": [
31+
"Use Blender Developer Tools to write a Blender 5.2 add-on operator that also runs on 4.5 LTS."
32+
],
33+
"brandColor": "#E87D0D",
34+
"composerIcon": "./assets/favicon.svg",
35+
"logo": "./assets/favicon.svg"
36+
}
37+
}

‎.github/workflows/release.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,10 @@ jobs:
205205
# Claude Code manifests: same version, rewritten via JSON.
206206
import json
207207
# live-session/ is the opt-in blender-live-session plugin (#483).
208+
# .codex-plugin/ is the Codex manifest, which carries the site,
209+
# privacy and terms URLs that HOL's registry scan asks for (#511).
208210
for path in ('.claude-plugin/plugin.json', '.claude-plugin/marketplace.json',
211+
'.codex-plugin/plugin.json',
209212
'live-session/.claude-plugin/plugin.json',
210213
'live-session/.cursor-plugin/plugin.json'):
211214
data = json.load(open(path, encoding='utf-8'))
@@ -238,6 +241,7 @@ jobs:
238241
# Explicit paths only: the files this workflow owns, never `git add -A`.
239242
git add -- VERSION CHANGELOG.md CLAUDE.md ROADMAP.md \
240243
.cursor-plugin/plugin.json .claude-plugin/plugin.json .claude-plugin/marketplace.json \
244+
.codex-plugin/plugin.json \
241245
live-session/.claude-plugin/plugin.json live-session/.cursor-plugin/plugin.json
242246
if [ -n "$(git status --porcelain)" ]; then
243247
echo "::warning::files changed that the release commit does not own:"

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ docs/site-upgrade-plan.md
3737
# docs/gallery/ stays committed)
3838
docs/index.html
3939
docs/404.html
40+
docs/privacy/
41+
docs/terms/
4042
docs/fonts/
4143
docs/assets/
4244

‎scripts/build_plugin_dist.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
as a self-contained marketplace (its marketplace.json points at "./"):
77
88
skills/ claude/ rules/ snippets/ templates/ LICENSE files
9-
.claude-plugin/ and .cursor-plugin/ manifests, the Cursor logo,
9+
.claude-plugin/, .cursor-plugin/ and .codex-plugin/ manifests, the logo,
1010
live-session/ (the opt-in blender-live-session plugin: its manifests,
1111
mcp.json and skill), and a short README
1212
@@ -38,10 +38,11 @@
3838
DIRS = ("skills", "claude", "exports", "rules", "snippets", "templates", "live-session")
3939
# The Cursor manifest ships too, so Cursor users can install the same slim
4040
# tree as a local plugin (~/.cursor/plugins/local/) instead of the full repo.
41-
# assets/favicon.svg is the Cursor manifest's "logo".
41+
# assets/favicon.svg is the Cursor manifest's "logo" and the Codex manifest's
42+
# icon; the Codex manifest ships so Codex can install the same tree (#511).
4243
FILES = ("LICENSE", ".claude-plugin/plugin.json",
4344
".cursor-plugin/plugin.json", ".cursor-plugin/marketplace.json",
44-
"assets/favicon.svg")
45+
".codex-plugin/plugin.json", "assets/favicon.svg")
4546
# marketplace.json entry -> source inside the dist tree.
4647
DIST_SOURCES = {"blender-developer-tools": "./", "blender-live-session": "./live-session"}
4748
# Every repo path the plugin ships. release.yml's plugin-content-changed.sh and

‎scripts/site/build_site.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
sys.path.insert(0, str(Path(__file__).resolve().parent))
2323
import chrome # noqa: E402
2424

25+
# Pages rendered from scripts/site/<slug>.html.j2 to docs/<slug>/index.html.
26+
LEGAL_PAGES = ("privacy", "terms")
27+
2528

2629
def load_json(path: Path) -> dict | list:
2730
with open(path, encoding="utf-8") as f:
@@ -540,7 +543,7 @@ def write_sitemap(repo_root: Path, out_dir: Path, canonical: str) -> None:
540543
if not canonical:
541544
return
542545
root = canonical.rstrip("/") + "/"
543-
urls = [root, root + "gallery/"]
546+
urls = [root, root + "gallery/"] + [f"{root}{slug}/" for slug in LEGAL_PAGES]
544547
for rel, key in (("examples/gallery.json", "examples"), ("showcase/gallery.json", "pieces")):
545548
path = repo_root / rel
546549
if not path.is_file():
@@ -757,6 +760,19 @@ def main():
757760
(out_dir / "404.html").write_text(not_found, encoding="utf-8")
758761
print(f"Wrote {out_dir / '404.html'}")
759762

763+
# Privacy and terms: the plugin manifests link these URLs (#511).
764+
for slug in LEGAL_PAGES:
765+
page = env.get_template(f"{slug}.html.j2").render(
766+
site=site, plugin=plugin, repo=repo, slug=slug,
767+
chrome_css=chrome.CSS, chrome_js=chrome.JS,
768+
chrome_header=chrome.header(root="../", repo=repo, title=plugin["displayName"],
769+
home_anchors=False),
770+
chrome_footer=chrome.footer(root="../", repo=repo, title=plugin["displayName"],
771+
license_=plugin.get("license", "")))
772+
(out_dir / slug).mkdir(parents=True, exist_ok=True)
773+
(out_dir / slug / "index.html").write_text(page, encoding="utf-8")
774+
print(f"Wrote {out_dir / slug / 'index.html'}")
775+
760776
write_sitemap(repo_root, out_dir, site.get("canonical", ""))
761777
write_llms_txt(repo_root, out_dir, site)
762778

‎scripts/site/legal.html.j2‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>{% block title %}{% endblock %} — {{ plugin.displayName }}</title>
7+
<meta name="description" content="{% block description %}{% endblock %}" />
8+
{% if site.canonical %}<link rel="canonical" href="{{ site.canonical }}{{ slug }}/" />{% endif %}
9+
{% if site.favicon %}<link rel="icon" href="../{{ site.favicon }}" />{% endif %}
10+
<meta name="theme-color" content="#1a1b1e" />
11+
<meta name="color-scheme" content="dark" />
12+
<style>
13+
/* Privacy and terms pages: one level below the site root, so every
14+
site-relative URL carries "../". Dense reading, so left-aligned. */
15+
@font-face { font-family: 'Barlow Condensed'; font-weight: 600; font-display: swap;
16+
src: url('../fonts/barlow-condensed-600.woff2') format('woff2'); }
17+
@font-face { font-family: 'Inter'; font-weight: 400; font-display: swap;
18+
src: url('../fonts/inter-regular.woff2') format('woff2'); }
19+
@font-face { font-family: 'Inter'; font-weight: 500; font-display: swap;
20+
src: url('../fonts/inter-medium.woff2') format('woff2'); }
21+
@font-face { font-family: 'JetBrains Mono'; font-weight: 400; font-display: swap;
22+
src: url('../fonts/jetbrains-mono-regular.woff2') format('woff2'); }
23+
{% include "tokens.css" %}
24+
* { box-sizing: border-box; margin: 0; padding: 0; }
25+
body { background: var(--bg); color: var(--text); font-family: var(--font-body);
26+
line-height: 1.65; font-size: 16px; -webkit-font-smoothing: antialiased; }
27+
a { color: var(--text); }
28+
a:hover { color: var(--select); }
29+
:focus-visible { outline: 2px solid var(--select); outline-offset: 2px; border-radius: 2px; }
30+
{{ chrome_css | safe }}
31+
main { max-width: 46rem; margin: 0 auto; padding: 3rem 1.25rem 4rem; }
32+
.hud { font-family: var(--font-mono); font-size: var(--fs-xs); letter-spacing: 0.04em;
33+
color: var(--dim); text-transform: uppercase; }
34+
h1 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
35+
font-size: 2.6rem; line-height: 1; margin: 0.5rem 0 1rem; }
36+
h2 { font-family: var(--font-display); font-weight: 600; text-transform: uppercase;
37+
font-size: 1.35rem; letter-spacing: 0.01em; margin: 2.25rem 0 0.6rem; }
38+
p, ul { color: var(--dim); margin-bottom: 0.85rem; }
39+
ul { padding-left: 1.25rem; }
40+
li { margin-bottom: 0.35rem; }
41+
strong { color: var(--text); font-weight: 500; }
42+
code { font-family: var(--font-mono); font-size: 0.9em; color: var(--text); }
43+
.lede { font-size: 1.05rem; color: var(--text); }
44+
</style>
45+
</head>
46+
<body>
47+
{{ chrome_header | safe }}
48+
<main>
49+
<p class="hud">{{ plugin.displayName }} &middot; updated {{ updated }}</p>
50+
{% block body %}{% endblock %}
51+
</main>
52+
{{ chrome_footer | safe }}
53+
<script>
54+
{{ chrome_js | safe }}
55+
</script>
56+
</body>
57+
</html>

‎scripts/site/privacy.html.j2‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{% extends "legal.html.j2" %}
2+
{% set updated = "2026-10-26" %}
3+
{% block title %}Privacy{% endblock %}
4+
{% block description %}{{ plugin.displayName }} collects no personal data: no telemetry, no accounts, no analytics. What runs where, and the third parties involved.{% endblock %}
5+
{% block body %}
6+
<h1>Privacy</h1>
7+
<p class="lede">{{ plugin.displayName }} collects no personal data. The pack has no
8+
telemetry, no accounts and no analytics, and neither does this site.</p>
9+
10+
<h2>What the pack is</h2>
11+
<p>Skills and rules are text files your coding agent reads on your machine. Snippets,
12+
templates and examples are Python you run in your own Blender. None of them send
13+
anything to us.</p>
14+
<p>One snippet, <code>snippets/extension-user-data.py</code>, shows how an add-on
15+
<em>you</em> write can fetch a URL. It only runs when Blender's
16+
<strong>Allow Online Access</strong> preference is on, and only if you copy it into
17+
your add-on and call it.</p>
18+
19+
<h2>The optional live-session plugin</h2>
20+
<p><code>blender-live-session</code> is a separate, opt-in plugin. It launches the
21+
official Blender Lab MCP server with <code>uvx</code>, which downloads the server
22+
from projects.blender.org and its dependencies from PyPI. The server talks to
23+
Blender over <code>localhost</code> on your machine. Nothing goes to us.</p>
24+
25+
<h2>Your AI agent</h2>
26+
<p>When your agent uses a skill, the skill text and anything the agent reads from your
27+
project go to your agent's model provider (for example Anthropic, OpenAI or Cursor)
28+
under that provider's privacy policy, not this one.</p>
29+
30+
<h2>Hosting</h2>
31+
<p>The source lives on GitHub, and this site is served by GitHub Pages. GitHub may log
32+
technical data such as IP addresses when you visit or clone; see the
33+
<a href="https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement">GitHub General Privacy Statement</a>.
34+
The site sets no cookies and loads no third-party scripts or fonts.</p>
35+
<p>If you open an issue, discussion or pull request, what you post is public on GitHub.</p>
36+
37+
<h2>Contact</h2>
38+
<p>Questions go to
39+
<a href="{{ repo }}/discussions">GitHub Discussions</a>. For security problems, see
40+
<a href="{{ repo }}/blob/main/SECURITY.md">SECURITY.md</a>. Changes to this page are
41+
tracked in the repository's history. See also the <a href="../terms/">terms</a>.</p>
42+
{% endblock %}

‎scripts/site/terms.html.j2‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{% extends "legal.html.j2" %}
2+
{% set updated = "2026-10-26" %}
3+
{% block title %}Terms{% endblock %}
4+
{% block description %}Terms of use for {{ plugin.displayName }}: the licenses that apply, no warranty, and your responsibility for code your agent writes.{% endblock %}
5+
{% block body %}
6+
<h1>Terms</h1>
7+
<p class="lede">{{ plugin.displayName }} is free to use. The licenses below are the
8+
terms; this page summarizes them and adds the few things a license doesn't cover.
9+
Where this page and a license differ, the license wins.</p>
10+
11+
<h2>Licenses</h2>
12+
<ul>
13+
<li><strong>Snippets and templates are MIT.</strong> Copy and adapt them into your own
14+
projects, including commercial ones; keep the copyright notice with substantial
15+
copies (<a href="{{ repo }}/blob/main/snippets/LICENSE">snippets</a>,
16+
<a href="{{ repo }}/blob/main/templates/LICENSE">templates</a>).</li>
17+
<li><strong>Everything else is CC BY-NC-ND 4.0:</strong> skills, rules, examples,
18+
showcase pieces, docs and this site
19+
(<a href="{{ repo }}/blob/main/LICENSE">LICENSE</a>). Don't redistribute modified
20+
copies of that material, and don't sell or commercially republish it.</li>
21+
<li><strong>What you build is yours.</strong> Using the skills and rules while you or
22+
your agent write your own add-on, script or pipeline, including paid and client
23+
work, is the intended use. See the
24+
<a href="{{ repo }}#license">README's license section</a> for the details.</li>
25+
</ul>
26+
27+
<h2>No warranty</h2>
28+
<p>The pack is provided as is, without warranty of any kind, as both licenses state.
29+
Skills and rules describe Blender's API as the maintainers verified it on the Blender
30+
versions named in the README; Blender changes, and so does what agents produce from
31+
the same instructions.</p>
32+
33+
<h2>Code your agent writes</h2>
34+
<p>You are responsible for reviewing and testing code before you run or ship it. Run
35+
batch jobs on copies of files you can afford to lose. The optional
36+
<code>blender-live-session</code> plugin lets an agent run Python inside your open
37+
Blender with no guard, so attach it only to scratch scenes.</p>
38+
39+
<h2>Third-party software</h2>
40+
<p>Blender, the Blender Lab MCP server and your AI agent are separate products under
41+
their own licenses and terms. The MCP server is GPL-3.0 and is referenced, not
42+
bundled. Blender is a registered trademark of the Blender Foundation; this project is
43+
not affiliated with or endorsed by it.</p>
44+
45+
<h2>Contact</h2>
46+
<p>Questions go to <a href="{{ repo }}/discussions">GitHub Discussions</a>. Changes to
47+
these terms are tracked in the repository's history. See also the
48+
<a href="../privacy/">privacy page</a>.</p>
49+
{% endblock %}

‎tests/check_manifest_meta.py‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,17 @@
2020
- live-session manifests: same name, description, version, homepage and
2121
license as each other, the marketplace entries, and (homepage, license) the
2222
core plugin
23+
- .codex-plugin/plugin.json: same name, description, version, homepage,
24+
license and keywords as the others; its interface websiteURL, privacyPolicyURL
25+
and termsOfServiceURL are the site, its privacy/ and its terms/ pages, and
26+
build_site.py renders both pages (HOL's registry scan reads them, #511); its
27+
skills path and icon files exist
2328
2429
Run: python tests/check_manifest_meta.py (exit 0 ok, 1 on mismatch)
2530
"""
2631
from __future__ import annotations
2732

33+
import ast
2834
import html
2935
import json
3036
import re
@@ -119,6 +125,40 @@ def check() -> list[str]:
119125
if live_cursor.get(key) != cursor.get(key):
120126
errors.append(f"live-session {key} {live_cursor.get(key)!r} != core {cursor.get(key)!r}")
121127

128+
codex = load(".codex-plugin/plugin.json")
129+
for key in ("name", "description", "version", "homepage", "license"):
130+
if codex.get(key) != cursor.get(key):
131+
errors.append(f".codex-plugin/plugin.json {key} {codex.get(key)!r} != "
132+
f".cursor-plugin/plugin.json {cursor.get(key)!r}")
133+
if set(codex.get("keywords", [])) != kw_claude:
134+
errors.append(".codex-plugin/plugin.json keywords differ from .claude-plugin/plugin.json")
135+
if "mcpServers" in codex:
136+
errors.append(".codex-plugin/plugin.json declares mcpServers; the MCP server belongs to "
137+
"the opt-in live-session/ plugin (#483)")
138+
skills = codex.get("skills", "")
139+
if not isinstance(skills, str) or not (root / skills).is_dir():
140+
errors.append(f".codex-plugin/plugin.json skills {skills!r} is not a directory")
141+
# Read LEGAL_PAGES without importing build_site.py (it needs Jinja2).
142+
tree = ast.parse((root / "scripts" / "site" / "build_site.py").read_text(encoding="utf-8"))
143+
LEGAL_PAGES = next((ast.literal_eval(n.value) for n in tree.body if isinstance(n, ast.Assign)
144+
and any(getattr(t, "id", "") == "LEGAL_PAGES" for t in n.targets)), ())
145+
iface = codex.get("interface") or {}
146+
site_root = home.rstrip("/") + "/"
147+
want = {"websiteURL": site_root,
148+
"privacyPolicyURL": f"{site_root}privacy/",
149+
"termsOfServiceURL": f"{site_root}terms/"}
150+
for key, url in want.items():
151+
if iface.get(key) != url:
152+
errors.append(f".codex-plugin/plugin.json interface.{key} {iface.get(key)!r} != {url!r}")
153+
for slug in ("privacy", "terms"):
154+
if slug not in LEGAL_PAGES:
155+
errors.append(f"scripts/site/build_site.py LEGAL_PAGES does not render {slug}/, "
156+
"which the Codex manifest links")
157+
for key in ("composerIcon", "logo"):
158+
path = iface.get(key)
159+
if path and not (root / path).is_file():
160+
errors.append(f".codex-plugin/plugin.json interface.{key} {path!r} does not exist")
161+
122162
mcp = live_cursor.get("mcpServers")
123163
if not isinstance(mcp, str) or not (root / "live-session" / mcp).is_file():
124164
errors.append(f"live-session mcpServers {mcp!r} is not a file in live-session/")
@@ -147,7 +187,8 @@ def main() -> int:
147187
if errors:
148188
return 1
149189
print("Manifest metadata consistent: description, homepage, license, keywords, logo, "
150-
"live-session plugin and its pinned MCP server")
190+
"Codex manifest and its site/privacy/terms URLs, live-session plugin and its "
191+
"pinned MCP server")
151192
return 0
152193

153194

0 commit comments

Comments
 (0)