@aiwg/cli 0.0.0-bootstrap.0 → 2026.7.18
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/LICENSE +21 -0
- package/README.md +22 -1
- package/bin/aiwg.mjs +349 -0
- package/dist/src/a2a/agent-card.js +145 -0
- package/dist/src/a2a/client.js +411 -0
- package/dist/src/a2a/hitl-cli.js +171 -0
- package/dist/src/a2a/hitl-driver.js +370 -0
- package/dist/src/a2a/hitl.js +181 -0
- package/dist/src/a2a/http.js +193 -0
- package/dist/src/a2a/jcs.js +128 -0
- package/dist/src/a2a/jws.js +187 -0
- package/dist/src/a2a/types.js +24 -0
- package/dist/src/a2a/webhook.js +240 -0
- package/dist/src/activity-log/cli.js +348 -0
- package/dist/src/activity-log/parser.js +81 -0
- package/dist/src/activity-log/types.js +50 -0
- package/dist/src/agents/agent-deployer.js +367 -0
- package/dist/src/agents/agent-packager.js +271 -0
- package/dist/src/agents/agent-validator.js +271 -0
- package/dist/src/agents/grounding/knowledge-base.js +161 -0
- package/dist/src/agents/grounding/types.js +12 -0
- package/dist/src/agents/index.js +10 -0
- package/dist/src/agents/packaged-agent-inventory.js +75 -0
- package/dist/src/agents/types.js +7 -0
- package/dist/src/api/index.d.ts +10 -0
- package/dist/src/api/index.js +10 -0
- package/dist/src/artifacts/address-parser.js +73 -0
- package/dist/src/artifacts/audit/cli.js +183 -0
- package/dist/src/artifacts/audit/drift.js +122 -0
- package/dist/src/artifacts/audit/types.js +12 -0
- package/dist/src/artifacts/backends/graphology-backend.js +160 -0
- package/dist/src/artifacts/backends/json-backend.js +129 -0
- package/dist/src/artifacts/backends/sqlite-backend.js +199 -0
- package/dist/src/artifacts/browser-export.js +622 -0
- package/dist/src/artifacts/capability-resolver.js +94 -0
- package/dist/src/artifacts/checksum-manifest.js +91 -0
- package/dist/src/artifacts/citation-parser.js +162 -0
- package/dist/src/artifacts/cli.js +1486 -0
- package/dist/src/artifacts/corpus-tools/citation-densify.js +311 -0
- package/dist/src/artifacts/corpus-tools/cli.js +393 -0
- package/dist/src/artifacts/corpus-tools/corpus-graph.js +225 -0
- package/dist/src/artifacts/corpus-tools/curator.js +186 -0
- package/dist/src/artifacts/corpus-tools/discovery-log.js +70 -0
- package/dist/src/artifacts/corpus-tools/funder-network.js +141 -0
- package/dist/src/artifacts/corpus-tools/induction-audit.js +233 -0
- package/dist/src/artifacts/corpus-tools/integrity-scan.js +202 -0
- package/dist/src/artifacts/corpus-tools/profile-communities.js +166 -0
- package/dist/src/artifacts/corpus-tools/profile-edges.js +72 -0
- package/dist/src/artifacts/corpus-tools/profile-embed.js +113 -0
- package/dist/src/artifacts/corpus-tools/profile-generate-fm.js +167 -0
- package/dist/src/artifacts/corpus-tools/profile-generate.js +181 -0
- package/dist/src/artifacts/corpus-tools/profile-metrics.js +98 -0
- package/dist/src/artifacts/corpus-tools/profile-status.js +86 -0
- package/dist/src/artifacts/corpus-tools/profile-temporal.js +139 -0
- package/dist/src/artifacts/corpus-tools/radar-init.js +93 -0
- package/dist/src/artifacts/corpus-tools/radar-report.js +118 -0
- package/dist/src/artifacts/corpus-tools/radar-shared.js +189 -0
- package/dist/src/artifacts/corpus-tools/radar-status.js +68 -0
- package/dist/src/artifacts/corpus-tools/sidecar-lint.js +233 -0
- package/dist/src/artifacts/corpus-tools/sidecar-repair.js +277 -0
- package/dist/src/artifacts/corpus-tools/snapshot.js +591 -0
- package/dist/src/artifacts/corpus-tools/source-types.js +164 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +243 -0
- package/dist/src/artifacts/corpus-views/build.js +126 -0
- package/dist/src/artifacts/corpus-views/corpus-config.js +124 -0
- package/dist/src/artifacts/corpus-views/ref-parser.js +437 -0
- package/dist/src/artifacts/corpus-views/renderers.js +353 -0
- package/dist/src/artifacts/corpus-views/taxonomies.js +131 -0
- package/dist/src/artifacts/dep-graph.js +173 -0
- package/dist/src/artifacts/discover-facets.js +375 -0
- package/dist/src/artifacts/embedding-index.js +354 -0
- package/dist/src/artifacts/enrichment/cli.js +240 -0
- package/dist/src/artifacts/enrichment/prompt.js +47 -0
- package/dist/src/artifacts/enrichment/store.js +90 -0
- package/dist/src/artifacts/enrichment/types.js +12 -0
- package/dist/src/artifacts/fortemi-core-query-adapter.js +475 -0
- package/dist/src/artifacts/fortemi-core-sync.js +251 -0
- package/dist/src/artifacts/fortemi-shard-export.js +44 -0
- package/dist/src/artifacts/fulltext.js +108 -0
- package/dist/src/artifacts/graph-backend.js +51 -0
- package/dist/src/artifacts/graph-query.js +247 -0
- package/dist/src/artifacts/hybrid-query.js +225 -0
- package/dist/src/artifacts/index-builder.js +1089 -0
- package/dist/src/artifacts/index-reader.js +112 -0
- package/dist/src/artifacts/index-status.js +193 -0
- package/dist/src/artifacts/legacy-index-migration.js +169 -0
- package/dist/src/artifacts/move.js +154 -0
- package/dist/src/artifacts/operational-state.js +239 -0
- package/dist/src/artifacts/prebuilt-build-lock.js +49 -0
- package/dist/src/artifacts/query-engine.js +1955 -0
- package/dist/src/artifacts/source-graph.js +529 -0
- package/dist/src/artifacts/stats.js +169 -0
- package/dist/src/artifacts/types.js +606 -0
- package/dist/src/artifacts/views/cli.js +293 -0
- package/dist/src/artifacts/views/definition.js +170 -0
- package/dist/src/artifacts/views/store.js +149 -0
- package/dist/src/artifacts/views/types.js +8 -0
- package/dist/src/artifacts/watcher.js +255 -0
- package/dist/src/catalog/builtin-models.json +636 -0
- package/dist/src/catalog/cli.js +228 -0
- package/dist/src/catalog/cli.mjs +289 -0
- package/dist/src/catalog/index.js +15 -0
- package/dist/src/catalog/loader.js +217 -0
- package/dist/src/catalog/loader.mjs +251 -0
- package/dist/src/catalog/sources.json +41 -0
- package/dist/src/catalog/types.js +8 -0
- package/dist/src/channel/manager.mjs +504 -0
- package/dist/src/cli/agent-spawn.js +178 -0
- package/dist/src/cli/cli-extension-loader.js +245 -0
- package/dist/src/cli/command-log.js +275 -0
- package/dist/src/cli/config-loader.js +232 -0
- package/dist/src/cli/env.js +82 -0
- package/dist/src/cli/errors.js +150 -0
- package/dist/src/cli/find-package-root.js +40 -0
- package/dist/src/cli/git-hooks.js +250 -0
- package/dist/src/cli/handlers/agentcard.js +206 -0
- package/dist/src/cli/handlers/artifacts.js +76 -0
- package/dist/src/cli/handlers/best-practices-audit.js +280 -0
- package/dist/src/cli/handlers/cockpit.js +137 -0
- package/dist/src/cli/handlers/command-log.js +37 -0
- package/dist/src/cli/handlers/daemon.js +59 -0
- package/dist/src/cli/handlers/diagnose.js +300 -0
- package/dist/src/cli/handlers/execution-mode.js +105 -0
- package/dist/src/cli/handlers/feedback.js +341 -0
- package/dist/src/cli/handlers/help.js +147 -0
- package/dist/src/cli/handlers/index.js +302 -0
- package/dist/src/cli/handlers/init.js +179 -0
- package/dist/src/cli/handlers/install.js +129 -0
- package/dist/src/cli/handlers/issues.js +46 -0
- package/dist/src/cli/handlers/lint.js +42 -0
- package/dist/src/cli/handlers/local-executor.js +315 -0
- package/dist/src/cli/handlers/marketplace.js +168 -0
- package/dist/src/cli/handlers/mc.js +1010 -0
- package/dist/src/cli/handlers/models.js +352 -0
- package/dist/src/cli/handlers/packages.js +155 -0
- package/dist/src/cli/handlers/ralph-launcher.js +717 -0
- package/dist/src/cli/handlers/ralph.js +575 -0
- package/dist/src/cli/handlers/refresh.js +432 -0
- package/dist/src/cli/handlers/regenerate.js +336 -0
- package/dist/src/cli/handlers/repo-access.js +109 -0
- package/dist/src/cli/handlers/run.js +165 -0
- package/dist/src/cli/handlers/runtime-info.js +301 -0
- package/dist/src/cli/handlers/sandbox.js +197 -0
- package/dist/src/cli/handlers/scaffolding.js +149 -0
- package/dist/src/cli/handlers/script-runner.js +121 -0
- package/dist/src/cli/handlers/sdlc-accelerate.js +148 -0
- package/dist/src/cli/handlers/serve.js +1835 -0
- package/dist/src/cli/handlers/session.js +348 -0
- package/dist/src/cli/handlers/setup.js +440 -0
- package/dist/src/cli/handlers/skill-lint.js +367 -0
- package/dist/src/cli/handlers/skill-usage.js +74 -0
- package/dist/src/cli/handlers/steward.js +567 -0
- package/dist/src/cli/handlers/subcommands.js +1702 -0
- package/dist/src/cli/handlers/team.js +337 -0
- package/dist/src/cli/handlers/types.js +11 -0
- package/dist/src/cli/handlers/use.js +2772 -0
- package/dist/src/cli/handlers/utilities.js +703 -0
- package/dist/src/cli/handlers/version.js +134 -0
- package/dist/src/cli/handlers/workspace-context.js +93 -0
- package/dist/src/cli/handlers/workspace.js +126 -0
- package/dist/src/cli/help-generator.js +178 -0
- package/dist/src/cli/hooks/builtin/activity-log-hook.js +126 -0
- package/dist/src/cli/hooks/executor.js +96 -0
- package/dist/src/cli/hooks/index.js +15 -0
- package/dist/src/cli/hooks/registry.js +143 -0
- package/dist/src/cli/hooks/types.js +13 -0
- package/dist/src/cli/log.js +492 -0
- package/dist/src/cli/project-isolation/detect.js +70 -0
- package/dist/src/cli/project-isolation/index.js +7 -0
- package/dist/src/cli/project-isolation/signals.js +17 -0
- package/dist/src/cli/project-isolation/warning.js +102 -0
- package/dist/src/cli/prompt-utils.js +155 -0
- package/dist/src/cli/provider-deployment-plan.js +126 -0
- package/dist/src/cli/provider-resolution.js +119 -0
- package/dist/src/cli/router.js +305 -0
- package/dist/src/cli/scope-resolver.js +392 -0
- package/dist/src/cli/skill-usage.js +616 -0
- package/dist/src/cli/ui.js +224 -0
- package/dist/src/cli/watch-service.js +203 -0
- package/dist/src/cli/workflow-orchestrator.js +478 -0
- package/dist/src/cli/workspace-signals.js +321 -0
- package/dist/src/community/footer.js +14 -0
- package/dist/src/community/links.js +123 -0
- package/dist/src/community/milestones.js +46 -0
- package/dist/src/community/nudge-policy.js +93 -0
- package/dist/src/config/aiwg-config.js +883 -0
- package/dist/src/config/cli.js +703 -0
- package/dist/src/config/gitignore.js +156 -0
- package/dist/src/config/project-artifacts.js +76 -0
- package/dist/src/config/user-config.js +428 -0
- package/dist/src/config/user-registry.js +127 -0
- package/dist/src/config/workspace.js +276 -0
- package/dist/src/contributors/detect.js +39 -0
- package/dist/src/contributors/discover.js +172 -0
- package/dist/src/contributors/heuristic.js +186 -0
- package/dist/src/contributors/index.js +11 -0
- package/dist/src/contributors/types.js +13 -0
- package/dist/src/contributors/validation.js +116 -0
- package/dist/src/data/community.schema.json +43 -0
- package/dist/src/data/community.yaml +16 -0
- package/dist/src/extensions/capability-index.js +270 -0
- package/dist/src/extensions/claude-hooks-installer.js +268 -0
- package/dist/src/extensions/commands/definitions.js +3485 -0
- package/dist/src/extensions/deployment-registration.js +463 -0
- package/dist/src/extensions/index.js +22 -0
- package/dist/src/extensions/loader.js +132 -0
- package/dist/src/extensions/managed-marker.js +100 -0
- package/dist/src/extensions/manifest.js +196 -0
- package/dist/src/extensions/project-local-activity.js +114 -0
- package/dist/src/extensions/project-local-discovery.js +261 -0
- package/dist/src/extensions/project-local-doctor.js +281 -0
- package/dist/src/extensions/project-local-gitignore.js +176 -0
- package/dist/src/extensions/project-local-paths.js +121 -0
- package/dist/src/extensions/project-local-promote.js +218 -0
- package/dist/src/extensions/project-local-remove.js +373 -0
- package/dist/src/extensions/project-local-scaffold.js +252 -0
- package/dist/src/extensions/project-quickref.js +291 -0
- package/dist/src/extensions/registry.js +368 -0
- package/dist/src/extensions/shadow-resolver.js +271 -0
- package/dist/src/extensions/types.js +86 -0
- package/dist/src/extensions/upstream-registry.js +195 -0
- package/dist/src/extensions/validation.js +631 -0
- package/dist/src/features/catalog.js +88 -0
- package/dist/src/features/cli.js +197 -0
- package/dist/src/features/installer.js +57 -0
- package/dist/src/features/paths.js +14 -0
- package/dist/src/features/runtime.js +21 -0
- package/dist/src/features/status.js +134 -0
- package/dist/src/hooks/laziness-detection.js +532 -0
- package/dist/src/intake/git-history-analyzer.js +486 -0
- package/dist/src/intake/index.js +9 -0
- package/dist/src/issues/cli.js +327 -0
- package/dist/src/issues/index.js +5 -0
- package/dist/src/issues/live.js +154 -0
- package/dist/src/issues/local.js +666 -0
- package/dist/src/issues/sync.js +143 -0
- package/dist/src/issues/types.js +2 -0
- package/dist/src/issues/workflows.js +164 -0
- package/dist/src/kb/cli.js +173 -0
- package/dist/src/lint/cli.js +171 -0
- package/dist/src/lint/loader.js +225 -0
- package/dist/src/lint/reporters.js +141 -0
- package/dist/src/lint/runner.js +364 -0
- package/dist/src/lint/types.js +10 -0
- package/dist/src/marketplace/cache.js +110 -0
- package/dist/src/marketplace/identifier.js +63 -0
- package/dist/src/marketplace/registry.js +36 -0
- package/dist/src/marketplace/sources/clawhub.js +53 -0
- package/dist/src/marketplace/sources/git.js +45 -0
- package/dist/src/marketplace/types.js +9 -0
- package/dist/src/mcp/adapters/codex-runtime.js +224 -0
- package/dist/src/mcp/cli.mjs +1197 -0
- package/dist/src/mcp/elicitation.mjs +149 -0
- package/dist/src/mcp/helpers.mjs +287 -0
- package/dist/src/mcp/index.mjs +11 -0
- package/dist/src/mcp/profiles.js +263 -0
- package/dist/src/mcp/profiles.mjs +360 -0
- package/dist/src/mcp/registry.js +376 -0
- package/dist/src/mcp/registry.mjs +387 -0
- package/dist/src/mcp/server.mjs +633 -0
- package/dist/src/mcp/test-simple.mjs +271 -0
- package/dist/src/mcp/tools/command-run.mjs +101 -0
- package/dist/src/mcp/tools/discovery.mjs +291 -0
- package/dist/src/mcp/tools/interaction.mjs +87 -0
- package/dist/src/mcp/tools/orchestration.mjs +320 -0
- package/dist/src/mcp/tools/subsystems.mjs +640 -0
- package/dist/src/memory/cli.js +166 -0
- package/dist/src/memory/project-registry.js +282 -0
- package/dist/src/metrics/artifact-metrics.js +184 -0
- package/dist/src/metrics/context-budget.js +174 -0
- package/dist/src/metrics/token-counter.js +133 -0
- package/dist/src/models/config-loader.js +228 -0
- package/dist/src/models/index.js +22 -0
- package/dist/src/models/model-capabilities.v1.json +120 -0
- package/dist/src/models/model-catalog.v1.json +96 -0
- package/dist/src/models/model-discovery.js +521 -0
- package/dist/src/models/provider-models.js +73 -0
- package/dist/src/models/provider-policy.js +273 -0
- package/dist/src/models/resolver.js +245 -0
- package/dist/src/models/router.js +70 -0
- package/dist/src/models/types.js +8 -0
- package/dist/src/models/wrapper-deployment.js +178 -0
- package/dist/src/models/wrapper-route.js +54 -0
- package/dist/src/ops/cli.js +268 -0
- package/dist/src/ops/registry.js +623 -0
- package/dist/src/packages/adapters/clawhub.js +113 -0
- package/dist/src/packages/adapters/git.js +153 -0
- package/dist/src/packages/adapters/gitea.js +93 -0
- package/dist/src/packages/adapters/github.js +69 -0
- package/dist/src/packages/adapters/local-cache.js +89 -0
- package/dist/src/packages/package-registry.js +143 -0
- package/dist/src/packages/registry.js +238 -0
- package/dist/src/packages/types.js +11 -0
- package/dist/src/plugin/cross-framework-ops.js +475 -0
- package/dist/src/plugin/framework-config-loader.js +235 -0
- package/dist/src/plugin/framework-detector.js +186 -0
- package/dist/src/plugin/framework-isolator.js +341 -0
- package/dist/src/plugin/framework-migration.js +371 -0
- package/dist/src/plugin/metadata-validator.js +906 -0
- package/dist/src/plugin/plugin-installer.js +436 -0
- package/dist/src/plugin/plugin-status.js +369 -0
- package/dist/src/plugin/plugin-uninstaller.js +473 -0
- package/dist/src/plugin/registry-validator.js +344 -0
- package/dist/src/plugin/skill-command-translator.js +415 -0
- package/dist/src/plugin/workspace-creator.js +189 -0
- package/dist/src/plugin/workspace-migrator.js +821 -0
- package/dist/src/policy/authorization.js +390 -0
- package/dist/src/policy/repo-access.js +215 -0
- package/dist/src/provenance/cli.js +16 -0
- package/dist/src/providers/capability-matrix.js +300 -0
- package/dist/src/providers/capability-matrix.yaml +514 -0
- package/dist/src/providers/provider-definitions.js +977 -0
- package/dist/src/providers/provider-definitions.mjs +159 -0
- package/dist/src/providers/provider-inventory.js +169 -0
- package/dist/src/quality/feedback-ab.js +174 -0
- package/dist/src/quality/feedback-collector.js +176 -0
- package/dist/src/quality/feedback-tracker.js +123 -0
- package/dist/src/quality/patterns/adr.json +24 -0
- package/dist/src/quality/patterns/sad.json +24 -0
- package/dist/src/quality/patterns/test-plan.json +24 -0
- package/dist/src/quality/patterns/use-case.json +24 -0
- package/dist/src/quality/scoring.js +157 -0
- package/dist/src/reflections/cli.js +15 -0
- package/dist/src/release/plan.js +139 -0
- package/dist/src/research/cache/manager.js +227 -0
- package/dist/src/research/clients/arxiv.js +156 -0
- package/dist/src/research/clients/base.js +197 -0
- package/dist/src/research/clients/crossref.js +112 -0
- package/dist/src/research/clients/semantic-scholar.js +126 -0
- package/dist/src/research/clients/unpaywall.js +87 -0
- package/dist/src/research/index.js +14 -0
- package/dist/src/research/query-cli.js +336 -0
- package/dist/src/research/services/acquisition.js +313 -0
- package/dist/src/research/services/archival.js +225 -0
- package/dist/src/research/services/citation.js +279 -0
- package/dist/src/research/services/discovery.js +243 -0
- package/dist/src/research/services/documentation.js +269 -0
- package/dist/src/research/services/index.js +14 -0
- package/dist/src/research/services/provenance.js +279 -0
- package/dist/src/research/services/quality.js +370 -0
- package/dist/src/research/services/types.js +7 -0
- package/dist/src/research/storage-cli.js +20 -0
- package/dist/src/research/types.js +47 -0
- package/dist/src/resources/index.d.ts +2 -0
- package/dist/src/resources/index.js +2 -0
- package/dist/src/resources/web-release.d.ts +89 -0
- package/dist/src/resources/web-release.js +886 -0
- package/dist/src/rlm/cache/cli.js +162 -0
- package/dist/src/rlm/cache/hash.js +35 -0
- package/dist/src/rlm/cache/store.js +170 -0
- package/dist/src/rlm/cache/types.js +8 -0
- package/dist/src/rlm/cli.js +650 -0
- package/dist/src/serve/a2a-terminal-observer.js +120 -0
- package/dist/src/serve/agent-router.js +205 -0
- package/dist/src/serve/dispatch-router.js +171 -0
- package/dist/src/serve/executor-registry.js +715 -0
- package/dist/src/serve/mission-conductor.js +177 -0
- package/dist/src/serve/orchestrator-adapter.js +113 -0
- package/dist/src/serve/orchestrator-override.js +94 -0
- package/dist/src/serve/orchestrator-pty.js +133 -0
- package/dist/src/serve/pty-bridge.js +799 -0
- package/dist/src/serve/sandbox-registry.js +832 -0
- package/dist/src/serve/screen-reader.js +228 -0
- package/dist/src/serve/stack-adapters.js +68 -0
- package/dist/src/serve/telemetry.js +143 -0
- package/dist/src/skills/adapters/clawhub.js +250 -0
- package/dist/src/skills/adapters/local.js +335 -0
- package/dist/src/skills/adapters/openclaw.js +316 -0
- package/dist/src/skills/cli.js +334 -0
- package/dist/src/skills/registry.js +117 -0
- package/dist/src/skills/run.js +259 -0
- package/dist/src/skills/runtime.js +94 -0
- package/dist/src/skills/types.js +10 -0
- package/dist/src/smiths/agentsmith/demo.js +116 -0
- package/dist/src/smiths/agentsmith/examples.js +300 -0
- package/dist/src/smiths/agentsmith/generator.js +503 -0
- package/dist/src/smiths/agentsmith/index.js +11 -0
- package/dist/src/smiths/agentsmith/types.js +8 -0
- package/dist/src/smiths/commandsmith/example.js +220 -0
- package/dist/src/smiths/commandsmith/generator.js +288 -0
- package/dist/src/smiths/commandsmith/index.js +41 -0
- package/dist/src/smiths/commandsmith/templates.js +296 -0
- package/dist/src/smiths/commandsmith/types.js +7 -0
- package/dist/src/smiths/context-pipeline/aiwg-md.js +104 -0
- package/dist/src/smiths/context-pipeline/allowlist.js +195 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +127 -0
- package/dist/src/smiths/context-pipeline/discovery.js +187 -0
- package/dist/src/smiths/context-pipeline/external-links-section.js +49 -0
- package/dist/src/smiths/context-pipeline/finalization.js +147 -0
- package/dist/src/smiths/context-pipeline/generator.js +477 -0
- package/dist/src/smiths/context-pipeline/index.js +30 -0
- package/dist/src/smiths/context-pipeline/legacy-inject.js +103 -0
- package/dist/src/smiths/context-pipeline/managed-hook.js +95 -0
- package/dist/src/smiths/context-pipeline/overflow.js +212 -0
- package/dist/src/smiths/context-pipeline/parallelism-section.js +94 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +64 -0
- package/dist/src/smiths/context-pipeline/sanitizer.js +93 -0
- package/dist/src/smiths/context-pipeline/types.js +23 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +988 -0
- package/dist/src/smiths/hook-bridge/codex-translator.js +129 -0
- package/dist/src/smiths/hook-bridge/copilot-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/factory-translator.js +43 -0
- package/dist/src/smiths/hook-bridge/hermes-translator.js +88 -0
- package/dist/src/smiths/hook-bridge/index.js +79 -0
- package/dist/src/smiths/hook-bridge/loader.js +116 -0
- package/dist/src/smiths/hook-bridge/shim.js +89 -0
- package/dist/src/smiths/hook-bridge/types.js +31 -0
- package/dist/src/smiths/index.js +39 -0
- package/dist/src/smiths/mcpsmith/analyzers/cli-analyzer.js +390 -0
- package/dist/src/smiths/mcpsmith/example.js +119 -0
- package/dist/src/smiths/mcpsmith/generator.js +236 -0
- package/dist/src/smiths/mcpsmith/index.js +17 -0
- package/dist/src/smiths/mcpsmith/types.js +10 -0
- package/dist/src/smiths/platform-paths.js +101 -0
- package/dist/src/smiths/skillsmith/codex-sidecar.js +85 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +249 -0
- package/dist/src/smiths/skillsmith/demo.js +83 -0
- package/dist/src/smiths/skillsmith/examples.js +161 -0
- package/dist/src/smiths/skillsmith/generator.js +316 -0
- package/dist/src/smiths/skillsmith/index.js +12 -0
- package/dist/src/smiths/skillsmith/namespace-adapter.js +180 -0
- package/dist/src/smiths/skillsmith/platform-resolver.js +157 -0
- package/dist/src/smiths/skillsmith/types.js +9 -0
- package/dist/src/smiths/toolsmith/index.js +10 -0
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +710 -0
- package/dist/src/smiths/toolsmith/types.js +8 -0
- package/dist/src/storage/backends/fortemi.js +227 -0
- package/dist/src/storage/backends/fs.js +134 -0
- package/dist/src/storage/backends/logseq.js +309 -0
- package/dist/src/storage/backends/obsidian.js +233 -0
- package/dist/src/storage/cli.js +468 -0
- package/dist/src/storage/config.js +272 -0
- package/dist/src/storage/index.js +108 -0
- package/dist/src/storage/subsystem-cli.js +201 -0
- package/dist/src/storage/types.js +33 -0
- package/dist/src/testing/corpus/corpus-builder.js +383 -0
- package/dist/src/testing/corpus/ground-truth-manager.js +460 -0
- package/dist/src/testing/corpus/index.js +10 -0
- package/dist/src/testing/fixtures/index.js +9 -0
- package/dist/src/testing/fixtures/test-data-factory.js +472 -0
- package/dist/src/testing/generators/index.js +11 -0
- package/dist/src/testing/generators/test-case-generator.js +393 -0
- package/dist/src/testing/generators/test-code-generator.js +384 -0
- package/dist/src/testing/generators/use-case-parser.js +325 -0
- package/dist/src/testing/index.js +11 -0
- package/dist/src/tracker/capability-protocol.js +118 -0
- package/dist/src/update/checker.mjs +396 -0
- package/dist/src/update/notifier.mjs +235 -0
- package/dist/src/writing/content-diversifier.js +804 -0
- package/dist/src/writing/example-generator.js +959 -0
- package/dist/src/writing/example-templates.js +99 -0
- package/dist/src/writing/pattern-library.js +486 -0
- package/dist/src/writing/patterns/banned-phrases.json +4413 -0
- package/dist/src/writing/patterns/formulaic-structures.json +1300 -0
- package/dist/src/writing/patterns/generic-adjectives.json +1510 -0
- package/dist/src/writing/patterns/hedging-language.json +2096 -0
- package/dist/src/writing/patterns/transition-words.json +1285 -0
- package/dist/src/writing/patterns/weak-verbs.json +1112 -0
- package/dist/src/writing/prompt-optimizer.js +660 -0
- package/dist/src/writing/prompt-templates.js +583 -0
- package/dist/src/writing/scoring-config-loader.js +168 -0
- package/dist/src/writing/validation-engine.js +595 -0
- package/dist/src/writing/validation-rules.js +380 -0
- package/dist/src/writing/voice-analyzer.js +439 -0
- package/dist/src/writing/voice-calibration.js +661 -0
- package/dist/src/writing/voice-profiles.json +588 -0
- package/package.json +65 -9
|
@@ -0,0 +1,1835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve Command Handler
|
|
3
|
+
*
|
|
4
|
+
* Starts a local HTTP + WebSocket server and opens the browser dashboard.
|
|
5
|
+
* Server stack: Hono serving static files, WebSocket PTY bridge, REST API.
|
|
6
|
+
*
|
|
7
|
+
* @issue #711
|
|
8
|
+
* @see #712 — WebSocket PTY bridge
|
|
9
|
+
* @see #714 — React app scaffold
|
|
10
|
+
*/
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { existsSync, readFileSync } from 'fs';
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
14
|
+
import { createPtyWsHandler, registry as ptyRegistry } from '../../serve/pty-bridge.js';
|
|
15
|
+
import { telemetryStore, createEvent } from '../../serve/telemetry.js';
|
|
16
|
+
import { sandboxRegistry, normalizeSandboxEvent, } from '../../serve/sandbox-registry.js';
|
|
17
|
+
import { routeTask } from '../../serve/agent-router.js';
|
|
18
|
+
import { routeDispatch } from '../../serve/dispatch-router.js';
|
|
19
|
+
import { observeA2ATerminalState } from '../../serve/a2a-terminal-observer.js';
|
|
20
|
+
import { executorRegistry, validateRegisterPayload, validateDispatchPayload, validateEventEnvelope, } from '../../serve/executor-registry.js';
|
|
21
|
+
import { handleWebhook, IdempotencyCache, PushSecretRegistry, } from '../../a2a/webhook.js';
|
|
22
|
+
import { AiwgError, EXIT_CODES } from '../errors.js';
|
|
23
|
+
import { projectAiwgPath } from '../../config/project-artifacts.js';
|
|
24
|
+
// A2A push-notification state — module-scoped so the test harness can
|
|
25
|
+
// monkey-patch them in if needed. One process serves one set of secrets.
|
|
26
|
+
const pushSecretRegistry = new PushSecretRegistry();
|
|
27
|
+
const webhookIdempotency = new IdempotencyCache();
|
|
28
|
+
const DEFAULT_PORT = 7337;
|
|
29
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
30
|
+
/**
|
|
31
|
+
* Parse --port, --bind, --no-open, --read-only flags from args
|
|
32
|
+
*/
|
|
33
|
+
function parseServeArgs(args) {
|
|
34
|
+
let port = DEFAULT_PORT;
|
|
35
|
+
let host = DEFAULT_HOST;
|
|
36
|
+
let open = true;
|
|
37
|
+
let readOnly = false;
|
|
38
|
+
let sandbox = null;
|
|
39
|
+
for (let i = 0; i < args.length; i++) {
|
|
40
|
+
const arg = args[i];
|
|
41
|
+
if (arg === '--port' && args[i + 1]) {
|
|
42
|
+
const parsed = parseInt(args[i + 1], 10);
|
|
43
|
+
if (!isNaN(parsed))
|
|
44
|
+
port = parsed;
|
|
45
|
+
i++;
|
|
46
|
+
}
|
|
47
|
+
else if (arg === '--bind' && args[i + 1]) {
|
|
48
|
+
host = args[i + 1];
|
|
49
|
+
i++;
|
|
50
|
+
}
|
|
51
|
+
else if (arg === '--sandbox' && args[i + 1]) {
|
|
52
|
+
sandbox = args[i + 1];
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
else if (arg === '--no-open') {
|
|
56
|
+
open = false;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === '--read-only') {
|
|
59
|
+
readOnly = true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { port, host, open, readOnly, sandbox };
|
|
63
|
+
}
|
|
64
|
+
// ============================================================
|
|
65
|
+
// WebSocket routing (#851)
|
|
66
|
+
//
|
|
67
|
+
// @hono/node-server v1.x does not export createNodeWebSocket.
|
|
68
|
+
// We wire WebSocket routes directly via the Node.js HTTP server's
|
|
69
|
+
// 'upgrade' event and the `ws` npm package instead.
|
|
70
|
+
// ============================================================
|
|
71
|
+
/**
|
|
72
|
+
* Verbose logging gate. Set `AIWG_SERVE_DEBUG=1` to print per-event
|
|
73
|
+
* traces (sandbox WS message flow, management/orchestrate proxy open/close).
|
|
74
|
+
* Errors and warnings log unconditionally.
|
|
75
|
+
*/
|
|
76
|
+
const SERVE_DEBUG = process.env.AIWG_SERVE_DEBUG === '1';
|
|
77
|
+
function logServeWarn(tag, message, meta) {
|
|
78
|
+
if (meta !== undefined) {
|
|
79
|
+
console.warn(`[serve:${tag}] ${message}`, meta);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.warn(`[serve:${tag}] ${message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function logServeDebug(tag, message, meta) {
|
|
86
|
+
if (!SERVE_DEBUG)
|
|
87
|
+
return;
|
|
88
|
+
if (meta !== undefined) {
|
|
89
|
+
console.log(`[serve:${tag}] ${message}`, meta);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(`[serve:${tag}] ${message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
96
|
+
function handleSandboxWs(ws, sandboxId, token) {
|
|
97
|
+
if (!sandboxRegistry.authenticate(sandboxId, token)) {
|
|
98
|
+
ws.close(4001, 'Unauthorized');
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
sandboxRegistry.setConnected(sandboxId, true);
|
|
102
|
+
ws.on('message', (data) => {
|
|
103
|
+
if (!sandboxRegistry.authenticate(sandboxId, token))
|
|
104
|
+
return;
|
|
105
|
+
try {
|
|
106
|
+
// #933: agentic-sandbox serializes SandboxEvent with
|
|
107
|
+
// rename_all = "snake_case", so the wire tag and field names are
|
|
108
|
+
// snake_case. normalizeSandboxEvent translates to the dot-notation
|
|
109
|
+
// + camelCase shape handleEvent expects. Prior to this, every
|
|
110
|
+
// agent_*/hitl_* event was silently dropped and the dashboard
|
|
111
|
+
// reported "0 agents".
|
|
112
|
+
const raw = JSON.parse(data.toString());
|
|
113
|
+
const event = normalizeSandboxEvent(raw);
|
|
114
|
+
event.sandboxId = sandboxId;
|
|
115
|
+
sandboxRegistry.handleEvent(event);
|
|
116
|
+
}
|
|
117
|
+
catch { /* ignore malformed events */ }
|
|
118
|
+
});
|
|
119
|
+
ws.on('close', () => {
|
|
120
|
+
sandboxRegistry.setConnected(sandboxId, false);
|
|
121
|
+
});
|
|
122
|
+
ws.on('error', (err) => {
|
|
123
|
+
console.error(`[sandbox-registry] WebSocket error for ${sandboxId}:`, err);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
127
|
+
function handleExecutorWs(ws, executorId, token) {
|
|
128
|
+
if (!executorRegistry.authenticate(executorId, token)) {
|
|
129
|
+
ws.close(4001, 'Unauthorized');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Duck-typed WS connection handle for registry.pushToExecutor()
|
|
133
|
+
executorRegistry.setConnected(executorId, true, ws);
|
|
134
|
+
ws.on('message', (data) => {
|
|
135
|
+
if (!executorRegistry.authenticate(executorId, token))
|
|
136
|
+
return;
|
|
137
|
+
try {
|
|
138
|
+
const raw = JSON.parse(data.toString());
|
|
139
|
+
const { valid } = validateEventEnvelope(raw);
|
|
140
|
+
if (!valid) {
|
|
141
|
+
logServeWarn('executor-ws', `invalid event envelope from executor ${executorId} — ignored`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const envelope = raw;
|
|
145
|
+
// Ensure executor_id matches the authenticated connection
|
|
146
|
+
envelope.executor_id = executorId;
|
|
147
|
+
executorRegistry.handleEvent(envelope);
|
|
148
|
+
}
|
|
149
|
+
catch { /* ignore malformed events */ }
|
|
150
|
+
});
|
|
151
|
+
ws.on('close', () => {
|
|
152
|
+
executorRegistry.setConnected(executorId, false);
|
|
153
|
+
logServeDebug('executor-ws', `executor ${executorId} WS disconnected`);
|
|
154
|
+
});
|
|
155
|
+
ws.on('error', (err) => {
|
|
156
|
+
console.error(`[executor-registry] WebSocket error for ${executorId}:`, err);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
160
|
+
function handlePtyWs(ws, sessionId, command, cmdArgs, cwd, wsEndpoint, agentId) {
|
|
161
|
+
// createPtyWsHandler expects a Hono-context-like object for param/query extraction.
|
|
162
|
+
// We provide a minimal shim since we've already parsed the URL.
|
|
163
|
+
const mockContext = {
|
|
164
|
+
req: {
|
|
165
|
+
param: (key) => key === 'sessionId' ? sessionId : undefined,
|
|
166
|
+
query: () => ({
|
|
167
|
+
command,
|
|
168
|
+
args: cmdArgs.join(','),
|
|
169
|
+
...(cwd ? { cwd } : {}),
|
|
170
|
+
...(wsEndpoint ? { wsEndpoint } : {}),
|
|
171
|
+
...(agentId ? { agentId } : {}),
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const handler = createPtyWsHandler(mockContext);
|
|
177
|
+
handler.onOpen?.(null, ws);
|
|
178
|
+
ws.on('message', (data) => {
|
|
179
|
+
handler.onMessage?.({ data: data.toString() });
|
|
180
|
+
});
|
|
181
|
+
ws.on('close', () => {
|
|
182
|
+
handler.onClose?.();
|
|
183
|
+
});
|
|
184
|
+
ws.on('error', (err) => {
|
|
185
|
+
handler.onError?.(err);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
+
async function setupWebSockets(httpServer, readOnly) {
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
let wsMod;
|
|
192
|
+
try {
|
|
193
|
+
// @ts-expect-error — ws lacks bundled types; we use the runtime constructor only
|
|
194
|
+
wsMod = await import('ws');
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
console.warn('[serve] ws package not available — WebSocket routes disabled. Install with: npm install ws');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// ws ships as CJS; ESM import may wrap in .default
|
|
201
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
202
|
+
const WebSocketServer = wsMod.WebSocketServer ??
|
|
203
|
+
wsMod.default?.WebSocketServer ??
|
|
204
|
+
wsMod.Server ??
|
|
205
|
+
wsMod.default?.Server;
|
|
206
|
+
if (!WebSocketServer) {
|
|
207
|
+
console.warn('[serve] Could not resolve WebSocketServer from ws package — WebSocket routes disabled.');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
212
|
+
httpServer.on('upgrade', (req, socket, head) => {
|
|
213
|
+
const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
|
|
214
|
+
const pathname = url.pathname;
|
|
215
|
+
// /ws/sandbox/:sandboxId
|
|
216
|
+
const sandboxMatch = pathname.match(/^\/ws\/sandbox\/([^/]+)$/);
|
|
217
|
+
if (sandboxMatch) {
|
|
218
|
+
const sandboxId = sandboxMatch[1];
|
|
219
|
+
const token = url.searchParams.get('token') ?? '';
|
|
220
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
221
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
222
|
+
handleSandboxWs(ws, sandboxId, token);
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
// /ws/executors/:executorId — executor contract v1 bidirectional event stream (#1179)
|
|
227
|
+
const executorMatch = pathname.match(/^\/ws\/executors\/([^/]+)$/);
|
|
228
|
+
if (executorMatch) {
|
|
229
|
+
const executorId = executorMatch[1];
|
|
230
|
+
const token = url.searchParams.get('token') ?? '';
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
233
|
+
handleExecutorWs(ws, executorId, token);
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
// /ws/pty/:sessionId (disabled in read-only mode)
|
|
238
|
+
// Optional query params:
|
|
239
|
+
// ?sandbox=<sandboxId> — target a specific registered sandbox
|
|
240
|
+
// ?agent=<agentId> — target a specific agent within that sandbox
|
|
241
|
+
// ?wsEndpoint=<url> — explicit management WS URL (overrides sandbox lookup)
|
|
242
|
+
// Without these params the PTY bridge auto-detects the first connected sandbox.
|
|
243
|
+
if (!readOnly) {
|
|
244
|
+
const ptyMatch = pathname.match(/^\/ws\/pty\/([^/]+)$/);
|
|
245
|
+
if (ptyMatch) {
|
|
246
|
+
const sessionId = ptyMatch[1];
|
|
247
|
+
const command = url.searchParams.get('command') ?? 'aiwg';
|
|
248
|
+
const argsParam = url.searchParams.get('args');
|
|
249
|
+
const cmdArgs = argsParam ? argsParam.split(',') : ['mc', 'watch'];
|
|
250
|
+
const cwd = url.searchParams.get('cwd') ?? undefined;
|
|
251
|
+
const agentId = url.searchParams.get('agent') ?? undefined;
|
|
252
|
+
// Resolve wsEndpoint: explicit param takes precedence over sandbox registry lookup
|
|
253
|
+
let wsEndpoint = url.searchParams.get('wsEndpoint') ?? undefined;
|
|
254
|
+
if (!wsEndpoint) {
|
|
255
|
+
const sandboxId = url.searchParams.get('sandbox') ?? undefined;
|
|
256
|
+
if (sandboxId) {
|
|
257
|
+
const sb = sandboxRegistry.get(sandboxId);
|
|
258
|
+
if (sb?.wsEndpoint)
|
|
259
|
+
wsEndpoint = sb.wsEndpoint;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
263
|
+
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
264
|
+
handlePtyWs(ws, sessionId, command, cmdArgs, cwd, wsEndpoint, agentId);
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// /ws/sandbox/:sandboxId/sessions/:sessionId/orchestrate
|
|
270
|
+
// Proxies to the sandbox management server's orchestrate WS endpoint.
|
|
271
|
+
// Browser speaks the orchestrate protocol (screen_update frames) directly;
|
|
272
|
+
// this bridge is purely a cross-origin WS relay.
|
|
273
|
+
const orchMatch = pathname.match(/^\/ws\/sandbox\/([^/]+)\/sessions\/([^/]+)\/orchestrate$/);
|
|
274
|
+
if (orchMatch) {
|
|
275
|
+
const sandboxId = orchMatch[1];
|
|
276
|
+
const sessionId = orchMatch[2];
|
|
277
|
+
const sandbox = sandboxRegistry.get(sandboxId);
|
|
278
|
+
if (!sandbox) {
|
|
279
|
+
logServeWarn('orch-proxy', `sandbox ${sandboxId} not found — rejecting orchestrate WS upgrade for session ${sessionId}`);
|
|
280
|
+
socket.destroy();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!sandbox.connected) {
|
|
284
|
+
logServeWarn('orch-proxy', `sandbox ${sandboxId} not connected — rejecting orchestrate WS upgrade for session ${sessionId}`);
|
|
285
|
+
socket.destroy();
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Convert httpEndpoint to ws:// URL for the sandbox orchestrate path
|
|
289
|
+
const orchWsUrl = sandbox.httpEndpoint.replace(/^http/, 'ws') + `/ws/sessions/${sessionId}/orchestrate`;
|
|
290
|
+
logServeDebug('orch-proxy', `upgrading browser → ${orchWsUrl} for session ${sessionId}`);
|
|
291
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
292
|
+
wss.handleUpgrade(req, socket, head, async (browserWs) => {
|
|
293
|
+
try {
|
|
294
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
295
|
+
const WS = wsMod.WebSocket ?? wsMod.default?.WebSocket ?? wsMod.default;
|
|
296
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
297
|
+
const sandboxWs = new WS(orchWsUrl);
|
|
298
|
+
sandboxWs.on('open', () => {
|
|
299
|
+
logServeDebug('orch-proxy', `upstream orchestrate WS open for session ${sessionId}`);
|
|
300
|
+
// Relay sandbox → browser
|
|
301
|
+
sandboxWs.on('message', (data) => {
|
|
302
|
+
if (browserWs.readyState === 1) {
|
|
303
|
+
try {
|
|
304
|
+
browserWs.send(typeof data === 'string' ? data : data.toString());
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
logServeWarn('orch-proxy', `relay sandbox→browser failed for ${sessionId}`, err);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
sandboxWs.on('close', (code, reason) => {
|
|
312
|
+
logServeDebug('orch-proxy', `upstream orchestrate WS closed for ${sessionId}: code=${code} reason=${reason?.toString?.() ?? ''}`);
|
|
313
|
+
try {
|
|
314
|
+
browserWs.close(1001, 'Sandbox closed');
|
|
315
|
+
}
|
|
316
|
+
catch { /* already closed */ }
|
|
317
|
+
});
|
|
318
|
+
sandboxWs.on('error', (err) => {
|
|
319
|
+
logServeWarn('orch-proxy', `upstream orchestrate WS error for ${sessionId}`, err);
|
|
320
|
+
try {
|
|
321
|
+
browserWs.close(1011, 'Sandbox WS error');
|
|
322
|
+
}
|
|
323
|
+
catch { /* already closed */ }
|
|
324
|
+
});
|
|
325
|
+
// Relay browser → sandbox
|
|
326
|
+
browserWs.on('message', (data) => {
|
|
327
|
+
if (sandboxWs.readyState === 1) {
|
|
328
|
+
try {
|
|
329
|
+
sandboxWs.send(typeof data === 'string' ? data : data.toString());
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
logServeWarn('orch-proxy', `relay browser→sandbox failed for ${sessionId}`, err);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
browserWs.on('close', () => { try {
|
|
337
|
+
sandboxWs.close();
|
|
338
|
+
}
|
|
339
|
+
catch { /* ignore */ } });
|
|
340
|
+
});
|
|
341
|
+
sandboxWs.on('error', (err) => {
|
|
342
|
+
logServeWarn('orch-proxy', `failed to connect to sandbox orchestrate WS at ${orchWsUrl}`, err);
|
|
343
|
+
try {
|
|
344
|
+
browserWs.close(1011, 'Could not connect to sandbox orchestrate WS');
|
|
345
|
+
}
|
|
346
|
+
catch { /* already closed */ }
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
logServeWarn('orch-proxy', `orchestrate proxy threw for session ${sessionId}`, err);
|
|
351
|
+
try {
|
|
352
|
+
browserWs.close(1011, 'Orchestrate proxy error');
|
|
353
|
+
}
|
|
354
|
+
catch { /* already closed */ }
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
// /ws/sandbox/:sandboxId/management
|
|
360
|
+
// Proxies to the sandbox management WS bus (wsEndpoint).
|
|
361
|
+
// The management WS is a multicast bus — all agents and sessions share one connection.
|
|
362
|
+
// The browser sends attach_session / send_input / pty_resize frames and receives
|
|
363
|
+
// output / session_attached / session_detached frames for all agents.
|
|
364
|
+
const mgmtMatch = pathname.match(/^\/ws\/sandbox\/([^/]+)\/management$/);
|
|
365
|
+
if (mgmtMatch) {
|
|
366
|
+
const sandboxId = mgmtMatch[1];
|
|
367
|
+
const sandbox = sandboxRegistry.get(sandboxId);
|
|
368
|
+
if (!sandbox) {
|
|
369
|
+
logServeWarn('mgmt-proxy', `sandbox ${sandboxId} not found in registry — rejecting WS upgrade`);
|
|
370
|
+
socket.destroy();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!sandbox.connected) {
|
|
374
|
+
logServeWarn('mgmt-proxy', `sandbox ${sandboxId} (${sandbox.name}) is not connected — rejecting WS upgrade. Last event: ${sandbox.lastEventAt}`);
|
|
375
|
+
socket.destroy();
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// wsEndpoint is already a full ws:// URL (e.g. ws://localhost:8121)
|
|
379
|
+
const mgmtWsUrl = sandbox.wsEndpoint;
|
|
380
|
+
logServeDebug('mgmt-proxy', `upgrading browser → ${mgmtWsUrl} for sandbox ${sandboxId} (${sandbox.name})`);
|
|
381
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
382
|
+
wss.handleUpgrade(req, socket, head, async (browserWs) => {
|
|
383
|
+
try {
|
|
384
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
385
|
+
const WS = wsMod.WebSocket ?? wsMod.default?.WebSocket ?? wsMod.default;
|
|
386
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
387
|
+
const sandboxWs = new WS(mgmtWsUrl);
|
|
388
|
+
// Open-race fix (#1151 follow-up): the browser fires its first
|
|
389
|
+
// messages (subscribe + list_sessions) inside its own ws.onopen
|
|
390
|
+
// handler, which fires the moment the upgrade completes —
|
|
391
|
+
// BEFORE the upstream sandbox WS finishes opening. If we register
|
|
392
|
+
// the browser→sandbox listener inside sandboxWs.on('open', …) the
|
|
393
|
+
// browser's first messages have no listener yet and get silently
|
|
394
|
+
// dropped, leaving the pane forever stuck on "Listing sessions…".
|
|
395
|
+
// Two-pane setups hit this asymmetrically — pure timing decides
|
|
396
|
+
// which pane "wins" the race. Workaround: register the browser
|
|
397
|
+
// listener immediately, queue messages while upstream is still
|
|
398
|
+
// connecting, flush on upstream open.
|
|
399
|
+
const pendingFromBrowser = [];
|
|
400
|
+
let upstreamOpen = false;
|
|
401
|
+
browserWs.on('message', (data) => {
|
|
402
|
+
if (upstreamOpen && sandboxWs.readyState === 1) {
|
|
403
|
+
try {
|
|
404
|
+
sandboxWs.send(typeof data === 'string' ? data : data.toString());
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
logServeWarn('mgmt-proxy', `relay browser→sandbox failed for ${sandboxId}`, err);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
// Upstream not ready yet — queue and flush on open. Cap the
|
|
412
|
+
// queue at a reasonable size so a hung upstream doesn't grow
|
|
413
|
+
// memory unbounded.
|
|
414
|
+
if (pendingFromBrowser.length < 64) {
|
|
415
|
+
pendingFromBrowser.push(data);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
logServeWarn('mgmt-proxy', `dropped browser message for ${sandboxId} — upstream still connecting and queue full`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
browserWs.on('close', () => { try {
|
|
423
|
+
sandboxWs.close();
|
|
424
|
+
}
|
|
425
|
+
catch { /* ignore */ } });
|
|
426
|
+
sandboxWs.on('open', () => {
|
|
427
|
+
logServeDebug('mgmt-proxy', `upstream WS open for sandbox ${sandboxId} (flushing ${pendingFromBrowser.length} queued msg)`);
|
|
428
|
+
upstreamOpen = true;
|
|
429
|
+
// Flush queued browser→sandbox messages in order.
|
|
430
|
+
while (pendingFromBrowser.length) {
|
|
431
|
+
const data = pendingFromBrowser.shift();
|
|
432
|
+
try {
|
|
433
|
+
sandboxWs.send(typeof data === 'string' ? data : data.toString());
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
logServeWarn('mgmt-proxy', `flush browser→sandbox failed for ${sandboxId}`, err);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Relay sandbox → browser
|
|
440
|
+
sandboxWs.on('message', (data) => {
|
|
441
|
+
if (browserWs.readyState === 1) {
|
|
442
|
+
try {
|
|
443
|
+
browserWs.send(typeof data === 'string' ? data : data.toString());
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
logServeWarn('mgmt-proxy', `relay sandbox→browser failed for ${sandboxId}`, err);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
sandboxWs.on('close', (code, reason) => {
|
|
451
|
+
logServeDebug('mgmt-proxy', `upstream WS closed for sandbox ${sandboxId}: code=${code} reason=${reason?.toString?.() ?? ''}`);
|
|
452
|
+
try {
|
|
453
|
+
browserWs.close(1001, 'Sandbox closed');
|
|
454
|
+
}
|
|
455
|
+
catch { /* already closed */ }
|
|
456
|
+
});
|
|
457
|
+
sandboxWs.on('error', (err) => {
|
|
458
|
+
logServeWarn('mgmt-proxy', `upstream WS error for sandbox ${sandboxId} (${mgmtWsUrl})`, err);
|
|
459
|
+
try {
|
|
460
|
+
browserWs.close(1011, 'Sandbox WS error');
|
|
461
|
+
}
|
|
462
|
+
catch { /* already closed */ }
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
sandboxWs.on('error', (err) => {
|
|
466
|
+
logServeWarn('mgmt-proxy', `failed to connect to sandbox management WS at ${mgmtWsUrl} for ${sandboxId}`, err);
|
|
467
|
+
try {
|
|
468
|
+
browserWs.close(1011, 'Could not connect to sandbox management WS');
|
|
469
|
+
}
|
|
470
|
+
catch { /* already closed */ }
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
logServeWarn('mgmt-proxy', `management proxy threw for sandbox ${sandboxId}`, err);
|
|
475
|
+
try {
|
|
476
|
+
browserWs.close(1011, 'Management proxy error');
|
|
477
|
+
}
|
|
478
|
+
catch { /* already closed */ }
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// Unknown WS path — reject cleanly
|
|
484
|
+
socket.destroy();
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Start the Hono HTTP server
|
|
489
|
+
*
|
|
490
|
+
* Uses dynamic require-style imports to avoid compile-time resolution of
|
|
491
|
+
* optional deps (hono, @hono/node-server) that are not yet in package.json.
|
|
492
|
+
* TypeScript sees only `unknown`-typed module shapes here.
|
|
493
|
+
*/
|
|
494
|
+
/**
|
|
495
|
+
* In-process server bootstrap. Exported for tier-3 integration tests
|
|
496
|
+
* (test/integration/serve-*.test.ts) so they can drive serve as a black-box
|
|
497
|
+
* client without spawning a child node process. The CLI handler also calls
|
|
498
|
+
* this internally — same code path, exactly one entry point.
|
|
499
|
+
*
|
|
500
|
+
* @see #1277 — moves the integration suite off spawn-based testing
|
|
501
|
+
*/
|
|
502
|
+
export async function startServer(opts) {
|
|
503
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
504
|
+
let honoMod;
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
+
let nodeMod;
|
|
507
|
+
try {
|
|
508
|
+
// Plain dynamic import — runtime-resolved, no static analysis issue, and
|
|
509
|
+
// works in sandboxed VM contexts like vitest where the Function-constructor
|
|
510
|
+
// import path raises ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING (#1277).
|
|
511
|
+
// hono is an optionalDependency; tsc may not find its types under
|
|
512
|
+
// `npm ci --omit=optional` (e.g. metadata-validation workflow). The
|
|
513
|
+
// try/catch + auto-install fallback below handles that at runtime.
|
|
514
|
+
// @ts-ignore — optional dep; may not be installed at typecheck time
|
|
515
|
+
honoMod = await import('hono');
|
|
516
|
+
// @ts-ignore — optional dep; may not be installed at typecheck time
|
|
517
|
+
nodeMod = await import('@hono/node-server');
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// Auto-install optional serve dependencies on first use
|
|
521
|
+
console.log('Installing serve dependencies (hono, @hono/node-server, ws)...');
|
|
522
|
+
const result = spawnSync('npm', ['install', '--save-optional', 'hono', '@hono/node-server', 'ws'], { stdio: 'inherit' });
|
|
523
|
+
if (result.status !== 0) {
|
|
524
|
+
throw new AiwgError({
|
|
525
|
+
code: 'ERR_SERVE_DEPS_INSTALL_FAILED',
|
|
526
|
+
message: 'Failed to install serve dependencies (hono, @hono/node-server, ws)',
|
|
527
|
+
hint: 'Install manually: npm install hono @hono/node-server ws',
|
|
528
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
// Retry imports after install
|
|
532
|
+
try {
|
|
533
|
+
// @ts-ignore — optional dep; may not be installed at typecheck time
|
|
534
|
+
honoMod = await import('hono');
|
|
535
|
+
// @ts-ignore — optional dep; may not be installed at typecheck time
|
|
536
|
+
nodeMod = await import('@hono/node-server');
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
throw new AiwgError({
|
|
540
|
+
code: 'ERR_SERVE_DEPS_LOAD_FAILED',
|
|
541
|
+
message: 'Serve dependencies installed but could not be loaded',
|
|
542
|
+
hint: 'Try: npm install hono @hono/node-server ws',
|
|
543
|
+
exitCode: EXIT_CODES.GENERAL,
|
|
544
|
+
cause: err,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const { Hono } = honoMod;
|
|
549
|
+
const { serve } = nodeMod;
|
|
550
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
551
|
+
const app = new Hono();
|
|
552
|
+
// WebSocket routes are handled via Node.js upgrade event below (see setupWebSockets).
|
|
553
|
+
// @hono/node-server v1.x does not export createNodeWebSocket.
|
|
554
|
+
// Health check
|
|
555
|
+
app.get('/api/health', (c) => c.json({ status: 'ok', readOnly: opts.readOnly }));
|
|
556
|
+
// Connection status — server health, PTY sessions, sandboxes, subsystem status (#887)
|
|
557
|
+
const serverStartTime = Date.now();
|
|
558
|
+
const COUNTS_TTL_MS = 5000;
|
|
559
|
+
const countsCache = new Map();
|
|
560
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
561
|
+
async function fetchWithTimeout(url) {
|
|
562
|
+
const ctrl = new AbortController();
|
|
563
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
564
|
+
try {
|
|
565
|
+
const resp = await fetch(url, { signal: ctrl.signal });
|
|
566
|
+
if (!resp.ok)
|
|
567
|
+
return null;
|
|
568
|
+
return await resp.json();
|
|
569
|
+
}
|
|
570
|
+
finally {
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function getInventoryCounts(s) {
|
|
575
|
+
if (!s.connected)
|
|
576
|
+
return { vmCount: null, containerCount: null };
|
|
577
|
+
const cached = countsCache.get(s.id);
|
|
578
|
+
if (cached && Date.now() - cached.at < COUNTS_TTL_MS)
|
|
579
|
+
return cached.counts;
|
|
580
|
+
let vmCount = null;
|
|
581
|
+
let containerCount = null;
|
|
582
|
+
try {
|
|
583
|
+
const [vms, containers] = await Promise.all([
|
|
584
|
+
fetchWithTimeout(`${s.httpEndpoint}/api/v1/vms`).catch(() => null),
|
|
585
|
+
fetchWithTimeout(`${s.httpEndpoint}/api/v1/containers`).catch(() => null),
|
|
586
|
+
]);
|
|
587
|
+
if (vms && typeof vms.total === 'number') {
|
|
588
|
+
vmCount = vms.total;
|
|
589
|
+
}
|
|
590
|
+
if (containers && typeof containers.total === 'number') {
|
|
591
|
+
containerCount = containers.total;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
catch { /* ignore */ }
|
|
595
|
+
const counts = { vmCount, containerCount };
|
|
596
|
+
countsCache.set(s.id, { at: Date.now(), counts });
|
|
597
|
+
return counts;
|
|
598
|
+
}
|
|
599
|
+
app.get('/api/connections', async (c) => {
|
|
600
|
+
const uptime = Date.now() - serverStartTime;
|
|
601
|
+
// PTY sessions
|
|
602
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
603
|
+
const sessions = [...ptyRegistry['sessions'].keys()];
|
|
604
|
+
// Sandboxes — augmented with VM/container counts (#1157).
|
|
605
|
+
// null counts indicate the sandbox is offline or didn't respond; the UI
|
|
606
|
+
// renders these as `?` rather than `0` so missing data is distinguishable
|
|
607
|
+
// from a confirmed empty inventory.
|
|
608
|
+
const sandboxList = sandboxRegistry.list();
|
|
609
|
+
const allSandboxes = await Promise.all(sandboxList.map(async (s) => {
|
|
610
|
+
const counts = await getInventoryCounts({ id: s.id, httpEndpoint: s.httpEndpoint, connected: s.connected });
|
|
611
|
+
return {
|
|
612
|
+
id: s.id,
|
|
613
|
+
name: s.name,
|
|
614
|
+
connected: s.connected,
|
|
615
|
+
agentCount: s.agentCount,
|
|
616
|
+
vmCount: counts.vmCount,
|
|
617
|
+
containerCount: counts.containerCount,
|
|
618
|
+
};
|
|
619
|
+
}));
|
|
620
|
+
// Ralph subsystem — read .aiwg/ralph/registry.json if present
|
|
621
|
+
let ralphStatus = 'unknown';
|
|
622
|
+
let activeLoops = 0;
|
|
623
|
+
try {
|
|
624
|
+
const ralphPath = projectAiwgPath(process.cwd(), 'ralph', 'registry.json');
|
|
625
|
+
if (existsSync(ralphPath)) {
|
|
626
|
+
const data = JSON.parse(readFileSync(ralphPath, 'utf-8'));
|
|
627
|
+
activeLoops = (data.active_loops ?? []).filter((l) => l.status === 'running').length;
|
|
628
|
+
ralphStatus = activeLoops > 0 ? 'active' : 'idle';
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch { /* ignore */ }
|
|
632
|
+
// Missions subsystem — check for mc session directory
|
|
633
|
+
let missionsStatus = 'unknown';
|
|
634
|
+
let missionsCount = 0;
|
|
635
|
+
try {
|
|
636
|
+
const mcPath = projectAiwgPath(process.cwd(), 'mc');
|
|
637
|
+
if (existsSync(mcPath)) {
|
|
638
|
+
missionsStatus = 'idle';
|
|
639
|
+
const registryPath = path.join(mcPath, 'registry.json');
|
|
640
|
+
if (existsSync(registryPath)) {
|
|
641
|
+
const data = JSON.parse(readFileSync(registryPath, 'utf-8'));
|
|
642
|
+
const activeSessions = (data.sessions ?? []).filter((s) => s.status === 'running');
|
|
643
|
+
missionsCount = activeSessions.length;
|
|
644
|
+
if (missionsCount > 0)
|
|
645
|
+
missionsStatus = 'active';
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch { /* ignore */ }
|
|
650
|
+
// Daemon subsystem — check for daemon PID file
|
|
651
|
+
let daemonStatus = 'unknown';
|
|
652
|
+
try {
|
|
653
|
+
const daemonPid = projectAiwgPath(process.cwd(), 'daemon', 'daemon.pid');
|
|
654
|
+
daemonStatus = existsSync(daemonPid) ? 'running' : 'stopped';
|
|
655
|
+
}
|
|
656
|
+
catch { /* ignore */ }
|
|
657
|
+
// RLM subsystem — check for rlm state
|
|
658
|
+
let rlmStatus = 'unknown';
|
|
659
|
+
try {
|
|
660
|
+
const rlmPath = projectAiwgPath(process.cwd(), 'rlm');
|
|
661
|
+
rlmStatus = existsSync(rlmPath) ? 'idle' : 'stopped';
|
|
662
|
+
}
|
|
663
|
+
catch { /* ignore */ }
|
|
664
|
+
// Semantic memory — check for memory index
|
|
665
|
+
let memoryStatus = 'unknown';
|
|
666
|
+
try {
|
|
667
|
+
const memPath = projectAiwgPath(process.cwd(), 'memory');
|
|
668
|
+
memoryStatus = existsSync(memPath) ? 'active' : 'stopped';
|
|
669
|
+
}
|
|
670
|
+
catch { /* ignore */ }
|
|
671
|
+
return c.json({
|
|
672
|
+
server: { status: 'ok', readOnly: opts.readOnly, uptime },
|
|
673
|
+
ptySessions: sessions,
|
|
674
|
+
sandboxes: allSandboxes,
|
|
675
|
+
mcpServers: [],
|
|
676
|
+
subsystems: {
|
|
677
|
+
ralph: { status: ralphStatus, activeLoops },
|
|
678
|
+
missions: { status: missionsStatus, count: missionsCount },
|
|
679
|
+
daemon: { status: daemonStatus },
|
|
680
|
+
rlm: { status: rlmStatus },
|
|
681
|
+
memory: { status: memoryStatus },
|
|
682
|
+
},
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
// REST stubs — filled in by #715 / #716
|
|
686
|
+
app.get('/api/sessions', (c) => {
|
|
687
|
+
const sessions = [...ptyRegistry['sessions'].keys()];
|
|
688
|
+
return c.json({ sessions });
|
|
689
|
+
});
|
|
690
|
+
// Mission Control API stubs (#715)
|
|
691
|
+
app.get('/api/missions', (c) => c.json({ missions: [], sessions: [] }));
|
|
692
|
+
app.get('/api/sessions/:id/missions', (c) => c.json({ missions: [] }));
|
|
693
|
+
// Legacy dispatch route — stub preserved for backward compat
|
|
694
|
+
// TODO: remove after v1.x once all callers migrate to /api/v1/sessions/:id/dispatch
|
|
695
|
+
app.post('/api/sessions/:id/dispatch', async (c) => {
|
|
696
|
+
// 301 → /api/v1/sessions/:id/dispatch so existing callers get a clear redirect signal
|
|
697
|
+
const sessionId = c.req.param('id');
|
|
698
|
+
return c.redirect(`/api/v1/sessions/${sessionId}/dispatch`, 301);
|
|
699
|
+
});
|
|
700
|
+
app.put('/api/missions/:id/pause', (c) => c.json({ ok: true }));
|
|
701
|
+
app.put('/api/missions/:id/resume', (c) => c.json({ ok: true }));
|
|
702
|
+
app.delete('/api/missions/:id', (c) => c.json({ ok: true }));
|
|
703
|
+
// ── Executor Registry API (#1179) ──────────────────────────────────────────
|
|
704
|
+
// POST /api/v1/executors/register → 201 with executor_id + token
|
|
705
|
+
app.post('/api/v1/executors/register', async (c) => {
|
|
706
|
+
let body;
|
|
707
|
+
try {
|
|
708
|
+
body = await c.req.json();
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
712
|
+
}
|
|
713
|
+
const { valid, errors } = validateRegisterPayload(body);
|
|
714
|
+
if (!valid) {
|
|
715
|
+
return c.json({ error: `Invalid register payload: ${errors}` }, 400);
|
|
716
|
+
}
|
|
717
|
+
const result = executorRegistry.register(body);
|
|
718
|
+
if ('status' in result && result.status === 400) {
|
|
719
|
+
return c.json({ error: result.error }, 400);
|
|
720
|
+
}
|
|
721
|
+
// Emit telemetry (reuse agent.spawn as closest match for executor registration)
|
|
722
|
+
const resp = result;
|
|
723
|
+
telemetryStore.ingest(createEvent('agent.spawn', resp.executor_id, { name: body.name }));
|
|
724
|
+
return c.json(resp, 201);
|
|
725
|
+
});
|
|
726
|
+
// DELETE /api/v1/executors/:id → 204; auth required
|
|
727
|
+
app.delete('/api/v1/executors/:id', (c) => {
|
|
728
|
+
const executorId = c.req.param('id');
|
|
729
|
+
const authHeader = c.req.header('authorization') ?? '';
|
|
730
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
|
|
731
|
+
if (!executorRegistry.authenticate(executorId, token)) {
|
|
732
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
733
|
+
}
|
|
734
|
+
const ok = executorRegistry.deregister(executorId, 'operator_deleted');
|
|
735
|
+
if (!ok)
|
|
736
|
+
return c.json({ error: 'Executor not found' }, 404);
|
|
737
|
+
return new Response(null, { status: 204 });
|
|
738
|
+
});
|
|
739
|
+
// GET /api/v1/executors → list
|
|
740
|
+
app.get('/api/v1/executors', (c) => {
|
|
741
|
+
return c.json({ executors: executorRegistry.list() });
|
|
742
|
+
});
|
|
743
|
+
// GET /api/v1/executors/:id → single executor status
|
|
744
|
+
app.get('/api/v1/executors/:id', (c) => {
|
|
745
|
+
const summary = executorRegistry.get(c.req.param('id'));
|
|
746
|
+
if (!summary)
|
|
747
|
+
return c.json({ error: 'Executor not found' }, 404);
|
|
748
|
+
return c.json(summary);
|
|
749
|
+
});
|
|
750
|
+
// POST /api/v1/sessions/:id/dispatch → 202; replaces the #715 stub
|
|
751
|
+
app.post('/api/v1/sessions/:id/dispatch', async (c) => {
|
|
752
|
+
const sessionId = c.req.param('id');
|
|
753
|
+
let body;
|
|
754
|
+
try {
|
|
755
|
+
body = await c.req.json();
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
759
|
+
}
|
|
760
|
+
// 1. Validate payload against schema
|
|
761
|
+
const { valid, errors } = validateDispatchPayload(body);
|
|
762
|
+
if (!valid) {
|
|
763
|
+
return c.json({ error: `Invalid dispatch payload: ${errors}` }, 400);
|
|
764
|
+
}
|
|
765
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
766
|
+
const payload = body;
|
|
767
|
+
const missionId = payload.mission_id;
|
|
768
|
+
const longRunning = payload.long_running === true;
|
|
769
|
+
const executorFilter = payload.executor_filter ?? {};
|
|
770
|
+
// 2. Resolve executor via registry's pickByFilter
|
|
771
|
+
const pickResult = executorRegistry.pickByFilter(executorFilter, longRunning);
|
|
772
|
+
if (!pickResult) {
|
|
773
|
+
if (longRunning) {
|
|
774
|
+
return c.json({ error: 'no_resumable_executor_available' }, 503);
|
|
775
|
+
}
|
|
776
|
+
return c.json({ error: 'no_executor_available' }, 503);
|
|
777
|
+
}
|
|
778
|
+
const { executor } = pickResult;
|
|
779
|
+
// 3. Forward dispatch via the dispatch router (#1252): try A2A v2 first,
|
|
780
|
+
// fall back to v1 /dispatch on 404 with a structured warning.
|
|
781
|
+
let estimatedStart;
|
|
782
|
+
let a2aInstanceId;
|
|
783
|
+
let dispatchPath = 'v2';
|
|
784
|
+
let a2aTask = undefined;
|
|
785
|
+
try {
|
|
786
|
+
const result = await routeDispatch(executor, payload, {
|
|
787
|
+
onV1Fallback: (info) => {
|
|
788
|
+
logServeWarn('dispatch', `v1 fallback for executor ${info.executorId}: ${info.reason}`);
|
|
789
|
+
telemetryStore.ingest(createEvent('v1.dispatch.fallback', sessionId, info, missionId));
|
|
790
|
+
},
|
|
791
|
+
onDeprecation: (info) => {
|
|
792
|
+
logServeWarn('dispatch', `v1 deprecation observed at ${info.path}` +
|
|
793
|
+
(info.sunset ? ` (sunset: ${info.sunset})` : ''));
|
|
794
|
+
telemetryStore.ingest(createEvent('v1.deprecation.observed', sessionId, { ...info }, missionId));
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
dispatchPath = result.dispatchPath;
|
|
798
|
+
a2aInstanceId = result.a2aInstanceId;
|
|
799
|
+
a2aTask = result.task;
|
|
800
|
+
if (result.estimatedStart)
|
|
801
|
+
estimatedStart = result.estimatedStart;
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
const msg = err.message ?? String(err);
|
|
805
|
+
logServeWarn('dispatch', `executor ${executor.executorId} dispatch failed: ${msg}`);
|
|
806
|
+
executorRegistry.assignMission(missionId, executor.executorId);
|
|
807
|
+
executorRegistry.failMission(missionId, msg);
|
|
808
|
+
telemetryStore.ingest(createEvent('mission.abort', sessionId, {
|
|
809
|
+
missionId,
|
|
810
|
+
executorId: executor.executorId,
|
|
811
|
+
error: msg,
|
|
812
|
+
}, missionId));
|
|
813
|
+
// Distinguish unreachable (network/timeout) from forward errors.
|
|
814
|
+
const status = /v1 dispatch failed: \d/.test(msg) ? 502 : 502;
|
|
815
|
+
const errorTag = /unreachable|ECONN|fetch failed/i.test(msg)
|
|
816
|
+
? 'executor_unreachable'
|
|
817
|
+
: 'executor_forward_failed';
|
|
818
|
+
return c.json({ error: errorTag, detail: msg }, status);
|
|
819
|
+
}
|
|
820
|
+
// 4. Record the mission and emit telemetry
|
|
821
|
+
executorRegistry.assignMission(missionId, executor.executorId);
|
|
822
|
+
if (dispatchPath === 'v2' && a2aTask && a2aInstanceId) {
|
|
823
|
+
void observeA2ATerminalState(executorRegistry, executor, missionId, a2aInstanceId, a2aTask, {
|
|
824
|
+
onError: (err) => {
|
|
825
|
+
logServeWarn('dispatch', `A2A terminal observer failed for mission ${missionId}: ${err.message ?? String(err)}`);
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
telemetryStore.ingest(createEvent('mission.dispatch', sessionId, {
|
|
830
|
+
missionId,
|
|
831
|
+
executorId: executor.executorId,
|
|
832
|
+
objective: payload.objective,
|
|
833
|
+
completion: payload.completion,
|
|
834
|
+
}, missionId));
|
|
835
|
+
// 5. Return 202 Accepted
|
|
836
|
+
const dispatchResp = {
|
|
837
|
+
mission_id: missionId,
|
|
838
|
+
executor_id: executor.executorId,
|
|
839
|
+
status: 'assigned',
|
|
840
|
+
dispatch_path: dispatchPath,
|
|
841
|
+
};
|
|
842
|
+
if (a2aInstanceId)
|
|
843
|
+
dispatchResp.a2a_instance_id = a2aInstanceId;
|
|
844
|
+
if (estimatedStart)
|
|
845
|
+
dispatchResp.estimated_start = estimatedStart;
|
|
846
|
+
return c.json(dispatchResp, 202);
|
|
847
|
+
});
|
|
848
|
+
// GET /api/v1/missions/:id → mission status snapshot
|
|
849
|
+
app.get('/api/v1/missions/:id', (c) => {
|
|
850
|
+
const mission = executorRegistry.getMission(c.req.param('id'));
|
|
851
|
+
if (!mission)
|
|
852
|
+
return c.json({ error: 'Mission not found' }, 404);
|
|
853
|
+
return c.json({
|
|
854
|
+
mission_id: mission.missionId,
|
|
855
|
+
executor_id: mission.executorId,
|
|
856
|
+
state: mission.state,
|
|
857
|
+
created_at: mission.createdAt,
|
|
858
|
+
updated_at: mission.updatedAt,
|
|
859
|
+
completed_at: mission.completedAt,
|
|
860
|
+
recent_events: mission.recentEvents,
|
|
861
|
+
pty_session_ref: mission.ptySessionRef,
|
|
862
|
+
exit_code: mission.exitCode,
|
|
863
|
+
error: mission.error,
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
// POST /api/v1/missions/:id/hitl_response → 200; forwards to owning executor over WS
|
|
867
|
+
app.post('/api/v1/missions/:id/hitl_response', async (c) => {
|
|
868
|
+
const missionId = c.req.param('id');
|
|
869
|
+
const mission = executorRegistry.getMission(missionId);
|
|
870
|
+
if (!mission)
|
|
871
|
+
return c.json({ error: 'Mission not found' }, 404);
|
|
872
|
+
let body;
|
|
873
|
+
try {
|
|
874
|
+
body = await c.req.json();
|
|
875
|
+
}
|
|
876
|
+
catch {
|
|
877
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
878
|
+
}
|
|
879
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
880
|
+
const payload = body;
|
|
881
|
+
if (!payload.hitl_id || !payload.response) {
|
|
882
|
+
return c.json({ error: 'hitl_id and response are required' }, 400);
|
|
883
|
+
}
|
|
884
|
+
// Push hitl_responded event to the executor over WS
|
|
885
|
+
const envelope = {
|
|
886
|
+
event: 'mission.hitl_responded',
|
|
887
|
+
executor_id: mission.executorId,
|
|
888
|
+
mission_id: missionId,
|
|
889
|
+
ts: new Date().toISOString(),
|
|
890
|
+
data: {
|
|
891
|
+
hitl_id: payload.hitl_id,
|
|
892
|
+
response: payload.response,
|
|
893
|
+
responded_at: new Date().toISOString(),
|
|
894
|
+
},
|
|
895
|
+
};
|
|
896
|
+
const pushed = executorRegistry.pushToExecutor(mission.executorId, envelope);
|
|
897
|
+
if (!pushed) {
|
|
898
|
+
return c.json({ error: 'executor_not_connected' }, 503);
|
|
899
|
+
}
|
|
900
|
+
return c.json({ ok: true });
|
|
901
|
+
});
|
|
902
|
+
// POST /api/v1/missions/:id/pause → 200
|
|
903
|
+
app.post('/api/v1/missions/:id/pause', (c) => {
|
|
904
|
+
const missionId = c.req.param('id');
|
|
905
|
+
const mission = executorRegistry.getMission(missionId);
|
|
906
|
+
if (!mission)
|
|
907
|
+
return c.json({ error: 'Mission not found' }, 404);
|
|
908
|
+
// Forward pause command to executor over WS
|
|
909
|
+
executorRegistry.pushToExecutor(mission.executorId, {
|
|
910
|
+
event: 'mission.paused',
|
|
911
|
+
executor_id: mission.executorId,
|
|
912
|
+
mission_id: missionId,
|
|
913
|
+
ts: new Date().toISOString(),
|
|
914
|
+
data: { state: 'paused', reason: 'operator_request' },
|
|
915
|
+
});
|
|
916
|
+
executorRegistry.transitionMission(missionId, 'paused');
|
|
917
|
+
return c.json({ ok: true });
|
|
918
|
+
});
|
|
919
|
+
// POST /api/v1/missions/:id/resume → 200
|
|
920
|
+
app.post('/api/v1/missions/:id/resume', (c) => {
|
|
921
|
+
const missionId = c.req.param('id');
|
|
922
|
+
const mission = executorRegistry.getMission(missionId);
|
|
923
|
+
if (!mission)
|
|
924
|
+
return c.json({ error: 'Mission not found' }, 404);
|
|
925
|
+
executorRegistry.pushToExecutor(mission.executorId, {
|
|
926
|
+
event: 'mission.resumed',
|
|
927
|
+
executor_id: mission.executorId,
|
|
928
|
+
mission_id: missionId,
|
|
929
|
+
ts: new Date().toISOString(),
|
|
930
|
+
data: { state: 'running', resumed_from: 'paused' },
|
|
931
|
+
});
|
|
932
|
+
executorRegistry.transitionMission(missionId, 'running');
|
|
933
|
+
return c.json({ ok: true });
|
|
934
|
+
});
|
|
935
|
+
// POST /api/v1/missions/:id/abort → 200
|
|
936
|
+
app.post('/api/v1/missions/:id/abort', (c) => {
|
|
937
|
+
const missionId = c.req.param('id');
|
|
938
|
+
const mission = executorRegistry.getMission(missionId);
|
|
939
|
+
if (!mission)
|
|
940
|
+
return c.json({ error: 'Mission not found' }, 404);
|
|
941
|
+
executorRegistry.pushToExecutor(mission.executorId, {
|
|
942
|
+
event: 'mission.aborted',
|
|
943
|
+
executor_id: mission.executorId,
|
|
944
|
+
mission_id: missionId,
|
|
945
|
+
ts: new Date().toISOString(),
|
|
946
|
+
data: { state: 'aborted', aborted_by: 'operator', reason: 'operator_request' },
|
|
947
|
+
});
|
|
948
|
+
executorRegistry.transitionMission(missionId, 'aborted');
|
|
949
|
+
return c.json({ ok: true });
|
|
950
|
+
});
|
|
951
|
+
// ── A2A push notification webhook receiver (#1256) ────────────────────
|
|
952
|
+
//
|
|
953
|
+
// Receives StreamResponse-shape payloads pushed by an agentic-sandbox
|
|
954
|
+
// executor when SSE isn't available (e.g. serverless missions). HMAC
|
|
955
|
+
// verification is on the raw body, replay window is 5 minutes,
|
|
956
|
+
// event-id idempotency is in-process. Per-mission secrets are
|
|
957
|
+
// registered via POST /api/v1/push-configs.
|
|
958
|
+
//
|
|
959
|
+
// The endpoint path is configurable via env so deployments behind a
|
|
960
|
+
// load-balancer with path rewriting can match upstream.
|
|
961
|
+
const webhookPath = process.env['AIWG_A2A_WEBHOOK_PATH'] ?? '/aiwg/webhooks/a2a';
|
|
962
|
+
app.post(webhookPath, async (c) => {
|
|
963
|
+
const configId = c.req.query('configId') ?? c.req.query('config_id') ?? '';
|
|
964
|
+
const signature = c.req.header('x-aiwg-signature') ?? c.req.header('X-AIWG-Signature') ?? undefined;
|
|
965
|
+
const eventId = c.req.header('x-aiwg-event-id') ?? c.req.header('X-AIWG-Event-Id') ?? undefined;
|
|
966
|
+
// Read raw body bytes — signature is computed over UTF-8 bytes.
|
|
967
|
+
const rawText = await c.req.text();
|
|
968
|
+
const bodyBuf = Buffer.from(rawText, 'utf8');
|
|
969
|
+
const result = await handleWebhook(configId, bodyBuf, signature, eventId, {
|
|
970
|
+
registry: pushSecretRegistry,
|
|
971
|
+
idempotency: webhookIdempotency,
|
|
972
|
+
route: async (entry, event) => {
|
|
973
|
+
// Append to mission recentEvents via a synthesized envelope.
|
|
974
|
+
// The 'mission.webhook' event type falls through the registry's
|
|
975
|
+
// default case (no state mutation), but timestamp + telemetry
|
|
976
|
+
// still record the delivery for operator visibility.
|
|
977
|
+
if (entry.missionId) {
|
|
978
|
+
const mission = executorRegistry.getMission(entry.missionId);
|
|
979
|
+
if (mission) {
|
|
980
|
+
executorRegistry.handleEvent({
|
|
981
|
+
event: 'mission.webhook',
|
|
982
|
+
executor_id: mission.executorId,
|
|
983
|
+
mission_id: entry.missionId,
|
|
984
|
+
ts: new Date().toISOString(),
|
|
985
|
+
data: { stream_event: event },
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
telemetryStore.ingest(createEvent('a2a.webhook.received', entry.missionId ?? entry.configId, {
|
|
990
|
+
configId: entry.configId,
|
|
991
|
+
...(entry.taskId ? { taskId: entry.taskId } : {}),
|
|
992
|
+
}));
|
|
993
|
+
},
|
|
994
|
+
});
|
|
995
|
+
return c.json(result.body, result.status);
|
|
996
|
+
});
|
|
997
|
+
// POST /api/v1/push-configs → register a per-mission webhook secret.
|
|
998
|
+
// Returns { configId } so the caller can pass it to
|
|
999
|
+
// A2AClient.createPushNotificationConfig with `metadata.aiwg.config_id`.
|
|
1000
|
+
app.post('/api/v1/push-configs', async (c) => {
|
|
1001
|
+
let body;
|
|
1002
|
+
try {
|
|
1003
|
+
body = await c.req.json();
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
1007
|
+
}
|
|
1008
|
+
const p = body;
|
|
1009
|
+
if (!p.configId || typeof p.configId !== 'string') {
|
|
1010
|
+
return c.json({ error: 'configId is required' }, 400);
|
|
1011
|
+
}
|
|
1012
|
+
if (!p.secret || typeof p.secret !== 'string' || p.secret.length < 16) {
|
|
1013
|
+
return c.json({ error: 'secret is required and must be ≥16 chars' }, 400);
|
|
1014
|
+
}
|
|
1015
|
+
pushSecretRegistry.register({
|
|
1016
|
+
configId: p.configId,
|
|
1017
|
+
secret: p.secret,
|
|
1018
|
+
...(p.missionId ? { missionId: p.missionId } : {}),
|
|
1019
|
+
...(p.taskId ? { taskId: p.taskId } : {}),
|
|
1020
|
+
...(p.metadata ? { metadata: p.metadata } : {}),
|
|
1021
|
+
});
|
|
1022
|
+
return c.json({ ok: true, configId: p.configId }, 201);
|
|
1023
|
+
});
|
|
1024
|
+
// DELETE /api/v1/push-configs/:configId → unregister on mission complete.
|
|
1025
|
+
app.delete('/api/v1/push-configs/:configId', (c) => {
|
|
1026
|
+
const configId = c.req.param('configId');
|
|
1027
|
+
const removed = pushSecretRegistry.unregister(configId);
|
|
1028
|
+
if (!removed)
|
|
1029
|
+
return c.json({ error: 'configId not found' }, 404);
|
|
1030
|
+
return new Response(null, { status: 204 });
|
|
1031
|
+
});
|
|
1032
|
+
// GET /api/v1/push-configs → debug surface; lists registered configIds.
|
|
1033
|
+
app.get('/api/v1/push-configs', (_c) => {
|
|
1034
|
+
return _c.json({ count: pushSecretRegistry.size() });
|
|
1035
|
+
});
|
|
1036
|
+
// Telemetry API (#716)
|
|
1037
|
+
app.get('/api/telemetry', (c) => {
|
|
1038
|
+
const sid = c.req.query('sessionId');
|
|
1039
|
+
const limit = parseInt(c.req.query('limit') ?? '100', 10);
|
|
1040
|
+
const events = telemetryStore.query(sid || 'default', { limit });
|
|
1041
|
+
return c.json({ events });
|
|
1042
|
+
});
|
|
1043
|
+
app.get('/api/telemetry/metrics', (c) => {
|
|
1044
|
+
const sid = c.req.query('sessionId') || 'default';
|
|
1045
|
+
return c.json(telemetryStore.metrics(sid));
|
|
1046
|
+
});
|
|
1047
|
+
app.post('/api/telemetry', async (c) => {
|
|
1048
|
+
try {
|
|
1049
|
+
const body = await c.req.json();
|
|
1050
|
+
telemetryStore.ingest(body);
|
|
1051
|
+
return c.json({ ok: true }, 201);
|
|
1052
|
+
}
|
|
1053
|
+
catch {
|
|
1054
|
+
return c.json({ error: 'Invalid event' }, 400);
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
if (!opts.readOnly) {
|
|
1058
|
+
app.post('/api/sessions', (c) => c.json({ id: null, error: 'Use /ws/pty/:sessionId to start a PTY session' }, 501));
|
|
1059
|
+
}
|
|
1060
|
+
// ---- Sandbox Registration API (#731) ----
|
|
1061
|
+
// Register a sandbox instance
|
|
1062
|
+
app.post('/api/sandboxes/register', async (c) => {
|
|
1063
|
+
try {
|
|
1064
|
+
const body = await c.req.json();
|
|
1065
|
+
if (!body.name || !body.grpc_endpoint || !body.ws_endpoint || !body.http_endpoint) {
|
|
1066
|
+
return c.json({ error: 'Missing required fields: name, grpc_endpoint, ws_endpoint, http_endpoint' }, 400);
|
|
1067
|
+
}
|
|
1068
|
+
const result = sandboxRegistry.register(body);
|
|
1069
|
+
return c.json(result, 201);
|
|
1070
|
+
}
|
|
1071
|
+
catch {
|
|
1072
|
+
return c.json({ error: 'Invalid request body' }, 400);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
// Clear all disconnected sandboxes (forces re-registration)
|
|
1076
|
+
app.delete('/api/sandboxes/offline', (c) => {
|
|
1077
|
+
const removed = sandboxRegistry.clearOffline();
|
|
1078
|
+
return c.json({ ok: true, removed });
|
|
1079
|
+
});
|
|
1080
|
+
// Admin-force deregister any sandbox by id (no sandbox token required — local dashboard only).
|
|
1081
|
+
// Idempotent: returns ok even if the sandbox is already gone (e.g. server restarted since last poll).
|
|
1082
|
+
app.delete('/api/sandboxes/:id/forget', (c) => {
|
|
1083
|
+
sandboxRegistry.deregister(c.req.param('id'));
|
|
1084
|
+
return c.json({ ok: true });
|
|
1085
|
+
});
|
|
1086
|
+
// Deregister a sandbox
|
|
1087
|
+
app.delete('/api/sandboxes/:id', (c) => {
|
|
1088
|
+
const id = c.req.param('id');
|
|
1089
|
+
const token = (c.req.header('authorization') ?? '').replace(/^Bearer\s+/i, '');
|
|
1090
|
+
if (!sandboxRegistry.authenticate(id, token)) {
|
|
1091
|
+
return c.json({ error: 'Unauthorized' }, 401);
|
|
1092
|
+
}
|
|
1093
|
+
sandboxRegistry.deregister(id);
|
|
1094
|
+
return c.json({ ok: true });
|
|
1095
|
+
});
|
|
1096
|
+
// List all registered sandboxes
|
|
1097
|
+
app.get('/api/sandboxes', (c) => {
|
|
1098
|
+
return c.json({ sandboxes: sandboxRegistry.list() });
|
|
1099
|
+
});
|
|
1100
|
+
// Get a single sandbox
|
|
1101
|
+
app.get('/api/sandboxes/:id', (c) => {
|
|
1102
|
+
const summary = sandboxRegistry.getSummary(c.req.param('id'));
|
|
1103
|
+
if (!summary)
|
|
1104
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1105
|
+
return c.json(summary);
|
|
1106
|
+
});
|
|
1107
|
+
// List agents for a specific sandbox
|
|
1108
|
+
app.get('/api/sandboxes/:id/agents', (c) => {
|
|
1109
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1110
|
+
if (!sandbox)
|
|
1111
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1112
|
+
return c.json({ agents: [...sandbox.agents.values()] });
|
|
1113
|
+
});
|
|
1114
|
+
// List all agents across all sandboxes
|
|
1115
|
+
app.get('/api/agents', (c) => {
|
|
1116
|
+
return c.json({ agents: sandboxRegistry.allAgents() });
|
|
1117
|
+
});
|
|
1118
|
+
// Routing candidates — returns all agents ranked by filter match + load score (#916)
|
|
1119
|
+
// Query params map directly to AgentFilter fields (JSON-encoded or comma-separated).
|
|
1120
|
+
app.get('/api/agents/candidates', (c) => {
|
|
1121
|
+
const q = c.req.query();
|
|
1122
|
+
const filter = {};
|
|
1123
|
+
if (q.sandbox_id)
|
|
1124
|
+
filter.sandbox_id = q.sandbox_id;
|
|
1125
|
+
if (q.agent_id)
|
|
1126
|
+
filter.agent_id = q.agent_id;
|
|
1127
|
+
if (q.agent_name)
|
|
1128
|
+
filter.agent_name = q.agent_name;
|
|
1129
|
+
if (q.frameworks)
|
|
1130
|
+
filter.frameworks = q.frameworks.split(',').filter(Boolean);
|
|
1131
|
+
if (q.agents)
|
|
1132
|
+
filter.agents = q.agents.split(',').filter(Boolean);
|
|
1133
|
+
if (q.skills)
|
|
1134
|
+
filter.skills = q.skills.split(',').filter(Boolean);
|
|
1135
|
+
if (q.max_cpu_percent)
|
|
1136
|
+
filter.max_cpu_percent = parseFloat(q.max_cpu_percent);
|
|
1137
|
+
if (q.min_memory_gb)
|
|
1138
|
+
filter.min_memory_gb = parseFloat(q.min_memory_gb);
|
|
1139
|
+
const allAgents = sandboxRegistry.allAgents();
|
|
1140
|
+
const result = routeTask(allAgents, filter);
|
|
1141
|
+
return c.json(result);
|
|
1142
|
+
});
|
|
1143
|
+
// Proxy endpoints for sandbox lifecycle (#733)
|
|
1144
|
+
// These forward to the registered sandbox's HTTP endpoint
|
|
1145
|
+
app.get('/api/sandboxes/:id/loadouts', async (c) => {
|
|
1146
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1147
|
+
if (!sandbox)
|
|
1148
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1149
|
+
try {
|
|
1150
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/loadouts`);
|
|
1151
|
+
return c.json(await resp.json(), resp.status);
|
|
1152
|
+
}
|
|
1153
|
+
catch (err) {
|
|
1154
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
// List VMs running inside a sandbox (#930)
|
|
1158
|
+
// Proxies GET /api/v1/vms on the sandbox HTTP endpoint.
|
|
1159
|
+
// Supports `state` and `prefix` query params per sandbox API contract.
|
|
1160
|
+
app.get('/api/sandboxes/:id/vms', async (c) => {
|
|
1161
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1162
|
+
if (!sandbox)
|
|
1163
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1164
|
+
try {
|
|
1165
|
+
const url = new URL(`${sandbox.httpEndpoint}/api/v1/vms`);
|
|
1166
|
+
const state = c.req.query('state');
|
|
1167
|
+
const prefix = c.req.query('prefix');
|
|
1168
|
+
if (state)
|
|
1169
|
+
url.searchParams.set('state', state);
|
|
1170
|
+
if (prefix)
|
|
1171
|
+
url.searchParams.set('prefix', prefix);
|
|
1172
|
+
const resp = await fetch(url.toString());
|
|
1173
|
+
return c.json(await resp.json(), resp.status);
|
|
1174
|
+
}
|
|
1175
|
+
catch (err) {
|
|
1176
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
// Get a single VM's details — resource allocation, IP, attached agent (#930)
|
|
1180
|
+
app.get('/api/sandboxes/:id/vms/:name', async (c) => {
|
|
1181
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1182
|
+
if (!sandbox)
|
|
1183
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1184
|
+
try {
|
|
1185
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/vms/${encodeURIComponent(c.req.param('name'))}`);
|
|
1186
|
+
return c.json(await resp.json(), resp.status);
|
|
1187
|
+
}
|
|
1188
|
+
catch (err) {
|
|
1189
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
// Authoritative agent list from sandbox HTTP API (#930).
|
|
1193
|
+
// Complements the event-cached view at /api/sandboxes/:id/agents by calling
|
|
1194
|
+
// the sandbox's own /api/v1/agents — useful when events have been missed or
|
|
1195
|
+
// when the dashboard needs full system_info / metrics not captured in events.
|
|
1196
|
+
app.get('/api/sandboxes/:id/agents/full', async (c) => {
|
|
1197
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1198
|
+
if (!sandbox)
|
|
1199
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1200
|
+
try {
|
|
1201
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents`);
|
|
1202
|
+
return c.json(await resp.json(), resp.status);
|
|
1203
|
+
}
|
|
1204
|
+
catch (err) {
|
|
1205
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
// Proxy the loadout registry (frameworks/providers/init scripts/presets)
|
|
1209
|
+
// used by the UI's compose builder.
|
|
1210
|
+
app.get('/api/sandboxes/:id/loadout-registry', async (c) => {
|
|
1211
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1212
|
+
if (!sandbox)
|
|
1213
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1214
|
+
try {
|
|
1215
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/loadout/registry`);
|
|
1216
|
+
return c.json(await resp.json(), resp.status);
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
// Pass-through proxy for VM creation. Forwards the request body verbatim to
|
|
1223
|
+
// the sandbox /api/v1/vms endpoint so the UI can use either preset
|
|
1224
|
+
// (profile/loadout) or custom (composition) shapes without translation loss.
|
|
1225
|
+
app.post('/api/sandboxes/:id/provision', async (c) => {
|
|
1226
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1227
|
+
if (!sandbox)
|
|
1228
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1229
|
+
try {
|
|
1230
|
+
const body = await c.req.json();
|
|
1231
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/vms`, {
|
|
1232
|
+
method: 'POST',
|
|
1233
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1234
|
+
body: JSON.stringify(body),
|
|
1235
|
+
});
|
|
1236
|
+
return c.json(await resp.json(), resp.status);
|
|
1237
|
+
}
|
|
1238
|
+
catch (err) {
|
|
1239
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
// Proxy agent lifecycle actions to sandbox
|
|
1243
|
+
for (const action of ['start', 'stop', 'destroy', 'reprovision']) {
|
|
1244
|
+
app.post(`/api/sandboxes/:id/agents/:aid/${action}`, async (c) => {
|
|
1245
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1246
|
+
if (!sandbox)
|
|
1247
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1248
|
+
try {
|
|
1249
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/${action}`, {
|
|
1250
|
+
method: 'POST',
|
|
1251
|
+
});
|
|
1252
|
+
return c.json(await resp.json(), resp.status);
|
|
1253
|
+
}
|
|
1254
|
+
catch (err) {
|
|
1255
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
app.delete('/api/sandboxes/:id/agents/:aid', async (c) => {
|
|
1260
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1261
|
+
if (!sandbox)
|
|
1262
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1263
|
+
try {
|
|
1264
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}`, {
|
|
1265
|
+
method: 'DELETE',
|
|
1266
|
+
});
|
|
1267
|
+
return c.json(await resp.json(), resp.status);
|
|
1268
|
+
}
|
|
1269
|
+
catch (err) {
|
|
1270
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
// ---- VM lifecycle proxies (#1146) ----
|
|
1274
|
+
// Mirror /api/v1/vms/{name}/{start,stop,restart,destroy,deploy-agent} and
|
|
1275
|
+
// DELETE /api/v1/vms/{name}. Bodies and query strings are forwarded as-is so
|
|
1276
|
+
// callers can pass libvirt-specific options (mode, timeout_seconds, force,
|
|
1277
|
+
// delete_disk, etc.) without AIWG having to know about them.
|
|
1278
|
+
for (const action of ['start', 'stop', 'restart', 'destroy', 'deploy-agent']) {
|
|
1279
|
+
app.post(`/api/sandboxes/:id/vms/:name/${action}`, async (c) => {
|
|
1280
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1281
|
+
if (!sandbox)
|
|
1282
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1283
|
+
try {
|
|
1284
|
+
const body = await c.req.text().catch(() => '');
|
|
1285
|
+
const headers = {};
|
|
1286
|
+
const ct = c.req.header('content-type');
|
|
1287
|
+
if (body && ct)
|
|
1288
|
+
headers['Content-Type'] = ct;
|
|
1289
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/vms/${encodeURIComponent(c.req.param('name'))}/${action}`, { method: 'POST', headers, body: body || undefined });
|
|
1290
|
+
const text = await resp.text();
|
|
1291
|
+
return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
|
|
1292
|
+
}
|
|
1293
|
+
catch (err) {
|
|
1294
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
app.delete('/api/sandboxes/:id/vms/:name', async (c) => {
|
|
1299
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1300
|
+
if (!sandbox)
|
|
1301
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1302
|
+
try {
|
|
1303
|
+
const url = new URL(`${sandbox.httpEndpoint}/api/v1/vms/${encodeURIComponent(c.req.param('name'))}`);
|
|
1304
|
+
const force = c.req.query('force');
|
|
1305
|
+
const deleteDisk = c.req.query('delete_disk');
|
|
1306
|
+
if (force)
|
|
1307
|
+
url.searchParams.set('force', force);
|
|
1308
|
+
if (deleteDisk)
|
|
1309
|
+
url.searchParams.set('delete_disk', deleteDisk);
|
|
1310
|
+
const resp = await fetch(url.toString(), { method: 'DELETE' });
|
|
1311
|
+
const text = await resp.text();
|
|
1312
|
+
return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
|
|
1313
|
+
}
|
|
1314
|
+
catch (err) {
|
|
1315
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
// ---- Containers proxies (#1146) ----
|
|
1319
|
+
// Mirror /api/v1/containers (list/get/create/delete/start/stop) and
|
|
1320
|
+
// /api/v1/container-images. Same pattern as the /vms proxy block — thin
|
|
1321
|
+
// pass-through so the dashboard's container surface lights up identically
|
|
1322
|
+
// to the agentic-sandbox dashboard's Instances panel.
|
|
1323
|
+
app.get('/api/sandboxes/:id/containers', async (c) => {
|
|
1324
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1325
|
+
if (!sandbox)
|
|
1326
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1327
|
+
try {
|
|
1328
|
+
const url = new URL(`${sandbox.httpEndpoint}/api/v1/containers`);
|
|
1329
|
+
const status = c.req.query('status');
|
|
1330
|
+
if (status)
|
|
1331
|
+
url.searchParams.set('status', status);
|
|
1332
|
+
const resp = await fetch(url.toString());
|
|
1333
|
+
return c.json(await resp.json(), resp.status);
|
|
1334
|
+
}
|
|
1335
|
+
catch (err) {
|
|
1336
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
app.get('/api/sandboxes/:id/containers/:name', async (c) => {
|
|
1340
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1341
|
+
if (!sandbox)
|
|
1342
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1343
|
+
try {
|
|
1344
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers/${encodeURIComponent(c.req.param('name'))}`);
|
|
1345
|
+
return c.json(await resp.json(), resp.status);
|
|
1346
|
+
}
|
|
1347
|
+
catch (err) {
|
|
1348
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
app.post('/api/sandboxes/:id/containers', async (c) => {
|
|
1352
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1353
|
+
if (!sandbox)
|
|
1354
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1355
|
+
try {
|
|
1356
|
+
const body = await c.req.json();
|
|
1357
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers`, {
|
|
1358
|
+
method: 'POST',
|
|
1359
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1360
|
+
body: JSON.stringify(body),
|
|
1361
|
+
});
|
|
1362
|
+
return c.json(await resp.json(), resp.status);
|
|
1363
|
+
}
|
|
1364
|
+
catch (err) {
|
|
1365
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1366
|
+
}
|
|
1367
|
+
});
|
|
1368
|
+
for (const action of ['start', 'stop']) {
|
|
1369
|
+
app.post(`/api/sandboxes/:id/containers/:name/${action}`, async (c) => {
|
|
1370
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1371
|
+
if (!sandbox)
|
|
1372
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1373
|
+
try {
|
|
1374
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers/${encodeURIComponent(c.req.param('name'))}/${action}`, { method: 'POST' });
|
|
1375
|
+
const text = await resp.text();
|
|
1376
|
+
return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
|
|
1377
|
+
}
|
|
1378
|
+
catch (err) {
|
|
1379
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
app.delete('/api/sandboxes/:id/containers/:name', async (c) => {
|
|
1384
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1385
|
+
if (!sandbox)
|
|
1386
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1387
|
+
try {
|
|
1388
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/containers/${encodeURIComponent(c.req.param('name'))}`, { method: 'DELETE' });
|
|
1389
|
+
const text = await resp.text();
|
|
1390
|
+
return c.body(text, resp.status, { 'Content-Type': resp.headers.get('content-type') ?? 'application/json' });
|
|
1391
|
+
}
|
|
1392
|
+
catch (err) {
|
|
1393
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
app.get('/api/sandboxes/:id/container-images', async (c) => {
|
|
1397
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1398
|
+
if (!sandbox)
|
|
1399
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1400
|
+
try {
|
|
1401
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/container-images`);
|
|
1402
|
+
return c.json(await resp.json(), resp.status);
|
|
1403
|
+
}
|
|
1404
|
+
catch (err) {
|
|
1405
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1406
|
+
}
|
|
1407
|
+
});
|
|
1408
|
+
// Agent identity alias endpoint (#917)
|
|
1409
|
+
// Assigns a stable human-readable logical name to an agent.
|
|
1410
|
+
// Persists in ~/.config/aiwg/sandbox-agents.json so name survives AIWG restarts.
|
|
1411
|
+
app.post('/api/sandboxes/:id/agents/:aid/alias', async (c) => {
|
|
1412
|
+
const sandboxId = c.req.param('id');
|
|
1413
|
+
const agentId = c.req.param('aid');
|
|
1414
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1415
|
+
const { name } = body;
|
|
1416
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
1417
|
+
return c.json({ error: 'name is required' }, 400);
|
|
1418
|
+
}
|
|
1419
|
+
const ok = sandboxRegistry.aliasAgent(sandboxId, agentId, name.trim());
|
|
1420
|
+
if (!ok)
|
|
1421
|
+
return c.json({ error: 'Agent or sandbox not found' }, 404);
|
|
1422
|
+
return c.json({ ok: true, logicalName: name.trim() });
|
|
1423
|
+
});
|
|
1424
|
+
// Resolve agent by agentId, instanceId, or logicalName (#917)
|
|
1425
|
+
app.get('/api/agents/resolve/:ref', (c) => {
|
|
1426
|
+
const ref = c.req.param('ref');
|
|
1427
|
+
const result = sandboxRegistry.resolveAgent(ref);
|
|
1428
|
+
if (!result)
|
|
1429
|
+
return c.json({ error: `Agent '${ref}' not found` }, 404);
|
|
1430
|
+
return c.json({
|
|
1431
|
+
sandboxId: result.sandbox.id,
|
|
1432
|
+
sandboxName: result.sandbox.name,
|
|
1433
|
+
agent: result.agent,
|
|
1434
|
+
});
|
|
1435
|
+
});
|
|
1436
|
+
// List known agent identities from persistent store (#917)
|
|
1437
|
+
app.get('/api/agents/identities', (c) => {
|
|
1438
|
+
return c.json({ identities: sandboxRegistry.knownAgentIdentities() });
|
|
1439
|
+
});
|
|
1440
|
+
// Session management proxy endpoints (#896)
|
|
1441
|
+
// Forwards to sandbox management HTTP API: GET/POST /api/v1/agents/:aid/sessions
|
|
1442
|
+
// and DELETE /api/v1/agents/:aid/sessions/:session
|
|
1443
|
+
// Live since agentic-sandbox v2026.7.x (#140 sessions API, #611 host-runtime
|
|
1444
|
+
// session listing); the 502 path is now only a genuine-unreachability fallback.
|
|
1445
|
+
app.get('/api/sandboxes/:id/agents/:aid/sessions', async (c) => {
|
|
1446
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1447
|
+
if (!sandbox)
|
|
1448
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1449
|
+
try {
|
|
1450
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/sessions`);
|
|
1451
|
+
return c.json(await resp.json(), resp.status);
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
app.post('/api/sandboxes/:id/agents/:aid/sessions', async (c) => {
|
|
1458
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1459
|
+
if (!sandbox)
|
|
1460
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1461
|
+
try {
|
|
1462
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1463
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/sessions`, {
|
|
1464
|
+
method: 'POST',
|
|
1465
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1466
|
+
body: JSON.stringify(body),
|
|
1467
|
+
});
|
|
1468
|
+
return c.json(await resp.json(), resp.status);
|
|
1469
|
+
}
|
|
1470
|
+
catch (err) {
|
|
1471
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1472
|
+
}
|
|
1473
|
+
});
|
|
1474
|
+
// DELETE /api/sandboxes/:id/agents/:aid/sessions/:session
|
|
1475
|
+
// Path key `:session` is the session_name (not session_id) — sandbox returns 204 No Content.
|
|
1476
|
+
app.delete('/api/sandboxes/:id/agents/:aid/sessions/:session', async (c) => {
|
|
1477
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1478
|
+
if (!sandbox)
|
|
1479
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1480
|
+
try {
|
|
1481
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/sessions/${c.req.param('session')}`, { method: 'DELETE' });
|
|
1482
|
+
if (resp.status === 204)
|
|
1483
|
+
return new Response(null, { status: 204 });
|
|
1484
|
+
return c.json(await resp.json(), resp.status);
|
|
1485
|
+
}
|
|
1486
|
+
catch (err) {
|
|
1487
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1488
|
+
}
|
|
1489
|
+
});
|
|
1490
|
+
// HITL endpoints (#732)
|
|
1491
|
+
app.get('/api/hitl', (c) => {
|
|
1492
|
+
return c.json({ requests: sandboxRegistry.pendingHitl() });
|
|
1493
|
+
});
|
|
1494
|
+
app.post('/api/hitl/:id/respond', async (c) => {
|
|
1495
|
+
const hitlId = c.req.param('id');
|
|
1496
|
+
// Use getHitl (not resolveHitl) — don't remove from queue until sandbox confirms (#908)
|
|
1497
|
+
const hitl = sandboxRegistry.getHitl(hitlId);
|
|
1498
|
+
if (!hitl)
|
|
1499
|
+
return c.json({ error: 'HITL request not found or already resolved' }, 404);
|
|
1500
|
+
const sandbox = sandboxRegistry.get(hitl.sandboxId);
|
|
1501
|
+
if (!sandbox)
|
|
1502
|
+
return c.json({ error: 'Sandbox no longer registered' }, 410);
|
|
1503
|
+
try {
|
|
1504
|
+
const { text } = await c.req.json();
|
|
1505
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/hitl/${hitlId}/respond`, {
|
|
1506
|
+
method: 'POST',
|
|
1507
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1508
|
+
body: JSON.stringify({ text }),
|
|
1509
|
+
});
|
|
1510
|
+
if (resp.ok || resp.status === 204) {
|
|
1511
|
+
// Remove from queue only after sandbox confirms receipt (#908)
|
|
1512
|
+
sandboxRegistry.resolveHitl(hitlId);
|
|
1513
|
+
// Notify browser subscribers that the HITL was responded to (#908)
|
|
1514
|
+
sandboxRegistry.emit({
|
|
1515
|
+
type: 'hitl.responded',
|
|
1516
|
+
sandboxId: hitl.sandboxId,
|
|
1517
|
+
agentId: hitl.agentId,
|
|
1518
|
+
timestamp: new Date().toISOString(),
|
|
1519
|
+
hitlId,
|
|
1520
|
+
sessionId: hitl.sessionId,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
// Return 200 regardless of sandbox's 204 (body-less responses cause issues on some clients)
|
|
1524
|
+
return c.json({ ok: resp.ok }, resp.ok ? 200 : resp.status);
|
|
1525
|
+
}
|
|
1526
|
+
catch (err) {
|
|
1527
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
app.post('/api/hitl/:id/dismiss', (c) => {
|
|
1531
|
+
const hitl = sandboxRegistry.resolveHitl(c.req.param('id'));
|
|
1532
|
+
if (!hitl)
|
|
1533
|
+
return c.json({ error: 'HITL request not found or already resolved' }, 404);
|
|
1534
|
+
return c.json({ ok: true });
|
|
1535
|
+
});
|
|
1536
|
+
// Task proxy endpoints (#907)
|
|
1537
|
+
// Forward to sandbox /api/v1/tasks — enables the serve dashboard task submission UI
|
|
1538
|
+
app.get('/api/sandboxes/:id/tasks', async (c) => {
|
|
1539
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1540
|
+
if (!sandbox)
|
|
1541
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1542
|
+
try {
|
|
1543
|
+
const url = new URL(`${sandbox.httpEndpoint}/api/v1/tasks`);
|
|
1544
|
+
const qs = c.req.query();
|
|
1545
|
+
for (const [k, v] of Object.entries(qs))
|
|
1546
|
+
url.searchParams.set(k, v);
|
|
1547
|
+
const resp = await fetch(url.toString());
|
|
1548
|
+
return c.json(await resp.json(), resp.status);
|
|
1549
|
+
}
|
|
1550
|
+
catch (err) {
|
|
1551
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
app.post('/api/sandboxes/:id/tasks', async (c) => {
|
|
1555
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1556
|
+
if (!sandbox)
|
|
1557
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1558
|
+
try {
|
|
1559
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1560
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/tasks`, {
|
|
1561
|
+
method: 'POST',
|
|
1562
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1563
|
+
body: JSON.stringify(body),
|
|
1564
|
+
});
|
|
1565
|
+
return c.json(await resp.json(), resp.status);
|
|
1566
|
+
}
|
|
1567
|
+
catch (err) {
|
|
1568
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
app.get('/api/sandboxes/:id/tasks/:taskId', async (c) => {
|
|
1572
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1573
|
+
if (!sandbox)
|
|
1574
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1575
|
+
try {
|
|
1576
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/tasks/${c.req.param('taskId')}`);
|
|
1577
|
+
return c.json(await resp.json(), resp.status);
|
|
1578
|
+
}
|
|
1579
|
+
catch (err) {
|
|
1580
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
app.delete('/api/sandboxes/:id/tasks/:taskId', async (c) => {
|
|
1584
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1585
|
+
if (!sandbox)
|
|
1586
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1587
|
+
try {
|
|
1588
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/tasks/${c.req.param('taskId')}`, {
|
|
1589
|
+
method: 'DELETE',
|
|
1590
|
+
});
|
|
1591
|
+
if (resp.status === 204)
|
|
1592
|
+
return new Response(null, { status: 204 });
|
|
1593
|
+
return c.json(await resp.json(), resp.status);
|
|
1594
|
+
}
|
|
1595
|
+
catch (err) {
|
|
1596
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
// Screen state proxy (#913)
|
|
1600
|
+
// Forwards to sandbox GET /api/v1/sessions/:sessionId/screen — already exists in sandbox.
|
|
1601
|
+
app.get('/api/sandboxes/:id/sessions/:sessionId/screen', async (c) => {
|
|
1602
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1603
|
+
if (!sandbox)
|
|
1604
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1605
|
+
try {
|
|
1606
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/sessions/${c.req.param('sessionId')}/screen`);
|
|
1607
|
+
return c.json(await resp.json(), resp.status);
|
|
1608
|
+
}
|
|
1609
|
+
catch (err) {
|
|
1610
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
// Remote AIWG exec proxy (#914)
|
|
1614
|
+
// Forwards to sandbox POST /api/v1/aiwg/exec — requires sandbox companion issue.
|
|
1615
|
+
// Will 502 gracefully until that endpoint is implemented.
|
|
1616
|
+
app.post('/api/sandboxes/:id/agents/:aid/aiwg/exec', async (c) => {
|
|
1617
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1618
|
+
if (!sandbox)
|
|
1619
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1620
|
+
try {
|
|
1621
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1622
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/aiwg/exec`, {
|
|
1623
|
+
method: 'POST',
|
|
1624
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1625
|
+
body: JSON.stringify(body),
|
|
1626
|
+
});
|
|
1627
|
+
return c.json(await resp.json(), resp.status);
|
|
1628
|
+
}
|
|
1629
|
+
catch (err) {
|
|
1630
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
// Framework update proxy (#910)
|
|
1634
|
+
// Instructs a sandbox agent to update a named framework.
|
|
1635
|
+
// Requires sandbox companion issue (PATCH /api/v1/agents/:id/frameworks/:name).
|
|
1636
|
+
app.patch('/api/sandboxes/:id/agents/:aid/frameworks/:name', async (c) => {
|
|
1637
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1638
|
+
if (!sandbox)
|
|
1639
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1640
|
+
try {
|
|
1641
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1642
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/frameworks/${c.req.param('name')}`, {
|
|
1643
|
+
method: 'PATCH',
|
|
1644
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1645
|
+
body: JSON.stringify(body),
|
|
1646
|
+
});
|
|
1647
|
+
if (resp.status === 204)
|
|
1648
|
+
return new Response(null, { status: 204 });
|
|
1649
|
+
return c.json(await resp.json(), resp.status);
|
|
1650
|
+
}
|
|
1651
|
+
catch (err) {
|
|
1652
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
// Agent manifest discovery and push (#909)
|
|
1656
|
+
// GET — list deployed manifests for a platform (with content hashes for drift detection)
|
|
1657
|
+
// POST — push/replace a single agent manifest
|
|
1658
|
+
// Requires sandbox companion issue (GET/POST /api/v1/agents/:id/manifests/:platform).
|
|
1659
|
+
app.get('/api/sandboxes/:id/agents/:aid/manifests/:platform', async (c) => {
|
|
1660
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1661
|
+
if (!sandbox)
|
|
1662
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1663
|
+
try {
|
|
1664
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/manifests/${c.req.param('platform')}`);
|
|
1665
|
+
return c.json(await resp.json(), resp.status);
|
|
1666
|
+
}
|
|
1667
|
+
catch (err) {
|
|
1668
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1669
|
+
}
|
|
1670
|
+
});
|
|
1671
|
+
app.post('/api/sandboxes/:id/agents/:aid/manifests/:platform', async (c) => {
|
|
1672
|
+
const sandbox = sandboxRegistry.get(c.req.param('id'));
|
|
1673
|
+
if (!sandbox)
|
|
1674
|
+
return c.json({ error: 'Sandbox not found' }, 404);
|
|
1675
|
+
try {
|
|
1676
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1677
|
+
const resp = await fetch(`${sandbox.httpEndpoint}/api/v1/agents/${c.req.param('aid')}/manifests/${c.req.param('platform')}`, {
|
|
1678
|
+
method: 'POST',
|
|
1679
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1680
|
+
body: JSON.stringify(body),
|
|
1681
|
+
});
|
|
1682
|
+
return c.json(await resp.json(), resp.status);
|
|
1683
|
+
}
|
|
1684
|
+
catch (err) {
|
|
1685
|
+
return c.json({ error: `Sandbox unreachable: ${err instanceof Error ? err.message : String(err)}` }, 502);
|
|
1686
|
+
}
|
|
1687
|
+
});
|
|
1688
|
+
// Static file serving — apps/web/dist/ (#714)
|
|
1689
|
+
// Only register serveStatic when the dist directory actually exists.
|
|
1690
|
+
// When it doesn't, fall back to a helpful HTML placeholder so the
|
|
1691
|
+
// browser gets a 503 with context rather than a bare "Not found".
|
|
1692
|
+
const webDistDir = path.join(opts.frameworkRoot, 'apps', 'web', 'dist');
|
|
1693
|
+
if (existsSync(webDistDir)) {
|
|
1694
|
+
try {
|
|
1695
|
+
const { serveStatic } = await (new Function('m', 'return import(m)'))('@hono/node-server/serve-static');
|
|
1696
|
+
app.use('/*', serveStatic({ root: webDistDir }));
|
|
1697
|
+
}
|
|
1698
|
+
catch {
|
|
1699
|
+
// serve-static import failed — fall through to placeholder below
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (!existsSync(webDistDir)) {
|
|
1703
|
+
app.get('/*', (c) => c.html(`<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
|
|
1704
|
+
<title>AIWG Dashboard</title>
|
|
1705
|
+
<style>body{font-family:system-ui,sans-serif;max-width:600px;margin:80px auto;padding:0 20px;color:#333}
|
|
1706
|
+
code{background:#f4f4f4;padding:2px 6px;border-radius:3px}
|
|
1707
|
+
a{color:#0070f3}</style></head><body>
|
|
1708
|
+
<h1>AIWG Dashboard</h1>
|
|
1709
|
+
<p>The dashboard UI has not been built yet.</p>
|
|
1710
|
+
<p>The <strong>API is fully operational</strong> — try
|
|
1711
|
+
<a href="/api/health">/api/health</a> or
|
|
1712
|
+
<a href="/api/sandboxes">/api/sandboxes</a>.</p>
|
|
1713
|
+
<hr>
|
|
1714
|
+
<p>To build the UI, run:<br><code>pnpm --filter @aiwg/web build</code></p>
|
|
1715
|
+
</body></html>`, 503));
|
|
1716
|
+
}
|
|
1717
|
+
// Fallback: 404 for API routes not matched above
|
|
1718
|
+
app.notFound((c) => {
|
|
1719
|
+
if (c.req.path.startsWith('/api/') || c.req.path.startsWith('/ws/')) {
|
|
1720
|
+
return c.json({ error: 'Not found' }, 404);
|
|
1721
|
+
}
|
|
1722
|
+
// SPA fallback — serve index.html for client-side routing
|
|
1723
|
+
return c.text('Not found', 404);
|
|
1724
|
+
});
|
|
1725
|
+
// Resolve the actual bound port (matters for opts.port === 0 which lets the
|
|
1726
|
+
// OS pick an ephemeral port). @hono/node-server's serve() accepts a
|
|
1727
|
+
// listen-complete callback that receives the AddressInfo for the bound socket.
|
|
1728
|
+
// We await that callback so the URL we return reflects the real port (#1275).
|
|
1729
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1730
|
+
const server = await new Promise((resolve, reject) => {
|
|
1731
|
+
try {
|
|
1732
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1733
|
+
const s = serve({ fetch: app.fetch, port: opts.port, hostname: opts.host }, () => resolve(s));
|
|
1734
|
+
// Fallback: if serve does not invoke the callback for some reason, resolve
|
|
1735
|
+
// after a short tick so we don't hang indefinitely.
|
|
1736
|
+
setTimeout(() => resolve(s), 100).unref?.();
|
|
1737
|
+
}
|
|
1738
|
+
catch (err) {
|
|
1739
|
+
reject(err);
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
// Discover the actual bound port. @hono/node-server returns the underlying
|
|
1743
|
+
// http.Server or a wrapper exposing .address() / .server.address().
|
|
1744
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1745
|
+
const addressSrc = server?.server ?? server;
|
|
1746
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1747
|
+
const addr = typeof addressSrc?.address === 'function' ? addressSrc.address() : null;
|
|
1748
|
+
const actualPort = typeof addr === 'object' && addr && typeof addr.port === 'number' ? addr.port : opts.port;
|
|
1749
|
+
const url = `http://${opts.host}:${actualPort}`;
|
|
1750
|
+
// Wire up WebSocket routes via Node.js upgrade event (#851)
|
|
1751
|
+
await setupWebSockets(server, opts.readOnly);
|
|
1752
|
+
return {
|
|
1753
|
+
url,
|
|
1754
|
+
close: () => {
|
|
1755
|
+
ptyRegistry.shutdown();
|
|
1756
|
+
sandboxRegistry.shutdown();
|
|
1757
|
+
if (typeof server.close === 'function') {
|
|
1758
|
+
server.close();
|
|
1759
|
+
}
|
|
1760
|
+
},
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Serve command handler
|
|
1765
|
+
*/
|
|
1766
|
+
export const serveHandler = {
|
|
1767
|
+
id: 'serve',
|
|
1768
|
+
name: 'Serve',
|
|
1769
|
+
description: 'Start local HTTP dashboard server',
|
|
1770
|
+
category: 'project',
|
|
1771
|
+
aliases: [],
|
|
1772
|
+
async execute(ctx) {
|
|
1773
|
+
const { port, host, open, readOnly } = parseServeArgs(ctx.args);
|
|
1774
|
+
let server;
|
|
1775
|
+
try {
|
|
1776
|
+
server = await startServer({ port, host, readOnly, frameworkRoot: ctx.frameworkRoot });
|
|
1777
|
+
}
|
|
1778
|
+
catch (error) {
|
|
1779
|
+
const { handlerResultFromError } = await import('../errors.js');
|
|
1780
|
+
return handlerResultFromError(error);
|
|
1781
|
+
}
|
|
1782
|
+
const { url } = server;
|
|
1783
|
+
console.log(`Dashboard: ${url}`);
|
|
1784
|
+
if (readOnly)
|
|
1785
|
+
console.log(' (read-only mode)');
|
|
1786
|
+
console.log('Press Ctrl+C to stop.');
|
|
1787
|
+
// Auto-open browser
|
|
1788
|
+
if (open) {
|
|
1789
|
+
try {
|
|
1790
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1791
|
+
const openMod = await (new Function('m', 'return import(m)'))('open');
|
|
1792
|
+
const openBrowser = openMod.default ?? openMod;
|
|
1793
|
+
await openBrowser(url);
|
|
1794
|
+
}
|
|
1795
|
+
catch {
|
|
1796
|
+
// open is optional — not a fatal error
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
// Keep process alive; shut down cleanly on SIGINT/SIGTERM.
|
|
1800
|
+
//
|
|
1801
|
+
// Registered listeners are removed on the resolve path so that the only
|
|
1802
|
+
// handler we install does not outlive this command (important for tests
|
|
1803
|
+
// or future in-process CLI flows). A 5-second hard deadline after signal
|
|
1804
|
+
// receipt forces process.exit() in case server.close() hangs draining
|
|
1805
|
+
// websocket connections.
|
|
1806
|
+
await new Promise((resolve) => {
|
|
1807
|
+
let settled = false;
|
|
1808
|
+
const shutdown = () => {
|
|
1809
|
+
if (settled)
|
|
1810
|
+
return;
|
|
1811
|
+
settled = true;
|
|
1812
|
+
// Immediately remove both handlers so a second signal doesn't
|
|
1813
|
+
// re-enter shutdown().
|
|
1814
|
+
process.removeListener('SIGINT', shutdown);
|
|
1815
|
+
process.removeListener('SIGTERM', shutdown);
|
|
1816
|
+
try {
|
|
1817
|
+
server.close();
|
|
1818
|
+
}
|
|
1819
|
+
catch {
|
|
1820
|
+
// Server may already be closing; safe to ignore.
|
|
1821
|
+
}
|
|
1822
|
+
// Hard deadline: if server.close() doesn't drain in 5s, force exit.
|
|
1823
|
+
const forceExit = setTimeout(() => {
|
|
1824
|
+
process.exit(143); // SIGTERM convention
|
|
1825
|
+
}, 5_000);
|
|
1826
|
+
forceExit.unref?.();
|
|
1827
|
+
resolve();
|
|
1828
|
+
};
|
|
1829
|
+
process.once('SIGINT', shutdown);
|
|
1830
|
+
process.once('SIGTERM', shutdown);
|
|
1831
|
+
});
|
|
1832
|
+
return { exitCode: 0 };
|
|
1833
|
+
},
|
|
1834
|
+
};
|
|
1835
|
+
//# sourceMappingURL=serve.js.map
|