@lesliechan721/aw 0.1.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/authoring/assets/aw-config.d.mts +2 -0
- package/dist/authoring/assets/aw-config.mjs +556 -0
- package/dist/authoring/assets/aw-config.mjs.map +1 -0
- package/dist/authoring/scripts/add-skill.d.mts +2 -0
- package/dist/authoring/scripts/add-skill.mjs +355 -0
- package/dist/authoring/scripts/add-skill.mjs.map +1 -0
- package/dist/authoring/scripts/check-plugin-versions.d.mts +2 -0
- package/dist/authoring/scripts/check-plugin-versions.mjs +5 -0
- package/dist/authoring/scripts/check-plugin-versions.mjs.map +1 -0
- package/dist/authoring/scripts/init-plugin.d.mts +2 -0
- package/dist/authoring/scripts/init-plugin.mjs +512 -0
- package/dist/authoring/scripts/init-plugin.mjs.map +1 -0
- package/dist/authoring/scripts/lib/javascript-import-scanner.d.mts +4 -0
- package/dist/authoring/scripts/lib/javascript-import-scanner.mjs +261 -0
- package/dist/authoring/scripts/lib/javascript-import-scanner.mjs.map +1 -0
- package/dist/authoring/scripts/lib/plugin-root.d.mts +26 -0
- package/dist/authoring/scripts/lib/plugin-root.mjs +125 -0
- package/dist/authoring/scripts/lib/plugin-root.mjs.map +1 -0
- package/dist/authoring/scripts/lib/plugin-version-check.d.mts +55 -0
- package/dist/authoring/scripts/lib/plugin-version-check.mjs +777 -0
- package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -0
- package/dist/authoring/scripts/lib/semver.d.mts +9 -0
- package/dist/authoring/scripts/lib/semver.mjs +63 -0
- package/dist/authoring/scripts/lib/semver.mjs.map +1 -0
- package/dist/authoring/scripts/lib/skill-scaffold.d.mts +124 -0
- package/dist/authoring/scripts/lib/skill-scaffold.mjs +374 -0
- package/dist/authoring/scripts/lib/skill-scaffold.mjs.map +1 -0
- package/dist/authoring/scripts/plugin-status.d.mts +2 -0
- package/dist/authoring/scripts/plugin-status.mjs +96 -0
- package/dist/authoring/scripts/plugin-status.mjs.map +1 -0
- package/dist/authoring/scripts/validate-plugin.d.mts +2 -0
- package/dist/authoring/scripts/validate-plugin.mjs +4442 -0
- package/dist/authoring/scripts/validate-plugin.mjs.map +1 -0
- package/dist/authoring/scripts/validate-skill.d.mts +2 -0
- package/dist/authoring/scripts/validate-skill.mjs +126 -0
- package/dist/authoring/scripts/validate-skill.mjs.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/scripts/aw-skill-eval-engine.d.ts +13 -0
- package/dist/scripts/aw-skill-eval-engine.js +31 -0
- package/dist/scripts/aw-skill-eval-engine.js.map +1 -0
- package/dist/scripts/bundle-plugins.d.ts +18 -0
- package/dist/scripts/bundle-plugins.js +123 -0
- package/dist/scripts/bundle-plugins.js.map +1 -0
- package/dist/scripts/codex-eval-app-server.d.ts +89 -0
- package/dist/scripts/codex-eval-app-server.js +1228 -0
- package/dist/scripts/codex-eval-app-server.js.map +1 -0
- package/dist/scripts/codex-eval-isolation.d.ts +61 -0
- package/dist/scripts/codex-eval-isolation.js +318 -0
- package/dist/scripts/codex-eval-isolation.js.map +1 -0
- package/dist/scripts/codex-eval-process-management.d.ts +8 -0
- package/dist/scripts/codex-eval-process-management.js +69 -0
- package/dist/scripts/codex-eval-process-management.js.map +1 -0
- package/dist/scripts/codex-eval-process-tree.d.ts +38 -0
- package/dist/scripts/codex-eval-process-tree.js +803 -0
- package/dist/scripts/codex-eval-process-tree.js.map +1 -0
- package/dist/scripts/codex-eval-runtime-config.d.ts +62 -0
- package/dist/scripts/codex-eval-runtime-config.js +843 -0
- package/dist/scripts/codex-eval-runtime-config.js.map +1 -0
- package/dist/scripts/codex-trigger-eval.d.ts +84 -0
- package/dist/scripts/codex-trigger-eval.js +968 -0
- package/dist/scripts/codex-trigger-eval.js.map +1 -0
- package/dist/scripts/eval-plugin-skills-audited.d.ts +6 -0
- package/dist/scripts/eval-plugin-skills-audited.js +261 -0
- package/dist/scripts/eval-plugin-skills-audited.js.map +1 -0
- package/dist/scripts/eval-plugin-skills.d.ts +45 -0
- package/dist/scripts/eval-plugin-skills.js +1586 -0
- package/dist/scripts/eval-plugin-skills.js.map +1 -0
- package/dist/scripts/eval-run-layout.d.ts +17 -0
- package/dist/scripts/eval-run-layout.js +26 -0
- package/dist/scripts/eval-run-layout.js.map +1 -0
- package/dist/scripts/plugin-eval-contract.d.ts +185 -0
- package/dist/scripts/plugin-eval-contract.js +695 -0
- package/dist/scripts/plugin-eval-contract.js.map +1 -0
- package/dist/scripts/plugin-eval-materializer.d.ts +13 -0
- package/dist/scripts/plugin-eval-materializer.js +421 -0
- package/dist/scripts/plugin-eval-materializer.js.map +1 -0
- package/dist/scripts/plugin-staging.d.ts +43 -0
- package/dist/scripts/plugin-staging.js +196 -0
- package/dist/scripts/plugin-staging.js.map +1 -0
- package/dist/scripts/skill-reporting.d.ts +212 -0
- package/dist/scripts/skill-reporting.js +922 -0
- package/dist/scripts/skill-reporting.js.map +1 -0
- package/dist/scripts/test-plugin-scripts.d.ts +7 -0
- package/dist/scripts/test-plugin-scripts.js +674 -0
- package/dist/scripts/test-plugin-scripts.js.map +1 -0
- package/dist/src/adapters/helpers.d.ts +44 -0
- package/dist/src/adapters/helpers.js +95 -0
- package/dist/src/adapters/helpers.js.map +1 -0
- package/dist/src/authoring/workspace-commands.d.ts +46 -0
- package/dist/src/authoring/workspace-commands.js +133 -0
- package/dist/src/authoring/workspace-commands.js.map +1 -0
- package/dist/src/commands/catalog-publish.d.ts +33 -0
- package/dist/src/commands/catalog-publish.js +374 -0
- package/dist/src/commands/catalog-publish.js.map +1 -0
- package/dist/src/commands/catalog-source.d.ts +111 -0
- package/dist/src/commands/catalog-source.js +266 -0
- package/dist/src/commands/catalog-source.js.map +1 -0
- package/dist/src/commands/console.d.ts +3 -0
- package/dist/src/commands/console.js +44 -0
- package/dist/src/commands/console.js.map +1 -0
- package/dist/src/commands/dependency-operation-plan.d.ts +28 -0
- package/dist/src/commands/dependency-operation-plan.js +15 -0
- package/dist/src/commands/dependency-operation-plan.js.map +1 -0
- package/dist/src/commands/init.d.ts +31 -0
- package/dist/src/commands/init.js +63 -0
- package/dist/src/commands/init.js.map +1 -0
- package/dist/src/commands/options.d.ts +68 -0
- package/dist/src/commands/options.js +146 -0
- package/dist/src/commands/options.js.map +1 -0
- package/dist/src/commands/rebuild-command.d.ts +77 -0
- package/dist/src/commands/rebuild-command.js +1681 -0
- package/dist/src/commands/rebuild-command.js.map +1 -0
- package/dist/src/commands/rebuild-integration.d.ts +94 -0
- package/dist/src/commands/rebuild-integration.js +3289 -0
- package/dist/src/commands/rebuild-integration.js.map +1 -0
- package/dist/src/commands/rebuild-operations.d.ts +110 -0
- package/dist/src/commands/rebuild-operations.js +791 -0
- package/dist/src/commands/rebuild-operations.js.map +1 -0
- package/dist/src/commands/review-publication.d.ts +99 -0
- package/dist/src/commands/review-publication.js +904 -0
- package/dist/src/commands/review-publication.js.map +1 -0
- package/dist/src/commands/runtime.d.ts +176 -0
- package/dist/src/commands/runtime.js +746 -0
- package/dist/src/commands/runtime.js.map +1 -0
- package/dist/src/commands/status-v4.d.ts +102 -0
- package/dist/src/commands/status-v4.js +844 -0
- package/dist/src/commands/status-v4.js.map +1 -0
- package/dist/src/commands/status.d.ts +93 -0
- package/dist/src/commands/status.js +442 -0
- package/dist/src/commands/status.js.map +1 -0
- package/dist/src/commands/wizard.d.ts +57 -0
- package/dist/src/commands/wizard.js +58 -0
- package/dist/src/commands/wizard.js.map +1 -0
- package/dist/src/commands/write-commands.d.ts +7 -0
- package/dist/src/commands/write-commands.js +167 -0
- package/dist/src/commands/write-commands.js.map +1 -0
- package/dist/src/constants.d.ts +25 -0
- package/dist/src/constants.js +26 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/generators/agents.d.ts +3 -0
- package/dist/src/generators/agents.js +46 -0
- package/dist/src/generators/agents.js.map +1 -0
- package/dist/src/generators/hooks.d.ts +15 -0
- package/dist/src/generators/hooks.js +76 -0
- package/dist/src/generators/hooks.js.map +1 -0
- package/dist/src/generators/instructions.d.ts +13 -0
- package/dist/src/generators/instructions.js +66 -0
- package/dist/src/generators/instructions.js.map +1 -0
- package/dist/src/generators/local-plugin.d.ts +53 -0
- package/dist/src/generators/local-plugin.js +1268 -0
- package/dist/src/generators/local-plugin.js.map +1 -0
- package/dist/src/generators/permissions.d.ts +10 -0
- package/dist/src/generators/permissions.js +37 -0
- package/dist/src/generators/permissions.js.map +1 -0
- package/dist/src/generators/shared.d.ts +2 -0
- package/dist/src/generators/shared.js +15 -0
- package/dist/src/generators/shared.js.map +1 -0
- package/dist/src/index.d.ts +35 -0
- package/dist/src/index.js +2344 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/output/command-receipt.d.ts +90 -0
- package/dist/src/output/command-receipt.js +129 -0
- package/dist/src/output/command-receipt.js.map +1 -0
- package/dist/src/output/debug-log.d.ts +4 -0
- package/dist/src/output/debug-log.js +97 -0
- package/dist/src/output/debug-log.js.map +1 -0
- package/dist/src/output/equivalent-command.d.ts +9 -0
- package/dist/src/output/equivalent-command.js +39 -0
- package/dist/src/output/equivalent-command.js.map +1 -0
- package/dist/src/output/model.d.ts +117 -0
- package/dist/src/output/model.js +145 -0
- package/dist/src/output/model.js.map +1 -0
- package/dist/src/output/rebuild-progress.d.ts +21 -0
- package/dist/src/output/rebuild-progress.js +138 -0
- package/dist/src/output/rebuild-progress.js.map +1 -0
- package/dist/src/output/redact.d.ts +3 -0
- package/dist/src/output/redact.js +39 -0
- package/dist/src/output/redact.js.map +1 -0
- package/dist/src/output/reporter.d.ts +9 -0
- package/dist/src/output/reporter.js +133 -0
- package/dist/src/output/reporter.js.map +1 -0
- package/dist/src/portable/commands.d.ts +120 -0
- package/dist/src/portable/commands.js +506 -0
- package/dist/src/portable/commands.js.map +1 -0
- package/dist/src/portable/materializer.d.ts +50 -0
- package/dist/src/portable/materializer.js +408 -0
- package/dist/src/portable/materializer.js.map +1 -0
- package/dist/src/portable/skills-cli.d.ts +43 -0
- package/dist/src/portable/skills-cli.js +148 -0
- package/dist/src/portable/skills-cli.js.map +1 -0
- package/dist/src/tui/components.d.ts +32 -0
- package/dist/src/tui/components.js +42 -0
- package/dist/src/tui/components.js.map +1 -0
- package/dist/src/tui/console-app-v4.d.ts +20 -0
- package/dist/src/tui/console-app-v4.js +2613 -0
- package/dist/src/tui/console-app-v4.js.map +1 -0
- package/dist/src/tui/controller-runtime.d.ts +11 -0
- package/dist/src/tui/controller-runtime.js +902 -0
- package/dist/src/tui/controller-runtime.js.map +1 -0
- package/dist/src/tui/controller.d.ts +367 -0
- package/dist/src/tui/controller.js +1724 -0
- package/dist/src/tui/controller.js.map +1 -0
- package/dist/src/tui/index.d.ts +13 -0
- package/dist/src/tui/index.js +14 -0
- package/dist/src/tui/index.js.map +1 -0
- package/dist/src/tui/keys.d.ts +12 -0
- package/dist/src/tui/keys.js +22 -0
- package/dist/src/tui/keys.js.map +1 -0
- package/dist/src/tui/layout.d.ts +12 -0
- package/dist/src/tui/layout.js +19 -0
- package/dist/src/tui/layout.js.map +1 -0
- package/dist/src/tui/model.d.ts +279 -0
- package/dist/src/tui/model.js +652 -0
- package/dist/src/tui/model.js.map +1 -0
- package/dist/src/tui/prompt.d.ts +8 -0
- package/dist/src/tui/prompt.js +242 -0
- package/dist/src/tui/prompt.js.map +1 -0
- package/dist/src/tui/router.d.ts +153 -0
- package/dist/src/tui/router.js +102 -0
- package/dist/src/tui/router.js.map +1 -0
- package/dist/src/tui/runner.d.ts +11 -0
- package/dist/src/tui/runner.js +71 -0
- package/dist/src/tui/runner.js.map +1 -0
- package/dist/src/tui/theme.d.ts +18 -0
- package/dist/src/tui/theme.js +20 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/tui/views.d.ts +57 -0
- package/dist/src/tui/views.js +173 -0
- package/dist/src/tui/views.js.map +1 -0
- package/dist/src/tui/wizard.d.ts +11 -0
- package/dist/src/tui/wizard.js +321 -0
- package/dist/src/tui/wizard.js.map +1 -0
- package/dist/src/types.d.ts +313 -0
- package/dist/src/types.js +33 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/active-marketplace-runtime-lock.d.ts +12 -0
- package/dist/src/utils/active-marketplace-runtime-lock.js +133 -0
- package/dist/src/utils/active-marketplace-runtime-lock.js.map +1 -0
- package/dist/src/utils/agent-version.d.ts +13 -0
- package/dist/src/utils/agent-version.js +172 -0
- package/dist/src/utils/agent-version.js.map +1 -0
- package/dist/src/utils/artifact-plan.d.ts +26 -0
- package/dist/src/utils/artifact-plan.js +444 -0
- package/dist/src/utils/artifact-plan.js.map +1 -0
- package/dist/src/utils/artifact-renderer.d.ts +124 -0
- package/dist/src/utils/artifact-renderer.js +442 -0
- package/dist/src/utils/artifact-renderer.js.map +1 -0
- package/dist/src/utils/artifact-sources.d.ts +8 -0
- package/dist/src/utils/artifact-sources.js +10 -0
- package/dist/src/utils/artifact-sources.js.map +1 -0
- package/dist/src/utils/aw-home-mutation-lock.d.ts +10 -0
- package/dist/src/utils/aw-home-mutation-lock.js +55 -0
- package/dist/src/utils/aw-home-mutation-lock.js.map +1 -0
- package/dist/src/utils/bundle-integrity.d.ts +3 -0
- package/dist/src/utils/bundle-integrity.js +88 -0
- package/dist/src/utils/bundle-integrity.js.map +1 -0
- package/dist/src/utils/bundle-layout.d.ts +4 -0
- package/dist/src/utils/bundle-layout.js +546 -0
- package/dist/src/utils/bundle-layout.js.map +1 -0
- package/dist/src/utils/catalog-digests.d.ts +8 -0
- package/dist/src/utils/catalog-digests.js +108 -0
- package/dist/src/utils/catalog-digests.js.map +1 -0
- package/dist/src/utils/catalog-manifest-v3.d.ts +2 -0
- package/dist/src/utils/catalog-manifest-v3.js +3 -0
- package/dist/src/utils/catalog-manifest-v3.js.map +1 -0
- package/dist/src/utils/catalog-manifest-v4.d.ts +5 -0
- package/dist/src/utils/catalog-manifest-v4.js +106 -0
- package/dist/src/utils/catalog-manifest-v4.js.map +1 -0
- package/dist/src/utils/catalog-public-error.d.ts +15 -0
- package/dist/src/utils/catalog-public-error.js +84 -0
- package/dist/src/utils/catalog-public-error.js.map +1 -0
- package/dist/src/utils/catalog-runtime-projection.d.ts +36 -0
- package/dist/src/utils/catalog-runtime-projection.js +172 -0
- package/dist/src/utils/catalog-runtime-projection.js.map +1 -0
- package/dist/src/utils/catalog-snapshot.d.ts +8 -0
- package/dist/src/utils/catalog-snapshot.js +12 -0
- package/dist/src/utils/catalog-snapshot.js.map +1 -0
- package/dist/src/utils/catalog-source-cache.d.ts +61 -0
- package/dist/src/utils/catalog-source-cache.js +276 -0
- package/dist/src/utils/catalog-source-cache.js.map +1 -0
- package/dist/src/utils/catalog-source-identity.d.ts +4 -0
- package/dist/src/utils/catalog-source-identity.js +118 -0
- package/dist/src/utils/catalog-source-identity.js.map +1 -0
- package/dist/src/utils/catalog-source-presentation.d.ts +1 -0
- package/dist/src/utils/catalog-source-presentation.js +32 -0
- package/dist/src/utils/catalog-source-presentation.js.map +1 -0
- package/dist/src/utils/catalog-source-references.d.ts +65 -0
- package/dist/src/utils/catalog-source-references.js +1418 -0
- package/dist/src/utils/catalog-source-references.js.map +1 -0
- package/dist/src/utils/catalog-sources.d.ts +39 -0
- package/dist/src/utils/catalog-sources.js +293 -0
- package/dist/src/utils/catalog-sources.js.map +1 -0
- package/dist/src/utils/codex-config.d.ts +124 -0
- package/dist/src/utils/codex-config.js +1011 -0
- package/dist/src/utils/codex-config.js.map +1 -0
- package/dist/src/utils/codex-project-surface.d.ts +3 -0
- package/dist/src/utils/codex-project-surface.js +49 -0
- package/dist/src/utils/codex-project-surface.js.map +1 -0
- package/dist/src/utils/codex-roles.d.ts +71 -0
- package/dist/src/utils/codex-roles.js +257 -0
- package/dist/src/utils/codex-roles.js.map +1 -0
- package/dist/src/utils/codex-skill-staging.d.ts +9 -0
- package/dist/src/utils/codex-skill-staging.js +8 -0
- package/dist/src/utils/codex-skill-staging.js.map +1 -0
- package/dist/src/utils/config-value.d.ts +3 -0
- package/dist/src/utils/config-value.js +23 -0
- package/dist/src/utils/config-value.js.map +1 -0
- package/dist/src/utils/dynamic-skill-tree.d.ts +32 -0
- package/dist/src/utils/dynamic-skill-tree.js +436 -0
- package/dist/src/utils/dynamic-skill-tree.js.map +1 -0
- package/dist/src/utils/file-rules.d.ts +1 -0
- package/dist/src/utils/file-rules.js +2 -0
- package/dist/src/utils/file-rules.js.map +1 -0
- package/dist/src/utils/git-catalog-provider.d.ts +7 -0
- package/dist/src/utils/git-catalog-provider.js +510 -0
- package/dist/src/utils/git-catalog-provider.js.map +1 -0
- package/dist/src/utils/hooks-contract.d.ts +1 -0
- package/dist/src/utils/hooks-contract.js +2 -0
- package/dist/src/utils/hooks-contract.js.map +1 -0
- package/dist/src/utils/json.d.ts +4 -0
- package/dist/src/utils/json.js +29 -0
- package/dist/src/utils/json.js.map +1 -0
- package/dist/src/utils/lifecycle-active-store.d.ts +175 -0
- package/dist/src/utils/lifecycle-active-store.js +1276 -0
- package/dist/src/utils/lifecycle-active-store.js.map +1 -0
- package/dist/src/utils/local-marketplace.d.ts +41 -0
- package/dist/src/utils/local-marketplace.js +317 -0
- package/dist/src/utils/local-marketplace.js.map +1 -0
- package/dist/src/utils/manifest.d.ts +14 -0
- package/dist/src/utils/manifest.js +36 -0
- package/dist/src/utils/manifest.js.map +1 -0
- package/dist/src/utils/marketplace.d.ts +3 -0
- package/dist/src/utils/marketplace.js +76 -0
- package/dist/src/utils/marketplace.js.map +1 -0
- package/dist/src/utils/multi-source-catalog.d.ts +72 -0
- package/dist/src/utils/multi-source-catalog.js +402 -0
- package/dist/src/utils/multi-source-catalog.js.map +1 -0
- package/dist/src/utils/native-cli.d.ts +75 -0
- package/dist/src/utils/native-cli.js +435 -0
- package/dist/src/utils/native-cli.js.map +1 -0
- package/dist/src/utils/orphan-marketplace-cleanup.d.ts +18 -0
- package/dist/src/utils/orphan-marketplace-cleanup.js +202 -0
- package/dist/src/utils/orphan-marketplace-cleanup.js.map +1 -0
- package/dist/src/utils/ownership.d.ts +130 -0
- package/dist/src/utils/ownership.js +766 -0
- package/dist/src/utils/ownership.js.map +1 -0
- package/dist/src/utils/path-catalog-provider.d.ts +7 -0
- package/dist/src/utils/path-catalog-provider.js +87 -0
- package/dist/src/utils/path-catalog-provider.js.map +1 -0
- package/dist/src/utils/paths.d.ts +41 -0
- package/dist/src/utils/paths.js +138 -0
- package/dist/src/utils/paths.js.map +1 -0
- package/dist/src/utils/platform-entries.d.ts +1 -0
- package/dist/src/utils/platform-entries.js +2 -0
- package/dist/src/utils/platform-entries.js.map +1 -0
- package/dist/src/utils/platform-support.d.ts +1 -0
- package/dist/src/utils/platform-support.js +2 -0
- package/dist/src/utils/platform-support.js.map +1 -0
- package/dist/src/utils/plugin-authoring-capabilities.d.ts +23 -0
- package/dist/src/utils/plugin-authoring-capabilities.js +37 -0
- package/dist/src/utils/plugin-authoring-capabilities.js.map +1 -0
- package/dist/src/utils/plugin-authoring-capabilities.json +92 -0
- package/dist/src/utils/plugin-config-write.d.ts +107 -0
- package/dist/src/utils/plugin-config-write.js +565 -0
- package/dist/src/utils/plugin-config-write.js.map +1 -0
- package/dist/src/utils/plugin-config.d.ts +396 -0
- package/dist/src/utils/plugin-config.js +1898 -0
- package/dist/src/utils/plugin-config.js.map +1 -0
- package/dist/src/utils/plugin-dependencies.d.ts +12 -0
- package/dist/src/utils/plugin-dependencies.js +162 -0
- package/dist/src/utils/plugin-dependencies.js.map +1 -0
- package/dist/src/utils/plugin-release.d.ts +2 -0
- package/dist/src/utils/plugin-release.js +2 -0
- package/dist/src/utils/plugin-release.js.map +1 -0
- package/dist/src/utils/plugin-script-contract.d.ts +1 -0
- package/dist/src/utils/plugin-script-contract.js +2 -0
- package/dist/src/utils/plugin-script-contract.js.map +1 -0
- package/dist/src/utils/plugin-scripts.d.ts +172 -0
- package/dist/src/utils/plugin-scripts.js +585 -0
- package/dist/src/utils/plugin-scripts.js.map +1 -0
- package/dist/src/utils/process-lock.d.ts +47 -0
- package/dist/src/utils/process-lock.js +532 -0
- package/dist/src/utils/process-lock.js.map +1 -0
- package/dist/src/utils/process-tree.d.ts +2 -0
- package/dist/src/utils/process-tree.js +2 -0
- package/dist/src/utils/process-tree.js.map +1 -0
- package/dist/src/utils/project-boundary.d.ts +21 -0
- package/dist/src/utils/project-boundary.js +95 -0
- package/dist/src/utils/project-boundary.js.map +1 -0
- package/dist/src/utils/rendered-contract.d.ts +8 -0
- package/dist/src/utils/rendered-contract.js +134 -0
- package/dist/src/utils/rendered-contract.js.map +1 -0
- package/dist/src/utils/runtime-skill-closure.d.ts +13 -0
- package/dist/src/utils/runtime-skill-closure.js +863 -0
- package/dist/src/utils/runtime-skill-closure.js.map +1 -0
- package/dist/src/utils/skill-authoring.d.ts +1 -0
- package/dist/src/utils/skill-authoring.js +2 -0
- package/dist/src/utils/skill-authoring.js.map +1 -0
- package/dist/src/utils/skill-frontmatter-capabilities.d.ts +201 -0
- package/dist/src/utils/skill-frontmatter-capabilities.js +5 -0
- package/dist/src/utils/skill-frontmatter-capabilities.js.map +1 -0
- package/dist/src/utils/skill-frontmatter-capabilities.json +200 -0
- package/dist/src/utils/skill-validation.d.ts +37 -0
- package/dist/src/utils/skill-validation.js +326 -0
- package/dist/src/utils/skill-validation.js.map +1 -0
- package/dist/src/utils/slash-commands.d.ts +1 -0
- package/dist/src/utils/slash-commands.js +2 -0
- package/dist/src/utils/slash-commands.js.map +1 -0
- package/dist/src/utils/state-paths.d.ts +13 -0
- package/dist/src/utils/state-paths.js +129 -0
- package/dist/src/utils/state-paths.js.map +1 -0
- package/dist/src/utils/target-eligibility.d.ts +7 -0
- package/dist/src/utils/target-eligibility.js +26 -0
- package/dist/src/utils/target-eligibility.js.map +1 -0
- package/dist/src/utils/target-order.d.ts +7 -0
- package/dist/src/utils/target-order.js +10 -0
- package/dist/src/utils/target-order.js.map +1 -0
- package/dist/src/utils/template-engine.d.ts +1 -0
- package/dist/src/utils/template-engine.js +2 -0
- package/dist/src/utils/template-engine.js.map +1 -0
- package/dist/src/utils/test-path-fence.d.ts +12 -0
- package/dist/src/utils/test-path-fence.js +31 -0
- package/dist/src/utils/test-path-fence.js.map +1 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +16 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,4442 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import * as os from 'node:os';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { checkPluginVersions } from './lib/plugin-version-check.mjs';
|
|
10
|
+
import { scanJavaScriptImportSpecifiers } from './lib/javascript-import-scanner.mjs';
|
|
11
|
+
import { assertAuthoringPluginPathFence, assertCanonicalAuthoringPluginRoot, } from './lib/plugin-root.mjs';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const authoringRuntimeRoot = path.resolve(__dirname, '..', '..');
|
|
15
|
+
const CLI_PACKAGE_ROOT = path.basename(authoringRuntimeRoot) === 'dist'
|
|
16
|
+
? path.dirname(authoringRuntimeRoot)
|
|
17
|
+
: authoringRuntimeRoot;
|
|
18
|
+
const errors = [];
|
|
19
|
+
const warnings = [];
|
|
20
|
+
let pluginAuthoringCapabilityRegistry = null;
|
|
21
|
+
const SOURCE_PLUGIN_MANIFEST_CONTRACT_VERSION = 7;
|
|
22
|
+
const MAX_SKILL_NAME_LEN = 64;
|
|
23
|
+
const MAX_SKILL_DESCRIPTION_LEN = 1024;
|
|
24
|
+
const MAX_SKILL_COMPATIBILITY_LEN = 500;
|
|
25
|
+
const MAX_SKILL_SHORT_DESCRIPTION_LEN = 1024;
|
|
26
|
+
const MAX_SKILL_LINES = 500;
|
|
27
|
+
const MAX_OPENAI_INTERFACE_NAME_LEN = 64;
|
|
28
|
+
const MIN_OPENAI_INTERFACE_DESCRIPTION_LEN = 25;
|
|
29
|
+
const MAX_OPENAI_INTERFACE_DESCRIPTION_LEN = 64;
|
|
30
|
+
const MAX_OPENAI_DEFAULT_PROMPT_LEN = 1024;
|
|
31
|
+
const MAX_OPENAI_DEPENDENCY_TYPE_LEN = 64;
|
|
32
|
+
const MAX_OPENAI_DEPENDENCY_TRANSPORT_LEN = 64;
|
|
33
|
+
const MAX_OPENAI_DEPENDENCY_VALUE_LEN = 1024;
|
|
34
|
+
const MAX_OPENAI_DEPENDENCY_DESCRIPTION_LEN = 1024;
|
|
35
|
+
const MAX_OPENAI_DEPENDENCY_COMMAND_LEN = 1024;
|
|
36
|
+
const MAX_OPENAI_DEPENDENCY_URL_LEN = 1024;
|
|
37
|
+
const CLAUDE_FRONTMATTER_SHELLS = new Set(['bash', 'powershell']);
|
|
38
|
+
const CLAUDE_SLASH_COMMAND_ONLY_FIELDS = new Set([
|
|
39
|
+
'allowed-tools',
|
|
40
|
+
'model',
|
|
41
|
+
'argument-hint',
|
|
42
|
+
'disable-model-invocation',
|
|
43
|
+
'shell',
|
|
44
|
+
]);
|
|
45
|
+
const CODEX_POLICY_PRODUCTS = new Set([
|
|
46
|
+
'atlas',
|
|
47
|
+
'ATLAS',
|
|
48
|
+
'chatgpt',
|
|
49
|
+
'CHATGPT',
|
|
50
|
+
'codex',
|
|
51
|
+
'CODEX',
|
|
52
|
+
]);
|
|
53
|
+
const MANIFEST_POLICY_PRODUCTS = new Set(getPluginAuthoringAcceptedValues('plugin-manifest', 'marketplace.policy.products'));
|
|
54
|
+
const MARKETPLACE_AUTHENTICATION_POLICIES = new Set(getPluginAuthoringAcceptedValues('plugin-manifest', 'marketplace.policy.authentication'));
|
|
55
|
+
const MARKETPLACE_INSTALLATION_POLICIES = new Set(getPluginAuthoringAcceptedValues('plugin-manifest', 'marketplace.policy.installation'));
|
|
56
|
+
const PUBLIC_HOOK_EVENTS = new Set(['before_tool', 'after_tool', 'session_start']);
|
|
57
|
+
const PUBLIC_SESSION_START_SOURCES = new Set(['startup', 'resume', 'clear']);
|
|
58
|
+
const PUBLIC_SESSION_START_DEFAULT_MATCHER = 'startup|resume|clear';
|
|
59
|
+
const PUBLIC_HOOK_RENDER_MAP = {
|
|
60
|
+
claude: {
|
|
61
|
+
before_tool: 'PreToolUse',
|
|
62
|
+
after_tool: 'PostToolUse',
|
|
63
|
+
session_start: 'SessionStart',
|
|
64
|
+
},
|
|
65
|
+
codex: {
|
|
66
|
+
before_tool: 'PreToolUse',
|
|
67
|
+
after_tool: 'PostToolUse',
|
|
68
|
+
session_start: 'SessionStart',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const NATIVE_HOOK_EVENTS = {
|
|
72
|
+
claude: new Set([
|
|
73
|
+
'PreToolUse',
|
|
74
|
+
'PostToolUse',
|
|
75
|
+
'PostToolUseFailure',
|
|
76
|
+
'Notification',
|
|
77
|
+
'UserPromptSubmit',
|
|
78
|
+
'SessionStart',
|
|
79
|
+
'SessionEnd',
|
|
80
|
+
'Stop',
|
|
81
|
+
'StopFailure',
|
|
82
|
+
'SubagentStart',
|
|
83
|
+
'SubagentStop',
|
|
84
|
+
'PreCompact',
|
|
85
|
+
'PostCompact',
|
|
86
|
+
'PermissionRequest',
|
|
87
|
+
'PermissionDenied',
|
|
88
|
+
'Setup',
|
|
89
|
+
'TeammateIdle',
|
|
90
|
+
'TaskCreated',
|
|
91
|
+
'TaskCompleted',
|
|
92
|
+
'Elicitation',
|
|
93
|
+
'ElicitationResult',
|
|
94
|
+
'ConfigChange',
|
|
95
|
+
'WorktreeCreate',
|
|
96
|
+
'WorktreeRemove',
|
|
97
|
+
'InstructionsLoaded',
|
|
98
|
+
'CwdChanged',
|
|
99
|
+
'FileChanged',
|
|
100
|
+
]),
|
|
101
|
+
codex: new Set([
|
|
102
|
+
'PreToolUse',
|
|
103
|
+
'PermissionRequest',
|
|
104
|
+
'PostToolUse',
|
|
105
|
+
'SessionStart',
|
|
106
|
+
'UserPromptSubmit',
|
|
107
|
+
'Stop',
|
|
108
|
+
]),
|
|
109
|
+
};
|
|
110
|
+
const MATCHERLESS_NATIVE_HOOK_EVENTS = {
|
|
111
|
+
claude: new Set([
|
|
112
|
+
'UserPromptSubmit',
|
|
113
|
+
'Stop',
|
|
114
|
+
'TeammateIdle',
|
|
115
|
+
'TaskCreated',
|
|
116
|
+
'TaskCompleted',
|
|
117
|
+
'WorktreeCreate',
|
|
118
|
+
'WorktreeRemove',
|
|
119
|
+
'CwdChanged',
|
|
120
|
+
]),
|
|
121
|
+
codex: new Set(['UserPromptSubmit', 'Stop']),
|
|
122
|
+
};
|
|
123
|
+
const TEMPLATE_TEXT_FILE_EXTENSIONS = /\.(md|markdown|txt|json|yaml|yml|toml|rules)$/i;
|
|
124
|
+
const SLASH_COMMAND_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
125
|
+
const DEFAULT_SLASH_COMMANDS_DIR = 'slash-commands';
|
|
126
|
+
const SCRIPT_NODE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
127
|
+
const SCRIPT_AUTO_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.sh', '.ps1', '.cmd', '.bat']);
|
|
128
|
+
const SCRIPT_RUNNERS = new Set(['node', 'sh', 'bash', 'pwsh', 'powershell', 'cmd', 'auto']);
|
|
129
|
+
const SCRIPT_OS_SELECTORS = new Set(['default', 'unix', 'windows', 'linux', 'darwin', 'win32']);
|
|
130
|
+
const SCRIPT_LIFECYCLE_EVENTS = new Set(['install', 'upgrade', 'uninstall']);
|
|
131
|
+
const SCRIPT_TIMEOUT_MIN_MS = 1000;
|
|
132
|
+
const SCRIPT_TIMEOUT_MAX_MS = 300000;
|
|
133
|
+
const CONFIG_FIELD_TYPES = new Set(getPluginAuthoringAcceptedValues('config-lifecycle-artifact', 'config.schema.<key>.type'));
|
|
134
|
+
const ARTIFACT_TARGETS = new Set(getPluginAuthoringAcceptedValues('config-lifecycle-artifact', 'artifacts.targets[].target'));
|
|
135
|
+
const ARTIFACT_STATES = new Set(getPluginAuthoringAcceptedValues('config-lifecycle-artifact', 'ArtifactPlan.artifacts[].state'));
|
|
136
|
+
const ARTIFACT_SCOPES = new Set(['user', 'project']);
|
|
137
|
+
const ARTIFACT_KINDS = new Set(['instruction', 'codexRole', 'skill']);
|
|
138
|
+
const ARTIFACT_PROJECT_MANAGED_KINDS = new Set(['instruction', 'agent', 'skill']);
|
|
139
|
+
const ARTIFACT_KIND_BY_TARGET = {
|
|
140
|
+
'codex.developerInstructionsBlock': 'instruction',
|
|
141
|
+
'codex.agentRole': 'codexRole',
|
|
142
|
+
'codex.skill': 'skill',
|
|
143
|
+
'claude.skill': 'skill',
|
|
144
|
+
'agent-skills.skill': 'skill',
|
|
145
|
+
};
|
|
146
|
+
const ARTIFACT_PROJECT_MANAGED_KIND_BY_TARGET = {
|
|
147
|
+
'codex.developerInstructionsBlock': 'instruction',
|
|
148
|
+
'codex.agentRole': 'agent',
|
|
149
|
+
'codex.skill': 'skill',
|
|
150
|
+
};
|
|
151
|
+
const PLUGIN_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
152
|
+
const PLUGIN_EVAL_SCHEMA_VERSION = 2;
|
|
153
|
+
const PLUGIN_EVAL_CASE_SCHEMA_VERSION = 1;
|
|
154
|
+
const PLUGIN_EVAL_SCOPES = new Set(['user', 'project']);
|
|
155
|
+
const PLUGIN_EVAL_SPLITS = new Set(['train', 'validation', 'sanity', 'all']);
|
|
156
|
+
const PLUGIN_EVAL_TRIGGER_CASE_SPLITS = new Set(['train', 'validation', 'sanity']);
|
|
157
|
+
const PLUGIN_EVAL_COMMAND_SUPPORTS = new Set([
|
|
158
|
+
'split',
|
|
159
|
+
'runs',
|
|
160
|
+
'case',
|
|
161
|
+
'model',
|
|
162
|
+
'codexBin',
|
|
163
|
+
'timeout',
|
|
164
|
+
'allowEnvironmentSkips',
|
|
165
|
+
]);
|
|
166
|
+
let yamlParser = null;
|
|
167
|
+
let skillFrontmatterCapabilityRegistry = null;
|
|
168
|
+
let pluginVersionCheckResult = null;
|
|
169
|
+
function error(msg) {
|
|
170
|
+
errors.push(msg);
|
|
171
|
+
}
|
|
172
|
+
function warn(msg) {
|
|
173
|
+
warnings.push(msg);
|
|
174
|
+
}
|
|
175
|
+
function loadYamlParser(sourceName) {
|
|
176
|
+
if (yamlParser)
|
|
177
|
+
return yamlParser;
|
|
178
|
+
try {
|
|
179
|
+
yamlParser = require('yaml');
|
|
180
|
+
return yamlParser;
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
throw new Error(`${sourceName}: YAML parser dependency "yaml" is not installed; run pnpm install in the agent-workflows repo`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function stripStandaloneTemplateControlLines(content) {
|
|
187
|
+
return content
|
|
188
|
+
.replace(/\r\n/g, '\n')
|
|
189
|
+
.split('\n')
|
|
190
|
+
.map((line) => {
|
|
191
|
+
const trimmed = line.trim();
|
|
192
|
+
if (/^{{[#/^]/.test(trimmed) || /^{{else\b/.test(trimmed)) {
|
|
193
|
+
return '';
|
|
194
|
+
}
|
|
195
|
+
return line;
|
|
196
|
+
})
|
|
197
|
+
.join('\n');
|
|
198
|
+
}
|
|
199
|
+
function stripDotSlash(value) {
|
|
200
|
+
return value.startsWith('./') ? value.slice(2) : value;
|
|
201
|
+
}
|
|
202
|
+
function escapeRegexSegment(value) {
|
|
203
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
204
|
+
}
|
|
205
|
+
function compileGlob(pattern) {
|
|
206
|
+
const normalized = pattern.split(path.sep).join('/');
|
|
207
|
+
let source = '^';
|
|
208
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
209
|
+
const char = normalized[index];
|
|
210
|
+
const next = normalized[index + 1];
|
|
211
|
+
if (char === '*' && next === '*') {
|
|
212
|
+
source += '.*';
|
|
213
|
+
index += 1;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (char === '*') {
|
|
217
|
+
source += '[^/]*';
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (char === '?') {
|
|
221
|
+
source += '.';
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
source += escapeRegexSegment(char);
|
|
225
|
+
}
|
|
226
|
+
source += '$';
|
|
227
|
+
return new RegExp(source);
|
|
228
|
+
}
|
|
229
|
+
function normalizeRelativePath(filePath) {
|
|
230
|
+
return path.posix.normalize(stripDotSlash(filePath).split(path.sep).join('/'));
|
|
231
|
+
}
|
|
232
|
+
function matchesGlob(filePath, pattern) {
|
|
233
|
+
return compileGlob(pattern).test(normalizeRelativePath(filePath));
|
|
234
|
+
}
|
|
235
|
+
function isTemplateTextFile(relativePath) {
|
|
236
|
+
return TEMPLATE_TEXT_FILE_EXTENSIONS.test(relativePath);
|
|
237
|
+
}
|
|
238
|
+
function templateIncludesPath(manifest, relativePath) {
|
|
239
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
240
|
+
const include = manifest.buildConfig?.templates?.include;
|
|
241
|
+
if (!Array.isArray(include) || !isTemplateTextFile(normalized))
|
|
242
|
+
return false;
|
|
243
|
+
return include.some((pattern) => matchesGlob(normalized, pattern));
|
|
244
|
+
}
|
|
245
|
+
function resolveTemplateValue(vars, platform) {
|
|
246
|
+
const resolved = {};
|
|
247
|
+
for (const [key, entry] of Object.entries(vars)) {
|
|
248
|
+
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
|
249
|
+
resolved[key] = entry[platform];
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return resolved;
|
|
253
|
+
}
|
|
254
|
+
function replaceConditionalBlocks(input, resolvedVars, tagName) {
|
|
255
|
+
const pattern = new RegExp(`{{#${tagName}\\s+([a-zA-Z0-9_]+)}}([\\s\\S]*?){{\\/${tagName}}}`, 'g');
|
|
256
|
+
return input.replace(pattern, (_match, rawKey, blockContent) => {
|
|
257
|
+
const truthy = resolvedVars[rawKey] === true;
|
|
258
|
+
if (tagName === 'if') {
|
|
259
|
+
return truthy ? blockContent : '';
|
|
260
|
+
}
|
|
261
|
+
return truthy ? '' : blockContent;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
function renderTemplate(content, vars, platform) {
|
|
265
|
+
const resolvedVars = resolveTemplateValue(vars, platform);
|
|
266
|
+
const withConditionals = replaceConditionalBlocks(replaceConditionalBlocks(content, resolvedVars, 'if'), resolvedVars, 'unless');
|
|
267
|
+
return withConditionals.replace(/{{([a-zA-Z0-9_]+)}}/g, (_match, rawKey) => {
|
|
268
|
+
const value = resolvedVars[rawKey];
|
|
269
|
+
if (value === undefined)
|
|
270
|
+
return '';
|
|
271
|
+
return String(value);
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function renderTemplateForPlatform(manifest, relativePath, content, platform) {
|
|
275
|
+
if (!templateIncludesPath(manifest, relativePath))
|
|
276
|
+
return content;
|
|
277
|
+
const vars = manifest.buildConfig?.templates?.vars ?? {};
|
|
278
|
+
return renderTemplate(content, vars, platform);
|
|
279
|
+
}
|
|
280
|
+
function renderedContentVariants(manifest, relativePath, content, platforms) {
|
|
281
|
+
if (!templateIncludesPath(manifest, relativePath)) {
|
|
282
|
+
return [{ content, platform: null }];
|
|
283
|
+
}
|
|
284
|
+
const seen = new Set();
|
|
285
|
+
const variants = [];
|
|
286
|
+
for (const platform of platforms) {
|
|
287
|
+
const rendered = renderTemplateForPlatform(manifest, relativePath, content, platform);
|
|
288
|
+
if (seen.has(rendered))
|
|
289
|
+
continue;
|
|
290
|
+
seen.add(rendered);
|
|
291
|
+
variants.push({ content: rendered, platform });
|
|
292
|
+
}
|
|
293
|
+
return variants;
|
|
294
|
+
}
|
|
295
|
+
function formatYamlError(parseError, lineCounter, sourceName) {
|
|
296
|
+
let location = '';
|
|
297
|
+
if (Array.isArray(parseError.pos) && Number.isInteger(parseError.pos[0])) {
|
|
298
|
+
const linePos = lineCounter.linePos(parseError.pos[0]);
|
|
299
|
+
location = `:${linePos.line}:${linePos.col}`;
|
|
300
|
+
}
|
|
301
|
+
let message = parseError.message || String(parseError);
|
|
302
|
+
if (parseError.code === 'BLOCK_AS_IMPLICIT_KEY') {
|
|
303
|
+
message = `${message}; plain scalar values containing ": " must be quoted`;
|
|
304
|
+
}
|
|
305
|
+
return `${sourceName}${location}: ${message}`;
|
|
306
|
+
}
|
|
307
|
+
function parseYaml(content, sourceName) {
|
|
308
|
+
const yaml = loadYamlParser(sourceName);
|
|
309
|
+
const lineCounter = new yaml.LineCounter();
|
|
310
|
+
const document = yaml.parseDocument(stripStandaloneTemplateControlLines(content), {
|
|
311
|
+
lineCounter,
|
|
312
|
+
prettyErrors: false,
|
|
313
|
+
});
|
|
314
|
+
if (document.errors.length > 0) {
|
|
315
|
+
throw new Error(formatYamlError(document.errors[0], lineCounter, sourceName));
|
|
316
|
+
}
|
|
317
|
+
const value = document.toJSON();
|
|
318
|
+
return value ?? {};
|
|
319
|
+
}
|
|
320
|
+
function parseFrontmatter(content, sourceName) {
|
|
321
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
322
|
+
if (!match)
|
|
323
|
+
return null;
|
|
324
|
+
return parseYaml(match[1], sourceName);
|
|
325
|
+
}
|
|
326
|
+
function loadPluginAuthoringCapabilityRegistry() {
|
|
327
|
+
if (pluginAuthoringCapabilityRegistry)
|
|
328
|
+
return pluginAuthoringCapabilityRegistry;
|
|
329
|
+
const registryPath = [
|
|
330
|
+
path.join(CLI_PACKAGE_ROOT, 'dist', 'src', 'utils', 'plugin-authoring-capabilities.json'),
|
|
331
|
+
path.resolve(__dirname, '..', '..', 'dist', 'src', 'utils', 'plugin-authoring-capabilities.json'),
|
|
332
|
+
].find((candidate) => fs.existsSync(candidate));
|
|
333
|
+
if (!registryPath) {
|
|
334
|
+
throw new Error('Plugin authoring capability registry not found in the built CLI.');
|
|
335
|
+
}
|
|
336
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
337
|
+
if (!registry || registry.schemaVersion !== 1 || !registry.surfaces) {
|
|
338
|
+
throw new Error(`Invalid plugin authoring capability registry: ${registryPath}`);
|
|
339
|
+
}
|
|
340
|
+
pluginAuthoringCapabilityRegistry = registry;
|
|
341
|
+
return registry;
|
|
342
|
+
}
|
|
343
|
+
function getPluginAuthoringAcceptedValues(surface, field) {
|
|
344
|
+
const entry = loadPluginAuthoringCapabilityRegistry().surfaces[surface]?.entries
|
|
345
|
+
?.find((candidate) => candidate.field === field);
|
|
346
|
+
if (!entry || !Array.isArray(entry.acceptedValues)) {
|
|
347
|
+
throw new Error(`Plugin authoring capability ${surface}.${field} has no accepted values.`);
|
|
348
|
+
}
|
|
349
|
+
return entry.acceptedValues;
|
|
350
|
+
}
|
|
351
|
+
function loadSkillFrontmatterCapabilityRegistry() {
|
|
352
|
+
if (skillFrontmatterCapabilityRegistry)
|
|
353
|
+
return skillFrontmatterCapabilityRegistry;
|
|
354
|
+
const registryPath = path.join(CLI_PACKAGE_ROOT, 'dist', 'src', 'utils', 'skill-frontmatter-capabilities.json');
|
|
355
|
+
if (!fs.existsSync(registryPath)) {
|
|
356
|
+
throw new Error(`Skill frontmatter capability registry not found: ${registryPath}`);
|
|
357
|
+
}
|
|
358
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
359
|
+
if (!registry ||
|
|
360
|
+
registry.schemaVersion !== 1 ||
|
|
361
|
+
!registry.profiles ||
|
|
362
|
+
!registry.fields) {
|
|
363
|
+
throw new Error(`Invalid Skill frontmatter capability registry: ${registryPath}`);
|
|
364
|
+
}
|
|
365
|
+
skillFrontmatterCapabilityRegistry = registry;
|
|
366
|
+
return registry;
|
|
367
|
+
}
|
|
368
|
+
async function tryLoadCliManifestTools() {
|
|
369
|
+
const manifestModulePath = path.join(CLI_PACKAGE_ROOT, 'dist', 'src', 'utils', 'manifest.js');
|
|
370
|
+
const runtimeProjectionModulePath = path.join(CLI_PACKAGE_ROOT, 'dist', 'src', 'utils', 'catalog-runtime-projection.js');
|
|
371
|
+
if (!fs.existsSync(manifestModulePath) || !fs.existsSync(runtimeProjectionModulePath))
|
|
372
|
+
return null;
|
|
373
|
+
try {
|
|
374
|
+
const [manifestModule, runtimeProjectionModule] = await Promise.all([
|
|
375
|
+
import(pathToFileURL(manifestModulePath).href),
|
|
376
|
+
import(pathToFileURL(runtimeProjectionModulePath).href),
|
|
377
|
+
]);
|
|
378
|
+
if (manifestModule.SOURCE_PLUGIN_MANIFEST_CONTRACT_VERSION
|
|
379
|
+
!== SOURCE_PLUGIN_MANIFEST_CONTRACT_VERSION) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
assertPluginInputTree: manifestModule.assertPluginInputTree ?? null,
|
|
384
|
+
validateSourcePluginManifest: manifestModule.validateSourcePluginManifest ?? null,
|
|
385
|
+
readPluginManifest: manifestModule.readPluginManifest ?? null,
|
|
386
|
+
discoverPlugins: manifestModule.discoverPlugins ?? null,
|
|
387
|
+
createRuntimeResolver: runtimeProjectionModule.createCatalogRuntimeImportResolver ?? null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
async function tryLoadCliSkillValidationTools() {
|
|
395
|
+
const modulePath = path.join(CLI_PACKAGE_ROOT, 'dist', 'src', 'utils', 'skill-validation.js');
|
|
396
|
+
if (!fs.existsSync(modulePath))
|
|
397
|
+
return null;
|
|
398
|
+
try {
|
|
399
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
400
|
+
if (typeof mod.parseSkillFrontmatter !== 'function'
|
|
401
|
+
|| typeof mod.validateSkillFrontmatter !== 'function'
|
|
402
|
+
|| typeof mod.validateCodexMetadata !== 'function') {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
parseSkillFrontmatter: mod.parseSkillFrontmatter,
|
|
407
|
+
validateSkillFrontmatter: mod.validateSkillFrontmatter,
|
|
408
|
+
validateCodexMetadata: mod.validateCodexMetadata,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
function findRepoRootCandidate(pluginDir) {
|
|
416
|
+
let currentPath = path.dirname(pluginDir);
|
|
417
|
+
while (true) {
|
|
418
|
+
if (fs.lstatSync(path.join(currentPath, '.git'), { throwIfNoEntry: false })) {
|
|
419
|
+
return currentPath;
|
|
420
|
+
}
|
|
421
|
+
const parentPath = path.dirname(currentPath);
|
|
422
|
+
if (parentPath === currentPath)
|
|
423
|
+
return null;
|
|
424
|
+
currentPath = parentPath;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function discoverCanonicalRepoRoot(repoRootCandidate) {
|
|
428
|
+
if (!repoRootCandidate)
|
|
429
|
+
return null;
|
|
430
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
431
|
+
cwd: repoRootCandidate,
|
|
432
|
+
encoding: 'utf8',
|
|
433
|
+
});
|
|
434
|
+
if (result.status !== 0 || !result.stdout.trim())
|
|
435
|
+
return null;
|
|
436
|
+
const repoRoot = path.resolve(result.stdout.trim());
|
|
437
|
+
return fs.realpathSync(repoRoot) === fs.realpathSync(repoRootCandidate)
|
|
438
|
+
? repoRoot
|
|
439
|
+
: null;
|
|
440
|
+
}
|
|
441
|
+
function resolveCanonicalRepoRoot(pluginDir, pluginName, repoRoot) {
|
|
442
|
+
if (!repoRoot)
|
|
443
|
+
return null;
|
|
444
|
+
return fs.realpathSync(pluginDir) === path.join(fs.realpathSync(repoRoot), 'plugins', pluginName)
|
|
445
|
+
? repoRoot
|
|
446
|
+
: null;
|
|
447
|
+
}
|
|
448
|
+
// ---------------------------------------------------------------------------
|
|
449
|
+
// Lightweight manifest checks (fallback when CLI not built)
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
const STRICT_SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))(?:\.(?:0|[1-9]\d*|(?:[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)))*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/;
|
|
452
|
+
function parseStrictSemVer(value) {
|
|
453
|
+
const stringValue = String(value);
|
|
454
|
+
const match = stringValue.match(STRICT_SEMVER_PATTERN);
|
|
455
|
+
if (!match)
|
|
456
|
+
return null;
|
|
457
|
+
return {
|
|
458
|
+
raw: stringValue,
|
|
459
|
+
major: Number(match[1]),
|
|
460
|
+
minor: Number(match[2]),
|
|
461
|
+
patch: Number(match[3]),
|
|
462
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
463
|
+
build: match[5] ? match[5].split('.') : [],
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
function comparePrereleaseIdentifier(left, right) {
|
|
467
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
468
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
469
|
+
if (leftNumeric && rightNumeric) {
|
|
470
|
+
return Number(left) - Number(right);
|
|
471
|
+
}
|
|
472
|
+
if (leftNumeric)
|
|
473
|
+
return -1;
|
|
474
|
+
if (rightNumeric)
|
|
475
|
+
return 1;
|
|
476
|
+
if (left < right)
|
|
477
|
+
return -1;
|
|
478
|
+
if (left > right)
|
|
479
|
+
return 1;
|
|
480
|
+
return 0;
|
|
481
|
+
}
|
|
482
|
+
function compareStrictSemVer(left, right) {
|
|
483
|
+
const leftVersion = parseStrictSemVer(left);
|
|
484
|
+
const rightVersion = parseStrictSemVer(right);
|
|
485
|
+
if (!leftVersion || !rightVersion) {
|
|
486
|
+
throw new Error('compareStrictSemVer requires strict SemVer inputs.');
|
|
487
|
+
}
|
|
488
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
489
|
+
if (leftVersion[key] !== rightVersion[key]) {
|
|
490
|
+
return leftVersion[key] - rightVersion[key];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const leftPrerelease = leftVersion.prerelease;
|
|
494
|
+
const rightPrerelease = rightVersion.prerelease;
|
|
495
|
+
if (leftPrerelease.length === 0 && rightPrerelease.length === 0) {
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
if (leftPrerelease.length === 0) {
|
|
499
|
+
return 1;
|
|
500
|
+
}
|
|
501
|
+
if (rightPrerelease.length === 0) {
|
|
502
|
+
return -1;
|
|
503
|
+
}
|
|
504
|
+
const maxLength = Math.max(leftPrerelease.length, rightPrerelease.length);
|
|
505
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
506
|
+
const leftIdentifier = leftPrerelease[index];
|
|
507
|
+
const rightIdentifier = rightPrerelease[index];
|
|
508
|
+
if (leftIdentifier === undefined)
|
|
509
|
+
return -1;
|
|
510
|
+
if (rightIdentifier === undefined)
|
|
511
|
+
return 1;
|
|
512
|
+
const result = comparePrereleaseIdentifier(leftIdentifier, rightIdentifier);
|
|
513
|
+
if (result !== 0) {
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return 0;
|
|
518
|
+
}
|
|
519
|
+
function validateStrictSemVerString(value, fieldName) {
|
|
520
|
+
if (value === undefined) {
|
|
521
|
+
error(`Invalid ${fieldName}: expected a strict SemVer 2.0.0 string. Received undefined.`);
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
if (typeof value !== 'string' || !STRICT_SEMVER_PATTERN.test(value)) {
|
|
525
|
+
error(`Invalid ${fieldName}: expected a strict SemVer 2.0.0 string. Received ${JSON.stringify(value)}.`);
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
function validateManifestLightweight(manifest) {
|
|
531
|
+
const input = expectRecord(manifest, 'plugin manifest');
|
|
532
|
+
if (!input)
|
|
533
|
+
return;
|
|
534
|
+
if (Object.prototype.hasOwnProperty.call(input, 'commands')) {
|
|
535
|
+
error('top-level "commands" has been removed; use "slashCommands" instead');
|
|
536
|
+
}
|
|
537
|
+
validateAllowedKeys(input, 'plugin manifest', [
|
|
538
|
+
'name',
|
|
539
|
+
'version',
|
|
540
|
+
'description',
|
|
541
|
+
'dependencies',
|
|
542
|
+
'compatibility',
|
|
543
|
+
'skills',
|
|
544
|
+
'slashCommands',
|
|
545
|
+
'mcpServers',
|
|
546
|
+
'apps',
|
|
547
|
+
'distributions',
|
|
548
|
+
'marketplace',
|
|
549
|
+
'interface',
|
|
550
|
+
'extensions',
|
|
551
|
+
'buildConfig',
|
|
552
|
+
'scripts',
|
|
553
|
+
'config',
|
|
554
|
+
'artifacts',
|
|
555
|
+
]);
|
|
556
|
+
if (manifest.dependencies !== undefined) {
|
|
557
|
+
if (!Array.isArray(manifest.dependencies)) {
|
|
558
|
+
error('dependencies must be an array');
|
|
559
|
+
}
|
|
560
|
+
else if (manifest.dependencies.length > 64) {
|
|
561
|
+
error('dependencies must contain at most 64 direct plugin dependencies');
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
const seen = new Set();
|
|
565
|
+
for (const [index, dependency] of manifest.dependencies.entries()) {
|
|
566
|
+
if (typeof dependency !== 'string' || !PLUGIN_NAME_PATTERN.test(dependency)) {
|
|
567
|
+
error(`dependencies[${index}] must be an ASCII lowercase hyphen-case plugin name`);
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (dependency === manifest.name) {
|
|
571
|
+
error(`plugin "${manifest.name}" cannot depend on itself`);
|
|
572
|
+
}
|
|
573
|
+
if (seen.has(dependency)) {
|
|
574
|
+
error(`dependencies contains duplicate plugin "${dependency}"`);
|
|
575
|
+
}
|
|
576
|
+
seen.add(dependency);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
for (const field of ['name', 'version', 'description', 'marketplace']) {
|
|
581
|
+
if (!manifest[field]) {
|
|
582
|
+
error(`Missing required field: ${field}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (manifest.name && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(manifest.name)) {
|
|
586
|
+
error(`manifest name "${manifest.name}" is not lowercase-hyphen-case`);
|
|
587
|
+
}
|
|
588
|
+
if (manifest.version) {
|
|
589
|
+
const pluginVersion = parseStrictSemVer(manifest.version);
|
|
590
|
+
if (!pluginVersion) {
|
|
591
|
+
error(`manifest.version must be a strict SemVer 2.0.0 string, got "${manifest.version}"`);
|
|
592
|
+
}
|
|
593
|
+
else if (pluginVersion.prerelease.length > 0 || pluginVersion.build.length > 0) {
|
|
594
|
+
error('manifest.version must be a stable SemVer without prerelease or build metadata');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
if (manifest.marketplace) {
|
|
598
|
+
const mp = manifest.marketplace;
|
|
599
|
+
if (!mp.category)
|
|
600
|
+
error('marketplace.category is required');
|
|
601
|
+
if (typeof mp.strict !== 'boolean')
|
|
602
|
+
error('marketplace.strict must be a boolean');
|
|
603
|
+
if (!mp.policy || !mp.policy.installation)
|
|
604
|
+
error('marketplace.policy.installation is required');
|
|
605
|
+
else if (!MARKETPLACE_INSTALLATION_POLICIES.has(mp.policy.installation))
|
|
606
|
+
error(`marketplace.policy.installation must be one of ${[...MARKETPLACE_INSTALLATION_POLICIES].map((value) => JSON.stringify(value)).join(', ')}`);
|
|
607
|
+
if (!mp.policy || !mp.policy.authentication)
|
|
608
|
+
error('marketplace.policy.authentication is required');
|
|
609
|
+
else if (!MARKETPLACE_AUTHENTICATION_POLICIES.has(mp.policy.authentication))
|
|
610
|
+
error('marketplace.policy.authentication must be "ON_INSTALL" or "ON_USE"');
|
|
611
|
+
validateManifestPolicyProducts(mp.policy?.products);
|
|
612
|
+
}
|
|
613
|
+
for (const field of ['skills', 'slashCommands', 'mcpServers', 'apps']) {
|
|
614
|
+
if (manifest[field] && !manifest[field].startsWith('./')) {
|
|
615
|
+
error(`${field} path must start with "./", got "${manifest[field]}"`);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const claudeCommands = manifest.buildConfig?.manifestOverrides?.claude?.commands;
|
|
619
|
+
if (claudeCommands !== undefined) {
|
|
620
|
+
error('buildConfig.manifestOverrides.claude.commands has been removed; use "slashCommands" instead');
|
|
621
|
+
}
|
|
622
|
+
for (const platform of ['claude', 'codex']) {
|
|
623
|
+
const rules = manifest.buildConfig?.fileRules?.[platform];
|
|
624
|
+
if (!rules)
|
|
625
|
+
continue;
|
|
626
|
+
for (const pattern of [...(rules.include ?? []), ...(rules.exclude ?? [])]) {
|
|
627
|
+
const normalized = String(pattern).replace(/\\/g, '/').replace(/^\.\//, '');
|
|
628
|
+
if (normalized === 'commands' || normalized.startsWith('commands/')) {
|
|
629
|
+
error(`buildConfig.fileRules.${platform} no longer accepts legacy "commands/**" source rules; use "slashCommands" instead`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
validateExtensionsLightweight(manifest.extensions);
|
|
634
|
+
validatePlatformFieldSupport(manifest);
|
|
635
|
+
}
|
|
636
|
+
function validatePortableNativeArtifactCapabilityLightweight(manifest) {
|
|
637
|
+
if (manifest.distributions?.agentSkills === undefined)
|
|
638
|
+
return;
|
|
639
|
+
const hasOtherNativeDeclaration = Boolean(manifest.skills
|
|
640
|
+
|| manifest.slashCommands
|
|
641
|
+
|| manifest.mcpServers
|
|
642
|
+
|| manifest.apps
|
|
643
|
+
|| (manifest.extensions?.instructions?.length ?? 0) > 0
|
|
644
|
+
|| (manifest.extensions?.permissions?.length ?? 0) > 0
|
|
645
|
+
|| (manifest.extensions?.hooks?.length ?? 0) > 0
|
|
646
|
+
|| (manifest.extensions?.nativeHooks?.claude?.length ?? 0) > 0
|
|
647
|
+
|| (manifest.extensions?.nativeHooks?.codex?.length ?? 0) > 0
|
|
648
|
+
|| (manifest.extensions?.agents?.length ?? 0) > 0);
|
|
649
|
+
const hasPresentNativeArtifact = (manifest.artifacts?.targets ?? []).some((target) => (target?.platform === 'claude' || target?.platform === 'codex')
|
|
650
|
+
&& Array.isArray(target.states)
|
|
651
|
+
&& target.states.includes('present'));
|
|
652
|
+
if (!hasOtherNativeDeclaration && !hasPresentNativeArtifact) {
|
|
653
|
+
const message = 'Invalid plugin manifest: portable-only plugins are not supported in V1. Declare at least one non-empty native capability.';
|
|
654
|
+
if (!errors.includes(message))
|
|
655
|
+
error(message);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function normalizeManifestProductToken(value) {
|
|
659
|
+
return String(value).trim().toUpperCase().replace(/[\s-]+/g, '_');
|
|
660
|
+
}
|
|
661
|
+
function resolveSupportedPlatforms(manifest) {
|
|
662
|
+
const products = manifest.marketplace?.policy?.products;
|
|
663
|
+
if (!Array.isArray(products) || products.length === 0) {
|
|
664
|
+
return new Set(['claude', 'codex']);
|
|
665
|
+
}
|
|
666
|
+
const supported = new Set();
|
|
667
|
+
for (const product of products) {
|
|
668
|
+
const normalized = normalizeManifestProductToken(product);
|
|
669
|
+
if (normalized === 'CLAUDE')
|
|
670
|
+
supported.add('claude');
|
|
671
|
+
if (normalized === 'CODEX')
|
|
672
|
+
supported.add('codex');
|
|
673
|
+
}
|
|
674
|
+
return supported;
|
|
675
|
+
}
|
|
676
|
+
function supportsManifestPlatform(manifest, platform) {
|
|
677
|
+
return resolveSupportedPlatforms(manifest).has(platform);
|
|
678
|
+
}
|
|
679
|
+
function formatManifestProductsForError(manifest) {
|
|
680
|
+
const products = manifest.marketplace?.policy?.products;
|
|
681
|
+
if (!Array.isArray(products) || products.length === 0) {
|
|
682
|
+
return 'marketplace.policy.products=[]';
|
|
683
|
+
}
|
|
684
|
+
const normalized = [...new Set(products.map(normalizeManifestProductToken))];
|
|
685
|
+
return `marketplace.policy.products=[${normalized.join(', ')}]`;
|
|
686
|
+
}
|
|
687
|
+
function platformProductName(platform) {
|
|
688
|
+
return platform === 'claude' ? 'CLAUDE' : 'CODEX';
|
|
689
|
+
}
|
|
690
|
+
function pluginNameForError(manifest) {
|
|
691
|
+
return typeof manifest.name === 'string' && manifest.name.trim()
|
|
692
|
+
? manifest.name.trim()
|
|
693
|
+
: '<unknown>';
|
|
694
|
+
}
|
|
695
|
+
function assertPlatformFieldSupported(manifest, fieldPath, requiredPlatform) {
|
|
696
|
+
if (supportsManifestPlatform(manifest, requiredPlatform))
|
|
697
|
+
return;
|
|
698
|
+
error(`Invalid plugin "${pluginNameForError(manifest)}": ${fieldPath} requires ${platformProductName(requiredPlatform)} support, but ${formatManifestProductsForError(manifest)}.`);
|
|
699
|
+
}
|
|
700
|
+
function hasNonEmptyOverride(value) {
|
|
701
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
702
|
+
}
|
|
703
|
+
function validateManifestPolicyProducts(products) {
|
|
704
|
+
if (products === undefined)
|
|
705
|
+
return;
|
|
706
|
+
if (!Array.isArray(products)) {
|
|
707
|
+
error('marketplace.policy.products must be an array');
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
for (const [index, product] of products.entries()) {
|
|
711
|
+
const fieldName = `marketplace.policy.products[${index}]`;
|
|
712
|
+
if (typeof product !== 'string' || product.trim() === '') {
|
|
713
|
+
error(`${fieldName} must be a non-empty string`);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const normalized = normalizeManifestProductToken(product);
|
|
717
|
+
if (!MANIFEST_POLICY_PRODUCTS.has(normalized)) {
|
|
718
|
+
error(`${fieldName} must be one of "CODEX" or "CLAUDE"`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function validatePlatformFieldSupport(manifest) {
|
|
723
|
+
if (manifest.buildConfig?.fileRules?.claude !== undefined) {
|
|
724
|
+
assertPlatformFieldSupported(manifest, 'buildConfig.fileRules.claude', 'claude');
|
|
725
|
+
}
|
|
726
|
+
if (manifest.buildConfig?.fileRules?.codex !== undefined) {
|
|
727
|
+
assertPlatformFieldSupported(manifest, 'buildConfig.fileRules.codex', 'codex');
|
|
728
|
+
}
|
|
729
|
+
if (manifest.buildConfig?.manifestOverrides?.claude !== undefined) {
|
|
730
|
+
assertPlatformFieldSupported(manifest, 'buildConfig.manifestOverrides.claude', 'claude');
|
|
731
|
+
}
|
|
732
|
+
if (manifest.buildConfig?.manifestOverrides?.codex !== undefined) {
|
|
733
|
+
assertPlatformFieldSupported(manifest, 'buildConfig.manifestOverrides.codex', 'codex');
|
|
734
|
+
}
|
|
735
|
+
if (manifest.apps) {
|
|
736
|
+
assertPlatformFieldSupported(manifest, 'apps', 'codex');
|
|
737
|
+
}
|
|
738
|
+
if (Array.isArray(manifest.extensions?.nativeHooks?.claude) && manifest.extensions.nativeHooks.claude.length > 0) {
|
|
739
|
+
assertPlatformFieldSupported(manifest, 'extensions.nativeHooks.claude', 'claude');
|
|
740
|
+
}
|
|
741
|
+
if (Array.isArray(manifest.extensions?.nativeHooks?.codex) && manifest.extensions.nativeHooks.codex.length > 0) {
|
|
742
|
+
assertPlatformFieldSupported(manifest, 'extensions.nativeHooks.codex', 'codex');
|
|
743
|
+
}
|
|
744
|
+
for (const [fieldName, entries] of [
|
|
745
|
+
['extensions.instructions', manifest.extensions?.instructions],
|
|
746
|
+
['extensions.permissions', manifest.extensions?.permissions],
|
|
747
|
+
['extensions.hooks', manifest.extensions?.hooks],
|
|
748
|
+
['extensions.agents', manifest.extensions?.agents],
|
|
749
|
+
]) {
|
|
750
|
+
if (!Array.isArray(entries))
|
|
751
|
+
continue;
|
|
752
|
+
entries.forEach((entry, index) => {
|
|
753
|
+
if (!isRecord(entry.agentOverrides))
|
|
754
|
+
return;
|
|
755
|
+
for (const platform of ['claude', 'codex']) {
|
|
756
|
+
if (hasNonEmptyOverride(entry.agentOverrides[platform])) {
|
|
757
|
+
assertPlatformFieldSupported(manifest, `${fieldName}[${index}].agentOverrides.${platform}`, platform);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
if (!Array.isArray(manifest.artifacts?.targets))
|
|
763
|
+
return;
|
|
764
|
+
manifest.artifacts.targets.forEach((target, index) => {
|
|
765
|
+
if (target?.platform === 'claude' || target?.platform === 'codex') {
|
|
766
|
+
assertPlatformFieldSupported(manifest, `artifacts.targets[${index}].platform`, target.platform);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function isTemplateAsset(manifest, relativePath) {
|
|
771
|
+
return templateIncludesPath(manifest, relativePath);
|
|
772
|
+
}
|
|
773
|
+
function isRecord(value) {
|
|
774
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
775
|
+
}
|
|
776
|
+
function validateAllowedKeys(record, fieldName, allowedKeys) {
|
|
777
|
+
for (const key of Object.keys(record)) {
|
|
778
|
+
if (!allowedKeys.includes(key)) {
|
|
779
|
+
error(`${fieldName} has unknown field "${key}"`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function validateManifestString(value, fieldName) {
|
|
784
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
785
|
+
error(`${fieldName} must be a non-empty string`);
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
return value;
|
|
789
|
+
}
|
|
790
|
+
function validatePublicHookEvent(value, fieldName) {
|
|
791
|
+
const event = validateManifestString(value, fieldName);
|
|
792
|
+
if (!event)
|
|
793
|
+
return null;
|
|
794
|
+
if (event === 'session_stop') {
|
|
795
|
+
error(`${fieldName}: public event "session_stop" has been removed; use extensions.nativeHooks.claude[].event = "Stop" or "SessionEnd", or extensions.nativeHooks.codex[].event = "Stop"`);
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
if (!PUBLIC_HOOK_EVENTS.has(event)) {
|
|
799
|
+
error(`${fieldName}: unsupported public hook event "${event}"`);
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
return event;
|
|
803
|
+
}
|
|
804
|
+
function normalizePublicSessionStartMatcher(matcher, fieldName) {
|
|
805
|
+
if (matcher === undefined)
|
|
806
|
+
return PUBLIC_SESSION_START_DEFAULT_MATCHER;
|
|
807
|
+
if (typeof matcher !== 'string' || matcher.trim() === '') {
|
|
808
|
+
error(`${fieldName} must be a non-empty string`);
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
const selected = new Set();
|
|
812
|
+
for (const part of matcher.split('|')) {
|
|
813
|
+
if (!PUBLIC_SESSION_START_SOURCES.has(part)) {
|
|
814
|
+
error(`${fieldName}: public session_start only allows startup, resume, clear, or their "|" union`);
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
selected.add(part);
|
|
818
|
+
}
|
|
819
|
+
return ['startup', 'resume', 'clear'].filter((part) => selected.has(part)).join('|');
|
|
820
|
+
}
|
|
821
|
+
function validatePublicHookMatcher(event, matcher, fieldName) {
|
|
822
|
+
if (matcher === undefined)
|
|
823
|
+
return event === 'session_start' ? PUBLIC_SESSION_START_DEFAULT_MATCHER : undefined;
|
|
824
|
+
if (typeof matcher !== 'string' || matcher.trim() === '') {
|
|
825
|
+
error(`${fieldName} must be a non-empty string`);
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
if (event === 'session_start') {
|
|
829
|
+
return normalizePublicSessionStartMatcher(matcher, fieldName);
|
|
830
|
+
}
|
|
831
|
+
return matcher;
|
|
832
|
+
}
|
|
833
|
+
function validateNativeHookEvent(value, platform, fieldName) {
|
|
834
|
+
const event = validateManifestString(value, fieldName);
|
|
835
|
+
if (!event)
|
|
836
|
+
return null;
|
|
837
|
+
if (!NATIVE_HOOK_EVENTS[platform].has(event)) {
|
|
838
|
+
error(`${fieldName}: unsupported ${platform} hook event "${event}"`);
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
return event;
|
|
842
|
+
}
|
|
843
|
+
function validateNativeHookMatcher(event, platform, matcher, fieldName) {
|
|
844
|
+
if (matcher === undefined)
|
|
845
|
+
return undefined;
|
|
846
|
+
if (typeof matcher !== 'string' || matcher.trim() === '') {
|
|
847
|
+
error(`${fieldName} must be a non-empty string`);
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
if (MATCHERLESS_NATIVE_HOOK_EVENTS[platform].has(event)) {
|
|
851
|
+
error(`${fieldName}: ${platform} event "${event}" does not allow matcher`);
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
return matcher;
|
|
855
|
+
}
|
|
856
|
+
function validateHookAgentOverrides(value, baseHook, fieldName, duplicateCollector) {
|
|
857
|
+
if (value === undefined) {
|
|
858
|
+
for (const platform of ['claude', 'codex']) {
|
|
859
|
+
duplicateCollector(platform, baseHook);
|
|
860
|
+
}
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const overrides = expectRecord(value, fieldName);
|
|
864
|
+
if (!overrides)
|
|
865
|
+
return;
|
|
866
|
+
validateAllowedKeys(overrides, fieldName, ['claude', 'codex']);
|
|
867
|
+
for (const platform of ['claude', 'codex']) {
|
|
868
|
+
const override = overrides[platform];
|
|
869
|
+
if (override === null)
|
|
870
|
+
continue;
|
|
871
|
+
if (override === undefined) {
|
|
872
|
+
duplicateCollector(platform, baseHook);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const overrideRecord = expectRecord(override, `${fieldName}.${platform}`);
|
|
876
|
+
if (!overrideRecord)
|
|
877
|
+
continue;
|
|
878
|
+
validateAllowedKeys(overrideRecord, `${fieldName}.${platform}`, ['event', 'matcher', 'command']);
|
|
879
|
+
const event = overrideRecord.event === undefined
|
|
880
|
+
? baseHook.event
|
|
881
|
+
: validatePublicHookEvent(overrideRecord.event, `${fieldName}.${platform}.event`);
|
|
882
|
+
const matcherValue = overrideRecord.matcher === undefined
|
|
883
|
+
? baseHook.matcher
|
|
884
|
+
: overrideRecord.matcher;
|
|
885
|
+
const matcher = event
|
|
886
|
+
? validatePublicHookMatcher(event, matcherValue, `${fieldName}.${platform}.matcher`)
|
|
887
|
+
: null;
|
|
888
|
+
const command = overrideRecord.command === undefined
|
|
889
|
+
? baseHook.command
|
|
890
|
+
: validateManifestString(overrideRecord.command, `${fieldName}.${platform}.command`);
|
|
891
|
+
if (event && matcher !== null && command) {
|
|
892
|
+
duplicateCollector(platform, { event, matcher, command });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
function validateHookEntryLightweight(entry, fieldName, duplicateCollector) {
|
|
897
|
+
const input = expectRecord(entry, fieldName);
|
|
898
|
+
if (!input)
|
|
899
|
+
return;
|
|
900
|
+
validateAllowedKeys(input, fieldName, ['event', 'matcher', 'command', 'agentOverrides']);
|
|
901
|
+
const event = validatePublicHookEvent(input.event, `${fieldName}.event`);
|
|
902
|
+
const matcher = event
|
|
903
|
+
? validatePublicHookMatcher(event, input.matcher, `${fieldName}.matcher`)
|
|
904
|
+
: null;
|
|
905
|
+
const command = validateManifestString(input.command, `${fieldName}.command`);
|
|
906
|
+
if (!event || matcher === null || !command)
|
|
907
|
+
return;
|
|
908
|
+
validateHookAgentOverrides(input.agentOverrides, { event, matcher, command }, `${fieldName}.agentOverrides`, duplicateCollector);
|
|
909
|
+
}
|
|
910
|
+
function validateNativeHookEntryLightweight(entry, platform, fieldName, duplicateCollector) {
|
|
911
|
+
const input = expectRecord(entry, fieldName);
|
|
912
|
+
if (!input)
|
|
913
|
+
return;
|
|
914
|
+
validateAllowedKeys(input, fieldName, ['event', 'matcher', 'command']);
|
|
915
|
+
const event = validateNativeHookEvent(input.event, platform, `${fieldName}.event`);
|
|
916
|
+
const matcher = event
|
|
917
|
+
? validateNativeHookMatcher(event, platform, input.matcher, `${fieldName}.matcher`)
|
|
918
|
+
: null;
|
|
919
|
+
const command = validateManifestString(input.command, `${fieldName}.command`);
|
|
920
|
+
if (!event || matcher === null || !command)
|
|
921
|
+
return;
|
|
922
|
+
duplicateCollector(platform, { event, matcher, command, native: true });
|
|
923
|
+
}
|
|
924
|
+
function validateExtensionsLightweight(extensions) {
|
|
925
|
+
if (extensions === undefined)
|
|
926
|
+
return;
|
|
927
|
+
const input = expectRecord(extensions, 'extensions');
|
|
928
|
+
if (!input)
|
|
929
|
+
return;
|
|
930
|
+
validateAllowedKeys(input, 'extensions', ['instructions', 'permissions', 'hooks', 'nativeHooks', 'agents']);
|
|
931
|
+
const seenHooks = {
|
|
932
|
+
claude: new Set(),
|
|
933
|
+
codex: new Set(),
|
|
934
|
+
};
|
|
935
|
+
const collectHook = (platform, hook) => {
|
|
936
|
+
const event = hook.native ? hook.event : PUBLIC_HOOK_RENDER_MAP[platform][hook.event];
|
|
937
|
+
const matcher = hook.matcher === '*' ? undefined : hook.matcher;
|
|
938
|
+
const key = JSON.stringify([event, matcher ?? null, hook.command]);
|
|
939
|
+
if (seenHooks[platform].has(key)) {
|
|
940
|
+
error(`extensions has duplicate ${platform} hook for event "${event}" and command "${hook.command}"`);
|
|
941
|
+
}
|
|
942
|
+
seenHooks[platform].add(key);
|
|
943
|
+
};
|
|
944
|
+
if (input.hooks !== undefined) {
|
|
945
|
+
if (!Array.isArray(input.hooks)) {
|
|
946
|
+
error('extensions.hooks must be an array');
|
|
947
|
+
}
|
|
948
|
+
else {
|
|
949
|
+
input.hooks.forEach((entry, index) => validateHookEntryLightweight(entry, `extensions.hooks[${index}]`, collectHook));
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (input.nativeHooks !== undefined) {
|
|
953
|
+
const nativeHooks = expectRecord(input.nativeHooks, 'extensions.nativeHooks');
|
|
954
|
+
if (nativeHooks) {
|
|
955
|
+
validateAllowedKeys(nativeHooks, 'extensions.nativeHooks', ['claude', 'codex']);
|
|
956
|
+
for (const platform of ['claude', 'codex']) {
|
|
957
|
+
if (nativeHooks[platform] === undefined)
|
|
958
|
+
continue;
|
|
959
|
+
if (!Array.isArray(nativeHooks[platform])) {
|
|
960
|
+
error(`extensions.nativeHooks.${platform} must be an array`);
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
nativeHooks[platform].forEach((entry, index) => validateNativeHookEntryLightweight(entry, platform, `extensions.nativeHooks.${platform}[${index}]`, collectHook));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function expectRecord(value, fieldName) {
|
|
969
|
+
if (!isRecord(value)) {
|
|
970
|
+
error(`${fieldName} must be an object`);
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
return value;
|
|
974
|
+
}
|
|
975
|
+
function validateNonEmptyString(value, fieldName, maxLength) {
|
|
976
|
+
if (typeof value !== 'string') {
|
|
977
|
+
error(`${fieldName} must be a string`);
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
const trimmed = value.trim();
|
|
981
|
+
if (trimmed.length === 0) {
|
|
982
|
+
error(`${fieldName} must not be empty`);
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
if (maxLength !== undefined && countCharacters(trimmed) > maxLength) {
|
|
986
|
+
error(`${fieldName} must be <= ${maxLength} characters`);
|
|
987
|
+
}
|
|
988
|
+
return trimmed;
|
|
989
|
+
}
|
|
990
|
+
function isPathInsideOrEqual(root, candidate) {
|
|
991
|
+
const relative = path.relative(root, candidate);
|
|
992
|
+
return relative === '' || (relative !== '..' &&
|
|
993
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
994
|
+
!path.isAbsolute(relative));
|
|
995
|
+
}
|
|
996
|
+
function validateRealPathBoundary(boundary, candidate, fieldName, boundaryName) {
|
|
997
|
+
const canonicalBoundary = fs.realpathSync(boundary);
|
|
998
|
+
const canonicalCandidate = fs.realpathSync(candidate);
|
|
999
|
+
if (!isPathInsideOrEqual(canonicalBoundary, canonicalCandidate)) {
|
|
1000
|
+
error(`${fieldName} must stay inside the ${boundaryName}`);
|
|
1001
|
+
return false;
|
|
1002
|
+
}
|
|
1003
|
+
return true;
|
|
1004
|
+
}
|
|
1005
|
+
function validateEvalRelativeFile(evalDir, value, fieldName) {
|
|
1006
|
+
const relativePath = validateNonEmptyString(value, fieldName);
|
|
1007
|
+
if (!relativePath)
|
|
1008
|
+
return null;
|
|
1009
|
+
const normalized = stripDotSlash(relativePath).split('\\').join('/');
|
|
1010
|
+
if (path.isAbsolute(normalized) || normalized.split('/').includes('..')) {
|
|
1011
|
+
error(`${fieldName} must stay inside the Skill eval directory`);
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
const absolutePath = path.resolve(evalDir, normalized);
|
|
1015
|
+
const relativeToRoot = path.relative(evalDir, absolutePath);
|
|
1016
|
+
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
|
|
1017
|
+
error(`${fieldName} must stay inside the Skill eval directory`);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
|
|
1021
|
+
error(`${fieldName} does not exist: ${normalized}`);
|
|
1022
|
+
return null;
|
|
1023
|
+
}
|
|
1024
|
+
if (!validateRealPathBoundary(evalDir, absolutePath, fieldName, 'Skill eval directory')) {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
return absolutePath;
|
|
1028
|
+
}
|
|
1029
|
+
function validateEvalWorkspacePath(value, fieldName, options = {}) {
|
|
1030
|
+
const raw = validateNonEmptyString(value, fieldName);
|
|
1031
|
+
if (!raw)
|
|
1032
|
+
return null;
|
|
1033
|
+
const treePattern = options.allowTreePattern === true && raw.endsWith('/**');
|
|
1034
|
+
const candidate = treePattern ? raw.slice(0, -3) : raw;
|
|
1035
|
+
if (candidate === '' ||
|
|
1036
|
+
candidate.includes('\0') ||
|
|
1037
|
+
candidate.includes('\\') ||
|
|
1038
|
+
candidate.includes('*') ||
|
|
1039
|
+
candidate.includes('?') ||
|
|
1040
|
+
candidate.includes('[') ||
|
|
1041
|
+
candidate.includes(']') ||
|
|
1042
|
+
path.isAbsolute(candidate) ||
|
|
1043
|
+
path.posix.normalize(candidate) !== candidate ||
|
|
1044
|
+
candidate.split('/').includes('..')) {
|
|
1045
|
+
error(`${fieldName} must be a normalized POSIX path inside the eval workspace`);
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
const firstSegment = candidate.split('/')[0];
|
|
1049
|
+
if (firstSegment === '.git' || firstSegment === '.agents' || firstSegment === '.codex') {
|
|
1050
|
+
error(`${fieldName} must not target an eval control directory`);
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
return raw;
|
|
1054
|
+
}
|
|
1055
|
+
function evalWorkspacePathMatchesPattern(relativePath, pattern) {
|
|
1056
|
+
if (pattern.endsWith('/**')) {
|
|
1057
|
+
const prefix = pattern.slice(0, -3);
|
|
1058
|
+
return relativePath === prefix || relativePath.startsWith(`${prefix}/`);
|
|
1059
|
+
}
|
|
1060
|
+
return relativePath === pattern;
|
|
1061
|
+
}
|
|
1062
|
+
function validateConversationCaseCorpus(evalDir, suite, fieldName) {
|
|
1063
|
+
const casesPath = validateEvalRelativeFile(evalDir, suite.cases, `${fieldName}.cases`);
|
|
1064
|
+
if (!casesPath)
|
|
1065
|
+
return;
|
|
1066
|
+
let document;
|
|
1067
|
+
try {
|
|
1068
|
+
document = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
|
|
1069
|
+
}
|
|
1070
|
+
catch (e) {
|
|
1071
|
+
error(`${fieldName}.cases is not valid JSON: ${e.message}`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const corpus = expectRecord(document, `${fieldName}.cases document`);
|
|
1075
|
+
if (!corpus)
|
|
1076
|
+
return;
|
|
1077
|
+
const skillName = validateNonEmptyString(corpus.skill_name, `${fieldName}.cases.skill_name`);
|
|
1078
|
+
if (skillName && skillName !== suite.skill) {
|
|
1079
|
+
error(`${fieldName}.cases.skill_name must match ${JSON.stringify(suite.skill)}`);
|
|
1080
|
+
}
|
|
1081
|
+
if (!Array.isArray(corpus.evals) || corpus.evals.length === 0) {
|
|
1082
|
+
error(`${fieldName}.cases.evals must be a non-empty array`);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const ids = new Set();
|
|
1086
|
+
corpus.evals.forEach((value, caseIndex) => {
|
|
1087
|
+
const caseField = `${fieldName}.cases.evals[${caseIndex}]`;
|
|
1088
|
+
const entry = expectRecord(value, caseField);
|
|
1089
|
+
if (!entry)
|
|
1090
|
+
return;
|
|
1091
|
+
const id = validateNonEmptyString(entry.id, `${caseField}.id`);
|
|
1092
|
+
if (id && ids.has(id)) {
|
|
1093
|
+
error(`${fieldName}.cases has duplicate case id ${JSON.stringify(id)}`);
|
|
1094
|
+
}
|
|
1095
|
+
else if (id) {
|
|
1096
|
+
ids.add(id);
|
|
1097
|
+
}
|
|
1098
|
+
validateNonEmptyString(entry.prompt, `${caseField}.prompt`);
|
|
1099
|
+
const sandbox = entry.sandbox === undefined
|
|
1100
|
+
? 'read-only'
|
|
1101
|
+
: validateNonEmptyString(entry.sandbox, `${caseField}.sandbox`);
|
|
1102
|
+
if (sandbox && sandbox !== 'read-only' && sandbox !== 'workspace-write') {
|
|
1103
|
+
error(`${caseField}.sandbox must be "read-only" or "workspace-write"`);
|
|
1104
|
+
}
|
|
1105
|
+
if (!Array.isArray(entry.scripted_replies)) {
|
|
1106
|
+
error(`${caseField}.scripted_replies must be an array`);
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
entry.scripted_replies.forEach((reply, replyIndex) => {
|
|
1110
|
+
const replyField = `${caseField}.scripted_replies[${replyIndex}]`;
|
|
1111
|
+
if (typeof reply === 'string')
|
|
1112
|
+
return;
|
|
1113
|
+
const replyEntry = expectRecord(reply, replyField);
|
|
1114
|
+
if (!replyEntry)
|
|
1115
|
+
return;
|
|
1116
|
+
if (!Array.isArray(replyEntry.answers_by_index) || replyEntry.answers_by_index.some((answer) => typeof answer !== 'string')) {
|
|
1117
|
+
error(`${replyField}.answers_by_index must be an array of strings`);
|
|
1118
|
+
}
|
|
1119
|
+
if (replyEntry.expected_question_count !== undefined &&
|
|
1120
|
+
(!Number.isInteger(replyEntry.expected_question_count) ||
|
|
1121
|
+
replyEntry.expected_question_count < 0)) {
|
|
1122
|
+
error(`${replyField}.expected_question_count must be a non-negative integer`);
|
|
1123
|
+
}
|
|
1124
|
+
validateNonEmptyString(replyEntry.unexpected_question_answer, `${replyField}.unexpected_question_answer`);
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
validateNonEmptyString(entry.expected_output, `${caseField}.expected_output`);
|
|
1128
|
+
if (entry.workspace_files !== undefined) {
|
|
1129
|
+
const workspaceFiles = expectRecord(entry.workspace_files, `${caseField}.workspace_files`);
|
|
1130
|
+
if (workspaceFiles) {
|
|
1131
|
+
const workspaceFileEntries = Object.entries(workspaceFiles);
|
|
1132
|
+
if (workspaceFileEntries.length === 0) {
|
|
1133
|
+
error(`${caseField}.workspace_files must contain at least one file`);
|
|
1134
|
+
}
|
|
1135
|
+
workspaceFileEntries.forEach(([relativePath, content]) => {
|
|
1136
|
+
validateEvalWorkspacePath(relativePath, `${caseField}.workspace_files.${relativePath}`, { allowTreePattern: false });
|
|
1137
|
+
validateNonEmptyString(content, `${caseField}.workspace_files.${relativePath}`);
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const review = expectRecord(entry.review, `${caseField}.review`);
|
|
1142
|
+
if (review) {
|
|
1143
|
+
if (!Array.isArray(review.checks) || review.checks.length === 0 || review.checks.some((check) => typeof check !== 'string' || check.trim() === '')) {
|
|
1144
|
+
error(`${caseField}.review.checks must be a non-empty array of strings`);
|
|
1145
|
+
}
|
|
1146
|
+
if (review.workspace_unchanged !== undefined &&
|
|
1147
|
+
typeof review.workspace_unchanged !== 'boolean') {
|
|
1148
|
+
error(`${caseField}.review.workspace_unchanged must be boolean`);
|
|
1149
|
+
}
|
|
1150
|
+
const allowedWorkspaceChanges = [];
|
|
1151
|
+
if (review.allowed_workspace_changes !== undefined) {
|
|
1152
|
+
if (!Array.isArray(review.allowed_workspace_changes) ||
|
|
1153
|
+
review.allowed_workspace_changes.length === 0) {
|
|
1154
|
+
error(`${caseField}.review.allowed_workspace_changes must be a non-empty array`);
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
review.allowed_workspace_changes.forEach((pathValue, pathIndex) => {
|
|
1158
|
+
const normalized = validateEvalWorkspacePath(pathValue, `${caseField}.review.allowed_workspace_changes[${pathIndex}]`, { allowTreePattern: true });
|
|
1159
|
+
if (normalized)
|
|
1160
|
+
allowedWorkspaceChanges.push(normalized);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
const artifactPaths = [];
|
|
1165
|
+
if (review.artifacts !== undefined) {
|
|
1166
|
+
if (!Array.isArray(review.artifacts) || review.artifacts.length === 0) {
|
|
1167
|
+
error(`${caseField}.review.artifacts must be a non-empty array`);
|
|
1168
|
+
}
|
|
1169
|
+
else {
|
|
1170
|
+
review.artifacts.forEach((artifactValue, artifactIndex) => {
|
|
1171
|
+
const artifactField = `${caseField}.review.artifacts[${artifactIndex}]`;
|
|
1172
|
+
const artifact = expectRecord(artifactValue, artifactField);
|
|
1173
|
+
if (!artifact)
|
|
1174
|
+
return;
|
|
1175
|
+
validateAllowedKeys(artifact, artifactField, ['path', 'exists', 'contains', 'not_contains', 'json']);
|
|
1176
|
+
const artifactPath = validateEvalWorkspacePath(artifact.path, `${artifactField}.path`);
|
|
1177
|
+
if (artifactPath)
|
|
1178
|
+
artifactPaths.push(artifactPath);
|
|
1179
|
+
if (artifact.exists !== undefined && typeof artifact.exists !== 'boolean') {
|
|
1180
|
+
error(`${artifactField}.exists must be boolean`);
|
|
1181
|
+
}
|
|
1182
|
+
for (const key of ['contains', 'not_contains']) {
|
|
1183
|
+
if (artifact[key] !== undefined &&
|
|
1184
|
+
(!Array.isArray(artifact[key]) || artifact[key].length === 0 || artifact[key].some((fragment) => typeof fragment !== 'string' || fragment.length === 0))) {
|
|
1185
|
+
error(`${artifactField}.${key} must be a non-empty array of strings`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (artifact.json !== undefined && !isRecord(artifact.json)) {
|
|
1189
|
+
error(`${artifactField}.json must be an object`);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (new Set(artifactPaths).size !== artifactPaths.length) {
|
|
1195
|
+
error(`${caseField}.review.artifacts contains duplicate paths`);
|
|
1196
|
+
}
|
|
1197
|
+
if (sandbox === 'workspace-write') {
|
|
1198
|
+
if (review.workspace_unchanged !== false) {
|
|
1199
|
+
error(`${caseField} workspace-write requires review.workspace_unchanged=false`);
|
|
1200
|
+
}
|
|
1201
|
+
if (allowedWorkspaceChanges.length === 0 || artifactPaths.length === 0) {
|
|
1202
|
+
error(`${caseField} workspace-write requires allowed changes and artifacts`);
|
|
1203
|
+
}
|
|
1204
|
+
const unmatchedArtifact = artifactPaths.find((artifactPath) => !allowedWorkspaceChanges.some((pattern) => evalWorkspacePathMatchesPattern(artifactPath, pattern)));
|
|
1205
|
+
if (unmatchedArtifact) {
|
|
1206
|
+
error(`${caseField} artifact ${JSON.stringify(unmatchedArtifact)} is outside allowed changes`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
function validateTriggerCaseCorpus(evalDir, suite, fieldName) {
|
|
1213
|
+
const casesPath = validateEvalRelativeFile(evalDir, suite.cases, `${fieldName}.cases`);
|
|
1214
|
+
if (!casesPath)
|
|
1215
|
+
return;
|
|
1216
|
+
let document;
|
|
1217
|
+
try {
|
|
1218
|
+
document = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
|
|
1219
|
+
}
|
|
1220
|
+
catch (e) {
|
|
1221
|
+
error(`${fieldName}.cases is not valid JSON: ${e.message}`);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const corpus = expectRecord(document, `${fieldName}.cases document`);
|
|
1225
|
+
if (!corpus)
|
|
1226
|
+
return;
|
|
1227
|
+
if (corpus.schemaVersion !== PLUGIN_EVAL_CASE_SCHEMA_VERSION) {
|
|
1228
|
+
error(`${fieldName}.cases.schemaVersion must be ${PLUGIN_EVAL_CASE_SCHEMA_VERSION}`);
|
|
1229
|
+
}
|
|
1230
|
+
if (corpus.skill !== suite.skill) {
|
|
1231
|
+
error(`${fieldName}.cases.skill must match ${JSON.stringify(suite.skill)}`);
|
|
1232
|
+
}
|
|
1233
|
+
if (!Array.isArray(corpus.cases) || corpus.cases.length === 0) {
|
|
1234
|
+
error(`${fieldName}.cases.cases must be a non-empty array`);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const ids = new Set();
|
|
1238
|
+
const seenSplits = new Set();
|
|
1239
|
+
let hasWorkspaceFixture = false;
|
|
1240
|
+
corpus.cases.forEach((value, caseIndex) => {
|
|
1241
|
+
const caseField = `${fieldName}.cases.cases[${caseIndex}]`;
|
|
1242
|
+
const entry = expectRecord(value, caseField);
|
|
1243
|
+
if (!entry)
|
|
1244
|
+
return;
|
|
1245
|
+
const id = validateNonEmptyString(entry.id, `${caseField}.id`);
|
|
1246
|
+
if (id && !/^[a-z0-9][a-z0-9_]*$/.test(id)) {
|
|
1247
|
+
error(`${caseField}.id must use lowercase snake_case`);
|
|
1248
|
+
}
|
|
1249
|
+
else if (id && ids.has(id)) {
|
|
1250
|
+
error(`${fieldName}.cases has duplicate case id ${JSON.stringify(id)}`);
|
|
1251
|
+
}
|
|
1252
|
+
else if (id) {
|
|
1253
|
+
ids.add(id);
|
|
1254
|
+
}
|
|
1255
|
+
if (!PLUGIN_EVAL_TRIGGER_CASE_SPLITS.has(entry.split)) {
|
|
1256
|
+
error(`${caseField}.split must be train, validation, or sanity`);
|
|
1257
|
+
}
|
|
1258
|
+
else {
|
|
1259
|
+
seenSplits.add(entry.split);
|
|
1260
|
+
}
|
|
1261
|
+
validateNonEmptyString(entry.query, `${caseField}.query`);
|
|
1262
|
+
const usesLegacyExpectation = entry.should_load_skill !== undefined;
|
|
1263
|
+
const usesRouteExpectation = entry.expected_loaded_skills !== undefined;
|
|
1264
|
+
if (usesLegacyExpectation === usesRouteExpectation) {
|
|
1265
|
+
error(`${caseField} must declare exactly one of should_load_skill or expected_loaded_skills`);
|
|
1266
|
+
}
|
|
1267
|
+
else if (usesLegacyExpectation && typeof entry.should_load_skill !== 'boolean') {
|
|
1268
|
+
error(`${caseField}.should_load_skill must be boolean`);
|
|
1269
|
+
}
|
|
1270
|
+
else if (usesRouteExpectation) {
|
|
1271
|
+
if (!Array.isArray(entry.expected_loaded_skills)) {
|
|
1272
|
+
error(`${caseField}.expected_loaded_skills must be an array`);
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
const expectedSkills = entry.expected_loaded_skills;
|
|
1276
|
+
expectedSkills.forEach((skillName, skillIndex) => {
|
|
1277
|
+
if (typeof skillName !== 'string' || skillName.trim() === '') {
|
|
1278
|
+
error(`${caseField}.expected_loaded_skills[${skillIndex}] must be a non-empty string`);
|
|
1279
|
+
}
|
|
1280
|
+
else if (!suite.skills.includes(skillName)) {
|
|
1281
|
+
error(`${caseField}.expected_loaded_skills[${skillIndex}] must name a participant Skill`);
|
|
1282
|
+
}
|
|
1283
|
+
});
|
|
1284
|
+
if (new Set(expectedSkills).size !== expectedSkills.length) {
|
|
1285
|
+
error(`${caseField}.expected_loaded_skills must not contain duplicates`);
|
|
1286
|
+
}
|
|
1287
|
+
if (suite.forbidMultipleSkillLoads === true && expectedSkills.length > 1) {
|
|
1288
|
+
error(`${caseField} expects multiple Skills but the suite forbids multiple Skill loads`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
if (suite.observeSpawnAgent === true) {
|
|
1293
|
+
if (typeof entry.should_spawn_agent !== 'boolean') {
|
|
1294
|
+
error(`${caseField}.should_spawn_agent is required when observeSpawnAgent=true`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
else if (entry.should_spawn_agent !== undefined) {
|
|
1298
|
+
error(`${caseField}.should_spawn_agent requires observeSpawnAgent=true`);
|
|
1299
|
+
}
|
|
1300
|
+
if (entry.workspace_fixture !== undefined) {
|
|
1301
|
+
hasWorkspaceFixture = true;
|
|
1302
|
+
validateNonEmptyString(entry.workspace_fixture, `${caseField}.workspace_fixture`);
|
|
1303
|
+
}
|
|
1304
|
+
if (entry.use_project_rules !== undefined &&
|
|
1305
|
+
typeof entry.use_project_rules !== 'boolean') {
|
|
1306
|
+
error(`${caseField}.use_project_rules must be boolean`);
|
|
1307
|
+
}
|
|
1308
|
+
if (entry.tags !== undefined) {
|
|
1309
|
+
if (!Array.isArray(entry.tags) || entry.tags.length === 0) {
|
|
1310
|
+
error(`${caseField}.tags must be a non-empty array`);
|
|
1311
|
+
}
|
|
1312
|
+
else {
|
|
1313
|
+
entry.tags.forEach((tag, tagIndex) => {
|
|
1314
|
+
validateNonEmptyString(tag, `${caseField}.tags[${tagIndex}]`);
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
if (hasWorkspaceFixture && suite.fixtureModule === undefined) {
|
|
1320
|
+
error(`${fieldName}.fixtureModule is required when cases declare workspace_fixture`);
|
|
1321
|
+
}
|
|
1322
|
+
if (Array.isArray(suite.releaseSplits)) {
|
|
1323
|
+
suite.releaseSplits.forEach((split) => {
|
|
1324
|
+
if (split !== 'all' && !seenSplits.has(split)) {
|
|
1325
|
+
error(`${fieldName}.cases has no cases for release split ${JSON.stringify(split)}`);
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function resolveEvalProfilePlugin(pluginDir, ownerManifest, pluginName, fieldName) {
|
|
1331
|
+
if (pluginName === ownerManifest.name) {
|
|
1332
|
+
return { pluginDir, manifest: ownerManifest };
|
|
1333
|
+
}
|
|
1334
|
+
const companionDir = path.join(path.dirname(pluginDir), pluginName);
|
|
1335
|
+
const manifestPath = path.join(companionDir, '.agents-plugin', 'plugin.json');
|
|
1336
|
+
if (!fs.existsSync(manifestPath)) {
|
|
1337
|
+
error(`${fieldName} must reference repo-local plugin "${pluginName}"`);
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
let manifest;
|
|
1341
|
+
try {
|
|
1342
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
1343
|
+
}
|
|
1344
|
+
catch (e) {
|
|
1345
|
+
error(`${fieldName} companion manifest is not valid JSON: ${e.message}`);
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
if (!isRecord(manifest) || manifest.name !== pluginName) {
|
|
1349
|
+
error(`${fieldName} companion source must declare plugin name "${pluginName}"`);
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
return { pluginDir: companionDir, manifest };
|
|
1353
|
+
}
|
|
1354
|
+
function validateOnePluginEvalManifest(pluginDir, pluginManifest, evalDir, expectedSkill) {
|
|
1355
|
+
const evalManifestPath = path.join(evalDir, 'manifest.json');
|
|
1356
|
+
const manifestField = path.relative(pluginDir, evalManifestPath).split(path.sep).join('/');
|
|
1357
|
+
let document;
|
|
1358
|
+
try {
|
|
1359
|
+
document = JSON.parse(fs.readFileSync(evalManifestPath, 'utf8'));
|
|
1360
|
+
}
|
|
1361
|
+
catch (e) {
|
|
1362
|
+
error(`${manifestField} is not valid JSON: ${e.message}`);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const manifest = expectRecord(document, manifestField);
|
|
1366
|
+
if (!manifest)
|
|
1367
|
+
return;
|
|
1368
|
+
validateAllowedKeys(manifest, manifestField, ['schemaVersion', 'plugin', 'skill', 'profiles', 'suites']);
|
|
1369
|
+
if (manifest.schemaVersion !== PLUGIN_EVAL_SCHEMA_VERSION) {
|
|
1370
|
+
error(`${manifestField}.schemaVersion must be ${PLUGIN_EVAL_SCHEMA_VERSION}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (manifest.plugin !== pluginManifest.name) {
|
|
1373
|
+
error(`${manifestField}.plugin must match plugin name ${JSON.stringify(pluginManifest.name)}`);
|
|
1374
|
+
}
|
|
1375
|
+
const skill = validateNonEmptyString(manifest.skill, `${manifestField}.skill`);
|
|
1376
|
+
if (skill && skill !== expectedSkill) {
|
|
1377
|
+
error(`${manifestField}.skill must match owning Skill ${JSON.stringify(expectedSkill)}`);
|
|
1378
|
+
}
|
|
1379
|
+
if (skill && !collectDeclaredSkillNames(pluginDir, pluginManifest).all.has(skill)) {
|
|
1380
|
+
error(`${manifestField}.skill must reference a declared static or dynamic skill`);
|
|
1381
|
+
}
|
|
1382
|
+
if (!Array.isArray(manifest.profiles) || manifest.profiles.length === 0) {
|
|
1383
|
+
error(`${manifestField}.profiles must be a non-empty array`);
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const profilesById = new Map();
|
|
1387
|
+
manifest.profiles.forEach((value, index) => {
|
|
1388
|
+
const fieldName = `${manifestField}.profiles[${index}]`;
|
|
1389
|
+
const profile = expectRecord(value, fieldName);
|
|
1390
|
+
if (!profile)
|
|
1391
|
+
return;
|
|
1392
|
+
validateAllowedKeys(profile, fieldName, ['id', 'scope', 'plugins', 'runModel', 'requiredForRelease']);
|
|
1393
|
+
const id = validateNonEmptyString(profile.id, `${fieldName}.id`);
|
|
1394
|
+
if (id && !PLUGIN_NAME_PATTERN.test(id)) {
|
|
1395
|
+
error(`${fieldName}.id must use lowercase kebab-case`);
|
|
1396
|
+
}
|
|
1397
|
+
else if (id && profilesById.has(id)) {
|
|
1398
|
+
error(`${manifestField} has duplicate profile id ${JSON.stringify(id)}`);
|
|
1399
|
+
}
|
|
1400
|
+
if (!PLUGIN_EVAL_SCOPES.has(profile.scope)) {
|
|
1401
|
+
error(`${fieldName}.scope must be user or project`);
|
|
1402
|
+
}
|
|
1403
|
+
const skillOwners = new Map();
|
|
1404
|
+
const installedPlugins = new Set();
|
|
1405
|
+
if (!Array.isArray(profile.plugins) || profile.plugins.length === 0) {
|
|
1406
|
+
error(`${fieldName}.plugins must be a non-empty array`);
|
|
1407
|
+
}
|
|
1408
|
+
else {
|
|
1409
|
+
profile.plugins.forEach((value, pluginIndex) => {
|
|
1410
|
+
const pluginField = `${fieldName}.plugins[${pluginIndex}]`;
|
|
1411
|
+
const installation = expectRecord(value, pluginField);
|
|
1412
|
+
if (!installation)
|
|
1413
|
+
return;
|
|
1414
|
+
validateAllowedKeys(installation, pluginField, ['name', 'config']);
|
|
1415
|
+
const pluginName = validateNonEmptyString(installation.name, `${pluginField}.name`);
|
|
1416
|
+
if (!pluginName)
|
|
1417
|
+
return;
|
|
1418
|
+
if (!PLUGIN_NAME_PATTERN.test(pluginName)) {
|
|
1419
|
+
error(`${pluginField}.name must use lowercase kebab-case`);
|
|
1420
|
+
}
|
|
1421
|
+
if (installedPlugins.has(pluginName)) {
|
|
1422
|
+
error(`${fieldName}.plugins has duplicate plugin ${JSON.stringify(pluginName)}`);
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
installedPlugins.add(pluginName);
|
|
1426
|
+
if (installation.config !== undefined && !isRecord(installation.config)) {
|
|
1427
|
+
error(`${pluginField}.config must be an object`);
|
|
1428
|
+
}
|
|
1429
|
+
const source = resolveEvalProfilePlugin(pluginDir, pluginManifest, pluginName, `${pluginField}.name`);
|
|
1430
|
+
if (!source)
|
|
1431
|
+
return;
|
|
1432
|
+
for (const skillName of collectDeclaredSkillNames(source.pluginDir, source.manifest).all) {
|
|
1433
|
+
const existing = skillOwners.get(skillName);
|
|
1434
|
+
if (existing && existing !== pluginName) {
|
|
1435
|
+
error(`${fieldName} installs duplicate Skill key ${JSON.stringify(skillName)} ` +
|
|
1436
|
+
`from plugins ${JSON.stringify(existing)} and ${JSON.stringify(pluginName)}`);
|
|
1437
|
+
}
|
|
1438
|
+
else {
|
|
1439
|
+
skillOwners.set(skillName, pluginName);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
if (!installedPlugins.has(pluginManifest.name)) {
|
|
1445
|
+
error(`${fieldName}.plugins must include owner plugin ${JSON.stringify(pluginManifest.name)}`);
|
|
1446
|
+
}
|
|
1447
|
+
if (typeof profile.runModel !== 'boolean') {
|
|
1448
|
+
error(`${fieldName}.runModel must be boolean`);
|
|
1449
|
+
}
|
|
1450
|
+
if (typeof profile.requiredForRelease !== 'boolean') {
|
|
1451
|
+
error(`${fieldName}.requiredForRelease must be boolean`);
|
|
1452
|
+
}
|
|
1453
|
+
if (profile.scope === 'user' && profile.runModel === true) {
|
|
1454
|
+
error(`${fieldName}: eval schema v2 does not allow real model execution for user scope`);
|
|
1455
|
+
}
|
|
1456
|
+
if (id && PLUGIN_NAME_PATTERN.test(id) && !profilesById.has(id)) {
|
|
1457
|
+
profilesById.set(id, { ...profile, skillOwners });
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
if (!manifest.profiles.some((profile) => isRecord(profile) && profile.scope === 'project' && profile.runModel === true)) {
|
|
1461
|
+
error(`${manifestField} must define at least one project profile with runModel=true`);
|
|
1462
|
+
}
|
|
1463
|
+
if (!Array.isArray(manifest.suites) || manifest.suites.length === 0) {
|
|
1464
|
+
error(`${manifestField}.suites must be a non-empty array`);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const suiteIds = new Set();
|
|
1468
|
+
manifest.suites.forEach((value, index) => {
|
|
1469
|
+
const fieldName = `${manifestField}.suites[${index}]`;
|
|
1470
|
+
const suite = expectRecord(value, fieldName);
|
|
1471
|
+
if (!suite)
|
|
1472
|
+
return;
|
|
1473
|
+
const id = validateNonEmptyString(suite.id, `${fieldName}.id`);
|
|
1474
|
+
if (id && !PLUGIN_NAME_PATTERN.test(id)) {
|
|
1475
|
+
error(`${fieldName}.id must use lowercase kebab-case`);
|
|
1476
|
+
}
|
|
1477
|
+
else if (id && suiteIds.has(id)) {
|
|
1478
|
+
error(`${manifestField} has duplicate suite id ${JSON.stringify(id)}`);
|
|
1479
|
+
}
|
|
1480
|
+
else if (id) {
|
|
1481
|
+
suiteIds.add(id);
|
|
1482
|
+
}
|
|
1483
|
+
if (suite.kind !== 'command' && suite.kind !== 'conversation' && suite.kind !== 'trigger') {
|
|
1484
|
+
error(`${fieldName}.kind must be command, conversation, or trigger`);
|
|
1485
|
+
}
|
|
1486
|
+
const suiteSkills = new Set();
|
|
1487
|
+
if (!Array.isArray(suite.skills) || suite.skills.length === 0) {
|
|
1488
|
+
error(`${fieldName}.skills must be a non-empty array`);
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
suite.skills.forEach((skillName, skillIndex) => {
|
|
1492
|
+
const skillField = `${fieldName}.skills[${skillIndex}]`;
|
|
1493
|
+
const normalized = validateNonEmptyString(skillName, skillField);
|
|
1494
|
+
if (!normalized)
|
|
1495
|
+
return;
|
|
1496
|
+
if (!PLUGIN_NAME_PATTERN.test(normalized)) {
|
|
1497
|
+
error(`${skillField} must use lowercase kebab-case`);
|
|
1498
|
+
}
|
|
1499
|
+
else if (suiteSkills.has(normalized)) {
|
|
1500
|
+
error(`${fieldName}.skills has duplicate Skill ${JSON.stringify(normalized)}`);
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
suiteSkills.add(normalized);
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
if (skill && !suiteSkills.has(skill)) {
|
|
1508
|
+
error(`${fieldName}.skills must include owner Skill ${JSON.stringify(skill)}`);
|
|
1509
|
+
}
|
|
1510
|
+
if (!Array.isArray(suite.profiles) || suite.profiles.length === 0) {
|
|
1511
|
+
error(`${fieldName}.profiles must be a non-empty array`);
|
|
1512
|
+
}
|
|
1513
|
+
else {
|
|
1514
|
+
suite.profiles.forEach((profileId, profileIndex) => {
|
|
1515
|
+
const profileField = `${fieldName}.profiles[${profileIndex}]`;
|
|
1516
|
+
if (typeof profileId !== 'string' || profileId.trim() === '') {
|
|
1517
|
+
error(`${profileField} must be a non-empty string`);
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const profile = profilesById.get(profileId);
|
|
1521
|
+
if (!profile) {
|
|
1522
|
+
error(`${fieldName} references unknown profile ${JSON.stringify(profileId)}`);
|
|
1523
|
+
}
|
|
1524
|
+
else if (profile.runModel !== true) {
|
|
1525
|
+
error(`${fieldName} cannot execute materialize-only profile ${JSON.stringify(profileId)}`);
|
|
1526
|
+
}
|
|
1527
|
+
if (profile) {
|
|
1528
|
+
for (const skillName of suiteSkills) {
|
|
1529
|
+
if (!profile.skillOwners.has(skillName)) {
|
|
1530
|
+
error(`${fieldName} requires Skill ${JSON.stringify(skillName)}, but profile ` +
|
|
1531
|
+
`${JSON.stringify(profileId)} does not install a plugin that declares it`);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
if (!Number.isInteger(suite.defaultRuns) || suite.defaultRuns <= 0) {
|
|
1538
|
+
error(`${fieldName}.defaultRuns must be a positive integer`);
|
|
1539
|
+
}
|
|
1540
|
+
if (!Number.isInteger(suite.perRunTimeoutMs) || suite.perRunTimeoutMs <= 0) {
|
|
1541
|
+
error(`${fieldName}.perRunTimeoutMs must be a positive integer`);
|
|
1542
|
+
}
|
|
1543
|
+
if (typeof suite.requiredForRelease !== 'boolean') {
|
|
1544
|
+
error(`${fieldName}.requiredForRelease must be boolean`);
|
|
1545
|
+
}
|
|
1546
|
+
if (!Array.isArray(suite.releaseSplits)) {
|
|
1547
|
+
error(`${fieldName}.releaseSplits must be an array`);
|
|
1548
|
+
}
|
|
1549
|
+
else {
|
|
1550
|
+
suite.releaseSplits.forEach((split, splitIndex) => {
|
|
1551
|
+
if (!PLUGIN_EVAL_SPLITS.has(split)) {
|
|
1552
|
+
error(`${fieldName}.releaseSplits[${splitIndex}] is unsupported`);
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
if (suite.kind === 'command') {
|
|
1557
|
+
validateAllowedKeys(suite, fieldName, [
|
|
1558
|
+
'id', 'kind', 'skills', 'entry', 'cases', 'profiles', 'defaultRuns', 'releaseSplits',
|
|
1559
|
+
'requiredForRelease', 'perRunTimeoutMs', 'suiteTimeoutMs', 'supports',
|
|
1560
|
+
]);
|
|
1561
|
+
validateEvalRelativeFile(evalDir, suite.entry, `${fieldName}.entry`);
|
|
1562
|
+
if (!Number.isInteger(suite.suiteTimeoutMs) || suite.suiteTimeoutMs <= 0) {
|
|
1563
|
+
error(`${fieldName}.suiteTimeoutMs must be a positive integer`);
|
|
1564
|
+
}
|
|
1565
|
+
else if (suite.suiteTimeoutMs < suite.perRunTimeoutMs) {
|
|
1566
|
+
error(`${fieldName}.suiteTimeoutMs must be greater than or equal to perRunTimeoutMs`);
|
|
1567
|
+
}
|
|
1568
|
+
if (!Array.isArray(suite.supports)) {
|
|
1569
|
+
error(`${fieldName}.supports must be an array`);
|
|
1570
|
+
}
|
|
1571
|
+
else {
|
|
1572
|
+
suite.supports.forEach((support, supportIndex) => {
|
|
1573
|
+
if (!PLUGIN_EVAL_COMMAND_SUPPORTS.has(support)) {
|
|
1574
|
+
error(`${fieldName}.supports[${supportIndex}] is unsupported`);
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1577
|
+
if (Array.isArray(suite.releaseSplits) &&
|
|
1578
|
+
suite.releaseSplits.length > 0 &&
|
|
1579
|
+
!suite.supports.includes('split')) {
|
|
1580
|
+
error(`${fieldName} declares releaseSplits but does not support split`);
|
|
1581
|
+
}
|
|
1582
|
+
const supportsCase = suite.supports.includes('case');
|
|
1583
|
+
if (supportsCase && suite.cases === undefined) {
|
|
1584
|
+
error(`${fieldName}.cases is required when supports includes case`);
|
|
1585
|
+
}
|
|
1586
|
+
else if (!supportsCase && suite.cases !== undefined) {
|
|
1587
|
+
error(`${fieldName}.cases requires supports to include case`);
|
|
1588
|
+
}
|
|
1589
|
+
if (supportsCase && suite.cases !== undefined) {
|
|
1590
|
+
const casesPath = validateEvalRelativeFile(evalDir, suite.cases, `${fieldName}.cases`);
|
|
1591
|
+
if (casesPath) {
|
|
1592
|
+
try {
|
|
1593
|
+
const casesDocument = JSON.parse(fs.readFileSync(casesPath, 'utf8'));
|
|
1594
|
+
if (!isRecord(casesDocument) || casesDocument.skill_name !== skill) {
|
|
1595
|
+
error(`${fieldName}.cases.skill_name must match ${JSON.stringify(skill)}`);
|
|
1596
|
+
}
|
|
1597
|
+
else if (!Array.isArray(casesDocument.evals) || casesDocument.evals.length === 0) {
|
|
1598
|
+
error(`${fieldName}.cases.evals must be a non-empty array`);
|
|
1599
|
+
}
|
|
1600
|
+
else {
|
|
1601
|
+
const caseIds = new Set();
|
|
1602
|
+
casesDocument.evals.forEach((entry, caseIndex) => {
|
|
1603
|
+
const caseField = `${fieldName}.cases.evals[${caseIndex}]`;
|
|
1604
|
+
if (!isRecord(entry) || typeof entry.id !== 'string' || !/^[a-z0-9_]+$/.test(entry.id)) {
|
|
1605
|
+
error(`${caseField}.id must use lowercase snake_case`);
|
|
1606
|
+
}
|
|
1607
|
+
else if (caseIds.has(entry.id)) {
|
|
1608
|
+
error(`${fieldName}.cases has duplicate case id ${JSON.stringify(entry.id)}`);
|
|
1609
|
+
}
|
|
1610
|
+
else {
|
|
1611
|
+
caseIds.add(entry.id);
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
catch (cause) {
|
|
1617
|
+
error(`${fieldName}.cases is not valid JSON: ${cause.message}`);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
else if (suite.kind === 'conversation' || suite.kind === 'trigger') {
|
|
1624
|
+
const normalizedSuite = { ...suite, skill };
|
|
1625
|
+
const isolation = suite.isolation ?? 'standard';
|
|
1626
|
+
if (isolation !== 'standard' && isolation !== 'strict') {
|
|
1627
|
+
error(`${fieldName}.isolation must be standard or strict`);
|
|
1628
|
+
}
|
|
1629
|
+
if (suite.kind === 'conversation') {
|
|
1630
|
+
validateAllowedKeys(suite, fieldName, [
|
|
1631
|
+
'id', 'kind', 'skills', 'cases', 'profiles', 'defaultRuns', 'releaseSplits',
|
|
1632
|
+
'requiredForRelease', 'perRunTimeoutMs', 'isolation',
|
|
1633
|
+
]);
|
|
1634
|
+
if (Array.isArray(suite.releaseSplits) && suite.releaseSplits.length > 0) {
|
|
1635
|
+
error(`${fieldName} cannot declare releaseSplits because conversation cases do not support split filtering`);
|
|
1636
|
+
}
|
|
1637
|
+
validateConversationCaseCorpus(evalDir, normalizedSuite, fieldName);
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
validateAllowedKeys(suite, fieldName, [
|
|
1641
|
+
'id', 'kind', 'skills', 'cases', 'fixtureModule', 'observeSpawnAgent', 'isolation',
|
|
1642
|
+
'strictNegativeSplits', 'minimumPassRate', 'forbidMultipleSkillLoads',
|
|
1643
|
+
'profiles', 'defaultRuns', 'releaseSplits',
|
|
1644
|
+
'requiredForRelease', 'perRunTimeoutMs',
|
|
1645
|
+
]);
|
|
1646
|
+
if (typeof suite.observeSpawnAgent !== 'boolean') {
|
|
1647
|
+
error(`${fieldName}.observeSpawnAgent must be boolean`);
|
|
1648
|
+
}
|
|
1649
|
+
if (!Array.isArray(suite.strictNegativeSplits)) {
|
|
1650
|
+
error(`${fieldName}.strictNegativeSplits must be an array`);
|
|
1651
|
+
}
|
|
1652
|
+
else {
|
|
1653
|
+
suite.strictNegativeSplits.forEach((split, splitIndex) => {
|
|
1654
|
+
if (!PLUGIN_EVAL_TRIGGER_CASE_SPLITS.has(split)) {
|
|
1655
|
+
error(`${fieldName}.strictNegativeSplits[${splitIndex}] is unsupported`);
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
if (suite.minimumPassRate !== undefined &&
|
|
1660
|
+
(typeof suite.minimumPassRate !== 'number' ||
|
|
1661
|
+
!Number.isFinite(suite.minimumPassRate) ||
|
|
1662
|
+
suite.minimumPassRate <= 0 ||
|
|
1663
|
+
suite.minimumPassRate > 1)) {
|
|
1664
|
+
error(`${fieldName}.minimumPassRate must be greater than 0 and at most 1`);
|
|
1665
|
+
}
|
|
1666
|
+
if (suite.forbidMultipleSkillLoads !== undefined &&
|
|
1667
|
+
typeof suite.forbidMultipleSkillLoads !== 'boolean') {
|
|
1668
|
+
error(`${fieldName}.forbidMultipleSkillLoads must be boolean`);
|
|
1669
|
+
}
|
|
1670
|
+
if (suite.fixtureModule !== undefined) {
|
|
1671
|
+
const fixturePath = validateEvalRelativeFile(evalDir, suite.fixtureModule, `${fieldName}.fixtureModule`);
|
|
1672
|
+
if (fixturePath && path.extname(fixturePath) !== '.mjs') {
|
|
1673
|
+
error(`${fieldName}.fixtureModule must reference an .mjs file`);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
validateTriggerCaseCorpus(evalDir, normalizedSuite, fieldName);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
});
|
|
1680
|
+
if (skill
|
|
1681
|
+
&& collectDeclaredSkillNames(pluginDir, pluginManifest).dynamic.has(skill)
|
|
1682
|
+
&& !manifest.suites.some((suite) => isRecord(suite) && suite.requiredForRelease === true)) {
|
|
1683
|
+
error(`${manifestField}.suites must include at least one release-required suite for dynamic Skill ${JSON.stringify(skill)}`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
function validatePluginEvalManifest(pluginDir, pluginManifest) {
|
|
1687
|
+
const legacyRoot = path.join(pluginDir, 'evals');
|
|
1688
|
+
if (fs.existsSync(legacyRoot)) {
|
|
1689
|
+
error('Plugin-root evals/ is legacy; put evals/ inside its owning static or dynamic Skill directory.');
|
|
1690
|
+
}
|
|
1691
|
+
const skillRoots = [...collectDeclaredSkillNames(pluginDir, pluginManifest).all]
|
|
1692
|
+
.sort()
|
|
1693
|
+
.map((skill) => ({ skill, root: path.join(pluginDir, 'skills', skill) }));
|
|
1694
|
+
const seenSkills = new Set();
|
|
1695
|
+
for (const owner of skillRoots) {
|
|
1696
|
+
const evalDir = path.join(owner.root, 'evals');
|
|
1697
|
+
const manifestPath = path.join(evalDir, 'manifest.json');
|
|
1698
|
+
if (!fs.existsSync(manifestPath))
|
|
1699
|
+
continue;
|
|
1700
|
+
if (seenSkills.has(owner.skill)) {
|
|
1701
|
+
error(`Skill ${JSON.stringify(owner.skill)} has more than one evals/manifest.json owner`);
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
seenSkills.add(owner.skill);
|
|
1705
|
+
validateOnePluginEvalManifest(pluginDir, pluginManifest, evalDir, owner.skill);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
function countCharacters(value) {
|
|
1709
|
+
return Array.from(value).length;
|
|
1710
|
+
}
|
|
1711
|
+
function countLines(value) {
|
|
1712
|
+
if (value.length === 0)
|
|
1713
|
+
return 0;
|
|
1714
|
+
const normalized = value.replace(/\r\n?/g, '\n');
|
|
1715
|
+
const withoutFinalNewline = normalized.endsWith('\n')
|
|
1716
|
+
? normalized.slice(0, -1)
|
|
1717
|
+
: normalized;
|
|
1718
|
+
return withoutFinalNewline.split('\n').length;
|
|
1719
|
+
}
|
|
1720
|
+
function sanitizeSingleLine(value) {
|
|
1721
|
+
return value.trim().split(/\s+/).filter(Boolean).join(' ');
|
|
1722
|
+
}
|
|
1723
|
+
function validateCodexMetadataString(record, key, fieldName, maxLength, required = false) {
|
|
1724
|
+
if (record[key] === undefined) {
|
|
1725
|
+
if (required) {
|
|
1726
|
+
error(`${fieldName}.${key} is required`);
|
|
1727
|
+
}
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
if (typeof record[key] !== 'string') {
|
|
1731
|
+
error(`${fieldName}.${key} must be a string`);
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
const sanitized = sanitizeSingleLine(record[key]);
|
|
1735
|
+
if (sanitized.length === 0) {
|
|
1736
|
+
error(`${fieldName}.${key} must not be empty`);
|
|
1737
|
+
return null;
|
|
1738
|
+
}
|
|
1739
|
+
if (countCharacters(sanitized) > maxLength) {
|
|
1740
|
+
error(`${fieldName}.${key} must be <= ${maxLength} characters`);
|
|
1741
|
+
}
|
|
1742
|
+
return sanitized;
|
|
1743
|
+
}
|
|
1744
|
+
function validateCodexShortDescription(record, fieldName) {
|
|
1745
|
+
const sanitized = validateCodexMetadataString(record, 'short_description', fieldName, MAX_OPENAI_INTERFACE_DESCRIPTION_LEN);
|
|
1746
|
+
if (sanitized !== null &&
|
|
1747
|
+
countCharacters(sanitized) < MIN_OPENAI_INTERFACE_DESCRIPTION_LEN) {
|
|
1748
|
+
error(`${fieldName}.short_description must be >= ${MIN_OPENAI_INTERFACE_DESCRIPTION_LEN} characters`);
|
|
1749
|
+
}
|
|
1750
|
+
return sanitized;
|
|
1751
|
+
}
|
|
1752
|
+
function validateCodexDefaultPrompt(record, key, fieldName, maxLength, skillReference, required = false) {
|
|
1753
|
+
const sanitized = validateCodexMetadataString(record, key, fieldName, maxLength, required);
|
|
1754
|
+
if (sanitized === null)
|
|
1755
|
+
return null;
|
|
1756
|
+
const mention = `$${skillReference}`;
|
|
1757
|
+
if (!sanitized.includes(mention)) {
|
|
1758
|
+
error(`${fieldName}.${key} must explicitly mention "${mention}"`);
|
|
1759
|
+
}
|
|
1760
|
+
return sanitized;
|
|
1761
|
+
}
|
|
1762
|
+
function validateCodexBrandColor(record, key, fieldName) {
|
|
1763
|
+
if (record[key] === undefined)
|
|
1764
|
+
return;
|
|
1765
|
+
if (typeof record[key] !== 'string') {
|
|
1766
|
+
error(`${fieldName}.${key} must be a string`);
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const value = record[key].trim();
|
|
1770
|
+
if (value.length === 0) {
|
|
1771
|
+
error(`${fieldName}.${key} must not be empty`);
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(value)) {
|
|
1775
|
+
error(`${fieldName}.${key} must match #RRGGBB`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
function validateCodexIconPath(record, key, fieldName) {
|
|
1779
|
+
if (record[key] === undefined)
|
|
1780
|
+
return;
|
|
1781
|
+
if (typeof record[key] !== 'string') {
|
|
1782
|
+
error(`${fieldName}.${key} must be a string`);
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
const value = record[key].trim();
|
|
1786
|
+
if (value.length === 0) {
|
|
1787
|
+
error(`${fieldName}.${key} must not be empty`);
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
1791
|
+
error(`${fieldName}.${key} must be a relative path under assets/`);
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const components = value.split('/');
|
|
1795
|
+
if (components.some((component) => component === '..')) {
|
|
1796
|
+
error(`${fieldName}.${key} must be a relative path under assets/`);
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
const normalized = path.posix.normalize(stripDotSlash(value));
|
|
1800
|
+
if (normalized !== 'assets' && !normalized.startsWith('assets/')) {
|
|
1801
|
+
error(`${fieldName}.${key} must be a relative path under assets/`);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
function validateAllowedTools(value, fieldName, options) {
|
|
1805
|
+
if (typeof value === 'string') {
|
|
1806
|
+
if (value.trim().length === 0) {
|
|
1807
|
+
error(`${fieldName} must not be an empty string`);
|
|
1808
|
+
}
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (!options.allowArray) {
|
|
1812
|
+
error(`${fieldName} must be a space-separated string in the Agent Skills portable core profile`);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
if (!Array.isArray(value)) {
|
|
1816
|
+
error(`${fieldName} must be a string or an array of strings`);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
for (const [index, item] of value.entries()) {
|
|
1820
|
+
if (typeof item !== 'string') {
|
|
1821
|
+
error(`${fieldName}[${index}] must be a string`);
|
|
1822
|
+
}
|
|
1823
|
+
else if (item.trim().length === 0) {
|
|
1824
|
+
error(`${fieldName}[${index}] must not be empty`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function isNonEmptyFrontmatterString(value) {
|
|
1829
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
1830
|
+
}
|
|
1831
|
+
function isFrontmatterStringArray(value) {
|
|
1832
|
+
return Array.isArray(value) && value.every(isNonEmptyFrontmatterString);
|
|
1833
|
+
}
|
|
1834
|
+
function frontmatterValueMatchesType(value, type) {
|
|
1835
|
+
switch (type) {
|
|
1836
|
+
case 'boolean':
|
|
1837
|
+
return typeof value === 'boolean';
|
|
1838
|
+
case 'boolean-or-string':
|
|
1839
|
+
return typeof value === 'boolean' || isNonEmptyFrontmatterString(value);
|
|
1840
|
+
case 'boolean-or-string-or-binary-number':
|
|
1841
|
+
return typeof value === 'boolean'
|
|
1842
|
+
|| isNonEmptyFrontmatterString(value)
|
|
1843
|
+
|| value === 0
|
|
1844
|
+
|| value === 1;
|
|
1845
|
+
case 'object':
|
|
1846
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
1847
|
+
case 'string':
|
|
1848
|
+
return isNonEmptyFrontmatterString(value);
|
|
1849
|
+
case 'string-map':
|
|
1850
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
1851
|
+
&& Object.values(value).every((entry) => typeof entry === 'string');
|
|
1852
|
+
case 'string-or-integer':
|
|
1853
|
+
return isNonEmptyFrontmatterString(value)
|
|
1854
|
+
|| (typeof value === 'number' && Number.isInteger(value));
|
|
1855
|
+
case 'string-or-string-array':
|
|
1856
|
+
return isNonEmptyFrontmatterString(value) || isFrontmatterStringArray(value);
|
|
1857
|
+
default:
|
|
1858
|
+
return false;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function skillFrontmatterProfile(options) {
|
|
1862
|
+
if (options.strictAgentSkills || !options.targetsClaude) {
|
|
1863
|
+
return 'portable-agent-skills';
|
|
1864
|
+
}
|
|
1865
|
+
return options.claudeSkillProfile === 'current'
|
|
1866
|
+
? 'claude-current'
|
|
1867
|
+
: 'claude-2.1.88';
|
|
1868
|
+
}
|
|
1869
|
+
function validateSkillFrontmatterCapabilityFields(fm, fieldName, profile) {
|
|
1870
|
+
let registry;
|
|
1871
|
+
try {
|
|
1872
|
+
registry = loadSkillFrontmatterCapabilityRegistry();
|
|
1873
|
+
}
|
|
1874
|
+
catch (registryError) {
|
|
1875
|
+
error(`${fieldName} capability registry error: ${registryError.message}`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
for (const [key, value] of Object.entries(fm)) {
|
|
1879
|
+
const capability = registry.fields[key];
|
|
1880
|
+
if (!capability) {
|
|
1881
|
+
error(`${fieldName} [unknown-field] field ${JSON.stringify(key)} is not registered for any supported profile`);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
if (!capability.profiles.includes(profile)) {
|
|
1885
|
+
if (profile === 'portable-agent-skills' && capability.owner !== 'agent-skills') {
|
|
1886
|
+
error(`${fieldName} [portable-profile-platform-extension] field ${JSON.stringify(key)} is a Claude Code extension`);
|
|
1887
|
+
}
|
|
1888
|
+
else {
|
|
1889
|
+
const version = capability.introducedVersion
|
|
1890
|
+
? `; documented minimum is Claude Code ${capability.introducedVersion}`
|
|
1891
|
+
: '';
|
|
1892
|
+
error(`${fieldName} [known-field-not-supported-by-target-version] field ${JSON.stringify(key)} is not supported by profile ${JSON.stringify(profile)}${version}`);
|
|
1893
|
+
}
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
const expectedType = capability.typeByProfile?.[profile] ?? capability.type;
|
|
1897
|
+
if (!frontmatterValueMatchesType(value, expectedType)) {
|
|
1898
|
+
error(`${fieldName} [known-field-invalid-type-value] field ${JSON.stringify(key)} must have type ${JSON.stringify(expectedType)} in profile ${JSON.stringify(profile)}`);
|
|
1899
|
+
continue;
|
|
1900
|
+
}
|
|
1901
|
+
const acceptedValues = capability.acceptedValuesByProfile?.[profile]
|
|
1902
|
+
?? capability.acceptedValues;
|
|
1903
|
+
const acceptedValuesCaseInsensitive = capability.acceptedValuesCaseInsensitiveByProfile?.[profile]
|
|
1904
|
+
?? capability.acceptedValuesCaseInsensitive
|
|
1905
|
+
?? false;
|
|
1906
|
+
if (typeof value === 'string' &&
|
|
1907
|
+
acceptedValues &&
|
|
1908
|
+
!acceptedValues.includes(acceptedValuesCaseInsensitive ? value.trim().toLowerCase() : value.trim())) {
|
|
1909
|
+
error(`${fieldName} [known-field-invalid-type-value] field ${JSON.stringify(key)} must be one of ${acceptedValues.map((entry) => JSON.stringify(entry)).join(', ')}`);
|
|
1910
|
+
}
|
|
1911
|
+
for (const [requiredKey, requiredValue] of Object.entries(capability.requires ?? {})) {
|
|
1912
|
+
if (fm[requiredKey] !== requiredValue) {
|
|
1913
|
+
error(`${fieldName} [known-field-requires-related-field] field ${JSON.stringify(key)} requires ${JSON.stringify(requiredKey)}=${JSON.stringify(requiredValue)}`);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
for (const [key, capability] of Object.entries(registry.fields)) {
|
|
1918
|
+
if (capability.required && capability.profiles.includes(profile) && fm[key] === undefined) {
|
|
1919
|
+
error(`${fieldName} [known-field-invalid-type-value] required field ${JSON.stringify(key)} is missing for profile ${JSON.stringify(profile)}`);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
function validateSkillFrontmatter(fm, skillName, options) {
|
|
1924
|
+
const fieldName = `skills/${skillName}/SKILL.md frontmatter`;
|
|
1925
|
+
const profile = skillFrontmatterProfile(options);
|
|
1926
|
+
validateSkillFrontmatterCapabilityFields(fm, fieldName, profile);
|
|
1927
|
+
if (fm.name === undefined) {
|
|
1928
|
+
error(`${fieldName} missing "name"`);
|
|
1929
|
+
}
|
|
1930
|
+
else {
|
|
1931
|
+
const name = validateNonEmptyString(fm.name, `${fieldName}.name`, MAX_SKILL_NAME_LEN);
|
|
1932
|
+
if (name) {
|
|
1933
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
1934
|
+
error(`${fieldName}.name must use ASCII lowercase hyphen-case`);
|
|
1935
|
+
}
|
|
1936
|
+
if (name !== skillName) {
|
|
1937
|
+
error(`${fieldName}.name "${name}" must match skill directory "${skillName}"`);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if (fm.description === undefined) {
|
|
1942
|
+
error(`${fieldName} missing "description"`);
|
|
1943
|
+
}
|
|
1944
|
+
else {
|
|
1945
|
+
validateNonEmptyString(fm.description, `${fieldName}.description`, MAX_SKILL_DESCRIPTION_LEN);
|
|
1946
|
+
}
|
|
1947
|
+
if (fm.license !== undefined) {
|
|
1948
|
+
validateNonEmptyString(fm.license, `${fieldName}.license`);
|
|
1949
|
+
}
|
|
1950
|
+
if (fm.compatibility !== undefined) {
|
|
1951
|
+
validateNonEmptyString(fm.compatibility, `${fieldName}.compatibility`, MAX_SKILL_COMPATIBILITY_LEN);
|
|
1952
|
+
}
|
|
1953
|
+
if (fm.metadata !== undefined) {
|
|
1954
|
+
const metadata = expectRecord(fm.metadata, `${fieldName}.metadata`);
|
|
1955
|
+
if (metadata) {
|
|
1956
|
+
if (profile !== 'claude-current') {
|
|
1957
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
1958
|
+
if (typeof value !== 'string') {
|
|
1959
|
+
error(`${fieldName}.metadata.${key} must be a string`);
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (metadata['short-description'] !== undefined) {
|
|
1964
|
+
validateNonEmptyString(metadata['short-description'], `${fieldName}.metadata.short-description`, MAX_SKILL_SHORT_DESCRIPTION_LEN);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
function isCodexTargeted(manifest) {
|
|
1970
|
+
return supportsManifestPlatform(manifest, 'codex');
|
|
1971
|
+
}
|
|
1972
|
+
function isClaudeTargeted(manifest) {
|
|
1973
|
+
return supportsManifestPlatform(manifest, 'claude');
|
|
1974
|
+
}
|
|
1975
|
+
function targetedPlatforms(manifest) {
|
|
1976
|
+
const platforms = [];
|
|
1977
|
+
if (isClaudeTargeted(manifest))
|
|
1978
|
+
platforms.push('claude');
|
|
1979
|
+
if (isCodexTargeted(manifest))
|
|
1980
|
+
platforms.push('codex');
|
|
1981
|
+
return platforms.length > 0 ? platforms : ['claude', 'codex'];
|
|
1982
|
+
}
|
|
1983
|
+
// ---------------------------------------------------------------------------
|
|
1984
|
+
// Supplemental checks (always run — not covered by CLI validator)
|
|
1985
|
+
// ---------------------------------------------------------------------------
|
|
1986
|
+
function validateNameMatchesDirectory(pluginDir, manifest) {
|
|
1987
|
+
const pluginName = path.basename(pluginDir);
|
|
1988
|
+
if (manifest.name && manifest.name !== pluginName) {
|
|
1989
|
+
error(`manifest name "${manifest.name}" does not match directory name "${pluginName}"`);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
function validateNoBuildOutputDirs(pluginDir) {
|
|
1993
|
+
if (fs.existsSync(path.join(pluginDir, '.claude-plugin'))) {
|
|
1994
|
+
error('.claude-plugin/ directory must not exist in plugin source (it is a build output)');
|
|
1995
|
+
}
|
|
1996
|
+
if (fs.existsSync(path.join(pluginDir, '.codex-plugin'))) {
|
|
1997
|
+
error('.codex-plugin/ directory must not exist in plugin source (it is a build output)');
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
function validateDeclaredPathsExist(pluginDir, manifest) {
|
|
2001
|
+
for (const field of ['skills', 'slashCommands', 'mcpServers', 'apps']) {
|
|
2002
|
+
if (!manifest[field])
|
|
2003
|
+
continue;
|
|
2004
|
+
const resolved = path.join(pluginDir, manifest[field]);
|
|
2005
|
+
if (!fs.existsSync(resolved)) {
|
|
2006
|
+
error(`${field} points to "${manifest[field]}" but path does not exist: ${resolved}`);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
function validateCanonicalSkillLayout(pluginDir, manifest) {
|
|
2011
|
+
if (manifest.skills && normalizeRelativePath(manifest.skills) !== 'skills') {
|
|
2012
|
+
error('skills must be "./skills"');
|
|
2013
|
+
}
|
|
2014
|
+
const pluginReadme = path.join(pluginDir, 'README.md');
|
|
2015
|
+
if (!fs.statSync(pluginReadme, { throwIfNoEntry: false })?.isFile()) {
|
|
2016
|
+
error('Plugin root README.md is required');
|
|
2017
|
+
}
|
|
2018
|
+
else {
|
|
2019
|
+
const content = fs.readFileSync(pluginReadme, 'utf8');
|
|
2020
|
+
if (content.trim() === '')
|
|
2021
|
+
error('Plugin root README.md must not be empty');
|
|
2022
|
+
}
|
|
2023
|
+
const declared = collectDeclaredSkillNames(pluginDir, manifest);
|
|
2024
|
+
const sourceBackedDynamicSkills = (Array.isArray(manifest.artifacts?.targets)
|
|
2025
|
+
? manifest.artifacts.targets
|
|
2026
|
+
: [])
|
|
2027
|
+
.filter((target) => target &&
|
|
2028
|
+
typeof target === 'object' &&
|
|
2029
|
+
(target.target === 'codex.skill' || target.target === 'claude.skill') &&
|
|
2030
|
+
typeof target.skillKey === 'string' &&
|
|
2031
|
+
Array.isArray(target.sourceDirs) &&
|
|
2032
|
+
target.sourceDirs.length > 0)
|
|
2033
|
+
.map((target) => target.skillKey.trim());
|
|
2034
|
+
for (const skill of new Set([...declared.static, ...sourceBackedDynamicSkills])) {
|
|
2035
|
+
const skillRoot = path.join(pluginDir, 'skills', skill);
|
|
2036
|
+
if (!fs.statSync(skillRoot, { throwIfNoEntry: false })?.isDirectory()) {
|
|
2037
|
+
error(`Skill ${JSON.stringify(skill)} must use source root "./skills/${skill}"`);
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const skillReadme = path.join(skillRoot, 'README.md');
|
|
2041
|
+
if (!fs.statSync(skillReadme, { throwIfNoEntry: false })?.isFile()) {
|
|
2042
|
+
error(`skills/${skill}/README.md is required`);
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
const content = fs.readFileSync(skillReadme, 'utf8');
|
|
2046
|
+
if (content.trim() === '') {
|
|
2047
|
+
error(`skills/${skill}/README.md must not be empty`);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
const portableRoot = manifest.distributions?.agentSkills?.skills;
|
|
2051
|
+
for (const skill of declared.portable) {
|
|
2052
|
+
const skillRoot = path.join(pluginDir, portableRoot, skill);
|
|
2053
|
+
const skillReadme = path.join(skillRoot, 'README.md');
|
|
2054
|
+
if (!fs.statSync(skillReadme, { throwIfNoEntry: false })?.isFile()) {
|
|
2055
|
+
error(`${normalizeRelativePath(path.join(portableRoot, skill, 'README.md'))} is required`);
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
if (fs.readFileSync(skillReadme, 'utf8').trim() === '') {
|
|
2059
|
+
error(`${normalizeRelativePath(path.join(portableRoot, skill, 'README.md'))} must not be empty`);
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
function validateDeclaredNativeAssets(pluginDir, manifest) {
|
|
2064
|
+
const defaults = [
|
|
2065
|
+
['skills', 'skills'],
|
|
2066
|
+
[DEFAULT_SLASH_COMMANDS_DIR, 'slashCommands'],
|
|
2067
|
+
['.mcp.json', 'mcpServers'],
|
|
2068
|
+
['.app.json', 'apps'],
|
|
2069
|
+
];
|
|
2070
|
+
for (const [filename, field] of defaults) {
|
|
2071
|
+
const fullPath = path.join(pluginDir, filename);
|
|
2072
|
+
if (fs.existsSync(fullPath) && !manifest[field]) {
|
|
2073
|
+
if (field === 'skills') {
|
|
2074
|
+
const dynamicSkills = new Set((Array.isArray(manifest.artifacts?.targets) ? manifest.artifacts.targets : [])
|
|
2075
|
+
.filter((target) => {
|
|
2076
|
+
if (!target ||
|
|
2077
|
+
typeof target !== 'object' ||
|
|
2078
|
+
(target.target !== 'codex.skill' && target.target !== 'claude.skill') ||
|
|
2079
|
+
typeof target.skillKey !== 'string' ||
|
|
2080
|
+
!Array.isArray(target.sourceDirs) ||
|
|
2081
|
+
target.sourceDirs.length === 0)
|
|
2082
|
+
return false;
|
|
2083
|
+
return target.sourceDirs.every((sourceDir) => typeof sourceDir === 'string' &&
|
|
2084
|
+
(normalizeRelativePath(sourceDir) === `skills/${target.skillKey}` ||
|
|
2085
|
+
normalizeRelativePath(sourceDir).startsWith(`skills/${target.skillKey}/`)));
|
|
2086
|
+
})
|
|
2087
|
+
.map((target) => target.skillKey));
|
|
2088
|
+
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
2089
|
+
if (entries.length > 0 &&
|
|
2090
|
+
entries.every((entry) => entry.isDirectory() && dynamicSkills.has(entry.name))) {
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
error(`"${filename}" exists but manifest does not declare "${field}" — declare it or remove the file`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
function validatePluginVersionContracts(manifest, options) {
|
|
2099
|
+
if (manifest.compatibility === undefined) {
|
|
2100
|
+
error('Invalid compatibility.aw.minVersion: expected a strict SemVer 2.0.0 string. Received undefined.');
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const compatibility = expectRecord(manifest.compatibility, 'compatibility');
|
|
2104
|
+
if (!compatibility) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
validateAllowedKeys(compatibility, 'compatibility', ['aw']);
|
|
2108
|
+
if (compatibility.aw === undefined) {
|
|
2109
|
+
error('Invalid compatibility.aw.minVersion: expected a strict SemVer 2.0.0 string. Received undefined.');
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
const aw = expectRecord(compatibility.aw, 'compatibility.aw');
|
|
2113
|
+
if (!aw) {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
validateAllowedKeys(aw, 'compatibility.aw', ['minVersion']);
|
|
2117
|
+
const pluginMinVersion = validateStrictSemVerString(aw.minVersion, 'compatibility.aw.minVersion');
|
|
2118
|
+
if (!pluginMinVersion || !options.catalogMinVersion) {
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
if (compareStrictSemVer(pluginMinVersion, options.catalogMinVersion) > 0) {
|
|
2122
|
+
error(`Invalid compatibility.aw.minVersion for plugin "${pluginNameForError(manifest)}": received ${JSON.stringify(pluginMinVersion)}, which is higher than target catalog minimum ${JSON.stringify(options.catalogMinVersion)}.`);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
function collectDeclaredSkillNames(pluginDir, manifest) {
|
|
2126
|
+
const skillsRoot = typeof manifest.skills === 'string'
|
|
2127
|
+
? path.join(pluginDir, manifest.skills)
|
|
2128
|
+
: null;
|
|
2129
|
+
const staticSkillNames = skillsRoot && fs.existsSync(skillsRoot)
|
|
2130
|
+
? fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
2131
|
+
.filter((entry) => entry.isDirectory())
|
|
2132
|
+
.map((entry) => entry.name)
|
|
2133
|
+
.sort()
|
|
2134
|
+
: [];
|
|
2135
|
+
const staticSkillNameSet = new Set(staticSkillNames);
|
|
2136
|
+
const artifactTargets = Array.isArray(manifest.artifacts?.targets)
|
|
2137
|
+
? manifest.artifacts.targets
|
|
2138
|
+
: [];
|
|
2139
|
+
const dynamicSkillNames = artifactTargets
|
|
2140
|
+
.filter((target) => target
|
|
2141
|
+
&& typeof target === 'object'
|
|
2142
|
+
&& (target.target === 'codex.skill' || target.target === 'claude.skill')
|
|
2143
|
+
&& typeof target.skillKey === 'string'
|
|
2144
|
+
&& target.skillKey.trim() !== '')
|
|
2145
|
+
.map((target) => target.skillKey.trim())
|
|
2146
|
+
.sort();
|
|
2147
|
+
const dynamicSkillNameSet = new Set(dynamicSkillNames);
|
|
2148
|
+
const portableRoot = typeof manifest.distributions?.agentSkills?.skills === 'string'
|
|
2149
|
+
? path.join(pluginDir, manifest.distributions.agentSkills.skills)
|
|
2150
|
+
: null;
|
|
2151
|
+
const portableRootStats = portableRoot
|
|
2152
|
+
? fs.lstatSync(portableRoot, { throwIfNoEntry: false })
|
|
2153
|
+
: null;
|
|
2154
|
+
const portableSkillNameSet = new Set(portableRootStats?.isDirectory() && !portableRootStats.isSymbolicLink()
|
|
2155
|
+
? fs.readdirSync(portableRoot, { withFileTypes: true })
|
|
2156
|
+
.filter((entry) => entry.isDirectory())
|
|
2157
|
+
.map((entry) => entry.name)
|
|
2158
|
+
: []);
|
|
2159
|
+
return {
|
|
2160
|
+
static: staticSkillNameSet,
|
|
2161
|
+
dynamic: dynamicSkillNameSet,
|
|
2162
|
+
portable: portableSkillNameSet,
|
|
2163
|
+
all: new Set([...staticSkillNames, ...dynamicSkillNames]),
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
function isNodeRunnablePath(relativePath) {
|
|
2167
|
+
return SCRIPT_NODE_EXTENSIONS.has(path.posix.extname(normalizeRelativePath(relativePath)));
|
|
2168
|
+
}
|
|
2169
|
+
function isAutoRunnablePath(relativePath) {
|
|
2170
|
+
return SCRIPT_AUTO_EXTENSIONS.has(path.posix.extname(normalizeRelativePath(relativePath)).toLowerCase());
|
|
2171
|
+
}
|
|
2172
|
+
function normalizePluginScriptPath(pluginDir, value, fieldName) {
|
|
2173
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
2174
|
+
error(`${fieldName} must be a non-empty string`);
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
if (/\s|[;&|<>`]/.test(value) || value.includes('$(')) {
|
|
2178
|
+
error(`${fieldName} must be a file path, not an inline command`);
|
|
2179
|
+
return null;
|
|
2180
|
+
}
|
|
2181
|
+
if (!value.startsWith('./')) {
|
|
2182
|
+
error(`${fieldName} must start with "./"`);
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
if (path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
2186
|
+
error(`${fieldName} must be relative to the plugin root`);
|
|
2187
|
+
return null;
|
|
2188
|
+
}
|
|
2189
|
+
const relativePath = normalizeRelativePath(value);
|
|
2190
|
+
if (relativePath === '.' || relativePath === '..' || relativePath.startsWith('../')) {
|
|
2191
|
+
error(`${fieldName} must stay inside the plugin root`);
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2194
|
+
const absolutePath = path.resolve(pluginDir, relativePath);
|
|
2195
|
+
const relativeFromRoot = path.relative(pluginDir, absolutePath);
|
|
2196
|
+
if (relativeFromRoot.startsWith('..') || path.isAbsolute(relativeFromRoot)) {
|
|
2197
|
+
error(`${fieldName} must stay inside the plugin root`);
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
return relativePath;
|
|
2201
|
+
}
|
|
2202
|
+
function validateDeclaredScriptFile(pluginDir, relativePath, fieldName) {
|
|
2203
|
+
if (!relativePath)
|
|
2204
|
+
return;
|
|
2205
|
+
const absolutePath = path.join(pluginDir, relativePath);
|
|
2206
|
+
const stats = fs.statSync(absolutePath, { throwIfNoEntry: false });
|
|
2207
|
+
if (!stats?.isFile()) {
|
|
2208
|
+
error(`${fieldName} points to "${relativePath}" but the file does not exist`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
function validateScriptEntry(pluginDir, value, fieldName, options = {}) {
|
|
2212
|
+
const entry = expectRecord(value, fieldName);
|
|
2213
|
+
if (!entry)
|
|
2214
|
+
return null;
|
|
2215
|
+
if ('command' in entry) {
|
|
2216
|
+
error(`${fieldName}.command is not supported; declare runner/path instead`);
|
|
2217
|
+
}
|
|
2218
|
+
validateAllowedKeys(entry, fieldName, ['runner', 'path', 'args']);
|
|
2219
|
+
const runner = validateManifestString(entry.runner, `${fieldName}.runner`);
|
|
2220
|
+
if (runner && !SCRIPT_RUNNERS.has(runner)) {
|
|
2221
|
+
error(`${fieldName}.runner has unsupported runner "${runner}"`);
|
|
2222
|
+
}
|
|
2223
|
+
if (options.requireNodeRunner && runner !== 'node') {
|
|
2224
|
+
error(`${fieldName}.runner must be "node"`);
|
|
2225
|
+
}
|
|
2226
|
+
const relativePath = normalizePluginScriptPath(pluginDir, entry.path, `${fieldName}.path`);
|
|
2227
|
+
validateDeclaredScriptFile(pluginDir, relativePath, `${fieldName}.path`);
|
|
2228
|
+
if (runner === 'auto' && relativePath && !isAutoRunnablePath(relativePath)) {
|
|
2229
|
+
error(`${fieldName}.path uses runner "auto" but has no supported extension (${[...SCRIPT_AUTO_EXTENSIONS].join(', ')})`);
|
|
2230
|
+
}
|
|
2231
|
+
if (options.requireNodePath && relativePath && !isNodeRunnablePath(relativePath)) {
|
|
2232
|
+
error(`${fieldName}.path must use a Node runnable extension (.js, .mjs, or .cjs)`);
|
|
2233
|
+
}
|
|
2234
|
+
if (entry.args !== undefined) {
|
|
2235
|
+
if (!Array.isArray(entry.args)) {
|
|
2236
|
+
error(`${fieldName}.args must be an array of strings`);
|
|
2237
|
+
}
|
|
2238
|
+
else {
|
|
2239
|
+
entry.args.forEach((arg, index) => {
|
|
2240
|
+
if (typeof arg !== 'string') {
|
|
2241
|
+
error(`${fieldName}.args[${index}] must be a string`);
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return runner && relativePath ? { runner, path: relativePath } : null;
|
|
2247
|
+
}
|
|
2248
|
+
function validateLifecycleDefinition(pluginDir, value, fieldName, runtimeEntries) {
|
|
2249
|
+
if (typeof value === 'string') {
|
|
2250
|
+
const relativePath = normalizePluginScriptPath(pluginDir, value, fieldName);
|
|
2251
|
+
validateDeclaredScriptFile(pluginDir, relativePath, fieldName);
|
|
2252
|
+
if (relativePath && !isAutoRunnablePath(relativePath)) {
|
|
2253
|
+
error(`${fieldName} uses runner "auto" but has no supported extension (${[...SCRIPT_AUTO_EXTENSIONS].join(', ')})`);
|
|
2254
|
+
}
|
|
2255
|
+
if (relativePath && isNodeRunnablePath(relativePath)) {
|
|
2256
|
+
runtimeEntries.push({ path: relativePath, fieldName });
|
|
2257
|
+
}
|
|
2258
|
+
return { default: relativePath ? { runner: 'auto', path: relativePath } : null };
|
|
2259
|
+
}
|
|
2260
|
+
const selector = expectRecord(value, fieldName);
|
|
2261
|
+
if (!selector)
|
|
2262
|
+
return {};
|
|
2263
|
+
if ('runner' in selector || 'path' in selector || 'command' in selector) {
|
|
2264
|
+
error(`${fieldName} must declare lifecycle scripts under OS selectors`);
|
|
2265
|
+
}
|
|
2266
|
+
for (const selectorName of Object.keys(selector)) {
|
|
2267
|
+
if (!SCRIPT_OS_SELECTORS.has(selectorName)) {
|
|
2268
|
+
error(`${fieldName} has unknown OS selector "${selectorName}"`);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
const normalized = {};
|
|
2272
|
+
for (const [selectorName, entryValue] of Object.entries(selector)) {
|
|
2273
|
+
const entry = validateScriptEntry(pluginDir, entryValue, `${fieldName}.${selectorName}`);
|
|
2274
|
+
if (entry) {
|
|
2275
|
+
normalized[selectorName] = entry;
|
|
2276
|
+
if (entry.runner === 'node' || (entry.runner === 'auto' && isNodeRunnablePath(entry.path))) {
|
|
2277
|
+
runtimeEntries.push({ path: entry.path, fieldName: `${fieldName}.${selectorName}.path` });
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
const orders = {
|
|
2282
|
+
linux: ['linux', 'unix', 'default'],
|
|
2283
|
+
darwin: ['darwin', 'unix', 'default'],
|
|
2284
|
+
win32: ['win32', 'windows', 'default'],
|
|
2285
|
+
};
|
|
2286
|
+
for (const [osName, order] of Object.entries(orders)) {
|
|
2287
|
+
if (!order.some((selectorName) => normalized[selectorName])) {
|
|
2288
|
+
error(`${fieldName} must include an entry that resolves on ${osName}`);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return normalized;
|
|
2292
|
+
}
|
|
2293
|
+
function isNonRegistryDependencySpecifier(specifier) {
|
|
2294
|
+
const trimmed = String(specifier).trim();
|
|
2295
|
+
return (/^(?:workspace|file|link|portal):/i.test(trimmed) ||
|
|
2296
|
+
/^(?:git\+|git:|ssh:)/i.test(trimmed) ||
|
|
2297
|
+
/^https?:\/\//i.test(trimmed) ||
|
|
2298
|
+
/(?:\.tgz|\.tar\.gz)(?:[#?].*)?$/i.test(trimmed) ||
|
|
2299
|
+
trimmed.startsWith('./') ||
|
|
2300
|
+
trimmed.startsWith('../') ||
|
|
2301
|
+
trimmed.startsWith('/') ||
|
|
2302
|
+
trimmed.startsWith('~') ||
|
|
2303
|
+
/^[\w.-]+\/[\w.-]+(?:#.*)?$/.test(trimmed));
|
|
2304
|
+
}
|
|
2305
|
+
function readPackageDependencyNames(packageJson, fieldName, dependencyField) {
|
|
2306
|
+
const rawDependencies = packageJson[dependencyField];
|
|
2307
|
+
if (rawDependencies === undefined)
|
|
2308
|
+
return [];
|
|
2309
|
+
const dependencies = expectRecord(rawDependencies, `${fieldName}.${dependencyField}`);
|
|
2310
|
+
if (!dependencies)
|
|
2311
|
+
return [];
|
|
2312
|
+
const names = [];
|
|
2313
|
+
for (const [name, specifier] of Object.entries(dependencies)) {
|
|
2314
|
+
if (typeof specifier !== 'string') {
|
|
2315
|
+
error(`${fieldName}.${dependencyField}.${name} must be a string specifier`);
|
|
2316
|
+
continue;
|
|
2317
|
+
}
|
|
2318
|
+
if (isNonRegistryDependencySpecifier(specifier)) {
|
|
2319
|
+
error(`${fieldName}.${dependencyField}.${name} uses non registry-backed specifier "${specifier}"`);
|
|
2320
|
+
}
|
|
2321
|
+
names.push(name);
|
|
2322
|
+
}
|
|
2323
|
+
return names;
|
|
2324
|
+
}
|
|
2325
|
+
function validatePackageRoot(pluginDir, packageRootValue, fieldName) {
|
|
2326
|
+
const packageRoot = normalizePluginScriptPath(pluginDir, packageRootValue, fieldName);
|
|
2327
|
+
if (!packageRoot)
|
|
2328
|
+
return null;
|
|
2329
|
+
const absolutePackageRoot = path.join(pluginDir, packageRoot);
|
|
2330
|
+
const stats = fs.statSync(absolutePackageRoot, { throwIfNoEntry: false });
|
|
2331
|
+
if (!stats?.isDirectory()) {
|
|
2332
|
+
error(`${fieldName} must point to a package directory`);
|
|
2333
|
+
return { packageRoot, dependencies: [], devDependencies: [] };
|
|
2334
|
+
}
|
|
2335
|
+
for (const lockfile of ['package-lock.json', 'yarn.lock', 'bun.lock', 'bun.lockb']) {
|
|
2336
|
+
if (fs.existsSync(path.join(absolutePackageRoot, lockfile))) {
|
|
2337
|
+
error(`${fieldName} only supports pnpm-lock.yaml, found ${lockfile}`);
|
|
2338
|
+
}
|
|
2339
|
+
}
|
|
2340
|
+
const packageJsonPath = path.join(absolutePackageRoot, 'package.json');
|
|
2341
|
+
const lockfilePath = path.join(absolutePackageRoot, 'pnpm-lock.yaml');
|
|
2342
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
2343
|
+
error(`${fieldName} is missing package.json`);
|
|
2344
|
+
}
|
|
2345
|
+
if (!fs.existsSync(lockfilePath)) {
|
|
2346
|
+
error(`${fieldName} is missing pnpm-lock.yaml`);
|
|
2347
|
+
}
|
|
2348
|
+
let packageJson = {};
|
|
2349
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
2350
|
+
try {
|
|
2351
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
2352
|
+
}
|
|
2353
|
+
catch (e) {
|
|
2354
|
+
error(`${fieldName}/package.json is not valid JSON: ${e.message}`);
|
|
2355
|
+
packageJson = {};
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (packageJson.optionalDependencies !== undefined) {
|
|
2359
|
+
error(`${fieldName}/package.json optionalDependencies are not supported`);
|
|
2360
|
+
}
|
|
2361
|
+
if (packageJson.bundledDependencies !== undefined || packageJson.bundleDependencies !== undefined) {
|
|
2362
|
+
error(`${fieldName}/package.json bundledDependencies are not supported`);
|
|
2363
|
+
}
|
|
2364
|
+
return {
|
|
2365
|
+
packageRoot,
|
|
2366
|
+
dependencies: readPackageDependencyNames(packageJson, fieldName, 'dependencies'),
|
|
2367
|
+
devDependencies: readPackageDependencyNames(packageJson, fieldName, 'devDependencies'),
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
2370
|
+
function isRelativeSpecifier(specifier) {
|
|
2371
|
+
return specifier === '.' || specifier === '..' || specifier.startsWith('./') || specifier.startsWith('../');
|
|
2372
|
+
}
|
|
2373
|
+
function packageNameFromSpecifier(specifier) {
|
|
2374
|
+
if (specifier.startsWith('@')) {
|
|
2375
|
+
const [scope, name] = specifier.split('/');
|
|
2376
|
+
return `${scope}/${name ?? ''}`;
|
|
2377
|
+
}
|
|
2378
|
+
return specifier.split('/')[0];
|
|
2379
|
+
}
|
|
2380
|
+
function resolveRelativeJavaScriptImport(importer, specifier, pluginDir) {
|
|
2381
|
+
const importerDir = path.posix.dirname(importer);
|
|
2382
|
+
const normalized = path.posix.normalize(path.posix.join(importerDir, specifier));
|
|
2383
|
+
if (normalized === '..' || normalized.startsWith('../') || path.posix.isAbsolute(normalized)) {
|
|
2384
|
+
return { escaped: true, path: normalized };
|
|
2385
|
+
}
|
|
2386
|
+
const candidates = [
|
|
2387
|
+
normalized,
|
|
2388
|
+
`${normalized}.js`,
|
|
2389
|
+
`${normalized}.mjs`,
|
|
2390
|
+
`${normalized}.cjs`,
|
|
2391
|
+
path.posix.join(normalized, 'index.js'),
|
|
2392
|
+
path.posix.join(normalized, 'index.mjs'),
|
|
2393
|
+
path.posix.join(normalized, 'index.cjs'),
|
|
2394
|
+
];
|
|
2395
|
+
const found = candidates.find((candidate) => fs.existsSync(path.join(pluginDir, candidate)));
|
|
2396
|
+
return found ? { escaped: false, path: found } : { escaped: false, path: null };
|
|
2397
|
+
}
|
|
2398
|
+
function pathIsUnderPackageRoot(relativePath, packageRoot) {
|
|
2399
|
+
if (!packageRoot)
|
|
2400
|
+
return false;
|
|
2401
|
+
return relativePath === packageRoot || relativePath.startsWith(`${packageRoot}/`);
|
|
2402
|
+
}
|
|
2403
|
+
function scanNodeImportContract(pluginDir, entryPaths, options) {
|
|
2404
|
+
const queue = [...new Set(entryPaths.filter(Boolean).map(normalizeRelativePath))];
|
|
2405
|
+
const visited = new Set();
|
|
2406
|
+
const dependencies = new Set(options.packageMeta?.dependencies ?? []);
|
|
2407
|
+
const devDependencies = new Set(options.packageMeta?.devDependencies ?? []);
|
|
2408
|
+
const packageRoot = options.packageMeta?.packageRoot;
|
|
2409
|
+
while (queue.length > 0) {
|
|
2410
|
+
const relativePath = queue.shift();
|
|
2411
|
+
if (!relativePath || visited.has(relativePath))
|
|
2412
|
+
continue;
|
|
2413
|
+
visited.add(relativePath);
|
|
2414
|
+
const absolutePath = path.join(pluginDir, relativePath);
|
|
2415
|
+
if (!fs.existsSync(absolutePath)) {
|
|
2416
|
+
error(`${options.label}: missing Node import file "${relativePath}"`);
|
|
2417
|
+
continue;
|
|
2418
|
+
}
|
|
2419
|
+
const source = fs.readFileSync(absolutePath, 'utf8');
|
|
2420
|
+
for (const { specifier } of scanJavaScriptImportSpecifiers(source)) {
|
|
2421
|
+
if (specifier.startsWith('node:'))
|
|
2422
|
+
continue;
|
|
2423
|
+
if (isRelativeSpecifier(specifier)) {
|
|
2424
|
+
const resolved = resolveRelativeJavaScriptImport(relativePath, specifier, pluginDir);
|
|
2425
|
+
if (resolved.escaped) {
|
|
2426
|
+
error(`${options.label}: relative import "${specifier}" from "${relativePath}" escapes the plugin root`);
|
|
2427
|
+
continue;
|
|
2428
|
+
}
|
|
2429
|
+
if (!resolved.path) {
|
|
2430
|
+
error(`${options.label}: unable to resolve relative import "${specifier}" from "${relativePath}"`);
|
|
2431
|
+
continue;
|
|
2432
|
+
}
|
|
2433
|
+
queue.push(resolved.path);
|
|
2434
|
+
continue;
|
|
2435
|
+
}
|
|
2436
|
+
if (specifier.startsWith('/') || specifier.includes(':')) {
|
|
2437
|
+
error(`${options.label}: unsupported import specifier "${specifier}" from "${relativePath}"`);
|
|
2438
|
+
continue;
|
|
2439
|
+
}
|
|
2440
|
+
const packageName = packageNameFromSpecifier(specifier);
|
|
2441
|
+
if (!packageRoot) {
|
|
2442
|
+
error(`${options.label}: bare import "${specifier}" from "${relativePath}" requires scripts.node.packageRoot`);
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
if (!pathIsUnderPackageRoot(relativePath, packageRoot)) {
|
|
2446
|
+
error(`${options.label}: bare import "${specifier}" from "${relativePath}" is outside scripts.node.packageRoot`);
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
if (dependencies.has(packageName))
|
|
2450
|
+
continue;
|
|
2451
|
+
if (devDependencies.has(packageName)) {
|
|
2452
|
+
if (!options.allowDevDependencies) {
|
|
2453
|
+
error(`${options.label}: runtime import "${specifier}" from "${relativePath}" uses dev dependency "${packageName}"`);
|
|
2454
|
+
}
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
error(`${options.label}: import "${specifier}" from "${relativePath}" is not declared in dependencies`);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
function listFilesRecursive(rootDir) {
|
|
2462
|
+
const output = [];
|
|
2463
|
+
if (!fs.existsSync(rootDir))
|
|
2464
|
+
return output;
|
|
2465
|
+
const walk = (dir) => {
|
|
2466
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2467
|
+
const absolutePath = path.join(dir, entry.name);
|
|
2468
|
+
if (entry.isDirectory()) {
|
|
2469
|
+
walk(absolutePath);
|
|
2470
|
+
}
|
|
2471
|
+
else if (entry.isFile()) {
|
|
2472
|
+
output.push(normalizeRelativePath(path.relative(rootDir, absolutePath)));
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
walk(rootDir);
|
|
2477
|
+
return output;
|
|
2478
|
+
}
|
|
2479
|
+
function validateConfigValueLightweight(field, value, fieldName) {
|
|
2480
|
+
if (!isRecord(field) || !CONFIG_FIELD_TYPES.has(field.type))
|
|
2481
|
+
return;
|
|
2482
|
+
if (field.type === 'string' || field.type === 'file' || field.type === 'directory') {
|
|
2483
|
+
validateManifestString(value, fieldName);
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
if (field.type === 'boolean') {
|
|
2487
|
+
if (typeof value !== 'boolean') {
|
|
2488
|
+
error(`${fieldName} must be a boolean`);
|
|
2489
|
+
}
|
|
2490
|
+
return;
|
|
2491
|
+
}
|
|
2492
|
+
if (field.type === 'number') {
|
|
2493
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2494
|
+
error(`${fieldName} must be a finite number`);
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
if (typeof field.min === 'number' && value < field.min) {
|
|
2498
|
+
error(`${fieldName} must be greater than or equal to ${field.min}`);
|
|
2499
|
+
}
|
|
2500
|
+
if (typeof field.max === 'number' && value > field.max) {
|
|
2501
|
+
error(`${fieldName} must be less than or equal to ${field.max}`);
|
|
2502
|
+
}
|
|
2503
|
+
return;
|
|
2504
|
+
}
|
|
2505
|
+
if (field.type === 'enum') {
|
|
2506
|
+
if (typeof value !== 'string' || !Array.isArray(field.values) || !field.values.includes(value)) {
|
|
2507
|
+
error(`${fieldName} must be one of config schema values`);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
function validateConfigPresentationLightweight(presentation, schema) {
|
|
2512
|
+
if (presentation === undefined)
|
|
2513
|
+
return;
|
|
2514
|
+
const root = expectRecord(presentation, 'config.presentation');
|
|
2515
|
+
if (!root)
|
|
2516
|
+
return;
|
|
2517
|
+
validateAllowedKeys(root, 'config.presentation', ['groups', 'fields', 'presets']);
|
|
2518
|
+
const schemaKeys = new Set(Object.keys(schema));
|
|
2519
|
+
if (root.groups !== undefined) {
|
|
2520
|
+
if (!Array.isArray(root.groups)) {
|
|
2521
|
+
error('config.presentation.groups must be an array');
|
|
2522
|
+
}
|
|
2523
|
+
else {
|
|
2524
|
+
const groupIds = new Set();
|
|
2525
|
+
const groupedFields = new Set();
|
|
2526
|
+
root.groups.forEach((rawGroup, index) => {
|
|
2527
|
+
const fieldName = `config.presentation.groups[${index}]`;
|
|
2528
|
+
const group = expectRecord(rawGroup, fieldName);
|
|
2529
|
+
if (!group)
|
|
2530
|
+
return;
|
|
2531
|
+
validateAllowedKeys(group, fieldName, ['id', 'title', 'description', 'fields', 'advanced']);
|
|
2532
|
+
const validId = validateManifestString(group.id, `${fieldName}.id`);
|
|
2533
|
+
const id = validId ? group.id : null;
|
|
2534
|
+
if (id && groupIds.has(id)) {
|
|
2535
|
+
error(`${fieldName}.id has duplicate group id "${id}"`);
|
|
2536
|
+
}
|
|
2537
|
+
else if (id) {
|
|
2538
|
+
groupIds.add(id);
|
|
2539
|
+
}
|
|
2540
|
+
validateManifestString(group.title, `${fieldName}.title`);
|
|
2541
|
+
if (group.description !== undefined) {
|
|
2542
|
+
validateManifestString(group.description, `${fieldName}.description`);
|
|
2543
|
+
}
|
|
2544
|
+
if (group.advanced !== undefined && typeof group.advanced !== 'boolean') {
|
|
2545
|
+
error(`${fieldName}.advanced must be a boolean`);
|
|
2546
|
+
}
|
|
2547
|
+
if (!Array.isArray(group.fields)) {
|
|
2548
|
+
error(`${fieldName}.fields must be an array`);
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
group.fields.forEach((key, fieldIndex) => {
|
|
2552
|
+
const keyFieldName = `${fieldName}.fields[${fieldIndex}]`;
|
|
2553
|
+
const validKey = validateManifestString(key, keyFieldName);
|
|
2554
|
+
if (!validKey)
|
|
2555
|
+
return;
|
|
2556
|
+
if (!schemaKeys.has(key)) {
|
|
2557
|
+
error(`${keyFieldName} references unknown config key "${key}"`);
|
|
2558
|
+
}
|
|
2559
|
+
if (groupedFields.has(key)) {
|
|
2560
|
+
error(`${keyFieldName} assigns config key "${key}" to more than one group`);
|
|
2561
|
+
}
|
|
2562
|
+
else {
|
|
2563
|
+
groupedFields.add(key);
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
if (root.fields !== undefined) {
|
|
2570
|
+
const fields = expectRecord(root.fields, 'config.presentation.fields');
|
|
2571
|
+
if (fields) {
|
|
2572
|
+
for (const [key, rawField] of Object.entries(fields)) {
|
|
2573
|
+
const fieldName = `config.presentation.fields.${key}`;
|
|
2574
|
+
const schemaField = schema[key];
|
|
2575
|
+
if (!schemaKeys.has(key)) {
|
|
2576
|
+
error(`config.presentation.fields references unknown config key "${key}"`);
|
|
2577
|
+
}
|
|
2578
|
+
const field = expectRecord(rawField, fieldName);
|
|
2579
|
+
if (!field)
|
|
2580
|
+
continue;
|
|
2581
|
+
validateAllowedKeys(field, fieldName, [
|
|
2582
|
+
'valueLabels',
|
|
2583
|
+
'recommendedValue',
|
|
2584
|
+
'placeholder',
|
|
2585
|
+
'unit',
|
|
2586
|
+
'advanced',
|
|
2587
|
+
]);
|
|
2588
|
+
if (field.placeholder !== undefined) {
|
|
2589
|
+
validateManifestString(field.placeholder, `${fieldName}.placeholder`);
|
|
2590
|
+
}
|
|
2591
|
+
if (field.unit !== undefined) {
|
|
2592
|
+
validateManifestString(field.unit, `${fieldName}.unit`);
|
|
2593
|
+
}
|
|
2594
|
+
if (field.advanced !== undefined && typeof field.advanced !== 'boolean') {
|
|
2595
|
+
error(`${fieldName}.advanced must be a boolean`);
|
|
2596
|
+
}
|
|
2597
|
+
if (field.valueLabels !== undefined) {
|
|
2598
|
+
const labels = expectRecord(field.valueLabels, `${fieldName}.valueLabels`);
|
|
2599
|
+
if (isRecord(schemaField) && schemaField.type !== 'enum') {
|
|
2600
|
+
error(`${fieldName}.valueLabels is only supported for enum config`);
|
|
2601
|
+
}
|
|
2602
|
+
if (labels) {
|
|
2603
|
+
for (const [token, label] of Object.entries(labels)) {
|
|
2604
|
+
if (!isRecord(schemaField) || !Array.isArray(schemaField.values) || !schemaField.values.includes(token)) {
|
|
2605
|
+
error(`${fieldName}.valueLabels references unknown enum token "${token}"`);
|
|
2606
|
+
}
|
|
2607
|
+
validateManifestString(label, `${fieldName}.valueLabels.${token}`);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (field.recommendedValue !== undefined) {
|
|
2612
|
+
validateConfigValueLightweight(schemaField, field.recommendedValue, `${fieldName}.recommendedValue`);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
if (root.presets !== undefined) {
|
|
2618
|
+
if (!Array.isArray(root.presets)) {
|
|
2619
|
+
error('config.presentation.presets must be an array');
|
|
2620
|
+
}
|
|
2621
|
+
else {
|
|
2622
|
+
const presetIds = new Set();
|
|
2623
|
+
root.presets.forEach((rawPreset, index) => {
|
|
2624
|
+
const fieldName = `config.presentation.presets[${index}]`;
|
|
2625
|
+
const preset = expectRecord(rawPreset, fieldName);
|
|
2626
|
+
if (!preset)
|
|
2627
|
+
return;
|
|
2628
|
+
validateAllowedKeys(preset, fieldName, ['id', 'title', 'description', 'values']);
|
|
2629
|
+
const validId = validateManifestString(preset.id, `${fieldName}.id`);
|
|
2630
|
+
const id = validId ? preset.id : null;
|
|
2631
|
+
if (id && presetIds.has(id)) {
|
|
2632
|
+
error(`${fieldName}.id has duplicate preset id "${id}"`);
|
|
2633
|
+
}
|
|
2634
|
+
else if (id) {
|
|
2635
|
+
presetIds.add(id);
|
|
2636
|
+
}
|
|
2637
|
+
validateManifestString(preset.title, `${fieldName}.title`);
|
|
2638
|
+
if (preset.description !== undefined) {
|
|
2639
|
+
validateManifestString(preset.description, `${fieldName}.description`);
|
|
2640
|
+
}
|
|
2641
|
+
const values = expectRecord(preset.values, `${fieldName}.values`);
|
|
2642
|
+
if (!values)
|
|
2643
|
+
return;
|
|
2644
|
+
for (const [key, value] of Object.entries(values)) {
|
|
2645
|
+
if (!schemaKeys.has(key)) {
|
|
2646
|
+
error(`${fieldName}.values references unknown config key "${key}"`);
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
const schemaField = schema[key];
|
|
2650
|
+
if (isRecord(schemaField)
|
|
2651
|
+
&& (schemaField.sensitive === true
|
|
2652
|
+
|| schemaField.type === 'file'
|
|
2653
|
+
|| schemaField.type === 'directory')) {
|
|
2654
|
+
error(`${fieldName}.values.${key} cannot contain sensitive, file, or directory config values`);
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
validateConfigValueLightweight(schemaField, value, `${fieldName}.values.${key}`);
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
function validateConfigSchemaLightweight(config) {
|
|
2664
|
+
if (config === undefined)
|
|
2665
|
+
return;
|
|
2666
|
+
const configRoot = expectRecord(config, 'config');
|
|
2667
|
+
if (!configRoot)
|
|
2668
|
+
return;
|
|
2669
|
+
validateAllowedKeys(configRoot, 'config', ['schema', 'presentation']);
|
|
2670
|
+
const schema = expectRecord(configRoot.schema, 'config.schema');
|
|
2671
|
+
if (!schema)
|
|
2672
|
+
return;
|
|
2673
|
+
for (const [key, rawField] of Object.entries(schema)) {
|
|
2674
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(key)) {
|
|
2675
|
+
error(`config.schema has invalid key "${key}"`);
|
|
2676
|
+
}
|
|
2677
|
+
const fieldName = `config.schema.${key}`;
|
|
2678
|
+
const field = expectRecord(rawField, fieldName);
|
|
2679
|
+
if (!field)
|
|
2680
|
+
continue;
|
|
2681
|
+
validateAllowedKeys(field, fieldName, [
|
|
2682
|
+
'type',
|
|
2683
|
+
'title',
|
|
2684
|
+
'description',
|
|
2685
|
+
'default',
|
|
2686
|
+
'required',
|
|
2687
|
+
'sensitive',
|
|
2688
|
+
'values',
|
|
2689
|
+
'min',
|
|
2690
|
+
'max',
|
|
2691
|
+
]);
|
|
2692
|
+
if (typeof field.type !== 'string' || !CONFIG_FIELD_TYPES.has(field.type)) {
|
|
2693
|
+
error(`${fieldName}.type must be string, number, boolean, enum, file, or directory`);
|
|
2694
|
+
}
|
|
2695
|
+
if (field.required !== undefined && typeof field.required !== 'boolean') {
|
|
2696
|
+
error(`${fieldName}.required must be a boolean`);
|
|
2697
|
+
}
|
|
2698
|
+
if (field.sensitive !== undefined && typeof field.sensitive !== 'boolean') {
|
|
2699
|
+
error(`${fieldName}.sensitive must be a boolean`);
|
|
2700
|
+
}
|
|
2701
|
+
if (field.values !== undefined) {
|
|
2702
|
+
if (field.type !== 'enum' || !Array.isArray(field.values) || field.values.length === 0) {
|
|
2703
|
+
error(`${fieldName}.values is only supported for enum config and must be non-empty`);
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
else if (field.type === 'enum') {
|
|
2707
|
+
error(`${fieldName}.values is required for enum config`);
|
|
2708
|
+
}
|
|
2709
|
+
if (field.type !== 'number' && (field.min !== undefined || field.max !== undefined)) {
|
|
2710
|
+
error(`${fieldName}.min/max are only supported for number config`);
|
|
2711
|
+
}
|
|
2712
|
+
if (field.min !== undefined && typeof field.min !== 'number') {
|
|
2713
|
+
error(`${fieldName}.min must be a number`);
|
|
2714
|
+
}
|
|
2715
|
+
if (field.max !== undefined && typeof field.max !== 'number') {
|
|
2716
|
+
error(`${fieldName}.max must be a number`);
|
|
2717
|
+
}
|
|
2718
|
+
if (typeof field.min === 'number' && typeof field.max === 'number' && field.min > field.max) {
|
|
2719
|
+
error(`${fieldName}.min must be less than or equal to max`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
validateConfigPresentationLightweight(configRoot.presentation, schema);
|
|
2723
|
+
}
|
|
2724
|
+
function configSchemaKeys(manifest) {
|
|
2725
|
+
const schema = manifest.config?.schema;
|
|
2726
|
+
return isRecord(schema) ? new Set(Object.keys(schema)) : new Set();
|
|
2727
|
+
}
|
|
2728
|
+
function configSchemaField(manifest, key) {
|
|
2729
|
+
const field = manifest.config?.schema?.[key];
|
|
2730
|
+
return isRecord(field) ? field : null;
|
|
2731
|
+
}
|
|
2732
|
+
function defaultConfigValues(manifest) {
|
|
2733
|
+
const schema = manifest.config?.schema;
|
|
2734
|
+
const values = {};
|
|
2735
|
+
if (!isRecord(schema))
|
|
2736
|
+
return values;
|
|
2737
|
+
for (const [key, field] of Object.entries(schema)) {
|
|
2738
|
+
if (isRecord(field) && Object.prototype.hasOwnProperty.call(field, 'default')) {
|
|
2739
|
+
values[key] = field.default;
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
return values;
|
|
2743
|
+
}
|
|
2744
|
+
function validateArtifactId(value, fieldName) {
|
|
2745
|
+
const id = validateManifestString(value, fieldName);
|
|
2746
|
+
if (!id)
|
|
2747
|
+
return null;
|
|
2748
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]*$/.test(id)) {
|
|
2749
|
+
error(`${fieldName}: expected a stable id using letters, numbers, ".", "_" or "-"`);
|
|
2750
|
+
}
|
|
2751
|
+
return id;
|
|
2752
|
+
}
|
|
2753
|
+
function validateCodexRoleKey(value, fieldName) {
|
|
2754
|
+
const roleKey = validateManifestString(value, fieldName);
|
|
2755
|
+
if (!roleKey)
|
|
2756
|
+
return null;
|
|
2757
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(roleKey)) {
|
|
2758
|
+
error(`${fieldName}: expected a valid Codex role key`);
|
|
2759
|
+
}
|
|
2760
|
+
return roleKey;
|
|
2761
|
+
}
|
|
2762
|
+
function validateCodexSkillKey(value, fieldName) {
|
|
2763
|
+
const skillKey = validateManifestString(value, fieldName);
|
|
2764
|
+
if (!skillKey)
|
|
2765
|
+
return null;
|
|
2766
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(skillKey)) {
|
|
2767
|
+
error(`${fieldName}: expected a valid Codex skill key`);
|
|
2768
|
+
}
|
|
2769
|
+
return skillKey;
|
|
2770
|
+
}
|
|
2771
|
+
function validatePortableSkillKey(value, fieldName) {
|
|
2772
|
+
const skillKey = validateManifestString(value, fieldName);
|
|
2773
|
+
if (!skillKey)
|
|
2774
|
+
return null;
|
|
2775
|
+
if (skillKey.length > MAX_SKILL_NAME_LEN || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(skillKey)) {
|
|
2776
|
+
error(`${fieldName}: expected at most 64 ASCII lowercase hyphen-case characters`);
|
|
2777
|
+
}
|
|
2778
|
+
return skillKey;
|
|
2779
|
+
}
|
|
2780
|
+
function validateStringArray(value, fieldName, options = {}) {
|
|
2781
|
+
if (!Array.isArray(value)) {
|
|
2782
|
+
error(`${fieldName} must be an array`);
|
|
2783
|
+
return [];
|
|
2784
|
+
}
|
|
2785
|
+
const output = [];
|
|
2786
|
+
for (const [index, entry] of value.entries()) {
|
|
2787
|
+
const itemFieldName = `${fieldName}[${index}]`;
|
|
2788
|
+
if (typeof entry !== 'string' || entry.trim() === '') {
|
|
2789
|
+
error(`${itemFieldName} must be a non-empty string`);
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
if (options.allowed && !options.allowed.has(entry)) {
|
|
2793
|
+
error(`${itemFieldName}: unsupported ${options.label ?? 'value'} "${entry}"`);
|
|
2794
|
+
continue;
|
|
2795
|
+
}
|
|
2796
|
+
if (!output.includes(entry))
|
|
2797
|
+
output.push(entry);
|
|
2798
|
+
}
|
|
2799
|
+
if (options.requireNonEmpty && output.length === 0) {
|
|
2800
|
+
error(`${fieldName} must contain at least one value`);
|
|
2801
|
+
}
|
|
2802
|
+
return output;
|
|
2803
|
+
}
|
|
2804
|
+
function validateArtifactTargetEntry(entry, index, manifest, seen, pluginDir) {
|
|
2805
|
+
const fieldName = `artifacts.targets[${index}]`;
|
|
2806
|
+
const target = expectRecord(entry, fieldName);
|
|
2807
|
+
if (!target)
|
|
2808
|
+
return null;
|
|
2809
|
+
validateAllowedKeys(target, fieldName, [
|
|
2810
|
+
'id',
|
|
2811
|
+
'platform',
|
|
2812
|
+
'distribution',
|
|
2813
|
+
'scopes',
|
|
2814
|
+
'target',
|
|
2815
|
+
'kind',
|
|
2816
|
+
'projectManagedKind',
|
|
2817
|
+
'states',
|
|
2818
|
+
'refreshKeys',
|
|
2819
|
+
'requiresCodexProjectConfig',
|
|
2820
|
+
'roleKey',
|
|
2821
|
+
'skillKey',
|
|
2822
|
+
'sourceDirs',
|
|
2823
|
+
'runtimeImports',
|
|
2824
|
+
]);
|
|
2825
|
+
const id = validateArtifactId(target.id, `${fieldName}.id`);
|
|
2826
|
+
const artifactTarget = validateManifestString(target.target, `${fieldName}.target`);
|
|
2827
|
+
if (artifactTarget && !ARTIFACT_TARGETS.has(artifactTarget)) {
|
|
2828
|
+
error(`${fieldName}.target: unsupported artifact target "${artifactTarget}"`);
|
|
2829
|
+
}
|
|
2830
|
+
const portable = artifactTarget === 'agent-skills.skill';
|
|
2831
|
+
const platform = portable || target.platform === undefined
|
|
2832
|
+
? undefined
|
|
2833
|
+
: validateManifestString(target.platform, `${fieldName}.platform`);
|
|
2834
|
+
const distribution = portable || target.distribution !== undefined
|
|
2835
|
+
? validateManifestString(target.distribution, `${fieldName}.distribution`)
|
|
2836
|
+
: undefined;
|
|
2837
|
+
if (portable) {
|
|
2838
|
+
if (distribution !== 'agent-skills') {
|
|
2839
|
+
error(`${fieldName}.distribution: expected "agent-skills"`);
|
|
2840
|
+
}
|
|
2841
|
+
if (target.platform !== undefined) {
|
|
2842
|
+
error(`${fieldName}.platform: distribution targets do not support platform`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
else {
|
|
2846
|
+
const expectedPlatform = artifactTarget === 'claude.skill' ? 'claude' : 'codex';
|
|
2847
|
+
if (platform && artifactTarget && platform !== expectedPlatform) {
|
|
2848
|
+
error(`${fieldName}.platform: target "${artifactTarget}" must use "${expectedPlatform}"`);
|
|
2849
|
+
}
|
|
2850
|
+
if (target.distribution !== undefined) {
|
|
2851
|
+
error(`${fieldName}.distribution: platform targets do not support distribution`);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
const key = id && artifactTarget ? `${id}:${artifactTarget}` : null;
|
|
2855
|
+
if (key) {
|
|
2856
|
+
if (seen.has(key)) {
|
|
2857
|
+
error(`${fieldName}: duplicate artifact id + target "${key}"`);
|
|
2858
|
+
}
|
|
2859
|
+
seen.add(key);
|
|
2860
|
+
}
|
|
2861
|
+
const expectedKind = ARTIFACT_KIND_BY_TARGET[artifactTarget];
|
|
2862
|
+
const kind = target.kind === undefined
|
|
2863
|
+
? expectedKind
|
|
2864
|
+
: validateManifestString(target.kind, `${fieldName}.kind`);
|
|
2865
|
+
if (kind && !ARTIFACT_KINDS.has(kind)) {
|
|
2866
|
+
error(`${fieldName}.kind: unsupported artifact kind "${kind}"`);
|
|
2867
|
+
}
|
|
2868
|
+
if (expectedKind && kind && kind !== expectedKind) {
|
|
2869
|
+
error(`${fieldName}.kind: target "${artifactTarget}" requires kind "${expectedKind}"`);
|
|
2870
|
+
}
|
|
2871
|
+
const expectedProjectManagedKind = ARTIFACT_PROJECT_MANAGED_KIND_BY_TARGET[artifactTarget];
|
|
2872
|
+
const projectManagedKind = target.projectManagedKind === undefined
|
|
2873
|
+
? expectedProjectManagedKind
|
|
2874
|
+
: validateManifestString(target.projectManagedKind, `${fieldName}.projectManagedKind`);
|
|
2875
|
+
if (projectManagedKind && !ARTIFACT_PROJECT_MANAGED_KINDS.has(projectManagedKind)) {
|
|
2876
|
+
error(`${fieldName}.projectManagedKind: unsupported Codex project managed kind "${projectManagedKind}"`);
|
|
2877
|
+
}
|
|
2878
|
+
if (expectedProjectManagedKind &&
|
|
2879
|
+
projectManagedKind &&
|
|
2880
|
+
projectManagedKind !== expectedProjectManagedKind) {
|
|
2881
|
+
error(`${fieldName}.projectManagedKind: target "${artifactTarget}" must map to "${expectedProjectManagedKind}"`);
|
|
2882
|
+
}
|
|
2883
|
+
if (!expectedProjectManagedKind && projectManagedKind) {
|
|
2884
|
+
error(`${fieldName}.projectManagedKind: target "${artifactTarget}" does not support projectManagedKind`);
|
|
2885
|
+
}
|
|
2886
|
+
const scopes = validateStringArray(target.scopes, `${fieldName}.scopes`, {
|
|
2887
|
+
allowed: ARTIFACT_SCOPES,
|
|
2888
|
+
label: 'artifact scope',
|
|
2889
|
+
requireNonEmpty: true,
|
|
2890
|
+
});
|
|
2891
|
+
const states = validateStringArray(target.states, `${fieldName}.states`, {
|
|
2892
|
+
allowed: ARTIFACT_STATES,
|
|
2893
|
+
label: 'artifact state',
|
|
2894
|
+
requireNonEmpty: true,
|
|
2895
|
+
});
|
|
2896
|
+
const refreshKeys = validateStringArray(target.refreshKeys, `${fieldName}.refreshKeys`);
|
|
2897
|
+
const schemaKeys = configSchemaKeys(manifest);
|
|
2898
|
+
for (const [refreshIndex, refreshKey] of refreshKeys.entries()) {
|
|
2899
|
+
if (!schemaKeys.has(refreshKey)) {
|
|
2900
|
+
error(`${fieldName}.refreshKeys[${refreshIndex}]: unknown config key "${refreshKey}"`);
|
|
2901
|
+
continue;
|
|
2902
|
+
}
|
|
2903
|
+
const field = configSchemaField(manifest, refreshKey);
|
|
2904
|
+
if (scopes.includes('project') &&
|
|
2905
|
+
(field?.type === 'file' || field?.type === 'directory')) {
|
|
2906
|
+
error(`${fieldName}.refreshKeys[${refreshIndex}]: project-scope repo-visible artifacts cannot depend on "${field.type}" config key "${refreshKey}"; use a portable string key or keep the path in user/project local config for lifecycle/helper code.`);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
if (target.requiresCodexProjectConfig !== undefined &&
|
|
2910
|
+
typeof target.requiresCodexProjectConfig !== 'boolean') {
|
|
2911
|
+
error(`${fieldName}.requiresCodexProjectConfig must be a boolean`);
|
|
2912
|
+
}
|
|
2913
|
+
if (artifactTarget === 'codex.skill' && target.requiresCodexProjectConfig === true) {
|
|
2914
|
+
error(`${fieldName}.requiresCodexProjectConfig: codex.skill writes the skill runtime and must not require Codex project config preflight`);
|
|
2915
|
+
}
|
|
2916
|
+
if ((platform === 'claude' || portable) && target.requiresCodexProjectConfig !== undefined) {
|
|
2917
|
+
error(`${fieldName}.requiresCodexProjectConfig: this target does not support the field`);
|
|
2918
|
+
}
|
|
2919
|
+
let roleKey;
|
|
2920
|
+
let skillKey;
|
|
2921
|
+
if (artifactTarget === 'codex.agentRole') {
|
|
2922
|
+
roleKey = validateCodexRoleKey(target.roleKey, `${fieldName}.roleKey`);
|
|
2923
|
+
if (!roleKey) {
|
|
2924
|
+
error(`${fieldName}.roleKey: codex.agentRole targets must declare a static roleKey`);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
else if (target.roleKey !== undefined) {
|
|
2928
|
+
error(`${fieldName}.roleKey: roleKey is only supported for codex.agentRole targets`);
|
|
2929
|
+
}
|
|
2930
|
+
if (artifactTarget === 'codex.skill' || artifactTarget === 'claude.skill' || portable) {
|
|
2931
|
+
skillKey = portable
|
|
2932
|
+
? validatePortableSkillKey(target.skillKey, `${fieldName}.skillKey`)
|
|
2933
|
+
: validateCodexSkillKey(target.skillKey, `${fieldName}.skillKey`);
|
|
2934
|
+
if (!skillKey) {
|
|
2935
|
+
error(`${fieldName}.skillKey: ${artifactTarget} targets must declare a static skillKey`);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
else if (target.skillKey !== undefined) {
|
|
2939
|
+
error(`${fieldName}.skillKey: skillKey is only supported for skill targets`);
|
|
2940
|
+
}
|
|
2941
|
+
const normalizedSourceDirs = Array.isArray(target.sourceDirs)
|
|
2942
|
+
? target.sourceDirs
|
|
2943
|
+
.filter((sourceDir) => typeof sourceDir === 'string' && sourceDir.trim() !== '')
|
|
2944
|
+
.map((sourceDir) => path.posix.normalize(sourceDir))
|
|
2945
|
+
: [];
|
|
2946
|
+
if (new Set(normalizedSourceDirs).size !== normalizedSourceDirs.length) {
|
|
2947
|
+
error(`${fieldName}.sourceDirs must not contain duplicate directories`);
|
|
2948
|
+
}
|
|
2949
|
+
const sourceDirs = target.sourceDirs === undefined
|
|
2950
|
+
? []
|
|
2951
|
+
: validateStringArray(target.sourceDirs, `${fieldName}.sourceDirs`);
|
|
2952
|
+
if (sourceDirs.length > 0 && artifactTarget !== 'codex.skill' && artifactTarget !== 'claude.skill' && !portable) {
|
|
2953
|
+
error(`${fieldName}.sourceDirs: sourceDirs are only supported for skill targets`);
|
|
2954
|
+
}
|
|
2955
|
+
for (const [sourceIndex, sourceDir] of sourceDirs.entries()) {
|
|
2956
|
+
const sourceField = `${fieldName}.sourceDirs[${sourceIndex}]`;
|
|
2957
|
+
if (!sourceDir.startsWith('./') || path.posix.isAbsolute(sourceDir)) {
|
|
2958
|
+
error(`${sourceField} must start with "./" and stay inside the plugin root`);
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
const normalized = path.posix.normalize(sourceDir);
|
|
2962
|
+
if (normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
|
|
2963
|
+
error(`${sourceField} must stay inside the plugin root`);
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2966
|
+
const stats = fs.lstatSync(path.join(pluginDir, normalized), { throwIfNoEntry: false });
|
|
2967
|
+
if (!stats?.isDirectory() || stats.isSymbolicLink()) {
|
|
2968
|
+
error(`${sourceField} must point to an existing directory`);
|
|
2969
|
+
}
|
|
2970
|
+
if (skillKey && !portable &&
|
|
2971
|
+
normalized !== `skills/${skillKey}` &&
|
|
2972
|
+
!normalized.startsWith(`skills/${skillKey}/`)) {
|
|
2973
|
+
error(`${sourceField} must stay inside "./skills/${skillKey}"`);
|
|
2974
|
+
}
|
|
2975
|
+
if (skillKey && !portable &&
|
|
2976
|
+
isSkillAuthorOnlyPath(path.posix.relative(`skills/${skillKey}`, normalized))) {
|
|
2977
|
+
error(`${sourceField} must not point inside an author-only Skill directory`);
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
if (skillKey && !portable &&
|
|
2981
|
+
sourceDirs.length > 0 &&
|
|
2982
|
+
!normalizedSourceDirs.includes(`skills/${skillKey}`)) {
|
|
2983
|
+
error(`${fieldName}.sourceDirs must include "./skills/${skillKey}" so README.md is published`);
|
|
2984
|
+
}
|
|
2985
|
+
return {
|
|
2986
|
+
id,
|
|
2987
|
+
platform,
|
|
2988
|
+
distribution,
|
|
2989
|
+
scopes,
|
|
2990
|
+
target: artifactTarget,
|
|
2991
|
+
kind,
|
|
2992
|
+
projectManagedKind,
|
|
2993
|
+
states,
|
|
2994
|
+
refreshKeys,
|
|
2995
|
+
roleKey,
|
|
2996
|
+
skillKey,
|
|
2997
|
+
sourceDirs,
|
|
2998
|
+
runtimeImports: Array.isArray(target.runtimeImports) ? target.runtimeImports : [],
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
function validateArtifactsConfigLightweight(pluginDir, manifest) {
|
|
3002
|
+
if (manifest.artifacts === undefined)
|
|
3003
|
+
return [];
|
|
3004
|
+
const artifacts = expectRecord(manifest.artifacts, 'artifacts');
|
|
3005
|
+
if (!artifacts)
|
|
3006
|
+
return [];
|
|
3007
|
+
validateAllowedKeys(artifacts, 'artifacts', ['targets']);
|
|
3008
|
+
if (artifacts.targets === undefined)
|
|
3009
|
+
return [];
|
|
3010
|
+
if (!Array.isArray(artifacts.targets)) {
|
|
3011
|
+
error('artifacts.targets must be an array');
|
|
3012
|
+
return [];
|
|
3013
|
+
}
|
|
3014
|
+
const seen = new Set();
|
|
3015
|
+
const declarations = artifacts.targets
|
|
3016
|
+
.map((entry, index) => validateArtifactTargetEntry(entry, index, manifest, seen, pluginDir))
|
|
3017
|
+
.filter(Boolean);
|
|
3018
|
+
const owners = new Map();
|
|
3019
|
+
for (const declaration of declarations) {
|
|
3020
|
+
if ((declaration.target !== 'codex.skill' && declaration.target !== 'claude.skill' && declaration.target !== 'agent-skills.skill') ||
|
|
3021
|
+
!declaration.skillKey) {
|
|
3022
|
+
continue;
|
|
3023
|
+
}
|
|
3024
|
+
for (const scope of declaration.scopes) {
|
|
3025
|
+
const ownerKey = `${declaration.platform ?? declaration.distribution}:${scope}:${declaration.skillKey}`;
|
|
3026
|
+
const existing = owners.get(ownerKey);
|
|
3027
|
+
if (existing) {
|
|
3028
|
+
error(`artifacts.targets has duplicate skill ownership "${ownerKey}" in artifacts "${existing}" and "${declaration.id}"`);
|
|
3029
|
+
}
|
|
3030
|
+
else {
|
|
3031
|
+
owners.set(ownerKey, declaration.id);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return declarations;
|
|
3036
|
+
}
|
|
3037
|
+
function validateArtifactPlanRefreshKeys(refreshKeys, declaration, fieldName) {
|
|
3038
|
+
const keys = validateStringArray(refreshKeys, `${fieldName}.refreshKeys`);
|
|
3039
|
+
for (const [index, key] of keys.entries()) {
|
|
3040
|
+
if (!declaration.refreshKeys.includes(key)) {
|
|
3041
|
+
error(`${fieldName}.refreshKeys[${index}]: "${key}" is not declared for artifact "${declaration.id}"`);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
for (const declaredKey of declaration.refreshKeys) {
|
|
3045
|
+
if (!keys.includes(declaredKey)) {
|
|
3046
|
+
error(`${fieldName}: artifact "${declaration.id}" must preserve declared refresh key "${declaredKey}"`);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
function isValidArtifactFilePath(relativePath) {
|
|
3051
|
+
return (relativePath.trim() !== '' &&
|
|
3052
|
+
!relativePath.includes('\\') &&
|
|
3053
|
+
!relativePath.startsWith('/') &&
|
|
3054
|
+
path.posix.normalize(relativePath) === relativePath &&
|
|
3055
|
+
!relativePath.split('/').some((segment) => segment === '' || segment === '.' || segment === '..'));
|
|
3056
|
+
}
|
|
3057
|
+
function isSkillAuthorOnlyPath(relativePath) {
|
|
3058
|
+
const [topLevel] = relativePath.split('/').filter(Boolean);
|
|
3059
|
+
return topLevel === 'evals' || topLevel === 'reports';
|
|
3060
|
+
}
|
|
3061
|
+
function collectArtifactSourceFiles(rootDir, fieldName, files) {
|
|
3062
|
+
const rootStats = fs.lstatSync(rootDir, { throwIfNoEntry: false });
|
|
3063
|
+
if (!rootStats?.isDirectory() || rootStats.isSymbolicLink())
|
|
3064
|
+
return;
|
|
3065
|
+
const visit = (currentDir, parentPath) => {
|
|
3066
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
|
|
3067
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
3068
|
+
for (const entry of entries) {
|
|
3069
|
+
const relativePath = parentPath ? `${parentPath}/${entry.name}` : entry.name;
|
|
3070
|
+
if (isSkillAuthorOnlyPath(relativePath))
|
|
3071
|
+
continue;
|
|
3072
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
3073
|
+
if (entry.isSymbolicLink()) {
|
|
3074
|
+
error(`${fieldName} contains unsupported symbolic link "${relativePath}"`);
|
|
3075
|
+
continue;
|
|
3076
|
+
}
|
|
3077
|
+
if (entry.isDirectory()) {
|
|
3078
|
+
visit(absolutePath, relativePath);
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
if (!entry.isFile()) {
|
|
3082
|
+
error(`${fieldName} contains unsupported non-regular file "${relativePath}"`);
|
|
3083
|
+
continue;
|
|
3084
|
+
}
|
|
3085
|
+
files.set(relativePath, fs.readFileSync(absolutePath));
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
visit(rootDir, '');
|
|
3089
|
+
}
|
|
3090
|
+
function findArtifactFilePrefixConflict(files) {
|
|
3091
|
+
const paths = [...files.keys()].sort();
|
|
3092
|
+
for (const [index, relativePath] of paths.entries()) {
|
|
3093
|
+
const prefix = paths.slice(0, index).find((candidate) => relativePath.startsWith(`${candidate}/`));
|
|
3094
|
+
if (prefix)
|
|
3095
|
+
return [prefix, relativePath];
|
|
3096
|
+
}
|
|
3097
|
+
return null;
|
|
3098
|
+
}
|
|
3099
|
+
function authorRuntimeImportKey(artifactId, importIndex) {
|
|
3100
|
+
return `${artifactId}\0${importIndex}`;
|
|
3101
|
+
}
|
|
3102
|
+
async function resolveAuthorRuntimeImports(options) {
|
|
3103
|
+
const artifactImports = (options.manifest.artifacts?.targets ?? []).flatMap((target) => (target.runtimeImports ?? []).map((reference, importIndex) => ({
|
|
3104
|
+
key: authorRuntimeImportKey(target.id, importIndex),
|
|
3105
|
+
label: `artifacts target ${JSON.stringify(target.id)} runtime import ${JSON.stringify(reference.name)}`,
|
|
3106
|
+
purpose: 'artifact-runtime',
|
|
3107
|
+
reference,
|
|
3108
|
+
})));
|
|
3109
|
+
const skillRuntimeImports = (options.manifest.buildConfig?.skillRuntimeFiles ?? []).flatMap((entry, importIndex) => {
|
|
3110
|
+
if (!isRecord(entry.import))
|
|
3111
|
+
return [];
|
|
3112
|
+
const platforms = [
|
|
3113
|
+
...(entry.platforms !== undefined || entry.distributions === undefined
|
|
3114
|
+
? (entry.platforms ?? [...resolveSupportedPlatforms(options.manifest)])
|
|
3115
|
+
: []),
|
|
3116
|
+
...(entry.distributions?.includes('agent-skills') ? [undefined] : []),
|
|
3117
|
+
];
|
|
3118
|
+
return platforms.length > 0 ? [{
|
|
3119
|
+
key: importIndex,
|
|
3120
|
+
label: `buildConfig.skillRuntimeFiles[${importIndex}].import`,
|
|
3121
|
+
purpose: 'skill-runtime-file',
|
|
3122
|
+
reference: entry.import,
|
|
3123
|
+
platforms,
|
|
3124
|
+
}] : [];
|
|
3125
|
+
});
|
|
3126
|
+
const requested = [...artifactImports, ...skillRuntimeImports];
|
|
3127
|
+
if (requested.length === 0) {
|
|
3128
|
+
return { artifactImports: new Map(), skillRuntimeImports: new Map() };
|
|
3129
|
+
}
|
|
3130
|
+
if (!options.repoRoot
|
|
3131
|
+
|| typeof options.cliManifestTools?.readPluginManifest !== 'function'
|
|
3132
|
+
|| typeof options.cliManifestTools?.discoverPlugins !== 'function'
|
|
3133
|
+
|| typeof options.cliManifestTools?.createRuntimeResolver !== 'function') {
|
|
3134
|
+
error('Portable runtime imports require a complete built CLI and canonical same-source Git workspace.');
|
|
3135
|
+
return null;
|
|
3136
|
+
}
|
|
3137
|
+
let consumer;
|
|
3138
|
+
let resolver;
|
|
3139
|
+
try {
|
|
3140
|
+
const sourceSet = await options.cliManifestTools.discoverPlugins(options.repoRoot);
|
|
3141
|
+
consumer = await options.cliManifestTools.readPluginManifest(options.pluginDir);
|
|
3142
|
+
if (!sourceSet.some((candidate) => candidate.manifest.name === consumer.manifest.name)) {
|
|
3143
|
+
sourceSet.push(consumer);
|
|
3144
|
+
}
|
|
3145
|
+
resolver = options.cliManifestTools.createRuntimeResolver(sourceSet);
|
|
3146
|
+
}
|
|
3147
|
+
catch {
|
|
3148
|
+
error('Portable runtime imports could not load the canonical same-source plugin set.');
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
const resolvedArtifacts = new Map();
|
|
3152
|
+
const resolvedSkillRuntimeFiles = new Map();
|
|
3153
|
+
for (const request of requested) {
|
|
3154
|
+
let value;
|
|
3155
|
+
try {
|
|
3156
|
+
for (const platform of request.platforms ?? [undefined]) {
|
|
3157
|
+
const resolution = await resolver.resolve({
|
|
3158
|
+
consumer,
|
|
3159
|
+
reference: request.reference,
|
|
3160
|
+
purpose: request.purpose,
|
|
3161
|
+
platform,
|
|
3162
|
+
});
|
|
3163
|
+
const bytes = fs.readFileSync(resolution.sourcePath);
|
|
3164
|
+
const digest = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
3165
|
+
if (digest !== resolution.sourceDigest)
|
|
3166
|
+
throw new Error('runtime export changed during resolution');
|
|
3167
|
+
const current = {
|
|
3168
|
+
bytes,
|
|
3169
|
+
digest: resolution.sourceDigest,
|
|
3170
|
+
providerPluginId: resolution.provider,
|
|
3171
|
+
providerVersion: resolution.providerVersion,
|
|
3172
|
+
exportName: resolution.name,
|
|
3173
|
+
source: resolution.source,
|
|
3174
|
+
};
|
|
3175
|
+
if (value && (value.digest !== current.digest
|
|
3176
|
+
|| value.providerPluginId !== current.providerPluginId
|
|
3177
|
+
|| value.providerVersion !== current.providerVersion
|
|
3178
|
+
|| value.exportName !== current.exportName
|
|
3179
|
+
|| value.source !== current.source)) {
|
|
3180
|
+
throw new Error('runtime export changed between projection surfaces');
|
|
3181
|
+
}
|
|
3182
|
+
value = current;
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
catch {
|
|
3186
|
+
const provider = typeof request.reference.plugin === 'string'
|
|
3187
|
+
? JSON.stringify(request.reference.plugin)
|
|
3188
|
+
: '<invalid-provider>';
|
|
3189
|
+
const exportName = typeof request.reference.export === 'string'
|
|
3190
|
+
? JSON.stringify(request.reference.export)
|
|
3191
|
+
: '<invalid-export>';
|
|
3192
|
+
error(`Cannot resolve ${request.label} from provider ${provider} export ${exportName}.`);
|
|
3193
|
+
continue;
|
|
3194
|
+
}
|
|
3195
|
+
if (request.purpose === 'artifact-runtime')
|
|
3196
|
+
resolvedArtifacts.set(request.key, value);
|
|
3197
|
+
else
|
|
3198
|
+
resolvedSkillRuntimeFiles.set(request.key, value);
|
|
3199
|
+
}
|
|
3200
|
+
return {
|
|
3201
|
+
artifactImports: resolvedArtifacts,
|
|
3202
|
+
skillRuntimeImports: resolvedSkillRuntimeFiles,
|
|
3203
|
+
};
|
|
3204
|
+
}
|
|
3205
|
+
function overlayPortableRuntimeFiles(files, manifest, pluginDir, skillKey, runtimeImports) {
|
|
3206
|
+
for (const [runtimeIndex, runtimeFile] of (manifest.buildConfig?.skillRuntimeFiles ?? []).entries()) {
|
|
3207
|
+
if (runtimeFile.skill !== skillKey
|
|
3208
|
+
|| !runtimeFile.distributions?.includes('agent-skills'))
|
|
3209
|
+
continue;
|
|
3210
|
+
if (typeof runtimeFile.source === 'string') {
|
|
3211
|
+
const sourcePath = path.join(pluginDir, normalizeRelativePath(runtimeFile.source));
|
|
3212
|
+
const stats = fs.lstatSync(sourcePath, { throwIfNoEntry: false });
|
|
3213
|
+
if (!stats?.isFile() || stats.isSymbolicLink()) {
|
|
3214
|
+
error(`buildConfig.skillRuntimeFiles[${runtimeIndex}].source must point to a regular file`);
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
files.set(runtimeFile.target, fs.readFileSync(sourcePath));
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
if (isRecord(runtimeFile.import)) {
|
|
3221
|
+
const resolved = runtimeImports?.skillRuntimeImports.get(runtimeIndex);
|
|
3222
|
+
if (!resolved) {
|
|
3223
|
+
error(`buildConfig.skillRuntimeFiles[${runtimeIndex}].import could not be resolved`);
|
|
3224
|
+
continue;
|
|
3225
|
+
}
|
|
3226
|
+
files.set(runtimeFile.target, resolved.bytes);
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
function validateRenderedSkillTree(options) {
|
|
3231
|
+
const { entry, declaration, fieldName, pluginDir, manifest, materializedRoot, skillValidationTools, } = options;
|
|
3232
|
+
if (entry.state !== 'present' || !isRecord(entry.content) || !isRecord(entry.content.files)) {
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
const files = new Map();
|
|
3236
|
+
declaration.sourceDirs.forEach((sourceDir, index) => {
|
|
3237
|
+
collectArtifactSourceFiles(path.join(pluginDir, normalizeRelativePath(sourceDir)), `artifacts.targets sourceDirs[${index}] for artifact "${declaration.id}"`, files);
|
|
3238
|
+
});
|
|
3239
|
+
if (declaration.target === 'agent-skills.skill') {
|
|
3240
|
+
overlayPortableRuntimeFiles(files, manifest, pluginDir, declaration.skillKey, options.runtimeImports);
|
|
3241
|
+
}
|
|
3242
|
+
for (const [relativePath, content] of Object.entries(entry.content.files)) {
|
|
3243
|
+
if (isSkillAuthorOnlyPath(relativePath)) {
|
|
3244
|
+
error(`${fieldName}.content.files path "${relativePath}" uses an author-only Skill directory`);
|
|
3245
|
+
continue;
|
|
3246
|
+
}
|
|
3247
|
+
if (isValidArtifactFilePath(relativePath) && typeof content === 'string') {
|
|
3248
|
+
files.set(relativePath, Buffer.from(content, 'utf8'));
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
const conflict = findArtifactFilePrefixConflict(files);
|
|
3252
|
+
if (conflict) {
|
|
3253
|
+
error(`${fieldName} final file tree path "${conflict[0]}" conflicts with "${conflict[1]}"`);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
const skillFile = files.get('SKILL.md');
|
|
3257
|
+
if (!skillFile || skillFile.toString('utf8').trim() === '') {
|
|
3258
|
+
error(`${fieldName} final file tree must contain a non-empty SKILL.md`);
|
|
3259
|
+
}
|
|
3260
|
+
else if (!skillValidationTools) {
|
|
3261
|
+
error(`${fieldName} final SKILL.md cannot be validated because the shared CLI Skill validator is unavailable`);
|
|
3262
|
+
}
|
|
3263
|
+
else {
|
|
3264
|
+
const sourcePath = `${fieldName} final SKILL.md`;
|
|
3265
|
+
const parsed = skillValidationTools.parseSkillFrontmatter(skillFile.toString('utf8'), sourcePath);
|
|
3266
|
+
for (const diagnostic of parsed.errors) {
|
|
3267
|
+
error(`${diagnostic.path} [${diagnostic.code}] ${diagnostic.message}`);
|
|
3268
|
+
}
|
|
3269
|
+
if (parsed.frontmatter) {
|
|
3270
|
+
const diagnostics = skillValidationTools.validateSkillFrontmatter(parsed.frontmatter, {
|
|
3271
|
+
profile: declaration.platform === 'claude' ? 'claude-2.1.88' : 'portable-agent-skills',
|
|
3272
|
+
expectedName: declaration.skillKey,
|
|
3273
|
+
sourcePath,
|
|
3274
|
+
});
|
|
3275
|
+
for (const diagnostic of diagnostics) {
|
|
3276
|
+
error(`${diagnostic.path} [${diagnostic.code}] ${diagnostic.message}`);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
const readme = files.get('README.md');
|
|
3281
|
+
if (!readme || readme.toString('utf8').trim() === '') {
|
|
3282
|
+
error(`${fieldName} final file tree must contain a non-empty README.md`);
|
|
3283
|
+
}
|
|
3284
|
+
const sensitiveValues = Object.entries(manifest.config?.schema ?? {})
|
|
3285
|
+
.filter(([, field]) => isRecord(field) && field.sensitive === true)
|
|
3286
|
+
.map(([key, field]) => Object.prototype.hasOwnProperty.call(field, 'default')
|
|
3287
|
+
? String(field.default)
|
|
3288
|
+
: String(defaultConfigValues(manifest)[key] ?? ''))
|
|
3289
|
+
.filter(Boolean);
|
|
3290
|
+
for (const [relativePath, content] of files) {
|
|
3291
|
+
const text = content.toString('utf8');
|
|
3292
|
+
if (/\{\{[^{}]+\}\}/u.test(text)) {
|
|
3293
|
+
error(`${fieldName} final file tree contains an unresolved template in "${relativePath}"`);
|
|
3294
|
+
}
|
|
3295
|
+
if (sensitiveValues.some((value) => text.includes(value))) {
|
|
3296
|
+
error(`${fieldName} final file tree exposes sensitive config in "${relativePath}"`);
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3299
|
+
if (declaration.target === 'codex.skill') {
|
|
3300
|
+
const openAiYaml = files.get('agents/openai.yaml');
|
|
3301
|
+
if (!openAiYaml || openAiYaml.toString('utf8').trim() === '') {
|
|
3302
|
+
error(`${fieldName} final file tree must contain a non-empty agents/openai.yaml`);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
fs.rmSync(materializedRoot, { recursive: true, force: true });
|
|
3306
|
+
for (const [relativePath, content] of files) {
|
|
3307
|
+
const targetPath = path.join(materializedRoot, ...relativePath.split('/'));
|
|
3308
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
3309
|
+
fs.writeFileSync(targetPath, content);
|
|
3310
|
+
}
|
|
3311
|
+
if (declaration.target === 'codex.skill' && files.has('agents/openai.yaml')) {
|
|
3312
|
+
validateOpenAiYaml(path.join(materializedRoot, 'agents', 'openai.yaml'), declaration.skillKey, true, manifest, materializedRoot, skillValidationTools);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
function validateStaticPortableFinalTrees(pluginDir, manifest, runtimeImports, skillValidationTools) {
|
|
3316
|
+
const portableRoot = manifest.distributions?.agentSkills?.skills;
|
|
3317
|
+
if (typeof portableRoot !== 'string')
|
|
3318
|
+
return;
|
|
3319
|
+
const rootDir = path.join(pluginDir, normalizeRelativePath(portableRoot));
|
|
3320
|
+
const rootStats = fs.lstatSync(rootDir, { throwIfNoEntry: false });
|
|
3321
|
+
if (!rootStats?.isDirectory() || rootStats.isSymbolicLink())
|
|
3322
|
+
return;
|
|
3323
|
+
const dynamicKeys = new Set((manifest.artifacts?.targets ?? [])
|
|
3324
|
+
.filter((target) => target.target === 'agent-skills.skill' && typeof target.skillKey === 'string')
|
|
3325
|
+
.map((target) => target.skillKey));
|
|
3326
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'aw-static-portable-final-'));
|
|
3327
|
+
try {
|
|
3328
|
+
for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
|
|
3329
|
+
if (!entry.isDirectory() || dynamicKeys.has(entry.name))
|
|
3330
|
+
continue;
|
|
3331
|
+
validateRenderedSkillTree({
|
|
3332
|
+
entry: { state: 'present', content: { files: {} } },
|
|
3333
|
+
declaration: {
|
|
3334
|
+
id: `static:${entry.name}`,
|
|
3335
|
+
distribution: 'agent-skills',
|
|
3336
|
+
target: 'agent-skills.skill',
|
|
3337
|
+
skillKey: entry.name,
|
|
3338
|
+
sourceDirs: [`${portableRoot}/${entry.name}`],
|
|
3339
|
+
},
|
|
3340
|
+
fieldName: `static portable Skill "${entry.name}"`,
|
|
3341
|
+
pluginDir,
|
|
3342
|
+
manifest,
|
|
3343
|
+
runtimeImports,
|
|
3344
|
+
materializedRoot: path.join(tempRoot, entry.name),
|
|
3345
|
+
skillValidationTools,
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
finally {
|
|
3350
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
function validateRenderedArtifactPlan(plan, declarations, scopeLabel, options) {
|
|
3354
|
+
const planField = `scripts.artifacts.render fixture (${scopeLabel})`;
|
|
3355
|
+
const root = expectRecord(plan, planField);
|
|
3356
|
+
if (!root)
|
|
3357
|
+
return;
|
|
3358
|
+
validateAllowedKeys(root, planField, ['version', 'artifacts']);
|
|
3359
|
+
if (root.version !== 2) {
|
|
3360
|
+
error(`${planField}.version must be 2`);
|
|
3361
|
+
}
|
|
3362
|
+
if (!Array.isArray(root.artifacts)) {
|
|
3363
|
+
error(`${planField}.artifacts must be an array`);
|
|
3364
|
+
return;
|
|
3365
|
+
}
|
|
3366
|
+
const byKey = new Map(declarations.map((target) => [`${target.id}:${target.target}`, target]));
|
|
3367
|
+
const returned = new Set();
|
|
3368
|
+
root.artifacts.forEach((artifact, index) => {
|
|
3369
|
+
const fieldName = `${planField}.artifacts[${index}]`;
|
|
3370
|
+
const entry = expectRecord(artifact, fieldName);
|
|
3371
|
+
if (!entry)
|
|
3372
|
+
return;
|
|
3373
|
+
validateAllowedKeys(entry, fieldName, [
|
|
3374
|
+
'id',
|
|
3375
|
+
'platform',
|
|
3376
|
+
'distribution',
|
|
3377
|
+
'target',
|
|
3378
|
+
'kind',
|
|
3379
|
+
'state',
|
|
3380
|
+
'refreshKeys',
|
|
3381
|
+
'disabledReason',
|
|
3382
|
+
'roleKey',
|
|
3383
|
+
'skillKey',
|
|
3384
|
+
'content',
|
|
3385
|
+
]);
|
|
3386
|
+
const id = validateArtifactId(entry.id, `${fieldName}.id`);
|
|
3387
|
+
const target = validateManifestString(entry.target, `${fieldName}.target`);
|
|
3388
|
+
const key = id && target ? `${id}:${target}` : null;
|
|
3389
|
+
const declaration = key ? byKey.get(key) : null;
|
|
3390
|
+
if (!declaration) {
|
|
3391
|
+
error(`${fieldName}: artifact "${key ?? '<unknown>'}" is not declared for ${scopeLabel}`);
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
if (returned.has(key)) {
|
|
3395
|
+
error(`${fieldName}: duplicate artifact id + target "${key}"`);
|
|
3396
|
+
}
|
|
3397
|
+
returned.add(key);
|
|
3398
|
+
if (declaration.distribution) {
|
|
3399
|
+
if (entry.distribution !== declaration.distribution) {
|
|
3400
|
+
error(`${fieldName}.distribution: expected "${declaration.distribution}"`);
|
|
3401
|
+
}
|
|
3402
|
+
if (entry.platform !== undefined) {
|
|
3403
|
+
error(`${fieldName}.platform: distribution artifacts do not support platform`);
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
else if (entry.platform !== declaration.platform) {
|
|
3407
|
+
error(`${fieldName}.platform: expected "${declaration.platform}"`);
|
|
3408
|
+
}
|
|
3409
|
+
if (entry.kind !== declaration.kind) {
|
|
3410
|
+
error(`${fieldName}.kind: expected "${declaration.kind}"`);
|
|
3411
|
+
}
|
|
3412
|
+
if (!declaration.states.includes(entry.state)) {
|
|
3413
|
+
error(`${fieldName}.state: "${entry.state}" is not declared for artifact "${declaration.id}"`);
|
|
3414
|
+
}
|
|
3415
|
+
validateArtifactPlanRefreshKeys(entry.refreshKeys, declaration, fieldName);
|
|
3416
|
+
if (entry.state === 'present' && entry.content === undefined) {
|
|
3417
|
+
error(`${fieldName}.content is required when state is "present"`);
|
|
3418
|
+
}
|
|
3419
|
+
if (entry.disabledReason !== undefined &&
|
|
3420
|
+
(typeof entry.disabledReason !== 'string' || entry.disabledReason.trim() === '')) {
|
|
3421
|
+
error(`${fieldName}.disabledReason must be a non-empty string`);
|
|
3422
|
+
}
|
|
3423
|
+
if (declaration.target === 'codex.agentRole') {
|
|
3424
|
+
if (entry.roleKey !== declaration.roleKey) {
|
|
3425
|
+
error(`${fieldName}.roleKey: expected "${declaration.roleKey}" from static metadata`);
|
|
3426
|
+
}
|
|
3427
|
+
if (isRecord(entry.content)) {
|
|
3428
|
+
const registration = entry.content.registration;
|
|
3429
|
+
if (isRecord(registration) && registration.config_file !== undefined) {
|
|
3430
|
+
error(`${fieldName}.content.registration.config_file must be generated by AW, not renderer`);
|
|
3431
|
+
}
|
|
3432
|
+
const roleFile = entry.content.roleFile;
|
|
3433
|
+
if (isRecord(roleFile) && roleFile.name !== declaration.roleKey) {
|
|
3434
|
+
error(`${fieldName}.content.roleFile.name: expected "${declaration.roleKey}"`);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
else if (entry.roleKey !== undefined) {
|
|
3439
|
+
error(`${fieldName}.roleKey is only supported for codex.agentRole artifacts`);
|
|
3440
|
+
}
|
|
3441
|
+
if (declaration.target === 'codex.skill' || declaration.target === 'claude.skill' || declaration.target === 'agent-skills.skill') {
|
|
3442
|
+
if (entry.skillKey !== declaration.skillKey) {
|
|
3443
|
+
error(`${fieldName}.skillKey: expected "${declaration.skillKey}" from static metadata`);
|
|
3444
|
+
}
|
|
3445
|
+
if (entry.state === 'present') {
|
|
3446
|
+
if (!isRecord(entry.content)) {
|
|
3447
|
+
error(`${fieldName}.content must be an object for dynamic skill artifacts`);
|
|
3448
|
+
}
|
|
3449
|
+
else {
|
|
3450
|
+
validateAllowedKeys(entry.content, `${fieldName}.content`, ['files']);
|
|
3451
|
+
const files = expectRecord(entry.content.files, `${fieldName}.content.files`);
|
|
3452
|
+
if (files) {
|
|
3453
|
+
const filePaths = Object.keys(files).sort();
|
|
3454
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
3455
|
+
if (!isValidArtifactFilePath(relativePath)) {
|
|
3456
|
+
error(`${fieldName}.content.files path "${relativePath}" must be a normalized POSIX relative path`);
|
|
3457
|
+
}
|
|
3458
|
+
if (typeof content !== 'string') {
|
|
3459
|
+
error(`${fieldName}.content.files.${relativePath} must be a string`);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
for (const [pathIndex, relativePath] of filePaths.entries()) {
|
|
3463
|
+
const conflict = filePaths.slice(0, pathIndex).find((candidate) => relativePath.startsWith(`${candidate}/`));
|
|
3464
|
+
if (conflict) {
|
|
3465
|
+
error(`${fieldName}.content.files path "${conflict}" conflicts with "${relativePath}"`);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
validateRenderedSkillTree({
|
|
3471
|
+
entry,
|
|
3472
|
+
declaration,
|
|
3473
|
+
fieldName,
|
|
3474
|
+
pluginDir: options.pluginDir,
|
|
3475
|
+
manifest: options.manifest,
|
|
3476
|
+
runtimeImports: options.runtimeImports,
|
|
3477
|
+
materializedRoot: path.join(options.tempRoot, `${declaration.platform ?? declaration.distribution}-${scopeLabel.split('/')[1]}-${index}`),
|
|
3478
|
+
skillValidationTools: options.skillValidationTools,
|
|
3479
|
+
});
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
else if (entry.skillKey !== undefined) {
|
|
3483
|
+
error(`${fieldName}.skillKey is only supported for dynamic skill artifacts`);
|
|
3484
|
+
}
|
|
3485
|
+
});
|
|
3486
|
+
for (const declaration of declarations) {
|
|
3487
|
+
const key = `${declaration.id}:${declaration.target}`;
|
|
3488
|
+
if (!returned.has(key)) {
|
|
3489
|
+
error(`${planField}: renderer must return artifact "${declaration.id}" target "${declaration.target}"`);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
function runArtifactRendererFixture(pluginDir, manifest, renderEntry, declarations, resolvedRuntimeImports, skillValidationTools) {
|
|
3494
|
+
if (!renderEntry || declarations.length === 0)
|
|
3495
|
+
return;
|
|
3496
|
+
const surfaceScopes = [...new Set(declarations.flatMap((target) => target.scopes.map((scope) => `${target.platform ?? target.distribution}:${scope}`)))];
|
|
3497
|
+
const values = defaultConfigValues(manifest);
|
|
3498
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'aw-artifact-render-'));
|
|
3499
|
+
try {
|
|
3500
|
+
for (const surfaceScope of surfaceScopes) {
|
|
3501
|
+
const [surface, scope] = surfaceScope.split(':');
|
|
3502
|
+
const distribution = surface === 'agent-skills' ? surface : undefined;
|
|
3503
|
+
const platform = distribution ? undefined : surface;
|
|
3504
|
+
const event = distribution ? 'materialize' : 'install';
|
|
3505
|
+
const scopedDeclarations = declarations.filter((target) => (distribution ? target.distribution === distribution : target.platform === platform)
|
|
3506
|
+
&& target.scopes.includes(scope));
|
|
3507
|
+
const contextPath = path.join(tempRoot, `context-${surface}-${scope}.json`);
|
|
3508
|
+
const runtimeImports = scopedDeclarations.flatMap((target) => target.runtimeImports.map((runtimeImport, importIndex) => {
|
|
3509
|
+
const resolved = resolvedRuntimeImports?.artifactImports.get(authorRuntimeImportKey(target.id, importIndex));
|
|
3510
|
+
if (!resolved)
|
|
3511
|
+
return null;
|
|
3512
|
+
const importPath = path.join(tempRoot, `runtime-${target.id}-${importIndex}`);
|
|
3513
|
+
fs.writeFileSync(importPath, resolved.bytes);
|
|
3514
|
+
return {
|
|
3515
|
+
artifactId: target.id,
|
|
3516
|
+
name: runtimeImport.name,
|
|
3517
|
+
providerPluginId: resolved.providerPluginId,
|
|
3518
|
+
exportName: resolved.exportName,
|
|
3519
|
+
path: importPath,
|
|
3520
|
+
digest: resolved.digest,
|
|
3521
|
+
};
|
|
3522
|
+
}).filter(Boolean));
|
|
3523
|
+
fs.writeFileSync(contextPath, JSON.stringify({
|
|
3524
|
+
event,
|
|
3525
|
+
surface: distribution
|
|
3526
|
+
? { kind: 'distribution', name: distribution }
|
|
3527
|
+
: { kind: 'platform', name: platform },
|
|
3528
|
+
...(platform ? { platform } : {}),
|
|
3529
|
+
...(distribution ? { distribution } : {}),
|
|
3530
|
+
scope,
|
|
3531
|
+
plugin: {
|
|
3532
|
+
name: manifest.name,
|
|
3533
|
+
version: manifest.version,
|
|
3534
|
+
rootDir: pluginDir,
|
|
3535
|
+
},
|
|
3536
|
+
config: {
|
|
3537
|
+
mode: 'artifact-render',
|
|
3538
|
+
targetScope: scope,
|
|
3539
|
+
schema: manifest.config?.schema ?? {},
|
|
3540
|
+
values,
|
|
3541
|
+
sources: Object.fromEntries(Object.keys(values).map((key) => [key, 'default'])),
|
|
3542
|
+
issues: [],
|
|
3543
|
+
},
|
|
3544
|
+
changedConfigKeys: [],
|
|
3545
|
+
previousArtifacts: [],
|
|
3546
|
+
artifactSources: scopedDeclarations.map((target) => ({
|
|
3547
|
+
id: target.id,
|
|
3548
|
+
target: target.target,
|
|
3549
|
+
sourceDirs: target.sourceDirs.map((sourceDir) => path.join(pluginDir, normalizeRelativePath(sourceDir))),
|
|
3550
|
+
})),
|
|
3551
|
+
runtimeImports,
|
|
3552
|
+
paths: {
|
|
3553
|
+
awHome: path.join(tempRoot, 'aw-home'),
|
|
3554
|
+
codexHome: path.join(tempRoot, 'codex-home'),
|
|
3555
|
+
claudeHome: path.join(tempRoot, 'claude-home'),
|
|
3556
|
+
manifestPath: path.join(pluginDir, '.agents-plugin', 'plugin.json'),
|
|
3557
|
+
configStores: {
|
|
3558
|
+
user: path.join(tempRoot, 'config-user.json'),
|
|
3559
|
+
project: path.join(tempRoot, 'config-project.json'),
|
|
3560
|
+
},
|
|
3561
|
+
},
|
|
3562
|
+
renderer: {
|
|
3563
|
+
runner: renderEntry.runner,
|
|
3564
|
+
path: renderEntry.path,
|
|
3565
|
+
args: renderEntry.args ?? [],
|
|
3566
|
+
manifestDigest: 'author-validator-fixture',
|
|
3567
|
+
rendererDigest: 'author-validator-fixture',
|
|
3568
|
+
},
|
|
3569
|
+
}), 'utf8');
|
|
3570
|
+
const rendererHome = path.join(tempRoot, 'home');
|
|
3571
|
+
const rendererTemp = path.join(tempRoot, 'tmp');
|
|
3572
|
+
fs.mkdirSync(rendererHome, { recursive: true });
|
|
3573
|
+
fs.mkdirSync(rendererTemp, { recursive: true });
|
|
3574
|
+
const rendererEnv = {
|
|
3575
|
+
AW_HOME: path.join(tempRoot, 'aw-home'),
|
|
3576
|
+
AW_CODEX_HOME: path.join(tempRoot, 'codex-home'),
|
|
3577
|
+
AW_CLAUDE_HOME: path.join(tempRoot, 'claude-home'),
|
|
3578
|
+
PATH: process.env.PATH ?? process.env.Path ?? '',
|
|
3579
|
+
AW_PLUGIN_CONTEXT: contextPath,
|
|
3580
|
+
AW_PLUGIN_ROOT: pluginDir,
|
|
3581
|
+
AW_PLUGIN_NAME: typeof manifest.name === 'string' ? manifest.name : '',
|
|
3582
|
+
AW_PLUGIN_VERSION: typeof manifest.version === 'string' ? manifest.version : '',
|
|
3583
|
+
AW_PLUGIN_EVENT: event,
|
|
3584
|
+
...(platform ? { AW_PLUGIN_PLATFORM: platform } : {}),
|
|
3585
|
+
...(distribution ? { AW_PLUGIN_DISTRIBUTION: distribution } : {}),
|
|
3586
|
+
AW_PLUGIN_SCOPE: scope,
|
|
3587
|
+
};
|
|
3588
|
+
if (process.platform === 'win32') {
|
|
3589
|
+
rendererEnv.USERPROFILE = rendererHome;
|
|
3590
|
+
rendererEnv.TEMP = rendererTemp;
|
|
3591
|
+
rendererEnv.TMP = rendererTemp;
|
|
3592
|
+
if (process.env.SystemRoot)
|
|
3593
|
+
rendererEnv.SystemRoot = process.env.SystemRoot;
|
|
3594
|
+
if (process.env.ComSpec)
|
|
3595
|
+
rendererEnv.ComSpec = process.env.ComSpec;
|
|
3596
|
+
}
|
|
3597
|
+
else {
|
|
3598
|
+
rendererEnv.HOME = rendererHome;
|
|
3599
|
+
rendererEnv.TMPDIR = rendererTemp;
|
|
3600
|
+
}
|
|
3601
|
+
const result = spawnSync(process.execPath, [path.join(pluginDir, renderEntry.path), ...(renderEntry.args ?? [])], {
|
|
3602
|
+
cwd: pluginDir,
|
|
3603
|
+
env: rendererEnv,
|
|
3604
|
+
encoding: 'utf8',
|
|
3605
|
+
timeout: 5000,
|
|
3606
|
+
maxBuffer: 1024 * 1024,
|
|
3607
|
+
});
|
|
3608
|
+
const fieldName = `scripts.artifacts.render fixture (${surface}/${scope})`;
|
|
3609
|
+
if (result.error) {
|
|
3610
|
+
error(`${fieldName} failed: ${result.error.message}`);
|
|
3611
|
+
continue;
|
|
3612
|
+
}
|
|
3613
|
+
if (result.status !== 0) {
|
|
3614
|
+
const stderr = String(result.stderr ?? '').trim().split('\n')[0] ?? '';
|
|
3615
|
+
error(`${fieldName} exited with code ${result.status}${stderr ? `: ${stderr}` : ''}`);
|
|
3616
|
+
continue;
|
|
3617
|
+
}
|
|
3618
|
+
let plan;
|
|
3619
|
+
try {
|
|
3620
|
+
plan = JSON.parse(result.stdout);
|
|
3621
|
+
}
|
|
3622
|
+
catch (e) {
|
|
3623
|
+
error(`${fieldName} stdout must be JSON ArtifactPlan: ${e.message}`);
|
|
3624
|
+
continue;
|
|
3625
|
+
}
|
|
3626
|
+
validateRenderedArtifactPlan(plan, scopedDeclarations, `${surface}/${scope}`, {
|
|
3627
|
+
pluginDir,
|
|
3628
|
+
manifest,
|
|
3629
|
+
runtimeImports: resolvedRuntimeImports,
|
|
3630
|
+
tempRoot,
|
|
3631
|
+
skillValidationTools,
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
finally {
|
|
3636
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
function validateDynamicArtifactsLightweight(pluginDir, manifest, renderEntry, runtimeImports, skillValidationTools) {
|
|
3640
|
+
const declarations = validateArtifactsConfigLightweight(pluginDir, manifest);
|
|
3641
|
+
const hasTargets = declarations.length > 0;
|
|
3642
|
+
const hasRenderer = Boolean(manifest.scripts?.artifacts?.render);
|
|
3643
|
+
if (hasTargets && !hasRenderer) {
|
|
3644
|
+
error('artifacts.targets requires scripts.artifacts.render');
|
|
3645
|
+
}
|
|
3646
|
+
if (hasRenderer && !hasTargets) {
|
|
3647
|
+
error('scripts.artifacts.render requires artifacts.targets');
|
|
3648
|
+
}
|
|
3649
|
+
runArtifactRendererFixture(pluginDir, manifest, renderEntry, declarations, runtimeImports, skillValidationTools);
|
|
3650
|
+
}
|
|
3651
|
+
function validateScriptsLightweight(pluginDir, manifest) {
|
|
3652
|
+
validateConfigSchemaLightweight(manifest.config);
|
|
3653
|
+
if (manifest.scripts === undefined)
|
|
3654
|
+
return null;
|
|
3655
|
+
const scripts = expectRecord(manifest.scripts, 'scripts');
|
|
3656
|
+
if (!scripts)
|
|
3657
|
+
return null;
|
|
3658
|
+
validateAllowedKeys(scripts, 'scripts', ['timeoutMs', 'lifecycle', 'helpers', 'node', 'artifacts', 'tests']);
|
|
3659
|
+
if (scripts.timeoutMs !== undefined &&
|
|
3660
|
+
(!Number.isInteger(scripts.timeoutMs) ||
|
|
3661
|
+
scripts.timeoutMs < SCRIPT_TIMEOUT_MIN_MS ||
|
|
3662
|
+
scripts.timeoutMs > SCRIPT_TIMEOUT_MAX_MS)) {
|
|
3663
|
+
error(`scripts.timeoutMs must be an integer between ${SCRIPT_TIMEOUT_MIN_MS} and ${SCRIPT_TIMEOUT_MAX_MS}`);
|
|
3664
|
+
}
|
|
3665
|
+
const runtimeNodeEntries = [];
|
|
3666
|
+
let artifactRenderEntry = null;
|
|
3667
|
+
let packageMeta = null;
|
|
3668
|
+
if (scripts.helpers !== undefined) {
|
|
3669
|
+
const helpers = expectRecord(scripts.helpers, 'scripts.helpers');
|
|
3670
|
+
if (helpers) {
|
|
3671
|
+
validateAllowedKeys(helpers, 'scripts.helpers', ['config']);
|
|
3672
|
+
const helperPath = normalizePluginScriptPath(pluginDir, helpers.config, 'scripts.helpers.config');
|
|
3673
|
+
validateDeclaredScriptFile(pluginDir, helperPath, 'scripts.helpers.config');
|
|
3674
|
+
if (helperPath && !isNodeRunnablePath(helperPath)) {
|
|
3675
|
+
error('scripts.helpers.config must use a Node runnable extension (.js, .mjs, or .cjs)');
|
|
3676
|
+
}
|
|
3677
|
+
if (helperPath)
|
|
3678
|
+
runtimeNodeEntries.push({ path: helperPath, fieldName: 'scripts.helpers.config' });
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
if (scripts.lifecycle !== undefined) {
|
|
3682
|
+
const lifecycle = expectRecord(scripts.lifecycle, 'scripts.lifecycle');
|
|
3683
|
+
if (lifecycle) {
|
|
3684
|
+
validateAllowedKeys(lifecycle, 'scripts.lifecycle', [...SCRIPT_LIFECYCLE_EVENTS]);
|
|
3685
|
+
for (const [eventName, definition] of Object.entries(lifecycle)) {
|
|
3686
|
+
if (!SCRIPT_LIFECYCLE_EVENTS.has(eventName)) {
|
|
3687
|
+
error(`scripts.lifecycle has unsupported event "${eventName}"`);
|
|
3688
|
+
continue;
|
|
3689
|
+
}
|
|
3690
|
+
validateLifecycleDefinition(pluginDir, definition, `scripts.lifecycle.${eventName}`, runtimeNodeEntries);
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
if (scripts.node !== undefined) {
|
|
3695
|
+
const node = expectRecord(scripts.node, 'scripts.node');
|
|
3696
|
+
if (node) {
|
|
3697
|
+
validateAllowedKeys(node, 'scripts.node', ['packageRoot', 'build']);
|
|
3698
|
+
if (node.packageRoot !== undefined) {
|
|
3699
|
+
packageMeta = validatePackageRoot(pluginDir, node.packageRoot, 'scripts.node.packageRoot');
|
|
3700
|
+
}
|
|
3701
|
+
if (node.build !== undefined) {
|
|
3702
|
+
const buildEntry = validateScriptEntry(pluginDir, node.build, 'scripts.node.build', {
|
|
3703
|
+
requireNodeRunner: true,
|
|
3704
|
+
requireNodePath: true,
|
|
3705
|
+
});
|
|
3706
|
+
if (buildEntry) {
|
|
3707
|
+
scanNodeImportContract(pluginDir, [buildEntry.path], {
|
|
3708
|
+
label: 'scripts.node.build import contract',
|
|
3709
|
+
packageMeta,
|
|
3710
|
+
allowDevDependencies: true,
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
if (scripts.artifacts !== undefined) {
|
|
3717
|
+
const artifacts = expectRecord(scripts.artifacts, 'scripts.artifacts');
|
|
3718
|
+
if (artifacts) {
|
|
3719
|
+
validateAllowedKeys(artifacts, 'scripts.artifacts', ['render']);
|
|
3720
|
+
if (artifacts.render !== undefined) {
|
|
3721
|
+
artifactRenderEntry = validateScriptEntry(pluginDir, artifacts.render, 'scripts.artifacts.render', {
|
|
3722
|
+
requireNodeRunner: true,
|
|
3723
|
+
requireNodePath: true,
|
|
3724
|
+
});
|
|
3725
|
+
if (artifactRenderEntry) {
|
|
3726
|
+
runtimeNodeEntries.push({
|
|
3727
|
+
path: artifactRenderEntry.path,
|
|
3728
|
+
fieldName: 'scripts.artifacts.render.path',
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
if (scripts.tests !== undefined) {
|
|
3735
|
+
const tests = expectRecord(scripts.tests, 'scripts.tests');
|
|
3736
|
+
if (tests) {
|
|
3737
|
+
validateAllowedKeys(tests, 'scripts.tests', ['node']);
|
|
3738
|
+
if (tests.node !== undefined) {
|
|
3739
|
+
if (!Array.isArray(tests.node)) {
|
|
3740
|
+
error('scripts.tests.node must be an array of glob strings');
|
|
3741
|
+
}
|
|
3742
|
+
else {
|
|
3743
|
+
const allFiles = listFilesRecursive(pluginDir);
|
|
3744
|
+
const testEntries = [];
|
|
3745
|
+
tests.node.forEach((pattern, index) => {
|
|
3746
|
+
if (typeof pattern !== 'string' || pattern.trim() === '') {
|
|
3747
|
+
error(`scripts.tests.node[${index}] must be a non-empty string`);
|
|
3748
|
+
return;
|
|
3749
|
+
}
|
|
3750
|
+
for (const filePath of allFiles) {
|
|
3751
|
+
if (matchesGlob(filePath, pattern)) {
|
|
3752
|
+
testEntries.push(filePath);
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
});
|
|
3756
|
+
scanNodeImportContract(pluginDir, testEntries, {
|
|
3757
|
+
label: 'scripts.tests.node import contract',
|
|
3758
|
+
packageMeta,
|
|
3759
|
+
allowDevDependencies: true,
|
|
3760
|
+
});
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3765
|
+
scanNodeImportContract(pluginDir, runtimeNodeEntries.map((entry) => entry.path), {
|
|
3766
|
+
label: 'runtime Node import contract',
|
|
3767
|
+
packageMeta,
|
|
3768
|
+
allowDevDependencies: false,
|
|
3769
|
+
});
|
|
3770
|
+
return artifactRenderEntry;
|
|
3771
|
+
}
|
|
3772
|
+
function validateMcpContent(pluginDir, manifest) {
|
|
3773
|
+
if (!manifest.mcpServers)
|
|
3774
|
+
return;
|
|
3775
|
+
if (isTemplateAsset(manifest, manifest.mcpServers))
|
|
3776
|
+
return;
|
|
3777
|
+
const mcpPath = path.join(pluginDir, manifest.mcpServers);
|
|
3778
|
+
if (!fs.existsSync(mcpPath))
|
|
3779
|
+
return;
|
|
3780
|
+
try {
|
|
3781
|
+
const content = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
|
|
3782
|
+
if (!content.mcpServers || typeof content.mcpServers !== 'object') {
|
|
3783
|
+
error(`${manifest.mcpServers} must have a "mcpServers" top-level key`);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
catch (e) {
|
|
3787
|
+
error(`${manifest.mcpServers} is not valid JSON: ${e.message}`);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
function validateAppsContent(pluginDir, manifest) {
|
|
3791
|
+
if (!manifest.apps)
|
|
3792
|
+
return;
|
|
3793
|
+
if (isTemplateAsset(manifest, manifest.apps))
|
|
3794
|
+
return;
|
|
3795
|
+
const appsPath = path.join(pluginDir, manifest.apps);
|
|
3796
|
+
if (!fs.existsSync(appsPath))
|
|
3797
|
+
return;
|
|
3798
|
+
try {
|
|
3799
|
+
const content = JSON.parse(fs.readFileSync(appsPath, 'utf8'));
|
|
3800
|
+
if (!content.apps || typeof content.apps !== 'object') {
|
|
3801
|
+
error(`${manifest.apps} must have an "apps" top-level key`);
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
catch (e) {
|
|
3805
|
+
error(`${manifest.apps} is not valid JSON: ${e.message}`);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
function hasCodexRepoManagedSurface(manifest) {
|
|
3809
|
+
if (manifest.skills || manifest.slashCommands || manifest.mcpServers) {
|
|
3810
|
+
return true;
|
|
3811
|
+
}
|
|
3812
|
+
if ((manifest.artifacts?.targets ?? []).some((target) => target?.platform === 'codex' &&
|
|
3813
|
+
Array.isArray(target.scopes) &&
|
|
3814
|
+
target.scopes.includes('project') &&
|
|
3815
|
+
typeof target.projectManagedKind === 'string')) {
|
|
3816
|
+
return true;
|
|
3817
|
+
}
|
|
3818
|
+
const extensions = manifest.extensions;
|
|
3819
|
+
if (!extensions || typeof extensions !== 'object' || Array.isArray(extensions)) {
|
|
3820
|
+
return false;
|
|
3821
|
+
}
|
|
3822
|
+
return ['instructions', 'permissions', 'hooks', 'agents'].some((key) => Array.isArray(extensions[key]) && extensions[key].length > 0) || (extensions.nativeHooks &&
|
|
3823
|
+
typeof extensions.nativeHooks === 'object' &&
|
|
3824
|
+
!Array.isArray(extensions.nativeHooks) &&
|
|
3825
|
+
Array.isArray(extensions.nativeHooks.codex) &&
|
|
3826
|
+
extensions.nativeHooks.codex.length > 0);
|
|
3827
|
+
}
|
|
3828
|
+
function warnCodexProjectScopeLimitations(manifest) {
|
|
3829
|
+
if (!isCodexTargeted(manifest)) {
|
|
3830
|
+
return;
|
|
3831
|
+
}
|
|
3832
|
+
if (manifest.apps) {
|
|
3833
|
+
warn('Codex project scope does not support apps; users must install this app surface with `aw install <plugin> -p codex -s user` or split the plugin.');
|
|
3834
|
+
}
|
|
3835
|
+
if (!hasCodexRepoManagedSurface(manifest)) {
|
|
3836
|
+
warn('This Codex-targeted plugin has no repo-managed project surface; Codex project scope will fail fast and only Codex user native install is applicable.');
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
function validateExtensionNameUniqueness(manifest) {
|
|
3840
|
+
if (!manifest.extensions)
|
|
3841
|
+
return;
|
|
3842
|
+
for (const extType of ['instructions', 'permissions', 'hooks', 'agents']) {
|
|
3843
|
+
const items = manifest.extensions[extType];
|
|
3844
|
+
if (!Array.isArray(items))
|
|
3845
|
+
continue;
|
|
3846
|
+
const names = new Set();
|
|
3847
|
+
for (const item of items) {
|
|
3848
|
+
if (item.name) {
|
|
3849
|
+
if (names.has(item.name)) {
|
|
3850
|
+
error(`extensions.${extType} has duplicate name: "${item.name}"`);
|
|
3851
|
+
}
|
|
3852
|
+
names.add(item.name);
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
function validateOpenAiYaml(openaiYamlPath, skillName, required, manifest, pluginDir, skillValidationTools) {
|
|
3858
|
+
const fieldName = `skills/${skillName}/agents/openai.yaml`;
|
|
3859
|
+
if (!fs.existsSync(openaiYamlPath)) {
|
|
3860
|
+
if (required) {
|
|
3861
|
+
error(`${fieldName} not found; Codex-targeted skills must declare Codex trigger policy`);
|
|
3862
|
+
}
|
|
3863
|
+
else {
|
|
3864
|
+
warn(`${fieldName} not found; this is acceptable only for non-Codex skills`);
|
|
3865
|
+
}
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (!skillValidationTools) {
|
|
3869
|
+
error(`${fieldName} cannot be validated because the shared CLI Skill validator is unavailable`);
|
|
3870
|
+
return;
|
|
3871
|
+
}
|
|
3872
|
+
try {
|
|
3873
|
+
const relativePath = path.relative(pluginDir, openaiYamlPath);
|
|
3874
|
+
const rawContent = fs.readFileSync(openaiYamlPath, 'utf8');
|
|
3875
|
+
const validation = skillValidationTools.validateCodexMetadata(renderTemplateForPlatform(manifest, relativePath, rawContent, 'codex'), skillName, fieldName);
|
|
3876
|
+
for (const entry of validation.errors) {
|
|
3877
|
+
error(`${entry.path} [${entry.code}] ${entry.message}`);
|
|
3878
|
+
}
|
|
3879
|
+
const skillRoot = path.dirname(path.dirname(openaiYamlPath));
|
|
3880
|
+
for (const assetPath of validation.assetPaths) {
|
|
3881
|
+
const absoluteAssetPath = path.join(skillRoot, ...assetPath.split('/'));
|
|
3882
|
+
const stats = fs.lstatSync(absoluteAssetPath, { throwIfNoEntry: false });
|
|
3883
|
+
if (!stats?.isFile() || stats.isSymbolicLink()) {
|
|
3884
|
+
error(`${fieldName} references missing regular file "${assetPath}"`);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
catch (e) {
|
|
3889
|
+
error(`${fieldName} shared validation failed: ${e.message}`);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
function validateSkills(pluginDir, manifest, options, skillValidationTools) {
|
|
3893
|
+
if (!manifest.skills)
|
|
3894
|
+
return;
|
|
3895
|
+
const skillsDir = path.join(pluginDir, manifest.skills);
|
|
3896
|
+
if (!fs.existsSync(skillsDir))
|
|
3897
|
+
return;
|
|
3898
|
+
const requiresCodexMetadata = isCodexTargeted(manifest);
|
|
3899
|
+
const targetsClaude = isClaudeTargeted(manifest);
|
|
3900
|
+
const skillDirs = fs
|
|
3901
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
3902
|
+
.filter((d) => d.isDirectory());
|
|
3903
|
+
if (skillDirs.length === 0) {
|
|
3904
|
+
error('skills directory exists but contains no skill subdirectories');
|
|
3905
|
+
}
|
|
3906
|
+
for (const dir of skillDirs) {
|
|
3907
|
+
const skillPath = path.join(skillsDir, dir.name);
|
|
3908
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
3909
|
+
const openaiYamlPath = path.join(skillPath, 'agents', 'openai.yaml');
|
|
3910
|
+
const skillMdRelativePath = path.relative(pluginDir, skillMdPath);
|
|
3911
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
3912
|
+
error(`skills/${dir.name}/SKILL.md not found`);
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
3916
|
+
const lineCount = countLines(content);
|
|
3917
|
+
if (lineCount >= MAX_SKILL_LINES) {
|
|
3918
|
+
warn(`skills/${dir.name}/SKILL.md has ${lineCount} lines; Agent Skills recommends keeping SKILL.md under ${MAX_SKILL_LINES} lines`);
|
|
3919
|
+
}
|
|
3920
|
+
const variants = renderedContentVariants(manifest, skillMdRelativePath, content, targetedPlatforms(manifest));
|
|
3921
|
+
for (const variant of variants) {
|
|
3922
|
+
const sourceName = variant.platform
|
|
3923
|
+
? `skills/${dir.name}/SKILL.md (${variant.platform} render)`
|
|
3924
|
+
: `skills/${dir.name}/SKILL.md`;
|
|
3925
|
+
if (!skillValidationTools) {
|
|
3926
|
+
error(`${sourceName} cannot be validated because the shared CLI Skill validator is unavailable`);
|
|
3927
|
+
}
|
|
3928
|
+
else {
|
|
3929
|
+
const parsed = skillValidationTools.parseSkillFrontmatter(variant.content, sourceName);
|
|
3930
|
+
for (const entry of parsed.errors) {
|
|
3931
|
+
error(`${entry.path} [${entry.code}] ${entry.message}`);
|
|
3932
|
+
}
|
|
3933
|
+
if (parsed.frontmatter) {
|
|
3934
|
+
const profile = options.strictAgentSkills || !targetsClaude
|
|
3935
|
+
? 'portable-agent-skills'
|
|
3936
|
+
: options.claudeSkillProfile === 'current'
|
|
3937
|
+
? 'claude-current'
|
|
3938
|
+
: 'claude-2.1.88';
|
|
3939
|
+
const diagnostics = skillValidationTools.validateSkillFrontmatter(parsed.frontmatter, {
|
|
3940
|
+
profile,
|
|
3941
|
+
expectedName: dir.name,
|
|
3942
|
+
sourcePath: sourceName,
|
|
3943
|
+
});
|
|
3944
|
+
for (const entry of diagnostics) {
|
|
3945
|
+
error(`${entry.path} [${entry.code}] ${entry.message}`);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
validateOpenAiYaml(openaiYamlPath, dir.name, requiresCodexMetadata, manifest, pluginDir, skillValidationTools);
|
|
3951
|
+
if (/\[TODO[:\]]/i.test(content)) {
|
|
3952
|
+
error(`skills/${dir.name}/SKILL.md still contains TODO placeholders`);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
function detectCodexSlashCommandForbiddenSyntax(content) {
|
|
3957
|
+
const checks = [
|
|
3958
|
+
[/\$ARGUMENTS\b/, '$ARGUMENTS'],
|
|
3959
|
+
[/\$\d+\b/, '$1'],
|
|
3960
|
+
[/```!/, '```!'],
|
|
3961
|
+
[/!`[^`\r\n]+`/, '!`command`'],
|
|
3962
|
+
];
|
|
3963
|
+
for (const [pattern, label] of checks) {
|
|
3964
|
+
if (pattern.test(content)) {
|
|
3965
|
+
return label;
|
|
3966
|
+
}
|
|
3967
|
+
}
|
|
3968
|
+
return null;
|
|
3969
|
+
}
|
|
3970
|
+
function validateSlashCommandAssetReference(commandRoot, commandRelativePath, value, fieldName) {
|
|
3971
|
+
if (value === undefined)
|
|
3972
|
+
return;
|
|
3973
|
+
validateCodexIconPath({ [fieldName]: value }, fieldName, `${commandRelativePath}.codex.interface`);
|
|
3974
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
3975
|
+
return;
|
|
3976
|
+
const normalized = normalizeRelativePath(stripDotSlash(value));
|
|
3977
|
+
const absolutePath = path.join(commandRoot, normalized);
|
|
3978
|
+
const stats = fs.statSync(absolutePath, { throwIfNoEntry: false });
|
|
3979
|
+
if (!stats) {
|
|
3980
|
+
error(`${commandRelativePath}.codex.interface.${fieldName} references missing asset "${value}"`);
|
|
3981
|
+
return;
|
|
3982
|
+
}
|
|
3983
|
+
if (!stats.isFile()) {
|
|
3984
|
+
error(`${commandRelativePath}.codex.interface.${fieldName} must reference a file, got "${value}"`);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
function validateSlashCommandFrontmatter(commandRoot, commandRelativePath, frontmatter, platform) {
|
|
3988
|
+
const fieldName = `${commandRelativePath} frontmatter`;
|
|
3989
|
+
const fm = expectRecord(frontmatter, fieldName);
|
|
3990
|
+
if (!fm)
|
|
3991
|
+
return;
|
|
3992
|
+
for (const key of Object.keys(fm)) {
|
|
3993
|
+
if (CLAUDE_SLASH_COMMAND_ONLY_FIELDS.has(key)) {
|
|
3994
|
+
error(`${fieldName}: "${key}" must be nested under "claude"`);
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
validateAllowedKeys(fm, fieldName, ['description', 'claude', 'codex']);
|
|
3998
|
+
validateNonEmptyString(fm.description, `${fieldName}.description`, MAX_SKILL_DESCRIPTION_LEN);
|
|
3999
|
+
if (platform === 'claude' && fm.claude !== undefined) {
|
|
4000
|
+
const claudeBlock = expectRecord(fm.claude, `${fieldName}.claude`);
|
|
4001
|
+
if (claudeBlock) {
|
|
4002
|
+
validateAllowedKeys(claudeBlock, `${fieldName}.claude`, [
|
|
4003
|
+
'description',
|
|
4004
|
+
'allowed-tools',
|
|
4005
|
+
'model',
|
|
4006
|
+
'argument-hint',
|
|
4007
|
+
'shell',
|
|
4008
|
+
]);
|
|
4009
|
+
if (claudeBlock.description !== undefined) {
|
|
4010
|
+
validateNonEmptyString(claudeBlock.description, `${fieldName}.claude.description`, MAX_SKILL_DESCRIPTION_LEN);
|
|
4011
|
+
}
|
|
4012
|
+
if (claudeBlock['allowed-tools'] !== undefined) {
|
|
4013
|
+
validateAllowedTools(claudeBlock['allowed-tools'], `${fieldName}.claude.allowed-tools`, { allowArray: true });
|
|
4014
|
+
}
|
|
4015
|
+
if (claudeBlock.model !== undefined) {
|
|
4016
|
+
validateNonEmptyString(claudeBlock.model, `${fieldName}.claude.model`);
|
|
4017
|
+
}
|
|
4018
|
+
if (claudeBlock['argument-hint'] !== undefined) {
|
|
4019
|
+
validateNonEmptyString(claudeBlock['argument-hint'], `${fieldName}.claude.argument-hint`);
|
|
4020
|
+
}
|
|
4021
|
+
if (claudeBlock.shell !== undefined) {
|
|
4022
|
+
if (typeof claudeBlock.shell !== 'string') {
|
|
4023
|
+
error(`${fieldName}.claude.shell must be a string`);
|
|
4024
|
+
}
|
|
4025
|
+
else if (!CLAUDE_FRONTMATTER_SHELLS.has(claudeBlock.shell.trim().toLowerCase())) {
|
|
4026
|
+
error(`${fieldName}.claude.shell must be "bash" or "powershell"`);
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
if (platform === 'codex' && fm.codex !== undefined) {
|
|
4032
|
+
const codexBlock = expectRecord(fm.codex, `${fieldName}.codex`);
|
|
4033
|
+
if (codexBlock) {
|
|
4034
|
+
if (codexBlock.policy !== undefined) {
|
|
4035
|
+
error(`${fieldName}.codex.policy is not supported; generated slashCommands skills are always manual-only`);
|
|
4036
|
+
}
|
|
4037
|
+
validateAllowedKeys(codexBlock, `${fieldName}.codex`, ['description', 'interface']);
|
|
4038
|
+
if (codexBlock.description !== undefined) {
|
|
4039
|
+
validateNonEmptyString(codexBlock.description, `${fieldName}.codex.description`, MAX_SKILL_DESCRIPTION_LEN);
|
|
4040
|
+
}
|
|
4041
|
+
if (codexBlock.interface !== undefined) {
|
|
4042
|
+
const interfaceBlock = expectRecord(codexBlock.interface, `${fieldName}.codex.interface`);
|
|
4043
|
+
if (interfaceBlock) {
|
|
4044
|
+
validateAllowedKeys(interfaceBlock, `${fieldName}.codex.interface`, [
|
|
4045
|
+
'display_name',
|
|
4046
|
+
'short_description',
|
|
4047
|
+
'icon_small',
|
|
4048
|
+
'icon_large',
|
|
4049
|
+
'brand_color',
|
|
4050
|
+
'default_prompt',
|
|
4051
|
+
]);
|
|
4052
|
+
validateCodexMetadataString(interfaceBlock, 'display_name', `${fieldName}.codex.interface`, MAX_OPENAI_INTERFACE_NAME_LEN);
|
|
4053
|
+
validateCodexShortDescription(interfaceBlock, `${fieldName}.codex.interface`);
|
|
4054
|
+
validateCodexDefaultPrompt(interfaceBlock, 'default_prompt', `${fieldName}.codex.interface`, MAX_OPENAI_DEFAULT_PROMPT_LEN, path.basename(commandRelativePath, path.extname(commandRelativePath)));
|
|
4055
|
+
validateCodexBrandColor(interfaceBlock, 'brand_color', `${fieldName}.codex.interface`);
|
|
4056
|
+
validateSlashCommandAssetReference(commandRoot, commandRelativePath, interfaceBlock.icon_small, 'icon_small');
|
|
4057
|
+
validateSlashCommandAssetReference(commandRoot, commandRelativePath, interfaceBlock.icon_large, 'icon_large');
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
}
|
|
4063
|
+
function validateSlashCommands(pluginDir, manifest) {
|
|
4064
|
+
const defaultSlashCommandsDir = path.join(pluginDir, DEFAULT_SLASH_COMMANDS_DIR);
|
|
4065
|
+
if (fs.existsSync(defaultSlashCommandsDir) && !manifest.slashCommands) {
|
|
4066
|
+
error(`"${DEFAULT_SLASH_COMMANDS_DIR}" exists but manifest does not declare "slashCommands"`);
|
|
4067
|
+
}
|
|
4068
|
+
if (!manifest.slashCommands)
|
|
4069
|
+
return;
|
|
4070
|
+
const targetPlatforms = resolveSupportedPlatforms(manifest);
|
|
4071
|
+
const slashCommandsRoot = path.join(pluginDir, manifest.slashCommands);
|
|
4072
|
+
const stats = fs.statSync(slashCommandsRoot, { throwIfNoEntry: false });
|
|
4073
|
+
if (!stats) {
|
|
4074
|
+
error(`slashCommands points to "${manifest.slashCommands}" but path does not exist`);
|
|
4075
|
+
return;
|
|
4076
|
+
}
|
|
4077
|
+
if (!stats.isDirectory()) {
|
|
4078
|
+
error(`slashCommands must point to a directory, got "${manifest.slashCommands}"`);
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
const skillNames = new Set();
|
|
4082
|
+
if (manifest.skills) {
|
|
4083
|
+
const skillsRoot = path.join(pluginDir, manifest.skills);
|
|
4084
|
+
if (fs.existsSync(skillsRoot)) {
|
|
4085
|
+
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
4086
|
+
if (entry.isDirectory()) {
|
|
4087
|
+
skillNames.add(entry.name);
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
const seenCommands = new Set();
|
|
4093
|
+
const commandFiles = [];
|
|
4094
|
+
for (const entry of fs.readdirSync(slashCommandsRoot, { withFileTypes: true })) {
|
|
4095
|
+
const relativePath = normalizeRelativePath(path.join(stripDotSlash(manifest.slashCommands), entry.name));
|
|
4096
|
+
if (entry.isDirectory()) {
|
|
4097
|
+
if (entry.name !== 'assets') {
|
|
4098
|
+
error(`slashCommands does not support nested directory "${relativePath}"; only root *.md files and assets/ are allowed`);
|
|
4099
|
+
}
|
|
4100
|
+
continue;
|
|
4101
|
+
}
|
|
4102
|
+
if (!entry.isFile()) {
|
|
4103
|
+
error(`slashCommands has unsupported entry "${relativePath}"`);
|
|
4104
|
+
continue;
|
|
4105
|
+
}
|
|
4106
|
+
if (path.posix.extname(entry.name) !== '.md') {
|
|
4107
|
+
error(`slashCommands only accepts root *.md files; found "${relativePath}"`);
|
|
4108
|
+
continue;
|
|
4109
|
+
}
|
|
4110
|
+
commandFiles.push(entry.name);
|
|
4111
|
+
}
|
|
4112
|
+
if (commandFiles.length === 0) {
|
|
4113
|
+
error(`slashCommands "${manifest.slashCommands}" must contain at least one root *.md command file`);
|
|
4114
|
+
return;
|
|
4115
|
+
}
|
|
4116
|
+
for (const fileName of commandFiles.sort()) {
|
|
4117
|
+
const commandName = path.posix.basename(fileName, '.md');
|
|
4118
|
+
const commandRelativePath = normalizeRelativePath(path.join(stripDotSlash(manifest.slashCommands), fileName));
|
|
4119
|
+
if (commandName.length === 0 ||
|
|
4120
|
+
commandName.length > 64 ||
|
|
4121
|
+
!SLASH_COMMAND_NAME_PATTERN.test(commandName)) {
|
|
4122
|
+
error(`${commandRelativePath} command name must use lowercase hyphen-case and be <= 64 characters`);
|
|
4123
|
+
continue;
|
|
4124
|
+
}
|
|
4125
|
+
if (seenCommands.has(commandName)) {
|
|
4126
|
+
error(`slashCommands has duplicate command name "${commandName}"`);
|
|
4127
|
+
continue;
|
|
4128
|
+
}
|
|
4129
|
+
if (skillNames.has(commandName)) {
|
|
4130
|
+
error(`slashCommands command "${commandName}" conflicts with source skill "${commandName}"`);
|
|
4131
|
+
continue;
|
|
4132
|
+
}
|
|
4133
|
+
seenCommands.add(commandName);
|
|
4134
|
+
const absolutePath = path.join(slashCommandsRoot, fileName);
|
|
4135
|
+
const rawContent = fs.readFileSync(absolutePath, 'utf8');
|
|
4136
|
+
for (const platform of ['claude', 'codex']) {
|
|
4137
|
+
if (!targetPlatforms.has(platform))
|
|
4138
|
+
continue;
|
|
4139
|
+
const rendered = renderTemplateForPlatform(manifest, commandRelativePath, rawContent, platform);
|
|
4140
|
+
if (platform === 'codex') {
|
|
4141
|
+
const codexForbiddenSyntax = detectCodexSlashCommandForbiddenSyntax(rendered);
|
|
4142
|
+
if (codexForbiddenSyntax) {
|
|
4143
|
+
error(`${commandRelativePath} Codex render still contains Claude-only syntax "${codexForbiddenSyntax}"`);
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
4146
|
+
let frontmatter;
|
|
4147
|
+
try {
|
|
4148
|
+
frontmatter = parseFrontmatter(rendered, `${commandRelativePath} (${platform} render)`);
|
|
4149
|
+
}
|
|
4150
|
+
catch (parseError) {
|
|
4151
|
+
error(`${commandRelativePath} (${platform} render) has invalid YAML frontmatter: ${parseError.message}`);
|
|
4152
|
+
continue;
|
|
4153
|
+
}
|
|
4154
|
+
if (!frontmatter) {
|
|
4155
|
+
error(`${commandRelativePath} (${platform} render) is missing YAML frontmatter`);
|
|
4156
|
+
continue;
|
|
4157
|
+
}
|
|
4158
|
+
validateSlashCommandFrontmatter(slashCommandsRoot, commandRelativePath, frontmatter, platform);
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
// ---------------------------------------------------------------------------
|
|
4163
|
+
// Main
|
|
4164
|
+
// ---------------------------------------------------------------------------
|
|
4165
|
+
function printUsage() {
|
|
4166
|
+
console.log(`Usage:
|
|
4167
|
+
aw plugin validate <plugin-directory> [options]
|
|
4168
|
+
|
|
4169
|
+
Options:
|
|
4170
|
+
--catalog-min-version <semver>
|
|
4171
|
+
Validate compatibility.aw.minVersion against a target catalog.
|
|
4172
|
+
--strict-agent-skills Allow only Agent Skills portable core frontmatter.
|
|
4173
|
+
--claude-skill-profile <minimum|current>
|
|
4174
|
+
Validate Claude fields at AW minimum 2.1.88 (default) or current docs.
|
|
4175
|
+
--version-base <git-ref> Compare the production bundle against this Git base. Default: HEAD.
|
|
4176
|
+
--workspace-root <path> Workspace root. Default: current directory.
|
|
4177
|
+
--json Emit one machine-readable JSON result to stdout.
|
|
4178
|
+
-h, --help Show this help message.
|
|
4179
|
+
`);
|
|
4180
|
+
}
|
|
4181
|
+
function exitCliError(message, json) {
|
|
4182
|
+
if (json) {
|
|
4183
|
+
console.log(JSON.stringify({
|
|
4184
|
+
ok: false,
|
|
4185
|
+
profile: 'unknown',
|
|
4186
|
+
warnings: [],
|
|
4187
|
+
errors: [message],
|
|
4188
|
+
}, null, 2));
|
|
4189
|
+
}
|
|
4190
|
+
else {
|
|
4191
|
+
console.error(message);
|
|
4192
|
+
}
|
|
4193
|
+
process.exit(1);
|
|
4194
|
+
}
|
|
4195
|
+
async function validatePluginDir(pluginDir, options) {
|
|
4196
|
+
const manifestPath = path.join(pluginDir, '.agents-plugin', 'plugin.json');
|
|
4197
|
+
const cliManifestTools = await tryLoadCliManifestTools();
|
|
4198
|
+
if (!cliManifestTools
|
|
4199
|
+
|| typeof cliManifestTools.assertPluginInputTree !== 'function'
|
|
4200
|
+
|| typeof cliManifestTools.validateSourcePluginManifest !== 'function') {
|
|
4201
|
+
error(`Plugin manifest contract v${SOURCE_PLUGIN_MANIFEST_CONTRACT_VERSION} is unavailable; install a complete current aw CLI and retry.`);
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
try {
|
|
4205
|
+
await cliManifestTools.assertPluginInputTree(pluginDir);
|
|
4206
|
+
}
|
|
4207
|
+
catch (e) {
|
|
4208
|
+
error(e.message);
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
if (!fs.existsSync(manifestPath)) {
|
|
4212
|
+
error('.agents-plugin/plugin.json not found');
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
let manifest;
|
|
4216
|
+
try {
|
|
4217
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
4218
|
+
}
|
|
4219
|
+
catch (e) {
|
|
4220
|
+
error(`.agents-plugin/plugin.json is not valid JSON: ${e.message}`);
|
|
4221
|
+
return;
|
|
4222
|
+
}
|
|
4223
|
+
if (options.enforceCanonicalRoot
|
|
4224
|
+
&& typeof manifest?.name === 'string'
|
|
4225
|
+
&& manifest.name !== '') {
|
|
4226
|
+
try {
|
|
4227
|
+
assertCanonicalAuthoringPluginRoot({
|
|
4228
|
+
workspaceRoot: options.repoRoot,
|
|
4229
|
+
pluginRoot: pluginDir,
|
|
4230
|
+
pluginName: manifest.name,
|
|
4231
|
+
requireManifest: true,
|
|
4232
|
+
});
|
|
4233
|
+
}
|
|
4234
|
+
catch (cause) {
|
|
4235
|
+
error(cause.message);
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
const skillValidationTools = await tryLoadCliSkillValidationTools();
|
|
4240
|
+
try {
|
|
4241
|
+
await cliManifestTools.validateSourcePluginManifest(pluginDir, manifest);
|
|
4242
|
+
}
|
|
4243
|
+
catch (e) {
|
|
4244
|
+
error(e.message);
|
|
4245
|
+
}
|
|
4246
|
+
validatePlatformFieldSupport(manifest);
|
|
4247
|
+
validatePortableNativeArtifactCapabilityLightweight(manifest);
|
|
4248
|
+
validateNameMatchesDirectory(pluginDir, manifest);
|
|
4249
|
+
validateNoBuildOutputDirs(pluginDir);
|
|
4250
|
+
validateDeclaredNativeAssets(pluginDir, manifest);
|
|
4251
|
+
validateCanonicalSkillLayout(pluginDir, manifest);
|
|
4252
|
+
validatePluginVersionContracts(manifest, options);
|
|
4253
|
+
validatePluginEvalManifest(pluginDir, manifest);
|
|
4254
|
+
const artifactRenderEntry = validateScriptsLightweight(pluginDir, manifest);
|
|
4255
|
+
const runtimeImports = await resolveAuthorRuntimeImports({
|
|
4256
|
+
pluginDir,
|
|
4257
|
+
manifest,
|
|
4258
|
+
repoRoot: options.repoRoot,
|
|
4259
|
+
cliManifestTools,
|
|
4260
|
+
});
|
|
4261
|
+
validateStaticPortableFinalTrees(pluginDir, manifest, runtimeImports, skillValidationTools);
|
|
4262
|
+
validateDynamicArtifactsLightweight(pluginDir, manifest, artifactRenderEntry, runtimeImports, skillValidationTools);
|
|
4263
|
+
validateMcpContent(pluginDir, manifest);
|
|
4264
|
+
validateAppsContent(pluginDir, manifest);
|
|
4265
|
+
warnCodexProjectScopeLimitations(manifest);
|
|
4266
|
+
validateExtensionNameUniqueness(manifest);
|
|
4267
|
+
validateSkills(pluginDir, manifest, options, skillValidationTools);
|
|
4268
|
+
validateSlashCommands(pluginDir, manifest);
|
|
4269
|
+
if (errors.length === 0) {
|
|
4270
|
+
const repoRoot = resolveCanonicalRepoRoot(pluginDir, manifest.name ?? '', options.gitRoot);
|
|
4271
|
+
if (repoRoot) {
|
|
4272
|
+
try {
|
|
4273
|
+
pluginVersionCheckResult = await checkPluginVersions({
|
|
4274
|
+
repoRoot,
|
|
4275
|
+
cliPackageRoot: CLI_PACKAGE_ROOT,
|
|
4276
|
+
base: options.versionBase,
|
|
4277
|
+
pluginNames: [manifest.name],
|
|
4278
|
+
});
|
|
4279
|
+
const check = pluginVersionCheckResult.checks[0];
|
|
4280
|
+
for (const message of check?.warnings ?? []) {
|
|
4281
|
+
warn(`plugin version check: ${message}`);
|
|
4282
|
+
}
|
|
4283
|
+
for (const message of check?.errors ?? []) {
|
|
4284
|
+
error(`plugin version check: ${message}`);
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
catch (versionError) {
|
|
4288
|
+
if (versionError?.code === 'bundlerUnavailable'
|
|
4289
|
+
|| versionError?.code === 'gitUnavailable') {
|
|
4290
|
+
warn(`plugin version check skipped: ${versionError.message}`);
|
|
4291
|
+
}
|
|
4292
|
+
else {
|
|
4293
|
+
error(`plugin version check failed: ${versionError.message}`);
|
|
4294
|
+
}
|
|
4295
|
+
}
|
|
4296
|
+
}
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
function parseArgs(argv) {
|
|
4300
|
+
const args = argv.slice(2);
|
|
4301
|
+
const options = {
|
|
4302
|
+
pluginDir: null,
|
|
4303
|
+
catalogMinVersion: null,
|
|
4304
|
+
strictAgentSkills: false,
|
|
4305
|
+
claudeSkillProfile: 'minimum',
|
|
4306
|
+
versionBase: 'HEAD',
|
|
4307
|
+
workspaceRoot: null,
|
|
4308
|
+
json: args.includes('--json'),
|
|
4309
|
+
};
|
|
4310
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4311
|
+
const arg = args[index];
|
|
4312
|
+
if (arg === '-h' || arg === '--help') {
|
|
4313
|
+
printUsage();
|
|
4314
|
+
process.exit(0);
|
|
4315
|
+
}
|
|
4316
|
+
if (arg === '--json') {
|
|
4317
|
+
continue;
|
|
4318
|
+
}
|
|
4319
|
+
if (arg === '--strict-agent-skills') {
|
|
4320
|
+
options.strictAgentSkills = true;
|
|
4321
|
+
continue;
|
|
4322
|
+
}
|
|
4323
|
+
if (arg === '--claude-skill-profile') {
|
|
4324
|
+
const value = args[index + 1];
|
|
4325
|
+
if (!value || value.startsWith('--')) {
|
|
4326
|
+
exitCliError('--claude-skill-profile requires a value.', options.json);
|
|
4327
|
+
}
|
|
4328
|
+
index += 1;
|
|
4329
|
+
if (value !== 'minimum' && value !== 'current') {
|
|
4330
|
+
exitCliError(`Invalid --claude-skill-profile: expected "minimum" or "current". Received ${JSON.stringify(value)}.`, options.json);
|
|
4331
|
+
}
|
|
4332
|
+
options.claudeSkillProfile = value;
|
|
4333
|
+
continue;
|
|
4334
|
+
}
|
|
4335
|
+
if (arg === '--version-base') {
|
|
4336
|
+
const value = args[index + 1];
|
|
4337
|
+
if (!value || value.startsWith('--')) {
|
|
4338
|
+
exitCliError('--version-base requires a value.', options.json);
|
|
4339
|
+
}
|
|
4340
|
+
index += 1;
|
|
4341
|
+
options.versionBase = value;
|
|
4342
|
+
continue;
|
|
4343
|
+
}
|
|
4344
|
+
if (arg === '--workspace-root') {
|
|
4345
|
+
const value = args[index + 1];
|
|
4346
|
+
if (!value || value.startsWith('--')) {
|
|
4347
|
+
exitCliError('--workspace-root requires a value.', options.json);
|
|
4348
|
+
}
|
|
4349
|
+
index += 1;
|
|
4350
|
+
options.workspaceRoot = value;
|
|
4351
|
+
continue;
|
|
4352
|
+
}
|
|
4353
|
+
if (!arg.startsWith('--')) {
|
|
4354
|
+
if (!options.pluginDir) {
|
|
4355
|
+
options.pluginDir = arg;
|
|
4356
|
+
continue;
|
|
4357
|
+
}
|
|
4358
|
+
exitCliError(`Unknown argument: ${arg}`, options.json);
|
|
4359
|
+
}
|
|
4360
|
+
if (arg === '--catalog-min-version') {
|
|
4361
|
+
const value = args[index + 1];
|
|
4362
|
+
if (!value || value.startsWith('--')) {
|
|
4363
|
+
exitCliError('--catalog-min-version requires a value.', options.json);
|
|
4364
|
+
}
|
|
4365
|
+
index += 1;
|
|
4366
|
+
if (!STRICT_SEMVER_PATTERN.test(value)) {
|
|
4367
|
+
exitCliError(`Invalid --catalog-min-version: expected a strict SemVer 2.0.0 string. Received ${JSON.stringify(value)}.`, options.json);
|
|
4368
|
+
}
|
|
4369
|
+
options.catalogMinVersion = value;
|
|
4370
|
+
continue;
|
|
4371
|
+
}
|
|
4372
|
+
exitCliError(`Unknown option: ${arg}`, options.json);
|
|
4373
|
+
}
|
|
4374
|
+
return options;
|
|
4375
|
+
}
|
|
4376
|
+
async function main() {
|
|
4377
|
+
const args = parseArgs(process.argv);
|
|
4378
|
+
if (!args.pluginDir) {
|
|
4379
|
+
exitCliError('plugin-directory is required; run with --help for usage.', args.json);
|
|
4380
|
+
}
|
|
4381
|
+
const requestedWorkspaceRoot = args.workspaceRoot === null
|
|
4382
|
+
? null
|
|
4383
|
+
: path.resolve(args.workspaceRoot);
|
|
4384
|
+
const resolved = path.resolve(requestedWorkspaceRoot ?? '.', args.pluginDir);
|
|
4385
|
+
if (!fs.existsSync(resolved)) {
|
|
4386
|
+
exitCliError(`[ERROR] Directory not found: ${resolved}`, args.json);
|
|
4387
|
+
}
|
|
4388
|
+
const repoRootCandidate = requestedWorkspaceRoot ?? findRepoRootCandidate(resolved);
|
|
4389
|
+
try {
|
|
4390
|
+
assertAuthoringPluginPathFence({
|
|
4391
|
+
repoRoot: repoRootCandidate ?? undefined,
|
|
4392
|
+
pluginRoot: resolved,
|
|
4393
|
+
requirePluginRoot: true,
|
|
4394
|
+
requireManifest: true,
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
catch (pathError) {
|
|
4398
|
+
exitCliError(`[ERROR] ${pathError.message}`, args.json);
|
|
4399
|
+
}
|
|
4400
|
+
const repoRoot = requestedWorkspaceRoot ?? discoverCanonicalRepoRoot(repoRootCandidate);
|
|
4401
|
+
const gitRoot = discoverCanonicalRepoRoot(findRepoRootCandidate(resolved));
|
|
4402
|
+
await validatePluginDir(resolved, {
|
|
4403
|
+
catalogMinVersion: args.catalogMinVersion,
|
|
4404
|
+
strictAgentSkills: args.strictAgentSkills,
|
|
4405
|
+
claudeSkillProfile: args.claudeSkillProfile,
|
|
4406
|
+
versionBase: args.versionBase,
|
|
4407
|
+
repoRoot,
|
|
4408
|
+
gitRoot,
|
|
4409
|
+
enforceCanonicalRoot: requestedWorkspaceRoot !== null,
|
|
4410
|
+
});
|
|
4411
|
+
if (args.json) {
|
|
4412
|
+
console.log(JSON.stringify({
|
|
4413
|
+
ok: errors.length === 0,
|
|
4414
|
+
profile: args.strictAgentSkills ? 'agent-skills-portable-core' : 'aw-source',
|
|
4415
|
+
claudeSkillProfile: args.claudeSkillProfile,
|
|
4416
|
+
pluginDirectory: resolved,
|
|
4417
|
+
versionCheck: pluginVersionCheckResult,
|
|
4418
|
+
warnings,
|
|
4419
|
+
errors,
|
|
4420
|
+
}, null, 2));
|
|
4421
|
+
if (errors.length > 0) {
|
|
4422
|
+
process.exit(1);
|
|
4423
|
+
}
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
if (warnings.length > 0) {
|
|
4427
|
+
console.log(`\n[WARN] ${warnings.length} warning(s):`);
|
|
4428
|
+
for (const w of warnings) {
|
|
4429
|
+
console.log(` - ${w}`);
|
|
4430
|
+
}
|
|
4431
|
+
}
|
|
4432
|
+
if (errors.length > 0) {
|
|
4433
|
+
console.log(`\n[FAIL] ${errors.length} error(s):`);
|
|
4434
|
+
for (const e of errors) {
|
|
4435
|
+
console.log(` - ${e}`);
|
|
4436
|
+
}
|
|
4437
|
+
process.exit(1);
|
|
4438
|
+
}
|
|
4439
|
+
console.log('[PASS] All checks passed.');
|
|
4440
|
+
}
|
|
4441
|
+
main();
|
|
4442
|
+
//# sourceMappingURL=validate-plugin.mjs.map
|