mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
feat: add dark skills.sh catalog control plane (#3211)
Ships the fail-closed skills.sh catalog control plane validated by the bounded 500-row permanent Test gate. No production ingestion, schedule, visibility, or bulk scanning is enabled.
This commit is contained in:
@@ -32,6 +32,7 @@
|
||||
"@tanstack/react-router": "1.170.18",
|
||||
"@tanstack/react-start": "1.168.32",
|
||||
"@vercel/analytics": "2.0.1",
|
||||
"@vercel/oidc": "^3.8.0",
|
||||
"@vercel/speed-insights": "2.0.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clawhub-schema": "workspace:0.0.2",
|
||||
@@ -67,6 +68,7 @@
|
||||
"zod": "4.4.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edge-runtime/vm": "^5.0.0",
|
||||
"@faker-js/faker": "^10.5.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@react-email/ui": "^6.9.0",
|
||||
@@ -82,6 +84,7 @@
|
||||
"@typescript/native": "npm:typescript@7.0.2",
|
||||
"@vitejs/plugin-react": "6.0.3",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"convex-test": "^0.0.54",
|
||||
"jsdom": "29.1.1",
|
||||
"nitro": "3.0.260610-beta",
|
||||
"only-allow": "1.2.2",
|
||||
@@ -252,6 +255,10 @@
|
||||
|
||||
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
|
||||
|
||||
"@edge-runtime/primitives": ["@edge-runtime/primitives@6.0.0", "", {}, "sha512-FqoxaBT+prPBHBwE1WXS1ocnu/VLTQyZ6NMUBAdbP7N2hsFTTxMC/jMu2D/8GAlMQfxeuppcPuCUk/HO3fpIvA=="],
|
||||
|
||||
"@edge-runtime/vm": ["@edge-runtime/vm@5.0.0", "", { "dependencies": { "@edge-runtime/primitives": "6.0.0" } }, "sha512-NKBGBSIKUG584qrS1tyxVpX/AKJKQw5HgjYEnPLC0QsTw79JrGn+qUr8CXFb955Iy7GUdiiUv1rJ6JBGvaKb6w=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
@@ -990,6 +997,12 @@
|
||||
|
||||
"@vercel/analytics": ["@vercel/analytics@2.0.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g=="],
|
||||
|
||||
"@vercel/cli-config": ["@vercel/cli-config@0.2.0", "", { "dependencies": { "xdg-app-paths": "5", "zod": "4.1.11" } }, "sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ=="],
|
||||
|
||||
"@vercel/cli-exec": ["@vercel/cli-exec@1.0.0", "", { "dependencies": { "execa": "5.1.1" } }, "sha512-kQF8LGie/Hbdq9/psJxLE7owRTcqMQMhgybU04gCeR7cbQAr5t8OrjefDNColJv1QSSucFt4pLwRiARVmlOnug=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.8.0", "", { "dependencies": { "@vercel/cli-config": "0.2.0", "@vercel/cli-exec": "1.0.0", "jose": "^5.9.6" } }, "sha512-r00laGW6Pv778RoR6M2NxX91ycSj+PBwVo+fOb9Bif+F0IyUKt25zrvBzfEzQpeAzbqOgPZyQibEWDdDFApd+A=="],
|
||||
|
||||
"@vercel/speed-insights": ["@vercel/speed-insights@2.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "nuxt": ">= 3", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@sveltejs/kit", "next", "nuxt", "react", "svelte", "vue", "vue-router"] }, "sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.3", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg=="],
|
||||
@@ -1104,6 +1117,8 @@
|
||||
|
||||
"convex-helpers": ["convex-helpers@0.1.120", "", { "peerDependencies": { "@standard-schema/spec": "^1.0.0", "convex": "^1.32.0", "hono": "^4.0.5", "react": "^17.0.2 || ^18.0.0 || ^19.0.0", "typescript": "^5.5 || ^6.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@standard-schema/spec", "hono", "react", "typescript", "zod"], "bin": { "convex-helpers": "bin.cjs" } }, "sha512-FPqtmN/10uoxmmhVq0ViNi08AnmHKT/eU1R8+ohmPZoHfg8noZEZCltApY/ASDdhq92o0kLZETggzBvMfHJDLQ=="],
|
||||
|
||||
"convex-test": ["convex-test@0.0.54", "", { "peerDependencies": { "convex": "^1.32.0" } }, "sha512-C0v2SQcuxrELAJRNzE6fQ686XDPor7UFoC22jcoJXeCRqhLo8ZIMZ4jyUHoo1UdAL2enCb0AbiprCVpLDN8u9Q=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="],
|
||||
@@ -1112,6 +1127,8 @@
|
||||
|
||||
"cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"crossws": ["crossws@0.4.9", "", { "peerDependencies": { "srvx": ">=0.11.5" }, "optionalPeers": ["srvx"] }, "sha512-iWx+1OMSG2aOHpjyf9AESOzkwsVdS49cXM9dVrI2PDhxU5l2RIWE/KG56gk4BbAnsMoycvniJ9OnOxO9LRzHVA=="],
|
||||
|
||||
"css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
|
||||
@@ -1262,6 +1279,8 @@
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
|
||||
|
||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||
|
||||
"exsolve": ["exsolve@1.1.0", "", {}, "sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw=="],
|
||||
@@ -1294,6 +1313,8 @@
|
||||
|
||||
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||
|
||||
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
"glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
@@ -1344,6 +1365,8 @@
|
||||
|
||||
"httpxy": ["httpxy@0.5.5", "", {}, "sha512-uDjmnPyp1q4Sgzf3w+J/Fc6UqcCEj0x4Wjp7OqK5dGhNeDgpyrAmnS6ey8QWrX3SWDon2DMKf9sBa5X9+CVyMA=="],
|
||||
|
||||
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||
@@ -1370,10 +1393,14 @@
|
||||
|
||||
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||
|
||||
"isbot": ["isbot@5.1.41", "", {}, "sha512-9WFV/Vhh0FEj6CQ7MoHweEL9/vLKPjeoD2I2htbAjX7kbW7VJs3OCpWOVyd+JraNTWVU6/DRx2MZy2KaUNXHcg=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
|
||||
|
||||
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
|
||||
@@ -1492,6 +1519,8 @@
|
||||
|
||||
"mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"mermaid": ["mermaid@11.16.0", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.0", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.0", "cytoscape": "^3.33.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.14", "dayjs": "^1.11.20", "dompurify": "^3.3.3", "es-toolkit": "^1.45.1", "katex": "^0.16.45", "khroma": "^2.1.0", "marked": "^16.3.0", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" } }, "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
@@ -1556,6 +1585,8 @@
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||
|
||||
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||
@@ -1582,6 +1613,8 @@
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
|
||||
|
||||
"nypm": ["nypm@0.6.6", "", { "dependencies": { "citty": "^0.2.2", "pathe": "^2.0.3", "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q=="],
|
||||
|
||||
"oauth4webapi": ["oauth4webapi@3.8.6", "", {}, "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ=="],
|
||||
@@ -1598,7 +1631,7 @@
|
||||
|
||||
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
|
||||
|
||||
"onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="],
|
||||
|
||||
@@ -1608,6 +1641,8 @@
|
||||
|
||||
"ora": ["ora@9.4.1", "", { "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" } }, "sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw=="],
|
||||
|
||||
"os-paths": ["os-paths@4.4.0", "", {}, "sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg=="],
|
||||
|
||||
"oxc-parser": ["oxc-parser@0.120.0", "", { "dependencies": { "@oxc-project/types": "^0.120.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.120.0", "@oxc-parser/binding-android-arm64": "0.120.0", "@oxc-parser/binding-darwin-arm64": "0.120.0", "@oxc-parser/binding-darwin-x64": "0.120.0", "@oxc-parser/binding-freebsd-x64": "0.120.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.120.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.120.0", "@oxc-parser/binding-linux-arm64-gnu": "0.120.0", "@oxc-parser/binding-linux-arm64-musl": "0.120.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.120.0", "@oxc-parser/binding-linux-riscv64-musl": "0.120.0", "@oxc-parser/binding-linux-s390x-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-gnu": "0.120.0", "@oxc-parser/binding-linux-x64-musl": "0.120.0", "@oxc-parser/binding-openharmony-arm64": "0.120.0", "@oxc-parser/binding-wasm32-wasi": "0.120.0", "@oxc-parser/binding-win32-arm64-msvc": "0.120.0", "@oxc-parser/binding-win32-ia32-msvc": "0.120.0", "@oxc-parser/binding-win32-x64-msvc": "0.120.0" } }, "sha512-WyPWZlcIm+Fkte63FGfgFB8mAAk33aH9h5N9lphXVOHSXEBFFsmYdOBedVKly363aWABjZdaj/m9lBfEY4wt+w=="],
|
||||
|
||||
"oxfmt": ["oxfmt@0.59.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.59.0", "@oxfmt/binding-android-arm64": "0.59.0", "@oxfmt/binding-darwin-arm64": "0.59.0", "@oxfmt/binding-darwin-x64": "0.59.0", "@oxfmt/binding-freebsd-x64": "0.59.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.59.0", "@oxfmt/binding-linux-arm-musleabihf": "0.59.0", "@oxfmt/binding-linux-arm64-gnu": "0.59.0", "@oxfmt/binding-linux-arm64-musl": "0.59.0", "@oxfmt/binding-linux-ppc64-gnu": "0.59.0", "@oxfmt/binding-linux-riscv64-gnu": "0.59.0", "@oxfmt/binding-linux-riscv64-musl": "0.59.0", "@oxfmt/binding-linux-s390x-gnu": "0.59.0", "@oxfmt/binding-linux-x64-gnu": "0.59.0", "@oxfmt/binding-linux-x64-musl": "0.59.0", "@oxfmt/binding-openharmony-arm64": "0.59.0", "@oxfmt/binding-win32-arm64-msvc": "0.59.0", "@oxfmt/binding-win32-ia32-msvc": "0.59.0", "@oxfmt/binding-win32-x64-msvc": "0.59.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-Xqk6cPZS1yMvVa7OAuenaDZUsgMDutvvbZ9/L5gSvAfW64+WN4HVhgipLj5rVERbYQt8fLs9TopyZ1rU1XEG/w=="],
|
||||
@@ -1628,6 +1663,8 @@
|
||||
|
||||
"path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
@@ -1756,13 +1793,17 @@
|
||||
|
||||
"sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="],
|
||||
|
||||
"shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
@@ -1804,6 +1845,8 @@
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
|
||||
|
||||
"stubborn-fs": ["stubborn-fs@2.0.0", "", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
|
||||
|
||||
"stubborn-utils": ["stubborn-utils@1.0.2", "", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
|
||||
@@ -1928,12 +1971,18 @@
|
||||
|
||||
"when-exit": ["when-exit@2.1.5", "", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||
|
||||
"xdg-app-paths": ["xdg-app-paths@5.5.1", "", { "dependencies": { "os-paths": "^4.0.1", "xdg-portable": "^7.2.0" } }, "sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ=="],
|
||||
|
||||
"xdg-portable": ["xdg-portable@7.3.0", "", { "dependencies": { "os-paths": "^4.0.1" } }, "sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw=="],
|
||||
|
||||
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
|
||||
|
||||
"xmlbuilder2": ["xmlbuilder2@4.0.3", "", { "dependencies": { "@oozcitak/dom": "^2.0.2", "@oozcitak/infra": "^2.0.2", "@oozcitak/util": "^10.0.0", "js-yaml": "^4.1.1" } }, "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA=="],
|
||||
@@ -2010,6 +2059,10 @@
|
||||
|
||||
"@types/ws/@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="],
|
||||
|
||||
"@vercel/cli-config/zod": ["zod@4.1.11", "", {}, "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg=="],
|
||||
|
||||
"@vercel/oidc/jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="],
|
||||
|
||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||
@@ -2064,6 +2117,10 @@
|
||||
|
||||
"react-email/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
|
||||
|
||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||
|
||||
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"rolldown/@oxc-project/types": ["@oxc-project/types@0.134.0", "", {}, "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ=="],
|
||||
|
||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||
|
||||
Vendored
+8
@@ -41,6 +41,7 @@ import type * as httpApiV1_packagesV1 from "../httpApiV1/packagesV1.js";
|
||||
import type * as httpApiV1_promotionsV1 from "../httpApiV1/promotionsV1.js";
|
||||
import type * as httpApiV1_publishersV1 from "../httpApiV1/publishersV1.js";
|
||||
import type * as httpApiV1_shared from "../httpApiV1/shared.js";
|
||||
import type * as httpApiV1_skillsShCatalogV1 from "../httpApiV1/skillsShCatalogV1.js";
|
||||
import type * as httpApiV1_skillsV1 from "../httpApiV1/skillsV1.js";
|
||||
import type * as httpApiV1_starsV1 from "../httpApiV1/starsV1.js";
|
||||
import type * as httpApiV1_transfersV1 from "../httpApiV1/transfersV1.js";
|
||||
@@ -129,6 +130,8 @@ import type * as lib_skillTags from "../lib/skillTags.js";
|
||||
import type * as lib_skillZip from "../lib/skillZip.js";
|
||||
import type * as lib_skills_index from "../lib/skills/index.js";
|
||||
import type * as lib_skills_slugResolution from "../lib/skills/slugResolution.js";
|
||||
import type * as lib_skillsShCatalogEnvironment from "../lib/skillsShCatalogEnvironment.js";
|
||||
import type * as lib_skillsShCatalogFixtures from "../lib/skillsShCatalogFixtures.js";
|
||||
import type * as lib_staticPublishScan from "../lib/staticPublishScan.js";
|
||||
import type * as lib_testSeed from "../lib/testSeed.js";
|
||||
import type * as lib_tokens from "../lib/tokens.js";
|
||||
@@ -163,6 +166,7 @@ import type * as skillCards from "../skillCards.js";
|
||||
import type * as skillStatEvents from "../skillStatEvents.js";
|
||||
import type * as skillTransfers from "../skillTransfers.js";
|
||||
import type * as skills from "../skills.js";
|
||||
import type * as skillsShCatalog from "../skillsShCatalog.js";
|
||||
import type * as stars from "../stars.js";
|
||||
import type * as statsMaintenance from "../statsMaintenance.js";
|
||||
import type * as telemetry from "../telemetry.js";
|
||||
@@ -212,6 +216,7 @@ declare const fullApi: ApiFromModules<{
|
||||
"httpApiV1/promotionsV1": typeof httpApiV1_promotionsV1;
|
||||
"httpApiV1/publishersV1": typeof httpApiV1_publishersV1;
|
||||
"httpApiV1/shared": typeof httpApiV1_shared;
|
||||
"httpApiV1/skillsShCatalogV1": typeof httpApiV1_skillsShCatalogV1;
|
||||
"httpApiV1/skillsV1": typeof httpApiV1_skillsV1;
|
||||
"httpApiV1/starsV1": typeof httpApiV1_starsV1;
|
||||
"httpApiV1/transfersV1": typeof httpApiV1_transfersV1;
|
||||
@@ -300,6 +305,8 @@ declare const fullApi: ApiFromModules<{
|
||||
"lib/skillZip": typeof lib_skillZip;
|
||||
"lib/skills/index": typeof lib_skills_index;
|
||||
"lib/skills/slugResolution": typeof lib_skills_slugResolution;
|
||||
"lib/skillsShCatalogEnvironment": typeof lib_skillsShCatalogEnvironment;
|
||||
"lib/skillsShCatalogFixtures": typeof lib_skillsShCatalogFixtures;
|
||||
"lib/staticPublishScan": typeof lib_staticPublishScan;
|
||||
"lib/testSeed": typeof lib_testSeed;
|
||||
"lib/tokens": typeof lib_tokens;
|
||||
@@ -334,6 +341,7 @@ declare const fullApi: ApiFromModules<{
|
||||
skillStatEvents: typeof skillStatEvents;
|
||||
skillTransfers: typeof skillTransfers;
|
||||
skills: typeof skills;
|
||||
skillsShCatalog: typeof skillsShCatalog;
|
||||
stars: typeof stars;
|
||||
statsMaintenance: typeof statsMaintenance;
|
||||
telemetry: typeof telemetry;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,7 @@ import {
|
||||
verifyDocsSessionV1Http,
|
||||
whoamiV1Http,
|
||||
contentRightsV1Http,
|
||||
skillsShCatalogTestV1Http,
|
||||
} from "./httpApiV1";
|
||||
import { preflightHandler } from "./httpPreflight";
|
||||
import { installRateLimitedRoutes } from "./lib/httpRouteRateLimit";
|
||||
@@ -324,6 +325,18 @@ http.route({
|
||||
handler: contentRightsV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
path: "/api/v1/operator/skills-sh/catalog-test",
|
||||
method: "GET",
|
||||
handler: skillsShCatalogTestV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
path: "/api/v1/operator/skills-sh/catalog-test",
|
||||
method: "POST",
|
||||
handler: skillsShCatalogTestV1Http,
|
||||
});
|
||||
|
||||
http.route({
|
||||
pathPrefix: `${ApiRoutes.contentRights}/`,
|
||||
method: "POST",
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
promotionsPostRouterV1Handler,
|
||||
} from "./httpApiV1/promotionsV1";
|
||||
import { createPublisherV1Handler } from "./httpApiV1/publishersV1";
|
||||
import { skillsShCatalogTestV1Handler } from "./httpApiV1/skillsShCatalogV1";
|
||||
import {
|
||||
exportSkillsV1Handler,
|
||||
listSkillsV1Handler,
|
||||
@@ -67,6 +68,7 @@ export const listBundlePluginsV1Http = httpAction(listBundlePluginsV1Handler);
|
||||
export const verifyDocsSessionV1Http = httpAction(verifyDocsSessionV1Handler);
|
||||
export const createPublisherV1Http = httpAction(createPublisherV1Handler);
|
||||
export const contentRightsV1Http = httpAction(contentRightsV1Handler);
|
||||
export const skillsShCatalogTestV1Http = httpAction(skillsShCatalogTestV1Handler);
|
||||
export const catalogFeedV1Http = httpAction(catalogFeedV1Handler);
|
||||
export const catalogSkillsFeedV1Http = httpAction(catalogSkillsFeedV1Handler);
|
||||
export const promotionsFeedV1Http = httpAction(promotionsFeedV1Handler);
|
||||
@@ -116,6 +118,7 @@ export const __handlers = {
|
||||
verifyDocsSessionV1Handler,
|
||||
createPublisherV1Handler,
|
||||
contentRightsV1Handler,
|
||||
skillsShCatalogTestV1Handler,
|
||||
catalogFeedV1Handler,
|
||||
catalogSkillsFeedV1Handler,
|
||||
searchSkillsV1Handler,
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
/* @vitest-environment node */
|
||||
import { createHash } from "node:crypto";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("../lib/httpRateLimit", () => ({
|
||||
applyRateLimit: vi.fn(async () => ({ ok: true, headers: {} })),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/githubAuth", () => ({
|
||||
buildGitHubApiHeaders: vi.fn(async () => ({ Authorization: "Bearer placeholder" })),
|
||||
}));
|
||||
|
||||
vi.mock("./shared", async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import("./shared")>();
|
||||
return {
|
||||
...original,
|
||||
requireApiTokenUserOrResponse: vi.fn(),
|
||||
requireAdminOrResponse: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { requireAdminOrResponse, requireApiTokenUserOrResponse } = await import("./shared");
|
||||
const { buildGitHubApiHeaders } = await import("../lib/githubAuth");
|
||||
const { skillsShCatalogTestV1Handler } = await import("./skillsShCatalogV1");
|
||||
|
||||
function sha256(value: string) {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function artifact(externalId: string, content: string) {
|
||||
const fileHash = sha256(content);
|
||||
return {
|
||||
externalId,
|
||||
artifactContentHash: sha256(`SKILL.md\0${fileHash}\n`),
|
||||
files: [
|
||||
{
|
||||
path: "SKILL.md",
|
||||
contentBase64: Buffer.from(content).toString("base64"),
|
||||
sha256: fileHash,
|
||||
contentType: "text/markdown",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("skills.sh catalog Test HTTP API", () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(requireApiTokenUserOrResponse).mockResolvedValue({
|
||||
ok: true,
|
||||
user: { handle: "catalog-operator" },
|
||||
userId: "users:operator",
|
||||
} as never);
|
||||
vi.mocked(requireAdminOrResponse).mockReturnValue({ ok: true } as never);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("deletes uploaded files for admissions the mutation skips", async () => {
|
||||
const storedIds = ["storage:linked", "storage:skipped"];
|
||||
const store = vi.fn(async () => storedIds.shift()!);
|
||||
const deleteStorage = vi.fn(async () => undefined);
|
||||
const runAction = vi.fn(async (_ref, args: Record<string, unknown>) => {
|
||||
expect(args).toMatchObject({
|
||||
externalIds: ["nvidia/skills/aiq-deploy", "nvidia/skills/aiq-toolkit"],
|
||||
});
|
||||
return {
|
||||
requested: 2,
|
||||
admitted: 1,
|
||||
skipped: 1,
|
||||
admittedExternalIds: ["nvidia/skills/aiq-deploy"],
|
||||
};
|
||||
});
|
||||
const ctx = {
|
||||
runQuery: vi.fn(async () => ({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})),
|
||||
runAction,
|
||||
storage: {
|
||||
store,
|
||||
delete: deleteStorage,
|
||||
},
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "admit",
|
||||
runId: "skillsShCatalogRuns:test",
|
||||
externalIds: ["nvidia/skills/aiq-deploy", "nvidia/skills/aiq-toolkit"],
|
||||
artifacts: [
|
||||
artifact("nvidia/skills/aiq-deploy", "# Linked"),
|
||||
artifact("nvidia/skills/aiq-toolkit", "# Skipped"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({ admitted: 1, skipped: 1 });
|
||||
expect(store).toHaveBeenCalledTimes(2);
|
||||
expect(deleteStorage).toHaveBeenCalledTimes(1);
|
||||
expect(deleteStorage).toHaveBeenCalledWith("storage:skipped");
|
||||
});
|
||||
|
||||
it("does not report a committed admission as failed when skipped-file cleanup fails", async () => {
|
||||
const storedIds = ["storage:linked", "storage:skipped"];
|
||||
const store = vi.fn(async () => storedIds.shift()!);
|
||||
const deleteStorage = vi.fn(async () => {
|
||||
throw new Error("temporary storage cleanup outage");
|
||||
});
|
||||
const ctx = {
|
||||
runQuery: vi.fn(async () => ({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})),
|
||||
runAction: vi.fn(async () => ({
|
||||
requested: 2,
|
||||
admitted: 1,
|
||||
skipped: 1,
|
||||
admittedExternalIds: ["nvidia/skills/aiq-deploy"],
|
||||
})),
|
||||
storage: {
|
||||
store,
|
||||
delete: deleteStorage,
|
||||
},
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "admit",
|
||||
runId: "skillsShCatalogRuns:test",
|
||||
externalIds: ["nvidia/skills/aiq-deploy", "nvidia/skills/aiq-toolkit"],
|
||||
artifacts: [
|
||||
artifact("nvidia/skills/aiq-deploy", "# Linked"),
|
||||
artifact("nvidia/skills/aiq-toolkit", "# Skipped"),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({ admitted: 1, skipped: 1 });
|
||||
expect(deleteStorage).toHaveBeenCalledWith("storage:skipped");
|
||||
});
|
||||
|
||||
it("reuses authenticated staging-live owner ids without a GitHub fetch", async () => {
|
||||
const githubFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", githubFetch);
|
||||
const ctx = {
|
||||
runQuery: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
missingOwners: [],
|
||||
}),
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "resolve-owners",
|
||||
owners: ["nvidia", "anthropics"],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toEqual({
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
fetches: 0,
|
||||
reused: 2,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
});
|
||||
expect(githubFetch).not.toHaveBeenCalled();
|
||||
expect(buildGitHubApiHeaders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fetches only owners missing from authenticated staging-live state", async () => {
|
||||
const githubFetch = vi.fn(async (url: string, init?: RequestInit) => {
|
||||
expect(init?.headers).toMatchObject({ Authorization: "Bearer placeholder" });
|
||||
const owner = url.split("/").at(-1)!;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: owner === "nvidia" ? 1_728_152 : 76_263_028,
|
||||
login: owner,
|
||||
}),
|
||||
);
|
||||
});
|
||||
vi.stubGlobal("fetch", githubFetch);
|
||||
const ctx = {
|
||||
runQuery: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
owners: [{ owner: "nvidia", login: "nvidia", id: 1_728_152 }],
|
||||
missingOwners: ["anthropics"],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
provenance: "stored-authenticated-staging-live-assignment-check",
|
||||
checked: 1,
|
||||
}),
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "resolve-owners",
|
||||
owners: ["nvidia", "anthropics"],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body).toEqual({
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live+live-github",
|
||||
fetches: 1,
|
||||
reused: 1,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
});
|
||||
expect(githubFetch).toHaveBeenCalledTimes(1);
|
||||
expect(buildGitHubApiHeaders).toHaveBeenCalledWith({
|
||||
userAgent: "clawhub/skills-sh-catalog-test",
|
||||
allowAnonymous: false,
|
||||
useGitHubApp: false,
|
||||
});
|
||||
expect(JSON.stringify(body)).not.toContain("Bearer placeholder");
|
||||
});
|
||||
|
||||
it("reports only non-secret HTTP status when authenticated owner lookup fails", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response("token=secret-response-body", { status: 404 })),
|
||||
);
|
||||
const ctx = {
|
||||
runQuery: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
owners: [],
|
||||
missingOwners: ["neondatabase"],
|
||||
}),
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "resolve-owners",
|
||||
owners: ["neondatabase"],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body).toBe("Authenticated GitHub owner lookup failed with HTTP 404: neondatabase");
|
||||
expect(body).not.toContain("secret-response-body");
|
||||
expect(body).not.toContain("Bearer placeholder");
|
||||
});
|
||||
|
||||
it("fails closed when a new owner lacks authenticated GitHub access", async () => {
|
||||
vi.mocked(buildGitHubApiHeaders).mockRejectedValueOnce(
|
||||
new Error("GitHub API authentication is not configured"),
|
||||
);
|
||||
const githubFetch = vi.fn();
|
||||
vi.stubGlobal("fetch", githubFetch);
|
||||
const ctx = {
|
||||
runQuery: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
owners: [],
|
||||
missingOwners: ["new-owner"],
|
||||
}),
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "resolve-owners",
|
||||
owners: ["new-owner"],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.text()).toBe("GitHub API authentication is not configured");
|
||||
expect(githubFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects a fetched owner id already assigned to another login", async () => {
|
||||
const githubFetch = vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ id: 1_728_152, login: "renamed-nvidia" }), {
|
||||
status: 200,
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal("fetch", githubFetch);
|
||||
const ctx = {
|
||||
runQuery: vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
environment: "test",
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "test-sha",
|
||||
control: {},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
owners: [],
|
||||
missingOwners: ["renamed-nvidia"],
|
||||
})
|
||||
.mockRejectedValueOnce(
|
||||
new Error("Authenticated GitHub owner id 1728152 is already assigned to another owner"),
|
||||
),
|
||||
} as never;
|
||||
const request = new Request("https://academic-chihuahua-392.convex.site/api/v1/ops", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
operation: "resolve-owners",
|
||||
owners: ["renamed-nvidia"],
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await skillsShCatalogTestV1Handler(ctx, request);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.text()).toBe(
|
||||
"Authenticated GitHub owner id 1728152 is already assigned to another owner",
|
||||
);
|
||||
expect(githubFetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
import { internal } from "../_generated/api";
|
||||
import type { Id } from "../_generated/dataModel";
|
||||
import type { ActionCtx } from "../_generated/server";
|
||||
import { buildGitHubApiHeaders } from "../lib/githubAuth";
|
||||
import { applyRateLimit } from "../lib/httpRateLimit";
|
||||
import { json, requireAdminOrResponse, requireApiTokenUserOrResponse, text } from "./shared";
|
||||
|
||||
const internalRefs = internal as unknown as {
|
||||
skillsShCatalog: {
|
||||
admitRealScansInternal: unknown;
|
||||
assertFreshGitHubOwnerAssignmentsInternal: unknown;
|
||||
getStagingLiveControlInternal: unknown;
|
||||
processStagingLiveBatchInternal: unknown;
|
||||
resolveKnownGitHubOwnersInternal: unknown;
|
||||
startStagingLiveRunInternal: unknown;
|
||||
};
|
||||
};
|
||||
const MAX_GITHUB_OWNER_RESOLUTIONS = 500;
|
||||
const GITHUB_OWNER_RESOLUTION_CONCURRENCY = 8;
|
||||
|
||||
async function runMutationRef<T>(
|
||||
ctx: ActionCtx,
|
||||
ref: unknown,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
return (await ctx.runMutation(ref as never, args as never)) as T;
|
||||
}
|
||||
|
||||
async function runActionRef<T>(ctx: ActionCtx, ref: unknown, args: Record<string, unknown>) {
|
||||
return (await ctx.runAction(ref as never, args as never)) as T;
|
||||
}
|
||||
|
||||
async function runQueryRef<T>(
|
||||
ctx: ActionCtx,
|
||||
ref: unknown,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<T> {
|
||||
return (await ctx.runQuery(ref as never, args as never)) as T;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
function requireString(record: Record<string, unknown>, key: string) {
|
||||
const value = record[key];
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`${key} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function requireNumber(record: Record<string, unknown>, key: string) {
|
||||
const value = record[key];
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) throw new Error(`${key} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
async function sha256Hex(bytes: Uint8Array) {
|
||||
const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
function decodeBase64(value: string) {
|
||||
const binary = atob(value);
|
||||
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
||||
}
|
||||
|
||||
function normalizeGitHubOwners(ownersValue: unknown) {
|
||||
if (!Array.isArray(ownersValue)) throw new Error("owners is required");
|
||||
const owners = Array.from(
|
||||
new Set(
|
||||
ownersValue.map((owner) => {
|
||||
if (typeof owner !== "string" || !owner.trim()) {
|
||||
throw new Error("owner must be a non-empty string");
|
||||
}
|
||||
return owner.trim().toLowerCase();
|
||||
}),
|
||||
),
|
||||
).sort();
|
||||
if (owners.length < 1 || owners.length > MAX_GITHUB_OWNER_RESOLUTIONS) {
|
||||
throw new Error(`owners must contain between 1 and ${MAX_GITHUB_OWNER_RESOLUTIONS} entries`);
|
||||
}
|
||||
return owners;
|
||||
}
|
||||
|
||||
async function fetchAuthenticatedGitHubOwners(owners: string[]) {
|
||||
if (owners.length === 0) return [];
|
||||
const headers = await buildGitHubApiHeaders({
|
||||
userAgent: "clawhub/skills-sh-catalog-test",
|
||||
allowAnonymous: false,
|
||||
useGitHubApp: false,
|
||||
});
|
||||
const resolved: Array<{ owner: string; id: number; login: string }> = [];
|
||||
for (let offset = 0; offset < owners.length; offset += GITHUB_OWNER_RESOLUTION_CONCURRENCY) {
|
||||
const batch = owners.slice(offset, offset + GITHUB_OWNER_RESOLUTION_CONCURRENCY);
|
||||
resolved.push(
|
||||
...(await Promise.all(
|
||||
batch.map(async (owner) => {
|
||||
const response = await fetch(
|
||||
`https://api.github.com/users/${encodeURIComponent(owner)}`,
|
||||
{
|
||||
headers,
|
||||
},
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Authenticated GitHub owner lookup failed with HTTP ${response.status}: ${owner}`,
|
||||
);
|
||||
}
|
||||
const payload = (await response.json()) as { id?: unknown; login?: unknown };
|
||||
const id = typeof payload.id === "number" ? payload.id : Number.NaN;
|
||||
const login = typeof payload.login === "string" ? payload.login.trim().toLowerCase() : "";
|
||||
if (!Number.isSafeInteger(id) || id <= 0 || login !== owner) {
|
||||
throw new Error(
|
||||
`Authenticated GitHub owner lookup returned invalid identity: ${owner}`,
|
||||
);
|
||||
}
|
||||
return { owner, id, login };
|
||||
}),
|
||||
)),
|
||||
);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async function resolveAuthenticatedGitHubOwners(ctx: ActionCtx, ownersValue: unknown) {
|
||||
const owners = normalizeGitHubOwners(ownersValue);
|
||||
const known = await runQueryRef<{
|
||||
provenance: "stored-authenticated-staging-live";
|
||||
owners: Array<{ owner: string; id: number; login: string }>;
|
||||
missingOwners: string[];
|
||||
}>(ctx, internalRefs.skillsShCatalog.resolveKnownGitHubOwnersInternal, { owners });
|
||||
const fetched = await fetchAuthenticatedGitHubOwners(known.missingOwners);
|
||||
if (fetched.length > 0) {
|
||||
await runQueryRef(ctx, internalRefs.skillsShCatalog.assertFreshGitHubOwnerAssignmentsInternal, {
|
||||
owners: fetched.map(({ owner, id }) => ({ owner, id })),
|
||||
});
|
||||
}
|
||||
const resolved = [...known.owners, ...fetched].sort((left, right) =>
|
||||
left.owner.localeCompare(right.owner),
|
||||
);
|
||||
if (resolved.length !== owners.length) {
|
||||
throw new Error("GitHub owner resolution did not return complete coverage");
|
||||
}
|
||||
return {
|
||||
authentication: "clawhub-github-authenticated" as const,
|
||||
provenance:
|
||||
known.owners.length === 0
|
||||
? ("live-github" as const)
|
||||
: fetched.length === 0
|
||||
? ("stored-authenticated-staging-live" as const)
|
||||
: ("stored-authenticated-staging-live+live-github" as const),
|
||||
fetches: fetched.length,
|
||||
reused: known.owners.length,
|
||||
owners: resolved,
|
||||
};
|
||||
}
|
||||
|
||||
async function storeArtifactFiles(
|
||||
ctx: ActionCtx,
|
||||
artifactsValue: unknown,
|
||||
): Promise<{
|
||||
artifacts: Array<{
|
||||
externalId: string;
|
||||
artifactContentHash: string;
|
||||
files: Array<{
|
||||
path: string;
|
||||
size: number;
|
||||
storageId: Id<"_storage">;
|
||||
sha256: string;
|
||||
contentType?: string;
|
||||
}>;
|
||||
}>;
|
||||
storageIds: Id<"_storage">[];
|
||||
}> {
|
||||
if (!Array.isArray(artifactsValue) || artifactsValue.length < 1 || artifactsValue.length > 10) {
|
||||
throw new Error("artifacts must contain between 1 and 10 entries");
|
||||
}
|
||||
const storageIds: Id<"_storage">[] = [];
|
||||
const artifacts = [];
|
||||
const externalIds = new Set<string>();
|
||||
try {
|
||||
for (const artifactValue of artifactsValue) {
|
||||
const artifact = asRecord(artifactValue);
|
||||
if (!artifact) throw new Error("artifact must be an object");
|
||||
const externalId = requireString(artifact, "externalId").trim().toLowerCase();
|
||||
if (externalIds.has(externalId)) throw new Error(`duplicate artifact: ${externalId}`);
|
||||
externalIds.add(externalId);
|
||||
const filesValue = artifact.files;
|
||||
if (!Array.isArray(filesValue) || filesValue.length < 1 || filesValue.length > 100) {
|
||||
throw new Error("artifact files must contain between 1 and 100 entries");
|
||||
}
|
||||
const files = [];
|
||||
const filePaths = new Set<string>();
|
||||
for (const fileValue of filesValue) {
|
||||
const file = asRecord(fileValue);
|
||||
if (!file) throw new Error("artifact file must be an object");
|
||||
const path = requireString(file, "path");
|
||||
if (filePaths.has(path)) throw new Error(`duplicate artifact file path: ${path}`);
|
||||
filePaths.add(path);
|
||||
const contentBase64 = requireString(file, "contentBase64");
|
||||
const bytes = decodeBase64(contentBase64);
|
||||
const declaredSha256 = requireString(file, "sha256").toLowerCase();
|
||||
if ((await sha256Hex(bytes)) !== declaredSha256) {
|
||||
throw new Error(`artifact file hash mismatch: ${path}`);
|
||||
}
|
||||
const contentType =
|
||||
typeof file.contentType === "string" && file.contentType.trim()
|
||||
? file.contentType.trim()
|
||||
: undefined;
|
||||
const storageId = await ctx.storage.store(
|
||||
new Blob([bytes], { type: contentType ?? "application/octet-stream" }),
|
||||
);
|
||||
storageIds.push(storageId);
|
||||
files.push({
|
||||
path,
|
||||
size: bytes.byteLength,
|
||||
storageId,
|
||||
sha256: declaredSha256,
|
||||
...(contentType ? { contentType } : {}),
|
||||
});
|
||||
}
|
||||
files.sort((left, right) => left.path.localeCompare(right.path));
|
||||
const artifactContentHash = requireString(artifact, "artifactContentHash").toLowerCase();
|
||||
const manifest = files.map((file) => `${file.path}\0${file.sha256}\n`).join("");
|
||||
const computedArtifactHash = await sha256Hex(new TextEncoder().encode(manifest));
|
||||
if (computedArtifactHash !== artifactContentHash) {
|
||||
throw new Error(`artifact manifest hash mismatch: ${externalId}`);
|
||||
}
|
||||
artifacts.push({
|
||||
externalId,
|
||||
artifactContentHash,
|
||||
files,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await Promise.allSettled(
|
||||
storageIds.map(async (storageId) => await ctx.storage.delete(storageId)),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
return { artifacts, storageIds };
|
||||
}
|
||||
|
||||
export async function skillsShCatalogTestV1Handler(ctx: ActionCtx, request: Request) {
|
||||
const rate = await applyRateLimit(ctx, request, request.method === "GET" ? "read" : "write");
|
||||
if (!rate.ok) return rate.response;
|
||||
const auth = await requireApiTokenUserOrResponse(ctx, request, rate.headers);
|
||||
if (!auth.ok) return auth.response;
|
||||
const admin = requireAdminOrResponse(auth.user, rate.headers);
|
||||
if (!admin.ok) return admin.response;
|
||||
|
||||
try {
|
||||
const staging = await runQueryRef<{
|
||||
environment: "test";
|
||||
deploymentName: string | null;
|
||||
buildSha: string | null;
|
||||
control: Record<string, unknown>;
|
||||
}>(ctx, internalRefs.skillsShCatalog.getStagingLiveControlInternal, {});
|
||||
if (request.method === "GET") return json(staging, 200, rate.headers);
|
||||
if (request.method !== "POST") return text("Not found", 404, rate.headers);
|
||||
|
||||
const body = asRecord(await request.json());
|
||||
if (!body) return text("Invalid JSON", 400, rate.headers);
|
||||
const operation = requireString(body, "operation");
|
||||
if (operation === "resolve-owners") {
|
||||
return json(await resolveAuthenticatedGitHubOwners(ctx, body.owners), 200, rate.headers);
|
||||
}
|
||||
if (operation === "start") {
|
||||
const result = await runMutationRef(
|
||||
ctx,
|
||||
internalRefs.skillsShCatalog.startStagingLiveRunInternal,
|
||||
{
|
||||
actor: auth.user.handle,
|
||||
reason: requireString(body, "reason"),
|
||||
snapshotId: requireString(body, "snapshotId"),
|
||||
sourceCapturedAt: requireString(body, "sourceCapturedAt"),
|
||||
snapshotCaptureFetches: requireNumber(body, "snapshotCaptureFetches"),
|
||||
fixtureLength: requireNumber(body, "fixtureLength"),
|
||||
},
|
||||
);
|
||||
return json(result, 200, rate.headers);
|
||||
}
|
||||
if (operation === "batch") {
|
||||
if (!Array.isArray(body.rows)) throw new Error("rows is required");
|
||||
const result = await runMutationRef(
|
||||
ctx,
|
||||
internalRefs.skillsShCatalog.processStagingLiveBatchInternal,
|
||||
{
|
||||
runId: requireString(body, "runId"),
|
||||
cursor: requireNumber(body, "cursor"),
|
||||
rows: body.rows,
|
||||
},
|
||||
);
|
||||
return json(result, 200, rate.headers);
|
||||
}
|
||||
if (operation === "admit") {
|
||||
if (!Array.isArray(body.externalIds)) throw new Error("externalIds is required");
|
||||
const stored = await storeArtifactFiles(ctx, body.artifacts);
|
||||
let result: {
|
||||
admittedExternalIds: string[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
try {
|
||||
result = await runActionRef(ctx, internalRefs.skillsShCatalog.admitRealScansInternal, {
|
||||
runId: requireString(body, "runId"),
|
||||
externalIds: body.externalIds,
|
||||
actorUserId: auth.userId,
|
||||
artifacts: stored.artifacts,
|
||||
});
|
||||
} catch (error) {
|
||||
await Promise.allSettled(
|
||||
stored.storageIds.map(async (storageId) => await ctx.storage.delete(storageId)),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
const admittedExternalIds = new Set(result.admittedExternalIds);
|
||||
const unlinkedStorageIds = stored.artifacts
|
||||
.filter((artifact) => !admittedExternalIds.has(artifact.externalId))
|
||||
.flatMap((artifact) => artifact.files.map((file) => file.storageId));
|
||||
await Promise.allSettled(
|
||||
unlinkedStorageIds.map(async (storageId) => await ctx.storage.delete(storageId)),
|
||||
);
|
||||
return json(result, 200, rate.headers);
|
||||
}
|
||||
return text("Unknown operation", 400, rate.headers);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "skills.sh Test operation failed";
|
||||
const unavailable =
|
||||
message.includes("permanent Test") || message.includes("available only in permanent Test");
|
||||
return text(unavailable ? "Not found" : message, unavailable ? 404 : 400, rate.headers);
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,12 @@ describe("retention policies", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps skills.sh scan attempts as durable exact-hash deduplication records", () => {
|
||||
expect(getRetentionPolicy("skillsShCatalogScanAttempts")).toMatchObject({
|
||||
classification: "permanent",
|
||||
});
|
||||
});
|
||||
|
||||
it("documents publisher abuse signals as durable review evidence", () => {
|
||||
expect(getRetentionPolicy("publisherAbuseSignals")).toMatchObject({
|
||||
classification: "permanent",
|
||||
|
||||
@@ -215,6 +215,12 @@ export const RETENTION_POLICIES = {
|
||||
promotions: permanent("Curated promotional offers; ended records stay for launch-page history."),
|
||||
auditLogs: permanent("Audit logs are durable compliance/security history."),
|
||||
systemSettings: permanent("Durable operator-controlled system settings."),
|
||||
skillsShCatalogControls: permanent("Durable skills.sh catalog operator controls."),
|
||||
skillsShCatalogRuns: permanent("Skills.sh catalog run, cursor, and rollback audit history."),
|
||||
skillsShCatalogEntries: permanent("Normalized unclaimed skills.sh catalog identities."),
|
||||
skillsShCatalogScanAttempts: permanent(
|
||||
"Exact-hash skills.sh scan attempts are durable audit and deduplication history.",
|
||||
),
|
||||
publisherAbuseScoreRuns: permanent("Abuse scoring run history."),
|
||||
publisherAbuseTemporalScanSamples: ephemeral(
|
||||
"Exact temporal percentile samples are temporary scan working state.",
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/* @vitest-environment node */
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getSkillsShFixtureEnvironmentPolicy } from "./skillsShCatalogEnvironment";
|
||||
|
||||
describe("skills.sh fixture environment policy", () => {
|
||||
it("allows only local development or the exact cron-disabled Test deployment", () => {
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CONVEX_CLOUD_URL: "http://127.0.0.1:3210",
|
||||
}),
|
||||
).toEqual({ allowed: true, environment: "local" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392",
|
||||
CLAWHUB_DISABLE_CRONS: "1",
|
||||
CLAWHUB_ENV: "test",
|
||||
CONVEX_CLOUD_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
}),
|
||||
).toEqual({ allowed: true, environment: "test" });
|
||||
});
|
||||
|
||||
it("rejects previews, production, and incomplete Test markers", () => {
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_PREVIEW: "1",
|
||||
CONVEX_DEPLOYMENT: "anonymous:clawhub",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "preview" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392",
|
||||
CLAWHUB_ENV: "test",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "test" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "wry-manatee-359",
|
||||
CLAWHUB_DISABLE_CRONS: "1",
|
||||
CLAWHUB_ENV: "test",
|
||||
CONVEX_CLOUD_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "test" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CONVEX_DEPLOYMENT: "prod:wry-manatee-359",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "production" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "wry-manatee-359",
|
||||
CONVEX_CLOUD_URL: "http://127.0.0.1:3210",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "production" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392",
|
||||
CLAWHUB_DISABLE_CRONS: "1",
|
||||
CLAWHUB_ENV: "test",
|
||||
CONVEX_DEPLOYMENT: "prod:wry-manatee-359",
|
||||
CONVEX_CLOUD_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "production" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392",
|
||||
CLAWHUB_DISABLE_CRONS: "1",
|
||||
CLAWHUB_ENV: "test",
|
||||
CONVEX_CLOUD_URL: "https://preview-project.convex.cloud",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "test" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392",
|
||||
CLAWHUB_DISABLE_CRONS: "1",
|
||||
CLAWHUB_ENV: "test",
|
||||
CLAWHUB_PREVIEW: "1",
|
||||
CONVEX_CLOUD_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "preview" });
|
||||
});
|
||||
|
||||
it("does not treat CLI-only local deployment markers as runtime proof", () => {
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CONVEX_DEPLOYMENT: "local:clawhub",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "unknown" });
|
||||
});
|
||||
|
||||
it("rejects conflicting populated Convex runtime URLs", () => {
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CLAWHUB_DEPLOYMENT_NAME: "academic-chihuahua-392",
|
||||
CLAWHUB_DISABLE_CRONS: "1",
|
||||
CLAWHUB_ENV: "test",
|
||||
CONVEX_CLOUD_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
CONVEX_SITE_URL: "https://preview-project.convex.site",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "test" });
|
||||
|
||||
expect(
|
||||
getSkillsShFixtureEnvironmentPolicy({
|
||||
CONVEX_CLOUD_URL: "http://127.0.0.1:3210",
|
||||
CONVEX_SITE_URL: "https://academic-chihuahua-392.convex.site",
|
||||
}),
|
||||
).toMatchObject({ allowed: false, environment: "unknown" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
type SkillsShCatalogEnvironment = {
|
||||
CLAWHUB_DEPLOYMENT_NAME?: string;
|
||||
CLAWHUB_DISABLE_CRONS?: string;
|
||||
CLAWHUB_ENV?: string;
|
||||
CLAWHUB_PREVIEW?: string;
|
||||
CONVEX_CLOUD_URL?: string;
|
||||
CONVEX_DEPLOYMENT?: string;
|
||||
CONVEX_SITE_URL?: string;
|
||||
DEV_AUTH_CONVEX_DEPLOYMENT?: string;
|
||||
};
|
||||
|
||||
export type SkillsShFixtureEnvironmentPolicy =
|
||||
| {
|
||||
allowed: true;
|
||||
environment: "local" | "test";
|
||||
}
|
||||
| {
|
||||
allowed: false;
|
||||
environment: "preview" | "test" | "production" | "unknown";
|
||||
reason: string;
|
||||
};
|
||||
|
||||
const TEST_DEPLOYMENT = "academic-chihuahua-392";
|
||||
const TEST_CLOUD_URL = `https://${TEST_DEPLOYMENT}.convex.cloud`;
|
||||
const TEST_SITE_URL = `https://${TEST_DEPLOYMENT}.convex.site`;
|
||||
|
||||
function populatedRuntimeUrls(env: SkillsShCatalogEnvironment) {
|
||||
return [
|
||||
{ actual: env.CONVEX_CLOUD_URL?.trim(), expectedTest: TEST_CLOUD_URL },
|
||||
{ actual: env.CONVEX_SITE_URL?.trim(), expectedTest: TEST_SITE_URL },
|
||||
].filter((entry): entry is { actual: string; expectedTest: string } => Boolean(entry.actual));
|
||||
}
|
||||
|
||||
function isLocalRuntimeUrl(value: string | undefined) {
|
||||
if (!value) return false;
|
||||
try {
|
||||
const hostname = new URL(value).hostname;
|
||||
return (
|
||||
hostname === "localhost" ||
|
||||
hostname === "127.0.0.1" ||
|
||||
hostname === "0.0.0.0" ||
|
||||
hostname === "[::1]"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function getSkillsShFixtureEnvironmentPolicy(
|
||||
env: SkillsShCatalogEnvironment = process.env,
|
||||
): SkillsShFixtureEnvironmentPolicy {
|
||||
if (env.CLAWHUB_PREVIEW === "1") {
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "preview",
|
||||
reason: "skills.sh catalog fixture work is disabled in Preview",
|
||||
};
|
||||
}
|
||||
|
||||
const deployment = env.CONVEX_DEPLOYMENT?.trim() || env.DEV_AUTH_CONVEX_DEPLOYMENT?.trim() || "";
|
||||
if (deployment.startsWith("prod:")) {
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "production",
|
||||
reason: "skills.sh catalog fixture work is disabled in production",
|
||||
};
|
||||
}
|
||||
|
||||
if (env.CLAWHUB_ENV === "test") {
|
||||
if (env.CLAWHUB_DISABLE_CRONS !== "1") {
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "test",
|
||||
reason: "skills.sh Test fixture work requires CLAWHUB_DISABLE_CRONS=1",
|
||||
};
|
||||
}
|
||||
if (env.CLAWHUB_DEPLOYMENT_NAME !== TEST_DEPLOYMENT) {
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "test",
|
||||
reason: `skills.sh Test fixture work requires CLAWHUB_DEPLOYMENT_NAME=${TEST_DEPLOYMENT}`,
|
||||
};
|
||||
}
|
||||
const runtimeUrls = populatedRuntimeUrls(env);
|
||||
if (
|
||||
runtimeUrls.length === 0 ||
|
||||
runtimeUrls.some((entry) => entry.actual !== entry.expectedTest)
|
||||
) {
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "test",
|
||||
reason: `skills.sh Test fixture work requires the ${TEST_DEPLOYMENT} Convex runtime URL`,
|
||||
};
|
||||
}
|
||||
return { allowed: true, environment: "test" };
|
||||
}
|
||||
|
||||
if (env.CLAWHUB_DEPLOYMENT_NAME) {
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "production",
|
||||
reason: "skills.sh catalog fixture work is disabled in production",
|
||||
};
|
||||
}
|
||||
const runtimeUrls = populatedRuntimeUrls(env);
|
||||
if (runtimeUrls.length > 0 && runtimeUrls.every((entry) => isLocalRuntimeUrl(entry.actual))) {
|
||||
return { allowed: true, environment: "local" };
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
environment: "unknown",
|
||||
reason: "skills.sh catalog fixture work requires an explicit local or Test environment",
|
||||
};
|
||||
}
|
||||
|
||||
export function assertSkillsShFixtureEnvironmentAllowed(
|
||||
env: SkillsShCatalogEnvironment = process.env,
|
||||
) {
|
||||
const policy = getSkillsShFixtureEnvironmentPolicy(env);
|
||||
if (!policy.allowed) throw new Error(policy.reason);
|
||||
return policy;
|
||||
}
|
||||
|
||||
export function assertSkillsShCatalogControlMutationAllowed(
|
||||
env: SkillsShCatalogEnvironment = process.env,
|
||||
) {
|
||||
if (env.CLAWHUB_PREVIEW === "1") {
|
||||
throw new Error("skills.sh catalog control mutations are disabled in Preview");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import frozenSnapshot from "../fixtures/skills-sh-500-2026-07-21.json";
|
||||
|
||||
export type SkillsShCatalogFixtureRow = {
|
||||
externalId: string;
|
||||
githubOwnerId: number;
|
||||
owner: string;
|
||||
repo: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
sourceUrl: string;
|
||||
githubRepoUrl: string;
|
||||
sourceContentHash: string;
|
||||
installs: number;
|
||||
};
|
||||
|
||||
type SkillsShCatalogFixture = {
|
||||
snapshotId: string;
|
||||
sourceKind: "fixture" | "frozen-snapshot";
|
||||
capturedAt: string | null;
|
||||
snapshotCaptureFetches: number;
|
||||
length: number;
|
||||
rowAt: (index: number) => SkillsShCatalogFixtureRow;
|
||||
findByExternalId: (externalId: string) => SkillsShCatalogFixtureRow | null;
|
||||
};
|
||||
|
||||
const AIQ_DEPLOY: SkillsShCatalogFixtureRow = {
|
||||
externalId: "nvidia/skills/aiq-deploy",
|
||||
githubOwnerId: 1_728_152,
|
||||
owner: "nvidia",
|
||||
repo: "skills",
|
||||
slug: "aiq-deploy",
|
||||
displayName: "AIQ Deploy",
|
||||
sourceUrl: "https://www.skills.sh/nvidia/skills/aiq-deploy",
|
||||
githubRepoUrl: "https://github.com/NVIDIA/skills",
|
||||
sourceContentHash: "c087fae29cc3882e03116c37921a7e092c42b15a04aa9a6c503e000a3e260ab5",
|
||||
installs: 100,
|
||||
};
|
||||
|
||||
const AIQ_DEPLOY_V2: SkillsShCatalogFixtureRow = {
|
||||
...AIQ_DEPLOY,
|
||||
displayName: "AIQ Deploy Updated",
|
||||
sourceContentHash: "d087fae29cc3882e03116c37921a7e092c42b15a04aa9a6c503e000a3e260ab5",
|
||||
installs: 101,
|
||||
};
|
||||
|
||||
const FROZEN_ROWS = frozenSnapshot.rows satisfies SkillsShCatalogFixtureRow[];
|
||||
|
||||
function fromRows(
|
||||
metadata: Omit<SkillsShCatalogFixture, "length" | "rowAt" | "findByExternalId">,
|
||||
rows: readonly SkillsShCatalogFixtureRow[],
|
||||
): SkillsShCatalogFixture {
|
||||
const byExternalId = new Map(rows.map((row) => [row.externalId, row]));
|
||||
return {
|
||||
...metadata,
|
||||
length: rows.length,
|
||||
rowAt(index) {
|
||||
const row = rows[index];
|
||||
if (!row) throw new Error(`skills.sh fixture row ${index} is out of bounds`);
|
||||
return row;
|
||||
},
|
||||
findByExternalId(externalId) {
|
||||
return byExternalId.get(externalId) ?? null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function changedFrozenRows() {
|
||||
return FROZEN_ROWS.map((row, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...row,
|
||||
displayName: `${row.displayName} Updated`,
|
||||
sourceContentHash: `${row.sourceContentHash.slice(0, -1)}${
|
||||
row.sourceContentHash.endsWith("0") ? "1" : "0"
|
||||
}`,
|
||||
installs: row.installs + 1,
|
||||
};
|
||||
}
|
||||
if (index === 1) {
|
||||
return {
|
||||
...row,
|
||||
installs: row.installs + 1,
|
||||
};
|
||||
}
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
||||
const SYNTHETIC_DISCOVERY_LENGTH = 20_000;
|
||||
|
||||
function syntheticDiscoveryRow(index: number): SkillsShCatalogFixtureRow {
|
||||
if (!Number.isInteger(index) || index < 0 || index >= SYNTHETIC_DISCOVERY_LENGTH) {
|
||||
throw new Error(`skills.sh synthetic fixture row ${index} is out of bounds`);
|
||||
}
|
||||
const suffix = index.toString().padStart(5, "0");
|
||||
return {
|
||||
externalId: `synthetic-owner/synthetic-repo/skill-${suffix}`,
|
||||
githubOwnerId: 9_999_999,
|
||||
owner: "synthetic-owner",
|
||||
repo: "synthetic-repo",
|
||||
slug: `skill-${suffix}`,
|
||||
displayName: `Synthetic Skill ${suffix}`,
|
||||
sourceUrl: `https://www.skills.sh/synthetic-owner/synthetic-repo/skill-${suffix}`,
|
||||
githubRepoUrl: "https://github.com/synthetic-owner/synthetic-repo",
|
||||
sourceContentHash: index.toString(16).padStart(64, "0"),
|
||||
installs: index,
|
||||
};
|
||||
}
|
||||
|
||||
const FIXTURES = {
|
||||
"nvidia-small-v1": fromRows(
|
||||
{
|
||||
snapshotId: "nvidia-small-v1",
|
||||
sourceKind: "fixture",
|
||||
capturedAt: null,
|
||||
snapshotCaptureFetches: 0,
|
||||
},
|
||||
[
|
||||
AIQ_DEPLOY,
|
||||
{ ...AIQ_DEPLOY },
|
||||
{
|
||||
externalId: "nvidia/skills/cuda-agent",
|
||||
githubOwnerId: 1_728_152,
|
||||
owner: "nvidia",
|
||||
repo: "skills",
|
||||
slug: "cuda-agent",
|
||||
displayName: "CUDA Agent",
|
||||
sourceUrl: "https://www.skills.sh/nvidia/skills/cuda-agent",
|
||||
githubRepoUrl: "https://github.com/NVIDIA/skills",
|
||||
sourceContentHash: "b087fae29cc3882e03116c37921a7e092c42b15a04aa9a6c503e000a3e260ab5",
|
||||
installs: 50,
|
||||
},
|
||||
],
|
||||
),
|
||||
"nvidia-small-v2": fromRows(
|
||||
{
|
||||
snapshotId: "nvidia-small-v2",
|
||||
sourceKind: "fixture",
|
||||
capturedAt: null,
|
||||
snapshotCaptureFetches: 0,
|
||||
},
|
||||
[AIQ_DEPLOY_V2],
|
||||
),
|
||||
"skills-sh-500-2026-07-21": fromRows(
|
||||
{
|
||||
snapshotId: frozenSnapshot.snapshotId,
|
||||
sourceKind: "frozen-snapshot",
|
||||
capturedAt: frozenSnapshot.capturedAt,
|
||||
snapshotCaptureFetches: frozenSnapshot.captureMetrics.skillsShFetches,
|
||||
},
|
||||
FROZEN_ROWS,
|
||||
),
|
||||
"skills-sh-500-2026-07-21-v2": fromRows(
|
||||
{
|
||||
snapshotId: `${frozenSnapshot.snapshotId}-v2`,
|
||||
sourceKind: "frozen-snapshot",
|
||||
capturedAt: frozenSnapshot.capturedAt,
|
||||
snapshotCaptureFetches: frozenSnapshot.captureMetrics.skillsShFetches,
|
||||
},
|
||||
changedFrozenRows(),
|
||||
),
|
||||
"synthetic-20000-v1": {
|
||||
snapshotId: "synthetic-20000-v1",
|
||||
sourceKind: "fixture",
|
||||
capturedAt: null,
|
||||
snapshotCaptureFetches: 0,
|
||||
length: SYNTHETIC_DISCOVERY_LENGTH,
|
||||
rowAt: syntheticDiscoveryRow,
|
||||
findByExternalId(externalId) {
|
||||
const match = /^synthetic-owner\/synthetic-repo\/skill-(\d{5})$/.exec(externalId);
|
||||
if (!match) return null;
|
||||
const index = Number(match[1]);
|
||||
return index < SYNTHETIC_DISCOVERY_LENGTH ? syntheticDiscoveryRow(index) : null;
|
||||
},
|
||||
},
|
||||
} satisfies Record<string, SkillsShCatalogFixture>;
|
||||
|
||||
export type SkillsShCatalogFixtureId = keyof typeof FIXTURES;
|
||||
|
||||
export function getSkillsShCatalogFixture(
|
||||
fixtureId: SkillsShCatalogFixtureId,
|
||||
): SkillsShCatalogFixture {
|
||||
return FIXTURES[fixtureId];
|
||||
}
|
||||
@@ -715,6 +715,7 @@ const securityScanJobSourceValidator = v.union(
|
||||
v.literal("backfill"),
|
||||
v.literal("bulk-rescan"),
|
||||
v.literal("manual"),
|
||||
v.literal("skills-sh-catalog-test"),
|
||||
);
|
||||
const skillCardGenerationJobStatusValidator = v.union(
|
||||
v.literal("queued"),
|
||||
@@ -780,6 +781,7 @@ const skillScanRequestSourceKindValidator = v.union(
|
||||
v.literal("upload"),
|
||||
v.literal("published"),
|
||||
v.literal("github"),
|
||||
v.literal("skills-sh-catalog"),
|
||||
);
|
||||
|
||||
const skills = defineTable({
|
||||
@@ -1917,6 +1919,7 @@ const skillScanRequests = defineTable({
|
||||
skillId: v.optional(v.id("skills")),
|
||||
skillVersionId: v.optional(v.id("skillVersions")),
|
||||
githubSkillScanId: v.optional(v.id("githubSkillScans")),
|
||||
skillsShCatalogAttemptId: v.optional(v.id("skillsShCatalogScanAttempts")),
|
||||
files: packageFilesValidator,
|
||||
fileChunkCount: v.optional(v.number()),
|
||||
fileManifestBytes: v.optional(v.number()),
|
||||
@@ -2751,6 +2754,181 @@ const systemSettings = defineTable({
|
||||
updatedByUserId: v.optional(v.id("users")),
|
||||
}).index("by_key_and_updated_at", ["key", "updatedAt"]);
|
||||
|
||||
const skillsShCatalogControls = defineTable({
|
||||
key: v.literal("global"),
|
||||
mode: v.union(v.literal("off"), v.literal("fixture"), v.literal("staging-live")),
|
||||
discoveryEnabled: v.boolean(),
|
||||
writesEnabled: v.boolean(),
|
||||
scanPlanningEnabled: v.boolean(),
|
||||
scanAdmissionEnabled: v.boolean(),
|
||||
publicVisibilityEnabled: v.boolean(),
|
||||
paused: v.boolean(),
|
||||
maxEntriesPerRun: v.number(),
|
||||
maxEntriesPerBatch: v.number(),
|
||||
maxWritesPerBatch: v.number(),
|
||||
maxPlannedScans: v.number(),
|
||||
maxScanAdmissionsPerBatch: v.number(),
|
||||
maxScanAdmissionsPerRun: v.number(),
|
||||
maxScanAdmissionsPerDay: v.number(),
|
||||
maxCatalogQueued: v.number(),
|
||||
maxCatalogInFlight: v.number(),
|
||||
maxNativeQueued: v.number(),
|
||||
maxNativeInFlight: v.number(),
|
||||
realScanAllowlist: v.array(v.string()),
|
||||
updatedBy: v.string(),
|
||||
reason: v.string(),
|
||||
updatedAt: v.number(),
|
||||
}).index("by_key", ["key"]);
|
||||
|
||||
const skillsShCatalogRunCountsValidator = v.object({
|
||||
observed: v.number(),
|
||||
wouldInsert: v.number(),
|
||||
wouldUpdate: v.number(),
|
||||
inserted: v.number(),
|
||||
updated: v.number(),
|
||||
unchanged: v.number(),
|
||||
rejected: v.number(),
|
||||
scansPlanned: v.number(),
|
||||
scansAdmitted: v.number(),
|
||||
scansCompleted: v.number(),
|
||||
scansCanceled: v.number(),
|
||||
});
|
||||
|
||||
const skillsShCatalogRuns = defineTable({
|
||||
fixtureId: v.union(
|
||||
v.literal("nvidia-small-v1"),
|
||||
v.literal("nvidia-small-v2"),
|
||||
v.literal("skills-sh-500-2026-07-21"),
|
||||
v.literal("skills-sh-500-2026-07-21-v2"),
|
||||
v.literal("synthetic-20000-v1"),
|
||||
v.literal("skills-sh-test-live-500"),
|
||||
),
|
||||
snapshotId: v.string(),
|
||||
sourceKind: v.union(
|
||||
v.literal("fixture"),
|
||||
v.literal("frozen-snapshot"),
|
||||
v.literal("staging-live"),
|
||||
),
|
||||
sourceCapturedAt: v.optional(v.string()),
|
||||
snapshotCaptureFetches: v.number(),
|
||||
dryRun: v.boolean(),
|
||||
status: v.union(
|
||||
v.literal("running"),
|
||||
v.literal("paused"),
|
||||
v.literal("completed"),
|
||||
v.literal("budget-exhausted"),
|
||||
v.literal("failed"),
|
||||
v.literal("canceling"),
|
||||
v.literal("canceled"),
|
||||
),
|
||||
cursor: v.number(),
|
||||
scanCursor: v.number(),
|
||||
fixtureLength: v.number(),
|
||||
counts: skillsShCatalogRunCountsValidator,
|
||||
budgets: v.object({
|
||||
maxEntriesPerRun: v.number(),
|
||||
maxEntriesPerBatch: v.number(),
|
||||
maxWritesPerBatch: v.number(),
|
||||
maxPlannedScans: v.number(),
|
||||
maxScanAdmissionsPerBatch: v.number(),
|
||||
maxScanAdmissionsPerRun: v.number(),
|
||||
maxScanAdmissionsPerDay: v.number(),
|
||||
}),
|
||||
operations: v.object({
|
||||
functionCalls: v.number(),
|
||||
dbReads: v.number(),
|
||||
dbWrites: v.number(),
|
||||
}),
|
||||
actor: v.string(),
|
||||
reason: v.string(),
|
||||
lastError: v.optional(v.string()),
|
||||
batchesProcessed: v.number(),
|
||||
scanAdmissionBatches: v.number(),
|
||||
lastBatchWrites: v.number(),
|
||||
lastBatchReads: v.number(),
|
||||
startedAt: v.number(),
|
||||
completedAt: v.optional(v.number()),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_started_at", ["startedAt"])
|
||||
.index("by_status_and_updated_at", ["status", "updatedAt"]);
|
||||
|
||||
const skillsShCatalogEntries = defineTable({
|
||||
externalId: v.string(),
|
||||
sourceKind: v.union(
|
||||
v.literal("fixture"),
|
||||
v.literal("frozen-snapshot"),
|
||||
v.literal("staging-live"),
|
||||
),
|
||||
githubOwnerId: v.number(),
|
||||
owner: v.string(),
|
||||
repo: v.string(),
|
||||
slug: v.string(),
|
||||
displayName: v.string(),
|
||||
sourceUrl: v.string(),
|
||||
githubRepoUrl: v.string(),
|
||||
sourceContentHash: v.string(),
|
||||
installs: v.number(),
|
||||
sourceSnapshotId: v.string(),
|
||||
publicVisible: v.boolean(),
|
||||
scanStatus: v.union(
|
||||
v.literal("not-planned"),
|
||||
v.literal("planned"),
|
||||
v.literal("queued"),
|
||||
v.literal("clean"),
|
||||
v.literal("suspicious"),
|
||||
v.literal("malicious"),
|
||||
v.literal("failed"),
|
||||
v.literal("canceled"),
|
||||
),
|
||||
firstObservedAt: v.number(),
|
||||
lastObservedAt: v.number(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_external_id", ["externalId"])
|
||||
.index("by_owner_and_source_kind_and_github_owner_id", ["owner", "sourceKind", "githubOwnerId"])
|
||||
.index("by_source_kind_and_github_owner_id_and_owner", ["sourceKind", "githubOwnerId", "owner"])
|
||||
.index("by_scan_status_and_updated_at", ["scanStatus", "updatedAt"]);
|
||||
|
||||
const skillsShCatalogScanAttempts = defineTable({
|
||||
entryId: v.id("skillsShCatalogEntries"),
|
||||
runId: v.id("skillsShCatalogRuns"),
|
||||
externalId: v.string(),
|
||||
sourceContentHash: v.string(),
|
||||
artifactContentHash: v.optional(v.string()),
|
||||
skillScanRequestId: v.optional(v.id("skillScanRequests")),
|
||||
securityScanJobId: v.optional(v.id("securityScanJobs")),
|
||||
source: v.union(v.literal("skills-sh-catalog-fixture"), v.literal("skills-sh-catalog-test")),
|
||||
dispatchKind: v.union(v.literal("deterministic"), v.literal("real")),
|
||||
priority: v.literal("low"),
|
||||
status: v.union(
|
||||
v.literal("queued"),
|
||||
v.literal("running"),
|
||||
v.literal("succeeded"),
|
||||
v.literal("failed"),
|
||||
v.literal("canceled"),
|
||||
),
|
||||
verdict: v.optional(
|
||||
v.union(
|
||||
v.literal("clean"),
|
||||
v.literal("suspicious"),
|
||||
v.literal("malicious"),
|
||||
v.literal("failed"),
|
||||
),
|
||||
),
|
||||
completedAt: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
.index("by_entry_and_source_content_hash", ["entryId", "sourceContentHash"])
|
||||
.index("by_run", ["runId"])
|
||||
.index("by_run_and_status", ["runId", "status"])
|
||||
.index("by_run_dispatch_kind_status_created_at", ["runId", "dispatchKind", "status", "createdAt"])
|
||||
.index("by_created_at", ["createdAt"])
|
||||
.index("by_status_and_created_at", ["status", "createdAt"])
|
||||
.index("by_dispatch_kind_and_status_and_created_at", ["dispatchKind", "status", "createdAt"]);
|
||||
|
||||
const publisherAbuseScoreRuns = defineTable({
|
||||
modelVersion: v.string(),
|
||||
modelConfig: publisherAbuseModelConfigValidator,
|
||||
@@ -3429,6 +3607,10 @@ export default defineSchema({
|
||||
promotions,
|
||||
auditLogs,
|
||||
systemSettings,
|
||||
skillsShCatalogControls,
|
||||
skillsShCatalogRuns,
|
||||
skillsShCatalogEntries,
|
||||
skillsShCatalogScanAttempts,
|
||||
publisherAbuseScoreRuns,
|
||||
publisherAbuseTemporalScanSamples,
|
||||
publisherAbuseTemporalScanCandidates,
|
||||
|
||||
+1209
-14
File diff suppressed because it is too large
Load Diff
+689
-41
@@ -21,6 +21,7 @@ import {
|
||||
MAX_SKILL_SCAN_REQUEST_MANIFEST_BYTES,
|
||||
serializedSkillScanRequestFilesBytes,
|
||||
} from "./lib/skillScanRequestFiles";
|
||||
import { getSkillsShFixtureEnvironmentPolicy } from "./lib/skillsShCatalogEnvironment";
|
||||
import { redactWorkerPublicText } from "./lib/workerTextRedaction";
|
||||
import { requestSecurityScanDispatch } from "./securityScanDispatch";
|
||||
|
||||
@@ -138,10 +139,21 @@ const jobSourceValidator = v.union(
|
||||
v.literal("backfill"),
|
||||
v.literal("bulk-rescan"),
|
||||
v.literal("manual"),
|
||||
v.literal("skills-sh-catalog-test"),
|
||||
);
|
||||
|
||||
type SecurityScanJobSource = "publish" | "vt-update" | "backfill" | "bulk-rescan" | "manual";
|
||||
const codexScanWorkerLaneValidator = v.union(v.literal("priority"), v.literal("shared"));
|
||||
type SecurityScanJobSource =
|
||||
| "publish"
|
||||
| "vt-update"
|
||||
| "backfill"
|
||||
| "bulk-rescan"
|
||||
| "manual"
|
||||
| "skills-sh-catalog-test";
|
||||
const codexScanWorkerLaneValidator = v.union(
|
||||
v.literal("priority"),
|
||||
v.literal("shared"),
|
||||
v.literal("catalog"),
|
||||
);
|
||||
|
||||
type CodexScanQueueHealth = {
|
||||
snapshotAt: number;
|
||||
@@ -158,6 +170,7 @@ const CLAIM_SOURCE_ORDER: SecurityScanJobSource[] = [
|
||||
"backfill",
|
||||
"vt-update",
|
||||
"bulk-rescan",
|
||||
"skills-sh-catalog-test",
|
||||
];
|
||||
|
||||
const SOURCE_PRIORITY: Record<SecurityScanJobSource, number> = {
|
||||
@@ -166,6 +179,7 @@ const SOURCE_PRIORITY: Record<SecurityScanJobSource, number> = {
|
||||
backfill: 3,
|
||||
"vt-update": 2,
|
||||
"bulk-rescan": 1,
|
||||
"skills-sh-catalog-test": 0,
|
||||
};
|
||||
|
||||
function higherPrioritySource(
|
||||
@@ -310,6 +324,12 @@ const githubSkillScanStatusValidator = v.union(
|
||||
v.literal("pending"),
|
||||
v.literal("failed"),
|
||||
);
|
||||
const catalogScanVerdictValidator = v.union(
|
||||
v.literal("clean"),
|
||||
v.literal("suspicious"),
|
||||
v.literal("malicious"),
|
||||
v.literal("failed"),
|
||||
);
|
||||
|
||||
const internalRefs = internal as unknown as {
|
||||
packages: {
|
||||
@@ -329,6 +349,7 @@ const internalRefs = internal as unknown as {
|
||||
getSkillScanRequestForUserInternal: unknown;
|
||||
getJobTargetInternal: unknown;
|
||||
recordGitHubSkillScanResultInternal: unknown;
|
||||
completeCatalogSkillScanJobInternal: unknown;
|
||||
recordSkillScanRequestFailedInternal: unknown;
|
||||
recordSkillScanRequestSucceededInternal: unknown;
|
||||
requeueJobLeaseInternal: unknown;
|
||||
@@ -1258,6 +1279,42 @@ async function enqueueSkillScanRequestJob(
|
||||
return jobId;
|
||||
}
|
||||
|
||||
export async function enqueueSkillsShCatalogScanRequest(
|
||||
ctx: MutationCtx,
|
||||
args: {
|
||||
actorUserId: Id<"users">;
|
||||
attemptId: Id<"skillsShCatalogScanAttempts">;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
artifactContentHash: string;
|
||||
files: Doc<"skillScanRequests">["files"];
|
||||
},
|
||||
) {
|
||||
const now = Date.now();
|
||||
const requestId = await ctx.db.insert("skillScanRequests", {
|
||||
actorUserId: args.actorUserId,
|
||||
sourceKind: "skills-sh-catalog",
|
||||
update: false,
|
||||
writtenBack: false,
|
||||
status: "queued",
|
||||
requestedJobSource: "skills-sh-catalog-test",
|
||||
requestedJobPriority: -100,
|
||||
slug: args.slug,
|
||||
displayName: args.displayName,
|
||||
skillsShCatalogAttemptId: args.attemptId,
|
||||
files: args.files,
|
||||
sha256hash: args.artifactContentHash,
|
||||
expiresAt: skillScanRequestExpiresAt(now),
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const jobId = await enqueueSkillScanRequestJob(ctx, requestId, {
|
||||
source: "skills-sh-catalog-test",
|
||||
priority: -100,
|
||||
});
|
||||
return { requestId, jobId };
|
||||
}
|
||||
|
||||
export const prepareGitHubSkillScanRequestInternal = internalMutation({
|
||||
args: {
|
||||
skillId: v.id("skills"),
|
||||
@@ -1868,6 +1925,227 @@ export const recordSkillScanRequestSucceededInternal = internalMutation({
|
||||
},
|
||||
});
|
||||
|
||||
export const completeCatalogSkillScanJobInternal = internalMutation({
|
||||
args: {
|
||||
attemptId: v.id("skillsShCatalogScanAttempts"),
|
||||
scanId: v.id("skillScanRequests"),
|
||||
jobId: v.id("securityScanJobs"),
|
||||
leaseToken: v.string(),
|
||||
artifactContentHash: v.string(),
|
||||
verdict: catalogScanVerdictValidator,
|
||||
runId: v.optional(v.string()),
|
||||
llmAnalysis: llmAnalysisValidator,
|
||||
skillSpectorAnalysis: v.optional(skillSpectorAnalysisValidator),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const environment = getSkillsShFixtureEnvironmentPolicy();
|
||||
if (!environment.allowed || environment.environment !== "test") {
|
||||
throw new ConvexError("catalog scan completion requires the permanent Test environment");
|
||||
}
|
||||
const [job, request, attempt] = await Promise.all([
|
||||
ctx.db.get(args.jobId),
|
||||
ctx.db.get(args.scanId),
|
||||
ctx.db.get(args.attemptId),
|
||||
]);
|
||||
if (
|
||||
!job ||
|
||||
job.source !== "skills-sh-catalog-test" ||
|
||||
job.targetKind !== "skillScanRequest" ||
|
||||
job.skillScanRequestId !== args.scanId
|
||||
) {
|
||||
throw new ConvexError("Catalog scan job linkage mismatch");
|
||||
}
|
||||
if (
|
||||
!request ||
|
||||
request.sourceKind !== "skills-sh-catalog" ||
|
||||
request.securityScanJobId !== args.jobId ||
|
||||
request.skillsShCatalogAttemptId !== args.attemptId
|
||||
) {
|
||||
throw new ConvexError("Catalog scan request linkage mismatch");
|
||||
}
|
||||
if (
|
||||
!attempt ||
|
||||
attempt.dispatchKind !== "real" ||
|
||||
attempt.skillScanRequestId !== args.scanId ||
|
||||
attempt.securityScanJobId !== args.jobId
|
||||
) {
|
||||
throw new ConvexError("Catalog scan attempt linkage mismatch");
|
||||
}
|
||||
const artifactContentHash = args.artifactContentHash.toLowerCase();
|
||||
if (
|
||||
!attempt.artifactContentHash ||
|
||||
attempt.artifactContentHash !== artifactContentHash ||
|
||||
request.sha256hash !== artifactContentHash
|
||||
) {
|
||||
throw new ConvexError("Catalog scan artifact hash mismatch");
|
||||
}
|
||||
if (
|
||||
attempt.status === "succeeded" ||
|
||||
attempt.status === "failed" ||
|
||||
attempt.status === "canceled"
|
||||
) {
|
||||
const expectedStatus = args.verdict === "failed" ? "failed" : "succeeded";
|
||||
if (
|
||||
attempt.status === expectedStatus &&
|
||||
attempt.verdict === args.verdict &&
|
||||
request.status === expectedStatus &&
|
||||
job.status === expectedStatus &&
|
||||
(expectedStatus !== "failed" ||
|
||||
(request.lastError === "Catalog scan analysis failed" &&
|
||||
job.lastError === "Catalog scan analysis failed"))
|
||||
) {
|
||||
return { ok: true as const, applied: true as const, publicVisible: false as const };
|
||||
}
|
||||
if (
|
||||
attempt.status === "canceled" &&
|
||||
request.status === "failed" &&
|
||||
job.status === "failed" &&
|
||||
request.lastError === job.lastError
|
||||
) {
|
||||
if (request.lastError === "Catalog run canceled before scan completion") {
|
||||
return { ok: true as const, applied: false as const, reason: "run-canceled" as const };
|
||||
}
|
||||
if (request.lastError === "Catalog source changed before scan completion") {
|
||||
return { ok: true as const, applied: false as const, reason: "stale-attempt" as const };
|
||||
}
|
||||
}
|
||||
throw new ConvexError("Catalog scan terminal result mismatch");
|
||||
}
|
||||
if (
|
||||
job.leaseToken !== args.leaseToken ||
|
||||
job.status !== "running" ||
|
||||
(attempt.status !== "queued" && attempt.status !== "running")
|
||||
) {
|
||||
throw new ConvexError("Catalog scan job lease mismatch");
|
||||
}
|
||||
|
||||
const [run, entry] = await Promise.all([
|
||||
ctx.db.get(attempt.runId),
|
||||
ctx.db.get(attempt.entryId),
|
||||
]);
|
||||
const now = Date.now();
|
||||
const terminalizeWithoutResult = async (reason: "run-canceled" | "stale-attempt") => {
|
||||
const entryStillCurrent = entry?.sourceContentHash === attempt.sourceContentHash;
|
||||
await ctx.db.patch(attempt._id, {
|
||||
status: "canceled",
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (entryStillCurrent) {
|
||||
await ctx.db.patch(entry._id, {
|
||||
scanStatus: "canceled",
|
||||
publicVisible: false,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
await ctx.db.patch(request._id, {
|
||||
status: "failed",
|
||||
lastError:
|
||||
reason === "run-canceled"
|
||||
? "Catalog run canceled before scan completion"
|
||||
: "Catalog source changed before scan completion",
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.patch(job._id, {
|
||||
status: "failed",
|
||||
lastError:
|
||||
reason === "run-canceled"
|
||||
? "Catalog run canceled before scan completion"
|
||||
: "Catalog source changed before scan completion",
|
||||
completedAt: now,
|
||||
leaseToken: undefined,
|
||||
leaseExpiresAt: undefined,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (run) {
|
||||
const [queued, running] = await Promise.all([
|
||||
ctx.db
|
||||
.query("skillsShCatalogScanAttempts")
|
||||
.withIndex("by_run_and_status", (q) => q.eq("runId", run._id).eq("status", "queued"))
|
||||
.first(),
|
||||
ctx.db
|
||||
.query("skillsShCatalogScanAttempts")
|
||||
.withIndex("by_run_and_status", (q) => q.eq("runId", run._id).eq("status", "running"))
|
||||
.first(),
|
||||
]);
|
||||
await ctx.db.patch(run._id, {
|
||||
...(reason === "run-canceled"
|
||||
? { status: queued || running ? ("canceling" as const) : ("canceled" as const) }
|
||||
: {}),
|
||||
counts: {
|
||||
...run.counts,
|
||||
scansCanceled: run.counts.scansCanceled + 1,
|
||||
},
|
||||
operations: {
|
||||
functionCalls: run.operations.functionCalls + 1,
|
||||
dbReads: run.operations.dbReads + 7,
|
||||
dbWrites: run.operations.dbWrites + (entryStillCurrent ? 5 : 4),
|
||||
},
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return { ok: true as const, applied: false as const, reason };
|
||||
};
|
||||
|
||||
if (run?.status === "canceling" || run?.status === "canceled") {
|
||||
return await terminalizeWithoutResult("run-canceled");
|
||||
}
|
||||
if (!entry || entry.sourceContentHash !== attempt.sourceContentHash) {
|
||||
return await terminalizeWithoutResult("stale-attempt");
|
||||
}
|
||||
|
||||
const scanFailed = args.verdict === "failed";
|
||||
await ctx.db.patch(attempt._id, {
|
||||
status: scanFailed ? "failed" : "succeeded",
|
||||
verdict: args.verdict,
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.patch(entry._id, {
|
||||
scanStatus: args.verdict,
|
||||
publicVisible: false,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.patch(request._id, {
|
||||
status: scanFailed ? "failed" : "succeeded",
|
||||
lastError: scanFailed ? "Catalog scan analysis failed" : undefined,
|
||||
llmAnalysis: args.llmAnalysis,
|
||||
...(args.skillSpectorAnalysis
|
||||
? { skillSpectorAnalysis: capSkillSpectorAnalysisForStorage(args.skillSpectorAnalysis) }
|
||||
: {}),
|
||||
writtenBack: request.writtenBack,
|
||||
runId: args.runId,
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
await ctx.db.patch(job._id, {
|
||||
status: scanFailed ? "failed" : "succeeded",
|
||||
lastError: scanFailed ? "Catalog scan analysis failed" : undefined,
|
||||
runId: args.runId,
|
||||
completedAt: now,
|
||||
leaseToken: undefined,
|
||||
leaseExpiresAt: undefined,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (run) {
|
||||
await ctx.db.patch(run._id, {
|
||||
counts: {
|
||||
...run.counts,
|
||||
scansCompleted: run.counts.scansCompleted + 1,
|
||||
},
|
||||
operations: {
|
||||
functionCalls: run.operations.functionCalls + 1,
|
||||
dbReads: run.operations.dbReads + 5,
|
||||
dbWrites: run.operations.dbWrites + 5,
|
||||
},
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return { ok: true as const, applied: true as const, publicVisible: false as const };
|
||||
},
|
||||
});
|
||||
|
||||
export const recordSkillScanRequestFailedInternal = internalMutation({
|
||||
args: {
|
||||
scanId: v.id("skillScanRequests"),
|
||||
@@ -1945,12 +2223,32 @@ export const pruneExpiredSkillScanRequestsInternal = internalMutation({
|
||||
let deletedRequests = 0;
|
||||
let deferredRequests = 0;
|
||||
for (const request of requests) {
|
||||
if (request.securityScanJobId) {
|
||||
const job = await ctx.db.get(request.securityScanJobId);
|
||||
if (job?.targetKind === "skillScanRequest") {
|
||||
await ctx.db.delete(job._id);
|
||||
deletedJobs += 1;
|
||||
}
|
||||
const job = request.securityScanJobId ? await ctx.db.get(request.securityScanJobId) : null;
|
||||
if (
|
||||
request.sourceKind === "skills-sh-catalog" &&
|
||||
job?.targetKind === "skillScanRequest" &&
|
||||
(job.status === "queued" || job.status === "running")
|
||||
) {
|
||||
deferredRequests += 1;
|
||||
continue;
|
||||
}
|
||||
if (request.sourceKind === "skills-sh-catalog" && request.skillsShCatalogAttemptId) {
|
||||
const attempt = await ctx.db.get(request.skillsShCatalogAttemptId);
|
||||
const run = attempt ? await ctx.db.get(attempt.runId) : null;
|
||||
await terminalizeBlockedCatalogRetry(
|
||||
ctx,
|
||||
{
|
||||
kind: "blocked",
|
||||
request,
|
||||
...(attempt ? { attempt } : {}),
|
||||
...(run ? { run } : {}),
|
||||
},
|
||||
now,
|
||||
);
|
||||
}
|
||||
if (job?.targetKind === "skillScanRequest") {
|
||||
await ctx.db.delete(job._id);
|
||||
deletedJobs += 1;
|
||||
}
|
||||
const fileChunks =
|
||||
request.sourceKind === "github"
|
||||
@@ -1977,7 +2275,11 @@ export const pruneExpiredSkillScanRequestsInternal = internalMutation({
|
||||
deferredRequests += 1;
|
||||
continue;
|
||||
}
|
||||
if (request.sourceKind === "upload" || request.sourceKind === "github") {
|
||||
if (
|
||||
request.sourceKind === "upload" ||
|
||||
request.sourceKind === "github" ||
|
||||
request.sourceKind === "skills-sh-catalog"
|
||||
) {
|
||||
for (const file of [...request.files, ...fileChunks.flatMap((chunk) => chunk.files)]) {
|
||||
try {
|
||||
await ctx.storage.delete(file.storageId);
|
||||
@@ -2369,9 +2671,35 @@ export const claimQueuedJobsInternal = internalMutation({
|
||||
const ready: Doc<"securityScanJobs">[] = [];
|
||||
const claimedIds = new Set<Id<"securityScanJobs">>();
|
||||
const remainingCapacity = () => capacity - ready.length;
|
||||
const addReadyJobs = (jobs: Doc<"securityScanJobs">[]) => {
|
||||
let catalogGate:
|
||||
| {
|
||||
control: Doc<"skillsShCatalogControls">;
|
||||
health: Awaited<ReturnType<typeof readCatalogClaimHealth>>;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
const loadCatalogGate = async () => {
|
||||
if (catalogGate !== undefined) return catalogGate;
|
||||
const environment = getSkillsShFixtureEnvironmentPolicy();
|
||||
if (!environment.allowed || environment.environment !== "test") {
|
||||
catalogGate = null;
|
||||
return catalogGate;
|
||||
}
|
||||
const control = await ctx.db
|
||||
.query("skillsShCatalogControls")
|
||||
.withIndex("by_key", (q) => q.eq("key", "global"))
|
||||
.unique();
|
||||
catalogGate = control
|
||||
? {
|
||||
control,
|
||||
health: await readCatalogClaimHealth(ctx, control),
|
||||
}
|
||||
: null;
|
||||
return catalogGate;
|
||||
};
|
||||
const addReadyJobs = (jobs: Doc<"securityScanJobs">[], stopAtCapacity = true) => {
|
||||
for (const job of jobs) {
|
||||
if (remainingCapacity() === 0) break;
|
||||
if (stopAtCapacity && remainingCapacity() === 0) break;
|
||||
if (claimedIds.has(job._id) || job.nextRunAt > now) continue;
|
||||
claimedIds.add(job._id);
|
||||
ready.push(job);
|
||||
@@ -2379,39 +2707,97 @@ export const claimQueuedJobsInternal = internalMutation({
|
||||
};
|
||||
const takeReadySourceJobs = async (source: SecurityScanJobSource) => {
|
||||
if (remainingCapacity() === 0) return [];
|
||||
let takeLimit = remainingCapacity();
|
||||
if (source === "skills-sh-catalog-test") {
|
||||
const gate = await loadCatalogGate();
|
||||
if (
|
||||
!gate ||
|
||||
gate.control.mode !== "staging-live" ||
|
||||
gate.control.paused ||
|
||||
!gate.control.scanAdmissionEnabled ||
|
||||
!gate.health.claimable
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
// Scan a bounded window independent of the current admission cap so paused
|
||||
// or canceled jobs cannot hide later runnable backlog after the cap is lowered.
|
||||
takeLimit = MAX_CODEX_SCAN_CLAIM_LIMIT;
|
||||
}
|
||||
return await ctx.db
|
||||
.query("securityScanJobs")
|
||||
.withIndex("by_status_source_next_run_at", (q) =>
|
||||
q.eq("status", "queued").eq("source", source).lte("nextRunAt", now),
|
||||
)
|
||||
.order("asc")
|
||||
.take(remainingCapacity());
|
||||
.take(takeLimit);
|
||||
};
|
||||
|
||||
addReadyJobs(await takeReadySourceJobs("manual"));
|
||||
if (args.lane === "catalog") {
|
||||
addReadyJobs(await takeReadySourceJobs("skills-sh-catalog-test"), false);
|
||||
} else {
|
||||
addReadyJobs(await takeReadySourceJobs("manual"));
|
||||
|
||||
if (remainingCapacity() > 0) {
|
||||
addReadyJobs(
|
||||
await ctx.db
|
||||
.query("securityScanJobs")
|
||||
.withIndex("by_status_malicious_signal_next_run_at", (q) =>
|
||||
q.eq("status", "queued").eq("hasMaliciousSignal", true).lte("nextRunAt", now),
|
||||
)
|
||||
.order("asc")
|
||||
.take(remainingCapacity()),
|
||||
);
|
||||
}
|
||||
if (remainingCapacity() > 0) {
|
||||
addReadyJobs(
|
||||
await ctx.db
|
||||
.query("securityScanJobs")
|
||||
.withIndex("by_status_malicious_signal_next_run_at", (q) =>
|
||||
q.eq("status", "queued").eq("hasMaliciousSignal", true).lte("nextRunAt", now),
|
||||
)
|
||||
.order("asc")
|
||||
.take(remainingCapacity()),
|
||||
);
|
||||
}
|
||||
|
||||
// Shared workers remain work-conserving and may help priority work. The dedicated
|
||||
// priority lane never claims bulk sources, which guarantees reserved fast-path capacity.
|
||||
for (const source of CLAIM_SOURCE_ORDER) {
|
||||
addReadyJobs(await takeReadySourceJobs(source));
|
||||
if (remainingCapacity() === 0) break;
|
||||
if (args.lane === "priority" && source === "publish") break;
|
||||
// Shared workers remain work-conserving and may help priority work. The dedicated
|
||||
// priority lane never claims bulk sources, which guarantees reserved fast-path capacity.
|
||||
for (const source of CLAIM_SOURCE_ORDER) {
|
||||
addReadyJobs(await takeReadySourceJobs(source), source !== "skills-sh-catalog-test");
|
||||
if (remainingCapacity() === 0) break;
|
||||
if (args.lane === "priority" && source === "publish") break;
|
||||
}
|
||||
}
|
||||
|
||||
const claimed = [];
|
||||
let catalogClaims = 0;
|
||||
for (const job of ready) {
|
||||
if (claimed.length >= capacity) break;
|
||||
let catalogAttemptId: Id<"skillsShCatalogScanAttempts"> | null = null;
|
||||
if (job.source === "skills-sh-catalog-test") {
|
||||
if (!job.skillScanRequestId) {
|
||||
continue;
|
||||
}
|
||||
const request = await ctx.db.get(job.skillScanRequestId);
|
||||
const attempt = request?.skillsShCatalogAttemptId
|
||||
? await ctx.db.get(request.skillsShCatalogAttemptId)
|
||||
: null;
|
||||
const run = attempt ? await ctx.db.get(attempt.runId) : null;
|
||||
await loadCatalogGate();
|
||||
const control = catalogGate?.control ?? null;
|
||||
const health = catalogGate?.health ?? null;
|
||||
if (
|
||||
!request ||
|
||||
request.sourceKind !== "skills-sh-catalog" ||
|
||||
!attempt ||
|
||||
attempt.skillScanRequestId !== request._id ||
|
||||
attempt.securityScanJobId !== job._id ||
|
||||
attempt.status !== "queued" ||
|
||||
!run ||
|
||||
run.status === "paused" ||
|
||||
run.status === "canceling" ||
|
||||
run.status === "canceled" ||
|
||||
run.status === "failed" ||
|
||||
!control ||
|
||||
control.mode !== "staging-live" ||
|
||||
control.paused ||
|
||||
!control.scanAdmissionEnabled ||
|
||||
!health?.claimable ||
|
||||
health.catalogInFlight + catalogClaims >= control.maxCatalogInFlight
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
catalogAttemptId = attempt._id;
|
||||
}
|
||||
const leaseToken = crypto.randomUUID();
|
||||
await ctx.db.patch(job._id, {
|
||||
status: "running",
|
||||
@@ -2429,6 +2815,13 @@ export const claimQueuedJobsInternal = internalMutation({
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
if (catalogAttemptId) {
|
||||
await ctx.db.patch(catalogAttemptId, {
|
||||
status: "running",
|
||||
updatedAt: now,
|
||||
});
|
||||
catalogClaims += 1;
|
||||
}
|
||||
claimed.push({
|
||||
...job,
|
||||
status: "running" as const,
|
||||
@@ -2442,6 +2835,169 @@ export const claimQueuedJobsInternal = internalMutation({
|
||||
},
|
||||
});
|
||||
|
||||
async function readCatalogClaimHealth(ctx: MutationCtx, control: Doc<"skillsShCatalogControls">) {
|
||||
const nativeSources = ["publish", "vt-update", "backfill", "bulk-rescan", "manual"] as const;
|
||||
const [nativeQueuedBySource, nativeRunningBySource, catalogQueued, catalogRunning] =
|
||||
await Promise.all([
|
||||
Promise.all(
|
||||
nativeSources.map(async (source) =>
|
||||
ctx.db
|
||||
.query("securityScanJobs")
|
||||
.withIndex("by_status_source_created_at", (q) =>
|
||||
q.eq("status", "queued").eq("source", source),
|
||||
)
|
||||
.take(control.maxNativeQueued + 1),
|
||||
),
|
||||
),
|
||||
Promise.all(
|
||||
nativeSources.map(async (source) =>
|
||||
ctx.db
|
||||
.query("securityScanJobs")
|
||||
.withIndex("by_status_source_created_at", (q) =>
|
||||
q.eq("status", "running").eq("source", source),
|
||||
)
|
||||
.take(control.maxNativeInFlight + 1),
|
||||
),
|
||||
),
|
||||
ctx.db
|
||||
.query("skillsShCatalogScanAttempts")
|
||||
.withIndex("by_dispatch_kind_and_status_and_created_at", (q) =>
|
||||
q.eq("dispatchKind", "real").eq("status", "queued"),
|
||||
)
|
||||
.take(control.maxCatalogQueued + 1),
|
||||
ctx.db
|
||||
.query("skillsShCatalogScanAttempts")
|
||||
.withIndex("by_dispatch_kind_and_status_and_created_at", (q) =>
|
||||
q.eq("dispatchKind", "real").eq("status", "running"),
|
||||
)
|
||||
.take(control.maxCatalogInFlight + 1),
|
||||
]);
|
||||
const nativeQueued = Math.min(
|
||||
control.maxNativeQueued + 1,
|
||||
nativeQueuedBySource.reduce((count, jobs) => count + jobs.length, 0),
|
||||
);
|
||||
const nativeInFlight = Math.min(
|
||||
control.maxNativeInFlight + 1,
|
||||
nativeRunningBySource.reduce((count, jobs) => count + jobs.length, 0),
|
||||
);
|
||||
return {
|
||||
nativeQueued,
|
||||
nativeInFlight,
|
||||
catalogQueued: catalogQueued.length,
|
||||
catalogInFlight: catalogRunning.length,
|
||||
// Queued depth is an admission limit, not a drain limit. Already admitted work
|
||||
// must remain claimable after an operator lowers maxCatalogQueued.
|
||||
claimable:
|
||||
nativeQueued <= control.maxNativeQueued &&
|
||||
nativeInFlight <= control.maxNativeInFlight &&
|
||||
catalogRunning.length <= control.maxCatalogInFlight,
|
||||
};
|
||||
}
|
||||
|
||||
type CatalogRetryDecision =
|
||||
| { kind: "not-catalog" }
|
||||
| {
|
||||
kind: "allowed";
|
||||
attemptId: Id<"skillsShCatalogScanAttempts">;
|
||||
requestId: Id<"skillScanRequests">;
|
||||
}
|
||||
| {
|
||||
kind: "blocked";
|
||||
request?: Doc<"skillScanRequests">;
|
||||
attempt?: Doc<"skillsShCatalogScanAttempts">;
|
||||
run?: Doc<"skillsShCatalogRuns">;
|
||||
};
|
||||
|
||||
async function prepareCatalogRetry(
|
||||
ctx: MutationCtx,
|
||||
job: Doc<"securityScanJobs">,
|
||||
now: number,
|
||||
retryAllowed = true,
|
||||
): Promise<CatalogRetryDecision> {
|
||||
if (job.source !== "skills-sh-catalog-test") return { kind: "not-catalog" };
|
||||
if (job.targetKind !== "skillScanRequest" || !job.skillScanRequestId) {
|
||||
return { kind: "blocked" };
|
||||
}
|
||||
const request = await ctx.db.get(job.skillScanRequestId);
|
||||
if (!request || request.sourceKind !== "skills-sh-catalog" || !request.skillsShCatalogAttemptId) {
|
||||
return { kind: "blocked", ...(request ? { request } : {}) };
|
||||
}
|
||||
const attempt = await ctx.db.get(request.skillsShCatalogAttemptId);
|
||||
if (
|
||||
!attempt ||
|
||||
attempt.skillScanRequestId !== request._id ||
|
||||
attempt.securityScanJobId !== job._id
|
||||
) {
|
||||
return { kind: "blocked", request };
|
||||
}
|
||||
const run = await ctx.db.get(attempt.runId);
|
||||
if (
|
||||
!retryAllowed ||
|
||||
(attempt.status !== "queued" && attempt.status !== "running") ||
|
||||
!run ||
|
||||
run.status === "canceling" ||
|
||||
run.status === "canceled" ||
|
||||
run.status === "failed"
|
||||
) {
|
||||
return { kind: "blocked", request, attempt, ...(run ? { run } : {}) };
|
||||
}
|
||||
await ctx.db.patch(attempt._id, {
|
||||
status: "queued",
|
||||
updatedAt: now,
|
||||
});
|
||||
return {
|
||||
kind: "allowed",
|
||||
attemptId: attempt._id,
|
||||
requestId: request._id,
|
||||
};
|
||||
}
|
||||
|
||||
async function terminalizeBlockedCatalogRetry(
|
||||
ctx: MutationCtx,
|
||||
decision: Extract<CatalogRetryDecision, { kind: "blocked" }>,
|
||||
now: number,
|
||||
) {
|
||||
const attempt = decision.attempt;
|
||||
if (!attempt || (attempt.status !== "queued" && attempt.status !== "running")) return;
|
||||
|
||||
const canceled = decision.run?.status === "canceling" || decision.run?.status === "canceled";
|
||||
await ctx.db.patch(attempt._id, {
|
||||
status: canceled ? "canceled" : "failed",
|
||||
verdict: canceled ? undefined : "failed",
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
const entry = await ctx.db.get(attempt.entryId);
|
||||
const entryShouldBeTerminalized =
|
||||
entry?.sourceContentHash === attempt.sourceContentHash &&
|
||||
(entry.scanStatus === "planned" || entry.scanStatus === "queued");
|
||||
if (entryShouldBeTerminalized) {
|
||||
await ctx.db.patch(entry._id, {
|
||||
scanStatus: canceled ? "canceled" : "failed",
|
||||
publicVisible: false,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
if (decision.run) {
|
||||
await ctx.db.patch(decision.run._id, {
|
||||
counts: {
|
||||
...decision.run.counts,
|
||||
scansCompleted: decision.run.counts.scansCompleted + (canceled ? 0 : 1),
|
||||
scansCanceled: decision.run.counts.scansCanceled + (canceled ? 1 : 0),
|
||||
},
|
||||
operations: {
|
||||
...decision.run.operations,
|
||||
functionCalls: decision.run.operations.functionCalls + 1,
|
||||
dbReads: decision.run.operations.dbReads + 4,
|
||||
dbWrites: decision.run.operations.dbWrites + (entryShouldBeTerminalized ? 3 : 2),
|
||||
},
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const requeueExpiredCodexScanJobsInternal = internalMutation({
|
||||
args: {
|
||||
limit: v.optional(v.number()),
|
||||
@@ -2459,7 +3015,30 @@ export const requeueExpiredCodexScanJobsInternal = internalMutation({
|
||||
Math.min(args.limit ?? MAX_EXPIRED_CODEX_SCAN_LEASE_REQUEUES, MAX_CODEX_SCAN_CLAIM_LIMIT),
|
||||
),
|
||||
);
|
||||
let requeued = 0;
|
||||
for (const job of jobs) {
|
||||
const catalogRetry = await prepareCatalogRetry(ctx, job, now);
|
||||
if (catalogRetry.kind === "blocked") {
|
||||
await terminalizeBlockedCatalogRetry(ctx, catalogRetry, now);
|
||||
await ctx.db.patch(job._id, {
|
||||
status: "failed",
|
||||
lastError: "Catalog scan retry blocked by inactive or invalid linkage",
|
||||
completedAt: now,
|
||||
leaseToken: undefined,
|
||||
leaseExpiresAt: undefined,
|
||||
workerId: undefined,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (catalogRetry.request) {
|
||||
await ctx.db.patch(catalogRetry.request._id, {
|
||||
status: "failed",
|
||||
lastError: "Catalog scan retry blocked by inactive or invalid linkage",
|
||||
completedAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
await ctx.db.patch(job._id, {
|
||||
status: "queued",
|
||||
leaseToken: undefined,
|
||||
@@ -2468,9 +3047,18 @@ export const requeueExpiredCodexScanJobsInternal = internalMutation({
|
||||
nextRunAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (catalogRetry.kind === "allowed") {
|
||||
await ctx.db.patch(catalogRetry.requestId, {
|
||||
status: "queued",
|
||||
lastError: undefined,
|
||||
completedAt: undefined,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
requeued += 1;
|
||||
}
|
||||
if (jobs.length > 0) await requestSecurityScanDispatch(ctx);
|
||||
return { requeued: jobs.length };
|
||||
if (requeued > 0) await requestSecurityScanDispatch(ctx);
|
||||
return { requeued };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2500,11 +3088,13 @@ export const requeueFailedSecurityScanJobsInternal = internalMutation({
|
||||
.gte("updatedAt", args.failedAfter)
|
||||
.lt("updatedAt", args.failedBefore),
|
||||
)
|
||||
.filter((q) => q.neq(q.field("source"), "skills-sh-catalog-test"))
|
||||
.order("asc")
|
||||
.take(limit + 1);
|
||||
const matched = jobs.slice(0, limit);
|
||||
const bySource: Partial<Record<SecurityScanJobSource, number>> = {};
|
||||
const byTargetKind: Partial<Record<Doc<"securityScanJobs">["targetKind"], number>> = {};
|
||||
let requeued = 0;
|
||||
|
||||
for (const job of matched) {
|
||||
bySource[job.source] = (bySource[job.source] ?? 0) + 1;
|
||||
@@ -2553,13 +3143,14 @@ export const requeueFailedSecurityScanJobsInternal = internalMutation({
|
||||
}
|
||||
}
|
||||
}
|
||||
requeued += 1;
|
||||
}
|
||||
|
||||
if (!args.dryRun && matched.length > 0) await requestSecurityScanDispatch(ctx);
|
||||
if (!args.dryRun && requeued > 0) await requestSecurityScanDispatch(ctx);
|
||||
return {
|
||||
dryRun: args.dryRun,
|
||||
matched: matched.length,
|
||||
requeued: args.dryRun ? 0 : matched.length,
|
||||
requeued: args.dryRun ? 0 : requeued,
|
||||
hasMore: jobs.length > limit,
|
||||
bySource,
|
||||
byTargetKind,
|
||||
@@ -2672,8 +3263,12 @@ export const failJobInternal = internalMutation({
|
||||
const job = await ctx.db.get(args.jobId);
|
||||
if (!job || job.leaseToken !== args.leaseToken) throw new ConvexError("Lease mismatch");
|
||||
const now = Date.now();
|
||||
const retry = job.attempts < MAX_ATTEMPTS;
|
||||
const catalogRetry = await prepareCatalogRetry(ctx, job, now, job.attempts < MAX_ATTEMPTS);
|
||||
const retry = job.attempts < MAX_ATTEMPTS && catalogRetry.kind !== "blocked";
|
||||
const error = sanitizeWorkerErrorDetail(args.error, 2000);
|
||||
if (catalogRetry.kind === "blocked") {
|
||||
await terminalizeBlockedCatalogRetry(ctx, catalogRetry, now);
|
||||
}
|
||||
await ctx.db.patch(args.jobId, {
|
||||
status: retry ? "queued" : "failed",
|
||||
lastError: error,
|
||||
@@ -2712,22 +3307,39 @@ export const requeueJobLeaseInternal = internalMutation({
|
||||
throw new ConvexError("Lease mismatch");
|
||||
}
|
||||
const now = Date.now();
|
||||
const catalogRetry = await prepareCatalogRetry(ctx, job, now);
|
||||
const retry = catalogRetry.kind !== "blocked";
|
||||
if (catalogRetry.kind === "blocked") {
|
||||
await terminalizeBlockedCatalogRetry(ctx, catalogRetry, now);
|
||||
}
|
||||
await ctx.db.patch(job._id, {
|
||||
status: "queued",
|
||||
attempts: Math.max(0, job.attempts - 1),
|
||||
status: retry ? "queued" : "failed",
|
||||
attempts: retry ? Math.max(0, job.attempts - 1) : job.attempts,
|
||||
...(retry
|
||||
? {}
|
||||
: {
|
||||
lastError: "Catalog scan retry blocked by inactive or invalid linkage",
|
||||
completedAt: now,
|
||||
}),
|
||||
leaseToken: undefined,
|
||||
leaseExpiresAt: undefined,
|
||||
workerId: undefined,
|
||||
nextRunAt: now + 60_000,
|
||||
nextRunAt: retry ? now + 60_000 : job.nextRunAt,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (job.targetKind === "skillScanRequest" && job.skillScanRequestId) {
|
||||
await ctx.db.patch(job.skillScanRequestId, {
|
||||
status: "queued",
|
||||
status: retry ? "queued" : "failed",
|
||||
...(retry
|
||||
? {}
|
||||
: {
|
||||
lastError: "Catalog scan retry blocked by inactive or invalid linkage",
|
||||
completedAt: now,
|
||||
}),
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
await requestSecurityScanDispatch(ctx);
|
||||
if (retry) await requestSecurityScanDispatch(ctx);
|
||||
return { ok: true as const, nextRunAt: now + 60_000 };
|
||||
},
|
||||
});
|
||||
@@ -2951,7 +3563,13 @@ export const completeCodexScanJob = action({
|
||||
},
|
||||
);
|
||||
if (!target) throw new ConvexError("Job not found");
|
||||
if (target.job.leaseToken !== args.leaseToken) throw new ConvexError("Lease mismatch");
|
||||
const isCatalogScanRequest =
|
||||
target.job.targetKind === "skillScanRequest" &&
|
||||
target.scanRequest?.sourceKind === "skills-sh-catalog" &&
|
||||
Boolean(target.scanRequest.skillsShCatalogAttemptId);
|
||||
if (!isCatalogScanRequest && target.job.leaseToken !== args.leaseToken) {
|
||||
throw new ConvexError("Lease mismatch");
|
||||
}
|
||||
|
||||
if (target.job.targetKind === "skillVersion" && target.version) {
|
||||
if (args.skillSpectorAnalysis) {
|
||||
@@ -3007,6 +3625,36 @@ export const completeCodexScanJob = action({
|
||||
});
|
||||
writtenBack = true;
|
||||
}
|
||||
if (
|
||||
target.scanRequest.sourceKind === "skills-sh-catalog" &&
|
||||
target.scanRequest.skillsShCatalogAttemptId
|
||||
) {
|
||||
const result = await runMutationRef<{ ok: true }>(
|
||||
ctx,
|
||||
internalRefs.securityScan.completeCatalogSkillScanJobInternal,
|
||||
{
|
||||
attemptId: target.scanRequest.skillsShCatalogAttemptId,
|
||||
scanId: target.scanRequest._id,
|
||||
jobId: args.jobId,
|
||||
leaseToken: args.leaseToken,
|
||||
artifactContentHash: target.scanRequest.sha256hash ?? "",
|
||||
verdict: githubSkillScanStatusFromLlmAnalysis(args.llmAnalysis),
|
||||
runId: args.runId,
|
||||
llmAnalysis: args.llmAnalysis,
|
||||
skillSpectorAnalysis,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await runMutationRef(
|
||||
ctx,
|
||||
internalRefs.securityScanDispatch.requestSecurityScanDispatchInternal,
|
||||
{},
|
||||
);
|
||||
} catch {
|
||||
console.warn("security scan dispatch request failed after catalog completion");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
await runMutationRef(ctx, internalRefs.securityScan.recordSkillScanRequestSucceededInternal, {
|
||||
scanId: target.scanRequest._id,
|
||||
jobId: args.jobId,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -70,6 +70,8 @@
|
||||
"seed:test:validate-snapshot": "bun scripts/staging-seed/validate-sanitized-snapshot.ts",
|
||||
"setup:worktree": "bun scripts/setup-worktree.ts",
|
||||
"skill-cards:worker": "bun scripts/skill-cards/run-skill-card-worker.ts",
|
||||
"skills-sh:capture-500": "bun scripts/skills-sh-catalog/capture-frozen-snapshot.ts",
|
||||
"skills-sh:prove-500": "bun scripts/skills-sh-catalog/prove-500.ts",
|
||||
"skills:install": "npx --yes skills@1.5.16 add openclaw/design-system --skill openclaw-design openclaw-brand openclaw-design-system openclaw-marketing-pages openclaw-design-audit --agent codex --copy --yes",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "vitest run -c vitest.e2e.config.ts",
|
||||
@@ -111,6 +113,7 @@
|
||||
"@tanstack/react-router": "1.170.18",
|
||||
"@tanstack/react-start": "1.168.32",
|
||||
"@vercel/analytics": "2.0.1",
|
||||
"@vercel/oidc": "^3.8.0",
|
||||
"@vercel/speed-insights": "2.0.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clawhub-schema": "workspace:0.0.2",
|
||||
@@ -146,6 +149,7 @@
|
||||
"zod": "4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edge-runtime/vm": "^5.0.0",
|
||||
"@faker-js/faker": "^10.5.0",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@react-email/ui": "^6.9.0",
|
||||
@@ -161,6 +165,7 @@
|
||||
"@typescript/native": "npm:typescript@7.0.2",
|
||||
"@vitejs/plugin-react": "6.0.3",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"convex-test": "^0.0.54",
|
||||
"jsdom": "29.1.1",
|
||||
"nitro": "3.0.260610-beta",
|
||||
"only-allow": "1.2.2",
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
{
|
||||
"verdict": "pass",
|
||||
"generatedAt": "2026-07-21T09:02:16.188Z",
|
||||
"deployment": {
|
||||
"kind": "disposable-local-convex",
|
||||
"name": "local-amantus-clawdhub-7449",
|
||||
"cloudUrl": "http://127.0.0.1:3210",
|
||||
"permanentTestDeployed": false,
|
||||
"productionDeployed": false
|
||||
},
|
||||
"runtime": {
|
||||
"elapsedMs": 42507,
|
||||
"memory": {
|
||||
"backendPid": 97459,
|
||||
"backendRssStartKiB": 816368,
|
||||
"backendRssPeakKiB": 891456,
|
||||
"backendRssEndKiB": 863968,
|
||||
"driverRssStartBytes": 51904512,
|
||||
"driverRssPeakBytes": 75415552,
|
||||
"driverRssEndBytes": 75415552,
|
||||
"driverHeapPeakBytes": 6130176
|
||||
},
|
||||
"clientCalls": {
|
||||
"mutation": 279,
|
||||
"query": 297,
|
||||
"expectedErrors": 8,
|
||||
"unexpectedErrors": 0
|
||||
},
|
||||
"sourceFetchesDuringProof": 0,
|
||||
"frozenSnapshotCaptureFetches": 528
|
||||
},
|
||||
"defaultState": {
|
||||
"controls": {
|
||||
"discoveryEnabled": false,
|
||||
"maxCatalogInFlight": 0,
|
||||
"maxCatalogQueued": 0,
|
||||
"maxEntriesPerBatch": 0,
|
||||
"maxEntriesPerRun": 0,
|
||||
"maxNativeInFlight": 0,
|
||||
"maxNativeQueued": 0,
|
||||
"maxPlannedScans": 0,
|
||||
"maxScanAdmissionsPerBatch": 0,
|
||||
"maxScanAdmissionsPerDay": 0,
|
||||
"maxScanAdmissionsPerRun": 0,
|
||||
"maxWritesPerBatch": 0,
|
||||
"mode": "off",
|
||||
"paused": true,
|
||||
"publicVisibilityEnabled": false,
|
||||
"realScanAllowlist": [],
|
||||
"reason": null,
|
||||
"scanAdmissionEnabled": false,
|
||||
"scanPlanningEnabled": false,
|
||||
"updatedAt": null,
|
||||
"updatedBy": null,
|
||||
"writesEnabled": false
|
||||
},
|
||||
"schedulesPresent": false,
|
||||
"catalogScanAttempts": 0,
|
||||
"realScanAttempts": 0
|
||||
},
|
||||
"discovery20000": {
|
||||
"observed": 20000,
|
||||
"plannedScans": 20000,
|
||||
"wouldInsert": 20000,
|
||||
"persistedInserts": 0,
|
||||
"authoritativePersistedEntries": 0,
|
||||
"authoritativeScanAttempts": 0,
|
||||
"authoritativeNativeScanJobs": 0,
|
||||
"batches": 200,
|
||||
"operationEstimates": {
|
||||
"dbReads": 20401,
|
||||
"dbWrites": 201,
|
||||
"functionCalls": 201
|
||||
}
|
||||
},
|
||||
"frozen500": {
|
||||
"capture": {
|
||||
"snapshotId": "skills-sh-500-2026-07-21",
|
||||
"capturedAt": "2026-07-21T02:35:28.482Z",
|
||||
"sourceFetches": 528
|
||||
},
|
||||
"configuredBudgets": {
|
||||
"maxEntriesPerBatch": 125,
|
||||
"maxEntriesPerRun": 500,
|
||||
"maxPlannedScans": 500,
|
||||
"maxScanAdmissionsPerBatch": 49,
|
||||
"maxScanAdmissionsPerDay": 500,
|
||||
"maxScanAdmissionsPerRun": 500,
|
||||
"maxWritesPerBatch": 100
|
||||
},
|
||||
"firstRun": {
|
||||
"expectedActions": {
|
||||
"wouldInsert": 500,
|
||||
"wouldUpdate": 0,
|
||||
"scansPlanned": 500
|
||||
},
|
||||
"persistedActions": {
|
||||
"inserted": 500,
|
||||
"updated": 0,
|
||||
"authoritativeEntries": 500,
|
||||
"authoritativeAttempts": 496,
|
||||
"completedDeterministicAttempts": 496
|
||||
},
|
||||
"counts": {
|
||||
"inserted": 500,
|
||||
"observed": 500,
|
||||
"rejected": 0,
|
||||
"scansAdmitted": 496,
|
||||
"scansCanceled": 0,
|
||||
"scansCompleted": 496,
|
||||
"scansPlanned": 500,
|
||||
"unchanged": 0,
|
||||
"updated": 0,
|
||||
"wouldInsert": 500,
|
||||
"wouldUpdate": 0
|
||||
},
|
||||
"batches": {
|
||||
"batchesProcessed": 6,
|
||||
"entriesObserved": 500,
|
||||
"lastBatchReads": 7,
|
||||
"lastBatchWrites": 6,
|
||||
"scanAdmissionBatches": 11,
|
||||
"scansAdmitted": 496,
|
||||
"scansPlanned": 500
|
||||
},
|
||||
"operationEstimates": {
|
||||
"dbReads": 2163,
|
||||
"dbWrites": 2515,
|
||||
"functionCalls": 31
|
||||
}
|
||||
},
|
||||
"pauseResume": {
|
||||
"pausedCursor": 99,
|
||||
"pausedObserved": 99,
|
||||
"resumedFinalCursor": 500,
|
||||
"resumedFinalObserved": 500,
|
||||
"replayedCompletedBatch": false
|
||||
},
|
||||
"identicalRerun": {
|
||||
"counts": {
|
||||
"inserted": 0,
|
||||
"observed": 500,
|
||||
"rejected": 0,
|
||||
"scansAdmitted": 0,
|
||||
"scansCanceled": 0,
|
||||
"scansCompleted": 0,
|
||||
"scansPlanned": 0,
|
||||
"unchanged": 500,
|
||||
"updated": 0,
|
||||
"wouldInsert": 0,
|
||||
"wouldUpdate": 0
|
||||
},
|
||||
"authoritativeAttempts": 0,
|
||||
"operationEstimates": {
|
||||
"dbReads": 1023,
|
||||
"dbWrites": 507,
|
||||
"functionCalls": 7
|
||||
}
|
||||
},
|
||||
"changedRerun": {
|
||||
"counts": {
|
||||
"inserted": 0,
|
||||
"observed": 500,
|
||||
"rejected": 0,
|
||||
"scansAdmitted": 1,
|
||||
"scansCanceled": 0,
|
||||
"scansCompleted": 1,
|
||||
"scansPlanned": 1,
|
||||
"unchanged": 498,
|
||||
"updated": 2,
|
||||
"wouldInsert": 0,
|
||||
"wouldUpdate": 2
|
||||
},
|
||||
"authoritativeAttempts": 1,
|
||||
"exactSourceHashRescan": "665c585455decbcb02116bc928adf5148b9cd0f67b7eb4908c8c0d647c00f680",
|
||||
"artifactHash": null,
|
||||
"hashSemantics": "sourceContentHash is the upstream observation; deterministic fixture completion is not an independent ClawScan artifact hash",
|
||||
"operationEstimates": {
|
||||
"dbReads": 1040,
|
||||
"dbWrites": 513,
|
||||
"functionCalls": 9
|
||||
}
|
||||
},
|
||||
"finalAuthoritativeEntries": 500,
|
||||
"allEntriesDark": true
|
||||
},
|
||||
"concurrency": {
|
||||
"sameRun": {
|
||||
"fulfilled": 1,
|
||||
"rejected": 1,
|
||||
"cap": 1
|
||||
},
|
||||
"queueHealth": {
|
||||
"queuedAtThresholdRejected": true,
|
||||
"inFlightAtThresholdRejected": true,
|
||||
"maxCatalogQueued": 1,
|
||||
"maxCatalogInFlight": 1,
|
||||
"maxNativeQueued": 0,
|
||||
"maxNativeInFlight": 0,
|
||||
"queuedThresholdObserved": {
|
||||
"nativeQueued": 0,
|
||||
"nativeInFlight": 0,
|
||||
"catalogQueued": 1,
|
||||
"catalogInFlight": 0
|
||||
},
|
||||
"inFlightThresholdObserved": {
|
||||
"nativeQueued": 0,
|
||||
"nativeInFlight": 0,
|
||||
"catalogQueued": 1,
|
||||
"catalogInFlight": 1
|
||||
}
|
||||
},
|
||||
"crossRunDaily": {
|
||||
"fulfilled": 1,
|
||||
"rejected": 1,
|
||||
"cap": 499,
|
||||
"attemptsBeforeRace": 498
|
||||
},
|
||||
"finalCatalogAttemptsToday": 500
|
||||
},
|
||||
"admissionContainment": {
|
||||
"deterministicOnlyInLocalProof": true,
|
||||
"realDispatchOutsideStagingLiveRejected": true,
|
||||
"realDispatchRejection": "[Request ID: add843a2d41d4f84] Server Error\nUncaught ConvexError: real skills.sh scan admission requires staging-live controls\n at handler (../convex/skillsShCatalog.ts:915:14)\n at async handler (../../node_modules/convex-helpers/server/customFunctions.js:268:27)\n"
|
||||
},
|
||||
"lifecycleContainment": {
|
||||
"canceledRunningAttempts": 1,
|
||||
"canceledQueuedAttempts": 1,
|
||||
"canceledAttemptReadback": [
|
||||
{
|
||||
"dispatchKind": "deterministic",
|
||||
"status": "canceled"
|
||||
},
|
||||
{
|
||||
"dispatchKind": "deterministic",
|
||||
"status": "canceled"
|
||||
}
|
||||
],
|
||||
"canceledRunAdmissionRejected": true,
|
||||
"canceledRunAdmissionRejection": "[Request ID: 286aefc65cc575ac] Server Error\nUncaught ConvexError: Cannot admit scans for canceled run\n at handler (../convex/skillsShCatalog.ts:902:24)\n at async handler (../../node_modules/convex-helpers/server/customFunctions.js:268:27)\n",
|
||||
"killSwitchAdmissionRejected": true,
|
||||
"killSwitchAdmissionRejection": "[Request ID: b4c1e019ae188e8a] Server Error\nUncaught ConvexError: skills.sh catalog controls are disabled\n at assertCatalogActive (../convex/skillsShCatalog.ts:157:14)\n at assertScanAdmissionEnabled (../convex/skillsShCatalog.ts:179:14)\n at handler (../convex/skillsShCatalog.ts:893:24)\n at async handler (../../node_modules/convex-helpers/server/customFunctions.js:268:27)\n"
|
||||
},
|
||||
"rollback": {
|
||||
"controls": {
|
||||
"discoveryEnabled": false,
|
||||
"maxCatalogInFlight": 10,
|
||||
"maxCatalogQueued": 50,
|
||||
"maxEntriesPerBatch": 125,
|
||||
"maxEntriesPerRun": 500,
|
||||
"maxNativeInFlight": 0,
|
||||
"maxNativeQueued": 0,
|
||||
"maxPlannedScans": 500,
|
||||
"maxScanAdmissionsPerBatch": 49,
|
||||
"maxScanAdmissionsPerDay": 500,
|
||||
"maxScanAdmissionsPerRun": 500,
|
||||
"maxWritesPerBatch": 100,
|
||||
"mode": "off",
|
||||
"paused": true,
|
||||
"publicVisibilityEnabled": false,
|
||||
"realScanAllowlist": [],
|
||||
"reason": "visibility and control rollback proof",
|
||||
"scanAdmissionEnabled": false,
|
||||
"scanPlanningEnabled": false,
|
||||
"updatedAt": 1784624535953,
|
||||
"updatedBy": "claw-556-local-proof",
|
||||
"writesEnabled": false
|
||||
},
|
||||
"schedulesPresent": false,
|
||||
"nativeBefore": {
|
||||
"skills": 0,
|
||||
"scanJobs": 0,
|
||||
"sha256": "3da07503745b0261278284c398b652c73edeb004df305c460686e5b0b12c05b0"
|
||||
},
|
||||
"nativeAfter": {
|
||||
"skills": 0,
|
||||
"scanJobs": 0,
|
||||
"sha256": "3da07503745b0261278284c398b652c73edeb004df305c460686e5b0b12c05b0"
|
||||
},
|
||||
"nativeStateUnchanged": true
|
||||
},
|
||||
"limits": {
|
||||
"operationCountsAreEstimates": true,
|
||||
"realTestQueueIntegration": "not deployed; real attempts require an allowlisted Test control and an external artifact fetch before completion",
|
||||
"statusPreviewLimit": {
|
||||
"entries": 50,
|
||||
"runs": 20,
|
||||
"scanAttempts": 50
|
||||
},
|
||||
"authoritativeReadback": "all entry, attempt, native skill, and native scan-job counts above use paginated internal queries"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"issue": "CLAW-556",
|
||||
"generatedAt": "2026-07-21T14:23:00Z",
|
||||
"question": "Does the permanent Test staging seam fetch and process exactly 500 live skills through the ClawHub Vercel OIDC boundary without broad scan admission, public visibility, scheduling, or native-state changes?",
|
||||
"verdict": "pass_for_review",
|
||||
"sourceRevision": {
|
||||
"gitSha": "06e8ff1a2b71f9d544623f85c4284446ad7878de",
|
||||
"branch": "pe/claw-556-skills-sh-catalog-control-plane",
|
||||
"convexDeployment": "academic-chihuahua-392",
|
||||
"convexBuildSha": "06e8ff1a2b71f9d544623f85c4284446ad7878de",
|
||||
"convexReadbackAt": "2026-07-21T14:09:48Z",
|
||||
"liveProofInvocationSha": "f74f895a0c0fe13c3a49de7f9ff0a9e6a9df0446",
|
||||
"hardeningScopeAfterLiveProof": "Artifact byte/hash validation, authoritative staging identity checks, direct real-dispatch rejection, admin authorization, missing-run callback rejection, source-kind collision rejection, allowlist normalization, terminal retry synchronization, strict fixture-versus-staging-live mode isolation, bounded multi-file write accounting, backlog draining, and terminal catalog-attempt synchronization. Discovery, normalization, persistence batching, cursoring, and the 500-row source contract were unchanged."
|
||||
},
|
||||
"vercel": {
|
||||
"project": "openclaw-foundation/clawhub",
|
||||
"verifiedOidcIdentity": {
|
||||
"ownerId": "team_pLdjXbfy0XvPRiNmAygTjTSH",
|
||||
"projectId": "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc",
|
||||
"environment": "test"
|
||||
},
|
||||
"liveInvocation": {
|
||||
"deploymentId": "dpl_9ndfyttJWaTC3LZibUgL371TstFx",
|
||||
"url": "clawhub-nh4plivpm-openclaw-foundation.vercel.app",
|
||||
"target": "test",
|
||||
"sourceSha": "f74f895a0c0fe13c3a49de7f9ff0a9e6a9df0446",
|
||||
"proofCompleted": true,
|
||||
"deploymentState": "ERROR",
|
||||
"deploymentLimit": "The one-shot proof build intentionally did not emit the normal application output after the bounded invocation, so Vercel marked that invocation deployment as ERROR after the proof response was captured."
|
||||
},
|
||||
"finalInertDeployment": {
|
||||
"deploymentId": "dpl_FvMrDG1W3PCDs2v5PggkF4Bnt7sV",
|
||||
"url": "clawhub-368srhyyu-openclaw-foundation.vercel.app",
|
||||
"target": "test",
|
||||
"sourceSha": "06e8ff1a2b71f9d544623f85c4284446ad7878de",
|
||||
"readyState": "READY",
|
||||
"createdAt": "2026-07-21T14:20:41.046Z",
|
||||
"runtime": "nodejs24.x",
|
||||
"memorySizeMiB": 2048,
|
||||
"timeoutSeconds": 300,
|
||||
"liveInvocationMarkerPresent": false,
|
||||
"externalProtectedStatuses": {
|
||||
"/ops/skills-sh/catalog-test": 302,
|
||||
"/api/v1/operator/skills-sh/catalog-test": 302
|
||||
},
|
||||
"deploymentProtectionEnabled": true,
|
||||
"newCatalogRunsAfterDeploy": 0,
|
||||
"sourceBinding": {
|
||||
"gitCommitSha": "06e8ff1a2b71f9d544623f85c4284446ad7878de",
|
||||
"purpose": "clean inert exact-head deployment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"liveSnapshot": {
|
||||
"snapshotId": "skills-sh-test-live-500:5dbb0caa415371e5",
|
||||
"capturedAt": "2026-07-21T09:55:56.277Z",
|
||||
"rows": 500,
|
||||
"nvidiaRows": 10,
|
||||
"requiredCollisionIds": [
|
||||
"anthropics/skills/frontend-design",
|
||||
"anthropics/claude-code/frontend-design"
|
||||
],
|
||||
"skillsShFetches": {
|
||||
"total": 507,
|
||||
"list": 2,
|
||||
"search": 1,
|
||||
"detail": 504,
|
||||
"runtimeMs": 38317,
|
||||
"skippedIncompleteDetails": 1
|
||||
},
|
||||
"githubOwnerResolution": {
|
||||
"authenticatedFetches": 70,
|
||||
"immutableOwnerIdsRequiredForEverySelectedRow": true,
|
||||
"placeholderOwnerIdsAccepted": false
|
||||
}
|
||||
},
|
||||
"configuredLimits": {
|
||||
"discoveryRows": 500,
|
||||
"entriesPerBatch": 50,
|
||||
"entriesPerRun": 500,
|
||||
"writesPerBatch": 100,
|
||||
"plannedScansPerRun": 500,
|
||||
"explicitRealScanAllowlistCeiling": 10,
|
||||
"realScanAdmissionsPerBatch": 1,
|
||||
"realScanAdmissionsPerRun": 1,
|
||||
"realScanAdmissionsPerDay": 1,
|
||||
"nativeQueuedThreshold": 3,
|
||||
"nativeInFlightThreshold": 0,
|
||||
"catalogQueuedThreshold": 1,
|
||||
"catalogInFlightThreshold": 1,
|
||||
"schedulesEnabled": false,
|
||||
"publicVisibilityEnabled": false
|
||||
},
|
||||
"liveRun": {
|
||||
"runId": "h17w3pzcbww82myvmz8a1msnxh8azbss",
|
||||
"status": "completed",
|
||||
"cursor": 500,
|
||||
"runtimeMs": 16148,
|
||||
"batchesProcessed": 10,
|
||||
"expectedActions": {
|
||||
"observed": 500,
|
||||
"inserts": 0,
|
||||
"updates": 0,
|
||||
"unchanged": 500,
|
||||
"scanPlans": 0,
|
||||
"realScanAdmissions": 0
|
||||
},
|
||||
"persistedActions": {
|
||||
"catalogEntries": 500,
|
||||
"inserted": 0,
|
||||
"updated": 0,
|
||||
"unchanged": 500,
|
||||
"scanAttempts": 0,
|
||||
"realScanJobs": 0
|
||||
},
|
||||
"consumedBudgets": {
|
||||
"entriesObserved": 500,
|
||||
"batchesProcessed": 10,
|
||||
"scansPlanned": 0,
|
||||
"scanAdmissionBatches": 0,
|
||||
"scansAdmitted": 0,
|
||||
"lastBatchReadsEstimate": 102,
|
||||
"lastBatchWritesEstimate": 51
|
||||
},
|
||||
"operationEstimates": {
|
||||
"dbReads": 1021,
|
||||
"dbWrites": 511,
|
||||
"functionCalls": 11
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
"identicalRerun": {
|
||||
"runId": "h17qgyp7yhwt5g4dvfyppdw0gd8ayt02",
|
||||
"status": "completed",
|
||||
"cursor": 500,
|
||||
"runtimeMs": 15979,
|
||||
"batchesProcessed": 10,
|
||||
"observed": 500,
|
||||
"unchanged": 500,
|
||||
"inserted": 0,
|
||||
"updated": 0,
|
||||
"scanPlans": 0,
|
||||
"scanAttempts": 0,
|
||||
"realScanJobs": 0,
|
||||
"errors": []
|
||||
},
|
||||
"changedInputRun": {
|
||||
"runId": "h17n18bmyq5tr5mdka784mm0dn8aycxe",
|
||||
"status": "completed",
|
||||
"runtimeMs": 16635,
|
||||
"observed": 500,
|
||||
"persistedMetadataUpdates": 453,
|
||||
"unchanged": 47,
|
||||
"changedSourceHashes": 3,
|
||||
"scanPlans": 3,
|
||||
"scanAttempts": 0,
|
||||
"realScanJobs": 0,
|
||||
"changedExternalIds": [
|
||||
"alchaincyf/huashu-design/huashu-design",
|
||||
"getpaperclipai/paperclip/paperclip-converting-plans-to-tasks",
|
||||
"getpaperclipai/paperclip/para-memory-files"
|
||||
],
|
||||
"errors": []
|
||||
},
|
||||
"pauseCancelKillSwitch": {
|
||||
"runId": "h17thn76pzx9phr0w0g9rxgrw58aznq9",
|
||||
"cursorAfterFirstBatch": 50,
|
||||
"pausedBatchRejected": true,
|
||||
"cursorAfterResumeBatch": 100,
|
||||
"batchesAfterResume": 2,
|
||||
"completedBatchReplayObserved": false,
|
||||
"finalStatus": "canceled",
|
||||
"attemptsCanceled": 0,
|
||||
"discoveryRejectedAfterKillSwitch": true,
|
||||
"admissionRejectedAfterKillSwitch": true
|
||||
},
|
||||
"admission": {
|
||||
"requested": 0,
|
||||
"admitted": 0,
|
||||
"skipped": 0,
|
||||
"realQueueAfterRun": 0,
|
||||
"catalogAttemptsAfterRun": 0,
|
||||
"nativeQueueBefore": {
|
||||
"queued": 3,
|
||||
"inFlight": 0
|
||||
},
|
||||
"nativeQueueAfter": {
|
||||
"queued": 3,
|
||||
"inFlight": 0
|
||||
},
|
||||
"reasonForZeroAdmissions": "The selected NVIDIA and representative collision candidates exposed incomplete source file bodies. The real path requires a complete fetched artifact and exact artifact hash, so admission failed closed rather than weakening validation."
|
||||
},
|
||||
"runtime": {
|
||||
"totalElapsedMs": 75008,
|
||||
"rssStartBytes": 160522240,
|
||||
"rssEndBytes": 269778944,
|
||||
"rssDeltaBytes": 109256704,
|
||||
"heapUsedStartBytes": 19133656,
|
||||
"heapUsedEndBytes": 98823704,
|
||||
"heapUsedDeltaBytes": 79690048,
|
||||
"sourceFetches": 507,
|
||||
"githubFetches": 70,
|
||||
"errors": 0,
|
||||
"operationCountsAreEstimates": true
|
||||
},
|
||||
"authoritativeReadback": {
|
||||
"nativeSkills": {
|
||||
"beforeCount": 67616,
|
||||
"afterCount": 67616,
|
||||
"beforeSha256": "267d17957e61a49f37dd009a5d88194b3587daf86281e253522ff1b7c9ad80f7",
|
||||
"afterSha256": "267d17957e61a49f37dd009a5d88194b3587daf86281e253522ff1b7c9ad80f7",
|
||||
"identical": true,
|
||||
"pagesReadAfter": 68
|
||||
},
|
||||
"nativeSecurityScanJobs": {
|
||||
"beforeCount": 3,
|
||||
"afterCount": 3,
|
||||
"beforeSha256": "3c08230f417303a66c0678e7c6192864d045bca72581ab1b51a9908c64f36ddc",
|
||||
"afterSha256": "3c08230f417303a66c0678e7c6192864d045bca72581ab1b51a9908c64f36ddc",
|
||||
"identical": true,
|
||||
"sources": {
|
||||
"publish": 3
|
||||
},
|
||||
"statuses": {
|
||||
"queued": 3
|
||||
}
|
||||
},
|
||||
"catalog": {
|
||||
"entries": 500,
|
||||
"entriesPublicVisible": 0,
|
||||
"entriesInstallable": 0,
|
||||
"scanStatusPlanned": 500,
|
||||
"scanAttempts": 0,
|
||||
"realScanQueue": 0
|
||||
}
|
||||
},
|
||||
"finalExactHeadReadback": {
|
||||
"capturedAt": "2026-07-21T14:22:02Z",
|
||||
"convexDeployment": "academic-chihuahua-392",
|
||||
"convexBuildSha": "06e8ff1a2b71f9d544623f85c4284446ad7878de",
|
||||
"vercelDeploymentId": "dpl_FvMrDG1W3PCDs2v5PggkF4Bnt7sV",
|
||||
"vercelSourceSha": "06e8ff1a2b71f9d544623f85c4284446ad7878de",
|
||||
"latestCatalogRunUpdatedAt": 1784627791018,
|
||||
"latestCatalogRunPredatesFinalDeploy": true,
|
||||
"control": {
|
||||
"mode": "off",
|
||||
"paused": true,
|
||||
"discoveryEnabled": false,
|
||||
"writesEnabled": false,
|
||||
"scanPlanningEnabled": false,
|
||||
"scanAdmissionEnabled": false,
|
||||
"publicVisibilityEnabled": false,
|
||||
"realScanAllowlist": []
|
||||
},
|
||||
"nativeSkills": {
|
||||
"count": 67616,
|
||||
"pagesRead": 68,
|
||||
"pageJsonSha256": "987ae4e1dd9bb5539b7febd1223f6a3eb8080cdbad759d3d7033cbe54f6d42f1"
|
||||
},
|
||||
"nativeSecurityScanJobs": {
|
||||
"count": 3,
|
||||
"pagesRead": 1,
|
||||
"cliJsonSha256": "e22b14a16593fb8be93959a91161da2a51f6fa52c62c570fea4425e5641b0759",
|
||||
"sources": {
|
||||
"publish": 3
|
||||
},
|
||||
"statuses": {
|
||||
"queued": 3
|
||||
},
|
||||
"catalogAttributed": 0
|
||||
},
|
||||
"catalog": {
|
||||
"entries": 500,
|
||||
"entryPagesRead": 1,
|
||||
"entriesCliJsonSha256": "eede6eb7beff0ce4959b38b2e64faf55c44dcff1f27a9ed5ba36affc89afba66",
|
||||
"sourceKinds": {
|
||||
"staging-live": 500
|
||||
},
|
||||
"scanStatuses": {
|
||||
"planned": 500
|
||||
},
|
||||
"entriesPublicVisible": 0,
|
||||
"entriesInstallable": 0,
|
||||
"scanAttempts": 0,
|
||||
"attemptPagesRead": 1,
|
||||
"attemptsCliJsonSha256": "2e2fdeefcc79db019282c22b2db56c47c9019a8b47a40a9083c53746c92f7caa",
|
||||
"realScanAttempts": 0
|
||||
},
|
||||
"hashNote": "These fresh hashes cover complete paginated Test readback at exact head 06e8ff1a. They are serialization-specific snapshots and are not compared numerically with the in-run ConvexHttpClient hashes; authoritative counts, source/status distributions, and the unchanged latest run timestamp establish that the inert deployments created no catalog or native work."
|
||||
},
|
||||
"finalControls": {
|
||||
"mode": "off",
|
||||
"paused": true,
|
||||
"discoveryEnabled": false,
|
||||
"writesEnabled": false,
|
||||
"scanPlanningEnabled": false,
|
||||
"scanAdmissionEnabled": false,
|
||||
"publicVisibilityEnabled": false,
|
||||
"realScanAllowlist": [],
|
||||
"schedulesPresent": false,
|
||||
"githubTokenPresentInConvex": false,
|
||||
"temporaryOperatorTokensRevoked": true,
|
||||
"temporaryInvocationFilesRemoved": true,
|
||||
"productionTouched": false
|
||||
},
|
||||
"alternateAuthenticatedRetry": {
|
||||
"attemptedAt": "2026-07-21T14:11:43Z",
|
||||
"requiredAnotherLiveFetch": false,
|
||||
"sourceFetches": 0,
|
||||
"catalogRunsCreated": 0,
|
||||
"scanAttemptsCreated": 0,
|
||||
"productionTouched": false,
|
||||
"diagnosis": [
|
||||
"The protected Test route could not be reached through vercel curl because the project automation bypass secret is intentionally unavailable locally.",
|
||||
"Two build-time OIDC retries failed before source authorization: one referenced the wrong token function name and one lacked the explicit Test build marker.",
|
||||
"A third deployment restored and ran the normal build command, so it produced no source fetch or catalog mutation."
|
||||
],
|
||||
"cleanup": {
|
||||
"controlMode": "off",
|
||||
"paused": true,
|
||||
"discoveryEnabled": false,
|
||||
"writesEnabled": false,
|
||||
"scanPlanningEnabled": false,
|
||||
"scanAdmissionEnabled": false,
|
||||
"publicVisibilityEnabled": false,
|
||||
"realScanAllowlist": [],
|
||||
"temporaryOperatorTokenRevoked": true,
|
||||
"temporaryInvocationFilePresent": false,
|
||||
"normalBuildCommandRestored": true
|
||||
}
|
||||
},
|
||||
"overloadAcceptance": {
|
||||
"local20000RowProofArtifact": "proof/claw-556/skills-sh-500-local-proof.json",
|
||||
"discovery20000RowsCreatesAutomaticNativeJobs": 0,
|
||||
"discovery20000RowsCreatesCatalogAttempts": 0,
|
||||
"permanentTestFetchRows": 500,
|
||||
"permanentTestRealScanCeiling": 10,
|
||||
"permanentTestRealScansObserved": 0,
|
||||
"discoveryWritesAdmissionVisibilityIndependentlyControlled": true,
|
||||
"unhealthyOrExhaustedAdmissionFailsClosed": true,
|
||||
"dailySchedulingDefaultOff": true
|
||||
},
|
||||
"hardeningValidation": {
|
||||
"focusedTests": {
|
||||
"files": 6,
|
||||
"testsPassed": 152
|
||||
},
|
||||
"convexCodegenWithTypeScript": "passed",
|
||||
"ciStatic": "passed",
|
||||
"ciUnit": {
|
||||
"filesPassed": 382,
|
||||
"filesSkipped": 1,
|
||||
"testsPassed": 4949,
|
||||
"testsSkipped": 1,
|
||||
"statementsPercent": 85.89,
|
||||
"branchesPercent": 76.71,
|
||||
"functionsPercent": 86.95,
|
||||
"linesPercent": 88.59
|
||||
},
|
||||
"ciTypesBuild": "passed",
|
||||
"structuredAutoreview": {
|
||||
"branchFindingAccepted": "Fixture work could advance under staging-live controls.",
|
||||
"fix": "Fixture start, processing, deterministic admission, start, completion, and callback now require fixture mode and a fixture run.",
|
||||
"finalLocalReview": "clean after final exact-head readback correction"
|
||||
},
|
||||
"liveFetchRepeatedAfterHardening": false,
|
||||
"reasonLiveFetchWasNotRepeated": "The post-proof changes only tightened staging admission, artifact validation, environment authorization, callback/retry synchronization, source-kind collision handling, fixture-versus-live mode isolation, bounded write accounting, and backlog draining. The exact 500-row source and discovery path was unchanged; exact head 06e8ff1a was deployed inert to both permanent Test runtimes and read back with zero new runs, attempts, or jobs."
|
||||
},
|
||||
"limits": [
|
||||
"The skills.sh detail contract did not provide complete file bodies for the selected real-scan candidates, so this gate proves fail-closed artifact admission rather than a terminal real ClawScan verdict.",
|
||||
"The live 500-row invocation ran at f74f895a; final hardened descendant 06e8ff1a was deployed to both exact Test runtimes with the live-fetch marker absent. The proof records this split explicitly rather than claiming a second live fetch.",
|
||||
"Vercel local-upload deployments do not expose a gitSource object. The source SHA is bound by the clean detached worktree, the fixed one-shot invoker assertion, the Convex APP_BUILD_SHA readback, and the final conventional commit chain.",
|
||||
"Database operation counts are code-path estimates. Runtime, memory, fetch counts, full paginated table counts, and canonical SHA-256 comparisons are measured."
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* @vitest-environment node */
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -10,6 +11,7 @@ import { processJob } from "./run-codex-scan-worker";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const execFileAsync = promisify(execFile);
|
||||
const sha256 = (content: string) => createHash("sha256").update(content).digest("hex");
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -45,7 +47,7 @@ function skillVersionJob(jobId: string): ClaimedJob {
|
||||
files: [
|
||||
{
|
||||
path: "SKILL.md",
|
||||
sha256: "abc123",
|
||||
sha256: sha256("# Skill"),
|
||||
size: 42,
|
||||
url: "data:text/plain,%23%20Skill",
|
||||
},
|
||||
@@ -86,7 +88,7 @@ function fileTarget(path: string, content: string): ClaimedJob["target"] {
|
||||
files: [
|
||||
{
|
||||
path,
|
||||
sha256: "artifact-sha",
|
||||
sha256: sha256(content),
|
||||
size: Buffer.byteLength(content),
|
||||
url: `data:text/plain,${encodeURIComponent(content)}`,
|
||||
},
|
||||
|
||||
@@ -385,7 +385,7 @@ describe("run-codex-scan-worker diagnostics", () => {
|
||||
files: [
|
||||
{
|
||||
path: "SKILL.md",
|
||||
sha256: "abc123",
|
||||
sha256: "e2151f8490121dc5e6fd36c1d4e00b6da5593595e3eb8ece76c1d0ec3f310979",
|
||||
size: 42,
|
||||
url: "data:text/plain,%23%20Skill",
|
||||
},
|
||||
@@ -409,7 +409,42 @@ describe("run-codex-scan-worker diagnostics", () => {
|
||||
source: "publish",
|
||||
targetKind: "skillVersion",
|
||||
});
|
||||
expect(metadata.target.files).toEqual([{ path: "SKILL.md", sha256: "abc123", size: 42 }]);
|
||||
expect(metadata.target.files).toEqual([
|
||||
{
|
||||
path: "SKILL.md",
|
||||
sha256: "e2151f8490121dc5e6fd36c1d4e00b6da5593595e3eb8ece76c1d0ec3f310979",
|
||||
size: 42,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("rejects downloaded bytes that do not match the stored file hash", async () => {
|
||||
const workspace = await tempDir();
|
||||
await expect(
|
||||
writeArtifactWorkspace(
|
||||
{
|
||||
job: {
|
||||
_id: "catalog-job",
|
||||
hasMaliciousSignal: false,
|
||||
leaseToken: "test-auth-token",
|
||||
source: "skills-sh-catalog-test",
|
||||
targetKind: "skillScanRequest",
|
||||
waitForVtUntil: 0,
|
||||
},
|
||||
target: {
|
||||
files: [
|
||||
{
|
||||
path: "SKILL.md",
|
||||
sha256: "0".repeat(64),
|
||||
size: 7,
|
||||
url: "data:text/plain,%23%20Skill",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
workspace,
|
||||
),
|
||||
).rejects.toThrow("Downloaded artifact hash mismatch for artifact file SKILL.md");
|
||||
});
|
||||
|
||||
it("materializes zero-byte directory markers with descendant files", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { mkdirSync, readFileSync } from "node:fs";
|
||||
import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -237,7 +238,8 @@ function parseArgs() {
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
||||
};
|
||||
const laneValue = get("--lane") ?? process.env.CODEX_SECURITY_SCAN_LANE;
|
||||
const lane: "priority" | "shared" = laneValue === "priority" ? "priority" : "shared";
|
||||
const lane: "priority" | "shared" | "catalog" =
|
||||
laneValue === "priority" || laneValue === "catalog" ? laneValue : "shared";
|
||||
return {
|
||||
batchLimit: numberFrom(
|
||||
get("--batch-limit") ?? get("--limit") ?? process.env.CODEX_SECURITY_SCAN_LIMIT,
|
||||
@@ -757,7 +759,14 @@ export async function writeArtifactWorkspace(job: ClaimedJob, workspace: string)
|
||||
|
||||
const { file, out } = candidate;
|
||||
await mkdir(dirname(out), { recursive: true });
|
||||
await writeFile(out, await download(file.url, { kind: "file", path: file.path }));
|
||||
const bytes = await download(file.url, { kind: "file", path: file.path });
|
||||
const actualSha256 = createHash("sha256").update(bytes).digest("hex");
|
||||
if (actualSha256 !== file.sha256.toLowerCase()) {
|
||||
throw new Error(
|
||||
`Downloaded artifact hash mismatch for artifact file ${safeWorkerArtifactPathLabel(file.path)}`,
|
||||
);
|
||||
}
|
||||
await writeFile(out, bytes);
|
||||
}
|
||||
|
||||
if (job.target.clawpackUrl) {
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/usr/bin/env bun
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import {
|
||||
fetchSkillsShCatalogDetail,
|
||||
fetchSkillsShCatalogPage,
|
||||
searchSkillsShCatalog,
|
||||
type SkillsShCatalogListRow,
|
||||
} from "../../server/skillsShCatalogSource";
|
||||
import { resolveAuthenticatedGitHubOwner } from "./github-owner-resolution";
|
||||
|
||||
const SNAPSHOT_ID = "skills-sh-500-2026-07-21";
|
||||
const ROW_LIMIT = 500;
|
||||
const DETAIL_CANDIDATE_LIMIT = 525;
|
||||
const DETAIL_CONCURRENCY = 8;
|
||||
const OUTPUT_PATH = resolve("convex/fixtures/skills-sh-500-2026-07-21.json");
|
||||
const REQUIRED_COLLISION_IDS = [
|
||||
"anthropics/skills/frontend-design",
|
||||
"anthropics/claude-code/frontend-design",
|
||||
] as const;
|
||||
|
||||
type FrozenRow = {
|
||||
externalId: string;
|
||||
githubOwnerId: number;
|
||||
owner: string;
|
||||
repo: string;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
sourceUrl: string;
|
||||
githubRepoUrl: string;
|
||||
sourceContentHash: string;
|
||||
installs: number;
|
||||
};
|
||||
|
||||
function isGitHubRow(row: SkillsShCatalogListRow) {
|
||||
const normalizedSource = row.source.trim().toLowerCase();
|
||||
const normalizedSlug = row.slug.trim().toLowerCase();
|
||||
return (
|
||||
row.sourceType === "github" &&
|
||||
normalizedSource.split("/").length === 2 &&
|
||||
/^[a-z0-9][a-z0-9-]*$/.test(normalizedSlug) &&
|
||||
row.id.trim().toLowerCase() === `${normalizedSource}/${normalizedSlug}`
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRow(row: SkillsShCatalogListRow) {
|
||||
const [owner = "", repo = ""] = row.source.split("/");
|
||||
return {
|
||||
...row,
|
||||
id: `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase()}/${row.slug
|
||||
.trim()
|
||||
.toLowerCase()}`,
|
||||
owner: owner.trim().toLowerCase(),
|
||||
repo: repo.trim().toLowerCase(),
|
||||
slug: row.slug.trim().toLowerCase(),
|
||||
};
|
||||
}
|
||||
|
||||
async function mapWithConcurrency<T, R>(
|
||||
values: readonly T[],
|
||||
concurrency: number,
|
||||
mapper: (value: T, index: number) => Promise<R>,
|
||||
) {
|
||||
const results = Array.from<R>({ length: values.length });
|
||||
let nextIndex = 0;
|
||||
async function worker() {
|
||||
while (nextIndex < values.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
results[index] = await mapper(values[index] as T, index);
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(concurrency, values.length) }, () => worker()));
|
||||
return results;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const startedAt = Date.now();
|
||||
const candidates = new Map<string, ReturnType<typeof normalizeRow>>();
|
||||
let listFetches = 0;
|
||||
for (let page = 0; candidates.size < ROW_LIMIT + 100; page += 1) {
|
||||
const response = await fetchSkillsShCatalogPage({ page, perPage: ROW_LIMIT });
|
||||
listFetches += 1;
|
||||
for (const row of response.data) {
|
||||
if (!isGitHubRow(row)) continue;
|
||||
const normalized = normalizeRow(row);
|
||||
candidates.set(normalized.id, normalized);
|
||||
}
|
||||
if (!response.pagination.hasMore) break;
|
||||
}
|
||||
|
||||
const nvidia = await searchSkillsShCatalog({
|
||||
query: "nvidia",
|
||||
owner: "nvidia",
|
||||
limit: 200,
|
||||
});
|
||||
const nvidiaRows = nvidia.data
|
||||
.filter(isGitHubRow)
|
||||
.map(normalizeRow)
|
||||
.filter((row) => row.owner === "nvidia")
|
||||
.slice(0, 10);
|
||||
|
||||
const requiredIds = new Set<string>([
|
||||
...REQUIRED_COLLISION_IDS,
|
||||
...nvidiaRows.map((row) => row.id),
|
||||
]);
|
||||
for (const row of nvidiaRows) candidates.set(row.id, row);
|
||||
for (const id of requiredIds) {
|
||||
if (!candidates.has(id)) {
|
||||
throw new Error(`Required skills.sh fixture row is missing: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const detailCandidates = [
|
||||
...Array.from(requiredIds, (id) => candidates.get(id)!),
|
||||
...Array.from(candidates.values()).filter((row) => !requiredIds.has(row.id)),
|
||||
].slice(0, DETAIL_CANDIDATE_LIMIT);
|
||||
if (detailCandidates.length !== DETAIL_CANDIDATE_LIMIT) {
|
||||
throw new Error(
|
||||
`Expected ${DETAIL_CANDIDATE_LIMIT} detail candidates, received ${detailCandidates.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const details = await mapWithConcurrency(
|
||||
detailCandidates,
|
||||
DETAIL_CONCURRENCY,
|
||||
async (row) => await fetchSkillsShCatalogDetail(row.id),
|
||||
);
|
||||
|
||||
const selected = detailCandidates
|
||||
.map((row, index) => ({ row, detail: details[index] }))
|
||||
.filter(
|
||||
(
|
||||
candidate,
|
||||
): candidate is {
|
||||
row: ReturnType<typeof normalizeRow>;
|
||||
detail: NonNullable<(typeof details)[number]> & { hash: string };
|
||||
} =>
|
||||
candidate.detail !== null &&
|
||||
candidate.detail.id.toLowerCase() === candidate.row.id &&
|
||||
typeof candidate.detail.hash === "string" &&
|
||||
/^[a-f0-9]{64}$/i.test(candidate.detail.hash),
|
||||
)
|
||||
.slice(0, ROW_LIMIT);
|
||||
if (selected.length !== ROW_LIMIT) {
|
||||
throw new Error(`Only ${selected.length} rows had canonical ids and exact content hashes`);
|
||||
}
|
||||
for (const id of requiredIds) {
|
||||
if (!selected.some((candidate) => candidate.row.id === id)) {
|
||||
throw new Error(`Required skills.sh fixture row lacks an exact detail hash: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
const owners = Array.from(new Set(selected.map(({ row }) => row.owner))).sort();
|
||||
const ownerIds = new Map(
|
||||
owners.map((owner) => {
|
||||
const resolved = resolveAuthenticatedGitHubOwner(owner);
|
||||
return [owner, resolved.id] as const;
|
||||
}),
|
||||
);
|
||||
const rows: FrozenRow[] = selected.map(({ row, detail }) => {
|
||||
return {
|
||||
externalId: row.id,
|
||||
githubOwnerId: ownerIds.get(row.owner)!,
|
||||
owner: row.owner,
|
||||
repo: row.repo,
|
||||
slug: row.slug,
|
||||
displayName: row.name.trim() || row.slug,
|
||||
sourceUrl: row.url,
|
||||
githubRepoUrl: row.installUrl ?? `https://github.com/${row.owner}/${row.repo}`,
|
||||
sourceContentHash: detail.hash.toLowerCase(),
|
||||
installs: row.installs,
|
||||
};
|
||||
});
|
||||
|
||||
const capturedAt = new Date().toISOString();
|
||||
const payload = {
|
||||
snapshotId: SNAPSHOT_ID,
|
||||
capturedAt,
|
||||
source: {
|
||||
project: "openclaw-foundation/clawhub",
|
||||
endpoint: "https://skills.sh/api/v1/skills",
|
||||
authentication: "vercel-oidc",
|
||||
requestedRows: ROW_LIMIT,
|
||||
},
|
||||
selection: {
|
||||
rows: rows.length,
|
||||
nvidiaRows: rows.filter((row) => row.owner === "nvidia").length,
|
||||
requiredCollisionIds: REQUIRED_COLLISION_IDS,
|
||||
},
|
||||
captureMetrics: {
|
||||
runtimeMs: Date.now() - startedAt,
|
||||
skillsShFetches: listFetches + 1 + details.length,
|
||||
githubOwnerFetches: owners.length,
|
||||
listFetches,
|
||||
searchFetches: 1,
|
||||
detailFetches: details.length,
|
||||
},
|
||||
rows,
|
||||
};
|
||||
await mkdir(dirname(OUTPUT_PATH), { recursive: true });
|
||||
await writeFile(OUTPUT_PATH, `${JSON.stringify(payload, null, 2)}\n`);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
outputPath: OUTPUT_PATH,
|
||||
snapshotId: SNAPSHOT_ID,
|
||||
rows: rows.length,
|
||||
nvidiaRows: payload.selection.nvidiaRows,
|
||||
collisionIds: REQUIRED_COLLISION_IDS,
|
||||
captureMetrics: payload.captureMetrics,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -0,0 +1,20 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export function resolveAuthenticatedGitHubOwner(ownerInput: string) {
|
||||
const owner = ownerInput.trim().toLowerCase();
|
||||
if (!owner) throw new Error("GitHub owner is required");
|
||||
const result = spawnSync("gh", ["api", `users/${owner}`, "--jq", "{id,login}"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`Authenticated GitHub owner lookup failed: ${owner}`);
|
||||
}
|
||||
const payload = JSON.parse(result.stdout) as { id?: unknown; login?: unknown };
|
||||
const id = typeof payload.id === "number" ? payload.id : Number.NaN;
|
||||
const login = typeof payload.login === "string" ? payload.login.trim().toLowerCase() : "";
|
||||
if (!Number.isSafeInteger(id) || id <= 0 || login !== owner) {
|
||||
throw new Error(`Authenticated GitHub owner lookup returned invalid identity: ${owner}`);
|
||||
}
|
||||
return { owner, id, login };
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
function requireEnv(name: string) {
|
||||
const value = process.env[name]?.trim();
|
||||
if (!value) throw new Error(`${name} is required`);
|
||||
return value;
|
||||
}
|
||||
|
||||
const targetUrl = requireEnv("CLAWHUB_TEST_CATALOG_GATE_URL");
|
||||
const operatorAuthorization = requireEnv("CLAWHUB_TEST_OPERATOR_TOKEN");
|
||||
const allowlist = (process.env.CLAWHUB_TEST_CATALOG_ALLOWLIST ?? "")
|
||||
.split(",")
|
||||
.map((externalId) => externalId.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
if (allowlist.length > 10) throw new Error("CLAWHUB_TEST_CATALOG_ALLOWLIST cannot exceed 10");
|
||||
|
||||
async function callGate(body: Record<string, unknown>) {
|
||||
const response = await fetch(targetUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${operatorAuthorization}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return { response, text: await response.text() };
|
||||
}
|
||||
|
||||
const request = {
|
||||
allowlist,
|
||||
reason: "CLAW-556 bounded permanent Test proof",
|
||||
};
|
||||
const execution = await callGate(request);
|
||||
if (!execution.response.ok) {
|
||||
throw new Error(
|
||||
`Permanent Test catalog gate returned HTTP ${execution.response.status}: ${execution.text}`,
|
||||
);
|
||||
}
|
||||
console.log(JSON.stringify(JSON.parse(execution.text) as Record<string, unknown>));
|
||||
|
||||
export {};
|
||||
@@ -0,0 +1,248 @@
|
||||
import { getVercelOidcToken } from "@vercel/oidc";
|
||||
import { defineEventHandler, getHeader, readBody } from "h3";
|
||||
import {
|
||||
captureSkillsShCatalogTestSnapshot,
|
||||
getSkillsShCatalogTestSourcePolicy,
|
||||
type SkillsShCatalogGitHubOwnerProof,
|
||||
} from "../../../skillsShCatalogSource";
|
||||
|
||||
const TEST_CONVEX_SITE_URL = "https://academic-chihuahua-392.convex.site";
|
||||
const OPERATOR_PATH = "/api/v1/operator/skills-sh/catalog-test";
|
||||
const MAX_BATCH_SIZE = 50;
|
||||
|
||||
type CatalogTestRequest = {
|
||||
allowlist?: string[];
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
function parseCatalogTestRequest(value: unknown): CatalogTestRequest | null {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
||||
const body = value as Record<string, unknown>;
|
||||
if (
|
||||
(body.allowlist !== undefined &&
|
||||
(!Array.isArray(body.allowlist) ||
|
||||
!body.allowlist.every((externalId) => typeof externalId === "string"))) ||
|
||||
(body.reason !== undefined && typeof body.reason !== "string")
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...(body.allowlist !== undefined ? { allowlist: body.allowlist as string[] } : {}),
|
||||
...(body.reason !== undefined ? { reason: body.reason as string } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function jsonResponse(payload: unknown, status = 200) {
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status,
|
||||
headers: {
|
||||
"Cache-Control": "no-store",
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function callConvexOperator(
|
||||
authorization: string,
|
||||
init: { method: "GET" } | { method: "POST"; body: Record<string, unknown> },
|
||||
) {
|
||||
const response = await fetch(`${TEST_CONVEX_SITE_URL}${OPERATOR_PATH}`, {
|
||||
method: init.method,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: authorization,
|
||||
...(init.method === "POST" ? { "Content-Type": "application/json" } : {}),
|
||||
},
|
||||
...(init.method === "POST" ? { body: JSON.stringify(init.body) } : {}),
|
||||
});
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`Convex Test operator returned HTTP ${response.status}: ${text.slice(0, 300)}`);
|
||||
}
|
||||
return JSON.parse(text) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
async function executeSnapshotRun(
|
||||
authorization: string,
|
||||
snapshot: {
|
||||
snapshotId: string;
|
||||
capturedAt: string;
|
||||
rows: Array<Record<string, unknown>>;
|
||||
metrics: { skillsShFetches: number };
|
||||
},
|
||||
reason: string,
|
||||
batchSize: number,
|
||||
) {
|
||||
const start = await callConvexOperator(authorization, {
|
||||
method: "POST",
|
||||
body: {
|
||||
operation: "start",
|
||||
reason,
|
||||
snapshotId: snapshot.snapshotId,
|
||||
sourceCapturedAt: snapshot.capturedAt,
|
||||
snapshotCaptureFetches: snapshot.metrics.skillsShFetches,
|
||||
fixtureLength: snapshot.rows.length,
|
||||
},
|
||||
});
|
||||
const runId = start.runId;
|
||||
if (typeof runId !== "string") throw new Error("Convex Test operator did not return a run id");
|
||||
|
||||
let run: Record<string, unknown> | null = null;
|
||||
for (let cursor = 0; cursor < snapshot.rows.length; cursor += batchSize) {
|
||||
run = await callConvexOperator(authorization, {
|
||||
method: "POST",
|
||||
body: {
|
||||
operation: "batch",
|
||||
runId,
|
||||
cursor,
|
||||
rows: snapshot.rows.slice(cursor, cursor + batchSize),
|
||||
},
|
||||
});
|
||||
}
|
||||
if (run?.status !== "completed" || run.cursor !== snapshot.rows.length) {
|
||||
throw new Error(`Convex Test snapshot run did not complete all ${snapshot.rows.length} rows`);
|
||||
}
|
||||
return { runId, run };
|
||||
}
|
||||
|
||||
function batchSizeFromControl(control: Record<string, unknown>) {
|
||||
const maxEntriesPerBatch = control.maxEntriesPerBatch;
|
||||
const maxWritesPerBatch = control.maxWritesPerBatch;
|
||||
if (
|
||||
typeof maxEntriesPerBatch !== "number" ||
|
||||
!Number.isInteger(maxEntriesPerBatch) ||
|
||||
maxEntriesPerBatch < 1 ||
|
||||
typeof maxWritesPerBatch !== "number" ||
|
||||
!Number.isInteger(maxWritesPerBatch) ||
|
||||
maxWritesPerBatch < 2
|
||||
) {
|
||||
throw new Error("Convex Test controls do not provide usable batch budgets");
|
||||
}
|
||||
return Math.min(MAX_BATCH_SIZE, maxEntriesPerBatch, Math.floor(maxWritesPerBatch / 2));
|
||||
}
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const policy = getSkillsShCatalogTestSourcePolicy(process.env);
|
||||
if (!policy.allowed) return jsonResponse({ error: "not_found" }, 404);
|
||||
const authorization = getHeader(event, "authorization")?.trim() ?? "";
|
||||
if (!authorization.toLowerCase().startsWith("bearer ")) {
|
||||
return jsonResponse({ error: "operator_authorization_required" }, 401);
|
||||
}
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await readBody(event);
|
||||
} catch {
|
||||
return jsonResponse({ error: "invalid_request_body" }, 400);
|
||||
}
|
||||
const body = parseCatalogTestRequest(rawBody);
|
||||
if (!body) return jsonResponse({ error: "invalid_request_body" }, 400);
|
||||
const allowlist = Array.from(
|
||||
new Set((body.allowlist ?? []).map((externalId) => externalId.trim().toLowerCase())),
|
||||
).filter(Boolean);
|
||||
if (allowlist.length > policy.maxRealScanAdmissions) {
|
||||
return jsonResponse({ error: "allowlist_exceeds_test_ceiling" }, 400);
|
||||
}
|
||||
const memoryStart = process.memoryUsage();
|
||||
const startedAt = Date.now();
|
||||
try {
|
||||
const staging = await callConvexOperator(authorization, { method: "GET" });
|
||||
const control = staging.control;
|
||||
if (!control || typeof control !== "object") {
|
||||
throw new Error("Convex Test operator did not return catalog controls");
|
||||
}
|
||||
const batchSize = batchSizeFromControl(control as Record<string, unknown>);
|
||||
const snapshot = await captureSkillsShCatalogTestSnapshot({
|
||||
env: process.env,
|
||||
async getOidcToken() {
|
||||
return await getVercelOidcToken();
|
||||
},
|
||||
readConvexControl: async () =>
|
||||
control as {
|
||||
mode: "off" | "fixture" | "staging-live";
|
||||
discoveryEnabled: boolean;
|
||||
writesEnabled: boolean;
|
||||
scanPlanningEnabled: boolean;
|
||||
maxEntriesPerRun: number;
|
||||
publicVisibilityEnabled: boolean;
|
||||
},
|
||||
admitExternalIds: allowlist,
|
||||
resolveGitHubOwners: async (owners) =>
|
||||
(await callConvexOperator(authorization, {
|
||||
method: "POST",
|
||||
body: {
|
||||
operation: "resolve-owners",
|
||||
owners,
|
||||
},
|
||||
})) as SkillsShCatalogGitHubOwnerProof,
|
||||
});
|
||||
const reason = body.reason?.trim() || "bounded permanent Test skills.sh proof";
|
||||
const firstRun = await executeSnapshotRun(authorization, snapshot, reason, batchSize);
|
||||
const identicalRerun = await executeSnapshotRun(
|
||||
authorization,
|
||||
snapshot,
|
||||
`${reason} identical rerun`,
|
||||
batchSize,
|
||||
);
|
||||
const admission =
|
||||
allowlist.length > 0
|
||||
? await callConvexOperator(authorization, {
|
||||
method: "POST",
|
||||
body: {
|
||||
operation: "admit",
|
||||
runId: firstRun.runId,
|
||||
externalIds: allowlist,
|
||||
artifacts: snapshot.artifacts,
|
||||
},
|
||||
})
|
||||
: { requested: 0, admitted: 0, skipped: 0 };
|
||||
if (
|
||||
admission.requested !== allowlist.length ||
|
||||
admission.admitted !== allowlist.length ||
|
||||
admission.skipped !== 0
|
||||
) {
|
||||
throw new Error("Convex Test did not admit the complete allowlist");
|
||||
}
|
||||
const memoryEnd = process.memoryUsage();
|
||||
return jsonResponse({
|
||||
ok: true,
|
||||
source: {
|
||||
project: "openclaw-foundation/clawhub",
|
||||
vercelSourceSha: process.env.VERCEL_GIT_COMMIT_SHA ?? null,
|
||||
vercelTargetEnvironment: process.env.VERCEL_TARGET_ENV ?? null,
|
||||
verifiedIdentity: snapshot.verifiedIdentity,
|
||||
snapshotId: snapshot.snapshotId,
|
||||
capturedAt: snapshot.capturedAt,
|
||||
selection: snapshot.selection,
|
||||
fetches: snapshot.metrics,
|
||||
},
|
||||
convex: {
|
||||
deploymentName: staging.deploymentName,
|
||||
buildSha: staging.buildSha,
|
||||
firstRun,
|
||||
identicalRerun,
|
||||
admission,
|
||||
},
|
||||
runtime: {
|
||||
elapsedMs: Date.now() - startedAt,
|
||||
rssStartBytes: memoryStart.rss,
|
||||
rssEndBytes: memoryEnd.rss,
|
||||
heapUsedStartBytes: memoryStart.heapUsed,
|
||||
heapUsedEndBytes: memoryEnd.heapUsed,
|
||||
},
|
||||
controls: {
|
||||
publicVisibilityEnabled: false,
|
||||
schedulesEnabled: false,
|
||||
maxRealScanAdmissions: policy.maxRealScanAdmissions,
|
||||
batchSize,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "skills_sh_catalog_test_failed",
|
||||
message: error instanceof Error ? error.message : "Unknown Test gate failure",
|
||||
},
|
||||
502,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,538 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
captureSkillsShCatalogTestSnapshot,
|
||||
fetchSkillsShCatalogDetail,
|
||||
fetchSkillsShCatalogPage,
|
||||
fetchSkillsShCatalogTestPage,
|
||||
getSkillsShCatalogTestSourcePolicy,
|
||||
SkillsShCatalogOwnerProofRequiredError,
|
||||
validateSkillsShCatalogGitHubOwnerProof,
|
||||
} from "./skillsShCatalogSource";
|
||||
|
||||
describe("skills.sh Vercel source boundary", () => {
|
||||
it("uses only the injected Vercel OIDC token for source authentication", async () => {
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [],
|
||||
pagination: { page: 0, perPage: 500, total: 0, hasMore: false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await fetchSkillsShCatalogPage(
|
||||
{ page: 0, perPage: 500 },
|
||||
{
|
||||
env: { VERCEL_OIDC_TOKEN: "short-lived-vercel-oidc" },
|
||||
fetchImpl,
|
||||
},
|
||||
);
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith("https://skills.sh/api/v1/skills?page=0&per_page=500", {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: "Bearer short-lived-vercel-oidc",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts a request-bound OIDC token without requiring an environment copy", async () => {
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: [],
|
||||
pagination: { page: 0, perPage: 500, total: 0, hasMore: false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await fetchSkillsShCatalogPage(
|
||||
{ page: 0, perPage: 500 },
|
||||
{
|
||||
env: {},
|
||||
oidcToken: "request-bound-oidc",
|
||||
fetchImpl,
|
||||
},
|
||||
);
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(expect.any(String), {
|
||||
headers: expect.objectContaining({
|
||||
Authorization: "Bearer request-bound-oidc",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed without OIDC and above the 500-row boundary", async () => {
|
||||
await expect(
|
||||
fetchSkillsShCatalogPage({ page: 0, perPage: 500 }, { env: {}, fetchImpl: vi.fn() }),
|
||||
).rejects.toThrow("requires VERCEL_OIDC_TOKEN");
|
||||
await expect(
|
||||
fetchSkillsShCatalogPage(
|
||||
{ page: 0, perPage: 501 },
|
||||
{ env: { VERCEL_OIDC_TOKEN: "token" }, fetchImpl: vi.fn() },
|
||||
),
|
||||
).rejects.toThrow("perPage must be an integer between 1 and 500");
|
||||
});
|
||||
|
||||
it("preserves repository-qualified detail ids", async () => {
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id: "anthropics/claude-code/frontend-design",
|
||||
source: "anthropics/claude-code",
|
||||
slug: "frontend-design",
|
||||
installs: 1,
|
||||
hash: "hash",
|
||||
files: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
await fetchSkillsShCatalogDetail("anthropics/claude-code/frontend-design", {
|
||||
env: { VERCEL_OIDC_TOKEN: "token" },
|
||||
fetchImpl,
|
||||
});
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
"https://skills.sh/api/v1/skills/anthropics/claude-code/frontend-design",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("requires the Test build, Preview runtime, baked backend, and explicit enable", () => {
|
||||
expect(
|
||||
getSkillsShCatalogTestSourcePolicy({
|
||||
VERCEL_ENV: "preview",
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED: "1",
|
||||
}),
|
||||
).toMatchObject({ allowed: false });
|
||||
|
||||
expect(
|
||||
getSkillsShCatalogTestSourcePolicy({
|
||||
VERCEL_ENV: "preview",
|
||||
VERCEL_TARGET_ENV: "test",
|
||||
VITE_CLAWHUB_DEPLOY_ENV: "test",
|
||||
VITE_CONVEX_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED: "1",
|
||||
}),
|
||||
).toEqual({
|
||||
allowed: true,
|
||||
environment: "test",
|
||||
maxDiscoveryRows: 500,
|
||||
maxRealScanAdmissions: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it("requires exact authenticated immutable owner coverage for the selected live set", () => {
|
||||
expect(
|
||||
Array.from(
|
||||
validateSkillsShCatalogGitHubOwnerProof(["anthropics", "nvidia"], {
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "live-github",
|
||||
fetches: 2,
|
||||
reused: 0,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
["anthropics", 76_263_028],
|
||||
["nvidia", 1_728_152],
|
||||
]);
|
||||
|
||||
expect(() =>
|
||||
validateSkillsShCatalogGitHubOwnerProof(["anthropics", "nvidia"], {
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "live-github",
|
||||
fetches: 1,
|
||||
reused: 0,
|
||||
owners: [{ owner: "nvidia", login: "nvidia", id: 1_728_152 }],
|
||||
}),
|
||||
).toThrow("lacks complete authenticated GitHub owner proof");
|
||||
expect(() =>
|
||||
validateSkillsShCatalogGitHubOwnerProof(["nvidia"], {
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
fetches: 0,
|
||||
reused: 1,
|
||||
owners: [{ owner: "nvidia", login: "renamed-owner", id: 1_728_152 }],
|
||||
}),
|
||||
).toThrow("invalid authenticated GitHub owner proof");
|
||||
});
|
||||
|
||||
it("selects 500 hash-qualified rows before requiring exact live owner proofs", async () => {
|
||||
const listRows = [
|
||||
{
|
||||
id: "anthropics/skills/frontend-design",
|
||||
installUrl: "https://github.com/anthropics/skills",
|
||||
installs: 100,
|
||||
name: "Frontend Design",
|
||||
slug: "frontend-design",
|
||||
source: "anthropics/skills",
|
||||
sourceType: "github",
|
||||
url: "https://skills.sh/anthropics/skills/frontend-design",
|
||||
},
|
||||
{
|
||||
id: "anthropics/claude-code/frontend-design",
|
||||
installUrl: "https://github.com/anthropics/claude-code",
|
||||
installs: 99,
|
||||
name: "Frontend Design",
|
||||
slug: "frontend-design",
|
||||
source: "anthropics/claude-code",
|
||||
sourceType: "github",
|
||||
url: "https://skills.sh/anthropics/claude-code/frontend-design",
|
||||
},
|
||||
...Array.from({ length: 498 }, (_, index) => ({
|
||||
id: `owner/repo-${index}/skill-${index}`,
|
||||
installUrl: `https://github.com/owner/repo-${index}`,
|
||||
installs: index,
|
||||
name: `Skill ${index}`,
|
||||
slug: `skill-${index}`,
|
||||
source: `owner/repo-${index}`,
|
||||
sourceType: "github",
|
||||
url: `https://skills.sh/owner/repo-${index}/skill-${index}`,
|
||||
})),
|
||||
];
|
||||
const nvidiaRows = Array.from({ length: 10 }, (_, index) => ({
|
||||
id: `nvidia/skills/nvidia-skill-${index}`,
|
||||
installUrl: "https://github.com/nvidia/skills",
|
||||
installs: 1_000 - index,
|
||||
name: `NVIDIA Skill ${index}`,
|
||||
slug: `nvidia-skill-${index}`,
|
||||
source: "nvidia/skills",
|
||||
sourceType: "github",
|
||||
url: `https://skills.sh/nvidia/skills/nvidia-skill-${index}`,
|
||||
}));
|
||||
const detailUrls: string[] = [];
|
||||
const fetchImpl = vi.fn(async (urlInput: string | URL | Request) => {
|
||||
const url = String(urlInput);
|
||||
if (url.includes("/skills?")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: listRows,
|
||||
pagination: {
|
||||
page: 0,
|
||||
perPage: 500,
|
||||
total: 500,
|
||||
hasMore: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (url.includes("/skills/search?")) {
|
||||
return new Response(JSON.stringify({ data: nvidiaRows }));
|
||||
}
|
||||
detailUrls.push(url);
|
||||
const id = decodeURIComponent(url.split("/api/v1/skills/")[1] ?? "");
|
||||
if (id === "owner/repo-0/skill-0") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
source: "owner/repo-0",
|
||||
slug: "skill-0",
|
||||
installs: 1,
|
||||
hash: null,
|
||||
files: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (id === "owner/repo-1/skill-1") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
source: "owner/repo-1",
|
||||
slug: "skill-1",
|
||||
installs: 1,
|
||||
hash: "b".repeat(64),
|
||||
files: null,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (id === "owner/repo-2/skill-2") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
source: "owner/repo-2",
|
||||
slug: "skill-2",
|
||||
installs: 1,
|
||||
hash: "c".repeat(64),
|
||||
files: [{ name: "SKILL.md" }],
|
||||
}),
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
id,
|
||||
source: id.split("/").slice(0, 2).join("/"),
|
||||
slug: id.split("/").at(-1),
|
||||
installs: 1,
|
||||
hash: "a".repeat(64),
|
||||
files: [{ name: "SKILL.md", content: `# ${id}` }],
|
||||
}),
|
||||
);
|
||||
});
|
||||
const readOidc = async () => "oidc";
|
||||
const verifyOidc = async () => ({
|
||||
payload: {
|
||||
owner_id: "team_pLdjXbfy0XvPRiNmAygTjTSH",
|
||||
project_id: "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc",
|
||||
environment: "test",
|
||||
sub: "owner:project:test",
|
||||
aud: "https://vercel.com",
|
||||
iss: "https://oidc.vercel.com",
|
||||
},
|
||||
});
|
||||
const options = {
|
||||
env: {
|
||||
VERCEL_ENV: "preview",
|
||||
VITE_CLAWHUB_DEPLOY_ENV: "test",
|
||||
VITE_CONVEX_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED: "1",
|
||||
},
|
||||
fetchImpl: fetchImpl as typeof fetch,
|
||||
getOidcToken: readOidc,
|
||||
verifyOidcToken: verifyOidc,
|
||||
readConvexControl: async () => ({
|
||||
mode: "staging-live" as const,
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
publicVisibilityEnabled: false,
|
||||
}),
|
||||
admitExternalIds: ["nvidia/skills/nvidia-skill-0"],
|
||||
};
|
||||
|
||||
await expect(
|
||||
captureSkillsShCatalogTestSnapshot({
|
||||
...options,
|
||||
admitExternalIds: ["owner/repo-1/skill-1"],
|
||||
}),
|
||||
).rejects.toThrow("live admission lacks artifact files");
|
||||
detailUrls.length = 0;
|
||||
|
||||
await expect(
|
||||
captureSkillsShCatalogTestSnapshot({
|
||||
...options,
|
||||
admitExternalIds: ["owner/repo-2/skill-2"],
|
||||
}),
|
||||
).rejects.toThrow("live admission has incomplete artifact files");
|
||||
detailUrls.length = 0;
|
||||
|
||||
const preflight = await captureSkillsShCatalogTestSnapshot(options).catch((error) => error);
|
||||
expect(preflight).toBeInstanceOf(SkillsShCatalogOwnerProofRequiredError);
|
||||
expect(preflight).toMatchObject({
|
||||
owners: ["anthropics", "nvidia", "owner"],
|
||||
sourcePreflight: {
|
||||
skillsShFetches: 506,
|
||||
listFetches: 1,
|
||||
searchFetches: 1,
|
||||
detailFetches: 504,
|
||||
selection: {
|
||||
rows: 500,
|
||||
nvidiaRows: 10,
|
||||
skippedIncompleteDetails: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(detailUrls).toHaveLength(504);
|
||||
|
||||
detailUrls.length = 0;
|
||||
const snapshot = await captureSkillsShCatalogTestSnapshot({
|
||||
...options,
|
||||
githubOwnerProof: {
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
fetches: 0,
|
||||
reused: 3,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
{ owner: "owner", login: "owner", id: 123 },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(snapshot.rows).toHaveLength(500);
|
||||
expect(snapshot.selection).toMatchObject({
|
||||
rows: 500,
|
||||
nvidiaRows: 10,
|
||||
requiredCollisionIds: [
|
||||
"anthropics/skills/frontend-design",
|
||||
"anthropics/claude-code/frontend-design",
|
||||
],
|
||||
skippedIncompleteDetails: 1,
|
||||
});
|
||||
expect(snapshot.rows.some((row) => row.externalId === "owner/repo-0/skill-0")).toBe(false);
|
||||
expect(snapshot.artifacts).toHaveLength(1);
|
||||
expect(snapshot.metrics).toMatchObject({
|
||||
skillsShFetches: 506,
|
||||
listFetches: 1,
|
||||
searchFetches: 1,
|
||||
detailFetches: 504,
|
||||
githubOwnerFetches: 0,
|
||||
githubOwnerIdsReused: 3,
|
||||
githubOwnerProofProvenance: "stored-authenticated-staging-live",
|
||||
skippedIncompleteDetails: 1,
|
||||
});
|
||||
expect(detailUrls).toHaveLength(504);
|
||||
});
|
||||
|
||||
it("rejects an ordinary Preview even when spoofable Test strings are present", async () => {
|
||||
await expect(
|
||||
fetchSkillsShCatalogTestPage({
|
||||
env: {
|
||||
VERCEL_ENV: "preview",
|
||||
VERCEL_TARGET_ENV: "test",
|
||||
VITE_CLAWHUB_DEPLOY_ENV: "test",
|
||||
VITE_CONVEX_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED: "1",
|
||||
},
|
||||
getOidcToken: async () => "ordinary-preview-token",
|
||||
verifyOidcToken: async () => {
|
||||
throw new Error("unexpected Vercel project");
|
||||
},
|
||||
readConvexControl: async () => ({
|
||||
mode: "staging-live",
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
publicVisibilityEnabled: false,
|
||||
}),
|
||||
}),
|
||||
).rejects.toThrow("unexpected Vercel project");
|
||||
});
|
||||
|
||||
it("rejects a verified ClawHub Preview token without the custom Test environment claim", async () => {
|
||||
await expect(
|
||||
fetchSkillsShCatalogTestPage({
|
||||
env: {
|
||||
VERCEL_ENV: "preview",
|
||||
VERCEL_TARGET_ENV: "test",
|
||||
VITE_CLAWHUB_DEPLOY_ENV: "test",
|
||||
VITE_CONVEX_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED: "1",
|
||||
},
|
||||
getOidcToken: async () => "ordinary-preview-token",
|
||||
verifyOidcToken: async () => ({
|
||||
payload: {
|
||||
owner_id: "team_pLdjXbfy0XvPRiNmAygTjTSH",
|
||||
project_id: "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc",
|
||||
environment: "preview",
|
||||
sub: "owner:project:preview",
|
||||
aud: "https://vercel.com",
|
||||
iss: "https://oidc.vercel.com",
|
||||
},
|
||||
}),
|
||||
readConvexControl: async () => ({
|
||||
mode: "staging-live",
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
publicVisibilityEnabled: false,
|
||||
}),
|
||||
}),
|
||||
).rejects.toThrow("verified ClawHub Vercel identity");
|
||||
});
|
||||
|
||||
it("fetches through the verified request token only when the dark Convex control allows it", async () => {
|
||||
const rows = Array.from({ length: 500 }, (_, index) => ({
|
||||
id: `owner/repo/skill-${index}`,
|
||||
installUrl: null,
|
||||
installs: index,
|
||||
name: `Skill ${index}`,
|
||||
slug: `skill-${index}`,
|
||||
source: "owner/repo",
|
||||
sourceType: "github",
|
||||
url: `https://skills.sh/owner/repo/skill-${index}`,
|
||||
}));
|
||||
const fetchImpl = vi.fn(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
data: rows,
|
||||
pagination: {
|
||||
page: 0,
|
||||
perPage: 500,
|
||||
total: 500,
|
||||
hasMore: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
const env = {
|
||||
VERCEL_ENV: "preview",
|
||||
VITE_CLAWHUB_DEPLOY_ENV: "test",
|
||||
VITE_CONVEX_URL: "https://academic-chihuahua-392.convex.cloud",
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED: "1",
|
||||
};
|
||||
const getOidcToken = vi.fn(async () => "request-token");
|
||||
const verifyOidcToken = vi.fn(async () => ({
|
||||
payload: {
|
||||
owner_id: "team_pLdjXbfy0XvPRiNmAygTjTSH",
|
||||
project_id: "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc",
|
||||
environment: "test",
|
||||
sub: "owner:project:test",
|
||||
aud: "https://vercel.com",
|
||||
iss: "https://oidc.vercel.com",
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await fetchSkillsShCatalogTestPage({
|
||||
env,
|
||||
fetchImpl,
|
||||
getOidcToken,
|
||||
verifyOidcToken,
|
||||
readConvexControl: async () => ({
|
||||
mode: "staging-live",
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
publicVisibilityEnabled: false,
|
||||
}),
|
||||
});
|
||||
|
||||
expect(result.page.data).toHaveLength(500);
|
||||
expect(getOidcToken).toHaveBeenCalledOnce();
|
||||
expect(verifyOidcToken).toHaveBeenCalledWith("request-token", {
|
||||
projectId: "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc",
|
||||
ownerId: "team_pLdjXbfy0XvPRiNmAygTjTSH",
|
||||
environment: "test",
|
||||
});
|
||||
expect(result.controls).toEqual({
|
||||
maxDiscoveryRows: 500,
|
||||
maxRealScanAdmissions: 10,
|
||||
publicVisibilityEnabled: false,
|
||||
});
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
"https://skills.sh/api/v1/skills?page=0&per_page=500",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: "Bearer request-token",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
fetchSkillsShCatalogTestPage({
|
||||
env,
|
||||
fetchImpl,
|
||||
getOidcToken: async () => "request-token",
|
||||
verifyOidcToken,
|
||||
readConvexControl: async () => ({
|
||||
mode: "fixture",
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
publicVisibilityEnabled: false,
|
||||
}),
|
||||
}),
|
||||
).rejects.toThrow("dark Convex staging control");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,678 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { getVercelOidcToken, verifyVercelOidcToken, type VercelOidcPayload } from "@vercel/oidc";
|
||||
|
||||
const SKILLS_SH_API_BASE = "https://skills.sh/api/v1";
|
||||
const MAX_SOURCE_PAGE_SIZE = 500;
|
||||
const MAX_TEST_SCAN_ADMISSIONS = 10;
|
||||
const DETAIL_CONCURRENCY = 8;
|
||||
const CLAWHUB_VERCEL_OWNER_ID = "team_pLdjXbfy0XvPRiNmAygTjTSH";
|
||||
const CLAWHUB_VERCEL_PROJECT_ID = "prj_UVAJPNPYrBwTEkPJwkpEySsge8Mc";
|
||||
const CLAWHUB_TEST_CONVEX_URL = "https://academic-chihuahua-392.convex.cloud";
|
||||
|
||||
export type SkillsShCatalogSourceEnv = {
|
||||
CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED?: string;
|
||||
VERCEL_ENV?: string;
|
||||
VERCEL_OIDC_TOKEN?: string;
|
||||
VERCEL_TARGET_ENV?: string;
|
||||
VITE_CLAWHUB_DEPLOY_ENV?: string;
|
||||
VITE_CONVEX_URL?: string;
|
||||
};
|
||||
|
||||
export type SkillsShCatalogListRow = {
|
||||
id: string;
|
||||
installUrl: string | null;
|
||||
installs: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
source: string;
|
||||
sourceType: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type SkillsShCatalogDetail = {
|
||||
id: string;
|
||||
source: string;
|
||||
slug: string;
|
||||
installs: number;
|
||||
hash: string | null;
|
||||
files: Array<{
|
||||
name?: unknown;
|
||||
content?: unknown;
|
||||
}> | null;
|
||||
};
|
||||
|
||||
type SkillsShCatalogPage = {
|
||||
data: SkillsShCatalogListRow[];
|
||||
pagination: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
total: number;
|
||||
hasMore: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
type SkillsShCatalogSearch = {
|
||||
data: SkillsShCatalogListRow[];
|
||||
};
|
||||
|
||||
type HashQualifiedSkillsShCatalogDetail = SkillsShCatalogDetail & {
|
||||
hash: string;
|
||||
};
|
||||
|
||||
function assertIntegerInRange(name: string, value: number, min: number, max: number) {
|
||||
if (!Number.isInteger(value) || value < min || value > max) {
|
||||
throw new Error(`${name} must be an integer between ${min} and ${max}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireOidcToken(env: SkillsShCatalogSourceEnv, requestOidcToken?: string) {
|
||||
const token = requestOidcToken?.trim() || env.VERCEL_OIDC_TOKEN?.trim();
|
||||
if (!token) {
|
||||
throw new Error("skills.sh catalog source requires VERCEL_OIDC_TOKEN");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function fetchSkillsShJson<T>(
|
||||
path: string,
|
||||
options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
oidcToken?: string;
|
||||
} = {},
|
||||
): Promise<T> {
|
||||
const env = options.env ?? process.env;
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const response = await fetchImpl(`${SKILLS_SH_API_BASE}${path}`, {
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${requireOidcToken(env, options.oidcToken)}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`skills.sh catalog source returned HTTP ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
export async function fetchSkillsShCatalogPage(
|
||||
args: {
|
||||
page: number;
|
||||
perPage: number;
|
||||
},
|
||||
options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
oidcToken?: string;
|
||||
} = {},
|
||||
) {
|
||||
assertIntegerInRange("page", args.page, 0, 100_000);
|
||||
assertIntegerInRange("perPage", args.perPage, 1, MAX_SOURCE_PAGE_SIZE);
|
||||
return await fetchSkillsShJson<SkillsShCatalogPage>(
|
||||
`/skills?page=${args.page}&per_page=${args.perPage}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function searchSkillsShCatalog(
|
||||
args: {
|
||||
query: string;
|
||||
owner?: string;
|
||||
limit: number;
|
||||
},
|
||||
options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
oidcToken?: string;
|
||||
} = {},
|
||||
) {
|
||||
assertIntegerInRange("limit", args.limit, 1, MAX_SOURCE_PAGE_SIZE);
|
||||
const params = new URLSearchParams({
|
||||
q: args.query,
|
||||
limit: String(args.limit),
|
||||
});
|
||||
if (args.owner) params.set("owner", args.owner);
|
||||
return await fetchSkillsShJson<SkillsShCatalogSearch>(
|
||||
`/skills/search?${params.toString()}`,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchSkillsShCatalogDetail(
|
||||
id: string,
|
||||
options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
oidcToken?: string;
|
||||
} = {},
|
||||
) {
|
||||
const normalizedId = id
|
||||
.split("/")
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join("/");
|
||||
if (normalizedId.split("/").length !== 3) {
|
||||
throw new Error("skills.sh catalog detail id must be owner/repo/skill");
|
||||
}
|
||||
return await fetchSkillsShJson<SkillsShCatalogDetail>(`/skills/${normalizedId}`, options);
|
||||
}
|
||||
|
||||
export function getSkillsShCatalogTestSourcePolicy(env: SkillsShCatalogSourceEnv = process.env) {
|
||||
if (env.VITE_CLAWHUB_DEPLOY_ENV !== "test") {
|
||||
return {
|
||||
allowed: false as const,
|
||||
environment: env.VITE_CLAWHUB_DEPLOY_ENV?.trim() || "unknown",
|
||||
reason: "skills.sh live Test discovery requires the Test build marker",
|
||||
};
|
||||
}
|
||||
if (env.VERCEL_ENV !== "preview") {
|
||||
return {
|
||||
allowed: false as const,
|
||||
environment: env.VERCEL_ENV?.trim() || "unknown",
|
||||
reason: "skills.sh live Test discovery requires the Vercel Preview runtime",
|
||||
};
|
||||
}
|
||||
if (env.VITE_CONVEX_URL !== CLAWHUB_TEST_CONVEX_URL) {
|
||||
return {
|
||||
allowed: false as const,
|
||||
environment: "test",
|
||||
reason: "skills.sh live Test discovery requires the baked Test Convex backend",
|
||||
};
|
||||
}
|
||||
if (env.CLAWHUB_SKILLS_SH_TEST_LIVE_FETCH_ENABLED !== "1") {
|
||||
return {
|
||||
allowed: false as const,
|
||||
environment: "test",
|
||||
reason: "skills.sh live Test discovery is disabled",
|
||||
};
|
||||
}
|
||||
return {
|
||||
allowed: true as const,
|
||||
environment: "test",
|
||||
maxDiscoveryRows: MAX_SOURCE_PAGE_SIZE,
|
||||
maxRealScanAdmissions: MAX_TEST_SCAN_ADMISSIONS,
|
||||
};
|
||||
}
|
||||
|
||||
type VerifyVercelOidc = (
|
||||
token: string,
|
||||
options: {
|
||||
projectId: string;
|
||||
ownerId: string;
|
||||
environment: string;
|
||||
},
|
||||
) => Promise<{ payload: VercelOidcPayload }>;
|
||||
|
||||
type SkillsShCatalogTestControl = {
|
||||
mode: "off" | "fixture" | "staging-live";
|
||||
discoveryEnabled: boolean;
|
||||
writesEnabled: boolean;
|
||||
scanPlanningEnabled: boolean;
|
||||
maxEntriesPerRun: number;
|
||||
publicVisibilityEnabled: boolean;
|
||||
};
|
||||
|
||||
export type SkillsShCatalogGitHubOwnerProof = {
|
||||
authentication: "clawhub-github-authenticated";
|
||||
provenance:
|
||||
| "live-github"
|
||||
| "stored-authenticated-staging-live"
|
||||
| "stored-authenticated-staging-live+live-github";
|
||||
fetches: number;
|
||||
reused: number;
|
||||
owners: Array<{ owner: string; id: number; login: string }>;
|
||||
};
|
||||
|
||||
export class SkillsShCatalogOwnerProofRequiredError extends Error {
|
||||
constructor(
|
||||
readonly owners: string[],
|
||||
readonly sourcePreflight: {
|
||||
skillsShFetches: number;
|
||||
listFetches: number;
|
||||
searchFetches: number;
|
||||
detailFetches: number;
|
||||
selection: {
|
||||
rows: number;
|
||||
nvidiaRows: number;
|
||||
requiredCollisionIds: readonly string[];
|
||||
skippedIncompleteDetails: number;
|
||||
};
|
||||
},
|
||||
) {
|
||||
super("skills.sh live Test source requires authenticated GitHub owner proofs");
|
||||
this.name = "SkillsShCatalogOwnerProofRequiredError";
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSkillsShCatalogGitHubOwnerProof(
|
||||
selectedOwners: readonly string[],
|
||||
proof: SkillsShCatalogGitHubOwnerProof,
|
||||
) {
|
||||
const expectedProvenance =
|
||||
proof.reused === 0
|
||||
? "live-github"
|
||||
: proof.fetches === 0
|
||||
? "stored-authenticated-staging-live"
|
||||
: "stored-authenticated-staging-live+live-github";
|
||||
if (
|
||||
proof.authentication !== "clawhub-github-authenticated" ||
|
||||
!Number.isInteger(proof.fetches) ||
|
||||
proof.fetches < 0 ||
|
||||
!Number.isInteger(proof.reused) ||
|
||||
proof.reused < 0 ||
|
||||
proof.fetches + proof.reused !== selectedOwners.length ||
|
||||
proof.provenance !== expectedProvenance ||
|
||||
proof.owners.length !== selectedOwners.length
|
||||
) {
|
||||
throw new Error("skills.sh live Test source lacks complete authenticated GitHub owner proof");
|
||||
}
|
||||
const selected = new Set(selectedOwners);
|
||||
const ids = new Map<string, number>();
|
||||
for (const resolvedOwner of proof.owners) {
|
||||
const owner = resolvedOwner.owner.trim().toLowerCase();
|
||||
const login = resolvedOwner.login.trim().toLowerCase();
|
||||
if (
|
||||
!selected.has(owner) ||
|
||||
login !== owner ||
|
||||
ids.has(owner) ||
|
||||
!Number.isSafeInteger(resolvedOwner.id) ||
|
||||
resolvedOwner.id <= 0
|
||||
) {
|
||||
throw new Error(
|
||||
"skills.sh live Test source returned invalid authenticated GitHub owner proof",
|
||||
);
|
||||
}
|
||||
ids.set(owner, resolvedOwner.id);
|
||||
}
|
||||
for (const owner of selectedOwners) {
|
||||
if (!ids.has(owner)) {
|
||||
throw new Error(
|
||||
`skills.sh live Test source lacks an authenticated immutable owner id for ${owner}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function authorizeSkillsShCatalogTestRequest(
|
||||
options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
getOidcToken?: () => Promise<string>;
|
||||
verifyOidcToken?: VerifyVercelOidc;
|
||||
} = {},
|
||||
) {
|
||||
const env = options.env ?? process.env;
|
||||
const policy = getSkillsShCatalogTestSourcePolicy(env);
|
||||
if (!policy.allowed) throw new Error(policy.reason);
|
||||
|
||||
const getOidcToken = options.getOidcToken ?? getVercelOidcToken;
|
||||
const verifyOidcToken = options.verifyOidcToken ?? verifyVercelOidcToken;
|
||||
const token = await getOidcToken();
|
||||
const verified = await verifyOidcToken(token, {
|
||||
projectId: CLAWHUB_VERCEL_PROJECT_ID,
|
||||
ownerId: CLAWHUB_VERCEL_OWNER_ID,
|
||||
environment: "test",
|
||||
});
|
||||
if (
|
||||
verified.payload.project_id !== CLAWHUB_VERCEL_PROJECT_ID ||
|
||||
verified.payload.owner_id !== CLAWHUB_VERCEL_OWNER_ID ||
|
||||
verified.payload.environment !== "test"
|
||||
) {
|
||||
throw new Error("skills.sh live Test discovery requires verified ClawHub Vercel identity");
|
||||
}
|
||||
return {
|
||||
...policy,
|
||||
oidcToken: token,
|
||||
verifiedIdentity: {
|
||||
ownerId: verified.payload.owner_id,
|
||||
projectId: verified.payload.project_id,
|
||||
environment: verified.payload.environment,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchSkillsShCatalogTestPage(options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
getOidcToken?: () => Promise<string>;
|
||||
verifyOidcToken?: VerifyVercelOidc;
|
||||
readConvexControl: () => Promise<SkillsShCatalogTestControl>;
|
||||
}) {
|
||||
const authorization = await authorizeSkillsShCatalogTestRequest(options);
|
||||
const control = await options.readConvexControl();
|
||||
if (
|
||||
control.mode !== "staging-live" ||
|
||||
!control.discoveryEnabled ||
|
||||
!control.writesEnabled ||
|
||||
!control.scanPlanningEnabled ||
|
||||
control.maxEntriesPerRun < 1 ||
|
||||
control.maxEntriesPerRun > authorization.maxDiscoveryRows ||
|
||||
control.publicVisibilityEnabled
|
||||
) {
|
||||
throw new Error("skills.sh live Test discovery requires the dark Convex staging control");
|
||||
}
|
||||
const page = await fetchSkillsShCatalogPage(
|
||||
{ page: 0, perPage: authorization.maxDiscoveryRows },
|
||||
{
|
||||
env: options.env,
|
||||
fetchImpl: options.fetchImpl,
|
||||
oidcToken: authorization.oidcToken,
|
||||
},
|
||||
);
|
||||
if (page.data.length > control.maxEntriesPerRun) {
|
||||
throw new Error("skills.sh live Test discovery exceeded the Convex run budget");
|
||||
}
|
||||
return {
|
||||
page,
|
||||
verifiedIdentity: authorization.verifiedIdentity,
|
||||
controls: {
|
||||
maxDiscoveryRows: control.maxEntriesPerRun,
|
||||
maxRealScanAdmissions: authorization.maxRealScanAdmissions,
|
||||
publicVisibilityEnabled: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const REQUIRED_COLLISION_IDS = [
|
||||
"anthropics/skills/frontend-design",
|
||||
"anthropics/claude-code/frontend-design",
|
||||
] as const;
|
||||
|
||||
function isGitHubCatalogRow(row: SkillsShCatalogListRow) {
|
||||
const source = row.source.trim().toLowerCase();
|
||||
const slug = row.slug.trim().toLowerCase();
|
||||
return (
|
||||
row.sourceType === "github" &&
|
||||
source.split("/").length === 2 &&
|
||||
/^[a-z0-9][a-z0-9-]*$/.test(slug) &&
|
||||
row.id.trim().toLowerCase() === `${source}/${slug}`
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeListRow(row: SkillsShCatalogListRow) {
|
||||
const [owner = "", repo = ""] = row.source.split("/");
|
||||
const slug = row.slug.trim().toLowerCase();
|
||||
return {
|
||||
...row,
|
||||
owner: owner.trim().toLowerCase(),
|
||||
repo: repo.trim().toLowerCase(),
|
||||
slug,
|
||||
externalId: `${owner.trim().toLowerCase()}/${repo.trim().toLowerCase()}/${slug}`,
|
||||
};
|
||||
}
|
||||
|
||||
function sha256Hex(bytes: Uint8Array | string) {
|
||||
return createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
type CompleteArtifactFile = { name: string; content: string };
|
||||
|
||||
function hasCompleteArtifactFiles(
|
||||
files: SkillsShCatalogDetail["files"],
|
||||
): files is CompleteArtifactFile[] {
|
||||
return (
|
||||
Array.isArray(files) &&
|
||||
files.length > 0 &&
|
||||
files.every(
|
||||
(file) =>
|
||||
typeof file.name === "string" &&
|
||||
file.name.trim().length > 0 &&
|
||||
typeof file.content === "string",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function buildArtifact(detail: SkillsShCatalogDetail) {
|
||||
if (!hasCompleteArtifactFiles(detail.files)) {
|
||||
throw new Error(`skills.sh live admission has incomplete artifact files: ${detail.id}`);
|
||||
}
|
||||
const files = detail.files
|
||||
.map((file) => {
|
||||
const bytes = Buffer.from(file.content, "utf8");
|
||||
return {
|
||||
path: file.name,
|
||||
size: bytes.byteLength,
|
||||
sha256: sha256Hex(bytes),
|
||||
contentType: file.name.toLowerCase().endsWith(".md")
|
||||
? "text/markdown; charset=utf-8"
|
||||
: "text/plain; charset=utf-8",
|
||||
contentBase64: bytes.toString("base64"),
|
||||
};
|
||||
})
|
||||
.sort((left, right) => left.path.localeCompare(right.path));
|
||||
const manifest = files.map((file) => `${file.path}\0${file.sha256}\n`).join("");
|
||||
return {
|
||||
artifactContentHash: sha256Hex(manifest),
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
function createSkillsShFetchOptions(
|
||||
env: SkillsShCatalogSourceEnv | undefined,
|
||||
fetchImpl: typeof fetch | undefined,
|
||||
value: string,
|
||||
) {
|
||||
return {
|
||||
env,
|
||||
fetchImpl,
|
||||
oidcToken: value,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectSkillsShCatalogTestRows(
|
||||
fetchOptions: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
oidcToken?: string;
|
||||
},
|
||||
requiredArtifactIds: ReadonlySet<string>,
|
||||
) {
|
||||
const candidates = new Map<string, ReturnType<typeof normalizeListRow>>();
|
||||
let listFetches = 0;
|
||||
for (let page = 0; candidates.size < MAX_SOURCE_PAGE_SIZE + 100; page += 1) {
|
||||
const response = await fetchSkillsShCatalogPage(
|
||||
{ page, perPage: MAX_SOURCE_PAGE_SIZE },
|
||||
fetchOptions,
|
||||
);
|
||||
listFetches += 1;
|
||||
for (const row of response.data) {
|
||||
if (!isGitHubCatalogRow(row)) continue;
|
||||
const normalized = normalizeListRow(row);
|
||||
candidates.set(normalized.externalId, normalized);
|
||||
}
|
||||
if (!response.pagination.hasMore) break;
|
||||
}
|
||||
const nvidia = await searchSkillsShCatalog(
|
||||
{ query: "nvidia", owner: "nvidia", limit: 200 },
|
||||
fetchOptions,
|
||||
);
|
||||
const nvidiaRows = nvidia.data
|
||||
.filter(isGitHubCatalogRow)
|
||||
.map(normalizeListRow)
|
||||
.filter((row) => row.owner === "nvidia")
|
||||
.slice(0, 10);
|
||||
const requiredIds = new Set([
|
||||
...REQUIRED_COLLISION_IDS,
|
||||
...nvidiaRows.map((row) => row.externalId),
|
||||
...requiredArtifactIds,
|
||||
]);
|
||||
for (const row of nvidiaRows) candidates.set(row.externalId, row);
|
||||
for (const id of requiredIds) {
|
||||
if (!candidates.has(id)) throw new Error(`Required skills.sh live row is missing: ${id}`);
|
||||
}
|
||||
const candidateRows = [
|
||||
...Array.from(requiredIds, (id) => candidates.get(id)!),
|
||||
...Array.from(candidates.values()).filter((row) => !requiredIds.has(row.externalId)),
|
||||
];
|
||||
const selected: Array<{
|
||||
row: ReturnType<typeof normalizeListRow>;
|
||||
detail: HashQualifiedSkillsShCatalogDetail;
|
||||
}> = [];
|
||||
let detailFetches = 0;
|
||||
let skippedIncompleteDetails = 0;
|
||||
for (
|
||||
let offset = 0;
|
||||
offset < candidateRows.length && selected.length < MAX_SOURCE_PAGE_SIZE;
|
||||
offset += DETAIL_CONCURRENCY
|
||||
) {
|
||||
const batch = candidateRows.slice(offset, offset + DETAIL_CONCURRENCY);
|
||||
const details = await Promise.all(
|
||||
batch.map(async (row) => await fetchSkillsShCatalogDetail(row.externalId, fetchOptions)),
|
||||
);
|
||||
detailFetches += details.length;
|
||||
for (let index = 0; index < batch.length; index += 1) {
|
||||
const row = batch[index]!;
|
||||
const detail = details[index]!;
|
||||
const hasExactHash =
|
||||
detail.id.trim().toLowerCase() === row.externalId &&
|
||||
typeof detail.hash === "string" &&
|
||||
/^[a-f0-9]{64}$/i.test(detail.hash);
|
||||
if (!hasExactHash) {
|
||||
if (requiredIds.has(row.externalId) || requiredArtifactIds.has(row.externalId)) {
|
||||
throw new Error(
|
||||
`Required skills.sh live row lacks an exact detail hash: ${row.externalId}`,
|
||||
);
|
||||
}
|
||||
skippedIncompleteDetails += 1;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
requiredArtifactIds.has(row.externalId) &&
|
||||
(!Array.isArray(detail.files) || detail.files.length < 1)
|
||||
) {
|
||||
throw new Error(`skills.sh live admission lacks artifact files: ${row.externalId}`);
|
||||
}
|
||||
if (requiredArtifactIds.has(row.externalId) && !hasCompleteArtifactFiles(detail.files)) {
|
||||
throw new Error(
|
||||
`skills.sh live admission has incomplete artifact files: ${row.externalId}`,
|
||||
);
|
||||
}
|
||||
selected.push({ row, detail: detail as HashQualifiedSkillsShCatalogDetail });
|
||||
if (selected.length === MAX_SOURCE_PAGE_SIZE) break;
|
||||
}
|
||||
}
|
||||
if (selected.length !== MAX_SOURCE_PAGE_SIZE) {
|
||||
throw new Error(`Expected ${MAX_SOURCE_PAGE_SIZE} hash-qualified live rows`);
|
||||
}
|
||||
return {
|
||||
selected,
|
||||
selectedOwners: Array.from(new Set(selected.map(({ row }) => row.owner))).sort(),
|
||||
listFetches,
|
||||
detailFetches,
|
||||
skippedIncompleteDetails,
|
||||
selection: {
|
||||
rows: selected.length,
|
||||
nvidiaRows: selected.filter(({ row }) => row.owner === "nvidia").length,
|
||||
requiredCollisionIds: REQUIRED_COLLISION_IDS,
|
||||
skippedIncompleteDetails,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function captureSkillsShCatalogTestSnapshot(options: {
|
||||
env?: SkillsShCatalogSourceEnv;
|
||||
fetchImpl?: typeof fetch;
|
||||
getOidcToken?: () => Promise<string>;
|
||||
verifyOidcToken?: VerifyVercelOidc;
|
||||
readConvexControl: () => Promise<SkillsShCatalogTestControl>;
|
||||
admitExternalIds?: string[];
|
||||
githubOwnerProof?: SkillsShCatalogGitHubOwnerProof;
|
||||
resolveGitHubOwners?: (owners: string[]) => Promise<SkillsShCatalogGitHubOwnerProof>;
|
||||
}) {
|
||||
const startedAt = Date.now();
|
||||
const authorization = await authorizeSkillsShCatalogTestRequest(options);
|
||||
const control = await options.readConvexControl();
|
||||
if (
|
||||
control.mode !== "staging-live" ||
|
||||
!control.discoveryEnabled ||
|
||||
!control.writesEnabled ||
|
||||
!control.scanPlanningEnabled ||
|
||||
control.maxEntriesPerRun !== MAX_SOURCE_PAGE_SIZE ||
|
||||
control.publicVisibilityEnabled
|
||||
) {
|
||||
throw new Error("skills.sh live Test capture requires the exact dark 500-row control");
|
||||
}
|
||||
const fetchOptions = createSkillsShFetchOptions(
|
||||
options.env,
|
||||
options.fetchImpl,
|
||||
authorization.oidcToken,
|
||||
);
|
||||
const admitted = new Set(
|
||||
(options.admitExternalIds ?? []).map((externalId) => externalId.trim().toLowerCase()),
|
||||
);
|
||||
if (admitted.size > authorization.maxRealScanAdmissions) {
|
||||
throw new Error(
|
||||
`skills.sh live Test admission cannot exceed ${authorization.maxRealScanAdmissions}`,
|
||||
);
|
||||
}
|
||||
const selection = await selectSkillsShCatalogTestRows(fetchOptions, admitted);
|
||||
const sourcePreflight = {
|
||||
skillsShFetches: selection.listFetches + 1 + selection.detailFetches,
|
||||
listFetches: selection.listFetches,
|
||||
searchFetches: 1,
|
||||
detailFetches: selection.detailFetches,
|
||||
selection: selection.selection,
|
||||
};
|
||||
const githubOwnerProof =
|
||||
options.githubOwnerProof ??
|
||||
(options.resolveGitHubOwners
|
||||
? await options.resolveGitHubOwners(selection.selectedOwners)
|
||||
: null);
|
||||
if (!githubOwnerProof) {
|
||||
throw new SkillsShCatalogOwnerProofRequiredError(selection.selectedOwners, sourcePreflight);
|
||||
}
|
||||
const githubOwnerIds = validateSkillsShCatalogGitHubOwnerProof(
|
||||
selection.selectedOwners,
|
||||
githubOwnerProof,
|
||||
);
|
||||
const selected = selection.selected;
|
||||
const rows = selected.map(({ row, detail }) => {
|
||||
const githubOwnerId = githubOwnerIds.get(row.owner)!;
|
||||
return {
|
||||
externalId: row.externalId,
|
||||
githubOwnerId,
|
||||
owner: row.owner,
|
||||
repo: row.repo,
|
||||
slug: row.slug,
|
||||
displayName: row.name.trim() || row.slug,
|
||||
sourceUrl: row.url,
|
||||
githubRepoUrl: row.installUrl ?? `https://github.com/${row.owner}/${row.repo}`,
|
||||
sourceContentHash: detail.hash.toLowerCase(),
|
||||
installs: row.installs,
|
||||
};
|
||||
});
|
||||
const artifacts = selected
|
||||
.filter(({ row }) => admitted.has(row.externalId))
|
||||
.map(({ row, detail }) => ({
|
||||
externalId: row.externalId,
|
||||
...buildArtifact(detail),
|
||||
}));
|
||||
if (artifacts.length !== admitted.size) {
|
||||
throw new Error("skills.sh live Test admission artifact is not present in the selected 500");
|
||||
}
|
||||
return {
|
||||
snapshotId: `skills-sh-test-live-500:${sha256Hex(
|
||||
rows.map((row) => `${row.externalId}:${row.sourceContentHash}\n`).join(""),
|
||||
).slice(0, 16)}`,
|
||||
capturedAt: new Date().toISOString(),
|
||||
rows,
|
||||
artifacts,
|
||||
verifiedIdentity: authorization.verifiedIdentity,
|
||||
selection: {
|
||||
rows: rows.length,
|
||||
nvidiaRows: rows.filter((row) => row.owner === "nvidia").length,
|
||||
requiredCollisionIds: REQUIRED_COLLISION_IDS,
|
||||
skippedIncompleteDetails: selection.skippedIncompleteDetails,
|
||||
},
|
||||
metrics: {
|
||||
runtimeMs: Date.now() - startedAt,
|
||||
skillsShFetches: selection.listFetches + 1 + selection.detailFetches,
|
||||
listFetches: selection.listFetches,
|
||||
searchFetches: 1,
|
||||
detailFetches: selection.detailFetches,
|
||||
githubOwnerFetches: githubOwnerProof.fetches,
|
||||
githubOwnerIdsReused: githubOwnerProof.reused,
|
||||
githubOwnerProofProvenance: githubOwnerProof.provenance,
|
||||
skippedIncompleteDetails: selection.skippedIncompleteDetails,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
/* @vitest-environment node */
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const getHeaderMock = vi.fn();
|
||||
const getVercelOidcTokenMock = vi.fn();
|
||||
const readBodyMock = vi.fn();
|
||||
const captureSnapshotMock = vi.fn();
|
||||
const sourcePolicyMock = vi.fn();
|
||||
|
||||
vi.mock("h3", () => ({
|
||||
defineEventHandler: (handler: unknown) => handler,
|
||||
getHeader: (...args: unknown[]) => getHeaderMock(...args),
|
||||
readBody: (...args: unknown[]) => readBodyMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@vercel/oidc", () => ({
|
||||
getVercelOidcToken: (...args: unknown[]) => getVercelOidcTokenMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./skillsShCatalogSource", () => ({
|
||||
captureSkillsShCatalogTestSnapshot: (...args: unknown[]) => captureSnapshotMock(...args),
|
||||
getSkillsShCatalogTestSourcePolicy: (...args: unknown[]) => sourcePolicyMock(...args),
|
||||
}));
|
||||
|
||||
describe("skills.sh permanent Test operator route", () => {
|
||||
beforeEach(() => {
|
||||
getHeaderMock.mockReset();
|
||||
getVercelOidcTokenMock.mockReset();
|
||||
readBodyMock.mockReset();
|
||||
captureSnapshotMock.mockReset();
|
||||
sourcePolicyMock.mockReset();
|
||||
sourcePolicyMock.mockReturnValue({
|
||||
allowed: true,
|
||||
environment: "test",
|
||||
maxDiscoveryRows: 500,
|
||||
maxRealScanAdmissions: 10,
|
||||
});
|
||||
getHeaderMock.mockImplementation((_event: unknown, name: string) => {
|
||||
if (name === "authorization") return "Bearer operator-token";
|
||||
return undefined;
|
||||
});
|
||||
getVercelOidcTokenMock.mockResolvedValue("request-oidc-token");
|
||||
readBodyMock.mockResolvedValue({
|
||||
allowlist: ["nvidia/skills/aiq-deploy"],
|
||||
reason: "bounded Test proof",
|
||||
});
|
||||
captureSnapshotMock.mockImplementation(async (options) => {
|
||||
expect(await options.getOidcToken()).toBe("request-oidc-token");
|
||||
expect(options.admitExternalIds).toEqual(["nvidia/skills/aiq-deploy"]);
|
||||
expect(await options.resolveGitHubOwners(["anthropics", "nvidia"])).toEqual({
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
fetches: 0,
|
||||
reused: 2,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
});
|
||||
return {
|
||||
snapshotId: "skills-sh-test-live-500:abc",
|
||||
capturedAt: "2026-07-21T00:00:00.000Z",
|
||||
rows: Array.from({ length: 500 }, (_, index) => ({
|
||||
externalId: `owner/repo/skill-${index}`,
|
||||
})),
|
||||
artifacts: [
|
||||
{
|
||||
externalId: "nvidia/skills/aiq-deploy",
|
||||
artifactContentHash: "a".repeat(64),
|
||||
files: [],
|
||||
},
|
||||
],
|
||||
verifiedIdentity: {
|
||||
ownerId: "team",
|
||||
projectId: "project",
|
||||
environment: "test",
|
||||
},
|
||||
selection: {
|
||||
rows: 500,
|
||||
nvidiaRows: 10,
|
||||
requiredCollisionIds: [],
|
||||
},
|
||||
metrics: {
|
||||
runtimeMs: 100,
|
||||
skillsShFetches: 502,
|
||||
listFetches: 1,
|
||||
searchFetches: 1,
|
||||
detailFetches: 500,
|
||||
githubOwnerFetches: 0,
|
||||
githubOwnerIdsReused: 2,
|
||||
githubOwnerProofProvenance: "stored-authenticated-staging-live",
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (_url: string, init: RequestInit) => {
|
||||
if (init.method === "GET") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "source-sha",
|
||||
control: {
|
||||
mode: "staging-live",
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
maxEntriesPerBatch: 100,
|
||||
maxWritesPerBatch: 100,
|
||||
publicVisibilityEnabled: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
const body = JSON.parse(String(init.body)) as { operation: string; cursor?: number };
|
||||
if (body.operation === "start") {
|
||||
return new Response(JSON.stringify({ runId: "skillsShCatalogRuns:test" }));
|
||||
}
|
||||
if (body.operation === "resolve-owners") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
fetches: 0,
|
||||
reused: 2,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (body.operation === "batch") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status: body.cursor === 450 ? "completed" : "running",
|
||||
cursor: (body.cursor ?? 0) + 50,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (body.operation === "admit") {
|
||||
return new Response(JSON.stringify({ requested: 1, admitted: 1, skipped: 0 }));
|
||||
}
|
||||
return new Response(JSON.stringify({ error: "unsupported_operation" }), { status: 400 });
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("resolves owners through Convex and never exposes the request OIDC token", async () => {
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
expect(body).toMatchObject({
|
||||
ok: true,
|
||||
source: {
|
||||
selection: { rows: 500 },
|
||||
fetches: {
|
||||
skillsShFetches: 502,
|
||||
githubOwnerFetches: 0,
|
||||
githubOwnerIdsReused: 2,
|
||||
githubOwnerProofProvenance: "stored-authenticated-staging-live",
|
||||
},
|
||||
},
|
||||
convex: {
|
||||
firstRun: { runId: "skillsShCatalogRuns:test" },
|
||||
identicalRerun: { runId: "skillsShCatalogRuns:test" },
|
||||
admission: { admitted: 1 },
|
||||
},
|
||||
controls: { publicVisibilityEnabled: false, schedulesEnabled: false },
|
||||
});
|
||||
expect(JSON.stringify(body)).not.toContain("request-oidc-token");
|
||||
|
||||
const fetchMock = vi.mocked(fetch);
|
||||
expect(fetchMock).toHaveBeenCalledTimes(25);
|
||||
for (const [, init] of fetchMock.mock.calls) {
|
||||
expect(init?.headers).toMatchObject({ Authorization: "Bearer operator-token" });
|
||||
expect(JSON.stringify(init)).not.toContain("request-oidc-token");
|
||||
}
|
||||
expect(
|
||||
fetchMock.mock.calls.some(([, init]) => {
|
||||
const body = init?.body ? JSON.parse(String(init.body)) : null;
|
||||
return body?.operation === "admit";
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("is a hard 404 outside the exact Test runtime", async () => {
|
||||
sourcePolicyMock.mockReturnValue({
|
||||
allowed: false,
|
||||
environment: "production",
|
||||
reason: "disabled",
|
||||
});
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
expect(response.status).toBe(404);
|
||||
expect(captureSnapshotMock).not.toHaveBeenCalled();
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ body: [], label: "array body" },
|
||||
{ body: { allowlist: {} }, label: "non-array allowlist" },
|
||||
{ body: { allowlist: [null] }, label: "non-string allowlist member" },
|
||||
{ body: { reason: 42 }, label: "non-string reason" },
|
||||
])("rejects a malformed $label before calling any backend", async ({ body }) => {
|
||||
readBodyMock.mockResolvedValue(body);
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(await response.json()).toEqual({ error: "invalid_request_body" });
|
||||
expect(captureSnapshotMock).not.toHaveBeenCalled();
|
||||
expect(fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sizes persistence batches from the active Convex row and write budgets", async () => {
|
||||
readBodyMock.mockResolvedValue({
|
||||
allowlist: ["nvidia/skills/aiq-deploy"],
|
||||
reason: "bounded Test proof",
|
||||
});
|
||||
const batchSizes: number[] = [];
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (_url: string, init: RequestInit) => {
|
||||
if (init.method === "GET") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
deploymentName: "academic-chihuahua-392",
|
||||
buildSha: "source-sha",
|
||||
control: {
|
||||
mode: "staging-live",
|
||||
discoveryEnabled: true,
|
||||
writesEnabled: true,
|
||||
scanPlanningEnabled: true,
|
||||
maxEntriesPerRun: 500,
|
||||
maxEntriesPerBatch: 20,
|
||||
maxWritesPerBatch: 30,
|
||||
publicVisibilityEnabled: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
const body = JSON.parse(String(init.body)) as {
|
||||
operation: string;
|
||||
cursor?: number;
|
||||
rows?: unknown[];
|
||||
};
|
||||
if (body.operation === "start") {
|
||||
return new Response(JSON.stringify({ runId: "skillsShCatalogRuns:test" }));
|
||||
}
|
||||
if (body.operation === "resolve-owners") {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
authentication: "clawhub-github-authenticated",
|
||||
provenance: "stored-authenticated-staging-live",
|
||||
fetches: 0,
|
||||
reused: 2,
|
||||
owners: [
|
||||
{ owner: "anthropics", login: "anthropics", id: 76_263_028 },
|
||||
{ owner: "nvidia", login: "nvidia", id: 1_728_152 },
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (body.operation === "batch") {
|
||||
batchSizes.push(body.rows?.length ?? 0);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
status:
|
||||
(body.cursor ?? 0) + (body.rows?.length ?? 0) >= 500 ? "completed" : "running",
|
||||
cursor: (body.cursor ?? 0) + (body.rows?.length ?? 0),
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (body.operation === "admit") {
|
||||
return new Response(JSON.stringify({ requested: 1, admitted: 1, skipped: 0 }));
|
||||
}
|
||||
return new Response(JSON.stringify({ error: "unsupported_operation" }), { status: 400 });
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.json()).toMatchObject({
|
||||
controls: { batchSize: 15 },
|
||||
});
|
||||
expect(batchSizes).toHaveLength(68);
|
||||
expect(Math.max(...batchSizes)).toBe(15);
|
||||
expect(batchSizes.reduce((sum, size) => sum + size, 0)).toBe(1_000);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ status: "running", cursor: 500 },
|
||||
{ status: "completed", cursor: 499 },
|
||||
])("fails closed when a snapshot run ends at $status with cursor $cursor", async (terminal) => {
|
||||
const baseFetch = vi.mocked(fetch);
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string, init: RequestInit) => {
|
||||
const body = init.body ? (JSON.parse(String(init.body)) as { operation?: string }) : null;
|
||||
if (body?.operation === "batch") {
|
||||
return new Response(JSON.stringify(terminal));
|
||||
}
|
||||
return await baseFetch(url, init);
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
|
||||
expect(response.status).toBe(502);
|
||||
expect(await response.json()).toMatchObject({
|
||||
error: "skills_sh_catalog_test_failed",
|
||||
message: expect.stringContaining("did not complete all 500 rows"),
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when any allowlisted real scan is skipped", async () => {
|
||||
const baseFetch = vi.mocked(fetch);
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string, init: RequestInit) => {
|
||||
const body = init.body ? (JSON.parse(String(init.body)) as { operation?: string }) : null;
|
||||
if (body?.operation === "admit") {
|
||||
return new Response(JSON.stringify({ requested: 1, admitted: 0, skipped: 1 }));
|
||||
}
|
||||
return await baseFetch(url, init);
|
||||
}),
|
||||
);
|
||||
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
|
||||
expect(response.status).toBe(502);
|
||||
expect(await response.json()).toMatchObject({
|
||||
error: "skills_sh_catalog_test_failed",
|
||||
message: expect.stringContaining("did not admit the complete allowlist"),
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the normalized deduplicated allowlist for capture, admission, and accounting", async () => {
|
||||
readBodyMock.mockResolvedValue({
|
||||
allowlist: [
|
||||
" NVIDIA/SKILLS/AIQ-DEPLOY ",
|
||||
"",
|
||||
"nvidia/skills/aiq-deploy",
|
||||
" nvidia/skills/aiq-deploy ",
|
||||
],
|
||||
});
|
||||
const handler = (await import("./routes/ops/skills-sh/catalog-test.post")).default;
|
||||
const response = (await handler({} as never)) as Response;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(captureSnapshotMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
admitExternalIds: ["nvidia/skills/aiq-deploy"],
|
||||
}),
|
||||
);
|
||||
const admitCall = vi.mocked(fetch).mock.calls.find(([, init]) => {
|
||||
const body = init?.body ? JSON.parse(String(init.body)) : null;
|
||||
return body?.operation === "admit";
|
||||
});
|
||||
expect(JSON.parse(String(admitCall?.[1]?.body))).toMatchObject({
|
||||
externalIds: ["nvidia/skills/aiq-deploy"],
|
||||
});
|
||||
expect(await response.json()).toMatchObject({
|
||||
convex: { admission: { requested: 1, admitted: 1, skipped: 0 } },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user