create-windy 0.2.34 → 0.3.0
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/dist/cli.js +167 -17
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +5 -0
- package/template/README.md +3 -1
- package/template/apps/agent-server/Dockerfile +1 -0
- package/template/apps/agent-server/README.md +18 -5
- package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
- package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
- package/template/apps/agent-server/pyproject.toml +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
- package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
- package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
- package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
- package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
- package/template/apps/server/package.json +4 -1
- package/template/apps/server/src/agent/run-facade.ts +2 -7
- package/template/apps/server/src/agent/tool-registry.ts +2 -6
- package/template/apps/server/src/application-services.ts +50 -0
- package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
- package/template/apps/server/src/audit/search-summary.ts +2 -6
- package/template/apps/server/src/auth/credential-restriction.ts +1 -0
- package/template/apps/server/src/auth/password.ts +30 -21
- package/template/apps/server/src/auth/routes.test.ts +44 -3
- package/template/apps/server/src/auth/routes.ts +6 -1
- package/template/apps/server/src/auth/runtime.ts +4 -1
- package/template/apps/server/src/auth/service.ts +25 -7
- package/template/apps/server/src/configuration/bootstrap.ts +20 -1
- package/template/apps/server/src/configuration/definition.test.ts +71 -0
- package/template/apps/server/src/configuration/definition.ts +242 -12
- package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
- package/template/apps/server/src/configuration/repository.ts +27 -2
- package/template/apps/server/src/configuration/routes.ts +4 -1
- package/template/apps/server/src/configuration/service.test.ts +100 -0
- package/template/apps/server/src/configuration/service.ts +50 -10
- package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
- package/template/apps/server/src/data-governance/export/service.ts +2 -6
- package/template/apps/server/src/example-modules.ts +4 -1
- package/template/apps/server/src/guards.ts +18 -5
- package/template/apps/server/src/http/request-source.test.ts +85 -0
- package/template/apps/server/src/http/request-source.ts +90 -0
- package/template/apps/server/src/index.ts +23 -6
- package/template/apps/server/src/license/routes.ts +2 -0
- package/template/apps/server/src/license/runtime-service.ts +10 -11
- package/template/apps/server/src/module-bootstrap.ts +2 -2
- package/template/apps/server/src/module-host/request-context.ts +6 -6
- package/template/apps/server/src/module-host/storage-scope.ts +2 -9
- package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
- package/template/apps/server/src/notification/channel-adapter.ts +46 -0
- package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
- package/template/apps/server/src/notification/delivery-policy.ts +37 -0
- package/template/apps/server/src/notification/delivery-repository.ts +198 -0
- package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
- package/template/apps/server/src/notification/delivery-worker.ts +88 -0
- package/template/apps/server/src/notification/drizzle-management.ts +180 -0
- package/template/apps/server/src/notification/drizzle-query.ts +160 -0
- package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
- package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
- package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
- package/template/apps/server/src/notification/extension-routes.ts +111 -0
- package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
- package/template/apps/server/src/notification/memory-support.ts +72 -0
- package/template/apps/server/src/notification/preference-repository.ts +95 -0
- package/template/apps/server/src/notification/recipient-directory.ts +104 -0
- package/template/apps/server/src/notification/repository.test.ts +171 -0
- package/template/apps/server/src/notification/repository.ts +179 -32
- package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
- package/template/apps/server/src/notification/route-support.ts +225 -0
- package/template/apps/server/src/notification/routes.test.ts +182 -0
- package/template/apps/server/src/notification/routes.ts +225 -132
- package/template/apps/server/src/notification/service.test.ts +104 -0
- package/template/apps/server/src/notification/service.ts +140 -0
- package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
- package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
- package/template/apps/server/src/persistence.integration.test.ts +41 -2
- package/template/apps/server/src/route-guards.ts +1 -0
- package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
- package/template/apps/server/src/runtime-development.ts +1 -0
- package/template/apps/server/src/runtime.test.ts +23 -9
- package/template/apps/server/src/runtime.ts +39 -2
- package/template/apps/server/src/scheduler/audit.ts +2 -11
- package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
- package/template/apps/server/src/scheduler/definitions.ts +7 -0
- package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
- package/template/apps/server/src/scheduler/routes.ts +2 -7
- package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
- package/template/apps/server/src/search/opensearch-client.ts +145 -0
- package/template/apps/server/src/search/service.ts +2 -6
- package/template/apps/server/src/settings/routes.ts +15 -3
- package/template/apps/server/src/settings/runtime.ts +10 -1
- package/template/apps/server/src/storage/runtime.ts +13 -0
- package/template/apps/server/src/system/audit-query.ts +33 -4
- package/template/apps/server/src/system/audit-routes.test.ts +58 -4
- package/template/apps/server/src/system/audit-routes.ts +6 -5
- package/template/apps/server/src/system/department-organization.ts +320 -0
- package/template/apps/server/src/system/department-routes.test.ts +207 -48
- package/template/apps/server/src/system/department-routes.ts +114 -33
- package/template/apps/server/src/system/drizzle-repository.ts +15 -7
- package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
- package/template/apps/server/src/system/drizzle-system.ts +37 -9
- package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
- package/template/apps/server/src/system/identity-route-config.ts +7 -2
- package/template/apps/server/src/system/mutation-runner.ts +23 -0
- package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
- package/template/apps/server/src/system/resource-access-response.ts +12 -6
- package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
- package/template/apps/server/src/system/route-helpers.ts +2 -5
- package/template/apps/server/src/system/route-types.ts +12 -3
- package/template/apps/server/src/system/routes-validation.test.ts +26 -0
- package/template/apps/server/src/system/routes.test.ts +10 -2
- package/template/apps/server/src/system/routes.ts +70 -36
- package/template/apps/server/src/system/seed.ts +7 -0
- package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
- package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
- package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
- package/template/apps/server/src/work-order/search-provider.ts +60 -1
- package/template/apps/server/src/work-order/service.ts +42 -10
- package/template/apps/web/src/app/error-pages.ts +9 -0
- package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
- package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
- package/template/apps/web/src/components/common/FormDialog.vue +16 -2
- package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
- package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
- package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
- package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
- package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
- package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
- package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
- package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
- package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
- package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
- package/template/apps/web/src/lib/date-time.ts +22 -10
- package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
- package/template/apps/web/src/main.ts +14 -1
- package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
- package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
- package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
- package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
- package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
- package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
- package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
- package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
- package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
- package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
- package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
- package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
- package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
- package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
- package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
- package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
- package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
- package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
- package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
- package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
- package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
- package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
- package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
- package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
- package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
- package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
- package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
- package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
- package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
- package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
- package/template/apps/web/src/pages/system/resource-config.ts +12 -4
- package/template/apps/web/src/router/error-navigation.ts +50 -0
- package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
- package/template/apps/web/src/router/index.ts +71 -0
- package/template/apps/web/src/router/index.webtest.ts +29 -0
- package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
- package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
- package/template/apps/web/src/services/auth-api.ts +5 -0
- package/template/apps/web/src/services/http.ts +43 -6
- package/template/apps/web/src/services/http.webtest.ts +70 -0
- package/template/apps/web/src/services/notification-api.ts +85 -2
- package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
- package/template/apps/web/src/services/search-api.ts +39 -0
- package/template/apps/web/src/services/system-api.ts +18 -0
- package/template/apps/web/vitest.config.ts +1 -0
- package/template/docker-compose.yml +66 -0
- package/template/docs/architecture/ai-runtime.md +14 -5
- package/template/docs/architecture/search-provider-protocol.md +38 -2
- package/template/docs/development/custom-error-pages.md +78 -0
- package/template/docs/development/custom-login-page.md +3 -3
- package/template/docs/platform/agent-runtime.md +32 -6
- package/template/docs/platform/audit-reliability.md +46 -2
- package/template/docs/platform/module-configuration.md +13 -4
- package/template/docs/platform/notifications.md +67 -11
- package/template/docs/platform/organization-membership.md +17 -3
- package/template/docs/platform/system-crud-api.md +16 -0
- package/template/docs/platform/web-system-crud.md +6 -2
- package/template/package.json +4 -0
- package/template/packages/config/index.test.ts +45 -0
- package/template/packages/config/package.json +1 -1
- package/template/packages/config/src/platform.ts +169 -48
- package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
- package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
- package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
- package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
- package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
- package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
- package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
- package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
- package/template/packages/database/drizzle/meta/_journal.json +28 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/schema/identity.ts +7 -0
- package/template/packages/database/src/schema/notifications.ts +135 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
- package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/system-api-permissions.ts +54 -0
- package/template/packages/modules/src/system-audit-actions.ts +10 -0
- package/template/packages/modules/src/system-features.ts +1 -1
- package/template/packages/modules/src/system-module-catalog.ts +2 -1
- package/template/packages/modules/src/system-modules.test.ts +13 -1
- package/template/packages/modules/src/system-modules.ts +12 -0
- package/template/packages/modules/src/system-permissions.ts +9 -5
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/package.json +1 -1
- package/template/packages/shared/src/audit.ts +25 -0
- package/template/packages/shared/src/module-configuration.ts +5 -0
- package/template/packages/shared/src/notification.ts +92 -1
- package/template/packages/shared/src/password.ts +7 -2
- package/template/packages/shared/src/platform-settings.ts +2 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/upload-service.test.ts +16 -0
- package/template/packages/storage/src/upload-service.ts +5 -1
- package/template/scripts/notification-postgres-regression.ts +27 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import tempfile
|
|
6
|
+
from collections.abc import Callable, Coroutine
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from southwind_agent_server.adk_adapter import AdkEngineAdapter
|
|
12
|
+
from southwind_agent_server.config import (
|
|
13
|
+
EnvironmentSecretResolver,
|
|
14
|
+
OpenAiCompatibleConfig,
|
|
15
|
+
SecretRef,
|
|
16
|
+
)
|
|
17
|
+
from southwind_agent_server.contracts import (
|
|
18
|
+
ProviderDataPolicy,
|
|
19
|
+
ProviderEgress,
|
|
20
|
+
ProviderFieldPolicy,
|
|
21
|
+
ProviderInputFieldPolicy,
|
|
22
|
+
RuntimeLimits,
|
|
23
|
+
RuntimeToolDefinition,
|
|
24
|
+
ToolNetworkPolicy,
|
|
25
|
+
)
|
|
26
|
+
from southwind_agent_server.engine import (
|
|
27
|
+
EngineCompleted,
|
|
28
|
+
EngineRunRequest,
|
|
29
|
+
OutputDelta,
|
|
30
|
+
SafeEngineError,
|
|
31
|
+
ToolCompleted,
|
|
32
|
+
ToolRequested,
|
|
33
|
+
UsageReported,
|
|
34
|
+
)
|
|
35
|
+
from southwind_agent_server.openai_provider import OpenAiCompatibleProvider
|
|
36
|
+
from southwind_agent_server.provider_audit import ProviderAuditStore
|
|
37
|
+
from southwind_agent_server.provider_governance import ProviderRequestGovernance
|
|
38
|
+
from southwind_agent_server.tool_host import ToolHostClient
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def test_adk_runner_maps_text_usage_and_terminal() -> None:
|
|
42
|
+
async def provider_handler(_request: httpx.Request) -> httpx.Response:
|
|
43
|
+
return sse_response(
|
|
44
|
+
[
|
|
45
|
+
{"choices": [{"delta": {"content": "你"}}]},
|
|
46
|
+
{
|
|
47
|
+
"choices": [{"delta": {"content": "好"}}],
|
|
48
|
+
"usage": {
|
|
49
|
+
"prompt_tokens": 2,
|
|
50
|
+
"completion_tokens": 2,
|
|
51
|
+
"total_tokens": 4,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
signals = [
|
|
58
|
+
signal async for signal in engine(provider_handler).execute(runtime_request())
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
assert [signal.text for signal in signals if isinstance(signal, OutputDelta)] == [
|
|
62
|
+
"你",
|
|
63
|
+
"好",
|
|
64
|
+
]
|
|
65
|
+
usage = [signal.usage for signal in signals if isinstance(signal, UsageReported)]
|
|
66
|
+
assert usage == [
|
|
67
|
+
{
|
|
68
|
+
"source": "provider-reported",
|
|
69
|
+
"inputTokens": 2,
|
|
70
|
+
"outputTokens": 2,
|
|
71
|
+
"totalTokens": 4,
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
assert isinstance(signals[-1], EngineCompleted)
|
|
75
|
+
assert signals[-1].output == {"text": "你好"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def test_adk_tool_events_execute_only_through_tool_host() -> None:
|
|
79
|
+
provider_calls = 0
|
|
80
|
+
tool_requests: list[dict[str, object]] = []
|
|
81
|
+
|
|
82
|
+
async def provider_handler(_request: httpx.Request) -> httpx.Response:
|
|
83
|
+
nonlocal provider_calls
|
|
84
|
+
provider_calls += 1
|
|
85
|
+
if provider_calls == 1:
|
|
86
|
+
return tool_call_response("business.records.list")
|
|
87
|
+
return sse_response([{"choices": [{"delta": {"content": "完成"}}]}])
|
|
88
|
+
|
|
89
|
+
async def tool_handler(request: httpx.Request) -> httpx.Response:
|
|
90
|
+
tool_requests.append(json.loads(request.content))
|
|
91
|
+
return httpx.Response(200, json={"value": {"total": 1}})
|
|
92
|
+
|
|
93
|
+
signals = [
|
|
94
|
+
signal
|
|
95
|
+
async for signal in engine(provider_handler, tool_handler).execute(
|
|
96
|
+
runtime_request("business.records.list")
|
|
97
|
+
)
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
assert any(isinstance(signal, ToolRequested) for signal in signals)
|
|
101
|
+
assert any(isinstance(signal, ToolCompleted) for signal in signals)
|
|
102
|
+
assert tool_requests == [
|
|
103
|
+
{
|
|
104
|
+
"executionGrant": "grant_1",
|
|
105
|
+
"runId": "run_1",
|
|
106
|
+
"toolCallId": "call_1",
|
|
107
|
+
"toolKey": "business.records.list",
|
|
108
|
+
"arguments": {"page": 2},
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
assert signals[-1].output == {"text": "完成"} # type: ignore[union-attr]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def test_adk_denies_unknown_tool_before_host_callback() -> None:
|
|
115
|
+
host_called = False
|
|
116
|
+
|
|
117
|
+
async def provider_handler(_request: httpx.Request) -> httpx.Response:
|
|
118
|
+
return tool_call_response("business.admin.delete")
|
|
119
|
+
|
|
120
|
+
async def tool_handler(_request: httpx.Request) -> httpx.Response:
|
|
121
|
+
nonlocal host_called
|
|
122
|
+
host_called = True
|
|
123
|
+
return httpx.Response(200, json={"value": None})
|
|
124
|
+
|
|
125
|
+
with pytest.raises(SafeEngineError) as captured:
|
|
126
|
+
async for _ in engine(provider_handler, tool_handler).execute(
|
|
127
|
+
runtime_request("business.records.list")
|
|
128
|
+
):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
assert captured.value.code == "TOOL_DENIED"
|
|
132
|
+
assert host_called is False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def test_adk_governance_rejects_before_provider_network() -> None:
|
|
136
|
+
provider_called = False
|
|
137
|
+
|
|
138
|
+
async def provider_handler(_request: httpx.Request) -> httpx.Response:
|
|
139
|
+
nonlocal provider_called
|
|
140
|
+
provider_called = True
|
|
141
|
+
return sse_response([])
|
|
142
|
+
|
|
143
|
+
request = runtime_request()
|
|
144
|
+
request = EngineRunRequest(
|
|
145
|
+
**{
|
|
146
|
+
**request.__dict__,
|
|
147
|
+
"instructions": "password=server-secret-value",
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
with pytest.raises(SafeEngineError) as captured:
|
|
152
|
+
async for _ in engine(provider_handler).execute(request):
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
assert captured.value.code == "FORBIDDEN"
|
|
156
|
+
assert provider_called is False
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
async def test_adk_cancel_and_provider_error_map_to_safe_errors() -> None:
|
|
160
|
+
started = asyncio.Event()
|
|
161
|
+
|
|
162
|
+
async def slow_provider(_request: httpx.Request) -> httpx.Response:
|
|
163
|
+
started.set()
|
|
164
|
+
await asyncio.sleep(0.03)
|
|
165
|
+
return sse_response([{"choices": [{"delta": {"content": "late"}}]}])
|
|
166
|
+
|
|
167
|
+
adapter = engine(slow_provider)
|
|
168
|
+
|
|
169
|
+
async def consume() -> None:
|
|
170
|
+
async for _ in adapter.execute(runtime_request()):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
task = asyncio.create_task(consume())
|
|
174
|
+
await started.wait()
|
|
175
|
+
await adapter.cancel("run_1")
|
|
176
|
+
with pytest.raises(SafeEngineError) as cancelled:
|
|
177
|
+
await task
|
|
178
|
+
assert cancelled.value.code == "RUN_CANCELLED"
|
|
179
|
+
|
|
180
|
+
async def failed_provider(_request: httpx.Request) -> httpx.Response:
|
|
181
|
+
return httpx.Response(500, text="provider-secret-body")
|
|
182
|
+
|
|
183
|
+
with pytest.raises(SafeEngineError) as failed:
|
|
184
|
+
async for _ in engine(failed_provider).execute(runtime_request()):
|
|
185
|
+
pass
|
|
186
|
+
assert failed.value.code == "PROVIDER_UNAVAILABLE"
|
|
187
|
+
assert "provider-secret-body" not in str(failed.value)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def engine(
|
|
191
|
+
provider_handler: Callable[[httpx.Request], Coroutine[None, None, httpx.Response]],
|
|
192
|
+
tool_handler: Callable[[httpx.Request], Coroutine[None, None, httpx.Response]]
|
|
193
|
+
| None = None,
|
|
194
|
+
) -> AdkEngineAdapter:
|
|
195
|
+
async def unused_tool(_request: httpx.Request) -> httpx.Response:
|
|
196
|
+
return httpx.Response(500, json={"error": "unused"})
|
|
197
|
+
|
|
198
|
+
provider = OpenAiCompatibleProvider(
|
|
199
|
+
OpenAiCompatibleConfig(
|
|
200
|
+
base_url="https://provider.test/v1",
|
|
201
|
+
model="qwen-compatible",
|
|
202
|
+
api_key_ref=SecretRef("env", "QWEN_API_KEY"),
|
|
203
|
+
https_host_allowlist=("provider.test",),
|
|
204
|
+
),
|
|
205
|
+
EnvironmentSecretResolver({"QWEN_API_KEY": "server-secret"}),
|
|
206
|
+
transport=httpx.MockTransport(provider_handler),
|
|
207
|
+
)
|
|
208
|
+
audit_path = tempfile.NamedTemporaryFile(suffix=".sqlite3", delete=False).name
|
|
209
|
+
return AdkEngineAdapter(
|
|
210
|
+
provider,
|
|
211
|
+
ToolHostClient(
|
|
212
|
+
"internal-token",
|
|
213
|
+
transport=httpx.MockTransport(tool_handler or unused_tool),
|
|
214
|
+
),
|
|
215
|
+
ProviderRequestGovernance(ProviderAuditStore(audit_path, retention_days=90)),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def runtime_request(tool_key: str | None = None) -> EngineRunRequest:
|
|
220
|
+
tools = (
|
|
221
|
+
(
|
|
222
|
+
RuntimeToolDefinition(
|
|
223
|
+
key=tool_key,
|
|
224
|
+
label="查询记录",
|
|
225
|
+
description="查询当前数据范围内的记录。",
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
if tool_key
|
|
229
|
+
else ()
|
|
230
|
+
)
|
|
231
|
+
return EngineRunRequest(
|
|
232
|
+
run_id="run_1",
|
|
233
|
+
operation_key="business.answer",
|
|
234
|
+
input={"question": "记录"},
|
|
235
|
+
instructions="回答业务问题。",
|
|
236
|
+
user_prompt="有多少记录?",
|
|
237
|
+
allowed_tools=tools,
|
|
238
|
+
limits=RuntimeLimits(max_duration_seconds=60, max_output_bytes=4096),
|
|
239
|
+
provider_egress=ProviderEgress(
|
|
240
|
+
mode="configured-endpoint",
|
|
241
|
+
deployment_key="ai.openai-compatible",
|
|
242
|
+
),
|
|
243
|
+
provider_data_policy=ProviderDataPolicy(
|
|
244
|
+
input_fields=[
|
|
245
|
+
ProviderInputFieldPolicy(
|
|
246
|
+
json_pointer="/question",
|
|
247
|
+
classification="internal",
|
|
248
|
+
dlp_action="redact",
|
|
249
|
+
)
|
|
250
|
+
],
|
|
251
|
+
envelope_fields=[
|
|
252
|
+
ProviderFieldPolicy(
|
|
253
|
+
field=field,
|
|
254
|
+
classification="internal",
|
|
255
|
+
dlp_action="reject" if field == "instructions" else "redact",
|
|
256
|
+
)
|
|
257
|
+
for field in (
|
|
258
|
+
"instructions",
|
|
259
|
+
"user-prompt",
|
|
260
|
+
"tool-definitions",
|
|
261
|
+
"provider-output",
|
|
262
|
+
"tool-results",
|
|
263
|
+
)
|
|
264
|
+
],
|
|
265
|
+
),
|
|
266
|
+
tool_network_policy=ToolNetworkPolicy(mode="forbidden"),
|
|
267
|
+
execution_grant="grant_1",
|
|
268
|
+
tool_host_url="http://server:9747",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def tool_call_response(name: str) -> httpx.Response:
|
|
273
|
+
return sse_response(
|
|
274
|
+
[
|
|
275
|
+
{
|
|
276
|
+
"choices": [
|
|
277
|
+
{
|
|
278
|
+
"delta": {
|
|
279
|
+
"tool_calls": [
|
|
280
|
+
{
|
|
281
|
+
"index": 0,
|
|
282
|
+
"id": "call_1",
|
|
283
|
+
"function": {
|
|
284
|
+
"name": name,
|
|
285
|
+
"arguments": '{"page":2}',
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def sse_response(chunks: list[object]) -> httpx.Response:
|
|
298
|
+
body = "".join(
|
|
299
|
+
f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n" for chunk in chunks
|
|
300
|
+
)
|
|
301
|
+
return httpx.Response(
|
|
302
|
+
200,
|
|
303
|
+
content=body.encode(),
|
|
304
|
+
headers={"content-type": "text/event-stream"},
|
|
305
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from google.adk.models.base_llm import BaseLlm
|
|
4
|
+
|
|
5
|
+
from adk_web_agents.safe_debug.agent import root_agent
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_adk_web_agent_has_no_business_tools_or_provider_model() -> None:
|
|
9
|
+
assert root_agent.name == "safe_debug"
|
|
10
|
+
assert root_agent.tools == []
|
|
11
|
+
assert isinstance(root_agent.model, BaseLlm)
|
|
12
|
+
assert root_agent.model.model == "windy-safe-debug"
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
"drizzle-orm": "^0.45.2",
|
|
27
27
|
"elysia": "^1.4.29",
|
|
28
28
|
"mysql2": "^3.22.6",
|
|
29
|
+
"nodemailer": "^9.0.3",
|
|
29
30
|
"pg": "^8.22.0",
|
|
30
31
|
"pino": "^10.3.1",
|
|
31
32
|
"zod": "^4.4.3"
|
|
32
33
|
},
|
|
33
|
-
"devDependencies": {
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/nodemailer": "^8.0.1"
|
|
36
|
+
}
|
|
34
37
|
}
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
ModuleAiOperationHandlerRegistration,
|
|
13
13
|
} from "@southwind-ai/server-sdk";
|
|
14
14
|
import { evaluateAccessGuards } from "../access-guards.js";
|
|
15
|
-
import type
|
|
15
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
16
16
|
import type { InitializedModuleHosts } from "../module-host.js";
|
|
17
17
|
import { toModuleActor } from "../module-host.js";
|
|
18
18
|
import type { createServerRuntime } from "../runtime.js";
|
|
@@ -154,12 +154,7 @@ export class AgentRunFacade {
|
|
|
154
154
|
id: randomUUIDv7(),
|
|
155
155
|
action: operation.definition.auditAction,
|
|
156
156
|
outcome: "success",
|
|
157
|
-
actor:
|
|
158
|
-
id: context.actor.id,
|
|
159
|
-
type:
|
|
160
|
-
context.actor.type === "anonymous" ? "user" : context.actor.type,
|
|
161
|
-
name: context.actor.name,
|
|
162
|
-
},
|
|
157
|
+
actor: auditActor(context),
|
|
163
158
|
target: {
|
|
164
159
|
type: "agent-run",
|
|
165
160
|
id: created.runId,
|
|
@@ -11,7 +11,7 @@ import type { AuditEvent, FeatureFlagDefinition } from "@southwind-ai/shared";
|
|
|
11
11
|
import Ajv, { type ValidateFunction } from "ajv";
|
|
12
12
|
import { evaluateAccessGuards } from "../access-guards.js";
|
|
13
13
|
import { isServerScopeContext } from "../data-access/context.js";
|
|
14
|
-
import type
|
|
14
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
15
15
|
import { findFeature } from "../route-guards.js";
|
|
16
16
|
import type { createServerRuntime } from "../runtime.js";
|
|
17
17
|
|
|
@@ -292,11 +292,7 @@ function toolEvent(
|
|
|
292
292
|
id: crypto.randomUUID(),
|
|
293
293
|
action: tool.auditAction,
|
|
294
294
|
outcome: "success",
|
|
295
|
-
actor:
|
|
296
|
-
id: context.actor.id,
|
|
297
|
-
type: context.actor.type === "anonymous" ? "user" : context.actor.type,
|
|
298
|
-
name: context.actor.name,
|
|
299
|
-
},
|
|
295
|
+
actor: auditActor(context),
|
|
300
296
|
target: { type: "agent-tool", id: tool.key, label: tool.label },
|
|
301
297
|
module: tool.module,
|
|
302
298
|
requestId: context.requestId,
|
|
@@ -20,6 +20,7 @@ import { DrizzleSchedulerRepository } from "./scheduler/drizzle-repository.js";
|
|
|
20
20
|
import { InMemorySchedulerRepository } from "./scheduler/repository.js";
|
|
21
21
|
import type { ScheduledTaskDefinition } from "./scheduler/types.js";
|
|
22
22
|
import type { createStorageRuntime } from "./storage/runtime.js";
|
|
23
|
+
import type { PlatformConfig } from "@southwind-ai/config";
|
|
23
24
|
import type { DurableHandlerRegistry } from "@southwind-ai/jobs";
|
|
24
25
|
import { collectActiveBlobRefs } from "./storage/blob-reference-registry.js";
|
|
25
26
|
import type { createSystemRepositories } from "./system/seed.js";
|
|
@@ -33,6 +34,17 @@ import {
|
|
|
33
34
|
} from "./workflow/effects.js";
|
|
34
35
|
import { InMemoryWorkflowRepository } from "./workflow/repository.js";
|
|
35
36
|
import { WorkflowService } from "./workflow/service.js";
|
|
37
|
+
import { NotificationChannelRegistry } from "./notification/channel-adapter.js";
|
|
38
|
+
import { DrizzleNotificationDeliveryRepository } from "./notification/delivery-repository.js";
|
|
39
|
+
import { NotificationDeliveryWorker } from "./notification/delivery-worker.js";
|
|
40
|
+
import { InMemoryNotificationDeliveryRepository } from "./notification/memory-delivery-repository.js";
|
|
41
|
+
import {
|
|
42
|
+
DrizzleNotificationPreferenceRepository,
|
|
43
|
+
InMemoryNotificationPreferenceRepository,
|
|
44
|
+
} from "./notification/preference-repository.js";
|
|
45
|
+
import { SystemNotificationRecipientDirectory } from "./notification/recipient-directory.js";
|
|
46
|
+
import { NotificationService } from "./notification/service.js";
|
|
47
|
+
import { SmtpNotificationChannelAdapter } from "./notification/smtp-adapter.js";
|
|
36
48
|
|
|
37
49
|
interface ApplicationServicesInput {
|
|
38
50
|
persistence: ReturnType<typeof createServerPersistence>;
|
|
@@ -42,6 +54,7 @@ interface ApplicationServicesInput {
|
|
|
42
54
|
storage: ReturnType<typeof createStorageRuntime>;
|
|
43
55
|
moduleTasks: ScheduledTaskDefinition[];
|
|
44
56
|
bulkRetentionDays: number;
|
|
57
|
+
mailConfig?: PlatformConfig["mail"];
|
|
45
58
|
installModuleDurableHandlers?: (registry: DurableHandlerRegistry) => void;
|
|
46
59
|
}
|
|
47
60
|
|
|
@@ -52,6 +65,32 @@ export async function createApplicationServices(
|
|
|
52
65
|
const notifications = persistence
|
|
53
66
|
? new DrizzleNotificationRepository(persistence.db)
|
|
54
67
|
: new InMemoryNotificationRepository();
|
|
68
|
+
const notificationPreferences = persistence
|
|
69
|
+
? new DrizzleNotificationPreferenceRepository(persistence.db)
|
|
70
|
+
: new InMemoryNotificationPreferenceRepository();
|
|
71
|
+
const notificationDeliveries = persistence
|
|
72
|
+
? new DrizzleNotificationDeliveryRepository(persistence.db)
|
|
73
|
+
: new InMemoryNotificationDeliveryRepository();
|
|
74
|
+
const notificationRecipients = new SystemNotificationRecipientDirectory(
|
|
75
|
+
input.systemRepositories,
|
|
76
|
+
);
|
|
77
|
+
const notificationChannels = new NotificationChannelRegistry(
|
|
78
|
+
input.mailConfig?.enabled
|
|
79
|
+
? [new SmtpNotificationChannelAdapter(input.mailConfig)]
|
|
80
|
+
: [],
|
|
81
|
+
);
|
|
82
|
+
const notificationService = new NotificationService(
|
|
83
|
+
notifications,
|
|
84
|
+
notificationPreferences,
|
|
85
|
+
notificationDeliveries,
|
|
86
|
+
notificationRecipients,
|
|
87
|
+
{ mandatoryCategories: ["安全"] },
|
|
88
|
+
);
|
|
89
|
+
const notificationDeliveryWorker = new NotificationDeliveryWorker(
|
|
90
|
+
notificationDeliveries,
|
|
91
|
+
notificationRecipients,
|
|
92
|
+
notificationChannels,
|
|
93
|
+
);
|
|
55
94
|
const deferredTimeout = new DeferredWorkflowTimeoutScheduler();
|
|
56
95
|
const workflow = new WorkflowService(
|
|
57
96
|
persistence
|
|
@@ -89,6 +128,15 @@ export async function createApplicationServices(
|
|
|
89
128
|
const result = await governedExportDispatch.runNext?.();
|
|
90
129
|
return { claimed: Boolean(result && result.status !== "idle") };
|
|
91
130
|
},
|
|
131
|
+
notificationDispatch: async () => {
|
|
132
|
+
const scheduled = await notificationService.dispatchScheduled();
|
|
133
|
+
const delivery = await notificationDeliveryWorker.runNext();
|
|
134
|
+
return {
|
|
135
|
+
claimed: scheduled.claimed + delivery.claimed > 0,
|
|
136
|
+
scheduled,
|
|
137
|
+
delivery,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
92
140
|
}),
|
|
93
141
|
...input.moduleTasks,
|
|
94
142
|
];
|
|
@@ -149,6 +197,8 @@ export async function createApplicationServices(
|
|
|
149
197
|
return {
|
|
150
198
|
...services,
|
|
151
199
|
notifications,
|
|
200
|
+
notificationService,
|
|
201
|
+
notificationDeliveryWorker,
|
|
152
202
|
workflow,
|
|
153
203
|
governedExports,
|
|
154
204
|
governedExportService,
|
|
@@ -74,6 +74,7 @@ export class DrizzleAuditLogReader {
|
|
|
74
74
|
|
|
75
75
|
function auditWhere(query: AuditEventListQuery): SQL | undefined {
|
|
76
76
|
const clauses = [
|
|
77
|
+
query.q ? keywordClause(query.q) : undefined,
|
|
77
78
|
query.includeScheduler
|
|
78
79
|
? undefined
|
|
79
80
|
: ne(auditLogs.actorId, "system_scheduler"),
|
|
@@ -91,6 +92,30 @@ function auditWhere(query: AuditEventListQuery): SQL | undefined {
|
|
|
91
92
|
return clauses.length ? and(...clauses) : undefined;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
function keywordClause(query: string): SQL {
|
|
96
|
+
const pattern = `%${escapeLike(query.toLowerCase())}%`;
|
|
97
|
+
return sql`lower(concat_ws(' ',
|
|
98
|
+
${auditLogs.action},
|
|
99
|
+
${auditLogs.actorId},
|
|
100
|
+
${auditLogs.resource},
|
|
101
|
+
${auditLogs.resourceId},
|
|
102
|
+
${auditLogs.ip},
|
|
103
|
+
coalesce(${auditLogs.metadata} ->> 'actorName', ''),
|
|
104
|
+
coalesce(${auditLogs.metadata} ->> 'targetLabel', ''),
|
|
105
|
+
coalesce(${auditLogs.metadata} ->> 'message', ''),
|
|
106
|
+
coalesce(${auditLogs.metadata} ->> 'module', ''),
|
|
107
|
+
coalesce(${auditLogs.metadata} ->> 'requestId', ''),
|
|
108
|
+
coalesce(${auditLogs.metadata}::text, '')
|
|
109
|
+
)) like ${pattern} escape '\\'`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function escapeLike(value: string): string {
|
|
113
|
+
return value
|
|
114
|
+
.replaceAll("\\", "\\\\")
|
|
115
|
+
.replaceAll("%", "\\%")
|
|
116
|
+
.replaceAll("_", "\\_");
|
|
117
|
+
}
|
|
118
|
+
|
|
94
119
|
function rowToAuditEvent(row: typeof auditLogs.$inferSelect): AuditEvent {
|
|
95
120
|
const metadata = readMetadata(row.metadata);
|
|
96
121
|
const outcome = readString(metadata.outcome, "success") as AuditOutcome;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AuditEvent } from "@southwind-ai/shared";
|
|
2
2
|
import { randomUUIDv7 } from "bun";
|
|
3
|
-
import type
|
|
3
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
4
4
|
|
|
5
5
|
export function searchAuditMetadata(
|
|
6
6
|
query: string,
|
|
@@ -26,11 +26,7 @@ export function searchAuditEvent(
|
|
|
26
26
|
id: randomUUIDv7(),
|
|
27
27
|
action: "data.search",
|
|
28
28
|
outcome: "success",
|
|
29
|
-
actor:
|
|
30
|
-
id: context.actor.id,
|
|
31
|
-
type: context.actor.type === "anonymous" ? "user" : context.actor.type,
|
|
32
|
-
name: context.actor.name,
|
|
33
|
-
},
|
|
29
|
+
actor: auditActor(context),
|
|
34
30
|
target: { type: resourceName },
|
|
35
31
|
requestId: context.requestId,
|
|
36
32
|
metadata: searchAuditMetadata(query, matched),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import argon2, { argon2id } from "argon2";
|
|
2
|
-
import { PASSWORD_POLICY } from "@southwind-ai/shared";
|
|
2
|
+
import { PASSWORD_POLICY, type PasswordPolicy } from "@southwind-ai/shared";
|
|
3
3
|
|
|
4
4
|
export { PASSWORD_POLICY } from "@southwind-ai/shared";
|
|
5
5
|
|
|
@@ -15,30 +15,39 @@ export interface PasswordHasher {
|
|
|
15
15
|
verify(hash: string, password: string): Promise<boolean>;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export const argon2PasswordHasher
|
|
19
|
-
async hash(password) {
|
|
20
|
-
assertPasswordPolicy(password);
|
|
21
|
-
return argon2.hash(password, ARGON2_OPTIONS);
|
|
22
|
-
},
|
|
23
|
-
async verify(hash, password) {
|
|
24
|
-
if (!password || password.length > PASSWORD_POLICY.maxLength) {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
return await argon2.verify(hash, password);
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
};
|
|
18
|
+
export const argon2PasswordHasher = createArgon2PasswordHasher();
|
|
34
19
|
|
|
35
|
-
export function
|
|
20
|
+
export function createArgon2PasswordHasher(
|
|
21
|
+
policyProvider: () => PasswordPolicy = () => PASSWORD_POLICY,
|
|
22
|
+
): PasswordHasher {
|
|
23
|
+
return {
|
|
24
|
+
async hash(password) {
|
|
25
|
+
assertPasswordPolicy(password, policyProvider());
|
|
26
|
+
return argon2.hash(password, ARGON2_OPTIONS);
|
|
27
|
+
},
|
|
28
|
+
async verify(hash, password) {
|
|
29
|
+
if (!password || password.length > 512) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return await argon2.verify(hash, password);
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function assertPasswordPolicy(
|
|
42
|
+
password: string,
|
|
43
|
+
policy: PasswordPolicy = PASSWORD_POLICY,
|
|
44
|
+
): void {
|
|
36
45
|
if (
|
|
37
|
-
password.length <
|
|
38
|
-
password.length >
|
|
46
|
+
password.length < policy.minLength ||
|
|
47
|
+
password.length > policy.maxLength
|
|
39
48
|
) {
|
|
40
49
|
throw new Error(
|
|
41
|
-
`密码长度必须为 ${
|
|
50
|
+
`密码长度必须为 ${policy.minLength}-${policy.maxLength} 个字符`,
|
|
42
51
|
);
|
|
43
52
|
}
|
|
44
53
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
-
import type { AuditEvent } from "@southwind-ai/shared";
|
|
2
|
+
import type { AuditEvent, PasswordPolicy } from "@southwind-ai/shared";
|
|
3
3
|
import { Elysia } from "elysia";
|
|
4
4
|
import { createServerRuntime } from "../runtime.js";
|
|
5
5
|
import { RepositoryActorResolver } from "./actor-resolver.js";
|
|
@@ -15,6 +15,37 @@ beforeAll(async () => {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
describe("platform auth API", () => {
|
|
18
|
+
test("密码策略端点返回当前运行时策略且禁止缓存", async () => {
|
|
19
|
+
let policy: PasswordPolicy = { minLength: 16, maxLength: 64 };
|
|
20
|
+
const fixture = authFixture([activeUser()], () => policy);
|
|
21
|
+
|
|
22
|
+
const first = await fixture.app.handle(
|
|
23
|
+
new Request("http://localhost/api/auth/password/policy"),
|
|
24
|
+
);
|
|
25
|
+
expect(first.headers.get("cache-control")).toBe("no-store");
|
|
26
|
+
await expect(first.json()).resolves.toEqual(policy);
|
|
27
|
+
|
|
28
|
+
policy = { minLength: 20, maxLength: 80 };
|
|
29
|
+
const second = await fixture.app.handle(
|
|
30
|
+
new Request("http://localhost/api/auth/password/policy"),
|
|
31
|
+
);
|
|
32
|
+
await expect(second.json()).resolves.toEqual(policy);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("会话有效期配置即时影响后续签发会话", async () => {
|
|
36
|
+
let ttlSeconds = 60;
|
|
37
|
+
const fixture = authFixture([activeUser()], undefined, () => ttlSeconds);
|
|
38
|
+
const first = await login(fixture.app, "admin");
|
|
39
|
+
|
|
40
|
+
ttlSeconds = 120;
|
|
41
|
+
const second = await login(fixture.app, "admin");
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
new Date(second.expiresAt).getTime() -
|
|
45
|
+
new Date(first.expiresAt).getTime(),
|
|
46
|
+
).toBeGreaterThan(59_000);
|
|
47
|
+
});
|
|
48
|
+
|
|
18
49
|
test("会话探测只返回当前 Actor,不暴露 Token", async () => {
|
|
19
50
|
const fixture = authFixture([activeUser()]);
|
|
20
51
|
const anonymous = await fixture.app.handle(
|
|
@@ -278,7 +309,11 @@ describe("platform auth API", () => {
|
|
|
278
309
|
});
|
|
279
310
|
});
|
|
280
311
|
|
|
281
|
-
function authFixture(
|
|
312
|
+
function authFixture(
|
|
313
|
+
users: AuthUserRecord[],
|
|
314
|
+
passwordPolicy?: () => PasswordPolicy,
|
|
315
|
+
sessionTtlSeconds?: () => number,
|
|
316
|
+
) {
|
|
282
317
|
const repository = new InMemoryAuthRepository(users);
|
|
283
318
|
const resolver = new RepositoryActorResolver(repository, {
|
|
284
319
|
sessionCookieName: "windy.sid",
|
|
@@ -286,6 +321,8 @@ function authFixture(users: AuthUserRecord[]) {
|
|
|
286
321
|
const events: AuditEvent[] = [];
|
|
287
322
|
const runtime = createServerRuntime({}, { actorResolver: resolver });
|
|
288
323
|
const service = new AuthService(repository, {
|
|
324
|
+
passwordPolicy,
|
|
325
|
+
sessionTtlSeconds,
|
|
289
326
|
auditWriter: {
|
|
290
327
|
write(event) {
|
|
291
328
|
events.push(event);
|
|
@@ -326,7 +363,11 @@ async function login(
|
|
|
326
363
|
}),
|
|
327
364
|
);
|
|
328
365
|
expect(response.status).toBe(200);
|
|
329
|
-
return (await response.json()) as {
|
|
366
|
+
return (await response.json()) as {
|
|
367
|
+
token: string;
|
|
368
|
+
sessionId: string;
|
|
369
|
+
expiresAt: string;
|
|
370
|
+
};
|
|
330
371
|
}
|
|
331
372
|
|
|
332
373
|
function readCookieValue(header: string, name: string): string {
|