create-windy 0.3.2 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -9
- package/dist/cli.js +825 -117
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +4 -0
- package/template/README.md +18 -11
- package/template/_gitignore +1 -0
- package/template/apps/agent-server/README.md +2 -0
- package/template/apps/agent-server/pyproject.toml +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
- package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
- package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
- package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
- package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
- package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
- package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
- package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
- package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
- package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
- package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
- package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
- package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
- package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
- package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
- package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
- package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
- package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
- package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
- package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
- package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
- package/template/apps/agent-server/tests/test_api.py +24 -1
- package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
- package/template/apps/agent-server/tests/test_config.py +4 -0
- package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
- package/template/apps/agent-server/tests/test_multimodal.py +51 -0
- package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
- package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
- package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
- package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
- package/template/apps/agent-server/tests/test_telemetry.py +25 -0
- package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
- package/template/apps/agent-server/uv.lock +165 -0
- package/template/apps/document-server/Dockerfile +19 -0
- package/template/apps/document-server/pyproject.toml +43 -0
- package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
- package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
- package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
- package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
- package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
- package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
- package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
- package/template/apps/document-server/tests/__init__.py +1 -0
- package/template/apps/document-server/tests/test_config.py +42 -0
- package/template/apps/document-server/tests/test_service.py +140 -0
- package/template/apps/document-server/uv.lock +430 -0
- package/template/apps/embedding-server/Dockerfile +17 -0
- package/template/apps/embedding-server/pyproject.toml +44 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
- package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
- package/template/apps/embedding-server/tests/__init__.py +1 -0
- package/template/apps/embedding-server/tests/test_adapter.py +92 -0
- package/template/apps/embedding-server/tests/test_config.py +38 -0
- package/template/apps/embedding-server/uv.lock +449 -0
- package/template/apps/server/src/agent/background-operation-port.ts +37 -0
- package/template/apps/server/src/agent/background-operation.test.ts +231 -0
- package/template/apps/server/src/agent/background-operation.ts +252 -0
- package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
- package/template/apps/server/src/agent/execution-grants.ts +7 -1
- package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
- package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
- package/template/apps/server/src/agent/idempotency-key.ts +11 -0
- package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
- package/template/apps/server/src/agent/remote-runtime.ts +173 -5
- package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
- package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
- package/template/apps/server/src/agent/run-authorization.ts +180 -0
- package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
- package/template/apps/server/src/agent/run-contracts.ts +88 -1
- package/template/apps/server/src/agent/run-definition-support.ts +58 -0
- package/template/apps/server/src/agent/run-errors.ts +25 -0
- package/template/apps/server/src/agent/run-facade.test.ts +57 -46
- package/template/apps/server/src/agent/run-facade.ts +185 -65
- package/template/apps/server/src/agent/run-release.ts +78 -0
- package/template/apps/server/src/agent/run-route-input.ts +169 -0
- package/template/apps/server/src/agent/run-routes.ts +63 -76
- package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
- package/template/apps/server/src/agent/sandbox-client.ts +200 -0
- package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
- package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
- package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
- package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
- package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
- package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
- package/template/apps/server/src/agent/tool-approval.ts +115 -0
- package/template/apps/server/src/agent/tool-audit.ts +62 -0
- package/template/apps/server/src/agent/tool-host.test.ts +39 -11
- package/template/apps/server/src/agent/tool-host.ts +184 -17
- package/template/apps/server/src/agent/tool-registry.ts +52 -47
- package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
- package/template/apps/server/src/agent/vision-input.ts +120 -0
- package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
- package/template/apps/server/src/ai-artifact/codec.ts +137 -0
- package/template/apps/server/src/ai-artifact/routes.ts +37 -0
- package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
- package/template/apps/server/src/application-services.ts +62 -2
- package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
- package/template/apps/server/src/auth/reauthentication.ts +19 -0
- package/template/apps/server/src/auth/runtime.ts +9 -1
- package/template/apps/server/src/data-access/context.ts +6 -4
- package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
- package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
- package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
- package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
- package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
- package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
- package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
- package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
- package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
- package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
- package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
- package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
- package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
- package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
- package/template/apps/server/src/data-governance/export/routes.ts +11 -1
- package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
- package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
- package/template/apps/server/src/data-governance/export/service.ts +90 -113
- package/template/apps/server/src/data-governance/export/source.ts +101 -0
- package/template/apps/server/src/data-governance/export/types.ts +27 -0
- package/template/apps/server/src/document/contracts.ts +76 -0
- package/template/apps/server/src/document/facade.ts +339 -0
- package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
- package/template/apps/server/src/document/remote-runtime.ts +223 -0
- package/template/apps/server/src/document/routes.ts +193 -0
- package/template/apps/server/src/embedding/contracts.ts +83 -0
- package/template/apps/server/src/embedding/error.ts +10 -0
- package/template/apps/server/src/embedding/facade.ts +217 -0
- package/template/apps/server/src/embedding/input.ts +172 -0
- package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
- package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
- package/template/apps/server/src/embedding/routes.ts +152 -0
- package/template/apps/server/src/index.ts +40 -43
- package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
- package/template/apps/server/src/jobs/artifact-service.ts +10 -0
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
- package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
- package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
- package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +17 -0
- package/template/apps/server/src/jobs/execution.test.ts +34 -0
- package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
- package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
- package/template/apps/server/src/jobs/repository.ts +3 -0
- package/template/apps/server/src/jobs/worker.ts +80 -41
- package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
- package/template/apps/server/src/module-host/durable-context.ts +124 -0
- package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
- package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
- package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
- package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
- package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
- package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
- package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
- package/template/apps/server/src/module-host/governed-resources.ts +203 -0
- package/template/apps/server/src/module-host/http-runtime.ts +95 -0
- package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
- package/template/apps/server/src/module-host/initialize.test.ts +49 -1
- package/template/apps/server/src/module-host/initialize.ts +39 -16
- package/template/apps/server/src/module-host/job-port.test.ts +262 -0
- package/template/apps/server/src/module-host/job-port.ts +334 -0
- package/template/apps/server/src/module-host/request-context.test.ts +88 -2
- package/template/apps/server/src/module-host/request-context.ts +56 -22
- package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
- package/template/apps/server/src/module-host/route-installer.ts +36 -1
- package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
- package/template/apps/server/src/module-host.ts +1 -0
- package/template/apps/server/src/module-runtime-validation.ts +7 -3
- package/template/apps/server/src/speech/contracts.ts +76 -0
- package/template/apps/server/src/speech/facade.ts +316 -0
- package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
- package/template/apps/server/src/speech/remote-runtime.ts +227 -0
- package/template/apps/server/src/speech/routes.ts +180 -0
- package/template/apps/server/src/system/actor.ts +39 -0
- package/template/apps/server/src/system/route-helpers.ts +1 -0
- package/template/apps/server/src/workflow/service.ts +2 -1
- package/template/apps/speech-server/Dockerfile +22 -0
- package/template/apps/speech-server/pyproject.toml +43 -0
- package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
- package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
- package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
- package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
- package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
- package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
- package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
- package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
- package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
- package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
- package/template/apps/speech-server/tests/__init__.py +1 -0
- package/template/apps/speech-server/tests/test_config.py +33 -0
- package/template/apps/speech-server/tests/test_service.py +154 -0
- package/template/apps/speech-server/uv.lock +430 -0
- package/template/apps/web/src/app/business-routes.ts +22 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
- package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
- package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
- package/template/apps/web/src/components/common/FormDialog.vue +6 -0
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
- package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
- package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
- package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
- package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
- package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
- package/template/apps/web/src/layout/AppShell.vue +28 -25
- package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
- package/template/apps/web/src/router/business-routes.ts +68 -0
- package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
- package/template/apps/web/src/router/index.ts +7 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/access-api.ts +5 -1
- package/template/apps/web/src/services/access-api.webtest.ts +35 -0
- package/template/docker-compose.yml +127 -0
- package/template/docs/architecture/ai-egress.md +12 -9
- package/template/docs/architecture/ai-runtime.md +23 -11
- package/template/docs/architecture/durable-jobs.md +25 -3
- package/template/docs/architecture/foundation-infrastructure.md +163 -0
- package/template/docs/architecture/module-host.md +115 -0
- package/template/docs/architecture/server-module-interface.md +39 -0
- package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
- package/template/docs/development/business-web-routes.md +66 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/development/module-lifecycle.md +71 -0
- package/template/docs/operations/observability.md +5 -1
- package/template/docs/platform/agent-runtime.md +21 -11
- package/template/docs/platform/agent-tool-sandbox.md +58 -0
- package/template/docs/platform/agent-tools.md +5 -0
- package/template/docs/platform/document-ai-runtime.md +204 -0
- package/template/docs/platform/governed-export-renderers.md +132 -0
- package/template/docs/platform/navigation-registry.md +5 -0
- package/template/docs/platform/organization-membership.md +2 -1
- package/template/docs/platform/speech-ai-runtime.md +156 -0
- package/template/docs/platform/system-crud-api.md +7 -1
- package/template/docs/platform/vision-ai-runtime.md +97 -0
- package/template/docs/platform/web-system-crud.md +6 -0
- package/template/package.json +16 -2
- package/template/packages/ai-client/index.ts +4 -0
- package/template/packages/ai-client/package.json +5 -2
- package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
- package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
- package/template/packages/ai-client/src/agent/client/client.ts +119 -7
- package/template/packages/ai-client/src/agent/client/run.ts +14 -0
- package/template/packages/ai-client/src/agent/client/types.ts +18 -0
- package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
- package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
- package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
- package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
- package/template/packages/ai-client/src/agent/index.ts +1 -0
- package/template/packages/ai-client/src/artifact-ref.ts +8 -0
- package/template/packages/ai-client/src/document/client.test.ts +114 -0
- package/template/packages/ai-client/src/document/client.ts +134 -0
- package/template/packages/ai-client/src/document/contracts.ts +152 -0
- package/template/packages/ai-client/src/document/error.ts +43 -0
- package/template/packages/ai-client/src/document/index.ts +3 -0
- package/template/packages/ai-client/src/document/run.ts +104 -0
- package/template/packages/ai-client/src/document/validation.ts +115 -0
- package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
- package/template/packages/ai-client/src/embedding/client.ts +96 -0
- package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
- package/template/packages/ai-client/src/embedding/error.ts +61 -0
- package/template/packages/ai-client/src/embedding/index.ts +3 -0
- package/template/packages/ai-client/src/embedding/validation.ts +117 -0
- package/template/packages/ai-client/src/speech/client.test.ts +135 -0
- package/template/packages/ai-client/src/speech/client.ts +104 -0
- package/template/packages/ai-client/src/speech/contracts.ts +129 -0
- package/template/packages/ai-client/src/speech/error.ts +43 -0
- package/template/packages/ai-client/src/speech/index.ts +3 -0
- package/template/packages/ai-client/src/speech/run.ts +133 -0
- package/template/packages/ai-client/src/speech/validation.ts +132 -0
- package/template/packages/config/package.json +2 -2
- package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
- package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
- package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
- package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
- package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
- package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
- package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
- package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +2 -2
- package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
- package/template/packages/database/src/schema/agent-runtime.ts +149 -0
- package/template/packages/database/src/schema/common.ts +2 -14
- package/template/packages/database/src/schema/durable-jobs.ts +5 -0
- package/template/packages/database/src/schema/governed-exports.ts +17 -5
- package/template/packages/database/src/schema/index.ts +3 -0
- package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
- package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
- package/template/packages/database/src/schema.test.ts +6 -23
- package/template/packages/jobs/package.json +3 -2
- package/template/packages/jobs/src/handlers.test.ts +13 -0
- package/template/packages/jobs/src/handlers.ts +7 -1
- package/template/packages/jobs/src/types.ts +4 -0
- package/template/packages/modules/package.json +2 -2
- package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
- package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
- package/template/packages/modules/src/ai-operation-composition.ts +59 -1
- package/template/packages/modules/src/manifest.ts +25 -1
- package/template/packages/modules/src/tool-composition.ts +18 -0
- package/template/packages/server-sdk/index.ts +50 -0
- package/template/packages/server-sdk/package.json +10 -4
- package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
- package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
- package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
- package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
- package/template/packages/server-sdk/src/durable-handler.ts +29 -0
- package/template/packages/server-sdk/src/jobs-port.ts +69 -0
- package/template/packages/server-sdk/src/module-host.ts +16 -2
- package/template/packages/server-sdk/src/request-context.ts +6 -2
- package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
- package/template/packages/server-sdk/src/tool-registration.ts +2 -0
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/rbac.ts +30 -0
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
|
-
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
3
2
|
import type { RequestGuardContext } from "../guards.js";
|
|
4
3
|
import type { RouteApp } from "../system/routes.js";
|
|
5
4
|
import type { AgentRunFacade } from "./run-facade.js";
|
|
6
|
-
import { AgentRunFacadeError } from "./run-
|
|
5
|
+
import { AgentRunFacadeError } from "./run-errors.js";
|
|
7
6
|
import type { AgentRuntimePort } from "./run-contracts.js";
|
|
8
7
|
import {
|
|
9
8
|
AgentToolHostError,
|
|
10
9
|
attachExecutionGrant,
|
|
11
10
|
type RestrictedAgentToolHost,
|
|
12
11
|
} from "./tool-host.js";
|
|
12
|
+
import {
|
|
13
|
+
AgentToolApprovalError,
|
|
14
|
+
AgentToolApprovalPending,
|
|
15
|
+
} from "./tool-approval.js";
|
|
16
|
+
import {
|
|
17
|
+
parseAttachRequest,
|
|
18
|
+
parseCursor,
|
|
19
|
+
parseOperationKey,
|
|
20
|
+
parseStartRequest,
|
|
21
|
+
parseToolRequest,
|
|
22
|
+
} from "./run-route-input.js";
|
|
13
23
|
|
|
14
24
|
interface RouteContext {
|
|
15
25
|
body?: unknown;
|
|
@@ -39,6 +49,8 @@ export function registerAgentRunRoutes(
|
|
|
39
49
|
request.input,
|
|
40
50
|
idempotencyKey,
|
|
41
51
|
context.guardContext,
|
|
52
|
+
request.artifacts,
|
|
53
|
+
context.request.signal,
|
|
42
54
|
);
|
|
43
55
|
} catch (error) {
|
|
44
56
|
return facadeError(error);
|
|
@@ -49,7 +61,8 @@ export function registerAgentRunRoutes(
|
|
|
49
61
|
const context = raw as RouteContext;
|
|
50
62
|
try {
|
|
51
63
|
const runId = required(context.params.runId);
|
|
52
|
-
|
|
64
|
+
const operationKey = parseOperationKey(context.query.operationKey);
|
|
65
|
+
await facade.authorizeRun(operationKey, runId, context.guardContext);
|
|
53
66
|
const cursor = parseCursor(
|
|
54
67
|
context.query.after,
|
|
55
68
|
context.request.headers.get("last-event-id"),
|
|
@@ -64,7 +77,8 @@ export function registerAgentRunRoutes(
|
|
|
64
77
|
const context = raw as RouteContext;
|
|
65
78
|
try {
|
|
66
79
|
const runId = required(context.params.runId);
|
|
67
|
-
|
|
80
|
+
const operationKey = parseOperationKey(context.query.operationKey);
|
|
81
|
+
await facade.authorizeRun(operationKey, runId, context.guardContext);
|
|
68
82
|
return await remote.result(runId, context.request.signal);
|
|
69
83
|
} catch (error) {
|
|
70
84
|
return facadeError(error);
|
|
@@ -75,7 +89,8 @@ export function registerAgentRunRoutes(
|
|
|
75
89
|
const context = raw as RouteContext;
|
|
76
90
|
try {
|
|
77
91
|
const runId = required(context.params.runId);
|
|
78
|
-
|
|
92
|
+
const operationKey = parseOperationKey(context.query.operationKey);
|
|
93
|
+
await facade.authorizeRun(operationKey, runId, context.guardContext);
|
|
79
94
|
const reason =
|
|
80
95
|
isRecord(context.body) && typeof context.body.reason === "string"
|
|
81
96
|
? context.body.reason.slice(0, 200)
|
|
@@ -86,6 +101,23 @@ export function registerAgentRunRoutes(
|
|
|
86
101
|
}
|
|
87
102
|
});
|
|
88
103
|
|
|
104
|
+
app.post("/agent/runs/:runId/attach", async (raw) => {
|
|
105
|
+
const context = raw as RouteContext;
|
|
106
|
+
try {
|
|
107
|
+
const runId = required(context.params.runId);
|
|
108
|
+
const request = parseAttachRequest(context.body);
|
|
109
|
+
return await facade.attach(
|
|
110
|
+
request.operationKey,
|
|
111
|
+
runId,
|
|
112
|
+
request.cursor,
|
|
113
|
+
context.guardContext,
|
|
114
|
+
context.request.signal,
|
|
115
|
+
);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return facadeError(error);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
89
121
|
app.post("/internal/agent/tools/execute", async (raw) => {
|
|
90
122
|
const context = raw as RouteContext;
|
|
91
123
|
if (!validInternalToken(context.request, internalToken)) {
|
|
@@ -97,6 +129,28 @@ export function registerAgentRunRoutes(
|
|
|
97
129
|
attachExecutionGrant(parsed.invocation, parsed.executionGrant),
|
|
98
130
|
);
|
|
99
131
|
} catch (error) {
|
|
132
|
+
if (error instanceof AgentToolApprovalPending) {
|
|
133
|
+
return Response.json(
|
|
134
|
+
{
|
|
135
|
+
code: "APPROVAL_REQUIRED",
|
|
136
|
+
message: error.message,
|
|
137
|
+
workItemId: error.workItemId,
|
|
138
|
+
retryable: true,
|
|
139
|
+
},
|
|
140
|
+
{ status: 202 },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (error instanceof AgentToolApprovalError) {
|
|
144
|
+
return Response.json(
|
|
145
|
+
{
|
|
146
|
+
code: error.code,
|
|
147
|
+
message: error.message,
|
|
148
|
+
workItemId: error.workItemId,
|
|
149
|
+
retryable: false,
|
|
150
|
+
},
|
|
151
|
+
{ status: error.code === "APPROVAL_UNAVAILABLE" ? 503 : 409 },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
100
154
|
if (error instanceof AgentToolHostError) {
|
|
101
155
|
return errorResponse(error.code, error.message, 403);
|
|
102
156
|
}
|
|
@@ -107,69 +161,10 @@ export function registerAgentRunRoutes(
|
|
|
107
161
|
return app;
|
|
108
162
|
}
|
|
109
163
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
!isRecord(body) ||
|
|
116
|
-
Object.keys(body).some((key) => !["operationKey", "input"].includes(key))
|
|
117
|
-
) {
|
|
118
|
-
throw new AgentRunFacadeError("INVALID_INPUT", "请求格式无效", 400);
|
|
119
|
-
}
|
|
120
|
-
if (typeof body.operationKey !== "string" || !body.operationKey.trim()) {
|
|
121
|
-
throw new AgentRunFacadeError("INVALID_INPUT", "Operation Key 无效", 400);
|
|
122
|
-
}
|
|
123
|
-
assertJson(body.input);
|
|
124
|
-
return { operationKey: body.operationKey, input: body.input };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function parseToolRequest(body: unknown) {
|
|
128
|
-
const allowed = [
|
|
129
|
-
"executionGrant",
|
|
130
|
-
"runId",
|
|
131
|
-
"toolCallId",
|
|
132
|
-
"toolKey",
|
|
133
|
-
"arguments",
|
|
134
|
-
];
|
|
135
|
-
if (
|
|
136
|
-
!isRecord(body) ||
|
|
137
|
-
Object.keys(body).some((key) => !allowed.includes(key))
|
|
138
|
-
) {
|
|
139
|
-
throw new Error("Tool 请求格式无效");
|
|
140
|
-
}
|
|
141
|
-
if (
|
|
142
|
-
typeof body.executionGrant !== "string" ||
|
|
143
|
-
typeof body.runId !== "string" ||
|
|
144
|
-
typeof body.toolCallId !== "string" ||
|
|
145
|
-
typeof body.toolKey !== "string" ||
|
|
146
|
-
!isRecord(body.arguments)
|
|
147
|
-
) {
|
|
148
|
-
throw new Error("Tool 请求字段无效");
|
|
149
|
-
}
|
|
150
|
-
assertJson(body.arguments);
|
|
151
|
-
return {
|
|
152
|
-
executionGrant: body.executionGrant,
|
|
153
|
-
invocation: {
|
|
154
|
-
runId: body.runId,
|
|
155
|
-
toolCallId: body.toolCallId,
|
|
156
|
-
toolKey: body.toolKey,
|
|
157
|
-
arguments: body.arguments,
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function parseCursor(after: string | undefined, header: string | null): number {
|
|
163
|
-
const values = [after, header].filter(
|
|
164
|
-
(value): value is string => value !== undefined && value !== null,
|
|
165
|
-
);
|
|
166
|
-
if (values.some((value) => !/^\d+$/.test(value))) {
|
|
167
|
-
throw new AgentRunFacadeError("INVALID_INPUT", "事件游标无效", 400);
|
|
168
|
-
}
|
|
169
|
-
return Math.max(0, ...values.map(Number));
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function validInternalToken(request: Request, expected: string): boolean {
|
|
164
|
+
export function validInternalToken(
|
|
165
|
+
request: Request,
|
|
166
|
+
expected: string,
|
|
167
|
+
): boolean {
|
|
173
168
|
const provided = request.headers
|
|
174
169
|
.get("authorization")
|
|
175
170
|
?.replace(/^Bearer /, "");
|
|
@@ -212,11 +207,3 @@ function required(value: string | undefined): string {
|
|
|
212
207
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
213
208
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
214
209
|
}
|
|
215
|
-
|
|
216
|
-
function assertJson(value: unknown): asserts value is ModuleAiJsonValue {
|
|
217
|
-
try {
|
|
218
|
-
if (JSON.stringify(value) === undefined) throw new Error();
|
|
219
|
-
} catch {
|
|
220
|
-
throw new AgentRunFacadeError("INVALID_INPUT", "请求必须是 JSON", 400);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
@@ -3,12 +3,49 @@ import type { ModuleManifest } from "@southwind-ai/modules";
|
|
|
3
3
|
import type { InitializedModuleHosts } from "../module-host.js";
|
|
4
4
|
import type { createServerRuntime } from "../runtime.js";
|
|
5
5
|
import type { RouteApp } from "../system/routes.js";
|
|
6
|
+
import type { createStorageRuntime } from "../storage/runtime.js";
|
|
7
|
+
import type { SystemRepositories } from "../system/seed.js";
|
|
8
|
+
import { resolveCurrentSystemActor } from "../system/actor.js";
|
|
9
|
+
import { AiArtifactCodec } from "../ai-artifact/codec.js";
|
|
10
|
+
import { registerAiArtifactRoutes } from "../ai-artifact/routes.js";
|
|
11
|
+
import { AiArtifactRuntime } from "../ai-artifact/runtime.js";
|
|
12
|
+
import { DocumentRunFacade } from "../document/facade.js";
|
|
13
|
+
import {
|
|
14
|
+
RemoteDocumentRuntime,
|
|
15
|
+
unavailableDocumentRuntime,
|
|
16
|
+
} from "../document/remote-runtime.js";
|
|
17
|
+
import { registerDocumentRunRoutes } from "../document/routes.js";
|
|
18
|
+
import { EmbeddingFacade } from "../embedding/facade.js";
|
|
19
|
+
import {
|
|
20
|
+
RemoteEmbeddingRuntime,
|
|
21
|
+
unavailableEmbeddingRuntime,
|
|
22
|
+
} from "../embedding/remote-runtime.js";
|
|
23
|
+
import { registerEmbeddingRoutes } from "../embedding/routes.js";
|
|
24
|
+
import { SpeechRunFacade } from "../speech/facade.js";
|
|
25
|
+
import {
|
|
26
|
+
RemoteSpeechRuntime,
|
|
27
|
+
unavailableSpeechRuntime,
|
|
28
|
+
} from "../speech/remote-runtime.js";
|
|
29
|
+
import { registerSpeechRunRoutes } from "../speech/routes.js";
|
|
6
30
|
import { ExecutionGrantStore } from "./execution-grants.js";
|
|
7
31
|
import { RemoteAgentRuntime } from "./remote-runtime.js";
|
|
8
|
-
import
|
|
32
|
+
import { GovernedToolRegistry } from "./tool-registry.js";
|
|
9
33
|
import { AgentRunFacade } from "./run-facade.js";
|
|
10
34
|
import { registerAgentRunRoutes } from "./run-routes.js";
|
|
11
35
|
import { RestrictedAgentToolHost } from "./tool-host.js";
|
|
36
|
+
import { createSystemToolHandlers, registerAgentRoutes } from "./routes.js";
|
|
37
|
+
import { createModuleToolHandlerMap } from "../module-host.js";
|
|
38
|
+
import { PlatformMcpAdapter } from "../mcp/adapter.js";
|
|
39
|
+
import { registerMcpRoutes } from "../mcp/routes.js";
|
|
40
|
+
import type { WorkflowService } from "../workflow/service.js";
|
|
41
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
42
|
+
import { DrizzleAgentRunBindingRepository } from "./drizzle-run-binding-repository.js";
|
|
43
|
+
import { AgentRunAuthorizationStore } from "./run-authorization.js";
|
|
44
|
+
import { InMemoryAgentRunBindingRepository } from "./run-binding-repository.js";
|
|
45
|
+
import { AgentBackgroundOperationExecutor } from "./background-operation.js";
|
|
46
|
+
import type { DeferredAgentBackgroundOperationPortFactory } from "./background-operation-port.js";
|
|
47
|
+
import { UnixSandboxToolClient } from "./sandbox-client.js";
|
|
48
|
+
import { createModuleAiOperationPort } from "../module-host/ai-operation-port.js";
|
|
12
49
|
|
|
13
50
|
export function registerAgentPlatformRuntime(input: {
|
|
14
51
|
app: RouteApp;
|
|
@@ -16,34 +53,153 @@ export function registerAgentPlatformRuntime(input: {
|
|
|
16
53
|
modules: readonly ModuleManifest[];
|
|
17
54
|
hosts: InitializedModuleHosts;
|
|
18
55
|
runtime: ReturnType<typeof createServerRuntime>;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
systemRepositories: SystemRepositories;
|
|
57
|
+
storage: ReturnType<typeof createStorageRuntime>;
|
|
58
|
+
workflow: WorkflowService;
|
|
59
|
+
db?: NodePgDatabase;
|
|
60
|
+
backgroundAi: DeferredAgentBackgroundOperationPortFactory;
|
|
61
|
+
}) {
|
|
62
|
+
const features = input.modules.flatMap(({ features }) => features);
|
|
63
|
+
const toolHandlers = new Map([
|
|
64
|
+
...createSystemToolHandlers(input.systemRepositories, input.runtime),
|
|
65
|
+
...createModuleToolHandlerMap(input.hosts, {
|
|
66
|
+
runtime: input.runtime,
|
|
67
|
+
features,
|
|
68
|
+
storage: input.storage,
|
|
69
|
+
sandbox: process.env.TOOL_SANDBOX_SOCKET?.trim()
|
|
70
|
+
? new UnixSandboxToolClient(process.env.TOOL_SANDBOX_SOCKET)
|
|
71
|
+
: undefined,
|
|
72
|
+
}),
|
|
73
|
+
]);
|
|
74
|
+
const toolRegistry = new GovernedToolRegistry(
|
|
75
|
+
input.modules,
|
|
76
|
+
toolHandlers,
|
|
77
|
+
input.runtime,
|
|
78
|
+
);
|
|
79
|
+
registerAgentRoutes(input.app, toolRegistry);
|
|
80
|
+
registerMcpRoutes(
|
|
81
|
+
input.app,
|
|
82
|
+
new PlatformMcpAdapter(toolRegistry, input.runtime, features),
|
|
83
|
+
);
|
|
84
|
+
if (!input.platformConfig.enableAI) {
|
|
85
|
+
return {
|
|
86
|
+
toolHandlers,
|
|
87
|
+
aiArtifacts: undefined,
|
|
88
|
+
aiOperations: undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
22
91
|
const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
|
|
92
|
+
const aiArtifacts = new AiArtifactRuntime(
|
|
93
|
+
new AiArtifactCodec(requiredEnvironment("AI_ARTIFACT_ENCRYPTION_SECRET")),
|
|
94
|
+
input.hosts,
|
|
95
|
+
input.runtime,
|
|
96
|
+
features,
|
|
97
|
+
input.storage,
|
|
98
|
+
(actorId) => resolveCurrentSystemActor(input.systemRepositories, actorId),
|
|
99
|
+
);
|
|
100
|
+
registerAiArtifactRoutes(input.app, aiArtifacts, internalToken);
|
|
23
101
|
const remote = new RemoteAgentRuntime({
|
|
24
102
|
baseUrl: process.env.AGENT_SERVER_URL || "http://agent-server:8080",
|
|
25
103
|
internalToken,
|
|
26
104
|
toolHostUrl: process.env.AGENT_TOOL_HOST_URL || "http://server:9747",
|
|
27
105
|
});
|
|
28
106
|
const grants = new ExecutionGrantStore();
|
|
107
|
+
const runAuthorizations = new AgentRunAuthorizationStore(
|
|
108
|
+
input.db
|
|
109
|
+
? new DrizzleAgentRunBindingRepository(input.db)
|
|
110
|
+
: new InMemoryAgentRunBindingRepository(),
|
|
111
|
+
internalToken,
|
|
112
|
+
);
|
|
113
|
+
const documentRemote = process.env.DOCUMENT_SERVER_URL?.trim()
|
|
114
|
+
? new RemoteDocumentRuntime({
|
|
115
|
+
baseUrl: process.env.DOCUMENT_SERVER_URL,
|
|
116
|
+
internalToken,
|
|
117
|
+
})
|
|
118
|
+
: unavailableDocumentRuntime;
|
|
119
|
+
const documentFacade = new DocumentRunFacade(
|
|
120
|
+
input.modules,
|
|
121
|
+
input.hosts,
|
|
122
|
+
input.runtime,
|
|
123
|
+
documentRemote,
|
|
124
|
+
aiArtifacts,
|
|
125
|
+
grants,
|
|
126
|
+
);
|
|
127
|
+
registerDocumentRunRoutes(input.app, documentFacade, documentRemote);
|
|
128
|
+
const speechRemote = process.env.SPEECH_SERVER_URL?.trim()
|
|
129
|
+
? new RemoteSpeechRuntime({
|
|
130
|
+
baseUrl: process.env.SPEECH_SERVER_URL,
|
|
131
|
+
internalToken,
|
|
132
|
+
})
|
|
133
|
+
: unavailableSpeechRuntime;
|
|
134
|
+
const speechFacade = new SpeechRunFacade(
|
|
135
|
+
input.modules,
|
|
136
|
+
input.hosts,
|
|
137
|
+
input.runtime,
|
|
138
|
+
speechRemote,
|
|
139
|
+
aiArtifacts,
|
|
140
|
+
grants,
|
|
141
|
+
);
|
|
142
|
+
registerSpeechRunRoutes(input.app, speechFacade, speechRemote);
|
|
143
|
+
const embeddingRemote = process.env.EMBEDDING_SERVER_URL?.trim()
|
|
144
|
+
? new RemoteEmbeddingRuntime({
|
|
145
|
+
baseUrl: process.env.EMBEDDING_SERVER_URL,
|
|
146
|
+
internalToken,
|
|
147
|
+
})
|
|
148
|
+
: unavailableEmbeddingRuntime;
|
|
149
|
+
const embeddingFacade = new EmbeddingFacade(
|
|
150
|
+
input.modules,
|
|
151
|
+
input.hosts,
|
|
152
|
+
input.runtime,
|
|
153
|
+
embeddingRemote,
|
|
154
|
+
aiArtifacts,
|
|
155
|
+
);
|
|
156
|
+
registerEmbeddingRoutes(input.app, embeddingFacade);
|
|
157
|
+
const agentFacade = new AgentRunFacade(
|
|
158
|
+
input.modules,
|
|
159
|
+
input.hosts,
|
|
160
|
+
input.runtime,
|
|
161
|
+
remote,
|
|
162
|
+
runAuthorizations,
|
|
163
|
+
aiArtifacts,
|
|
164
|
+
);
|
|
29
165
|
registerAgentRunRoutes(
|
|
30
166
|
input.app,
|
|
31
|
-
|
|
32
|
-
input.modules,
|
|
33
|
-
input.hosts,
|
|
34
|
-
input.runtime,
|
|
35
|
-
remote,
|
|
36
|
-
grants,
|
|
37
|
-
),
|
|
167
|
+
agentFacade,
|
|
38
168
|
remote,
|
|
39
169
|
new RestrictedAgentToolHost(
|
|
40
170
|
input.modules,
|
|
41
|
-
input.
|
|
171
|
+
input.hosts,
|
|
172
|
+
toolRegistry,
|
|
42
173
|
input.runtime,
|
|
43
|
-
|
|
174
|
+
runAuthorizations,
|
|
175
|
+
input.workflow,
|
|
176
|
+
(actorId) => resolveCurrentSystemActor(input.systemRepositories, actorId),
|
|
44
177
|
),
|
|
45
178
|
internalToken,
|
|
46
179
|
);
|
|
180
|
+
input.backgroundAi.bind(
|
|
181
|
+
new AgentBackgroundOperationExecutor(
|
|
182
|
+
input.modules,
|
|
183
|
+
input.hosts,
|
|
184
|
+
agentFacade,
|
|
185
|
+
remote,
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
const aiOperations = (
|
|
189
|
+
moduleName: string,
|
|
190
|
+
guardContext: Parameters<typeof createModuleAiOperationPort>[1],
|
|
191
|
+
signal: AbortSignal,
|
|
192
|
+
) =>
|
|
193
|
+
createModuleAiOperationPort(moduleName, guardContext, signal, {
|
|
194
|
+
agentFacade,
|
|
195
|
+
agentRemote: remote,
|
|
196
|
+
documentFacade,
|
|
197
|
+
documentRemote,
|
|
198
|
+
speechFacade,
|
|
199
|
+
speechRemote,
|
|
200
|
+
embeddingFacade,
|
|
201
|
+
});
|
|
202
|
+
return { toolHandlers, aiArtifacts, aiOperations };
|
|
47
203
|
}
|
|
48
204
|
|
|
49
205
|
function requiredEnvironment(key: string): string {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import type {
|
|
4
|
+
ModuleToolInput,
|
|
5
|
+
ModuleToolResult,
|
|
6
|
+
} from "@southwind-ai/server-sdk";
|
|
7
|
+
import {
|
|
8
|
+
type SandboxToolRequest,
|
|
9
|
+
type SandboxToolResponse,
|
|
10
|
+
signSandboxRequest,
|
|
11
|
+
} from "./sandbox-protocol.js";
|
|
12
|
+
|
|
13
|
+
const maxResponseBytes = 2 * 1_024 * 1_024;
|
|
14
|
+
|
|
15
|
+
export interface SandboxToolExecutor {
|
|
16
|
+
execute(
|
|
17
|
+
toolKey: string,
|
|
18
|
+
input: ModuleToolInput,
|
|
19
|
+
idempotencyKey: string,
|
|
20
|
+
signal: AbortSignal,
|
|
21
|
+
): Promise<ModuleToolResult>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class UnixSandboxToolClient implements SandboxToolExecutor {
|
|
25
|
+
constructor(private readonly socketPath: string) {
|
|
26
|
+
if (
|
|
27
|
+
!socketPath.startsWith("/") ||
|
|
28
|
+
socketPath.includes("..") ||
|
|
29
|
+
socketPath.length > 180
|
|
30
|
+
) {
|
|
31
|
+
throw new Error("Tool Sandbox Socket 路径无效");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async execute(
|
|
36
|
+
toolKey: string,
|
|
37
|
+
input: ModuleToolInput,
|
|
38
|
+
idempotencyKey: string,
|
|
39
|
+
signal: AbortSignal,
|
|
40
|
+
): Promise<ModuleToolResult> {
|
|
41
|
+
signal.throwIfAborted();
|
|
42
|
+
const invocationId = crypto.randomUUID();
|
|
43
|
+
let lastError: unknown;
|
|
44
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
45
|
+
try {
|
|
46
|
+
return await this.attempt(
|
|
47
|
+
{ invocationId, toolKey, idempotencyKey, input },
|
|
48
|
+
signal,
|
|
49
|
+
);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
lastError = error;
|
|
52
|
+
if (
|
|
53
|
+
signal.aborted ||
|
|
54
|
+
(error instanceof SandboxToolClientError &&
|
|
55
|
+
!error.retryableConnection)
|
|
56
|
+
) {
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
throw asClientError(lastError);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async attempt(
|
|
65
|
+
input: Omit<SandboxToolRequest, "deadline" | "grant">,
|
|
66
|
+
signal: AbortSignal,
|
|
67
|
+
) {
|
|
68
|
+
const key = await readFile(`${this.socketPath}.key`);
|
|
69
|
+
if (key.byteLength !== 32) {
|
|
70
|
+
throw new SandboxToolClientError(
|
|
71
|
+
"SANDBOX_AUTH_INVALID",
|
|
72
|
+
"Tool Sandbox 授权材料无效",
|
|
73
|
+
true,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const unsigned = {
|
|
77
|
+
...input,
|
|
78
|
+
deadline: Date.now() + 5 * 60_000,
|
|
79
|
+
};
|
|
80
|
+
const request: SandboxToolRequest = {
|
|
81
|
+
...unsigned,
|
|
82
|
+
grant: signSandboxRequest(unsigned, key),
|
|
83
|
+
};
|
|
84
|
+
const response = await exchange(this.socketPath, request, signal);
|
|
85
|
+
if (response.invocationId !== input.invocationId) {
|
|
86
|
+
throw new SandboxToolClientError(
|
|
87
|
+
"SANDBOX_AUTH_INVALID",
|
|
88
|
+
"Tool Sandbox 响应标识无效",
|
|
89
|
+
true,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (!response.ok) {
|
|
93
|
+
throw new SandboxToolClientError(
|
|
94
|
+
response.code,
|
|
95
|
+
response.message,
|
|
96
|
+
response.code === "SANDBOX_AUTH_INVALID",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return response.result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class SandboxToolClientError extends Error {
|
|
104
|
+
constructor(
|
|
105
|
+
readonly code:
|
|
106
|
+
| "SANDBOX_UNAVAILABLE"
|
|
107
|
+
| "SANDBOX_AUTH_INVALID"
|
|
108
|
+
| "SANDBOX_POLICY_DENIED"
|
|
109
|
+
| "SANDBOX_INPUT_INVALID"
|
|
110
|
+
| "SANDBOX_HANDLER_FAILED"
|
|
111
|
+
| "SANDBOX_TIMEOUT",
|
|
112
|
+
message: string,
|
|
113
|
+
readonly retryableConnection: boolean,
|
|
114
|
+
) {
|
|
115
|
+
super(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function exchange(
|
|
120
|
+
socketPath: string,
|
|
121
|
+
request: SandboxToolRequest,
|
|
122
|
+
signal: AbortSignal,
|
|
123
|
+
): Promise<SandboxToolResponse> {
|
|
124
|
+
signal.throwIfAborted();
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const socket = createConnection(socketPath);
|
|
127
|
+
let body = "";
|
|
128
|
+
const fail = (error: unknown) => {
|
|
129
|
+
socket.destroy();
|
|
130
|
+
reject(asClientError(error));
|
|
131
|
+
};
|
|
132
|
+
const abort = () => fail(signal.reason);
|
|
133
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
134
|
+
socket.setEncoding("utf8");
|
|
135
|
+
socket.once("connect", () => socket.write(`${JSON.stringify(request)}\n`));
|
|
136
|
+
socket.on("data", (chunk: string) => {
|
|
137
|
+
body += chunk;
|
|
138
|
+
if (Buffer.byteLength(body) > maxResponseBytes) {
|
|
139
|
+
fail(new Error("Tool Sandbox 响应超过上限"));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const newline = body.indexOf("\n");
|
|
143
|
+
if (newline < 0) return;
|
|
144
|
+
try {
|
|
145
|
+
resolve(parseResponse(JSON.parse(body.slice(0, newline))));
|
|
146
|
+
socket.end();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
fail(error);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
socket.once("error", fail);
|
|
152
|
+
socket.once("close", () => {
|
|
153
|
+
signal.removeEventListener("abort", abort);
|
|
154
|
+
if (!body.includes("\n")) {
|
|
155
|
+
reject(
|
|
156
|
+
new SandboxToolClientError(
|
|
157
|
+
"SANDBOX_UNAVAILABLE",
|
|
158
|
+
"Tool Sandbox 连接中断",
|
|
159
|
+
true,
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseResponse(value: unknown): SandboxToolResponse {
|
|
168
|
+
if (
|
|
169
|
+
!isRecord(value) ||
|
|
170
|
+
typeof value.invocationId !== "string" ||
|
|
171
|
+
typeof value.ok !== "boolean"
|
|
172
|
+
) {
|
|
173
|
+
throw new Error("Tool Sandbox 响应无效");
|
|
174
|
+
}
|
|
175
|
+
if (value.ok && isRecord(value.result)) {
|
|
176
|
+
return value as unknown as SandboxToolResponse;
|
|
177
|
+
}
|
|
178
|
+
if (
|
|
179
|
+
!value.ok &&
|
|
180
|
+
typeof value.code === "string" &&
|
|
181
|
+
typeof value.message === "string" &&
|
|
182
|
+
typeof value.retryable === "boolean"
|
|
183
|
+
) {
|
|
184
|
+
return value as unknown as SandboxToolResponse;
|
|
185
|
+
}
|
|
186
|
+
throw new Error("Tool Sandbox 响应字段无效");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function asClientError(error: unknown) {
|
|
190
|
+
if (error instanceof SandboxToolClientError) return error;
|
|
191
|
+
return new SandboxToolClientError(
|
|
192
|
+
"SANDBOX_UNAVAILABLE",
|
|
193
|
+
"Tool Sandbox 暂不可用",
|
|
194
|
+
true,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
199
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
200
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
ModuleToolInput,
|
|
4
|
+
ModuleToolResult,
|
|
5
|
+
} from "@southwind-ai/server-sdk";
|
|
6
|
+
|
|
7
|
+
export interface SandboxToolRequest {
|
|
8
|
+
readonly invocationId: string;
|
|
9
|
+
readonly toolKey: string;
|
|
10
|
+
readonly idempotencyKey: string;
|
|
11
|
+
readonly deadline: number;
|
|
12
|
+
readonly input: ModuleToolInput;
|
|
13
|
+
readonly grant: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface SandboxToolSuccess {
|
|
17
|
+
readonly invocationId: string;
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly result: ModuleToolResult;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SandboxToolFailure {
|
|
23
|
+
readonly invocationId: string;
|
|
24
|
+
readonly ok: false;
|
|
25
|
+
readonly code:
|
|
26
|
+
| "SANDBOX_AUTH_INVALID"
|
|
27
|
+
| "SANDBOX_POLICY_DENIED"
|
|
28
|
+
| "SANDBOX_INPUT_INVALID"
|
|
29
|
+
| "SANDBOX_HANDLER_FAILED"
|
|
30
|
+
| "SANDBOX_TIMEOUT";
|
|
31
|
+
readonly message: string;
|
|
32
|
+
readonly retryable: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SandboxToolResponse = SandboxToolSuccess | SandboxToolFailure;
|
|
36
|
+
|
|
37
|
+
export function signSandboxRequest(
|
|
38
|
+
request: Omit<SandboxToolRequest, "grant">,
|
|
39
|
+
key: Uint8Array,
|
|
40
|
+
): string {
|
|
41
|
+
return createHmac("sha256", key).update(canonicalJson(request)).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validSandboxGrant(
|
|
45
|
+
request: SandboxToolRequest,
|
|
46
|
+
key: Uint8Array,
|
|
47
|
+
): boolean {
|
|
48
|
+
if (!/^[a-f0-9]{64}$/.test(request.grant)) return false;
|
|
49
|
+
const expected = signSandboxRequest(
|
|
50
|
+
{
|
|
51
|
+
invocationId: request.invocationId,
|
|
52
|
+
toolKey: request.toolKey,
|
|
53
|
+
idempotencyKey: request.idempotencyKey,
|
|
54
|
+
deadline: request.deadline,
|
|
55
|
+
input: request.input,
|
|
56
|
+
},
|
|
57
|
+
key,
|
|
58
|
+
);
|
|
59
|
+
return timingSafeEqual(Buffer.from(request.grant), Buffer.from(expected));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function canonicalJson(value: unknown): string {
|
|
63
|
+
return JSON.stringify(canonicalValue(value));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonicalValue(value: unknown): unknown {
|
|
67
|
+
if (Array.isArray(value)) return value.map(canonicalValue);
|
|
68
|
+
if (value !== null && typeof value === "object") {
|
|
69
|
+
return Object.fromEntries(
|
|
70
|
+
Object.entries(value as Record<string, unknown>)
|
|
71
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
72
|
+
.map(([key, item]) => [key, canonicalValue(item)]),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|