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,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
@@ -65,6 +66,10 @@ class ToolHostClient:
|
|
|
65
66
|
raise denied("Agent Tool Host 暂不可用", retryable=True) from None
|
|
66
67
|
if len(body) > self._max_response_bytes:
|
|
67
68
|
raise denied("Agent Tool 响应超过安全上限")
|
|
69
|
+
if response.status_code == 202:
|
|
70
|
+
raise approval_pending(body)
|
|
71
|
+
if response.status_code == 409:
|
|
72
|
+
raise approval_resolved(body)
|
|
68
73
|
if not response.is_success:
|
|
69
74
|
raise denied("Agent Tool 执行被拒绝")
|
|
70
75
|
try:
|
|
@@ -78,3 +83,70 @@ class ToolHostClient:
|
|
|
78
83
|
|
|
79
84
|
def denied(message: str, *, retryable: bool = False) -> SafeEngineError:
|
|
80
85
|
return SafeEngineError("TOOL_DENIED", message, retryable=retryable)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def canonical_tool_call_id(
|
|
89
|
+
run_id: str,
|
|
90
|
+
tool_key: str,
|
|
91
|
+
arguments: dict[str, JsonValue],
|
|
92
|
+
) -> str:
|
|
93
|
+
encoded = json.dumps(
|
|
94
|
+
{
|
|
95
|
+
"runId": run_id,
|
|
96
|
+
"toolKey": tool_key,
|
|
97
|
+
"arguments": arguments,
|
|
98
|
+
},
|
|
99
|
+
ensure_ascii=False,
|
|
100
|
+
separators=(",", ":"),
|
|
101
|
+
sort_keys=True,
|
|
102
|
+
).encode()
|
|
103
|
+
return f"tool_{hashlib.sha256(encoded).hexdigest()[:40]}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ToolApprovalPending(Exception):
|
|
107
|
+
def __init__(self, work_item_id: str) -> None:
|
|
108
|
+
super().__init__("Agent Tool 等待人工审批")
|
|
109
|
+
self.work_item_id = work_item_id
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class ToolApprovalResolved(Exception):
|
|
113
|
+
def __init__(self, work_item_id: str, outcome: str) -> None:
|
|
114
|
+
super().__init__("Agent Tool 审批未通过")
|
|
115
|
+
self.work_item_id = work_item_id
|
|
116
|
+
self.outcome = outcome
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def approval_pending(body: bytes) -> ToolApprovalPending:
|
|
120
|
+
value = safe_object(body)
|
|
121
|
+
if value.get("code") != "APPROVAL_REQUIRED" or not isinstance(
|
|
122
|
+
value.get("workItemId"), str
|
|
123
|
+
):
|
|
124
|
+
raise denied("Agent Tool 审批响应无效")
|
|
125
|
+
work_item_id = value["workItemId"]
|
|
126
|
+
if not isinstance(work_item_id, str):
|
|
127
|
+
raise denied("Agent Tool 审批响应无效")
|
|
128
|
+
return ToolApprovalPending(work_item_id)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def approval_resolved(body: bytes) -> ToolApprovalResolved:
|
|
132
|
+
value = safe_object(body)
|
|
133
|
+
code = value.get("code")
|
|
134
|
+
work_item_id = value.get("workItemId")
|
|
135
|
+
outcomes = {
|
|
136
|
+
"APPROVAL_REJECTED": "rejected",
|
|
137
|
+
"APPROVAL_CANCELLED": "cancelled",
|
|
138
|
+
"APPROVAL_TIMED_OUT": "timed-out",
|
|
139
|
+
}
|
|
140
|
+
if code not in outcomes or not isinstance(work_item_id, str):
|
|
141
|
+
raise denied("Agent Tool 审批响应无效")
|
|
142
|
+
return ToolApprovalResolved(work_item_id, outcomes[code])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def safe_object(body: bytes) -> dict[str, JsonValue]:
|
|
146
|
+
try:
|
|
147
|
+
value = json.loads(body)
|
|
148
|
+
except json.JSONDecodeError:
|
|
149
|
+
raise denied("Agent Tool 审批响应无效") from None
|
|
150
|
+
if not isinstance(value, dict):
|
|
151
|
+
raise denied("Agent Tool 审批响应无效")
|
|
152
|
+
return value
|
|
@@ -35,7 +35,7 @@ from southwind_agent_server.engine import (
|
|
|
35
35
|
from southwind_agent_server.openai_provider import OpenAiCompatibleProvider
|
|
36
36
|
from southwind_agent_server.provider_audit import ProviderAuditStore
|
|
37
37
|
from southwind_agent_server.provider_governance import ProviderRequestGovernance
|
|
38
|
-
from southwind_agent_server.tool_host import ToolHostClient
|
|
38
|
+
from southwind_agent_server.tool_host import ToolHostClient, canonical_tool_call_id
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
async def test_adk_runner_maps_text_usage_and_terminal() -> None:
|
|
@@ -103,7 +103,11 @@ async def test_adk_tool_events_execute_only_through_tool_host() -> None:
|
|
|
103
103
|
{
|
|
104
104
|
"executionGrant": "grant_1",
|
|
105
105
|
"runId": "run_1",
|
|
106
|
-
"toolCallId":
|
|
106
|
+
"toolCallId": canonical_tool_call_id(
|
|
107
|
+
"run_1",
|
|
108
|
+
"business.records.list",
|
|
109
|
+
{"page": 2},
|
|
110
|
+
),
|
|
107
111
|
"toolKey": "business.records.list",
|
|
108
112
|
"arguments": {"page": 2},
|
|
109
113
|
}
|
|
@@ -9,8 +9,9 @@ import httpx
|
|
|
9
9
|
import pytest
|
|
10
10
|
from fastapi import FastAPI
|
|
11
11
|
|
|
12
|
-
from southwind_agent_server.app import create_app
|
|
12
|
+
from southwind_agent_server.app import create_app
|
|
13
13
|
from southwind_agent_server.deterministic import DeterministicEngineAdapter
|
|
14
|
+
from southwind_agent_server.http_helpers import provider_governance_snapshot
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
@pytest.fixture
|
|
@@ -101,6 +102,28 @@ async def test_replay_supports_header_and_after(client: httpx.AsyncClient) -> No
|
|
|
101
102
|
assert [event["sequence"] for event in events(from_query)] == [4]
|
|
102
103
|
|
|
103
104
|
|
|
105
|
+
async def test_status_exposes_only_canonical_terminal_event(
|
|
106
|
+
client: httpx.AsyncClient,
|
|
107
|
+
) -> None:
|
|
108
|
+
created = await start(client, "test.echo", {"text": "status"}, "status-1")
|
|
109
|
+
run_id = str(created["runId"])
|
|
110
|
+
await wait_for_terminal(client, run_id)
|
|
111
|
+
|
|
112
|
+
response = await client.get(f"/api/agent/runs/{run_id}")
|
|
113
|
+
|
|
114
|
+
assert response.status_code == 200
|
|
115
|
+
assert response.json() == {
|
|
116
|
+
"runId": run_id,
|
|
117
|
+
"status": "completed",
|
|
118
|
+
"lastSequence": 4,
|
|
119
|
+
"terminalEvent": {
|
|
120
|
+
"type": "run.completed",
|
|
121
|
+
"runId": run_id,
|
|
122
|
+
"sequence": 4,
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
104
127
|
async def test_idempotency_reuses_or_conflicts(client: httpx.AsyncClient) -> None:
|
|
105
128
|
first = await start(client, "test.echo", {"text": "same"}, "same-key")
|
|
106
129
|
second = await start(client, "test.echo", {"text": "same"}, "same-key")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import pytest
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
from southwind_agent_server.artifact_input import ArtifactLoader
|
|
11
|
+
from southwind_agent_server.contracts import RuntimeArtifact, RuntimeLimits
|
|
12
|
+
from southwind_agent_server.engine import SafeEngineError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def test_loader_uses_fixed_endpoint_and_sanitizes_image() -> None:
|
|
16
|
+
source = png(4, 3)
|
|
17
|
+
digest = hashlib.sha256(source).hexdigest()
|
|
18
|
+
|
|
19
|
+
async def respond(request: httpx.Request) -> httpx.Response:
|
|
20
|
+
assert str(request.url) == "http://server.internal/api/internal/ai/artifacts"
|
|
21
|
+
assert request.headers["x-ai-artifact-ref"] == "opaque-ref"
|
|
22
|
+
assert request.headers["authorization"] == "Bearer internal"
|
|
23
|
+
return httpx.Response(
|
|
24
|
+
200,
|
|
25
|
+
content=source,
|
|
26
|
+
headers={"content-type": "image/png"},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
prepared = await ArtifactLoader(
|
|
30
|
+
"http://server.internal/api/internal/ai/artifacts",
|
|
31
|
+
"internal",
|
|
32
|
+
transport=httpx.MockTransport(respond),
|
|
33
|
+
).prepare(
|
|
34
|
+
[
|
|
35
|
+
RuntimeArtifact(
|
|
36
|
+
value="opaque-ref",
|
|
37
|
+
media_type="image/png",
|
|
38
|
+
byte_size=len(source),
|
|
39
|
+
sha256=digest,
|
|
40
|
+
)
|
|
41
|
+
],
|
|
42
|
+
["text", "image"],
|
|
43
|
+
RuntimeLimits(
|
|
44
|
+
max_input_bytes=4096,
|
|
45
|
+
max_output_bytes=4096,
|
|
46
|
+
max_artifacts=1,
|
|
47
|
+
max_pixels_per_artifact=12,
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
assert len(prepared) == 1
|
|
52
|
+
assert prepared[0].media_type == "image/png"
|
|
53
|
+
assert prepared[0].data_url.startswith("data:image/png;base64,")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def test_loader_rejects_pixel_budget_and_integrity_drift() -> None:
|
|
57
|
+
source = png(4, 3)
|
|
58
|
+
|
|
59
|
+
async def respond(_request: httpx.Request) -> httpx.Response:
|
|
60
|
+
return httpx.Response(
|
|
61
|
+
200,
|
|
62
|
+
content=source,
|
|
63
|
+
headers={"content-type": "image/png"},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
loader = ArtifactLoader(
|
|
67
|
+
"http://server.internal/api/internal/ai/artifacts",
|
|
68
|
+
"internal",
|
|
69
|
+
transport=httpx.MockTransport(respond),
|
|
70
|
+
)
|
|
71
|
+
artifact = RuntimeArtifact(
|
|
72
|
+
value="opaque-ref",
|
|
73
|
+
media_type="image/png",
|
|
74
|
+
byte_size=len(source),
|
|
75
|
+
sha256=hashlib.sha256(source).hexdigest(),
|
|
76
|
+
)
|
|
77
|
+
with pytest.raises(SafeEngineError) as pixels:
|
|
78
|
+
await loader.prepare(
|
|
79
|
+
[artifact],
|
|
80
|
+
["text", "image"],
|
|
81
|
+
RuntimeLimits(
|
|
82
|
+
max_input_bytes=4096,
|
|
83
|
+
max_output_bytes=4096,
|
|
84
|
+
max_artifacts=1,
|
|
85
|
+
max_pixels_per_artifact=11,
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
assert pixels.value.code == "BUDGET_EXCEEDED"
|
|
89
|
+
|
|
90
|
+
with pytest.raises(SafeEngineError) as integrity:
|
|
91
|
+
await loader.prepare(
|
|
92
|
+
[artifact.model_copy(update={"sha256": "a" * 64})],
|
|
93
|
+
["text", "image"],
|
|
94
|
+
RuntimeLimits(
|
|
95
|
+
max_input_bytes=4096,
|
|
96
|
+
max_output_bytes=4096,
|
|
97
|
+
max_artifacts=1,
|
|
98
|
+
max_pixels_per_artifact=12,
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
assert integrity.value.code == "INVALID_INPUT"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def png(width: int, height: int) -> bytes:
|
|
105
|
+
target = BytesIO()
|
|
106
|
+
Image.new("RGB", (width, height), color=(10, 20, 30)).save(
|
|
107
|
+
target,
|
|
108
|
+
format="PNG",
|
|
109
|
+
)
|
|
110
|
+
return target.getvalue()
|
|
@@ -15,11 +15,15 @@ def test_server_config_uses_secret_ref_and_normalizes_endpoint() -> None:
|
|
|
15
15
|
"QWEN_MODEL": "qwen-plus",
|
|
16
16
|
"QWEN_API_KEY": "server-secret",
|
|
17
17
|
"AI_PROVIDER_HTTPS_HOST_ALLOWLIST": "example.com",
|
|
18
|
+
"AI_PROVIDER_CAPABILITIES": "text-generation,vision",
|
|
19
|
+
"AI_PROVIDER_INPUT_MODALITIES": "text,image",
|
|
18
20
|
}
|
|
19
21
|
config = OpenAiCompatibleConfig.from_environment(environment)
|
|
20
22
|
assert config is not None
|
|
21
23
|
assert config.base_url == "https://example.com/compatible-mode/v1"
|
|
22
24
|
assert config.https_host_allowlist == ("example.com",)
|
|
25
|
+
assert config.capabilities == ("text-generation", "vision")
|
|
26
|
+
assert config.input_modalities == ("text", "image")
|
|
23
27
|
assert str(config.api_key_ref) == "[SecretRef]"
|
|
24
28
|
resolver = EnvironmentSecretResolver(environment)
|
|
25
29
|
assert config.api_key_ref is not None
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from southwind_agent_server.contracts import StartRunRequest
|
|
2
|
+
from southwind_agent_server.fingerprint import request_fingerprint, resolved_release
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_legacy_release_is_scoped_to_operation() -> None:
|
|
6
|
+
first = StartRunRequest(operation_key="business.first", input=None)
|
|
7
|
+
second = StartRunRequest(operation_key="business.second", input=None)
|
|
8
|
+
|
|
9
|
+
assert resolved_release(first)["releaseId"] != resolved_release(second)["releaseId"]
|
|
10
|
+
assert request_fingerprint(first) != request_fingerprint(second)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from southwind_agent_server.contracts import JsonValue
|
|
6
|
+
from southwind_agent_server.engine import PreparedArtifact, SafeEngineError
|
|
7
|
+
from southwind_agent_server.multimodal import (
|
|
8
|
+
attach_artifacts,
|
|
9
|
+
final_output,
|
|
10
|
+
response_format,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_typed_image_part_and_json_schema_output() -> None:
|
|
15
|
+
artifact = PreparedArtifact(
|
|
16
|
+
media_type="image/png",
|
|
17
|
+
data_url="data:image/png;base64,AAAA",
|
|
18
|
+
byte_size=3,
|
|
19
|
+
sha256="a" * 64,
|
|
20
|
+
)
|
|
21
|
+
messages = attach_artifacts(
|
|
22
|
+
[{"role": "user", "content": "提取可见字段"}],
|
|
23
|
+
(artifact,),
|
|
24
|
+
)
|
|
25
|
+
assert messages[0]["content"] == [
|
|
26
|
+
{"type": "text", "text": "提取可见字段"},
|
|
27
|
+
{
|
|
28
|
+
"type": "image_url",
|
|
29
|
+
"image_url": {
|
|
30
|
+
"url": "data:image/png;base64,AAAA",
|
|
31
|
+
"detail": "auto",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
schema: dict[str, JsonValue] = {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": ["name"],
|
|
38
|
+
"properties": {"name": {"type": "string"}},
|
|
39
|
+
}
|
|
40
|
+
assert response_format("evidence.output.v1", schema) == {
|
|
41
|
+
"type": "json_schema",
|
|
42
|
+
"json_schema": {
|
|
43
|
+
"name": "evidence_output_v1",
|
|
44
|
+
"strict": True,
|
|
45
|
+
"schema": schema,
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
assert final_output('{"name":"票据"}', schema) == {"name": "票据"}
|
|
49
|
+
with pytest.raises(SafeEngineError) as invalid:
|
|
50
|
+
final_output('{"name":42}', schema)
|
|
51
|
+
assert invalid.value.code == "OUTPUT_VALIDATION_FAILED"
|
|
@@ -15,7 +15,7 @@ from southwind_agent_server.engine import (
|
|
|
15
15
|
ToolRequested,
|
|
16
16
|
)
|
|
17
17
|
from southwind_agent_server.openai_engine import OpenAiCompatibleEngineAdapter
|
|
18
|
-
from southwind_agent_server.tool_host import ToolHostClient
|
|
18
|
+
from southwind_agent_server.tool_host import ToolHostClient, canonical_tool_call_id
|
|
19
19
|
from tests.openai_provider_test_support import (
|
|
20
20
|
create_provider,
|
|
21
21
|
governance,
|
|
@@ -128,7 +128,11 @@ async def test_engine_executes_only_allowed_tool_through_host() -> None:
|
|
|
128
128
|
assert tool_requests[0] == {
|
|
129
129
|
"executionGrant": "grant_1",
|
|
130
130
|
"runId": "run_1",
|
|
131
|
-
"toolCallId":
|
|
131
|
+
"toolCallId": canonical_tool_call_id(
|
|
132
|
+
"run_1",
|
|
133
|
+
"business.records.list",
|
|
134
|
+
{"page": 2},
|
|
135
|
+
),
|
|
132
136
|
"toolKey": "business.records.list",
|
|
133
137
|
"arguments": {"page": 2},
|
|
134
138
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import cast
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from psycopg import AsyncConnection
|
|
9
|
+
|
|
10
|
+
from southwind_agent_server.contracts import JsonValue
|
|
11
|
+
from southwind_agent_server.postgres_store import PostgresRunStore
|
|
12
|
+
from southwind_agent_server.store import IdempotencyConflictError, RunRecord
|
|
13
|
+
from southwind_agent_server.store_cipher import RunStoreCipher
|
|
14
|
+
|
|
15
|
+
DATABASE_URL = os.environ.get("AGENT_TEST_DATABASE_URL", "")
|
|
16
|
+
pytestmark = pytest.mark.skipif(
|
|
17
|
+
not DATABASE_URL,
|
|
18
|
+
reason="需要 AGENT_TEST_DATABASE_URL",
|
|
19
|
+
)
|
|
20
|
+
TEST_CIPHER = RunStoreCipher("postgres-test-secret-" * 2)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def test_multi_replica_replay_recovery_and_immutable_release() -> None:
|
|
24
|
+
suffix = uuid.uuid4().hex
|
|
25
|
+
first = PostgresRunStore(DATABASE_URL, TEST_CIPHER, lease_seconds=1)
|
|
26
|
+
second = PostgresRunStore(DATABASE_URL, TEST_CIPHER, lease_seconds=1)
|
|
27
|
+
record = fixture(suffix)
|
|
28
|
+
try:
|
|
29
|
+
created, is_new = await first.create_or_get(
|
|
30
|
+
record,
|
|
31
|
+
{"input": {"secret": f"sensitive-{suffix}"}},
|
|
32
|
+
)
|
|
33
|
+
repeated, repeated_is_new = await second.create_or_get(record)
|
|
34
|
+
await first.append(record.run_id, "run.started")
|
|
35
|
+
await first.append(record.run_id, "output.delta", {"text": "one"})
|
|
36
|
+
replayed, terminal = await second.events_after(record.run_id, 1, 0)
|
|
37
|
+
|
|
38
|
+
await first.release_lease(record.run_id)
|
|
39
|
+
recovered = await second.recoverable()
|
|
40
|
+
await second.complete(record.run_id, {"answer": "done"})
|
|
41
|
+
await first.append(record.run_id, "run.cancelled")
|
|
42
|
+
final = await first.get(record.run_id)
|
|
43
|
+
|
|
44
|
+
evaluation = await second.record_evaluation(
|
|
45
|
+
release_id(suffix),
|
|
46
|
+
"quality",
|
|
47
|
+
"1",
|
|
48
|
+
{"accuracy": 1.0},
|
|
49
|
+
)
|
|
50
|
+
repeated_evaluation = await first.record_evaluation(
|
|
51
|
+
release_id(suffix),
|
|
52
|
+
"quality",
|
|
53
|
+
"1",
|
|
54
|
+
{"accuracy": 1.0},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert is_new is True
|
|
58
|
+
assert created.run_id == repeated.run_id
|
|
59
|
+
assert repeated_is_new is False
|
|
60
|
+
assert [event["sequence"] for event in replayed] == [2]
|
|
61
|
+
assert terminal is False
|
|
62
|
+
assert recovered[0][0].run_id == record.run_id
|
|
63
|
+
assert final is not None
|
|
64
|
+
assert final.status == "completed"
|
|
65
|
+
assert [event["type"] for event in final.events].count("run.completed") == 1
|
|
66
|
+
assert evaluation == repeated_evaluation
|
|
67
|
+
await assert_encrypted(record.run_id, suffix)
|
|
68
|
+
|
|
69
|
+
conflict = fixture(f"{suffix}_other")
|
|
70
|
+
conflict.release = {
|
|
71
|
+
**cast(dict[str, JsonValue], conflict.release),
|
|
72
|
+
"releaseId": release_id(suffix),
|
|
73
|
+
"promptVersion": "changed",
|
|
74
|
+
}
|
|
75
|
+
with pytest.raises(IdempotencyConflictError):
|
|
76
|
+
await second.create_or_get(conflict)
|
|
77
|
+
finally:
|
|
78
|
+
await first.close()
|
|
79
|
+
await second.close()
|
|
80
|
+
await cleanup(suffix)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def fixture(suffix: str) -> RunRecord:
|
|
84
|
+
return RunRecord(
|
|
85
|
+
run_id=f"run_{suffix}",
|
|
86
|
+
operation_key="test.echo",
|
|
87
|
+
input={"secret": f"sensitive-{suffix}"},
|
|
88
|
+
idempotency_key=f"key_{suffix}",
|
|
89
|
+
fingerprint=f"{suffix:0<64}"[:64],
|
|
90
|
+
release={
|
|
91
|
+
"releaseId": release_id(suffix),
|
|
92
|
+
"operationVersion": "1.0.0",
|
|
93
|
+
"workflowVersion": "workflow-1",
|
|
94
|
+
"promptVersion": "prompt-1",
|
|
95
|
+
"routeVersion": "route-1",
|
|
96
|
+
"toolSchemaVersion": "schema-1",
|
|
97
|
+
"manifestDigest": "0" * 64,
|
|
98
|
+
},
|
|
99
|
+
trace_id="1" * 32,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def release_id(suffix: str) -> str:
|
|
104
|
+
return f"release_{suffix[:40]}"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def assert_encrypted(run_id: str, suffix: str) -> None:
|
|
108
|
+
async with await AsyncConnection.connect(DATABASE_URL) as connection:
|
|
109
|
+
cursor = await connection.execute(
|
|
110
|
+
"""
|
|
111
|
+
select invocation_cipher, output_cipher from agent_runtime_runs
|
|
112
|
+
where run_id = %s
|
|
113
|
+
""",
|
|
114
|
+
(run_id,),
|
|
115
|
+
)
|
|
116
|
+
row = await cursor.fetchone()
|
|
117
|
+
assert row is not None
|
|
118
|
+
assert f"sensitive-{suffix}" not in str(row)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def cleanup(suffix: str) -> None:
|
|
122
|
+
async with await AsyncConnection.connect(DATABASE_URL) as connection:
|
|
123
|
+
await connection.execute(
|
|
124
|
+
"delete from agent_evaluation_results where release_id = %s",
|
|
125
|
+
(release_id(suffix),),
|
|
126
|
+
)
|
|
127
|
+
await connection.execute(
|
|
128
|
+
"delete from agent_runtime_runs where run_id like %s",
|
|
129
|
+
(f"run_{suffix}%",),
|
|
130
|
+
)
|
|
131
|
+
await connection.commit()
|
|
@@ -108,6 +108,67 @@ def test_missing_allowlist_and_dlp_reject_before_network(tmp_path: Path) -> None
|
|
|
108
108
|
assert decisions == [("reject", 0), ("reject", 0)]
|
|
109
109
|
|
|
110
110
|
|
|
111
|
+
def test_multimodal_attempt_requires_artifact_and_schema_allowlist(
|
|
112
|
+
tmp_path: Path,
|
|
113
|
+
) -> None:
|
|
114
|
+
path = tmp_path / "provider-audit.sqlite3"
|
|
115
|
+
governance = ProviderRequestGovernance(
|
|
116
|
+
ProviderAuditStore(str(path), retention_days=90)
|
|
117
|
+
)
|
|
118
|
+
multimodal = policy(user_action="redact")
|
|
119
|
+
multimodal.envelope_fields.extend(
|
|
120
|
+
[
|
|
121
|
+
ProviderFieldPolicy(
|
|
122
|
+
field="artifact-content",
|
|
123
|
+
classification="sensitive",
|
|
124
|
+
dlp_action="reject",
|
|
125
|
+
),
|
|
126
|
+
ProviderFieldPolicy(
|
|
127
|
+
field="output-schema",
|
|
128
|
+
classification="internal",
|
|
129
|
+
dlp_action="reject",
|
|
130
|
+
),
|
|
131
|
+
]
|
|
132
|
+
)
|
|
133
|
+
result = governance.authorize(
|
|
134
|
+
deployment_key="ai.openai-compatible",
|
|
135
|
+
operation_key="business.vision",
|
|
136
|
+
run_id="run_vision",
|
|
137
|
+
attempt=1,
|
|
138
|
+
messages=[
|
|
139
|
+
{"role": "system", "content": "只提取可见内容"},
|
|
140
|
+
{
|
|
141
|
+
"role": "user",
|
|
142
|
+
"content": [
|
|
143
|
+
{"type": "text", "text": "提取字段"},
|
|
144
|
+
{
|
|
145
|
+
"type": "image_url",
|
|
146
|
+
"image_url": {
|
|
147
|
+
"url": "data:image/png;base64,AAAA",
|
|
148
|
+
"detail": "auto",
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
tools=(),
|
|
155
|
+
policy=multimodal,
|
|
156
|
+
artifact_bytes=3,
|
|
157
|
+
output_schema={"type": "object"},
|
|
158
|
+
)
|
|
159
|
+
assert result.output_schema == {"type": "object"}
|
|
160
|
+
row = (
|
|
161
|
+
sqlite3.connect(path)
|
|
162
|
+
.execute(
|
|
163
|
+
"select classifications, sent_bytes, decision from provider_attempt_audit"
|
|
164
|
+
)
|
|
165
|
+
.fetchone()
|
|
166
|
+
)
|
|
167
|
+
assert row[0] == "internal,sensitive"
|
|
168
|
+
assert row[1] > 3
|
|
169
|
+
assert row[2] == "allow"
|
|
170
|
+
|
|
171
|
+
|
|
111
172
|
def test_attempts_are_unique_append_only_and_ttl_purge_is_bounded(
|
|
112
173
|
tmp_path: Path,
|
|
113
174
|
) -> None:
|