From d674d601474a24e5cf6d2cb75eb10f2e0495207c Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Wed, 9 Sep 2026 16:02:57 -0400 Subject: [PATCH 1/2] chore(skills): harden ClawHub bundles --- scripts/validate-skills.ts | 27 +++++++++++++++++++++++++++ skills/README.md | 2 +- skills/VALIDATION.md | 2 +- skills/furniture-fit/.clawhubignore | 13 +++++++++++++ skills/pascal-3d/.clawhubignore | 13 +++++++++++++ 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 skills/furniture-fit/.clawhubignore create mode 100644 skills/pascal-3d/.clawhubignore diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts index bb271e4a7..144e33896 100644 --- a/scripts/validate-skills.ts +++ b/scripts/validate-skills.ts @@ -21,6 +21,21 @@ const openAiCapabilityLimit = 20 const openAiCapabilityLengthLimit = 120 const openAiListingUrlLimit = 1024 const openAiImageByteLimit = 5 * 1024 * 1024 +const clawHubRequiredIgnorePatterns = [ + '.env*', + '.next/', + 'dist/', + 'node_modules/', + 'coverage/', + 'test-results/', + 'playwright-report/', + 'screenshots/', + '*.lock', + '*.lockb', + 'package-lock.json', + 'pnpm-lock.yaml', + 'yarn.lock', +] as const const openAiCategories = new Set([ 'Productivity', 'Creativity', @@ -204,6 +219,18 @@ for (const entry of readdirSync(join(root, 'skills'), { withFileTypes: true })) for (const skillName of skillNames) { const skillRoot = join(root, 'skills', skillName) const skillFile = join(skillRoot, 'SKILL.md') + const clawHubIgnoreFile = join(skillRoot, '.clawhubignore') + const clawHubIgnorePatterns = new Set( + read(clawHubIgnoreFile) + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')), + ) + for (const pattern of clawHubRequiredIgnorePatterns) { + if (!clawHubIgnorePatterns.has(pattern)) { + fail(`${skillName}: .clawhubignore is missing ${pattern}`) + } + } const content = read(skillFile) const fields = frontmatter(content, skillFile) if (fields.name !== skillName) fail(`${skillName}: frontmatter name does not match directory`) diff --git a/skills/README.md b/skills/README.md index bd280a4e3..07d495c3c 100644 --- a/skills/README.md +++ b/skills/README.md @@ -81,4 +81,4 @@ bun scripts/validate-skills.ts claude plugin validate . --strict ``` -The repository validator checks frontmatter, bundled links, task and trigger fixtures, semantic furniture next-action decision cases, the publishing suite, portable and compatibility manifest consistency, OpenAI public-directory metadata limits, bundled branding assets, and accidental private-path or credential leakage. +The repository validator checks frontmatter, bundled links, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies, the publishing suite, portable and compatibility manifest consistency, OpenAI public-directory metadata limits, bundled branding assets, and accidental private-path or credential leakage. diff --git a/skills/VALIDATION.md b/skills/VALIDATION.md index 0dde6ad93..1adcae824 100644 --- a/skills/VALIDATION.md +++ b/skills/VALIDATION.md @@ -8,7 +8,7 @@ This release retains the portable root Agent Plugins manifest, Codex compatibili Both skills require Pascal MCP tools for their tool-backed workflows. OpenAI's [submission guide](https://developers.openai.com/plugins/deploy/submission) therefore places this package on the **With MCP** route, combining the hosted MCP server with the skills, rather than Skills only. This source is not submission-ready: the production endpoint has not passed OpenAI Scan Tools for this candidate, OAuth-compatible reviewer access and credentials have not been prepared, domain verification has not been completed, and the positive review cases do not yet name provisioned disposable fixtures with stable identifiers and reset instructions. The review suite records those blockers explicitly. Platform access, verified publisher identity, countries, policy attestations, review, approval, and the developer's separate publish action also remain external. The validator follows the current [submission error reference](https://developers.openai.com/plugins/deploy/submission-errors) for the checked metadata constraints. -For ClawHub, both canonical skill folders pass `clawhub` 0.23.3 `skill publish --dry-run --json` with their intended owner, slug, version, categories, topics, and public source metadata. OpenClaw 2026.9.3 installs both folders into an isolated workspace, parses the optional `PASCAL_API_KEY` and homepage metadata, and reports both skills eligible without a hosted credential. The documented local and hosted `openclaw mcp add` command shapes were saved successfully in isolated state without contacting Pascal. The OpenClaw `skills-sh:` resolver did not install the existing skills.sh source before ClawHub indexing, so public instructions do not claim that path as a pre-publication workaround. +For ClawHub, both canonical skill folders carry scoped `.clawhubignore` policies and pass `clawhub` 0.23.3 `skill publish --dry-run --json` with their intended owner, slug, version, categories, topics, and public source metadata. OpenClaw 2026.9.3 installs both folders into an isolated workspace, parses the optional `PASCAL_API_KEY` and homepage metadata, and reports both skills eligible without a hosted credential. The documented local and hosted `openclaw mcp add` command shapes were saved successfully in isolated state without contacting Pascal. The OpenClaw `skills-sh:` resolver did not install the existing skills.sh source before ClawHub indexing, so public instructions do not claim that path as a pre-publication workaround. These checks do not create a ClawHub publisher or release, accept the mandatory MIT-0 publication terms, run ClawHub's post-upload security scanners, prove a live Pascal MCP connection from OpenClaw, or establish installs, useful tasks, or retention. Those remain separate release and adoption evidence. diff --git a/skills/furniture-fit/.clawhubignore b/skills/furniture-fit/.clawhubignore new file mode 100644 index 000000000..b6135e14b --- /dev/null +++ b/skills/furniture-fit/.clawhubignore @@ -0,0 +1,13 @@ +.env* +.next/ +dist/ +node_modules/ +coverage/ +test-results/ +playwright-report/ +screenshots/ +*.lock +*.lockb +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/skills/pascal-3d/.clawhubignore b/skills/pascal-3d/.clawhubignore new file mode 100644 index 000000000..b6135e14b --- /dev/null +++ b/skills/pascal-3d/.clawhubignore @@ -0,0 +1,13 @@ +.env* +.next/ +dist/ +node_modules/ +coverage/ +test-results/ +playwright-report/ +screenshots/ +*.lock +*.lockb +package-lock.json +pnpm-lock.yaml +yarn.lock From 57cbaaee3d37d261d6074d7ad3e976c1fbb28dcd Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Wed, 9 Sep 2026 16:21:35 -0400 Subject: [PATCH 2/2] test(skills): reject ClawHub ignore overrides --- .github/workflows/ci.yml | 4 ++- scripts/clawhub-ignore-policy.test.ts | 47 +++++++++++++++++++++++++++ scripts/clawhub-ignore-policy.ts | 43 ++++++++++++++++++++++++ scripts/validate-skills.ts | 32 ++++-------------- skills/README.md | 3 +- skills/VALIDATION.md | 2 +- 6 files changed, 103 insertions(+), 28 deletions(-) create mode 100644 scripts/clawhub-ignore-policy.test.ts create mode 100644 scripts/clawhub-ignore-policy.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 048269aae..e038c8b4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,9 @@ jobs: run: bun run check - name: Validate agent skills and plugin packages - run: bun scripts/validate-skills.ts + run: | + bun test scripts/clawhub-ignore-policy.test.ts + bun scripts/validate-skills.ts - name: Type check run: bun run check-types diff --git a/scripts/clawhub-ignore-policy.test.ts b/scripts/clawhub-ignore-policy.test.ts new file mode 100644 index 000000000..f2de889d6 --- /dev/null +++ b/scripts/clawhub-ignore-policy.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from 'bun:test' +import { readFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { clawHubRequiredIgnorePatterns, validateClawHubIgnorePolicy } from './clawhub-ignore-policy' + +const repositoryRoot = resolve(import.meta.dir, '..') +const canonicalPolicy = `${clawHubRequiredIgnorePatterns.join('\n')}\n` + +describe('ClawHub ignore policy', () => { + test.each(['pascal-3d', 'furniture-fit'])('%s uses the protected policy', (skillName) => { + const content = readFileSync( + join(repositoryRoot, 'skills', skillName, '.clawhubignore'), + 'utf8', + ) + expect(validateClawHubIgnorePolicy(content)).toEqual([]) + }) + + test.each([ + ['a broad re-inclusion', '!*'], + ['a protected directory re-inclusion', '!dist/'], + ['a nested protected file re-inclusion', '!screenshots/public.png'], + ['a whitespace-prefixed re-inclusion', ' !.env.example'], + ])('rejects %s rule appended after the exclusions', (_label, reinclude) => { + expect(validateClawHubIgnorePolicy(`${canonicalPolicy}${reinclude}\n`)).toContain( + `.clawhubignore must not contain re-inclusion rule ${reinclude.trim()}`, + ) + }) + + test('rejects a later legacy ignore file that could override the canonical policy', () => { + expect(validateClawHubIgnorePolicy(canonicalPolicy, true)).toContain( + '.clawdhubignore must not coexist with the canonical ignore policy', + ) + }) + + test('reports a missing protected pattern', () => { + const incompletePolicy = canonicalPolicy.replace('screenshots/\n', '') + expect(validateClawHubIgnorePolicy(incompletePolicy)).toContain( + '.clawhubignore is missing screenshots/', + ) + }) + + test('rejects an empty canonical policy even when a legacy file exists', () => { + const failures = validateClawHubIgnorePolicy('', true) + expect(failures).toContain('.clawhubignore is missing .env*') + expect(failures).toContain('.clawdhubignore must not coexist with the canonical ignore policy') + }) +}) diff --git a/scripts/clawhub-ignore-policy.ts b/scripts/clawhub-ignore-policy.ts new file mode 100644 index 000000000..9819cdd97 --- /dev/null +++ b/scripts/clawhub-ignore-policy.ts @@ -0,0 +1,43 @@ +export const clawHubRequiredIgnorePatterns = [ + '.env*', + '.next/', + 'dist/', + 'node_modules/', + 'coverage/', + 'test-results/', + 'playwright-report/', + 'screenshots/', + '*.lock', + '*.lockb', + 'package-lock.json', + 'pnpm-lock.yaml', + 'yarn.lock', +] as const + +export function validateClawHubIgnorePolicy( + content: string, + hasLegacyIgnoreFile = false, +): string[] { + const patterns = content + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + const patternSet = new Set(patterns) + const failures: string[] = [] + + for (const pattern of clawHubRequiredIgnorePatterns) { + if (!patternSet.has(pattern)) failures.push(`.clawhubignore is missing ${pattern}`) + } + + for (const pattern of patterns) { + if (pattern.startsWith('!')) { + failures.push(`.clawhubignore must not contain re-inclusion rule ${pattern}`) + } + } + + if (hasLegacyIgnoreFile) { + failures.push('.clawdhubignore must not coexist with the canonical ignore policy') + } + + return failures +} diff --git a/scripts/validate-skills.ts b/scripts/validate-skills.ts index 144e33896..0e8743e7e 100644 --- a/scripts/validate-skills.ts +++ b/scripts/validate-skills.ts @@ -2,6 +2,7 @@ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node import { dirname, extname, join, relative, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { XMLParser, XMLValidator } from 'fast-xml-parser' +import { validateClawHubIgnorePolicy } from './clawhub-ignore-policy' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const skillNames = ['pascal-3d', 'furniture-fit'] as const @@ -21,21 +22,6 @@ const openAiCapabilityLimit = 20 const openAiCapabilityLengthLimit = 120 const openAiListingUrlLimit = 1024 const openAiImageByteLimit = 5 * 1024 * 1024 -const clawHubRequiredIgnorePatterns = [ - '.env*', - '.next/', - 'dist/', - 'node_modules/', - 'coverage/', - 'test-results/', - 'playwright-report/', - 'screenshots/', - '*.lock', - '*.lockb', - 'package-lock.json', - 'pnpm-lock.yaml', - 'yarn.lock', -] as const const openAiCategories = new Set([ 'Productivity', 'Creativity', @@ -220,16 +206,12 @@ for (const skillName of skillNames) { const skillRoot = join(root, 'skills', skillName) const skillFile = join(skillRoot, 'SKILL.md') const clawHubIgnoreFile = join(skillRoot, '.clawhubignore') - const clawHubIgnorePatterns = new Set( - read(clawHubIgnoreFile) - .split('\n') - .map((line) => line.trim()) - .filter((line) => line && !line.startsWith('#')), - ) - for (const pattern of clawHubRequiredIgnorePatterns) { - if (!clawHubIgnorePatterns.has(pattern)) { - fail(`${skillName}: .clawhubignore is missing ${pattern}`) - } + const clawHubIgnoreContent = read(clawHubIgnoreFile) + for (const policyFailure of validateClawHubIgnorePolicy( + clawHubIgnoreContent, + existsSync(join(skillRoot, '.clawdhubignore')), + )) { + fail(`${skillName}: ${policyFailure}`) } const content = read(skillFile) const fields = frontmatter(content, skillFile) diff --git a/skills/README.md b/skills/README.md index 07d495c3c..f8bd9d9cc 100644 --- a/skills/README.md +++ b/skills/README.md @@ -77,8 +77,9 @@ The `source-reviewed` date records a code and public-documentation review. The ` ## Validate the source package ```bash +bun test scripts/clawhub-ignore-policy.test.ts bun scripts/validate-skills.ts claude plugin validate . --strict ``` -The repository validator checks frontmatter, bundled links, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies, the publishing suite, portable and compatibility manifest consistency, OpenAI public-directory metadata limits, bundled branding assets, and accidental private-path or credential leakage. +The repository validator checks frontmatter, bundled links, task and trigger fixtures, semantic furniture next-action decision cases, scoped ClawHub ignore policies without re-inclusion overrides, the publishing suite, portable and compatibility manifest consistency, OpenAI public-directory metadata limits, bundled branding assets, and accidental private-path or credential leakage. diff --git a/skills/VALIDATION.md b/skills/VALIDATION.md index 1adcae824..844aa7268 100644 --- a/skills/VALIDATION.md +++ b/skills/VALIDATION.md @@ -8,7 +8,7 @@ This release retains the portable root Agent Plugins manifest, Codex compatibili Both skills require Pascal MCP tools for their tool-backed workflows. OpenAI's [submission guide](https://developers.openai.com/plugins/deploy/submission) therefore places this package on the **With MCP** route, combining the hosted MCP server with the skills, rather than Skills only. This source is not submission-ready: the production endpoint has not passed OpenAI Scan Tools for this candidate, OAuth-compatible reviewer access and credentials have not been prepared, domain verification has not been completed, and the positive review cases do not yet name provisioned disposable fixtures with stable identifiers and reset instructions. The review suite records those blockers explicitly. Platform access, verified publisher identity, countries, policy attestations, review, approval, and the developer's separate publish action also remain external. The validator follows the current [submission error reference](https://developers.openai.com/plugins/deploy/submission-errors) for the checked metadata constraints. -For ClawHub, both canonical skill folders carry scoped `.clawhubignore` policies and pass `clawhub` 0.23.3 `skill publish --dry-run --json` with their intended owner, slug, version, categories, topics, and public source metadata. OpenClaw 2026.9.3 installs both folders into an isolated workspace, parses the optional `PASCAL_API_KEY` and homepage metadata, and reports both skills eligible without a hosted credential. The documented local and hosted `openclaw mcp add` command shapes were saved successfully in isolated state without contacting Pascal. The OpenClaw `skills-sh:` resolver did not install the existing skills.sh source before ClawHub indexing, so public instructions do not claim that path as a pre-publication workaround. +For ClawHub, both canonical skill folders carry scoped `.clawhubignore` policies that reject later re-inclusion rules and the legacy override file. Focused tests cover broad, directory, nested-file, and whitespace-prefixed negation attempts. Both folders pass `clawhub` 0.23.3 `skill publish --dry-run --json` with their intended owner, slug, version, categories, topics, and public source metadata. OpenClaw 2026.9.3 installs both folders into an isolated workspace, parses the optional `PASCAL_API_KEY` and homepage metadata, and reports both skills eligible without a hosted credential. The documented local and hosted `openclaw mcp add` command shapes were saved successfully in isolated state without contacting Pascal. The OpenClaw `skills-sh:` resolver did not install the existing skills.sh source before ClawHub indexing, so public instructions do not claim that path as a pre-publication workaround. These checks do not create a ClawHub publisher or release, accept the mandatory MIT-0 publication terms, run ClawHub's post-upload security scanners, prove a live Pascal MCP connection from OpenClaw, or establish installs, useful tasks, or retention. Those remain separate release and adoption evidence.