Compare commits

...

10 Commits

Author SHA1 Message Date
davida-ps 4a4b547b92 ci(skills): pin clawhub CLI by hash via committed lockfile (#268)
* ci(skills): pin clawhub CLI by hash via committed lockfile

Scorecard flags the skill-release workflow's npm install of the clawhub
CLI (code-scanning alerts #25/#26): version pinning alone carries no
integrity guarantee. Install it with npm ci from a committed
package-lock.json instead, so every package (clawhub + 35 transitive
deps) is verified against its sha512 hash at install time.

The publish-payload patch step now resolves the module from the local
node_modules instead of npm root -g.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skill-release): authenticate pinned clawhub install

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-15 18:12:36 +03:00
davida-ps 6f51e53cdd fix(skill-release): report SkillSpector scans on PRs (#267)
* fix(skill-release): report skillspector scans on PRs

* fix(skill-release): address PR report review comments

* fix(deps): update vite audit chain

* docs(wiki): document skillspector release evidence

* docs(wiki): centralize skillspector release details
2026-06-14 19:28:11 +03:00
github-actions[bot] d8dec965a8 chore: update NVD/GHSA advisories - 34 NVD new, 0 NVD updated (#270)
Automated update from NVD CVE and GHSA advisory feeds.
Keywords: openclaw, nanoclaw, hermes, picoclaw
Poll window: 2026-06-10T08:30:16Z to 2026-06-14T07:32:26.000Z

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-14 13:23:30 +03:00
davida-ps 9fd3059271 fix(traffic): require a traffic-capable PAT for the archive workflow (#265)
* fix(traffic): use a traffic-capable PAT for the archive workflow

The daily Archive GitHub Traffic run has failed since creation: the
TRAFFIC_ARCHIVE_TOKEN secret was never provisioned, so the workflow fell
back to github.token, which GitHub categorically rejects on traffic
endpoints (403 "Resource not accessible by integration").

- Fall back to the existing POLL_NVD_CVES_PAT automation token instead
  of github.token, keeping TRAFFIC_ARCHIVE_TOKEN as the preferred
  override once provisioned.
- Fail fast with an actionable error when no traffic-capable token is
  configured.
- Explain token requirements in the script's 401/403 errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(traffic): require dedicated TRAFFIC_ARCHIVE_TOKEN, drop expired PAT fallback

A live dispatch confirmed POLL_NVD_CVES_PAT is expired (401 Bad
credentials), so falling back to it only trades one daily failure for
another. Require the dedicated secret and fail fast with setup
instructions instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 08:25:56 +03:00
davida-ps 1b676fd42c fix(skills): scan staged payload with SkillSpector (#264)
* fix(skills): scan staged payload with skillspector

* fix(skills): embed skillspector report in releases

* fix(skills): use body path for release notes
2026-06-10 17:18:54 +03:00
davida-ps 59d54ed778 fix(skills): namespace ClawHub skill slugs (#263)
* fix(release): map ClawHub publish slugs

* fix(release): share skill platform parsing
2026-06-10 16:39:19 +03:00
Burak Bayır d99f324f72 feat(openclaw-traffic-guardian): add social action review scope (#261)
* feat(openclaw-traffic-guardian): add social action review scope

* fix(openclaw-traffic-guardian): cover background repeats

* fix(openclaw-traffic-guardian): address policy review release gates

* docs(openclaw-traffic-guardian): credit policy review contributor

* docs(openclaw-traffic-guardian): inline contributor credit

* docs(openclaw-traffic-guardian): reference policy review spec

* ci(skills): allow unreleased version edits

* ci(skills): use directory name for release tag checks

---------

Co-authored-by: kriptoburak <kriptoburak@users.noreply.github.com>
Co-authored-by: David Abutbul <David.a@prompt.security>
2026-06-10 14:46:52 +03:00
davida-ps c1d1824f86 ci(skills): publish release trust packets + expand skill installer awareness (vercel) (#262)
* ci(skills): publish release trust packets

* ci(skills): simulate beta tag releases

* ci(skills): match release version bump rules

* chore(skills): group agent skills for installer

* chore(skills): make clawtributor global

* chore(skills): bump all skills for trust release

* ci(skills): require npx install docs

* fix(skills): simulate prerelease tag versions

* fix(skills): aggregate trust artifact checksum failures

* fix(frontend): advertise npx skills suite install

* chore(frontend): drop ad hoc homepage copy test

* fix(ci): run skill release tooling tests
2026-06-10 13:22:22 +03:00
github-actions[bot] d7312d7429 chore: update NVD/GHSA advisories - 1 NVD new, 0 NVD updated (#257)
Automated update from NVD CVE and GHSA advisory feeds.
Keywords: openclaw, nanoclaw, hermes, picoclaw
Poll window: 2026-06-03T07:38:12Z to 2026-06-10T08:29:07.000Z

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-10 11:35:17 +03:00
davida-ps cb58e588c5 fix(workflow): filter dispatched codeql runs with jq (#260) 2026-06-10 11:23:30 +03:00
104 changed files with 7046 additions and 4452 deletions
+56
View File
@@ -0,0 +1,56 @@
{
"plugins": [
{
"name": "global-skills",
"source": "./",
"skills": [
"./skills/clawtributor"
]
},
{
"name": "hermes-skills",
"source": "./",
"skills": [
"./skills/hermes-attestation-guardian",
"./skills/hermes-traffic-guardian"
]
},
{
"name": "nano-claw-skills",
"source": "./",
"skills": [
"./skills/clawsec-nanoclaw",
"./skills/nanoclaw-traffic-guardian"
]
},
{
"name": "open-claw-skills",
"source": "./",
"skills": [
"./skills/clawsec-clawhub-checker",
"./skills/clawsec-feed",
"./skills/clawsec-scanner",
"./skills/clawsec-suite",
"./skills/openclaw-audit-watchdog",
"./skills/openclaw-traffic-guardian",
"./skills/soul-guardian"
]
},
{
"name": "pico-claw-skills",
"source": "./",
"skills": [
"./skills/picoclaw-security-guardian",
"./skills/picoclaw-self-pen-testing",
"./skills/picoclaw-traffic-guardian"
]
},
{
"name": "repo-internal-skills",
"source": "./",
"skills": [
"./skills/claw-release"
]
}
]
}
+406
View File
@@ -0,0 +1,406 @@
{
"name": "clawhub-cli-pin",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "clawhub-cli-pin",
"dependencies": {
"clawhub": "0.7.0"
}
},
"node_modules/@ark/schema": {
"version": "0.56.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/@ark/schema/-/schema-0.56.0.tgz",
"integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==",
"dependencies": {
"@ark/util": "0.56.0"
}
},
"node_modules/@ark/util": {
"version": "0.56.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/@ark/util/-/util-0.56.0.tgz",
"integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA=="
},
"node_modules/@clack/core": {
"version": "0.5.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/@clack/core/-/core-0.5.0.tgz",
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
"dependencies": {
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@clack/prompts": {
"version": "0.11.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/@clack/prompts/-/prompts-0.11.0.tgz",
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
"dependencies": {
"@clack/core": "0.5.0",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/arkregex": {
"version": "0.0.5",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/arkregex/-/arkregex-0.0.5.tgz",
"integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==",
"dependencies": {
"@ark/util": "0.56.0"
}
},
"node_modules/arktype": {
"version": "2.2.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/arktype/-/arktype-2.2.0.tgz",
"integrity": "sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ==",
"dependencies": {
"@ark/schema": "0.56.0",
"@ark/util": "0.56.0",
"arkregex": "0.0.5"
}
},
"node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/clawhub": {
"version": "0.7.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/clawhub/-/clawhub-0.7.0.tgz",
"integrity": "sha512-volW6SbX8PawlnRxxCoUTKv5Pi+N3MrBi3hlO5/m9bVaO43UFciEeYti9+01c2U5n/SKhUkw7ASvnleyNmcoSA==",
"dependencies": {
"@clack/prompts": "^0.11.0",
"arktype": "^2.1.29",
"commander": "^14.0.2",
"fflate": "^0.8.2",
"ignore": "^7.0.5",
"json5": "^2.2.3",
"mime": "^4.1.0",
"ora": "^9.0.0",
"p-retry": "^7.1.1",
"semver": "^7.7.3",
"undici": "^7.16.0"
},
"bin": {
"clawdhub": "bin/clawdhub.js",
"clawhub": "bin/clawdhub.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/cli-cursor/-/cli-cursor-5.0.0.tgz",
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
"dependencies": {
"restore-cursor": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/cli-spinners/-/cli-spinners-3.4.0.tgz",
"integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==",
"engines": {
"node": ">=18.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/commander/-/commander-14.0.3.tgz",
"integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==",
"engines": {
"node": ">=20"
}
},
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="
},
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/is-interactive": {
"version": "2.0.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/is-interactive/-/is-interactive-2.0.0.tgz",
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-network-error": {
"version": "1.3.2",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/is-network-error/-/is-network-error-1.3.2.tgz",
"integrity": "sha512-PhBY86zaxNZUuWP6h13Vu5oFe0XY6/UlKzQnYFELzGVHygP3MxmvTfYSG7GN3aIab/iWudSMgjSnG9Dq+nHrgA==",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/log-symbols": {
"version": "7.0.1",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/log-symbols/-/log-symbols-7.0.1.tgz",
"integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==",
"dependencies": {
"is-unicode-supported": "^2.0.0",
"yoctocolors": "^2.1.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime": {
"version": "4.1.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/mime/-/mime-4.1.0.tgz",
"integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==",
"funding": [
"https://github.com/sponsors/broofa"
],
"bin": {
"mime": "bin/cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/mimic-function": {
"version": "5.0.1",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/mimic-function/-/mimic-function-5.0.1.tgz",
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/onetime": {
"version": "7.0.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/onetime/-/onetime-7.0.0.tgz",
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
"dependencies": {
"mimic-function": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora": {
"version": "9.4.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/ora/-/ora-9.4.0.tgz",
"integrity": "sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==",
"dependencies": {
"chalk": "^5.6.2",
"cli-cursor": "^5.0.0",
"cli-spinners": "^3.2.0",
"is-interactive": "^2.0.0",
"is-unicode-supported": "^2.1.0",
"log-symbols": "^7.0.1",
"stdin-discarder": "^0.3.2",
"string-width": "^8.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-retry": {
"version": "7.1.1",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/p-retry/-/p-retry-7.1.1.tgz",
"integrity": "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==",
"dependencies": {
"is-network-error": "^1.1.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/restore-cursor/-/restore-cursor-5.1.0.tgz",
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
"dependencies": {
"onetime": "^7.0.0",
"signal-exit": "^4.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/semver": {
"version": "7.8.4",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/semver/-/semver-7.8.4.tgz",
"integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/sisteransi/-/sisteransi-1.0.5.tgz",
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
},
"node_modules/stdin-discarder": {
"version": "0.3.2",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/stdin-discarder/-/stdin-discarder-0.3.2.tgz",
"integrity": "sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width": {
"version": "8.2.1",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/string-width/-/string-width-8.2.1.tgz",
"integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==",
"dependencies": {
"get-east-asian-width": "^1.5.0",
"strip-ansi": "^7.1.2"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/undici": {
"version": "7.27.2",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/undici/-/undici-7.27.2.tgz",
"integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/yoctocolors": {
"version": "2.1.2",
"resolved": "https://prompt-security-443370709039.d.codeartifact.eu-north-1.amazonaws.com/npm/npm-proxy/yoctocolors/-/yoctocolors-2.1.2.tgz",
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"name": "clawhub-cli-pin",
"private": true,
"description": "Pins the clawhub CLI used by skill-release.yml; package-lock.json provides the integrity hashes. Bump the version here and regenerate the lockfile with: npm install --package-lock-only",
"dependencies": {
"clawhub": "0.7.0"
}
}
+14 -2
View File
@@ -53,9 +53,21 @@ jobs:
- name: Collect traffic
env:
GH_TRAFFIC_TOKEN: ${{ secrets.TRAFFIC_ARCHIVE_TOKEN || github.token }}
# Traffic endpoints reject the Actions GITHUB_TOKEN ("Resource not
# accessible by integration") — a PAT from a user with push access
# is required: classic with repo scope, or fine-grained with read
# access to Administration on this repository.
GH_TRAFFIC_TOKEN: ${{ secrets.TRAFFIC_ARCHIVE_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: node scripts/archive-github-traffic.mjs --archive-dir "${TRAFFIC_ARCHIVE_DIR}"
run: |
set -euo pipefail
if [ -z "${GH_TRAFFIC_TOKEN}" ]; then
echo "::error::No traffic-capable token configured. Set the TRAFFIC_ARCHIVE_TOKEN secret to a PAT with push access (classic: repo scope; fine-grained: Administration read)."
exit 1
fi
node scripts/archive-github-traffic.mjs --archive-dir "${TRAFFIC_ARCHIVE_DIR}"
- name: Commit archive
run: |
+6 -2
View File
@@ -111,8 +111,12 @@ jobs:
run: node scripts/test-nvd-ghsa-consolidation-workflow.mjs
- name: NVD + GHSA Pipeline Dry Run
run: node scripts/test-nvd-ghsa-pipeline-dry-run.mjs
- name: Skill Release Workflow Tests
run: node scripts/test-skill-release-workflow.mjs
- name: Skill Release Tooling Tests
run: |
set -euo pipefail
for test_file in scripts/test-skill-*.mjs; do
node "$test_file"
done
- name: Deploy Pages Advisory Checksums Tests
run: node scripts/test-deploy-pages-checksums.mjs
- name: GitHub Traffic Archive Tests
+1 -1
View File
@@ -1069,7 +1069,7 @@ jobs:
--event workflow_dispatch \
--limit 50 \
--json databaseId,createdAt,headSha \
--jq --arg since "$DISPATCHED_AT" --arg sha "$EXPECTED_HEAD_SHA" '
| jq -r --arg since "$DISPATCHED_AT" --arg sha "$EXPECTED_HEAD_SHA" '
map(select(.createdAt >= $since and .headSha == $sha))
| sort_by(.createdAt)
| last
+571 -143
View File
@@ -7,6 +7,9 @@ on:
pull_request:
paths:
- 'skills/**'
- '.github/workflows/skill-release.yml'
- 'scripts/ci/**'
- 'scripts/test-skill-*.mjs'
workflow_dispatch:
inputs:
tag:
@@ -16,8 +19,8 @@ on:
permissions: read-all
env:
CLAWHUB_CLI_VERSION: 0.7.0
# The clawhub CLI version is pinned (with integrity hashes) in
# .github/clawhub-cli/package-lock.json — bump it there.
concurrency:
group: skill-release-${{ github.ref }}
@@ -35,6 +38,11 @@ jobs:
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- name: Verify signing key consistency (repo + docs)
run: ./scripts/ci/verify_signing_key_consistency.sh
@@ -82,7 +90,15 @@ jobs:
'skills/*/**' \
':(exclude)skills/*/test/**' \
':(exclude)skills/*/tests/**' \
| awk -F/ 'NF >= 3 {print $1 "/" $2}' \
| awk -F/ '
NF >= 3 {
path = tolower($0)
name = tolower($NF)
if (path ~ /(^|\/)(__tests__|test|tests)\//) next
if (name ~ /^(test|spec)[_-]/ || name ~ /\.(test|spec)\./) next
print $1 "/" $2
}
' \
| sort -u > "${touched_skills_file}"
if [ ! -s "${touched_skills_file}" ]; then
@@ -144,14 +160,6 @@ jobs:
md_version_changed=true
fi
if [ "${json_version_changed}" != "true" ] && [ "${md_version_changed}" != "true" ]; then
echo "::error file=${skill_dir}::Changed skill package has no version bump. Update skill.json and SKILL.md versions and add CHANGELOG.md release notes."
failures=$((failures + 1))
continue
fi
echo "Version bump detected for ${skill_dir} (skill.json changed: ${json_version_changed}, SKILL.md changed: ${md_version_changed})"
if [ ! -f "${json_path}" ]; then
echo "::error file=${json_path}::Missing skill.json after version bump."
failures=$((failures + 1))
@@ -182,6 +190,20 @@ jobs:
continue
fi
skill_release_name="$(basename "${skill_dir}")"
release_tag="${skill_release_name}-v${head_json_version}"
if [ "${json_version_changed}" != "true" ] && [ "${md_version_changed}" != "true" ]; then
if git show-ref --verify --quiet "refs/tags/${release_tag}"; then
echo "::error file=${skill_dir}::Changed skill package has no version bump and release tag ${release_tag} already exists. Update skill.json and SKILL.md versions and add CHANGELOG.md release notes."
failures=$((failures + 1))
continue
fi
echo "No version bump detected for ${skill_dir}, but release tag ${release_tag} does not exist; treating ${head_json_version} as unreleased."
else
echo "Version bump detected for ${skill_dir} (skill.json changed: ${json_version_changed}, SKILL.md changed: ${md_version_changed})"
fi
echo "Version parity OK for ${skill_dir}: ${head_json_version}"
changelog_path="${skill_dir}/CHANGELOG.md"
@@ -223,11 +245,17 @@ jobs:
fi
if [ "${failures}" -gt 0 ]; then
echo "::error::Found ${failures} skill metadata/release-notes issue(s) across ${checked_skills} bumped skill(s)."
echo "::error::Found ${failures} skill metadata/release-notes issue(s) across ${checked_skills} changed skill(s)."
exit 1
fi
echo "Validated ${checked_skills} bumped skill(s): version parity and changelog release notes are present."
echo "Validated ${checked_skills} changed skill(s): version parity and changelog release notes are present."
- name: Validate npx skills install docs
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/ci/validate_skill_install_docs.mjs --base "$BASE_SHA" --head "$HEAD_SHA"
release:
if: github.event_name == 'pull_request'
@@ -241,6 +269,21 @@ jobs:
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- name: Install SkillSpector
run: |
set -euo pipefail
python3 -m venv /tmp/skillspector-venv
. /tmp/skillspector-venv/bin/activate
git clone --depth 1 https://github.com/NVIDIA/SkillSpector.git /tmp/skillspector
make -C /tmp/skillspector install
echo "/tmp/skillspector-venv/bin" >> "$GITHUB_PATH"
skillspector --help >/dev/null
- name: Generate test signing key for dry-run
run: |
set -euo pipefail
@@ -365,12 +408,15 @@ jobs:
}
touched_skills_file="$(mktemp)"
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- 'skills/*/skill.json' 'skills/*/SKILL.md' \
git diff --name-only "${BASE_SHA}...${HEAD_SHA}" -- \
'skills/*/**' \
':(exclude)skills/*/test/**' \
':(exclude)skills/*/tests/**' \
| awk -F/ 'NF >= 3 {print $1 "/" $2}' \
| sort -u > "${touched_skills_file}"
if [ ! -s "${touched_skills_file}" ]; then
echo "No skill metadata files changed in this PR."
echo "No release-relevant skill package files changed in this PR."
rm -f "${touched_skills_file}"
exit 0
fi
@@ -396,7 +442,52 @@ jobs:
is_test_release_path() {
local lower="${1,,}"
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == */test/* || "$lower" == */tests/* ]]
local name="${lower##*/}"
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == __tests__/* || "$lower" == */test/* || "$lower" == */tests/* || "$lower" == */__tests__/* || "$name" == test_* || "$name" == test-* || "$name" == spec_* || "$name" == spec-* || "$name" == *.test.* || "$name" == *.spec.* ]]
}
generate_skillspector_report() {
local skill_dir="$1"
local report_path="$2"
set +e
skillspector scan "${skill_dir}" --no-llm --format markdown --output "${report_path}"
local status=$?
set -e
if [ ! -s "${report_path}" ]; then
echo "::error file=${skill_dir}::SkillSpector did not produce a report."
return 1
fi
if [ "${status}" -ne 0 ]; then
echo "::warning file=${report_path}::SkillSpector returned exit code ${status}; report is included for review."
fi
}
add_release_asset_checksum() {
local out_assets="$1"
local asset="$2"
local file_path="${out_assets}/${asset}"
local sha256
local size
local tmp_json
if [ ! -s "${file_path}" ]; then
echo "::error file=${file_path}::Required release trust artifact is missing or empty."
return 1
fi
sha256="$(sha256sum "${file_path}" | awk '{print $1}')"
size="$(stat -c%s "${file_path}" 2>/dev/null || stat -f%z "${file_path}")"
tmp_json="$(mktemp)"
jq \
--arg key "${asset}" \
--arg sha "${sha256}" \
--argjson sz "${size}" \
'.files += {($key): {sha256: $sha, size: $sz, path: $key}}' \
"${out_assets}/checksums.json" > "${tmp_json}"
mv "${tmp_json}" "${out_assets}/checksums.json"
}
while IFS= read -r skill_dir; do
@@ -447,11 +538,6 @@ jobs:
md_version_changed=true
fi
if [ "${json_version_changed}" != "true" ] && [ "${md_version_changed}" != "true" ]; then
echo "No version bump detected for ${skill_dir}; skipping dry-run."
continue
fi
if [ -z "${head_json_version}" ] || [ -z "${head_md_version}" ] || [ "${head_json_version}" != "${head_md_version}" ]; then
echo "::error file=${skill_dir}::Version metadata is invalid for dry-run. Ensure validate-pr-version-sync passes."
failures=$((failures + 1))
@@ -540,9 +626,9 @@ jobs:
# --- Create zip preserving directory structure ---
zip_name="${skill_name}-v${version}.zip"
(cd "${staging_dir}" && zip -qr "${OLDPWD}/${out_assets}/${zip_name}" .)
if unzip -Z1 "${out_assets}/${zip_name}" | grep -Eiq '(^|/)(test|tests)/'; then
if unzip -Z1 "${out_assets}/${zip_name}" | grep -Eiq '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.'; then
echo "::error::Dry-run release archive contains test-only files: ${zip_name}"
unzip -Z1 "${out_assets}/${zip_name}" | grep -Ei '(^|/)(test|tests)/' || true
unzip -Z1 "${out_assets}/${zip_name}" | grep -Ei '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.' || true
failures=$((failures + 1))
fi
@@ -622,6 +708,58 @@ jobs:
continue
fi
# --- Generate release trust packet and include it in signed checksums ---
node scripts/ci/generate_skill_release_trust_packet.mjs \
"${skill_dir}" \
"${out_assets}" \
--repository "${{ github.repository }}" \
--tag "${tag}" \
--source-ref "${HEAD_SHA}"
# --- Generate SkillSpector report ---
if ! generate_skillspector_report "${inner_dir}" "${out_assets}/skillspector-report.md"; then
failures=$((failures + 1))
rm -rf "${staging_dir}"
echo "::endgroup::"
continue
fi
if ! add_release_asset_checksum "${out_assets}" "skill-card.md"; then
failures=$((failures + 1))
rm -rf "${staging_dir}"
echo "::endgroup::"
continue
fi
if ! add_release_asset_checksum "${out_assets}" "permissions.json"; then
failures=$((failures + 1))
rm -rf "${staging_dir}"
echo "::endgroup::"
continue
fi
if ! add_release_asset_checksum "${out_assets}" "install.md"; then
failures=$((failures + 1))
rm -rf "${staging_dir}"
echo "::endgroup::"
continue
fi
if ! add_release_asset_checksum "${out_assets}" "skillspector-report.md"; then
failures=$((failures + 1))
rm -rf "${staging_dir}"
echo "::endgroup::"
continue
fi
if ! jq -e . "${out_assets}/checksums.json" >/dev/null 2>&1; then
echo "::error::Generated checksums.json is invalid JSON after adding release trust artifacts."
failures=$((failures + 1))
rm -rf "${staging_dir}"
echo "::endgroup::"
continue
fi
# --- Copy skill.json and root-level docs alongside the zip ---
cp "${json_path}" "${out_assets}/skill.json"
if [ -f "${skill_dir}/SKILL.md" ]; then
@@ -646,12 +784,227 @@ jobs:
fi
if [ "${dry_run_count}" -eq 0 ]; then
echo "No version bumps detected in changed skill metadata files."
echo "No changed skill directories required dry-run assets."
exit 0
fi
echo "Release dry-run completed successfully for ${dry_run_count} changed skill(s)."
- name: Prepare SkillSpector PR report artifact
if: always()
run: |
set -euo pipefail
rm -rf dist/skillspector-pr-reports
mkdir -p dist/dry-run dist/skillspector-pr-reports
found_reports=false
while IFS= read -r report_path; do
tag="${report_path#dist/dry-run/}"
tag="${tag%%/*}"
mkdir -p "dist/skillspector-pr-reports/${tag}"
cp "${report_path}" "dist/skillspector-pr-reports/${tag}/skillspector-report.md"
found_reports=true
done < <(find dist/dry-run -path '*/release-assets/skillspector-report.md' -type f | sort)
if [ "${found_reports}" != "true" ]; then
printf 'No SkillSpector reports were generated for this pull request.\n' > dist/skillspector-pr-reports/NO_SKILLSPECTOR_REPORTS.txt
fi
- name: Upload SkillSpector PR reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: skillspector-pr-reports
path: dist/skillspector-pr-reports
retention-days: 14
comment-skillspector-report:
if: always() && github.event_name == 'pull_request' && needs.release.result != 'cancelled'
needs: release
runs-on: ubuntu-latest
continue-on-error: true
permissions:
actions: read
contents: read
issues: write
pull-requests: read
steps:
- name: Download SkillSpector reports
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: skillspector-pr-reports
path: skillspector-pr-reports
- name: Comment SkillSpector reports
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const fs = require("node:fs/promises");
const path = require("node:path");
const root = "skillspector-pr-reports";
const maxCommentLength = 65000;
async function findReports(dir) {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch (error) {
if (error.code === "ENOENT") {
return [];
}
throw error;
}
const reports = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
reports.push(...await findReports(fullPath));
} else if (entry.isFile() && entry.name === "skillspector-report.md") {
reports.push(fullPath);
}
}
return reports;
}
function tagFromReportPath(reportPath) {
const parts = reportPath.split(path.sep);
const releaseAssetsIndex = parts.lastIndexOf("release-assets");
if (releaseAssetsIndex > 0) {
return parts[releaseAssetsIndex - 1];
}
return path.basename(path.dirname(reportPath));
}
function sanitizeReportForComment(report) {
const omittedBlock = "_[code block omitted from PR comment; download the workflow artifact for raw details]_";
return report
.replace(/```[\s\S]*?```/g, omittedBlock)
.split(/\r?\n/)
.filter((line) => !/^\s{4,}\S/.test(line))
.join("\n")
.replace(/`[^`\n]*`/g, "`[inline snippet omitted]`")
.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[redacted-email]")
.replace(/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, "[redacted-aws-key]")
.replace(/\b(?:ghp|gho|ghu|ghs|ghr|github_pat|glpat|xox[baprs]?|sk|pk)_[A-Za-z0-9_=-]{12,}\b/gi, "[redacted-token]")
.replace(/\b[A-Za-z0-9+/]{40,}={0,2}\b/g, "[redacted-secret-like-value]")
.trimEnd();
}
function buildComment({ tag, report }) {
const marker = `<!-- clawsec-skillspector-report:${tag} -->`;
const sanitizedReport = sanitizeReportForComment(report);
const footer = [
"_Generated by the Skill Release dry-run for `" + tag + "`._",
"_Raw snippets, code blocks, inline code, emails, and token-like values are omitted from this PR comment._",
"_Download the `skillspector-pr-reports` workflow artifact for the full report._",
].join("\n");
let body = `${marker}\n${sanitizedReport}\n\n${footer}`;
if (body.length <= maxCommentLength) {
return body;
}
const truncatedFooter = [
"_Report truncated because it exceeds GitHub's comment size limit._",
"_Download the `skillspector-pr-reports` workflow artifact for the full report._",
footer,
].join("\n");
const budget = maxCommentLength - marker.length - truncatedFooter.length - 8;
return `${marker}\n${sanitizedReport.slice(0, Math.max(0, budget)).trimEnd()}\n\n${truncatedFooter}`;
}
const reports = await findReports(root);
if (reports.length === 0) {
core.info("No SkillSpector reports found; nothing to comment.");
return;
}
const comments = await github.paginate(github.rest.issues.listComments, {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
per_page: 100,
});
for (const reportPath of reports.sort()) {
const tag = tagFromReportPath(reportPath);
const report = await fs.readFile(reportPath, "utf8");
const marker = `<!-- clawsec-skillspector-report:${tag} -->`;
const body = buildComment({ tag, report });
const existing = comments.find((comment) => comment.body?.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated SkillSpector PR comment for ${tag}.`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
core.info(`Created SkillSpector PR comment for ${tag}.`);
}
}
simulate-tag-release-build:
if: github.event_name == 'pull_request'
needs: validate-pr-version-sync
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- name: Install SkillSpector
run: |
set -euo pipefail
python3 -m venv /tmp/skillspector-venv
. /tmp/skillspector-venv/bin/activate
git clone --depth 1 https://github.com/NVIDIA/SkillSpector.git /tmp/skillspector
make -C /tmp/skillspector install
echo "/tmp/skillspector-venv/bin" >> "$GITHUB_PATH"
skillspector --help >/dev/null
- name: Simulate tag release build
run: |
set -euo pipefail
mkdir -p dist/tag-release-simulation
for skill_json in skills/*/skill.json; do
skill_dir="${skill_json%/skill.json}"
skill_name="$(basename "${skill_dir}")"
echo "::group::Simulate tag release build for ${skill_name}"
node scripts/ci/simulate_skill_tag_release.mjs \
"${skill_dir}" \
"dist/tag-release-simulation/${skill_name}" \
--repository "${{ github.repository }}" \
--source-ref "${{ github.event.pull_request.head.sha }}"
jq -e '.simulated_version | test("^[0-9]+\\.[0-9]+\\.[0-9]+(-[a-zA-Z0-9]+)?$")' \
"dist/tag-release-simulation/${skill_name}/simulation-summary.json" >/dev/null
test -s "dist/tag-release-simulation/${skill_name}/release-assets/checksums.json"
test -s "dist/tag-release-simulation/${skill_name}/release-assets/checksums.sig"
test -s "dist/tag-release-simulation/${skill_name}/release-assets/signing-public.pem"
test -s "dist/tag-release-simulation/${skill_name}/release-assets/skillspector-report.md"
echo "::endgroup::"
done
release-tag:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
@@ -664,6 +1017,7 @@ jobs:
publishable: ${{ steps.publishable.outputs.publishable }}
openclaw_skill: ${{ steps.publishable.outputs.openclaw_skill }}
publish_clawhub: ${{ steps.publishable.outputs.publish_clawhub }}
clawhub_slug: ${{ steps.publishable.outputs.clawhub_slug }}
steps:
- name: Parse tag
id: parse
@@ -761,16 +1115,32 @@ jobs:
PUBLISH_CLAWHUB=true
fi
CLAWHUB_SLUG=$(node scripts/ci/resolve_clawhub_slug.mjs "$SKILL_PATH")
echo "internal=${INTERNAL}" >> $GITHUB_OUTPUT
echo "openclaw_skill=${OPENCLAW_SKILL}" >> $GITHUB_OUTPUT
echo "publish_clawhub=${PUBLISH_CLAWHUB}" >> $GITHUB_OUTPUT
echo "publishable=${PUBLISHABLE}" >> $GITHUB_OUTPUT
echo "clawhub_slug=${CLAWHUB_SLUG}" >> $GITHUB_OUTPUT
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 20
- name: Validate npx skills install docs
run: node scripts/ci/validate_skill_install_docs.mjs --skills "${{ steps.parse.outputs.skill_path }}"
- name: Install SkillSpector
run: |
set -euo pipefail
python3 -m venv /tmp/skillspector-venv
. /tmp/skillspector-venv/bin/activate
git clone --depth 1 https://github.com/NVIDIA/SkillSpector.git /tmp/skillspector
make -C /tmp/skillspector install
echo "/tmp/skillspector-venv/bin" >> "$GITHUB_PATH"
skillspector --help >/dev/null
- name: Sign embedded advisory feed and verify
if: hashFiles(format('skills/{0}/advisories/feed.json', steps.parse.outputs.skill_name)) != ''
uses: ./.github/actions/sign-and-verify
@@ -867,7 +1237,51 @@ jobs:
is_test_release_path() {
local lower="${1,,}"
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == */test/* || "$lower" == */tests/* ]]
local name="${lower##*/}"
[[ "$lower" == test/* || "$lower" == tests/* || "$lower" == __tests__/* || "$lower" == */test/* || "$lower" == */tests/* || "$lower" == */__tests__/* || "$name" == test_* || "$name" == test-* || "$name" == spec_* || "$name" == spec-* || "$name" == *.test.* || "$name" == *.spec.* ]]
}
generate_skillspector_report() {
local skill_dir="$1"
local report_path="$2"
set +e
skillspector scan "${skill_dir}" --no-llm --format markdown --output "${report_path}"
local status=$?
set -e
if [ ! -s "${report_path}" ]; then
echo "::error file=${skill_dir}::SkillSpector did not produce a report."
return 1
fi
if [ "${status}" -ne 0 ]; then
echo "::warning file=${report_path}::SkillSpector returned exit code ${status}; report is included for review."
fi
}
add_release_asset_checksum() {
local asset="$1"
local file_path="release-assets/${asset}"
local sha256
local size
local tmp_json
if [ ! -s "${file_path}" ]; then
echo "::error file=${file_path}::Required release trust artifact is missing or empty."
return 1
fi
sha256="$(sha256sum "${file_path}" | awk '{print $1}')"
size="$(stat -c%s "${file_path}" 2>/dev/null || stat -f%z "${file_path}")"
tmp_json="$(mktemp)"
jq \
--arg key "${asset}" \
--arg sha "${sha256}" \
--argjson sz "${size}" \
'.files += {($key): {sha256: $sha, size: $sz, path: $key}}' \
release-assets/checksums.json > "${tmp_json}"
mv "${tmp_json}" release-assets/checksums.json
}
# --- Stage SBOM files preserving directory structure ---
@@ -905,9 +1319,9 @@ jobs:
# --- Create zip preserving directory structure ---
ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip"
(cd "$STAGING_DIR" && zip -qr "$OLDPWD/release-assets/$ZIP_NAME" .)
if unzip -Z1 "release-assets/$ZIP_NAME" | grep -Eiq '(^|/)(test|tests)/'; then
if unzip -Z1 "release-assets/$ZIP_NAME" | grep -Eiq '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.'; then
echo "::error::Release archive contains test-only files: $ZIP_NAME"
unzip -Z1 "release-assets/$ZIP_NAME" | grep -Ei '(^|/)(test|tests)/' || true
unzip -Z1 "release-assets/$ZIP_NAME" | grep -Ei '(^|/)(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\.(test|spec)\.' || true
exit 1
fi
@@ -971,6 +1385,32 @@ jobs:
files: $files
}' > "release-assets/checksums.json"
# --- Generate release trust packet and include it in signed checksums ---
node scripts/ci/generate_skill_release_trust_packet.mjs \
"$SKILL_PATH" \
release-assets \
--repository "${{ github.repository }}" \
--tag "$TAG" \
--source-ref "$TAG"
# --- Generate SkillSpector report ---
generate_skillspector_report "$INNER_DIR" "release-assets/skillspector-report.md"
test -s release-assets/skill-card.md
test -s release-assets/permissions.json
test -s release-assets/install.md
test -s release-assets/skillspector-report.md
add_release_asset_checksum "skill-card.md"
add_release_asset_checksum "permissions.json"
add_release_asset_checksum "install.md"
add_release_asset_checksum "skillspector-report.md"
if ! jq -e . "release-assets/checksums.json" >/dev/null 2>&1; then
echo "::error::Generated checksums.json is invalid JSON after adding release trust artifacts."
exit 1
fi
# --- Copy skill.json and root-level docs alongside the zip ---
cp "$SKILL_PATH/skill.json" release-assets/skill.json
if [ -f "$SKILL_PATH/SKILL.md" ]; then
@@ -1055,6 +1495,7 @@ jobs:
run: |
set -euo pipefail
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
CLAWHUB_SLUG="${{ steps.publishable.outputs.clawhub_slug }}"
VERSION="${{ steps.parse.outputs.version }}"
REPO="${{ github.repository }}"
TAG="${{ github.ref_name }}"
@@ -1062,13 +1503,33 @@ jobs:
{
echo "quick_install<<INSTALL_EOF"
cat <<EOF
### Agent Skills CLI
**Codex global install:**
\`\`\`bash
npx skills add ${REPO} --skill ${SKILL_NAME} --agent codex --global --yes
\`\`\`
**OpenClaw global install:**
\`\`\`bash
npx skills add ${REPO} --skill ${SKILL_NAME} --agent openclaw --global --yes
\`\`\`
**Update an installed skill:**
\`\`\`bash
npx skills update ${SKILL_NAME}
\`\`\`
EOF
if [ "${{ steps.publishable.outputs.publish_clawhub }}" = "true" ] && [ "${{ steps.publishable.outputs.openclaw_skill }}" = "true" ]; then
cat <<EOF
### Quick Install
**Via ClawHub (recommended):**
\`\`\`bash
npx clawhub@latest install ${SKILL_NAME}
npx clawhub@latest install ${CLAWHUB_SLUG}
\`\`\`
**If you already have \`clawsec-suite\` installed:**
@@ -1115,38 +1576,61 @@ jobs:
echo "INSTALL_EOF"
} >> "$GITHUB_OUTPUT"
- name: Prepare GitHub release body
env:
SKILL_NAME: ${{ steps.parse.outputs.skill_name }}
VERSION: ${{ steps.parse.outputs.version }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
QUICK_INSTALL: ${{ steps.install.outputs.quick_install }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
node -e '
const { readFileSync, writeFileSync } = require("node:fs");
const bodyPath = `${process.env.RUNNER_TEMP}/skill-release-body.md`;
const report = readFileSync("release-assets/skillspector-report.md", "utf8").trimEnd();
const body = [
`## ${process.env.SKILL_NAME} ${process.env.VERSION}`,
"",
process.env.CHANGELOG || "",
"",
process.env.QUICK_INSTALL || "",
"",
report,
"",
`Download the generated release-payload scan: [skillspector-report.md](https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/skillspector-report.md)`,
"",
"### Verification",
"",
"`checksums.json` is cryptographically signed (`checksums.sig`) using the ClawSec CI signing key.",
"Verify the signature first, then trust hashes from `checksums.json`:",
"```bash",
`curl -sLO https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/checksums.json`,
`curl -sLO https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/checksums.sig`,
`curl -sLO https://github.com/${process.env.REPO}/releases/download/${process.env.TAG}/signing-public.pem`,
"openssl base64 -d -A -in checksums.sig -out checksums.sig.bin",
"openssl pkeyutl -verify -rawin -pubin -inkey signing-public.pem -sigfile checksums.sig.bin -in checksums.json",
"```",
"",
"### Files",
"",
"See `checksums.json` for the complete file manifest with SHA256 hashes.",
"The zip archive preserves the full directory structure of the skill.",
"",
"---",
"*Released by ClawSec skill distribution pipeline*",
].join("\n");
writeFileSync(bodyPath, `${body}\n`);
'
- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
name: "${{ steps.parse.outputs.skill_name }} ${{ steps.parse.outputs.version }}"
tag_name: ${{ github.ref_name }}
files: release-assets/*
body: |
## ${{ steps.parse.outputs.skill_name }} ${{ steps.parse.outputs.version }}
${{ steps.changelog.outputs.changelog }}
${{ steps.install.outputs.quick_install }}
### Verification
`checksums.json` is cryptographically signed (`checksums.sig`) using the ClawSec CI signing key.
Verify the signature first, then trust hashes from `checksums.json`:
```bash
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.json
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/checksums.sig
curl -sLO https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/signing-public.pem
openssl base64 -d -A -in checksums.sig -out checksums.sig.bin
openssl pkeyutl -verify -rawin -pubin -inkey signing-public.pem -sigfile checksums.sig.bin -in checksums.json
```
### Files
See `checksums.json` for the complete file manifest with SHA256 hashes.
The zip archive preserves the full directory structure of the skill.
---
*Released by ClawSec skill distribution pipeline*
body_path: ${{ runner.temp }}/skill-release-body.md
draft: false
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
env:
@@ -1201,6 +1685,10 @@ jobs:
contents: read
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}
AWS_REGION: eu-north-1
steps:
- name: Check if publishable
if: needs.release-tag.outputs.publish_clawhub != 'true'
@@ -1220,51 +1708,12 @@ jobs:
- name: Install clawhub CLI
if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
run: npm install -g clawhub@${CLAWHUB_CLI_VERSION}
run: bash scripts/ci/install_clawhub_cli.sh
- name: Patch clawhub publish payload workaround
# Temporary: clawhub@0.7.0 publish payload is missing acceptLicenseTerms.
if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
run: |
node <<'NODE'
const { execSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");
const npmRoot = execSync("npm root -g", { encoding: "utf8" }).trim();
const publishScriptPath = path.join(
npmRoot,
"clawhub",
"dist",
"cli",
"commands",
"publish.js"
);
if (!fs.existsSync(publishScriptPath)) {
throw new Error(`clawhub publish script not found: ${publishScriptPath}`);
}
const original = fs.readFileSync(publishScriptPath, "utf8");
if (original.includes("acceptLicenseTerms: true")) {
console.log(`[patch-clawhub] Already patched: ${publishScriptPath}`);
process.exit(0);
}
const payloadPattern = /changelog,\r?\n(\s*)tags,/;
if (!payloadPattern.test(original)) {
throw new Error(
`[patch-clawhub] Could not find expected publish payload pattern in ${publishScriptPath}`
);
}
const patched = original.replace(
payloadPattern,
(_, indent) => `changelog,\n${indent}acceptLicenseTerms: true,\n${indent}tags,`
);
fs.writeFileSync(publishScriptPath, patched, "utf8");
console.log(`[patch-clawhub] Patched: ${publishScriptPath}`);
NODE
run: node scripts/ci/patch_clawhub_publish_payload.mjs
- name: Login to ClawHub
if: needs.release-tag.outputs.publish_clawhub == 'true' && env.CLAWHUB_TOKEN != ''
@@ -1284,23 +1733,24 @@ jobs:
SITE=${CLAWHUB_SITE:-https://clawhub.ai}
REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
SKILL_NAME="${{ needs.release-tag.outputs.skill_name }}"
CLAWHUB_SLUG="${{ needs.release-tag.outputs.clawhub_slug }}"
VERSION="${{ needs.release-tag.outputs.version }}"
export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"
set +e
CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
clawhub inspect "$SKILL_NAME" --version "$VERSION" --json \
clawhub inspect "$CLAWHUB_SLUG" --version "$VERSION" --json \
> /tmp/clawhub-existing-version.json 2> /tmp/clawhub-existing-version.err
STATUS=$?
set -e
if [ "$STATUS" -eq 0 ]; then
echo "::error::ClawHub already contains ${SKILL_NAME}@${VERSION}. Bump the version before tagging."
echo "::error::ClawHub already contains ${CLAWHUB_SLUG}@${VERSION}. Bump the version before tagging."
exit 1
fi
if grep -Eqi "Version not found|Skill not found" /tmp/clawhub-existing-version.err; then
echo "No existing ${SKILL_NAME}@${VERSION} detected in ClawHub. Proceeding."
echo "No existing ${CLAWHUB_SLUG}@${VERSION} detected in ClawHub. Proceeding."
else
echo "::error::Failed to verify ClawHub version precondition."
cat /tmp/clawhub-existing-version.err
@@ -1315,6 +1765,7 @@ jobs:
REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
SKILL_PATH="${{ needs.release-tag.outputs.skill_path }}"
SKILL_NAME="${{ needs.release-tag.outputs.skill_name }}"
CLAWHUB_SLUG="${{ needs.release-tag.outputs.clawhub_slug }}"
VERSION="${{ needs.release-tag.outputs.version }}"
NAME=$(jq -r '.name' "$SKILL_PATH/skill.json")
CHANGELOG="Release ${VERSION} via CI"
@@ -1323,7 +1774,7 @@ jobs:
if ! CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
clawhub publish "$SKILL_PATH" \
--slug "$SKILL_NAME" \
--slug "$CLAWHUB_SLUG" \
--name "$NAME" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
@@ -1333,7 +1784,7 @@ jobs:
exit 1
fi
echo "✓ Successfully published $SKILL_NAME@$VERSION to ClawHub"
echo "✓ Successfully published $SKILL_NAME@$VERSION to ClawHub as $CLAWHUB_SLUG"
republish-clawhub:
# Manual workflow to republish a specific tag to ClawHub
@@ -1344,6 +1795,10 @@ jobs:
contents: read
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}
AWS_REGION: eu-north-1
steps:
- name: Parse tag
id: parse
@@ -1360,6 +1815,12 @@ jobs:
echo "Parsed tag: skill=${SKILL_NAME}, version=${VERSION}"
- name: Checkout workflow helpers
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Prepare ClawHub slug helper
run: cp scripts/ci/resolve_clawhub_slug.mjs "$RUNNER_TEMP/resolve_clawhub_slug.mjs"
- name: Checkout tag
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -1389,6 +1850,8 @@ jobs:
exit 1
fi
CLAWHUB_SLUG=$(node "$RUNNER_TEMP/resolve_clawhub_slug.mjs" "$SKILL_PATH")
echo "clawhub_slug=${CLAWHUB_SLUG}" >> $GITHUB_OUTPUT
echo "Skill is publishable to ClawHub"
- name: Setup Node
@@ -1396,51 +1859,15 @@ jobs:
with:
node-version: 20
- name: Validate npx skills install docs
run: node scripts/ci/validate_skill_install_docs.mjs --skills "${{ steps.parse.outputs.skill_path }}"
- name: Install clawhub CLI
run: npm install -g clawhub@${CLAWHUB_CLI_VERSION}
run: bash scripts/ci/install_clawhub_cli.sh
- name: Patch clawhub publish payload workaround
# Temporary: clawhub@0.7.0 publish payload is missing acceptLicenseTerms.
run: |
node <<'NODE'
const { execSync } = require("node:child_process");
const fs = require("node:fs");
const path = require("node:path");
const npmRoot = execSync("npm root -g", { encoding: "utf8" }).trim();
const publishScriptPath = path.join(
npmRoot,
"clawhub",
"dist",
"cli",
"commands",
"publish.js"
);
if (!fs.existsSync(publishScriptPath)) {
throw new Error(`clawhub publish script not found: ${publishScriptPath}`);
}
const original = fs.readFileSync(publishScriptPath, "utf8");
if (original.includes("acceptLicenseTerms: true")) {
console.log(`[patch-clawhub] Already patched: ${publishScriptPath}`);
process.exit(0);
}
const payloadPattern = /changelog,\r?\n(\s*)tags,/;
if (!payloadPattern.test(original)) {
throw new Error(
`[patch-clawhub] Could not find expected publish payload pattern in ${publishScriptPath}`
);
}
const patched = original.replace(
payloadPattern,
(_, indent) => `changelog,\n${indent}acceptLicenseTerms: true,\n${indent}tags,`
);
fs.writeFileSync(publishScriptPath, patched, "utf8");
console.log(`[patch-clawhub] Patched: ${publishScriptPath}`);
NODE
run: node scripts/ci/patch_clawhub_publish_payload.mjs
- name: Login to ClawHub
run: |
@@ -1464,18 +1891,19 @@ jobs:
REGISTRY=${CLAWHUB_REGISTRY:-$SITE}
SKILL_PATH="${{ steps.parse.outputs.skill_path }}"
SKILL_NAME="${{ steps.parse.outputs.skill_name }}"
CLAWHUB_SLUG="${{ steps.publishable.outputs.clawhub_slug }}"
VERSION="${{ steps.parse.outputs.version }}"
NAME=$(jq -r '.name' "$SKILL_PATH/skill.json")
CHANGELOG="Manual republish of ${VERSION} via workflow_dispatch"
export CLAWHUB_CONFIG_PATH="$HOME/.clawhub-ci/config.json"
echo "Publishing $SKILL_NAME@$VERSION to ClawHub..."
echo "Publishing $SKILL_NAME@$VERSION to ClawHub as $CLAWHUB_SLUG..."
# Publish with idempotent retry handling
if ! CLAWHUB_DISABLE_TELEMETRY=1 CLAWHUB_SITE="$SITE" CLAWHUB_REGISTRY="$REGISTRY" \
clawhub publish "$SKILL_PATH" \
--slug "$SKILL_NAME" \
--slug "$CLAWHUB_SLUG" \
--name "$NAME" \
--version "$VERSION" \
--changelog "$CHANGELOG" \
+1 -1
View File
@@ -58,7 +58,7 @@ ClawSec is a **complete security skill suite for AI agent platforms**. It provid
| clawsec-nanoclaw | NanoClaw | Yes | Yes | Yes | Yes | No |
| clawsec-scanner | OpenClaw | Yes | No | Yes | Yes | No |
| clawsec-suite | OpenClaw | Yes | Yes | No | Yes | No |
| clawtributor | OpenClaw | Yes | No | No | No | No |
| clawtributor | All core platforms | No | No | No | No | No |
| hermes-attestation-guardian | Hermes | Yes (signed advisory feed verification) | Yes | No | Limited (advisory preflight gating only; no artifact signature/provenance install verification) | No |
| hermes-traffic-guardian | Hermes | No | Planned posture export only | No | No | Spec baseline |
| nanoclaw-traffic-guardian | NanoClaw | No | No | No | No | Spec baseline |
+1211 -1519
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
v+PiWmjIkY6zdIyI9xJX0l0aTy0Azp1+LoZR6qaiDZJnXFuSBX4Sw/x5tMdTb0xSbqdDTJOZwwWI8coPVepzBw==
jPrlTYwicRwoQgTs5Rk3Y3g6Lz78jNRs9ZNf0R09M4jkJokZENxfvhvHphI9MH4u+7wv0sFZ+yZbQtJ42y+hCQ==
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
SCkRaPMF6IYDwZuR7/JJXxpB7A7ebuMvLqK827uWX0yfEJr7l2gyLpxvHsEpWJDzE4gchxd5yqJx5qF/yqNwAg==
M1Jm4YHXsm0msygmd+XCJBRWMrXIjQfv1Y5v7XS8RCachLQwEzUJ1nhhic6CXxItNLmvgmDjVCMPVdHpnOMqDA==
+2 -2
View File
@@ -7,7 +7,7 @@
ClawSec
Security skill suite for AI agents (integrity checks, drift detection, advisory feed).
Agent install:
Available via clawhub: npx clawhub@latest install clawsec-suite
Available via npx skills: npx skills add prompt-security/clawsec --skill clawsec-suite -a openclaw -y
OR
curl -sL https://clawsec.prompt.security/releases/latest/download/SKILL.md
-->
@@ -141,7 +141,7 @@
ClawSec
Security skill suite for AI agents (integrity checks, drift detection, advisory feed).
Agent install:
Available via clawhub: npx clawhub@latest install clawsec-suite
Available via npx skills: npx skills add prompt-security/clawsec --skill clawsec-suite -a openclaw -y
OR
curl -sL https://clawsec.prompt.security/releases/latest/download/SKILL.md
</noscript>
+478 -948
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -31,13 +31,13 @@
"@types/node": "^25.8.0",
"@typescript-eslint/eslint-plugin": "^8.55.0",
"@typescript-eslint/parser": "^8.58.1",
"@vitejs/plugin-react": "^5.1.4",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^9.39.4",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"fast-check": "^4.7.0",
"typescript": "~5.9.3",
"vite": "^7.3.2"
"vite": "^8.0.16"
},
"overrides": {
"ajv": "6.14.0",
+14 -2
View File
@@ -13,7 +13,7 @@ export const Home: React.FC = () => {
const [currentFileIndex, setCurrentFileIndex] = useState(0);
const [currentPlatformIndex, setCurrentPlatformIndex] = useState(0);
const curlCommand = `npx clawhub@latest install clawsec-suite`;
const curlCommand = `npx skills add prompt-security/clawsec --skill clawsec-suite -a openclaw -y`;
// Rotate file names every 2-3 seconds
useEffect(() => {
@@ -44,7 +44,7 @@ export const Home: React.FC = () => {
};
}, []);
const humanInstruction = `Please install clawsec-suite from clawhubnpx clawhub@latest install clawsec-suite`;
const humanInstruction = `Please install clawsec-suite with npx skills add prompt-security/clawsec --skill clawsec-suite -a openclaw -y`;
const handleCopyCurl = () => {
navigator.clipboard.writeText(curlCommand);
@@ -285,6 +285,18 @@ export const Home: React.FC = () => {
</div>
</>
)}
<p className="mt-4 text-center text-xs leading-relaxed text-gray-500">
* For harnesses other than OpenClaw, consult the{' '}
<a
href="https://github.com/prompt-security/clawsec#skill-feature-matrix"
target="_blank"
rel="noreferrer"
className="text-clawd-accent hover:text-clawd-accent/80 underline underline-offset-2"
>
README Skill Feature Matrix
</a>
.
</p>
</div>
</div>
</section>
+8 -1
View File
@@ -321,7 +321,14 @@ const fetchJson = async ({ repo, token, pathname, fetchImpl }) => {
if (!response.ok) {
const body = await response.text().catch(() => '');
const suffix = body ? ` ${body.slice(0, 500)}` : '';
throw new Error(`GitHub traffic API request failed for ${repo}: ${url.pathname}${url.search} returned ${response.status}.${suffix}`);
const lacksPushAccess = response.status === 403
&& /resource not accessible|must have push access/i.test(body);
const hint = lacksPushAccess
? ' Traffic endpoints require a token with push access to the repository; the Actions GITHUB_TOKEN is always rejected. Use a classic PAT with the repo scope or a fine-grained PAT with read access to Administration.'
: response.status === 401
? ' The token was rejected as invalid — it may be expired or revoked. Rotate the TRAFFIC_ARCHIVE_TOKEN secret.'
: '';
throw new Error(`GitHub traffic API request failed for ${repo}: ${url.pathname}${url.search} returned ${response.status}.${suffix}${hint}`);
}
return response.json();
@@ -0,0 +1,359 @@
#!/usr/bin/env node
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { installAgentForSkill, PLATFORM_KEYS } from "./skill_platforms.mjs";
const KNOWN_AGENT_TYPES = new Set(["codex", "hermes-agent", "openclaw", "universal"]);
function usage() {
return [
"Usage: node scripts/ci/generate_skill_release_trust_packet.mjs <skill-dir> <output-dir> [options]",
"",
"Options:",
" --repository <owner/repo> Source repository used in install instructions",
" --tag <tag> Release tag for this skill",
" --source-ref <ref> Source ref for npx skills examples",
].join("\n");
}
function parseArgs(argv) {
const positional = [];
const options = {
repository: "prompt-security/clawsec",
tag: "",
sourceRef: "main",
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--repository") {
options.repository = argv[++i];
} else if (token === "--tag") {
options.tag = argv[++i];
} else if (token === "--source-ref") {
options.sourceRef = argv[++i];
} else if (token === "--help" || token === "-h") {
console.log(usage());
process.exit(0);
} else if (token.startsWith("--")) {
throw new Error(`Unknown option: ${token}`);
} else {
positional.push(token);
}
}
if (positional.length !== 2) {
throw new Error(usage());
}
return {
skillDir: positional[0],
outputDir: positional[1],
...options,
};
}
function parseFrontmatter(markdown) {
if (!markdown.startsWith("---\n")) {
return {};
}
const end = markdown.indexOf("\n---", 4);
if (end === -1) {
return {};
}
const result = {};
const frontmatter = markdown.slice(4, end).split("\n");
for (const line of frontmatter) {
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
if (match) {
result[match[1]] = match[2].replace(/^["']|["']$/g, "").trim();
}
}
return result;
}
function asArray(value) {
if (Array.isArray(value)) {
return value.filter((item) => item !== null && item !== undefined).map(String);
}
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
return [];
}
function unique(values) {
return [...new Set(values.filter(Boolean))];
}
function detectPlatform(skill) {
for (const key of PLATFORM_KEYS) {
if (skill[key] && typeof skill[key] === "object") {
return key;
}
}
return skill.platform || "agent-skills";
}
function platformMetadata(skill, platform) {
const direct = skill[platform];
return direct && typeof direct === "object" ? direct : {};
}
function collectRequiredBinaries(metadata) {
const requires = metadata.requires && typeof metadata.requires === "object" ? metadata.requires : {};
const bins = asArray(requires.bins);
for (const [key, value] of Object.entries(requires)) {
if (key !== "bins" && typeof value === "string") {
bins.push(key);
}
}
return unique(bins);
}
function collectOptionalBinaries(metadata) {
return unique([
...asArray(metadata.runtime?.optional_bins),
...asArray(metadata.runtime?.optionalBins),
]);
}
function collectRequiredEnv(metadata) {
const requires = metadata.requires && typeof metadata.requires === "object" ? metadata.requires : {};
return unique([
...asArray(requires.env),
...asArray(metadata.runtime?.required_env),
...asArray(metadata.runtime?.requiredEnv),
]);
}
function collectOptionalEnv(metadata) {
return unique([
...asArray(metadata.runtime?.optional_env),
...asArray(metadata.runtime?.optionalEnv),
]);
}
function stringifyCapabilities(skill, metadata) {
const capabilities = metadata.capabilities ?? skill.capabilities ?? {};
if (Array.isArray(capabilities)) {
return capabilities;
}
if (capabilities && typeof capabilities === "object") {
return Object.entries(capabilities).map(([key, value]) => `${key}: ${String(value)}`);
}
if (typeof capabilities === "string") {
return [capabilities];
}
return [];
}
function requireField(skill, fieldName) {
if (!skill[fieldName] || typeof skill[fieldName] !== "string" || !skill[fieldName].trim()) {
throw new Error(`skill.json missing required trust-packet field: ${fieldName}`);
}
return skill[fieldName].trim();
}
function codeBlock(command) {
return ["```bash", command, "```"].join("\n");
}
function buildPermissions({ skill, metadata, platform, generatedAt }) {
const execution = metadata.execution && typeof metadata.execution === "object" ? metadata.execution : {};
const permissions = {
schema_version: "1",
generated_at: generatedAt,
skill: skill.name,
version: skill.version,
platform,
required_binaries: collectRequiredBinaries(metadata),
optional_binaries: collectOptionalBinaries(metadata),
required_env: collectRequiredEnv(metadata),
optional_env: collectOptionalEnv(metadata),
network_egress: execution.network_egress || "Not declared in skill metadata.",
persistence: execution.persistence || "Not declared in skill metadata.",
automatic_execution: typeof execution.always === "boolean" ? execution.always : "Not declared in skill metadata.",
capabilities: stringifyCapabilities(skill, metadata),
operator_review: asArray(metadata.operator_review),
};
return permissions;
}
function buildSkillCard({ skill, frontmatter, permissions, repository, tag, sourceRef }) {
const homepage = skill.homepage || frontmatter.homepage || `https://github.com/${repository}`;
const supportRef = `${repository}@${tag || sourceRef}`;
const licenseRef = `https://github.com/${repository}/blob/${tag || sourceRef}/LICENSE`;
const outputTypes = ["Markdown instructions", "release artifact files"];
if (permissions.capabilities.length > 0) {
outputTypes.push("local security findings or status reports");
}
return `# Skill Card
## Description
The \`${skill.name}\` skill provides this capability: ${skill.description}
This skill is intended for operator-reviewed security workflows, not unattended production mutation without the review steps declared in the skill instructions.
## Owner
prompt-security
## License/Terms of Use
${skill.license}
License reference: ${licenseRef}
Project homepage: ${homepage}
## Use Case
Use this skill for ${permissions.platform} workflows where an agent or operator needs the capability described in \`${skill.name}\`.
## Deployment Geography for Use
Global, subject to the operator's local compliance, network, and data-handling requirements.
## Known Risks and Mitigations
Risk: The skill may run commands, inspect local files, install hooks, or fetch remote security metadata depending on the workflow.
Mitigation: Review \`permissions.json\`, \`SKILL.md\`, and the signed \`checksums.json\` before enabling the skill. Keep high-impact actions approval-gated.
Risk: Security findings and remediation guidance can be incomplete or wrong.
Mitigation: Treat output as operator guidance. Review proposed removals, installs, configuration changes, and reports before acting.
## References
- Source release: ${supportRef}
- Skill instructions: SKILL.md
- Permission summary: permissions.json
- SkillSpector scan: skillspector-report.md
- Signed release manifest: checksums.json and checksums.sig
## Skill Output
Output type(s): ${outputTypes.join(", ")}
Output format: Markdown, JSON, shell commands, or local files as documented by the skill.
Output parameters: See \`SKILL.md\`, \`permissions.json\`, and release checksums for exact files and side effects.
Other properties: Release assets are covered by signed SHA-256 checksums.
## Skill Version
${skill.version}${tag ? ` (${tag})` : ""}
## Ethical Considerations
Use this skill only on systems, agents, repositories, and workspaces where you have authorization. Review generated security reports before sharing them because they may contain operational details.
`;
}
function buildInstallDoc({ skill, repository, tag, sourceRef }) {
const refSuffix = sourceRef && sourceRef !== "main" ? `#${sourceRef}` : "";
const source = `${repository}${refSuffix}`;
const releaseUrl = tag ? `https://github.com/${repository}/releases/tag/${tag}` : `https://github.com/${repository}`;
const agent = installAgentForSkill(skill, KNOWN_AGENT_TYPES);
return `# Install and Update ${skill.name}
## Install With Agent Skills CLI
Harness-aware global install:
${codeBlock(`npx skills add ${source} --skill ${skill.name} --agent ${agent} --global --yes`)}
Project-local install for compatible agents:
${codeBlock(`npx skills add ${source} --skill ${skill.name} --yes`)}
## Update
Update this skill when installed through the Skills CLI:
${codeBlock(`npx skills update ${skill.name}`)}
List installed skills:
${codeBlock("npx skills list")}
## Verify Release Artifact
When installing from a GitHub release instead of the Skills CLI, download the archive, \`checksums.json\`, \`checksums.sig\`, and \`signing-public.pem\` from:
${releaseUrl}
Verify \`checksums.json\` before trusting the archive or standalone files.
`;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const skillDir = path.resolve(args.skillDir);
const outputDir = path.resolve(args.outputDir);
const skillJsonPath = path.join(skillDir, "skill.json");
const skillMdPath = path.join(skillDir, "SKILL.md");
const [skillJsonRaw, skillMdRaw] = await Promise.all([
readFile(skillJsonPath, "utf8"),
readFile(skillMdPath, "utf8"),
]);
const skill = JSON.parse(skillJsonRaw);
const frontmatter = parseFrontmatter(skillMdRaw);
skill.name = requireField(skill, "name");
skill.version = requireField(skill, "version");
skill.description = requireField(skill, "description");
skill.license = requireField(skill, "license");
const platform = detectPlatform(skill);
const metadata = platformMetadata(skill, platform);
const generatedAt = new Date().toISOString();
const permissions = buildPermissions({ skill, metadata, platform, generatedAt });
await mkdir(outputDir, { recursive: true });
await Promise.all([
writeFile(
path.join(outputDir, "permissions.json"),
`${JSON.stringify(permissions, null, 2)}\n`,
),
writeFile(
path.join(outputDir, "skill-card.md"),
buildSkillCard({
skill,
frontmatter,
permissions,
repository: args.repository,
tag: args.tag,
sourceRef: args.sourceRef,
}),
),
writeFile(
path.join(outputDir, "install.md"),
buildInstallDoc({
skill,
repository: args.repository,
tag: args.tag,
sourceRef: args.sourceRef,
}),
),
]);
console.log(`Generated release trust packet for ${skill.name} in ${outputDir}`);
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail
CLI_PREFIX="${CLAWHUB_CLI_PREFIX:-.github/clawhub-cli}"
CODEARTIFACT_DOMAIN="${CODEARTIFACT_DOMAIN:-prompt-security}"
CODEARTIFACT_DOMAIN_OWNER="${CODEARTIFACT_DOMAIN_OWNER:-443370709039}"
CODEARTIFACT_REPOSITORY="${CODEARTIFACT_REPOSITORY:-npm-proxy}"
AWS_REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-eu-north-1}}"
if ! command -v aws >/dev/null 2>&1; then
echo "::error::aws CLI is required to authenticate npm against CodeArtifact"
exit 1
fi
if ! aws sts get-caller-identity >/dev/null 2>&1; then
echo "::error::AWS credentials are required before installing the CodeArtifact-pinned clawhub CLI"
exit 1
fi
aws codeartifact login \
--tool npm \
--domain "$CODEARTIFACT_DOMAIN" \
--domain-owner "$CODEARTIFACT_DOMAIN_OWNER" \
--repository "$CODEARTIFACT_REPOSITORY" \
--region "$AWS_REGION"
npm ci --prefix "$CLI_PREFIX"
if [ -n "${GITHUB_PATH:-}" ]; then
workspace="${GITHUB_WORKSPACE:-$(pwd)}"
echo "${workspace}/${CLI_PREFIX}/node_modules/.bin" >> "$GITHUB_PATH"
fi
@@ -0,0 +1,35 @@
import fs from "node:fs";
import path from "node:path";
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
const npmRoot = path.join(workspace, ".github", "clawhub-cli", "node_modules");
const publishScriptPath = path.join(
npmRoot,
"clawhub",
"dist",
"cli",
"commands",
"publish.js",
);
if (!fs.existsSync(publishScriptPath)) {
throw new Error(`clawhub publish script not found: ${publishScriptPath}`);
}
const original = fs.readFileSync(publishScriptPath, "utf8");
if (original.includes("acceptLicenseTerms: true")) {
console.log(`[patch-clawhub] Already patched: ${publishScriptPath}`);
process.exit(0);
}
const payloadPattern = /changelog,\r?\n(\s*)tags,/;
if (!payloadPattern.test(original)) {
throw new Error(`[patch-clawhub] Could not find expected publish payload pattern in ${publishScriptPath}`);
}
const patched = original.replace(
payloadPattern,
(_, indent) => `changelog,\n${indent}acceptLicenseTerms: true,\n${indent}tags,`,
);
fs.writeFileSync(publishScriptPath, patched, "utf8");
console.log(`[patch-clawhub] Patched: ${publishScriptPath}`);
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env node
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { collectDeclaredPlatforms, PLATFORM_KEYS } from "./skill_platforms.mjs";
const EXPLICIT_SLUGS = new Map([
["openclaw-traffic-guardian", "clawsec-openclaw-traffic-guardian"],
["openclaw-audit-watchdog", "clawsec-openclaw-audit-watchdog"],
["soul-guardian", "clawsec-openclaw-soul-guardian"],
["hermes-attestation-guardian", "clawsec-hermes-attestation-guardian"],
["hermes-traffic-guardian", "clawsec-hermes-traffic-guardian"],
["nanoclaw-traffic-guardian", "clawsec-nanoclaw-traffic-guardian"],
["picoclaw-security-guardian", "clawsec-picoclaw-security-guardian"],
["picoclaw-self-pen-testing", "clawsec-picoclaw-self-pen-testing"],
["picoclaw-traffic-guardian", "clawsec-picoclaw-traffic-guardian"],
["clawtributor", "clawsec-clawtributor"],
]);
function usage() {
return [
"Usage: node scripts/ci/resolve_clawhub_slug.mjs <skill-dir-or-name>",
"",
"Prints the ClawHub slug for a skill without changing the GitHub release tag or skill package name.",
].join("\n");
}
function loadSkill(input) {
const skillJsonPath = existsSync(path.join(input, "skill.json")) ? path.join(input, "skill.json") : null;
if (!skillJsonPath) {
return { name: input, platforms: [] };
}
const skill = JSON.parse(readFileSync(skillJsonPath, "utf8"));
if (!skill.name || typeof skill.name !== "string") {
throw new Error(`${skillJsonPath} missing string field: name`);
}
return { name: skill.name, platforms: collectDeclaredPlatforms(skill) };
}
export function resolveClawHubSlug({ name, platforms = [] }) {
if (!/^[a-z0-9-]+$/.test(name)) {
throw new Error(`Invalid skill name for ClawHub slug mapping: ${name}`);
}
if (name.startsWith("clawsec-")) {
return name;
}
if (EXPLICIT_SLUGS.has(name)) {
return EXPLICIT_SLUGS.get(name);
}
if (PLATFORM_KEYS.some((platform) => name.startsWith(`${platform}-`))) {
return `clawsec-${name}`;
}
const declaredPlatforms = collectDeclaredPlatforms({ platforms });
if (declaredPlatforms.length === 1 && PLATFORM_KEYS.includes(declaredPlatforms[0])) {
return `clawsec-${declaredPlatforms[0]}-${name}`;
}
return `clawsec-${name}`;
}
if (import.meta.url === `file://${process.argv[1]}`) {
const input = process.argv[2];
if (!input || input === "--help" || input === "-h") {
console.log(usage());
process.exit(input ? 0 : 1);
}
try {
console.log(resolveClawHubSlug(loadSkill(input)));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
+520
View File
@@ -0,0 +1,520 @@
#!/usr/bin/env node
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
cp,
mkdir,
mkdtemp,
readFile,
rm,
stat,
writeFile,
} from "node:fs/promises";
import { existsSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
const TRUST_ARTIFACTS = [
"skill-card.md",
"permissions.json",
"install.md",
"skillspector-report.md",
];
function usage() {
return [
"Usage: node scripts/ci/simulate_skill_tag_release.mjs <skill-dir> <output-dir> [options]",
"",
"Options:",
" --repository <owner/repo> Source repository used in release metadata",
" --source-ref <ref> Source ref used in npx skills examples",
" --skillspector-bin <path> SkillSpector executable to run",
].join("\n");
}
function parseArgs(argv) {
const positional = [];
const options = {
repository: "prompt-security/clawsec",
sourceRef: "main",
skillspectorBin: "skillspector",
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--repository") {
options.repository = argv[++i];
} else if (token === "--source-ref") {
options.sourceRef = argv[++i];
} else if (token === "--skillspector-bin") {
options.skillspectorBin = argv[++i];
} else if (token === "--help" || token === "-h") {
console.log(usage());
process.exit(0);
} else if (token.startsWith("--")) {
throw new Error(`Unknown option: ${token}`);
} else {
positional.push(token);
}
}
if (positional.length !== 2) {
throw new Error(usage());
}
return {
skillDir: positional[0],
outputDir: positional[1],
...options,
};
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
encoding: "utf8",
...options,
});
if (result.status !== 0) {
throw new Error(
[
`Command failed: ${command} ${args.join(" ")}`,
result.stdout ? `stdout:\n${result.stdout}` : "",
result.stderr ? `stderr:\n${result.stderr}` : "",
].filter(Boolean).join("\n"),
);
}
return result.stdout;
}
function runAllowFailure(command, args, options = {}) {
return spawnSync(command, args, {
encoding: "utf8",
...options,
});
}
function nextSimulatedReleaseVersion(version) {
const versionMatch = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9]+))?$/);
if (!versionMatch) {
throw new Error(`Cannot derive simulated release version from unsupported version: ${version}`);
}
const [, major, minor, patch, prerelease] = versionMatch;
if (!prerelease) {
return `${major}.${minor}.${Number(patch) + 1}`;
}
const prereleaseMatch = prerelease.match(/^(.*?)(\d+)$/);
if (prereleaseMatch) {
const [, label, number] = prereleaseMatch;
return `${major}.${minor}.${patch}-${label}${Number(number) + 1}`;
}
return `${major}.${minor}.${patch}-${prerelease}1`;
}
function normalizeReleasePath(rawPath) {
let releasePath = rawPath.replaceAll("\\", "/");
while (releasePath.startsWith("./")) {
releasePath = releasePath.slice(2);
}
while (releasePath.includes("//")) {
releasePath = releasePath.replaceAll("//", "/");
}
if (
releasePath === "" ||
releasePath.startsWith("/") ||
/^[A-Za-z]:/.test(releasePath) ||
releasePath === ".." ||
releasePath.startsWith("../") ||
releasePath.endsWith("/..") ||
releasePath.includes("/../")
) {
throw new Error(`Unsafe release path: ${rawPath}`);
}
return releasePath;
}
function isTestReleasePath(releasePath) {
const lower = releasePath.toLowerCase();
return lower === "test" ||
lower === "tests" ||
lower.startsWith("test/") ||
lower.startsWith("tests/") ||
lower.includes("/test/") ||
lower.includes("/tests/");
}
async function sha256File(filePath) {
const buffer = await readFile(filePath);
return createHash("sha256").update(buffer).digest("hex");
}
async function fileSize(filePath) {
return (await stat(filePath)).size;
}
async function checksumEntry(filePath, releasePath) {
return {
sha256: await sha256File(filePath),
size: await fileSize(filePath),
path: releasePath,
};
}
function replaceSkillMarkdownVersion(markdown, version) {
if (!markdown.startsWith("---\n")) {
throw new Error("SKILL.md is missing YAML frontmatter");
}
const end = markdown.indexOf("\n---", 4);
if (end === -1) {
throw new Error("SKILL.md frontmatter is not closed");
}
const frontmatter = markdown.slice(0, end);
if (!/^version:\s*.+$/m.test(frontmatter)) {
throw new Error("SKILL.md frontmatter is missing a version field");
}
return markdown.replace(/^version:\s*.+$/m, `version: ${version}`);
}
async function addSimulatedChangelogEntry(skillDir, version) {
const changelogPath = path.join(skillDir, "CHANGELOG.md");
if (!existsSync(changelogPath)) {
return;
}
const today = new Date().toISOString().slice(0, 10);
const original = await readFile(changelogPath, "utf8");
if (original.includes(`## [${version}] -`)) {
return;
}
const entry = [
`## [${version}] - ${today}`,
"",
"- Simulated prerelease build for release-pipeline validation.",
"",
"---",
"",
].join("\n");
await writeFile(changelogPath, `${entry}${original}`);
}
async function writeJson(filePath, value) {
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
}
async function signFileBase64({ keyPath, inputPath, outputPath, tempRoot }) {
const sigBin = path.join(tempRoot, `${path.basename(outputPath)}.bin`);
run("openssl", ["pkeyutl", "-sign", "-rawin", "-inkey", keyPath, "-in", inputPath, "-out", sigBin]);
run("openssl", ["base64", "-A", "-in", sigBin, "-out", outputPath]);
await rm(sigBin, { force: true });
}
async function verifyFileBase64Signature({ publicKeyPath, inputPath, signaturePath, tempRoot }) {
const sigBin = path.join(tempRoot, `${path.basename(signaturePath)}.verify.bin`);
run("openssl", ["base64", "-d", "-A", "-in", signaturePath, "-out", sigBin]);
run("openssl", [
"pkeyutl",
"-verify",
"-rawin",
"-pubin",
"-inkey",
publicKeyPath,
"-sigfile",
sigBin,
"-in",
inputPath,
]);
await rm(sigBin, { force: true });
}
async function createSigningKeyPair(tempRoot) {
const keyDir = await mkdtemp(path.join(tempRoot, "signing-"));
const privateKeyPath = path.join(keyDir, "private.pem");
const publicKeyPath = path.join(keyDir, "public.pem");
run("openssl", ["genpkey", "-algorithm", "ED25519", "-out", privateKeyPath]);
run("openssl", ["pkey", "-in", privateKeyPath, "-pubout", "-out", publicKeyPath]);
return { privateKeyPath, publicKeyPath };
}
async function signAdvisoryArtifacts(skillDir, tempRoot) {
const advisoryDir = path.join(skillDir, "advisories");
const feedPath = path.join(advisoryDir, "feed.json");
if (!existsSync(feedPath)) {
return;
}
const { privateKeyPath, publicKeyPath } = await createSigningKeyPair(tempRoot);
const feedSignaturePath = path.join(advisoryDir, "feed.json.sig");
const checksumsPath = path.join(advisoryDir, "checksums.json");
const checksumsSignaturePath = path.join(advisoryDir, "checksums.json.sig");
const publicKeyOutputPath = path.join(advisoryDir, "feed-signing-public.pem");
await signFileBase64({
keyPath: privateKeyPath,
inputPath: feedPath,
outputPath: feedSignaturePath,
tempRoot,
});
await verifyFileBase64Signature({
publicKeyPath,
inputPath: feedPath,
signaturePath: feedSignaturePath,
tempRoot,
});
await writeJson(checksumsPath, {
schema_version: "1",
algorithm: "sha256",
version: "simulation",
generated_at: new Date().toISOString(),
files: {
"advisories/feed.json": await checksumEntry(feedPath, "advisories/feed.json"),
"advisories/feed.json.sig": await checksumEntry(feedSignaturePath, "advisories/feed.json.sig"),
},
});
await signFileBase64({
keyPath: privateKeyPath,
inputPath: checksumsPath,
outputPath: checksumsSignaturePath,
tempRoot,
});
await verifyFileBase64Signature({
publicKeyPath,
inputPath: checksumsPath,
signaturePath: checksumsSignaturePath,
tempRoot,
});
await cp(publicKeyPath, publicKeyOutputPath);
}
async function addReleaseAssetChecksum({ releaseAssetsDir, manifest, asset }) {
const filePath = path.join(releaseAssetsDir, asset);
if (!existsSync(filePath) || (await fileSize(filePath)) === 0) {
throw new Error(`Required release trust artifact is missing or empty: ${filePath}`);
}
manifest.files[asset] = await checksumEntry(filePath, asset);
}
async function stageSbomFiles({ skillDir, innerDir, sbomFiles }) {
for (const entry of sbomFiles) {
const releasePath = normalizeReleasePath(entry.path);
if (isTestReleasePath(releasePath)) {
continue;
}
const fullPath = path.join(skillDir, releasePath);
if (!existsSync(fullPath)) {
throw new Error(`SBOM references missing file: ${releasePath}`);
}
const destination = path.join(innerDir, releasePath);
await mkdir(path.dirname(destination), { recursive: true });
await cp(fullPath, destination);
}
}
async function buildFilesManifest({ skillDir, skillJsonPath, sbomFiles }) {
const files = {};
for (const entry of sbomFiles) {
const releasePath = normalizeReleasePath(entry.path);
if (isTestReleasePath(releasePath)) {
continue;
}
const fullPath = path.join(skillDir, releasePath);
if (existsSync(fullPath)) {
files[releasePath] = await checksumEntry(fullPath, releasePath);
}
}
files["skill.json"] = {
sha256: await sha256File(skillJsonPath),
size: await fileSize(skillJsonPath),
};
return files;
}
async function runSkillSpector({ skillspectorBin, skillDir, reportPath }) {
const result = runAllowFailure(skillspectorBin, [
"scan",
skillDir,
"--no-llm",
"--format",
"markdown",
"--output",
reportPath,
]);
if (!existsSync(reportPath) || (await fileSize(reportPath)) === 0) {
throw new Error(
[
"SkillSpector did not produce a report.",
result.stdout ? `stdout:\n${result.stdout}` : "",
result.stderr ? `stderr:\n${result.stderr}` : "",
].filter(Boolean).join("\n"),
);
}
if (result.status !== 0) {
console.warn(`SkillSpector returned exit code ${result.status}; report is included for review.`);
}
}
async function main() {
const args = parseArgs(process.argv.slice(2));
const sourceSkillDir = path.resolve(args.skillDir);
const outputDir = path.resolve(args.outputDir);
const releaseAssetsDir = path.join(outputDir, "release-assets");
const tempRoot = await mkdtemp(path.join(tmpdir(), "clawsec-release-sim-"));
try {
const skillName = path.basename(sourceSkillDir);
const tempSkillDir = path.join(tempRoot, skillName);
await cp(sourceSkillDir, tempSkillDir, { recursive: true });
const skillJsonPath = path.join(tempSkillDir, "skill.json");
const skillMdPath = path.join(tempSkillDir, "SKILL.md");
const skill = JSON.parse(await readFile(skillJsonPath, "utf8"));
const originalVersion = skill.version;
const simulatedVersion = nextSimulatedReleaseVersion(originalVersion);
const tag = `${skillName}-v${simulatedVersion}`;
const zipName = `${tag}.zip`;
skill.version = simulatedVersion;
await writeJson(skillJsonPath, skill);
await writeFile(
skillMdPath,
replaceSkillMarkdownVersion(await readFile(skillMdPath, "utf8"), simulatedVersion),
);
await addSimulatedChangelogEntry(tempSkillDir, simulatedVersion);
await signAdvisoryArtifacts(tempSkillDir, tempRoot);
if (!skill.sbom || !Array.isArray(skill.sbom.files)) {
throw new Error(`skill.json missing required release field: sbom.files`);
}
await mkdir(releaseAssetsDir, { recursive: true });
const stagingDir = await mkdtemp(path.join(tempRoot, "staging-"));
const innerDir = path.join(stagingDir, skillName);
await mkdir(innerDir, { recursive: true });
await stageSbomFiles({
skillDir: tempSkillDir,
innerDir,
sbomFiles: skill.sbom.files,
});
await cp(skillJsonPath, path.join(innerDir, "skill.json"));
run("python3", ["scripts/ci/verify_skill_release_import_closure.py", innerDir], {
cwd: process.cwd(),
});
run("zip", ["-qr", path.join(releaseAssetsDir, zipName), "."], {
cwd: stagingDir,
});
const zipContents = run("unzip", ["-Z1", path.join(releaseAssetsDir, zipName)]);
if (zipContents.split("\n").some((entry) => /(^|\/)(test|tests)\//i.test(entry))) {
throw new Error(`Simulated release archive contains test-only files: ${zipName}`);
}
const manifest = {
skill: skillName,
version: simulatedVersion,
generated_at: new Date().toISOString(),
repository: args.repository,
tag,
archive: {
filename: zipName,
sha256: await sha256File(path.join(releaseAssetsDir, zipName)),
size: await fileSize(path.join(releaseAssetsDir, zipName)),
url: `https://github.com/${args.repository}/releases/download/${tag}/${zipName}`,
},
files: await buildFilesManifest({
skillDir: tempSkillDir,
skillJsonPath,
sbomFiles: skill.sbom.files,
}),
};
await writeJson(path.join(releaseAssetsDir, "checksums.json"), manifest);
run(process.execPath, [
"scripts/ci/generate_skill_release_trust_packet.mjs",
tempSkillDir,
releaseAssetsDir,
"--repository",
args.repository,
"--tag",
tag,
"--source-ref",
args.sourceRef,
]);
await runSkillSpector({
skillspectorBin: args.skillspectorBin,
skillDir: innerDir,
reportPath: path.join(releaseAssetsDir, "skillspector-report.md"),
});
for (const artifact of TRUST_ARTIFACTS) {
await addReleaseAssetChecksum({ releaseAssetsDir, manifest, asset: artifact });
}
await writeJson(path.join(releaseAssetsDir, "checksums.json"), manifest);
await cp(skillJsonPath, path.join(releaseAssetsDir, "skill.json"));
await cp(skillMdPath, path.join(releaseAssetsDir, "SKILL.md"));
if (existsSync(path.join(tempSkillDir, "README.md"))) {
await cp(path.join(tempSkillDir, "README.md"), path.join(releaseAssetsDir, "README.md"));
}
const { privateKeyPath, publicKeyPath } = await createSigningKeyPair(tempRoot);
await signFileBase64({
keyPath: privateKeyPath,
inputPath: path.join(releaseAssetsDir, "checksums.json"),
outputPath: path.join(releaseAssetsDir, "checksums.sig"),
tempRoot,
});
await verifyFileBase64Signature({
publicKeyPath,
inputPath: path.join(releaseAssetsDir, "checksums.json"),
signaturePath: path.join(releaseAssetsDir, "checksums.sig"),
tempRoot,
});
await cp(publicKeyPath, path.join(releaseAssetsDir, "signing-public.pem"));
await writeJson(path.join(outputDir, "simulation-summary.json"), {
skill: skillName,
original_version: originalVersion,
simulated_version: simulatedVersion,
tag,
release_assets: path.relative(outputDir, releaseAssetsDir),
archive: `release-assets/${zipName}`,
});
console.log(`Simulated tag release build for ${skillName}: ${tag}`);
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
+52
View File
@@ -0,0 +1,52 @@
export const PLATFORM_KEYS = Object.freeze(["openclaw", "nanoclaw", "hermes", "picoclaw"]);
const PLATFORM_AGENT_ALIASES = new Map([["hermes", "hermes-agent"]]);
function asStringArray(value) {
if (Array.isArray(value)) {
return value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim());
}
if (typeof value === "string" && value.trim()) {
return [value.trim()];
}
return [];
}
export function collectDeclaredPlatforms(skill) {
const platforms = new Set([
...asStringArray(skill.platform),
...asStringArray(skill.platforms),
]);
for (const key of PLATFORM_KEYS) {
if (skill[key] && typeof skill[key] === "object") {
platforms.add(key);
}
}
return [...platforms];
}
export function installAgentForSkill(skill, agentTypes, fallback = "openclaw") {
const platforms = collectDeclaredPlatforms(skill);
if (platforms.length === 0) {
return fallback;
}
const matchedAgents = new Set();
let allPlatformsMatched = true;
for (const platform of platforms) {
const candidate = PLATFORM_AGENT_ALIASES.get(platform) || platform;
if (agentTypes.has(candidate)) {
matchedAgents.add(candidate);
} else {
allPlatformsMatched = false;
}
}
if (allPlatformsMatched && matchedAgents.size === 1) {
return [...matchedAgents][0];
}
return fallback;
}
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env node
import { readFile, readdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { spawnSync } from "node:child_process";
import https from "node:https";
import path from "node:path";
import { installAgentForSkill } from "./skill_platforms.mjs";
const DEFAULT_REPOSITORY = "prompt-security/clawsec";
const DEFAULT_AGENT_TYPES_URL = "https://raw.githubusercontent.com/vercel-labs/skills/main/src/types.ts";
const DOC_FILENAMES = ["README.md", "SKILL.md"];
function usage() {
return [
"Usage: node scripts/ci/validate_skill_install_docs.mjs [options]",
"",
"Options:",
" --root <dir> Repository root. Defaults to current working directory.",
" --repository <owner/repo> Expected npx skills source. Defaults to prompt-security/clawsec.",
" --base <sha> Base ref for changed-skill detection.",
" --head <sha> Head ref for changed-skill detection.",
" --skills <dir[,dir...]> Skill directories to validate.",
" --all Validate every skill directory with skill.json.",
" --agent-types-file <path> Read Vercel AgentType source from a local file.",
" --agent-types-url <url> Read Vercel AgentType source from a URL.",
].join("\n");
}
function parseArgs(argv) {
const options = {
root: process.cwd(),
repository: DEFAULT_REPOSITORY,
base: process.env.BASE_SHA || "",
head: process.env.HEAD_SHA || "",
skillDirs: [],
all: false,
agentTypesFile: "",
agentTypesUrl: DEFAULT_AGENT_TYPES_URL,
};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--root") {
options.root = argv[++i];
} else if (token === "--repository") {
options.repository = argv[++i];
} else if (token === "--base") {
options.base = argv[++i];
} else if (token === "--head") {
options.head = argv[++i];
} else if (token === "--skills") {
options.skillDirs.push(...argv[++i].split(",").map((item) => item.trim()).filter(Boolean));
} else if (token === "--all") {
options.all = true;
} else if (token === "--agent-types-file") {
options.agentTypesFile = argv[++i];
} else if (token === "--agent-types-url") {
options.agentTypesUrl = argv[++i];
} else if (token === "--help" || token === "-h") {
console.log(usage());
process.exit(0);
} else {
throw new Error(`Unknown option: ${token}\n${usage()}`);
}
}
return {
...options,
root: path.resolve(options.root),
};
}
function fetchText(url) {
return new Promise((resolve, reject) => {
https
.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to fetch ${url}: HTTP ${response.statusCode}`));
response.resume();
return;
}
response.setEncoding("utf8");
let body = "";
response.on("data", (chunk) => {
body += chunk;
});
response.on("end", () => resolve(body));
})
.on("error", reject);
});
}
async function readAgentTypeSource(options) {
if (options.agentTypesFile) {
return readFile(path.resolve(options.agentTypesFile), "utf8");
}
return fetchText(options.agentTypesUrl);
}
function parseAgentTypes(source) {
const match = source.match(/export\s+type\s+AgentType\s*=\s*([\s\S]*?);/);
if (!match) {
throw new Error("Could not find export type AgentType in Vercel skills type source.");
}
const agents = new Set();
const agentTypeBody = match[1];
for (const agentMatch of agentTypeBody.matchAll(/['"]([^'"]+)['"]/g)) {
agents.add(agentMatch[1]);
}
if (agents.size === 0) {
throw new Error("Vercel AgentType list was empty.");
}
return agents;
}
async function listAllSkillDirs(root) {
const skillsRoot = path.join(root, "skills");
const entries = await readdir(skillsRoot, { withFileTypes: true });
return entries
.filter((entry) => entry.isDirectory())
.map((entry) => `skills/${entry.name}`)
.filter((skillDir) => existsSync(path.join(root, skillDir, "skill.json")))
.sort();
}
function changedSkillDirs({ root, base, head }) {
if (!base || !head) {
throw new Error("Provide --skills, --all, or both --base and --head for changed-skill detection.");
}
const result = spawnSync(
"git",
[
"-C",
root,
"diff",
"--name-only",
`${base}...${head}`,
"--",
"skills/*/**",
":(exclude)skills/*/test/**",
":(exclude)skills/*/tests/**",
],
{ encoding: "utf8" },
);
if (result.status !== 0) {
throw new Error(`git diff failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
}
return [
...new Set(
result.stdout
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((filePath) => filePath.split("/").slice(0, 2).join("/"))
.filter((skillDir) => /^skills\/[^/]+$/.test(skillDir)),
),
].sort();
}
async function readJson(filePath) {
return JSON.parse(await readFile(filePath, "utf8"));
}
function hasRequiredCommand(markdown, { repository, skillName, agent }) {
return markdown
.split("\n")
.map((line) => line.replace(/\s+/g, " ").trim())
.filter((line) => line.includes("npx skills add"))
.some((line) => {
return (
line.includes(`npx skills add ${repository}`) &&
line.includes(`--skill ${skillName}`) &&
(line.includes(`-a ${agent}`) || line.includes(`--agent ${agent}`)) &&
(line.includes(" -y") || line.includes(" --yes"))
);
});
}
async function validateSkill({ root, skillDir, repository, agentTypes }) {
const skillJsonPath = path.join(root, skillDir, "skill.json");
const skill = await readJson(skillJsonPath);
const skillName = skill.name || path.basename(skillDir);
const agent = installAgentForSkill(skill, agentTypes);
const command = `npx skills add ${repository} --skill ${skillName} -a ${agent} -y`;
const failures = [];
for (const filename of DOC_FILENAMES) {
const docPath = path.join(root, skillDir, filename);
if (!existsSync(docPath)) {
failures.push(`Missing required install documentation file: ${path.join(skillDir, filename)}`);
continue;
}
const markdown = await readFile(docPath, "utf8");
if (!hasRequiredCommand(markdown, { repository, skillName, agent })) {
failures.push(`Missing required npx skills install command in ${path.join(skillDir, filename)}: ${command}`);
}
}
return {
skillDir,
skillName,
agent,
failures,
};
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const agentTypes = parseAgentTypes(await readAgentTypeSource(options));
let skillDirs = options.skillDirs;
if (options.all) {
skillDirs = await listAllSkillDirs(options.root);
} else if (skillDirs.length === 0) {
skillDirs = changedSkillDirs(options);
}
if (skillDirs.length === 0) {
console.log("No skill install docs to validate.");
return;
}
const results = [];
for (const skillDir of skillDirs) {
const skillJsonPath = path.join(options.root, skillDir, "skill.json");
if (!existsSync(skillJsonPath)) {
console.log(`Skipping removed skill directory: ${skillDir}`);
continue;
}
results.push(
await validateSkill({
root: options.root,
skillDir,
repository: options.repository,
agentTypes,
}),
);
}
const failures = results.flatMap((result) => result.failures);
if (failures.length > 0) {
for (const failure of failures) {
console.error(`::error::${failure}`);
}
throw new Error(`Found ${failures.length} npx skills install documentation issue(s).`);
}
for (const result of results) {
console.log(`npx skills install docs OK for ${result.skillName}: -a ${result.agent}`);
}
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});
+36 -1
View File
@@ -76,6 +76,40 @@ test('fetchGitHubTraffic requests the daily GitHub traffic endpoints with auth',
assert.deepEqual(snapshot.clones.clones, responses[`/repos/${TEST_REPOSITORY}/traffic/clones?per=day`].clones);
});
test('fetchGitHubTraffic explains traffic token requirements on 403', async () => {
const fetchImpl = async () => new globalThis.Response(
JSON.stringify({ message: 'Resource not accessible by integration' }),
{ status: 403 },
);
await assert.rejects(
fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'installation-token',
capturedAt,
fetchImpl,
}),
/returned 403\..*push access/,
);
});
test('fetchGitHubTraffic flags invalid tokens on 401', async () => {
const fetchImpl = async () => new globalThis.Response(
JSON.stringify({ message: 'Bad credentials' }),
{ status: 401 },
);
await assert.rejects(
fetchGitHubTraffic({
repo: TEST_REPOSITORY,
token: 'expired-token',
capturedAt,
fetchImpl,
}),
/returned 401\..*expired or revoked/,
);
});
test('mergeTrafficArchive upserts daily views and clones without double-counting overlapping windows', () => {
const archive = mergeTrafficArchive(
{
@@ -232,7 +266,8 @@ test('traffic archive workflow uses a daily schedule and a dedicated archive bra
assert.match(workflow, /cron:\s+'17 3 \* \* \*'/);
assert.match(workflow, /TRAFFIC_ARCHIVE_BRANCH:\s+traffic-archive/);
assert.match(workflow, /TRAFFIC_ARCHIVE_TOKEN/);
assert.match(workflow, /GH_TRAFFIC_TOKEN:\s*\$\{\{\s*secrets\.TRAFFIC_ARCHIVE_TOKEN\b/);
assert.doesNotMatch(workflow, /GH_TRAFFIC_TOKEN:[^\n]*github\.token/);
assert.match(workflow, /node scripts\/archive-github-traffic\.mjs/);
assert.match(workflow, /git add traffic\/archive\.json traffic\/summary\.json/);
assert.match(workflow, /git rm --ignore-unmatch traffic\/README\.md/);
@@ -47,6 +47,16 @@ assert.match(
/git add "\$FEED_PATH" "\$FEED_SIG_PATH" "\$GHSA_FEED_PATH" "\$GHSA_FEED_SIG_PATH" "\$SKILL_FEED_PATH" "\$SKILL_FEED_SIG_PATH"/,
'NVD workflow PR must include both NVD and GHSA feed artifacts',
);
assert.doesNotMatch(
workflow,
/gh run list[\s\S]*--jq --arg/,
'CodeQL run lookup must not pass jq CLI flags through gh --jq',
);
assert.match(
workflow,
/gh run list[\s\S]*--json databaseId,createdAt,headSha \\\s*\n\s+\| jq -r --arg since "\$DISPATCHED_AT" --arg sha "\$EXPECTED_HEAD_SHA"/,
'CodeQL run lookup must filter the gh JSON output with jq variables',
);
assert.match(
ciWorkflow,
/name: NVD \+ GHSA Pipeline Dry Run[\s\S]*node scripts\/test-nvd-ghsa-pipeline-dry-run\.mjs/,
+45
View File
@@ -0,0 +1,45 @@
import assert from "node:assert/strict";
import { resolveClawHubSlug } from "./ci/resolve_clawhub_slug.mjs";
import { collectDeclaredPlatforms, installAgentForSkill } from "./ci/skill_platforms.mjs";
const cases = [
["openclaw-traffic-guardian", ["openclaw"], "clawsec-openclaw-traffic-guardian"],
["openclaw-audit-watchdog", ["openclaw"], "clawsec-openclaw-audit-watchdog"],
["soul-guardian", ["openclaw"], "clawsec-openclaw-soul-guardian"],
["hermes-attestation-guardian", ["hermes"], "clawsec-hermes-attestation-guardian"],
["hermes-traffic-guardian", ["hermes"], "clawsec-hermes-traffic-guardian"],
["nanoclaw-traffic-guardian", ["nanoclaw"], "clawsec-nanoclaw-traffic-guardian"],
["picoclaw-security-guardian", ["picoclaw"], "clawsec-picoclaw-security-guardian"],
["picoclaw-self-pen-testing", ["picoclaw"], "clawsec-picoclaw-self-pen-testing"],
["picoclaw-traffic-guardian", ["picoclaw"], "clawsec-picoclaw-traffic-guardian"],
["clawtributor", ["openclaw", "nanoclaw", "hermes", "picoclaw"], "clawsec-clawtributor"],
["clawsec-feed", ["openclaw"], "clawsec-feed"],
["clawsec-suite", ["openclaw"], "clawsec-suite"],
];
for (const [name, platforms, expected] of cases) {
assert.equal(resolveClawHubSlug({ name, platforms }), expected, `${name} should map to ${expected}`);
assert.equal(resolveClawHubSlug({ name }), expected, `${name} should map to ${expected} without metadata`);
}
assert.throws(
() => resolveClawHubSlug({ name: "../openclaw-traffic-guardian", platforms: ["openclaw"] }),
/Invalid skill name/,
"unsafe skill names must be rejected",
);
assert.deepEqual(
collectDeclaredPlatforms({
platform: "openclaw",
platforms: ["hermes", "openclaw", ""],
picoclaw: { requires: {} },
}),
["openclaw", "hermes", "picoclaw"],
"declared platform parsing should combine legacy fields, arrays, and platform metadata keys",
);
assert.equal(
installAgentForSkill({ platform: "hermes" }, new Set(["codex", "hermes-agent", "openclaw"])),
"hermes-agent",
"install agent selection should reuse platform aliases",
);
+137
View File
@@ -0,0 +1,137 @@
import assert from "node:assert/strict";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
const validator = "scripts/ci/validate_skill_install_docs.mjs";
const workflow = await readFile(".github/workflows/skill-release.yml", "utf8");
const tempRoot = await mkdtemp(path.join(tmpdir(), "clawsec-install-docs-"));
const agentTypesPath = path.join(tempRoot, "vercel-types.ts");
function runValidator(args) {
return spawnSync(
process.execPath,
[validator, "--root", tempRoot, "--agent-types-file", agentTypesPath, ...args],
{
encoding: "utf8",
},
);
}
async function writeSkill({ name, metadata, readme, skillMd }) {
const skillDir = path.join(tempRoot, "skills", name);
await mkdir(skillDir, { recursive: true });
await writeFile(
path.join(skillDir, "skill.json"),
JSON.stringify(
{
name,
version: "1.0.0",
description: `${name} test skill`,
license: "AGPL-3.0-or-later",
...metadata,
},
null,
2,
),
);
await writeFile(path.join(skillDir, "README.md"), readme);
await writeFile(path.join(skillDir, "SKILL.md"), skillMd);
}
try {
await writeFile(
agentTypesPath,
"export type AgentType = | 'codex' | 'hermes-agent' | 'openclaw' | 'universal';\n",
);
await writeSkill({
name: "hermes-example",
metadata: { hermes: { category: "security" } },
readme: "# Hermes Example\n\n## Installation\n\nMissing the Skills CLI command.\n",
skillMd: "---\nname: hermes-example\nversion: 1.0.0\n---\n\n## Installation\n\nMissing the Skills CLI command.\n",
});
const missingHermes = runValidator(["--skills", "skills/hermes-example"]);
assert.equal(missingHermes.status, 1, "missing Hermes install docs must fail validation");
assert.match(
missingHermes.stderr,
/npx skills add prompt-security\/clawsec --skill hermes-example -a hermes-agent -y/,
"Hermes skills must require the hermes-agent installer target",
);
await writeSkill({
name: "hermes-example",
metadata: { hermes: { category: "security" } },
readme:
"# Hermes Example\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill hermes-example -a hermes-agent -y\n```\n",
skillMd:
"---\nname: hermes-example\nversion: 1.0.0\n---\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill hermes-example -a hermes-agent -y\n```\n",
});
const validHermes = runValidator(["--skills", "skills/hermes-example"]);
assert.equal(
validHermes.status,
0,
`valid Hermes install docs should pass\nstdout:\n${validHermes.stdout}\nstderr:\n${validHermes.stderr}`,
);
await writeSkill({
name: "codex-example",
metadata: { platform: "codex" },
readme:
"# Codex Example\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill codex-example -a openclaw -y\n```\n",
skillMd:
"---\nname: codex-example\nversion: 1.0.0\n---\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill codex-example -a openclaw -y\n```\n",
});
const wrongExactTarget = runValidator(["--skills", "skills/codex-example"]);
assert.equal(wrongExactTarget.status, 1, "exact AgentType matches must use their matched target");
assert.match(
wrongExactTarget.stderr,
/npx skills add prompt-security\/clawsec --skill codex-example -a codex -y/,
"Exact AgentType matches must not fall back to openclaw",
);
await writeSkill({
name: "nanoclaw-example",
metadata: { platform: "nanoclaw", nanoclaw: { category: "security" } },
readme:
"# NanoClaw Example\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill nanoclaw-example -a hermes-agent -y\n```\n",
skillMd:
"---\nname: nanoclaw-example\nversion: 1.0.0\n---\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill nanoclaw-example -a hermes-agent -y\n```\n",
});
const wrongNanoTarget = runValidator(["--skills", "skills/nanoclaw-example"]);
assert.equal(wrongNanoTarget.status, 1, "NanoClaw docs must fail when they use the Hermes target");
assert.match(
wrongNanoTarget.stderr,
/npx skills add prompt-security\/clawsec --skill nanoclaw-example -a openclaw -y/,
"NanoClaw skills must install through the openclaw target",
);
await writeSkill({
name: "nanoclaw-example",
metadata: { platform: "nanoclaw", nanoclaw: { category: "security" } },
readme:
"# NanoClaw Example\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill nanoclaw-example -a openclaw -y\n```\n",
skillMd:
"---\nname: nanoclaw-example\nversion: 1.0.0\n---\n\n## Vercel Skills Installation\n\n```bash\nnpx skills add prompt-security/clawsec --skill nanoclaw-example -a openclaw -y\n```\n",
});
const validNano = runValidator(["--skills", "skills/nanoclaw-example"]);
assert.equal(
validNano.status,
0,
`valid NanoClaw install docs should pass\nstdout:\n${validNano.stdout}\nstderr:\n${validNano.stderr}`,
);
assert.match(
workflow,
/Validate npx skills install docs/,
"Skill release workflow must run the install-doc validator",
);
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
+368 -3
View File
@@ -2,7 +2,13 @@ import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
const workflowPath = new URL('../.github/workflows/skill-release.yml', import.meta.url);
const ciWorkflowPath = new URL('../.github/workflows/ci.yml', import.meta.url);
const installClawhubCliPath = new URL('./ci/install_clawhub_cli.sh', import.meta.url);
const patchClawhubPayloadPath = new URL('./ci/patch_clawhub_publish_payload.mjs', import.meta.url);
const workflow = await readFile(workflowPath, 'utf8');
const ciWorkflow = await readFile(ciWorkflowPath, 'utf8');
const installClawhubCli = await readFile(installClawhubCliPath, 'utf8');
const patchClawhubPayload = await readFile(patchClawhubPayloadPath, 'utf8');
assert.match(
workflow,
@@ -10,20 +16,379 @@ assert.match(
'Skill release workflow must run when any skill package file changes',
);
assert.match(
workflow,
/pull_request:[\s\S]*paths:[\s\S]*- '\.github\/workflows\/skill-release\.yml'[\s\S]*- 'scripts\/ci\/\*\*'/,
'Skill release workflow must also run when the release pipeline itself changes',
);
assert.ok(
ciWorkflow.includes(` - name: Skill Release Tooling Tests
run: |
set -euo pipefail
for test_file in scripts/test-skill-*.mjs; do
node "$test_file"
done`),
'CI must run every scripts/test-skill-*.mjs file so new skill release tests are not orphaned',
);
assert.match(
workflow,
/git diff --name-only "\$\{BASE_SHA\}\.\.\.\$\{HEAD_SHA\}" --[\s\S]*'skills\/\*\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/test\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/tests\/\*\*'/,
'Skill release validation must ignore test-only skill changes while inspecting release-relevant skill files',
);
assert.ok(
workflow.includes('name = tolower($NF)')
&& workflow.includes('name ~ /^(test|spec)[_-]/')
&& workflow.includes('name ~ /\\.(test|spec)\\./'),
'Skill release validation must filter test-named skill files such as scripts/test_*.py before selecting dry-run skill directories',
);
assert.doesNotMatch(
workflow,
/No version bump detected for \$\{skill_dir\}; skipping\./,
'Changed skill directories without a version bump must fail validation instead of being skipped',
'Changed skill directories without a version bump must not be skipped without release-tag validation',
);
assert.match(
workflow,
/::error file=\$\{skill_dir\}::Changed skill package has no version bump\./,
'Skill release validation must emit an explicit missing-version-bump error',
/skill_release_name="\$\(basename "\$\{skill_dir\}"\)"/,
'Skill release validation must derive the release tag prefix from the skill package directory',
);
assert.match(
workflow,
/release_tag="\$\{skill_release_name\}-v\$\{head_json_version\}"/,
'Skill release validation must use the skill package directory name for release tag checks',
);
assert.doesNotMatch(
workflow,
/release_tag="\$\{head_skill_name\}-v\$\{head_json_version\}"/,
'Skill release validation must not use skill.json name for release tag checks because release tags resolve to skill directories',
);
assert.match(
workflow,
/git show-ref --verify --quiet "refs\/tags\/\$\{release_tag\}"/,
'Skill release validation must check whether the current skill version has already been tagged',
);
assert.match(
workflow,
/No version bump detected for \$\{skill_dir\}, but release tag \$\{release_tag\} does not exist; treating \$\{head_json_version\} as unreleased\./,
'Skill release validation must allow edits to an unchanged version when that release tag does not exist yet',
);
assert.match(
workflow,
/::error file=\$\{skill_dir\}::Changed skill package has no version bump and release tag \$\{release_tag\} already exists\./,
'Skill release validation must still fail unchanged versions after their release tag exists',
);
assert.match(
workflow,
/Install SkillSpector/,
'Skill release workflow must install SkillSpector before publishing release evidence',
);
assert.match(
workflow,
/Generate SkillSpector report/,
'Skill release workflow must generate a SkillSpector report for each released skill',
);
assert.doesNotMatch(
workflow,
/"### SkillSpector Security Report"/,
'GitHub release notes must not add a duplicate SkillSpector heading before the generated report',
);
assert.match(
workflow,
/readFileSync\("release-assets\/skillspector-report\.md", "utf8"\)[\s\S]*report,[\s\S]*\[skillspector-report\.md\]\(https:\/\/github\.com\/\$\{process\.env\.REPO\}\/releases\/download\/\$\{process\.env\.TAG\}\/skillspector-report\.md\)/,
'GitHub release notes must embed the generated SkillSpector report and include a direct report link',
);
assert.match(
workflow,
/readFileSync\("release-assets\/skillspector-report\.md", "utf8"\)/,
'GitHub release notes must load the generated SkillSpector report content into the release body file',
);
assert.match(
workflow,
/body_path: \$\{\{ runner\.temp \}\}\/skill-release-body\.md/,
'GitHub release creation must use body_path for the generated release body file',
);
assert.doesNotMatch(
workflow,
/SKILLSPECTOR_REPORT_EOF|\$\{\{ steps\.skillspector_report\.outputs\.body \}\}|cat release-assets\/skillspector-report\.md[\s\S]*>> "\$GITHUB_OUTPUT"/,
'SkillSpector report content must not be sent through GitHub Actions step outputs',
);
assert.match(
workflow,
/generate_skillspector_report "\$\{inner_dir\}" "\$\{out_assets\}\/skillspector-report\.md"/,
'PR dry-run SkillSpector scan must target the staged release payload, not the source skill directory',
);
assert.match(
workflow,
/Run release dry-run for changed skills[\s\S]*git diff --name-only "\$\{BASE_SHA\}\.\.\.\$\{HEAD_SHA\}" --[\s\S]*'skills\/\*\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/test\/\*\*'[\s\S]*':\(exclude\)skills\/\*\/tests\/\*\*'/,
'PR dry-run SkillSpector scan must run when any release-relevant skill package file changes',
);
assert.ok(
workflow.includes('local name="${lower##*/}"')
&& workflow.includes('"$name" == test_*')
&& workflow.includes('"$name" == *.test.*')
&& workflow.includes('(__tests__|test|tests)/|(^|/)(test|spec)[_-]|(^|/).*\\.(test|spec)\\.'),
'Skill release archives must exclude test directories and test-named files from staged release payloads',
);
assert.doesNotMatch(
workflow,
/generate_skillspector_report "\$\{skill_dir\}" "\$\{out_assets\}\/skillspector-report\.md"/,
'PR dry-run SkillSpector scan must not include source-only test directories',
);
assert.match(
workflow,
/generate_skillspector_report "\$INNER_DIR" "release-assets\/skillspector-report\.md"/,
'Tag release SkillSpector scan must target the staged release payload, not the source skill directory',
);
assert.doesNotMatch(
workflow,
/generate_skillspector_report "\$SKILL_PATH" "release-assets\/skillspector-report\.md"/,
'Tag release SkillSpector scan must not include source-only test directories',
);
assert.match(
workflow,
/Generate release trust packet/,
'Skill release workflow must generate skill cards, permission summaries, and npx install instructions',
);
for (const artifact of ['skill-card.md', 'permissions.json', 'install.md', 'skillspector-report.md']) {
assert.match(
workflow,
new RegExp(`release-assets/${artifact.replace('.', '\\.')}`),
`Skill release workflow must publish ${artifact} in release assets`,
);
}
const escapeRegExp = (literal) => literal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
for (const artifact of ['skill-card.md', 'permissions.json', 'install.md', 'skillspector-report.md']) {
assert.match(
workflow,
new RegExp(
String.raw`if ! add_release_asset_checksum "\$\{out_assets\}" "${escapeRegExp(artifact)}"; then` +
String.raw`[\s\S]*?failures=\$\(\(failures \+ 1\)\)[\s\S]*?continue[\s\S]*?fi`,
),
`PR dry-run validation must aggregate and continue when ${artifact} cannot be checksummed`,
);
}
assert.match(
workflow,
/add_release_asset_checksum "skill-card\.md"/,
'Skill card must be included in the signed checksums manifest',
);
assert.match(
workflow,
/add_release_asset_checksum "permissions\.json"/,
'Permissions summary must be included in the signed checksums manifest',
);
assert.match(
workflow,
/add_release_asset_checksum "install\.md"/,
'npx install/update instructions must be included in the signed checksums manifest',
);
assert.match(
workflow,
/add_release_asset_checksum "skillspector-report\.md"/,
'SkillSpector report must be included in the signed checksums manifest',
);
assert.match(
workflow,
/Upload SkillSpector PR reports[\s\S]*actions\/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7\.0\.1[\s\S]*name: skillspector-pr-reports/,
'PR dry-run must upload generated SkillSpector reports as workflow artifacts',
);
assert.match(
workflow,
/comment-skillspector-report:[\s\S]*needs: release[\s\S]*issues: write[\s\S]*actions\/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8\.0\.1/,
'Skill release workflow must download generated SkillSpector reports in a separate PR comment job with comment permissions',
);
assert.match(
workflow,
/comment-skillspector-report:[\s\S]*if: always\(\) && github\.event_name == 'pull_request' && needs\.release\.result != 'cancelled'[\s\S]*Download SkillSpector reports[\s\S]*continue-on-error: true/,
'SkillSpector PR comments must still run when the release dry-run produced reports but the release job failed later',
);
assert.match(
workflow,
/function sanitizeReportForComment\(report\)[\s\S]*code block omitted from PR comment[\s\S]*inline snippet omitted[\s\S]*redacted-email[\s\S]*redacted-token/,
'SkillSpector PR comments must sanitize raw report content before posting to the PR',
);
assert.match(
workflow,
/const sanitizedReport = sanitizeReportForComment\(report\);[\s\S]*`\$\{marker\}\\n\$\{sanitizedReport\}/,
'SkillSpector PR comments must use the sanitized report body, not the raw artifact text',
);
assert.doesNotMatch(
workflow,
/`\$\{marker\}\\n\$\{report\.trimEnd\(\)\}/,
'SkillSpector PR comments must not post report.trimEnd() verbatim',
);
assert.match(
workflow,
/clawsec-skillspector-report:\$\{tag\}[\s\S]*github\.rest\.issues\.updateComment[\s\S]*github\.rest\.issues\.createComment/,
'SkillSpector PR comments must use stable per-skill markers and update existing comments before creating new ones',
);
assert.match(
workflow,
/Simulate tag release build/,
'Skill release workflow must simulate a tag release build during PR validation',
);
assert.match(
workflow,
/simulate_skill_tag_release\.mjs/,
'Skill release workflow must call the tag release simulation script',
);
assert.ok(
workflow.includes('simulated_version | test("^[0-9]+\\\\.[0-9]+\\\\.[0-9]+(-[a-zA-Z0-9]+)?$")'),
'Skill release workflow must accept every prerelease version format that release-skill.sh accepts',
);
assert.match(
workflow,
/clawhub_slug: \$\{\{ steps\.publishable\.outputs\.clawhub_slug \}\}/,
'Skill release workflow must expose the resolved ClawHub slug from release-tag outputs',
);
assert.match(
workflow,
/CLAWHUB_SLUG=\$\(node scripts\/ci\/resolve_clawhub_slug\.mjs "\$SKILL_PATH"\)/,
'Skill release workflow must resolve the ClawHub slug from the skill package path',
);
assert.match(
workflow,
/cp scripts\/ci\/resolve_clawhub_slug\.mjs "\$RUNNER_TEMP\/resolve_clawhub_slug\.mjs"/,
'Manual ClawHub republish must preserve the current slug helper before checking out an older release tag',
);
assert.match(
workflow,
/CLAWHUB_SLUG=\$\(node "\$RUNNER_TEMP\/resolve_clawhub_slug\.mjs" "\$SKILL_PATH"\)/,
'Manual ClawHub republish must resolve slugs with the preserved helper against the checked-out tag metadata',
);
assert.match(
workflow,
/npx clawhub@latest install \$\{CLAWHUB_SLUG\}/,
'GitHub release quick install instructions must use the resolved ClawHub slug',
);
assert.match(
workflow,
/clawhub inspect "\$CLAWHUB_SLUG" --version "\$VERSION" --json/,
'Duplicate ClawHub version guard must inspect the resolved ClawHub slug',
);
assert.match(
workflow,
/--slug "\$CLAWHUB_SLUG"/,
'ClawHub publish must use the resolved ClawHub slug',
);
assert.equal(
workflow.match(/bash scripts\/ci\/install_clawhub_cli\.sh/g)?.length,
2,
'ClawHub publish and republish jobs must share the same pinned CLI installer',
);
assert.equal(
workflow.match(/node scripts\/ci\/patch_clawhub_publish_payload\.mjs/g)?.length,
2,
'ClawHub publish and republish jobs must share the same payload patch helper',
);
assert.doesNotMatch(
workflow,
/npm ci --prefix \.github\/clawhub-cli/,
'ClawHub CLI installation must not be duplicated inline in the workflow',
);
assert.doesNotMatch(
workflow,
/node <<'NODE'[\s\S]*acceptLicenseTerms: true/,
'ClawHub payload patching must not be duplicated inline in the workflow',
);
for (const secret of ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN']) {
assert.match(
workflow,
new RegExp(`${secret}: \\$\\{\\{ secrets\\.${secret} \\}\\}`),
`ClawHub jobs must expose ${secret} for CodeArtifact npm authentication`,
);
}
assert.match(
installClawhubCli,
/aws codeartifact login[\s\S]*--domain "\$CODEARTIFACT_DOMAIN"[\s\S]*--domain-owner "\$CODEARTIFACT_DOMAIN_OWNER"[\s\S]*--repository "\$CODEARTIFACT_REPOSITORY"[\s\S]*--region "\$AWS_REGION"/,
'ClawHub CLI installer must authenticate npm against CodeArtifact before npm ci',
);
assert.match(
installClawhubCli,
/npm ci --prefix "\$CLI_PREFIX"/,
'ClawHub CLI installer must install from the committed lockfile prefix',
);
assert.match(
installClawhubCli,
/"\$\{workspace\}\/\$\{CLI_PREFIX\}\/node_modules\/\.bin" >> "\$GITHUB_PATH"/,
'ClawHub CLI installer must expose the pinned clawhub binary on GITHUB_PATH',
);
assert.match(
patchClawhubPayload,
/const payloadPattern = \/changelog,\\r\?\\n\(\\s\*\)tags,\/;/,
'ClawHub payload patch helper must target the expected publish payload shape',
);
assert.match(
patchClawhubPayload,
/acceptLicenseTerms: true/,
'ClawHub payload patch helper must preserve the acceptLicenseTerms workaround',
);
assert.doesNotMatch(
workflow,
/clawhub inspect "\$SKILL_NAME" --version "\$VERSION" --json/,
'Duplicate ClawHub version guard must not inspect the raw skill package name',
);
assert.doesNotMatch(
workflow,
/--slug "\$SKILL_NAME"/,
'ClawHub publish must not use the raw skill package name as the ClawHub slug',
);
@@ -0,0 +1,184 @@
import assert from "node:assert/strict";
import { chmod, cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
const tempRoot = await mkdtemp(path.join(tmpdir(), "clawsec-tag-release-sim-"));
const fakeSkillspector = path.join(tempRoot, "skillspector");
async function prereleaseFixture(sourceSkillDir, version, fixtureGroup) {
const fixtureDir = path.join(tempRoot, fixtureGroup, path.basename(sourceSkillDir));
await cp(sourceSkillDir, fixtureDir, { recursive: true });
const skillJsonPath = path.join(fixtureDir, "skill.json");
const skill = JSON.parse(await readFile(skillJsonPath, "utf8"));
skill.version = version;
await writeFile(skillJsonPath, `${JSON.stringify(skill, null, 2)}\n`);
const skillMdPath = path.join(fixtureDir, "SKILL.md");
const skillMd = await readFile(skillMdPath, "utf8");
await writeFile(skillMdPath, skillMd.replace(/^version:\s*.+$/m, `version: ${version}`));
return fixtureDir;
}
async function runSimulation({ skillDir, outputDir, expectedOriginal, expectedSimulated, expectedAgent }) {
const result = spawnSync(
process.execPath,
[
"scripts/ci/simulate_skill_tag_release.mjs",
skillDir,
outputDir,
"--repository",
"prompt-security/clawsec",
"--source-ref",
"pull-request-head",
"--skillspector-bin",
fakeSkillspector,
],
{ encoding: "utf8" },
);
assert.equal(
result.status,
0,
`tag release simulation failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
const skillName = path.basename(skillDir);
const expectedTag = `${skillName}-v${expectedSimulated}`;
const summary = JSON.parse(await readFile(path.join(outputDir, "simulation-summary.json"), "utf8"));
assert.equal(summary.skill, skillName);
assert.equal(summary.original_version, expectedOriginal);
assert.equal(summary.simulated_version, expectedSimulated);
assert.equal(summary.tag, expectedTag);
const releaseAssetsDir = path.join(outputDir, "release-assets");
const checksums = JSON.parse(await readFile(path.join(releaseAssetsDir, "checksums.json"), "utf8"));
assert.equal(checksums.skill, skillName);
assert.equal(checksums.version, expectedSimulated);
assert.equal(checksums.tag, expectedTag);
assert.equal(checksums.archive.filename, `${expectedTag}.zip`);
for (const artifact of [
"skill-card.md",
"permissions.json",
"install.md",
"skillspector-report.md",
"checksums.sig",
"signing-public.pem",
]) {
assert.ok(
checksums.files[artifact] || artifact.endsWith(".sig") || artifact === "signing-public.pem",
`expected ${artifact} to be represented in the release output`,
);
const file = await readFile(path.join(releaseAssetsDir, artifact));
assert.ok(file.length > 0, `${artifact} should not be empty`);
}
const archive = await readFile(path.join(releaseAssetsDir, `${expectedTag}.zip`));
assert.ok(archive.length > 0, "release archive should not be empty");
const install = await readFile(path.join(releaseAssetsDir, "install.md"), "utf8");
assert.match(
install,
new RegExp(
`npx skills add prompt-security/clawsec#pull-request-head --skill ${skillName} --agent ${expectedAgent} --global --yes`,
),
);
assert.match(install, new RegExp(`npx skills update ${skillName}`));
}
try {
await writeFile(
fakeSkillspector,
`#!/usr/bin/env node
import { readdirSync, writeFileSync } from "node:fs";
import path from "node:path";
const scanIndex = process.argv.indexOf("scan");
if (scanIndex === -1 || !process.argv[scanIndex + 1]) {
console.error("missing scan target");
process.exit(2);
}
function containsTestDirectory(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const lowerName = entry.name.toLowerCase();
if (lowerName === "test" || lowerName === "tests") {
return true;
}
if (containsTestDirectory(path.join(dir, entry.name))) {
return true;
}
}
return false;
}
const scanTarget = process.argv[scanIndex + 1];
if (containsTestDirectory(scanTarget)) {
console.error("SkillSpector test fixture must scan the staged release payload, not source test directories.");
process.exit(42);
}
const outputIndex = process.argv.indexOf("--output");
if (outputIndex === -1 || !process.argv[outputIndex + 1]) {
console.error("missing --output");
process.exit(2);
}
writeFileSync(process.argv[outputIndex + 1], "# Fake SkillSpector Report\\n\\nNo live scan executed in unit test.\\n");
`,
{ mode: 0o700 },
);
await chmod(fakeSkillspector, 0o700);
await runSimulation({
skillDir: "skills/clawsec-suite",
outputDir: path.join(tempRoot, "stable"),
expectedOriginal: "0.1.10",
expectedSimulated: "0.1.11",
expectedAgent: "openclaw",
});
await runSimulation({
skillDir: "skills/hermes-traffic-guardian",
outputDir: path.join(tempRoot, "beta"),
expectedOriginal: "0.0.1-beta3",
expectedSimulated: "0.0.1-beta4",
expectedAgent: "hermes-agent",
});
const alphaSkillDir = await prereleaseFixture("skills/picoclaw-self-pen-testing", "0.0.3-alpha1", "alpha-fixture");
await runSimulation({
skillDir: alphaSkillDir,
outputDir: path.join(tempRoot, "alpha"),
expectedOriginal: "0.0.3-alpha1",
expectedSimulated: "0.0.3-alpha2",
expectedAgent: "openclaw",
});
const rcSkillDir = await prereleaseFixture("skills/picoclaw-security-guardian", "0.0.4-rc1", "rc-fixture");
await runSimulation({
skillDir: rcSkillDir,
outputDir: path.join(tempRoot, "rc"),
expectedOriginal: "0.0.4-rc1",
expectedSimulated: "0.0.4-rc2",
expectedAgent: "openclaw",
});
const previewSkillDir = await prereleaseFixture("skills/openclaw-traffic-guardian", "0.0.1-preview", "preview-fixture");
await runSimulation({
skillDir: previewSkillDir,
outputDir: path.join(tempRoot, "preview"),
expectedOriginal: "0.0.1-preview",
expectedSimulated: "0.0.1-preview1",
expectedAgent: "openclaw",
});
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
+79
View File
@@ -0,0 +1,79 @@
import assert from "node:assert/strict";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
const outputDir = await mkdtemp(path.join(tmpdir(), "clawsec-trust-packet-"));
function runTrustPacket(skillDir, targetDir, tag) {
return spawnSync(
process.execPath,
[
"scripts/ci/generate_skill_release_trust_packet.mjs",
skillDir,
targetDir,
"--repository",
"prompt-security/clawsec",
"--tag",
tag,
"--source-ref",
"main",
],
{ encoding: "utf8" },
);
}
try {
const result = runTrustPacket("skills/clawsec-suite", outputDir, "clawsec-suite-v0.1.10");
assert.equal(
result.status,
0,
`trust packet generator failed\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
const skillCard = await readFile(path.join(outputDir, "skill-card.md"), "utf8");
const permissions = JSON.parse(await readFile(path.join(outputDir, "permissions.json"), "utf8"));
const install = await readFile(path.join(outputDir, "install.md"), "utf8");
assert.match(skillCard, /^# Skill Card/m);
assert.match(skillCard, /## License\/Terms of Use/);
assert.match(skillCard, /AGPL-3\.0-or-later/);
assert.match(skillCard, /skillspector-report\.md/);
assert.match(skillCard, /clawsec-suite-v0\.1\.10/);
assert.equal(permissions.skill, "clawsec-suite");
assert.equal(permissions.version, "0.1.10");
assert.equal(permissions.platform, "openclaw");
assert.deepEqual(
permissions.required_binaries,
["node", "npx", "openclaw", "curl", "jq", "shasum", "openssl", "unzip"],
);
assert.match(permissions.network_egress, /signed advisory feed/);
assert.match(permissions.persistence, /OpenClaw advisory hook/);
assert.ok(Array.isArray(permissions.operator_review));
assert.ok(permissions.operator_review.length > 0);
assert.match(install, /npx skills add prompt-security\/clawsec --skill clawsec-suite --agent openclaw --global --yes/);
assert.match(install, /npx skills update clawsec-suite/);
const hermesOutputDir = path.join(outputDir, "hermes");
const hermesResult = runTrustPacket(
"skills/hermes-attestation-guardian",
hermesOutputDir,
"hermes-attestation-guardian-v0.1.4",
);
assert.equal(
hermesResult.status,
0,
`Hermes trust packet generator failed\nstdout:\n${hermesResult.stdout}\nstderr:\n${hermesResult.stderr}`,
);
const hermesInstall = await readFile(path.join(hermesOutputDir, "install.md"), "utf8");
assert.match(
hermesInstall,
/npx skills add prompt-security\/clawsec --skill hermes-attestation-guardian --agent hermes-agent --global --yes/,
);
} finally {
await rm(outputDir, { recursive: true, force: true });
}
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## [0.0.4] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
- Marked the release helper with top-level internal metadata so compatible installers can hide it from normal agent-facing discovery.
## [0.0.3] - 2026-05-14
### Security
+11
View File
@@ -0,0 +1,11 @@
# Claw Release
Release automation for Claw skills and website. Guides through version bumping, tagging, and release verification.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill claw-release -a openclaw -y
```
+16 -4
View File
@@ -1,9 +1,14 @@
---
name: claw-release
version: 0.0.3
version: 0.0.4
description: Release automation for Claw skills and website. Guides through version bumping, tagging, and release verification.
homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"🚀","category":"utility","internal":true}}
metadata:
internal: true
openclaw:
emoji: "🚀"
category: "utility"
internal: true
clawdis:
emoji: "🚀"
requires:
@@ -18,6 +23,14 @@ Internal tool for releasing skills and managing the ClawSec catalog.
---
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill claw-release -a openclaw -y
```
## Operational Notes
- Internal maintainer workflow only.
@@ -26,7 +39,6 @@ Internal tool for releasing skills and managing the ClawSec catalog.
- Side effects: creates commits, tags, pushes to remote, and publishes GitHub Releases
- Trust model: run only from a trusted checkout with a clean working tree and maintainer approval
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
@@ -35,7 +47,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="claw-release"
VERSION="0.0.3"
VERSION="0.0.4"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claw-release",
"version": "0.0.3",
"version": "0.0.4",
"description": "Release automation for Claw skills and website. Guides through version bumping, tagging, and release verification.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,11 @@
# Changelog
## [0.0.6] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.5] - 2026-06-07
### Security
+8
View File
@@ -2,6 +2,14 @@
A `clawsec-suite` companion skill that adds a standalone reputation gate before guarded installs.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-clawhub-checker -a openclaw -y
```
## Operational Notes
- Required runtime: `node`, `clawhub`, `openclaw`
+9 -2
View File
@@ -1,6 +1,6 @@
---
name: clawsec-clawhub-checker
version: 0.0.5
version: 0.0.6
description: ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.
homepage: https://clawsec.prompt.security
clawdis:
@@ -14,6 +14,14 @@ clawdis:
Adds a reputation gate on top of the `clawsec-suite` guarded installer.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-clawhub-checker -a openclaw -y
```
## Operational Notes
- Required runtime: `node`, `clawhub`, `openclaw`
@@ -45,7 +53,6 @@ Optional preflight check (validates local paths and prints recommended command):
node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs
```
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-clawhub-checker",
"version": "0.0.5",
"version": "0.0.6",
"description": "ClawHub reputation checker for clawsec-suite. Adds a standalone reputation gate before guarded skill installation.",
"author": "abutbul",
"license": "AGPL-3.0-or-later",
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [0.0.9] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.8] - 2026-05-24
### Changed
+8
View File
@@ -2,6 +2,14 @@
Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence and stay informed about emerging threats.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-feed -a openclaw -y
```
## Operational Notes
- Required runtime for standalone installation: `bash`, `curl`, `jq`, `shasum`, `unzip`
+10 -4
View File
@@ -1,6 +1,6 @@
---
name: clawsec-feed
version: 0.0.8
version: 0.0.9
description: Security advisory feed package for OpenClaw-related threats and vulnerabilities. The upstream feed is updated daily; local automation is handled by clawsec-suite or the operator.
homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"📡","category":"security"}}
@@ -16,6 +16,14 @@ Security advisory feed monitoring for AI agents. Subscribe to community-driven t
The default `feed.json` is the consolidated agent feed. It includes NVD CVEs, approved community advisories, and provisional GitHub Security Advisories that do not have CVE IDs yet.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-feed -a openclaw -y
```
## Operational Notes
- Required runtime for standalone installation: `bash`, `curl`, `jq`, `shasum`, `unzip`
@@ -66,7 +74,6 @@ Standalone installation is a network download workflow. Verify the release sourc
Installation Steps:
Agents should review scripts before execution!
```bash
# Get latest clawsec-feed release tag
LATEST_TAG=$(curl -sSL https://api.github.com/repos/prompt-security/ClawSec/releases | \
@@ -81,7 +88,6 @@ Once you have this skill file, proceed to **[Deploy ClawSec Feed](#deploy-clawse
---
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
@@ -90,7 +96,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="clawsec-feed"
VERSION="0.0.8"
VERSION="0.0.9"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1 +1 @@
v+PiWmjIkY6zdIyI9xJX0l0aTy0Azp1+LoZR6qaiDZJnXFuSBX4Sw/x5tMdTb0xSbqdDTJOZwwWI8coPVepzBw==
jPrlTYwicRwoQgTs5Rk3Y3g6Lz78jNRs9ZNf0R09M4jkJokZENxfvhvHphI9MH4u+7wv0sFZ+yZbQtJ42y+hCQ==
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-feed",
"version": "0.0.8",
"version": "0.0.9",
"description": "Security advisory feed monitoring for AI agents. Subscribe to community-driven threat intelligence.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [0.0.8] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.7] - 2026-06-07
### Security
+8
View File
@@ -2,6 +2,14 @@
ClawSec now supports NanoClaw, a containerized WhatsApp bot powered by Claude agents.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-nanoclaw -a openclaw -y
```
## What Changed
### Advisory Feed Monitoring
+9 -2
View File
@@ -1,6 +1,6 @@
---
name: clawsec-nanoclaw
version: 0.0.7
version: 0.0.8
description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot
---
@@ -8,6 +8,14 @@ description: Use when checking for security vulnerabilities in NanoClaw skills,
Security advisory monitoring that protects your WhatsApp bot from known vulnerabilities in skills and dependencies.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-nanoclaw -a openclaw -y
```
## Overview
ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills, includes exploitability context for triage, and alerts you to issues in existing ones.
@@ -201,7 +209,6 @@ See [INSTALL.md](./INSTALL.md) for setup and [docs/](./docs/) for advanced usage
- Provides actionable remediation steps
- Zero false positives (curated feed only)
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-nanoclaw",
"version": "0.0.7",
"version": "0.0.8",
"description": "ClawSec security suite for NanoClaw - Advisory feed monitoring, MCP tools for vulnerability checking, and Ed25519 signature verification for containerized WhatsApp bot agents",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [0.0.5] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.4] - 2026-06-07
### Security
+11
View File
@@ -0,0 +1,11 @@
# Clawsec Scanner
Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific static hook inspection for OpenClaw hooks.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-scanner -a openclaw -y
```
+9 -1
View File
@@ -1,6 +1,6 @@
---
name: clawsec-scanner
version: 0.0.4
version: 0.0.5
description: Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific static hook inspection for OpenClaw hooks.
homepage: https://clawsec.prompt.security
clawdis:
@@ -20,6 +20,14 @@ Comprehensive security scanner for agent platforms that automates vulnerability
- **Unified Reporting**: Consolidated vulnerability reports with severity classification and remediation guidance
- **Continuous Monitoring**: OpenClaw hook integration for automated periodic scanning
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-scanner -a openclaw -y
```
## Features
### Multi-Engine Scanning
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-scanner",
"version": "0.0.4",
"version": "0.0.5",
"description": "Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific static hook inspection for OpenClaw hooks.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [0.1.10] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
All notable changes to the ClawSec Suite will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+11
View File
@@ -0,0 +1,11 @@
# Clawsec Suite
ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-suite -a openclaw -y
```
+9 -1
View File
@@ -1,6 +1,6 @@
---
name: clawsec-suite
version: 0.1.9
version: 0.1.10
description: ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.
homepage: https://clawsec.prompt.security
clawdis:
@@ -11,6 +11,14 @@ clawdis:
# ClawSec Suite
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawsec-suite -a openclaw -y
```
## Operational Notes
- Required runtime: `node`, `npx`, `openclaw`, `curl`, `jq`, `shasum`, `openssl`, `unzip`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "clawsec-suite",
"version": "0.1.9",
"version": "0.1.10",
"description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+10
View File
@@ -1,5 +1,15 @@
# Changelog
## [0.0.7] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
- Marked Clawtributor as a harness-neutral global skill for OpenClaw, NanoClaw, Hermes, and Picoclaw installer grouping.
- Removed OpenClaw CLI as a declared runtime requirement because reporting is manual, approval-gated, and not tied to an OpenClaw command path.
- Documented Vercel skills installer usage alongside the OpenClaw/ClawHub install path.
- Moved local report/state guidance to `~/.clawsec/clawtributor/`.
## [0.0.6] - 2026-05-14
### Security
+22
View File
@@ -2,6 +2,20 @@
Community incident reporting for AI agents.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawtributor -a openclaw -y
```
Codex install is also supported:
```bash
npx skills add prompt-security/clawsec --skill clawtributor -a codex -y
```
## Operational Notes
- Reporting is opt-in for every submission
@@ -17,6 +31,14 @@ Community incident reporting for AI agents.
## Quick Install
Vercel skills installer:
```bash
npx skills add prompt-security/clawsec --skill clawtributor -a codex -y
```
OpenClaw/ClawHub:
```bash
npx clawhub@latest install clawtributor
```
+38 -12
View File
@@ -1,23 +1,44 @@
---
name: clawtributor
version: 0.0.6
description: Community incident reporting for AI agents. Contribute to collective security by reporting threats.
version: 0.0.7
description: Harness-neutral community incident reporting for AI agents. Contribute to collective security by reporting threats.
homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"🤝","category":"security"}}
platforms:
- openclaw
- nanoclaw
- hermes
- picoclaw
metadata:
global: true
openclaw:
emoji: "🤝"
category: "security"
clawdis:
emoji: "🤝"
requires:
bins: [openclaw]
---
# Clawtributor 🤝
Community incident reporting for AI agents. Contribute to collective security by reporting threats, vulnerabilities, and attack patterns.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill clawtributor -a openclaw -y
```
Codex install is also supported:
```bash
npx skills add prompt-security/clawsec --skill clawtributor -a codex -y
```
## Operational Notes
- Recommended install path: ClawHub registry (`npx clawhub@latest install clawtributor`)
- Side effects: creates local report/state files under `~/.openclaw/`
- Recommended install path: harness-native skills installer; use ClawHub for OpenClaw/ClawHub environments (`npx clawhub@latest install clawtributor`)
- Side effects: creates local report/state files under `~/.clawsec/clawtributor/`
- Network behavior: none unless the user explicitly approves manual submission
- Trust model: reporting is opt-in for every submission; sanitize evidence before it leaves the host
@@ -27,7 +48,13 @@ Community incident reporting for AI agents. Contribute to collective security by
## Installation
Install from the registry:
Install with your harness-native skills installer. For the Vercel skills installer:
```bash
npx skills add prompt-security/clawsec --skill clawtributor -a codex -y
```
For OpenClaw/ClawHub environments, install from the registry:
```bash
npx clawhub@latest install clawtributor
@@ -44,7 +71,6 @@ I will keep reports local unless you explicitly approve submission.
---
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
@@ -53,7 +79,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="clawtributor"
VERSION="0.0.6"
VERSION="0.0.7"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
@@ -233,7 +259,7 @@ See [reporting.md](./reporting.md) for the full report format and submission gui
### Step 1: Prepare report locally
- Save the report JSON under `~/.openclaw/clawtributor-reports/`
- Save the report JSON under `~/.clawsec/clawtributor/reports/`
- Keep file permissions private (`chmod 600`)
- Confirm the report is sanitized before sharing
@@ -284,7 +310,7 @@ DO NOT include:
## State Tracking
Track submitted reports in `~/.openclaw/clawtributor-state.json`.
Track submitted reports in `~/.clawsec/clawtributor/state.json`.
Example:
+10 -7
View File
@@ -1,16 +1,24 @@
{
"name": "clawtributor",
"version": "0.0.6",
"description": "Community incident reporting for AI agents. Contribute to collective security by reporting threats.",
"version": "0.0.7",
"description": "Harness-neutral community incident reporting for AI agents. Contribute to collective security by reporting threats.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
"homepage": "https://clawsec.prompt.security",
"platforms": [
"openclaw",
"nanoclaw",
"hermes",
"picoclaw"
],
"keywords": [
"security",
"reporting",
"community",
"agents",
"ai",
"global",
"harness-neutral",
"vulnerability",
"contribution"
],
@@ -36,11 +44,6 @@
"openclaw": {
"emoji": "🤝",
"category": "security",
"requires": {
"bins": [
"openclaw"
]
},
"execution": {
"always": false,
"persistence": "Stores local report/state files only; no recurring automation is created by default.",
@@ -1,5 +1,11 @@
# Changelog
## [0.1.4] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.1.3] - 2026-05-24
### Changed
@@ -4,6 +4,14 @@ Hermes-only attestation, advisory verification, and guarded verification workflo
Status: implemented (v0.1.0), Hermes-only.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill hermes-attestation-guardian -a hermes-agent -y
```
## Capabilities
This skill now covers the full Hermes-side capability set expected from the clawsec-suite parity workstream:
+9 -2
View File
@@ -1,6 +1,6 @@
---
name: hermes-attestation-guardian
version: 0.1.3
version: 0.1.4
description: Hermes-only runtime security attestation and drift detection skill for operator-managed Hermes infrastructure.
homepage: https://clawsec.prompt.security
hermes:
@@ -15,6 +15,13 @@ IMPORTANT SCOPE:
- This skill targets Hermes infrastructure only (CLI/Gateway/profile-managed deployments).
- This skill is not an OpenClaw runtime hook package.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill hermes-attestation-guardian -a hermes-agent -y
```
## Release Artifact Verification
@@ -24,7 +31,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="hermes-attestation-guardian"
VERSION="0.1.3"
VERSION="0.1.4"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
@@ -1,6 +1,6 @@
{
"name": "hermes-attestation-guardian",
"version": "0.1.3",
"version": "0.1.4",
"description": "Hermes-only runtime security attestation and drift detection skill. Generates deterministic posture artifacts, verifies integrity fail-closed, and classifies baseline drift severity.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,11 @@
# Changelog
## [0.0.1-beta3] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.1-beta2] - 2026-05-13
### Security
+8 -1
View File
@@ -4,6 +4,14 @@ Baseline skill for Hermes runtime traffic monitoring.
This package is intentionally a spec scaffold. Builders should add the Hermes-specific monitor implementation here while preserving the safety contract in `SKILL.md` and `SPEC.md`.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill hermes-traffic-guardian -a hermes-agent -y
```
## Intended Capability
- detect outbound secret exfiltration in Hermes HTTP/HTTPS traffic
@@ -15,4 +23,3 @@ This package is intentionally a spec scaffold. Builders should add the Hermes-sp
## Builder Notes
Keep runtime ownership in this skill. `hermes-attestation-guardian` should only attest this skill's state, config, and output fingerprints.
+9 -3
View File
@@ -1,6 +1,6 @@
---
name: hermes-traffic-guardian
version: 0.0.1-beta2
version: 0.0.1-beta3
description: Hermes runtime traffic monitoring baseline for opt-in proxy inspection, egress detection, and attestation-aware traffic posture.
homepage: https://clawsec.prompt.security
author: prompt-security
@@ -15,6 +15,13 @@ hermes:
This is a baseline specification skill. It intentionally does not ship a proxy or runtime implementation yet.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill hermes-traffic-guardian -a hermes-agent -y
```
## Release Artifact Verification
@@ -24,7 +31,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="hermes-traffic-guardian"
VERSION="0.0.1-beta2"
VERSION="0.0.1-beta3"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
@@ -145,4 +152,3 @@ Read `SPEC.md` before implementing. Use the placeholder folders as follows:
- default blocking
- sending traffic to external services
- collecting full request/response bodies
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hermes-traffic-guardian",
"version": "0.0.1-beta2",
"version": "0.0.1-beta3",
"description": "Hermes runtime traffic monitoring baseline for opt-in proxy inspection, egress detection, and attestation-aware traffic posture.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,11 @@
# Changelog
## [0.0.1-beta3] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.1-beta2] - 2026-05-13
### Security
+8 -1
View File
@@ -4,6 +4,14 @@ Baseline skill for NanoClaw runtime traffic monitoring.
This package is intentionally a spec scaffold. Builders should add the NanoClaw-specific host-service, IPC, and MCP implementation here while preserving the safety contract in `SKILL.md` and `SPEC.md`.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill nanoclaw-traffic-guardian -a openclaw -y
```
## Intended Capability
- detect outbound secret exfiltration in NanoClaw host-managed traffic
@@ -15,4 +23,3 @@ This package is intentionally a spec scaffold. Builders should add the NanoClaw-
## Builder Notes
Follow the existing `clawsec-nanoclaw` pattern: host services own privileged operations, while MCP tools expose bounded requests and redacted responses.
+9 -3
View File
@@ -1,6 +1,6 @@
---
name: nanoclaw-traffic-guardian
version: 0.0.1-beta2
version: 0.0.1-beta3
description: NanoClaw runtime traffic monitoring baseline for host-side proxy inspection with container-safe MCP and IPC status surfaces.
homepage: https://clawsec.prompt.security
author: prompt-security
@@ -14,6 +14,13 @@ nanoclaw:
This is a baseline specification skill. It intentionally does not ship a proxy or runtime implementation yet.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill nanoclaw-traffic-guardian -a openclaw -y
```
## Release Artifact Verification
@@ -23,7 +30,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="nanoclaw-traffic-guardian"
VERSION="0.0.1-beta2"
VERSION="0.0.1-beta3"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
@@ -146,4 +153,3 @@ Read `SPEC.md` before implementing. Use the placeholder folders as follows:
- default blocking
- sending traffic to external services
- exposing raw request/response bodies to the container
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "nanoclaw-traffic-guardian",
"version": "0.0.1-beta2",
"version": "0.0.1-beta3",
"description": "NanoClaw runtime traffic monitoring baseline for host-side proxy inspection with container-safe MCP and IPC status surfaces.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,11 @@
# Changelog
## [0.1.7] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.1.6] - 2026-05-16
### Fixed
+8
View File
@@ -2,6 +2,14 @@
Automated daily security audits for OpenClaw agents with DM delivery and optional email reporting.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill openclaw-audit-watchdog -a openclaw -y
```
## Overview
The Audit Watchdog provides automated security monitoring for your OpenClaw agent deployments:
+10 -3
View File
@@ -1,6 +1,6 @@
---
name: openclaw-audit-watchdog
version: 0.1.6
version: 0.1.7
description: Automated daily security audits for OpenClaw agents with DM delivery and optional email reporting. Runs deep audits, creates or updates a recurring cron job, and sends formatted reports to configured recipients.
homepage: https://clawsec.prompt.security
metadata:
@@ -29,6 +29,14 @@ clawdis:
# Prompt Security Audit (openclaw)
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill openclaw-audit-watchdog -a openclaw -y
```
## Installation Options
You can get openclaw-audit-watchdog in two ways:
@@ -65,7 +73,6 @@ Continue below for standalone installation instructions.
---
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
@@ -74,7 +81,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="openclaw-audit-watchdog"
VERSION="0.1.6"
VERSION="0.1.7"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openclaw-audit-watchdog",
"version": "0.1.6",
"version": "0.1.7",
"description": "Automated daily security audits for OpenClaw agents with DM delivery and optional email reporting. Creates or updates an unattended cron job and sends formatted reports to configured recipients.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,15 @@
# Changelog
## [0.0.1-beta3] - 2026-06-10
### Security
- Added the `POLICY_REVIEW` scope for approval-sensitive social-account mutation requests, contributed by @kriptoburak.
- Defined required JSONL metadata for social-account mutation findings, including source type, mutation category, approval-marker presence, and execution context.
### Changed
- Clarified that persistent social monitor and webhook configuration changes are review findings, while read-only social research should remain covered by no-false-positive tests.
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.1-beta2] - 2026-05-13
### Security
+9 -1
View File
@@ -4,10 +4,19 @@ Baseline skill for OpenClaw runtime traffic monitoring.
This package is intentionally a spec scaffold. Builders should add the OpenClaw-specific monitor implementation here while preserving the safety contract in `SKILL.md` and `SPEC.md`.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill openclaw-traffic-guardian -a openclaw -y
```
## Intended Capability
- detect outbound secret exfiltration in agent HTTP/HTTPS traffic
- detect inbound command-injection and tool-abuse payloads
- record operator-review findings for approval-sensitive social-account mutations
- write redacted local JSONL findings
- provide explicit start, stop, status, and log-query commands
- integrate with `clawsec-suite` as an optional add-on
@@ -15,4 +24,3 @@ This package is intentionally a spec scaffold. Builders should add the OpenClaw-
## Builder Notes
Use `SPEC.md` as the implementation contract. Keep runtime changes opt-in and scoped to the OpenClaw process being monitored.
+15 -6
View File
@@ -1,7 +1,7 @@
---
name: openclaw-traffic-guardian
version: 0.0.1-beta2
description: OpenClaw runtime traffic monitoring baseline for opt-in HTTP/HTTPS proxy inspection, egress detection, and inbound injection detection.
version: 0.0.1-beta3
description: OpenClaw runtime traffic monitoring baseline for opt-in HTTP/HTTPS proxy inspection, egress detection, inbound injection detection, and social-account policy review.
homepage: https://clawsec.prompt.security
author: prompt-security
license: AGPL-3.0-or-later
@@ -15,6 +15,13 @@ clawdis:
This is a baseline specification skill. It intentionally does not ship a proxy or runtime implementation yet.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill openclaw-traffic-guardian -a openclaw -y
```
## Release Artifact Verification
@@ -24,7 +31,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="openclaw-traffic-guardian"
VERSION="0.0.1-beta2"
VERSION="0.0.1-beta3"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
@@ -103,6 +110,7 @@ Builders should use this skill as the OpenClaw landing zone for runtime traffic
- optional HTTPS inspection with per-process CA trust
- outbound exfiltration detection
- inbound injection detection
- approval-sensitive social-account mutation review
- redacted local threat logs
- optional OpenClaw hook/status integration
@@ -136,8 +144,10 @@ Read `SPEC.md` before implementing. Use the placeholder folders as follows:
3. Scope proxy environment variables to the target OpenClaw process.
4. Inspect HTTP request/response text up to a bounded byte limit.
5. Support optional HTTPS MITM only when the operator supplies per-process trust configuration.
6. Emit JSONL findings with redacted snippets.
7. Provide a `status` command that reports mode, listener, CA fingerprint if present, and last findings.
6. Flag requests matching `SPEC.md`'s Outbound POLICY_REVIEW cases as operator-review findings, including TweetClaw or other X/Twitter automation writes and scheduler/background-runner repeats without a fresh operator-approval marker.
7. Detect repeat/background-runner context from bounded request metadata such as paths, headers, user-agent, client context, tool invocation metadata, or scheduler identifiers.
8. Emit JSONL findings with redacted snippets plus source type, mutation category, approval-marker presence, and direct-operator versus background-runner context.
9. Provide a `status` command that reports mode, listener, CA fingerprint if present, and last findings.
## Out of Scope for v0.0.1 Implementation
@@ -146,4 +156,3 @@ Read `SPEC.md` before implementing. Use the placeholder folders as follows:
- default blocking
- sending traffic to external services
- collecting full request/response bodies
+26 -1
View File
@@ -45,6 +45,24 @@ Findings must be JSON objects with these fields:
}
```
`POLICY_REVIEW` findings must keep the same base schema and add these fields:
```json
{
"threat_type": "POLICY_REVIEW",
"pattern": "social_account_mutation",
"source_type": "openclaw_tool_request",
"mutation_category": "post",
"approval_marker_present": false,
"execution_context": "background_runner"
}
```
- `source_type`: `http_request`, `openclaw_tool_request`, or `unknown`.
- `mutation_category`: `post`, `reply`, `repost`, `like`, `follow`, `unfollow`, `dm`, `media_upload`, `persistent_monitor`, `webhook_config`, `giveaway_draw`, or `other_social_account_mutation`.
- `approval_marker_present`: boolean; do not persist marker secrets or full approval tokens.
- `execution_context`: `direct_operator`, `scheduler`, `background_runner`, or `unknown`.
## Minimum Detection Set
Outbound EXFIL:
@@ -64,6 +82,12 @@ Inbound INJECTION:
- destructive remove commands
- SSH authorized-key injection shapes
Outbound POLICY_REVIEW:
- social-account write requests such as post, reply, repost, like, follow, unfollow, DM, media upload, persistent monitor creation/update, webhook configuration changes, or giveaway draw actions
- OpenClaw plugin/tool requests that invoke TweetClaw or another X/Twitter automation plugin for account mutation
- scheduler or background-runner requests that would repeat social-account mutations without a fresh operator approval
## Safety Requirements
- Default mode is detect-and-log.
@@ -72,6 +96,7 @@ Inbound INJECTION:
- Maximum scan bytes must be configurable and bounded.
- CA trust must be per-process by default.
- System trust-store instructions must require explicit operator confirmation and must never run automatically.
- POLICY_REVIEW findings must create an operator-review record only; they must not auto-block, auto-approve, or rewrite the requested action.
## Tests Required Before Release
@@ -79,7 +104,7 @@ Inbound INJECTION:
- redaction tests proving secrets are not persisted
- proxy fixture tests for HTTP request and response inspection
- no-false-positive tests for common benign traffic
- policy-review fixture tests for TweetClaw/social-account mutation examples and benign read-only social research requests
- lifecycle tests for stale PID/state cleanup
- status output tests
- OpenClaw hook integration tests if hook files are added
+11 -4
View File
@@ -1,7 +1,7 @@
{
"name": "openclaw-traffic-guardian",
"version": "0.0.1-beta2",
"description": "OpenClaw runtime traffic monitoring baseline for opt-in HTTP/HTTPS proxy inspection, egress detection, and inbound injection detection.",
"version": "0.0.1-beta3",
"description": "OpenClaw runtime traffic monitoring baseline for opt-in HTTP/HTTPS proxy inspection, egress detection, inbound injection detection, and social-account policy review.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
"homepage": "https://clawsec.prompt.security/",
@@ -15,7 +15,10 @@
"injection",
"proxy",
"mitm",
"runtime"
"runtime",
"policy-review",
"operator-review",
"social-account-mutation"
],
"sbom": {
"files": [
@@ -84,6 +87,7 @@
"https_mitm_inspection": "planned_optional",
"egress_exfiltration_detection": "planned",
"inbound_injection_detection": "planned",
"social_account_policy_review": "planned",
"blocking": "future_version"
},
"execution": {
@@ -96,6 +100,7 @@
"Default to detect-and-log mode; blocking is out of scope for v0.0.1 implementation.",
"Scope HTTP_PROXY/HTTPS_PROXY to the OpenClaw process being monitored.",
"Redact secret snippets before writing logs or sending conversation alerts.",
"Record POLICY_REVIEW findings for approval-sensitive social-account mutations without auto-blocking, auto-approving, or rewriting requests.",
"Integrate with clawsec-suite as an optional add-on, not a default install."
],
"triggers": [
@@ -103,7 +108,9 @@
"openclaw traffic monitoring",
"monitor openclaw egress",
"inspect openclaw http traffic",
"detect openclaw exfiltration"
"detect openclaw exfiltration",
"review social account mutations",
"detect tweetclaw write actions"
]
}
}
@@ -1,5 +1,11 @@
# Changelog
## [0.0.4] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.3] - 2026-05-24
### Changed
+8 -1
View File
@@ -6,6 +6,14 @@ Status: implemented (v0.0.1), Picoclaw-specific.
Detailed architecture/operator docs: `wiki/modules/picoclaw-security-guardian.md`.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill picoclaw-security-guardian -a openclaw -y
```
## Support matrix mapping
| Skill name | supported platform | security feed | config drift | agent posture-review lane | chain of supply verification |
@@ -48,4 +56,3 @@ test/picoclaw_security_guardian_sandbox_regression.sh
```
It uses Docker to publish the skill through a local ClawHub-compatible registry, installs it with Picoclaw's own `find_skills` / `install_skill` flow into an isolated Picoclaw workspace, confirms Picoclaw's skill loader can list/load it, then verifies the installed copy's profile, drift, advisory, and supply-chain paths.
+9 -2
View File
@@ -1,6 +1,6 @@
---
name: picoclaw-security-guardian
version: 0.0.3
version: 0.0.4
description: Picoclaw security posture skill with advisory awareness, configuration drift detection, and supply-chain verification guidance.
homepage: https://clawsec.prompt.security
author: prompt-security
@@ -18,6 +18,13 @@ picoclaw:
Detailed architecture/operator docs: `wiki/modules/picoclaw-security-guardian.md`.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill picoclaw-security-guardian -a openclaw -y
```
## Release Artifact Verification
@@ -27,7 +34,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="picoclaw-security-guardian"
VERSION="0.0.3"
VERSION="0.0.4"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picoclaw-security-guardian",
"version": "0.0.3",
"version": "0.0.4",
"description": "Picoclaw security posture skill with advisory awareness, configuration drift detection, and supply-chain verification guidance.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,11 @@
# Changelog
## [0.0.3] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.2] - 2026-05-13
### Security
@@ -4,6 +4,14 @@ Picoclaw-only local posture-review findings package for ClawSec.
Status: implemented (v0.0.1), Picoclaw-specific.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill picoclaw-self-pen-testing -a openclaw -y
```
## What it does
Given a generated Picoclaw posture profile, it emits severity-ranked findings and a summary count for local operator review.
+9 -2
View File
@@ -1,6 +1,6 @@
---
name: picoclaw-self-pen-testing
version: 0.0.2
version: 0.0.3
description: Picoclaw-only local posture-review skill focused on read-only findings and safe operator remediation guidance.
homepage: https://clawsec.prompt.security
author: prompt-security
@@ -18,6 +18,13 @@ picoclaw:
Purpose: keep Picoclaw posture-review checks isolated from the broader guardian package so moderation-sensitive checks can be versioned/published independently.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill picoclaw-self-pen-testing -a openclaw -y
```
## Release Artifact Verification
@@ -27,7 +34,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="picoclaw-self-pen-testing"
VERSION="0.0.2"
VERSION="0.0.3"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picoclaw-self-pen-testing",
"version": "0.0.2",
"version": "0.0.3",
"description": "Picoclaw-only local posture-review skill focused on read-only findings and safe operator remediation guidance.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
@@ -1,5 +1,11 @@
# Changelog
## [0.0.1-beta3] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.1-beta2] - 2026-05-13
### Security
+8 -1
View File
@@ -4,6 +4,14 @@ Baseline skill for Picoclaw runtime traffic monitoring.
This package is intentionally a spec scaffold. Builders should add the Picoclaw-specific monitor implementation here while preserving the safety contract in `SKILL.md` and `SPEC.md`.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill picoclaw-traffic-guardian -a openclaw -y
```
## Intended Capability
- detect outbound secret exfiltration in Picoclaw gateway HTTP/HTTPS traffic
@@ -15,4 +23,3 @@ This package is intentionally a spec scaffold. Builders should add the Picoclaw-
## Builder Notes
Keep runtime ownership in this skill. `picoclaw-security-guardian` should only profile and drift-check this skill's state, config, and output fingerprints.
+9 -3
View File
@@ -1,6 +1,6 @@
---
name: picoclaw-traffic-guardian
version: 0.0.1-beta2
version: 0.0.1-beta3
description: Picoclaw runtime traffic monitoring baseline for lightweight AI gateway proxy inspection, egress detection, and posture integration.
homepage: https://clawsec.prompt.security
author: prompt-security
@@ -15,6 +15,13 @@ picoclaw:
This is a baseline specification skill. It intentionally does not ship a proxy or runtime implementation yet.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill picoclaw-traffic-guardian -a openclaw -y
```
## Release Artifact Verification
@@ -24,7 +31,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="picoclaw-traffic-guardian"
VERSION="0.0.1-beta2"
VERSION="0.0.1-beta3"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
@@ -145,4 +152,3 @@ Read `SPEC.md` before implementing. Use the placeholder folders as follows:
- default blocking
- sending traffic to external services
- collecting full request/response bodies
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "picoclaw-traffic-guardian",
"version": "0.0.1-beta2",
"version": "0.0.1-beta3",
"description": "Picoclaw runtime traffic monitoring baseline for lightweight AI gateway proxy inspection, egress detection, and posture integration.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## [0.0.7] - 2026-06-10
### Changed
- Re-released skill package with updated marketplace grouping and signed release trust artifacts for Vercel-compatible skill installation.
## [0.0.6] - 2026-05-14
### Security
+8
View File
@@ -2,6 +2,14 @@
A small, dependency-free integrity guard for OpenClaw agent workspaces.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill soul-guardian -a openclaw -y
```
## Operational Notes
- Required runtime: `python3`
+10 -3
View File
@@ -1,6 +1,6 @@
---
name: soul-guardian
version: 0.0.6
version: 0.0.7
description: Drift detection + baseline integrity guard for agent workspace files with automatic alerting support
homepage: https://clawsec.prompt.security
metadata: {"openclaw":{"emoji":"👻","category":"security"}}
@@ -14,6 +14,14 @@ clawdis:
Protects your agent's core files (SOUL.md, AGENTS.md, etc.) from unauthorized changes with automatic detection, restoration, and **user alerting**.
## Vercel Skills Installation
Install with the Vercel Skills CLI for this harness:
```bash
npx skills add prompt-security/clawsec --skill soul-guardian -a openclaw -y
```
## Operational Notes
- Required runtime: `python3`
@@ -22,7 +30,6 @@ Protects your agent's core files (SOUL.md, AGENTS.md, etc.) from unauthorized ch
- Network behavior: none by default
- Trust model: any scheduling is opt-in, but restore mode intentionally overwrites drifted files
## Release Artifact Verification
For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key.
@@ -31,7 +38,7 @@ For standalone installs, verify the signed release manifest before trusting `SKI
set -euo pipefail
SKILL_NAME="soul-guardian"
VERSION="0.0.6"
VERSION="0.0.7"
REPO="prompt-security/clawsec"
TAG="${SKILL_NAME}-v${VERSION}"
BASE="https://github.com/${REPO}/releases/download/${TAG}"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "soul-guardian",
"version": "0.0.6",
"version": "0.0.7",
"description": "Drift detection and baseline integrity guard for agent workspace prompt files. Auto-restore critical files with tamper-evident audit logging.",
"author": "prompt-security",
"license": "AGPL-3.0-or-later",
+3
View File
@@ -35,6 +35,7 @@
| GitHub API | Deploy/release workflows | Discover releases, download assets, publish outputs. |
| GitHub Pages | Deploy workflow | Serve static site and mirrored artifacts. |
| ClawHub CLI/registry | Install scripts + optional publish jobs | Install and publish skills. |
| [NVIDIA SkillSpector](https://github.com/NVIDIA/SkillSpector) | Skill release workflow | Scan staged skill release payloads and produce Markdown release evidence. |
| Optional local SMTP/sendmail | `openclaw-audit-watchdog` scripts | Deliver audit reports by email. |
## Development Tools
@@ -46,6 +47,7 @@
| Bandit | `bandit -r utils/ -ll` | Python security checks. |
| Trivy | Workflow + optional local run | FS/config vulnerability scans. |
| Gitleaks | `scripts/prepare-to-push.sh` optional local run | Secret leak detection before push. |
| SkillSpector | `.github/workflows/skill-release.yml` | Release-payload scanner used for PR comments and signed release artifacts. |
## Example Snippets
```json
@@ -83,6 +85,7 @@ skips = ["B101"]
- PR validation enforces version parity between `skill.json` and `SKILL.md` frontmatter for bumped skills.
- The public skills index keeps latest discovered version per skill for UI display.
- Signed artifact manifests (`checksums.json`) are versioned per release and include file hashes and URLs.
- SkillSpector reports are generated per release payload and included in signed artifact manifests.
## Source References
- package.json
+2 -1
View File
@@ -15,6 +15,7 @@
| --- | --- |
| Skill Tag | Git tag formatted as `<skill>-v<semver>` used by release automation. |
| Release Assets | Files attached to GitHub release (zip, `skill.json`, checksums, signatures). |
| SkillSpector Report | Markdown security scan evidence generated from a staged skill release payload. |
| Catalog Index | `public/skills/index.json`, generated list consumed by web catalog. |
| Embedded Components | Capability bundle from one skill included in another (for example feed embedded in suite). |
@@ -39,7 +40,7 @@
| --- | --- |
| Poll NVD CVEs Workflow | Scheduled workflow that fetches and transforms NVD CVEs into advisories. |
| Community Advisory Workflow | Issue-label-triggered workflow that publishes approved community advisories. |
| Skill Release Workflow | Tag-triggered packaging/signing/publishing pipeline for skills. |
| Skill Release Workflow | PR and tag-triggered packaging/signing/publishing pipeline for skills. |
| Deploy Pages Workflow | Workflow that builds site assets and mirrors release/advisory artifacts. |
## Source References

Some files were not shown because too many files have changed in this diff Show More