@pi-harness/pi-harness 0.1.30 → 0.1.32
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/README.md +1 -1
- package/apps/web/dist/assets/index-BqOM0elx.css +2 -0
- package/apps/web/dist/assets/index-x2UhIAWm.js +25 -0
- package/apps/web/dist/index.html +2 -2
- package/apps/web/package.json +1 -1
- package/apps/web/profile/cordis.yml +0 -221
- package/apps/web/server-dist/bin.js +11 -3
- package/apps/web/server-dist/bin.js.map +1 -1
- package/apps/web/src/server/bin.ts +10 -2
- package/apps/web/src/style.css +30 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +70 -18
- package/node_modules/@pi-harness/api-gateway/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/at-file.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.json +1 -7
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cordis-group.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cordis-logger-console.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cordis-timer.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/dependency-checker.json +2 -10
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/history-compressor.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -7
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-client.json +2 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/openpets.json +2 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/skill-guard.json +2 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/test-harness.json +2 -10
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -9
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -8
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -7
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/workspace-search.json +2 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -7
- package/node_modules/@pi-harness/api-gateway/dist/marketplace.d.ts +18 -2
- package/node_modules/@pi-harness/api-gateway/dist/marketplace.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace.js +93 -10
- package/node_modules/@pi-harness/api-gateway/dist/marketplace.js.map +1 -1
- package/node_modules/@pi-harness/api-gateway/package.json +1 -27
- package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/main.js +19 -3
- package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
- package/node_modules/@pi-harness/cli/package.json +1 -1
- package/node_modules/@pi-harness/cli/profiles/default/cordis.yml +0 -87
- package/node_modules/@pi-harness/host-webserver/package.json +1 -1
- package/node_modules/@pi-harness/web-app/package.json +1 -1
- package/package.json +6 -79
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +70 -18
- package/packages/api-gateway/dist/index.js.map +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/at-file.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-fetch.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context-doctor.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/context.json +1 -7
- package/packages/api-gateway/dist/marketplace-entries/official/cordis-group.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cordis-logger-console.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cordis-timer.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/dependency-checker.json +2 -10
- package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/git-time-capsule.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -7
- package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +2 -8
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/openpets.json +2 -8
- package/packages/api-gateway/dist/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/plugin-stars.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/readme-gen.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-bridge.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/session-insights.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/skill-guard.json +2 -9
- package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/test-harness.json +2 -10
- package/packages/api-gateway/dist/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/token-guard.json +1 -9
- package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -8
- package/packages/api-gateway/dist/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/vision-toolkit.json +1 -7
- package/packages/api-gateway/dist/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/workspace-search.json +2 -2
- package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -7
- package/packages/api-gateway/dist/marketplace.d.ts +18 -2
- package/packages/api-gateway/dist/marketplace.d.ts.map +1 -1
- package/packages/api-gateway/dist/marketplace.js +93 -10
- package/packages/api-gateway/dist/marketplace.js.map +1 -1
- package/packages/api-gateway/package.json +1 -27
- package/packages/api-gateway/src/index.ts +85 -23
- package/packages/api-gateway/src/marketplace-entries/official/agent-teams.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/anchored-standard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/annotation.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/archify.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/at-file.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/auto-mode.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/better-sidebar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-fetch.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/cli-notifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/code2skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/colleague-skill.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/context-doctor.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/context.json +1 -7
- package/packages/api-gateway/src/marketplace-entries/official/cordis-group.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cordis-logger-console.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cordis-timer.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cost-meter.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/dependency-checker.json +2 -10
- package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/genui.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/git-time-capsule.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -7
- package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +2 -8
- package/packages/api-gateway/src/marketplace-entries/official/mcp-panel.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mirage-bridge.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mock-server.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/modlens.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/module-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/obsidian-sync.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/openpets.json +2 -8
- package/packages/api-gateway/src/marketplace-entries/official/plan-execute.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-check.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-dev.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-finder.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-radar.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/plugin-stars.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/prompt-library.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/readme-gen.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/reviewer-bot.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/runtime-doctor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/secure-audit.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bookmarks.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-bridge.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/session-compare.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-export.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/session-insights.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/session-search.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-catalog.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/skill-guard.json +2 -9
- package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/synapse.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/tab-manager.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/taskboard.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/telemetry-blocker.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/test-harness.json +2 -10
- package/packages/api-gateway/src/marketplace-entries/official/theme-studio.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/token-guard.json +1 -9
- package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -8
- package/packages/api-gateway/src/marketplace-entries/official/undo-savepoint.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/vision-toolkit.json +1 -7
- package/packages/api-gateway/src/marketplace-entries/official/web-research.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-navigator.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/workspace-search.json +2 -2
- package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -7
- package/packages/api-gateway/src/marketplace.ts +113 -12
- package/packages/api-gateway/test/index.test.ts +297 -5
- package/packages/api-gateway/test/marketplace-schema.test.ts +12 -0
- package/packages/api-gateway/test/marketplace.test.ts +172 -240
- package/packages/api-gateway/test/packed-package.test.ts +18 -6
- package/packages/api-gateway/test/release-contract.test.ts +8 -4
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/cli/dist/main.d.ts.map +1 -1
- package/packages/cli/dist/main.js +19 -3
- package/packages/cli/dist/main.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/profiles/default/cordis.yml +0 -87
- package/packages/cli/src/main.ts +18 -3
- package/packages/cli/test/cli.test.ts +20 -0
- package/packages/cli/test/profiles/plugins/cordis.yml +122 -0
- package/packages/cli/test/profiles.integration.test.ts +40 -23
- package/packages/host-webserver/package.json +1 -1
- package/apps/web/dist/assets/index-B7pwZ76Z.js +0 -25
- package/apps/web/dist/assets/index-xF647pQJ.css +0 -2
|
@@ -29,6 +29,8 @@ export interface MarketplaceStatistics {
|
|
|
29
29
|
export type MarketplaceStatisticsFetcher = (url: string, init?: RequestInit) => Promise<Response>;
|
|
30
30
|
|
|
31
31
|
export interface MarketplaceStatisticsLoaderOptions {
|
|
32
|
+
readonly concurrency?: number;
|
|
33
|
+
readonly failureMaxTtlMs?: number;
|
|
32
34
|
readonly failureTtlMs?: number;
|
|
33
35
|
readonly fetcher?: MarketplaceStatisticsFetcher;
|
|
34
36
|
readonly now?: () => number;
|
|
@@ -36,6 +38,18 @@ export interface MarketplaceStatisticsLoaderOptions {
|
|
|
36
38
|
readonly ttlMs?: number;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
export interface MarketplaceCachedStatistics {
|
|
42
|
+
/** False until every catalogued package has been looked up once. A caller that reorders on statistics uses this to reorder once, when the set is complete, rather than on every poll while the background warm-up trickles in. */
|
|
43
|
+
readonly ready: boolean;
|
|
44
|
+
readonly statistics: ReadonlyMap<string, MarketplaceStatistics>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MarketplaceStatisticsLoader {
|
|
48
|
+
(plugins: readonly MarketplacePlugin[]): Promise<ReadonlyMap<string, MarketplaceStatistics>>;
|
|
49
|
+
// Returns only what is already cached and warms the misses in the background, so a request never waits on the npm registry.
|
|
50
|
+
readonly readCached: (plugins: readonly MarketplacePlugin[]) => MarketplaceCachedStatistics;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
interface NpmPackageStatisticsResult {
|
|
40
54
|
readonly failed: boolean;
|
|
41
55
|
readonly statistics?: MarketplaceStatistics;
|
|
@@ -55,6 +69,36 @@ export interface MarketplaceCategory {
|
|
|
55
69
|
readonly count: number;
|
|
56
70
|
}
|
|
57
71
|
|
|
72
|
+
export interface MarketplaceCapability {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly label: string;
|
|
75
|
+
readonly count: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What a plugin does to the machine it is installed on, ordered from the least invasive to the most, so a reader can decide from the badges alone. This replaced free text, which produced 299 values across 78 entries with 284 of them held by a single plugin: the filter listed nearly one option per plugin and nothing could be compared against anything else. Every value here is derived from the plugin's own imports, including those it inherits from a plugin it depends on. */
|
|
79
|
+
const MARKETPLACE_CAPABILITY_VOCABULARY: readonly { readonly id: string; readonly label: string }[] = [
|
|
80
|
+
{ id: "read-only", label: "只读运行" },
|
|
81
|
+
{ id: "session-data", label: "读取会话内容" },
|
|
82
|
+
{ id: "reads-files", label: "读取本机文件" },
|
|
83
|
+
{ id: "writes-files", label: "写入本机文件" },
|
|
84
|
+
{ id: "runs-commands", label: "执行本机命令" },
|
|
85
|
+
{ id: "local-server", label: "监听本地端口" },
|
|
86
|
+
{ id: "network-access", label: "访问网络" },
|
|
87
|
+
{ id: "model-calls", label: "额外调用模型" },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** A badge that says a plugin changes nothing is a claim about trust, so an entry that also carries one of these is rejected rather than shown. */
|
|
91
|
+
const MARKETPLACE_EFFECT_CAPABILITIES: readonly string[] = ["writes-files", "runs-commands", "local-server", "network-access", "model-calls"];
|
|
92
|
+
|
|
93
|
+
const marketplaceCapabilityLabels = new Map(MARKETPLACE_CAPABILITY_VOCABULARY.map((capability) => [capability.id, capability.label]));
|
|
94
|
+
|
|
95
|
+
function isMarketplaceCapabilityList(value: unknown): value is readonly string[] {
|
|
96
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
97
|
+
const capabilities = value.filter((entry): entry is string => typeof entry === "string" && marketplaceCapabilityLabels.has(entry));
|
|
98
|
+
if (capabilities.length !== value.length || new Set(capabilities).size !== capabilities.length) return false;
|
|
99
|
+
return !capabilities.includes("read-only") || !capabilities.some((entry) => MARKETPLACE_EFFECT_CAPABILITIES.includes(entry));
|
|
100
|
+
}
|
|
101
|
+
|
|
58
102
|
const npmPackagePattern = /^(?:@[a-z0-9._~-]+\/)?[a-z0-9._~-]+(?:\/[a-z0-9._~-]+)*$/;
|
|
59
103
|
const entryIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
60
104
|
const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
@@ -92,9 +136,7 @@ function isMarketplacePlugin(value: unknown): value is MarketplacePlugin {
|
|
|
92
136
|
categoryIdPattern.test(category.id) &&
|
|
93
137
|
typeof category.label === "string" &&
|
|
94
138
|
category.label.trim() !== "" &&
|
|
95
|
-
|
|
96
|
-
value.capabilities.length > 0 &&
|
|
97
|
-
value.capabilities.every((entry) => typeof entry === "string" && entry.trim() !== "") &&
|
|
139
|
+
isMarketplaceCapabilityList(value.capabilities) &&
|
|
98
140
|
Array.isArray(value.hooks) &&
|
|
99
141
|
value.hooks.length > 0 &&
|
|
100
142
|
value.hooks.every((entry) => typeof entry === "string" && entry.trim() !== "") &&
|
|
@@ -129,11 +171,17 @@ function loadMarketplacePlugins(): readonly MarketplacePlugin[] {
|
|
|
129
171
|
});
|
|
130
172
|
const ids = new Set<string>();
|
|
131
173
|
const packages = new Set<string>();
|
|
174
|
+
// The category tabs are built from whichever entry is read first, so two entries that spell the same category differently would make the label depend on file order.
|
|
175
|
+
const categoryLabels = new Map<string, string>();
|
|
132
176
|
for (const plugin of plugins) {
|
|
133
177
|
if (ids.has(plugin.id)) throw new Error(`Duplicate marketplace id: ${plugin.id}`);
|
|
134
178
|
if (packages.has(plugin.packageName)) throw new Error(`Duplicate marketplace package: ${plugin.packageName}`);
|
|
179
|
+
const categoryLabel = categoryLabels.get(plugin.category.id);
|
|
180
|
+
if (categoryLabel !== undefined && categoryLabel !== plugin.category.label)
|
|
181
|
+
throw new Error(`Conflicting marketplace category label for ${plugin.category.id}: ${categoryLabel} and ${plugin.category.label}`);
|
|
135
182
|
ids.add(plugin.id);
|
|
136
183
|
packages.add(plugin.packageName);
|
|
184
|
+
categoryLabels.set(plugin.category.id, plugin.category.label);
|
|
137
185
|
}
|
|
138
186
|
return plugins;
|
|
139
187
|
}
|
|
@@ -199,35 +247,82 @@ async function fetchNpmPackageStatistics(packageName: string, fetcher: Marketpla
|
|
|
199
247
|
return { failed, statistics };
|
|
200
248
|
}
|
|
201
249
|
|
|
202
|
-
export function createMarketplaceStatisticsLoader(options: MarketplaceStatisticsLoaderOptions = {}) {
|
|
250
|
+
export function createMarketplaceStatisticsLoader(options: MarketplaceStatisticsLoaderOptions = {}): MarketplaceStatisticsLoader {
|
|
203
251
|
const fetcher = options.fetcher ?? ((url: string, init?: RequestInit) => globalThis.fetch(url, init));
|
|
204
252
|
const failureTtlMs = options.failureTtlMs ?? 60_000;
|
|
253
|
+
// Consecutive failures double the retry window up to this ceiling: an open console polls the catalog every few seconds, so a flat failure window would send one registry lookup per catalogued package every minute for as long as the tab stays open.
|
|
254
|
+
const failureMaxTtlMs = options.failureMaxTtlMs ?? 30 * 60_000;
|
|
205
255
|
const now = options.now ?? Date.now;
|
|
206
256
|
const timeoutMs = options.timeoutMs ?? 3_000;
|
|
207
257
|
const ttlMs = options.ttlMs ?? 6 * 60 * 60 * 1_000;
|
|
208
|
-
|
|
258
|
+
// The registry answers an unthrottled fan-out over the whole catalogue with 429s, so lookups queue behind a small number of slots instead of leaving at once.
|
|
259
|
+
const concurrency = Math.max(1, Math.trunc(options.concurrency ?? 4));
|
|
260
|
+
const cache = new Map<string, { readonly expiresAt: number; readonly failures: number; readonly result: NpmPackageStatisticsResult }>();
|
|
209
261
|
const inFlight = new Map<string, Promise<NpmPackageStatisticsResult>>();
|
|
262
|
+
const waiting: (() => void)[] = [];
|
|
263
|
+
let active = 0;
|
|
264
|
+
const acquireSlot = async (): Promise<void> => {
|
|
265
|
+
if (active < concurrency) {
|
|
266
|
+
active += 1;
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
await new Promise<void>((resolveSlot) => waiting.push(resolveSlot));
|
|
270
|
+
};
|
|
271
|
+
const releaseSlot = (): void => {
|
|
272
|
+
const next = waiting.shift();
|
|
273
|
+
if (next === undefined) active -= 1;
|
|
274
|
+
else next();
|
|
275
|
+
};
|
|
276
|
+
const failureTtl = (failures: number): number => Math.min(failureTtlMs * 2 ** Math.min(failures - 1, 20), failureMaxTtlMs);
|
|
210
277
|
const loadPackage = async (packageName: string): Promise<NpmPackageStatisticsResult> => {
|
|
211
278
|
const timestamp = now();
|
|
212
279
|
const cached = cache.get(packageName);
|
|
213
280
|
if (cached !== undefined && cached.expiresAt > timestamp) return cached.result;
|
|
214
281
|
const pending = inFlight.get(packageName);
|
|
215
282
|
if (pending !== undefined) return pending;
|
|
216
|
-
|
|
283
|
+
// The slot is taken inside the promise so the in-flight entry is still registered synchronously and concurrent callers keep sharing one request.
|
|
284
|
+
const request = (async () => {
|
|
285
|
+
await acquireSlot();
|
|
286
|
+
try {
|
|
287
|
+
return await fetchNpmPackageStatistics(packageName, fetcher, timeoutMs);
|
|
288
|
+
} finally {
|
|
289
|
+
releaseSlot();
|
|
290
|
+
}
|
|
291
|
+
})();
|
|
217
292
|
inFlight.set(packageName, request);
|
|
218
293
|
try {
|
|
219
294
|
const result = await request;
|
|
220
|
-
|
|
295
|
+
const failures = result.failed ? (cached?.failures ?? 0) + 1 : 0;
|
|
296
|
+
cache.set(packageName, { expiresAt: now() + (result.failed ? failureTtl(failures) : ttlMs), failures, result });
|
|
221
297
|
return result;
|
|
222
298
|
} finally {
|
|
223
299
|
if (inFlight.get(packageName) === request) inFlight.delete(packageName);
|
|
224
300
|
}
|
|
225
301
|
};
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
302
|
+
const packageNames = (plugins: readonly MarketplacePlugin[]): readonly string[] =>
|
|
303
|
+
[...new Set(plugins.map((plugin) => marketplaceNpmPackageName(plugin.packageName)))].sort();
|
|
304
|
+
const load = async (plugins: readonly MarketplacePlugin[]): Promise<ReadonlyMap<string, MarketplaceStatistics>> => {
|
|
305
|
+
const entries = await Promise.all(packageNames(plugins).map(async (packageName) => [packageName, await loadPackage(packageName)] as const));
|
|
229
306
|
return new Map(entries.flatMap(([packageName, result]) => (result.statistics === undefined ? [] : [[packageName, result.statistics] as const])));
|
|
230
307
|
};
|
|
308
|
+
const readCached = (plugins: readonly MarketplacePlugin[]): MarketplaceCachedStatistics => {
|
|
309
|
+
const timestamp = now();
|
|
310
|
+
const statistics = new Map<string, MarketplaceStatistics>();
|
|
311
|
+
// Readiness tracks whether a package has ever been looked up, not whether its entry is fresh, so an expiring entry refreshes in the background without dropping the catalogue back to an unsorted order.
|
|
312
|
+
let ready = true;
|
|
313
|
+
for (const packageName of packageNames(plugins)) {
|
|
314
|
+
const cached = cache.get(packageName);
|
|
315
|
+
if (cached === undefined) ready = false;
|
|
316
|
+
if (cached !== undefined && cached.expiresAt > timestamp) {
|
|
317
|
+
if (cached.result.statistics !== undefined) statistics.set(packageName, cached.result.statistics);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
// A prewarm has nobody to report to, and a catalogue sorted without npm statistics is not worth crashing the process over an unhandled rejection.
|
|
321
|
+
void loadPackage(packageName).catch(() => {});
|
|
322
|
+
}
|
|
323
|
+
return { ready, statistics };
|
|
324
|
+
};
|
|
325
|
+
return Object.assign(load, { readCached });
|
|
231
326
|
}
|
|
232
327
|
|
|
233
328
|
export function attachMarketplaceStatistics(
|
|
@@ -259,7 +354,9 @@ export function searchMarketplace(query = "", capability = "", category = ""): r
|
|
|
259
354
|
const normalizedCapability = capability.trim().toLowerCase();
|
|
260
355
|
const normalizedCategory = category.trim().toLowerCase();
|
|
261
356
|
return MARKETPLACE_PLUGINS.filter((plugin) => {
|
|
262
|
-
|
|
357
|
+
// The capability labels are searched alongside their ids: the ids are what the filter and the URL carry, and the labels are what the reader sees on the card.
|
|
358
|
+
const capabilities = plugin.capabilities.flatMap((item) => [item, marketplaceCapabilityLabels.get(item) ?? item]);
|
|
359
|
+
const searchable = [plugin.name, plugin.packageName, plugin.description, plugin.author, ...capabilities, ...plugin.hooks].join(" ").toLowerCase();
|
|
263
360
|
return (
|
|
264
361
|
(normalizedQuery === "" || searchable.includes(normalizedQuery)) &&
|
|
265
362
|
(normalizedCapability === "" || plugin.capabilities.some((item) => item.toLowerCase() === normalizedCapability)) &&
|
|
@@ -281,7 +378,11 @@ export function paginateMarketplace(items: readonly MarketplacePlugin[], page =
|
|
|
281
378
|
};
|
|
282
379
|
}
|
|
283
380
|
|
|
284
|
-
|
|
381
|
+
// Kept in vocabulary order rather than sorted, so the filter reads from the least invasive option to the most, and an unused capability is left out rather than offering a filter that returns nothing.
|
|
382
|
+
export const MARKETPLACE_CAPABILITIES: readonly MarketplaceCapability[] = MARKETPLACE_CAPABILITY_VOCABULARY.map((capability) => ({
|
|
383
|
+
...capability,
|
|
384
|
+
count: MARKETPLACE_PLUGINS.filter((plugin) => plugin.capabilities.includes(capability.id)).length,
|
|
385
|
+
})).filter((capability) => capability.count > 0);
|
|
285
386
|
export const MARKETPLACE_CATEGORIES: readonly MarketplaceCategory[] = [
|
|
286
387
|
...new Map(MARKETPLACE_PLUGINS.map((plugin) => [plugin.category.id, { id: plugin.category.id, label: plugin.category.label, count: 0 }])).values(),
|
|
287
388
|
]
|
|
@@ -10,7 +10,7 @@ import timerPlugin from "@deepseek-ai/cordis-plugin-timer";
|
|
|
10
10
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
12
12
|
import webServerPlugin from "@pi-harness/host-webserver";
|
|
13
|
-
import { PiPluginUiRegistry, PiToolRegistry } from "@pi-harness/core";
|
|
13
|
+
import { PiPluginUiRegistry, PiToolRegistry, PiToolRegistryLeasedError } from "@pi-harness/core";
|
|
14
14
|
import type { MarketplacePlugin } from "../src/marketplace.js";
|
|
15
15
|
import apiPlugin from "../src/index.js";
|
|
16
16
|
import type * as FsPromises from "node:fs/promises";
|
|
@@ -2122,11 +2122,11 @@ describe("API gateway plugin", () => {
|
|
|
2122
2122
|
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2123
2123
|
await context.plugin(apiPlugin);
|
|
2124
2124
|
|
|
2125
|
-
const response = await fetch(context.webServer.url + "/api/marketplace?q=timer&capability=
|
|
2125
|
+
const response = await fetch(context.webServer.url + "/api/marketplace?q=timer&capability=read-only&category=runtime&page=0&pageSize=1");
|
|
2126
2126
|
expect(response.status).toBe(200);
|
|
2127
2127
|
const payload = (await response.json()) as {
|
|
2128
2128
|
items?: readonly { packageName?: unknown; status?: unknown }[];
|
|
2129
|
-
capabilities?: readonly unknown[];
|
|
2129
|
+
capabilities?: readonly { id?: unknown; label?: unknown; count?: unknown }[];
|
|
2130
2130
|
categories?: readonly { id?: unknown; label?: unknown; count?: unknown }[];
|
|
2131
2131
|
total?: number;
|
|
2132
2132
|
page?: number;
|
|
@@ -2136,7 +2136,7 @@ describe("API gateway plugin", () => {
|
|
|
2136
2136
|
expect(payload.items).toHaveLength(1);
|
|
2137
2137
|
expect(payload.items?.[0]).toMatchObject({ packageName: "@deepseek-ai/cordis-plugin-timer", status: "verified" });
|
|
2138
2138
|
expect(payload).toMatchObject({ total: 1, page: 0, pageSize: 1, hasNext: false });
|
|
2139
|
-
expect(payload.capabilities).
|
|
2139
|
+
expect(payload.capabilities).toEqual(expect.arrayContaining([expect.objectContaining({ id: "read-only", label: "只读运行" })]));
|
|
2140
2140
|
expect(payload.categories).toEqual(expect.arrayContaining([expect.objectContaining({ id: "runtime", label: "运行时", count: 1 })]));
|
|
2141
2141
|
const tooLong = await fetch(context.webServer.url + "/api/marketplace?q=" + "x".repeat(121));
|
|
2142
2142
|
expect(tooLong.status).toBe(400);
|
|
@@ -2146,6 +2146,89 @@ describe("API gateway plugin", () => {
|
|
|
2146
2146
|
expect(invalidSort.status).toBe(400);
|
|
2147
2147
|
});
|
|
2148
2148
|
|
|
2149
|
+
// The console renders nothing until every one of its startup requests has answered, so the recommended sort is served from the npm statistics already cached and the misses are warmed behind the response. A registry that never answers must cost the page nothing.
|
|
2150
|
+
test("answers the recommended marketplace sort while the npm registry never replies", async () => {
|
|
2151
|
+
const context = new Context();
|
|
2152
|
+
contexts.push(context);
|
|
2153
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2154
|
+
const session = { sessionId: "marketplace-sort-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2155
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2156
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2157
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2158
|
+
await context.plugin(apiPlugin);
|
|
2159
|
+
const originalFetch = globalThis.fetch;
|
|
2160
|
+
const registryRequests: string[] = [];
|
|
2161
|
+
globalThis.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
|
2162
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
2163
|
+
if (url.startsWith("https://registry.npmjs.org/") || url.startsWith("https://api.npmjs.org/")) {
|
|
2164
|
+
registryRequests.push(url);
|
|
2165
|
+
return new Promise<Response>(() => {});
|
|
2166
|
+
}
|
|
2167
|
+
return originalFetch(input, init);
|
|
2168
|
+
};
|
|
2169
|
+
try {
|
|
2170
|
+
const response = await fetch(context.webServer.url + "/api/marketplace?sort=recommended&pageSize=2");
|
|
2171
|
+
|
|
2172
|
+
expect(response.status).toBe(200);
|
|
2173
|
+
const payload = (await response.json()) as { items?: readonly unknown[]; page?: number; pageSize?: number };
|
|
2174
|
+
expect(payload.items).toHaveLength(2);
|
|
2175
|
+
expect(payload).toMatchObject({ page: 0, pageSize: 2 });
|
|
2176
|
+
// The background prewarm queues behind a handful of slots instead of asking the registry about every catalogued package at once.
|
|
2177
|
+
await vi.waitFor(() => expect(registryRequests.length).toBeGreaterThan(0));
|
|
2178
|
+
expect(registryRequests.length).toBeLessThanOrEqual(4);
|
|
2179
|
+
} finally {
|
|
2180
|
+
globalThis.fetch = originalFetch;
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
|
|
2184
|
+
// The message goes straight to a console user who clicked a button, so it reads as npm's own report; the command line stays on the error cause for the log.
|
|
2185
|
+
test("reports a failed plugin install in npm's own words without the command line", async () => {
|
|
2186
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-install-failure-"));
|
|
2187
|
+
temporaryDirectories.push(directory);
|
|
2188
|
+
const shimDirectory = join(directory, "bin");
|
|
2189
|
+
await mkdir(shimDirectory);
|
|
2190
|
+
await writeFile(join(directory, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
2191
|
+
const configPath = join(directory, "profile.yml");
|
|
2192
|
+
const profileBefore = '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n';
|
|
2193
|
+
await writeFile(configPath, profileBefore, "utf8");
|
|
2194
|
+
await writeFile(
|
|
2195
|
+
join(shimDirectory, "npm"),
|
|
2196
|
+
"#!/bin/sh\nprintf 'npm error code E404\\nnpm error 404 Not Found - GET https://registry.npmjs.org/@pi-harness/plugin-skill-guard\\nnpm error 404 The package is not in the npm registry\\nnpm error A complete log of this run can be found in: /tmp/npm-debug.log\\n' >&2\nexit 1\n",
|
|
2197
|
+
{ mode: 0o755 },
|
|
2198
|
+
);
|
|
2199
|
+
const context = new Context();
|
|
2200
|
+
contexts.push(context);
|
|
2201
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2202
|
+
const session = { sessionId: "install-failure-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2203
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2204
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2205
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
|
|
2206
|
+
context.reflect.provide("loader", { entries: () => [], create: () => Promise.resolve("marketplace-entry"), remove: () => Promise.resolve() });
|
|
2207
|
+
await context.plugin(apiPlugin);
|
|
2208
|
+
|
|
2209
|
+
const originalPath = process.env.PATH ?? "";
|
|
2210
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
2211
|
+
let response: Response;
|
|
2212
|
+
try {
|
|
2213
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
2214
|
+
method: "POST",
|
|
2215
|
+
headers: { "content-type": "application/json" },
|
|
2216
|
+
body: JSON.stringify({ id: "skill-guard" }),
|
|
2217
|
+
});
|
|
2218
|
+
} finally {
|
|
2219
|
+
process.env.PATH = originalPath;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
expect(response.status).toBe(502);
|
|
2223
|
+
const payload = (await response.json()) as { error?: string };
|
|
2224
|
+
expect(payload.error).toBe(
|
|
2225
|
+
"npm error code E404 npm error 404 Not Found - GET https://registry.npmjs.org/@pi-harness/plugin-skill-guard npm error 404 The package is not in the npm registry",
|
|
2226
|
+
);
|
|
2227
|
+
expect(payload.error).not.toContain("--save-exact");
|
|
2228
|
+
expect(payload.error).not.toContain("npm-debug.log");
|
|
2229
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
|
|
2230
|
+
});
|
|
2231
|
+
|
|
2149
2232
|
// An official plugin is its own npm package, so it takes the same install path a community package takes: npm first, then the profile row, then the loader entry. Nothing about `source: "official"` shortcuts any of it.
|
|
2150
2233
|
test("installs an official plugin through npm exactly like a community package", async () => {
|
|
2151
2234
|
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-install-"));
|
|
@@ -2190,12 +2273,167 @@ describe("API gateway plugin", () => {
|
|
|
2190
2273
|
}
|
|
2191
2274
|
|
|
2192
2275
|
expect(response.status).toBe(200);
|
|
2193
|
-
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: "skill-guard" } });
|
|
2276
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, restartRequired: false, plugin: { id: "skill-guard" } });
|
|
2194
2277
|
expect((await readFile(npmLog, "utf8")).trim()).toMatch(/^install --save-exact --package-lock=false @pi-harness\/plugin-skill-guard@\d/u);
|
|
2195
2278
|
expect(created).toEqual([expect.objectContaining({ name: "@pi-harness/plugin-skill-guard" })]);
|
|
2196
2279
|
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@pi-harness/plugin-skill-guard"');
|
|
2197
2280
|
});
|
|
2198
2281
|
|
|
2282
|
+
// A plugin registers its tools while it activates, and pi-runtime takes the tool registry when it activates, so an installed entry only works if it sits ahead of the runtime inside the runtime's own group.
|
|
2283
|
+
test("installs a marketplace plugin into the runtime's group ahead of the runtime", async () => {
|
|
2284
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-placement-"));
|
|
2285
|
+
temporaryDirectories.push(directory);
|
|
2286
|
+
const shimDirectory = join(directory, "bin");
|
|
2287
|
+
await mkdir(shimDirectory);
|
|
2288
|
+
await writeFile(join(directory, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
2289
|
+
const configPath = join(directory, "profile.yml");
|
|
2290
|
+
await writeFile(
|
|
2291
|
+
configPath,
|
|
2292
|
+
'- id: agent\n name: cordis:group\n group: true\n config:\n - id: tools\n name: "@pi-harness/core/plugins/tools"\n config: {}\n - id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n',
|
|
2293
|
+
"utf8",
|
|
2294
|
+
);
|
|
2295
|
+
await writeFile(join(shimDirectory, "npm"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
2296
|
+
const context = new Context();
|
|
2297
|
+
contexts.push(context);
|
|
2298
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2299
|
+
const session = { sessionId: "marketplace-placement-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2300
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2301
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2302
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
|
|
2303
|
+
const runtimeOptions = { id: "runtime", name: "@pi-harness/core/plugins/runtime", config: {} };
|
|
2304
|
+
const runtimeGroup = { data: [{ id: "tools", name: "@pi-harness/core/plugins/tools", config: {} }, runtimeOptions] };
|
|
2305
|
+
const loaderEntries = [
|
|
2306
|
+
// The loader addresses a nested group by its qualified id, while the profile file names the same entry by its own id.
|
|
2307
|
+
{ id: "profile:agent", options: { id: "agent", name: "cordis:group" }, subgroup: runtimeGroup },
|
|
2308
|
+
{ id: "profile:runtime", options: runtimeOptions, parent: runtimeGroup },
|
|
2309
|
+
];
|
|
2310
|
+
const created: Array<[unknown, unknown, unknown]> = [];
|
|
2311
|
+
context.reflect.provide("loader", {
|
|
2312
|
+
entries: () => loaderEntries,
|
|
2313
|
+
create: (options: unknown, parent: unknown, position: unknown) => {
|
|
2314
|
+
created.push([options, parent, position]);
|
|
2315
|
+
return Promise.resolve("marketplace-entry");
|
|
2316
|
+
},
|
|
2317
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
2318
|
+
remove: () => Promise.resolve(),
|
|
2319
|
+
});
|
|
2320
|
+
await context.plugin(apiPlugin);
|
|
2321
|
+
|
|
2322
|
+
const originalPath = process.env.PATH ?? "";
|
|
2323
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
2324
|
+
let response: Response;
|
|
2325
|
+
try {
|
|
2326
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
2327
|
+
method: "POST",
|
|
2328
|
+
headers: { "content-type": "application/json" },
|
|
2329
|
+
body: JSON.stringify({ id: "skill-guard" }),
|
|
2330
|
+
});
|
|
2331
|
+
} finally {
|
|
2332
|
+
process.env.PATH = originalPath;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
expect(response.status).toBe(200);
|
|
2336
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, restartRequired: false });
|
|
2337
|
+
expect(created).toEqual([[expect.objectContaining({ id: "marketplace-skill-guard", name: "@pi-harness/plugin-skill-guard" }), "profile:agent", 1]]);
|
|
2338
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe(
|
|
2339
|
+
'- id: agent\n name: cordis:group\n group: true\n config:\n - id: tools\n name: "@pi-harness/core/plugins/tools"\n config: {}\n - id: marketplace-skill-guard\n name: "@pi-harness/plugin-skill-guard"\n config: {}\n - id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n',
|
|
2340
|
+
);
|
|
2341
|
+
});
|
|
2342
|
+
|
|
2343
|
+
// The runtime snapshots the tool set when it takes the registry, so a plugin that contributes tools cannot join a harness that is already running. Rolling the install back would leave the user unable to install it at all, so the package and the profile row stay and the console asks for a restart.
|
|
2344
|
+
test("keeps an installed plugin in place when the runtime already leased the tool registry", async () => {
|
|
2345
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-leased-"));
|
|
2346
|
+
temporaryDirectories.push(directory);
|
|
2347
|
+
const shimDirectory = join(directory, "bin");
|
|
2348
|
+
await mkdir(shimDirectory);
|
|
2349
|
+
await writeFile(join(directory, "package.json"), '{ "name": "harness", "dependencies": {} }\n', "utf8");
|
|
2350
|
+
const configPath = join(directory, "profile.yml");
|
|
2351
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
2352
|
+
await writeFile(join(shimDirectory, "npm"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
2353
|
+
const context = new Context();
|
|
2354
|
+
contexts.push(context);
|
|
2355
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2356
|
+
const session = { sessionId: "marketplace-leased-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2357
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2358
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2359
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
|
|
2360
|
+
const removed: string[] = [];
|
|
2361
|
+
context.reflect.provide("loader", {
|
|
2362
|
+
entries: () => [],
|
|
2363
|
+
create: () => Promise.resolve("marketplace-entry"),
|
|
2364
|
+
// The loader wraps an activation failure in its own error chain, which is what the gateway has to see through.
|
|
2365
|
+
resolve: () => ({
|
|
2366
|
+
fiber: {
|
|
2367
|
+
await: () => Promise.reject(new Error("failed to apply loader entry", { cause: new PiToolRegistryLeasedError("skill_scan") })),
|
|
2368
|
+
},
|
|
2369
|
+
}),
|
|
2370
|
+
remove: (id: string) => {
|
|
2371
|
+
removed.push(id);
|
|
2372
|
+
return Promise.resolve();
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
await context.plugin(apiPlugin);
|
|
2376
|
+
|
|
2377
|
+
const originalPath = process.env.PATH ?? "";
|
|
2378
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
2379
|
+
let response: Response;
|
|
2380
|
+
try {
|
|
2381
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
2382
|
+
method: "POST",
|
|
2383
|
+
headers: { "content-type": "application/json" },
|
|
2384
|
+
body: JSON.stringify({ id: "skill-guard" }),
|
|
2385
|
+
});
|
|
2386
|
+
} finally {
|
|
2387
|
+
process.env.PATH = originalPath;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
expect(response.status).toBe(200);
|
|
2391
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, restartRequired: true, plugin: { id: "skill-guard" } });
|
|
2392
|
+
expect(removed).toEqual(["marketplace-entry"]);
|
|
2393
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@pi-harness/plugin-skill-guard"');
|
|
2394
|
+
await expect(readFile(join(directory, "package.json"), "utf8")).resolves.toBe('{ "name": "harness", "dependencies": {} }\n');
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
// Reverting the profile here would leave a plugin that can be switched off but never on again, so the change stays and the plugin comes back on the next start.
|
|
2398
|
+
test("keeps a re-enabled plugin enabled in the profile when the runtime already leased the tool registry", async () => {
|
|
2399
|
+
const context = new Context();
|
|
2400
|
+
contexts.push(context);
|
|
2401
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-toggle-leased-"));
|
|
2402
|
+
temporaryDirectories.push(directory);
|
|
2403
|
+
const configPath = join(directory, "profile.yml");
|
|
2404
|
+
const loaderEntry = {
|
|
2405
|
+
id: "profile:marketplace-skill-guard",
|
|
2406
|
+
options: { id: "marketplace-skill-guard", name: "@pi-harness/plugin-skill-guard", config: {} },
|
|
2407
|
+
update: () => Promise.reject(new Error("failed to apply loader entry", { cause: new PiToolRegistryLeasedError("skill_scan") })),
|
|
2408
|
+
};
|
|
2409
|
+
await writeFile(
|
|
2410
|
+
configPath,
|
|
2411
|
+
'- id: marketplace-skill-guard\n name: "@pi-harness/plugin-skill-guard"\n disabled: true\n config: {}\n- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n',
|
|
2412
|
+
"utf8",
|
|
2413
|
+
);
|
|
2414
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2415
|
+
const session = { sessionId: "plugin-toggle-leased-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2416
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2417
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2418
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
|
|
2419
|
+
context.reflect.provide("loader", {
|
|
2420
|
+
*entries() {
|
|
2421
|
+
yield loaderEntry;
|
|
2422
|
+
},
|
|
2423
|
+
});
|
|
2424
|
+
await context.plugin(apiPlugin);
|
|
2425
|
+
|
|
2426
|
+
const response = await fetch(context.webServer.url + "/api/plugins/toggle", {
|
|
2427
|
+
method: "POST",
|
|
2428
|
+
headers: { "content-type": "application/json" },
|
|
2429
|
+
body: JSON.stringify({ id: "marketplace-skill-guard", enabled: true }),
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
expect(response.status).toBe(200);
|
|
2433
|
+
await expect(response.json()).resolves.toMatchObject({ restartRequired: true, plugin: { id: "marketplace-skill-guard" } });
|
|
2434
|
+
await expect(readFile(configPath, "utf8")).resolves.not.toContain("disabled: true");
|
|
2435
|
+
});
|
|
2436
|
+
|
|
2199
2437
|
test("locks marketplace installation before reading a request body", async () => {
|
|
2200
2438
|
const cwd = await mkdtemp(join(tmpdir(), "pi-harness-api-plugin-install-lock-"));
|
|
2201
2439
|
temporaryDirectories.push(cwd);
|
|
@@ -2455,6 +2693,60 @@ describe("API gateway plugin", () => {
|
|
|
2455
2693
|
await expect(readFile(configPath, "utf8")).resolves.toContain("- id: sibling");
|
|
2456
2694
|
});
|
|
2457
2695
|
|
|
2696
|
+
test("reports a failed plugin uninstall in npm's own words without the command line", async () => {
|
|
2697
|
+
const context = new Context();
|
|
2698
|
+
contexts.push(context);
|
|
2699
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-uninstall-failure-"));
|
|
2700
|
+
temporaryDirectories.push(directory);
|
|
2701
|
+
const shimDirectory = join(directory, "bin");
|
|
2702
|
+
await mkdir(shimDirectory);
|
|
2703
|
+
await writeFile(join(directory, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
2704
|
+
await writeFile(join(shimDirectory, "npm"), "#!/bin/sh\nprintf 'npm error code EACCES\\nnpm error syscall unlink\\n' >&2\nexit 1\n", { mode: 0o755 });
|
|
2705
|
+
const configPath = join(directory, "profile.yml");
|
|
2706
|
+
const entryId = "skill-guard";
|
|
2707
|
+
const loaderEntry = {
|
|
2708
|
+
id: `profile:${entryId}`,
|
|
2709
|
+
options: { id: entryId, name: "@pi-harness/plugin-skill-guard", config: {} },
|
|
2710
|
+
parent: { tree: { write() {} }, remove: () => Promise.resolve() },
|
|
2711
|
+
};
|
|
2712
|
+
const profileBefore = `- id: ${entryId}\n name: ${JSON.stringify(loaderEntry.options.name)}\n config: {}\n- id: sibling\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n`;
|
|
2713
|
+
await writeFile(configPath, profileBefore, "utf8");
|
|
2714
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2715
|
+
const session = { sessionId: "uninstall-failure-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2716
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2717
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2718
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: directory, configPath, args: [], requestExit() {} });
|
|
2719
|
+
const restored: unknown[] = [];
|
|
2720
|
+
context.reflect.provide("loader", {
|
|
2721
|
+
entries: () => [loaderEntry],
|
|
2722
|
+
create: (options: unknown) => {
|
|
2723
|
+
restored.push(options);
|
|
2724
|
+
return Promise.resolve(`profile:${entryId}`);
|
|
2725
|
+
},
|
|
2726
|
+
});
|
|
2727
|
+
await context.plugin(apiPlugin);
|
|
2728
|
+
|
|
2729
|
+
const originalPath = process.env.PATH ?? "";
|
|
2730
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
2731
|
+
let response: Response;
|
|
2732
|
+
try {
|
|
2733
|
+
response = await fetch(context.webServer.url + "/api/plugins/uninstall", {
|
|
2734
|
+
method: "POST",
|
|
2735
|
+
headers: { "content-type": "application/json" },
|
|
2736
|
+
body: JSON.stringify({ id: entryId }),
|
|
2737
|
+
});
|
|
2738
|
+
} finally {
|
|
2739
|
+
process.env.PATH = originalPath;
|
|
2740
|
+
}
|
|
2741
|
+
|
|
2742
|
+
expect(response.status).toBe(502);
|
|
2743
|
+
const payload = (await response.json()) as { error?: string };
|
|
2744
|
+
expect(payload.error).toBe("npm error code EACCES npm error syscall unlink");
|
|
2745
|
+
expect(payload.error).not.toContain("--package-lock=false");
|
|
2746
|
+
expect(restored).toEqual([expect.objectContaining({ id: entryId, name: "@pi-harness/plugin-skill-guard" })]);
|
|
2747
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
|
|
2748
|
+
});
|
|
2749
|
+
|
|
2458
2750
|
test("commits selected workspace files only after an explicit message", async () => {
|
|
2459
2751
|
const context = new Context();
|
|
2460
2752
|
contexts.push(context);
|
|
@@ -31,6 +31,9 @@ const supportedKeywords = new Set([
|
|
|
31
31
|
"if",
|
|
32
32
|
"then",
|
|
33
33
|
"else",
|
|
34
|
+
"allOf",
|
|
35
|
+
"contains",
|
|
36
|
+
"not",
|
|
34
37
|
]);
|
|
35
38
|
|
|
36
39
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -75,6 +78,8 @@ function validate(schema: Schema, value: unknown, path = "$"): string[] {
|
|
|
75
78
|
if (typeof schema.minItems === "number" && value.length < schema.minItems) errors.push(`${path}: fewer than ${schema.minItems} items`);
|
|
76
79
|
if (schema.uniqueItems === true && new Set(value.map((item) => JSON.stringify(item))).size !== value.length) errors.push(`${path}: items are not unique`);
|
|
77
80
|
if (isRecord(schema.items)) value.forEach((item, index) => errors.push(...validate(schema.items as Schema, item, `${path}[${index}]`)));
|
|
81
|
+
if (isRecord(schema.contains) && !value.some((item) => validate(schema.contains as Schema, item, path).length === 0))
|
|
82
|
+
errors.push(`${path}: contains no item matching ${JSON.stringify(schema.contains)}`);
|
|
78
83
|
}
|
|
79
84
|
if (isRecord(value)) {
|
|
80
85
|
const properties = isRecord(schema.properties) ? schema.properties : {};
|
|
@@ -91,6 +96,8 @@ function validate(schema: Schema, value: unknown, path = "$"): string[] {
|
|
|
91
96
|
const branch = validate(schema.if, value, path).length === 0 ? schema.then : schema.else;
|
|
92
97
|
if (isRecord(branch)) errors.push(...validate(branch, value, path));
|
|
93
98
|
}
|
|
99
|
+
if (isRecord(schema.not) && validate(schema.not, value, path).length === 0) errors.push(`${path}: matches ${JSON.stringify(schema.not)}`);
|
|
100
|
+
if (Array.isArray(schema.allOf)) for (const branch of schema.allOf) if (isRecord(branch)) errors.push(...validate(branch, value, path));
|
|
94
101
|
return errors;
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -133,6 +140,11 @@ describe("marketplace entry schema", () => {
|
|
|
133
140
|
expect(validate(schema, { ...validEntry(), profile: { name: "@pi-harness/plugin-agent-teams", config: [] } })).not.toEqual([]);
|
|
134
141
|
expect(validate(schema, { ...groupEntry(), profile: { name: "@deepseek-ai/cordis-plugin-group", group: true, config: {} } })).not.toEqual([]);
|
|
135
142
|
expect(validate(schema, { ...validEntry(), capabilities: [] })).not.toEqual([]);
|
|
143
|
+
expect(validate(schema, { ...validEntry(), capabilities: ["bounded skill packaging"] })).not.toEqual([]);
|
|
144
|
+
expect(validate(schema, { ...validEntry(), capabilities: ["read-only", "read-only"] })).not.toEqual([]);
|
|
145
|
+
// A plugin cannot claim it changes nothing and also declare an effect; the console shows that claim as a badge next to the Install button.
|
|
146
|
+
expect(validate(schema, { ...validEntry(), capabilities: ["read-only", "runs-commands"] })).not.toEqual([]);
|
|
147
|
+
expect(validate(schema, { ...validEntry(), capabilities: ["session-data", "runs-commands"] })).toEqual([]);
|
|
136
148
|
expect(validate(schema, { ...validEntry(), repository: "http://example.com" })).not.toEqual([]);
|
|
137
149
|
expect(validate(schema, { ...validEntry(), unexpected: true })).not.toEqual([]);
|
|
138
150
|
});
|