create-windy 0.3.17 → 0.3.19

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.
Files changed (58) hide show
  1. package/README.md +14 -36
  2. package/dist/cli.js +1534 -3079
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +6 -0
  6. package/template/README.md +5 -10
  7. package/template/apps/agent-server/Dockerfile +2 -1
  8. package/template/apps/agent-server/README.md +13 -5
  9. package/template/apps/agent-server/adk_web_agents/canonical_debug/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +125 -0
  11. package/template/apps/agent-server/project/adk-web-debug.json +4 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +348 -0
  13. package/template/apps/agent-server/tests/test_adk_debug_result_polling.py +139 -0
  14. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +235 -0
  15. package/template/apps/server/src/agent/run-debug-authorization.ts +27 -0
  16. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +56 -0
  17. package/template/apps/server/src/agent/run-facade.test-support.ts +235 -0
  18. package/template/apps/server/src/agent/run-facade.test.ts +8 -244
  19. package/template/apps/server/src/agent/run-facade.ts +23 -48
  20. package/template/apps/server/src/agent/run-operation-registry.ts +51 -0
  21. package/template/apps/server/src/agent/runtime-bootstrap.ts +5 -2
  22. package/template/apps/server/src/agent/tool-audit.ts +3 -0
  23. package/template/apps/server/src/agent/tool-registry.test.ts +29 -0
  24. package/template/apps/server/src/application-services.ts +83 -15
  25. package/template/apps/server/src/guards.ts +16 -0
  26. package/template/apps/server/src/index.ts +23 -20
  27. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +0 -1
  28. package/template/apps/server/src/jobs/legacy-adapter.test.ts +6 -0
  29. package/template/apps/server/src/jobs/legacy-service-runtime.ts +18 -0
  30. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +84 -0
  31. package/template/apps/server/src/module-host/durable-context.test.ts +14 -0
  32. package/template/apps/server/src/module-host/durable-context.ts +17 -15
  33. package/template/apps/server/src/module-host/request-context.test.ts +30 -1
  34. package/template/apps/server/src/module-host/request-context.ts +15 -2
  35. package/template/apps/server/src/module-host/storage-port.ts +1 -1
  36. package/template/apps/server/src/platform-bootstrap.ts +16 -0
  37. package/template/apps/server/src/runtime-adk-debug.test.ts +246 -0
  38. package/template/apps/server/src/runtime-adk-debug.ts +158 -0
  39. package/template/apps/server/src/runtime-request-actor.test.ts +55 -0
  40. package/template/apps/server/src/runtime-request-actor.ts +22 -0
  41. package/template/apps/server/src/runtime.test.ts +3 -0
  42. package/template/apps/server/src/runtime.ts +36 -3
  43. package/template/apps/server/src/scheduler/definitions.test.ts +16 -0
  44. package/template/docker-compose.agent-debug.yml +56 -0
  45. package/template/docker-compose.yml +1 -39
  46. package/template/docs/architecture/ai-runtime.md +14 -5
  47. package/template/docs/architecture/module-manifest-composition.md +14 -0
  48. package/template/docs/development/custom-login-page.md +2 -16
  49. package/template/docs/platform/agent-runtime.md +59 -16
  50. package/template/docs/platform/agent-tool-sandbox.md +4 -3
  51. package/template/docs/platform/platform-authentication.md +9 -1
  52. package/template/package.json +1 -1
  53. package/template/packages/database/drizzle/0043_activate_agent_dependencies.sql +196 -0
  54. package/template/packages/database/drizzle/meta/0043_snapshot.json +8335 -0
  55. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  56. package/template/packages/database/src/schema-notification-delivery.test.ts +15 -6
  57. package/template/packages/database/src/schema-workflow-notification.test.ts +44 -29
  58. package/template/scripts/dev-adk-web.ts +107 -0
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import httpx
6
+ import pytest
7
+
8
+ from southwind_agent_server.adk_debug import CanonicalRunDebugClient
9
+
10
+ OPERATION_KEY = "customer.copilot.global"
11
+
12
+
13
+ async def test_client_waits_for_result_after_stream_closes() -> None:
14
+ result_attempts = 0
15
+ started_runs = 0
16
+
17
+ async def handler(request: httpx.Request) -> httpx.Response:
18
+ nonlocal result_attempts, started_runs
19
+ if request.url.path == "/api/agent/runs":
20
+ started_runs += 1
21
+ return httpx.Response(200, json={"runId": "run_race"})
22
+ if request.url.path.endswith("/events"):
23
+ return httpx.Response(
24
+ 200,
25
+ content=(
26
+ b'event: run.started\ndata: {"sequence":1,"type":"run.started"}\n\n'
27
+ ),
28
+ )
29
+ if request.url.path.endswith("/result"):
30
+ result_attempts += 1
31
+ if result_attempts < 3:
32
+ return httpx.Response(
33
+ 409,
34
+ json={"code": "RUN_CONFLICT", "retryable": True},
35
+ )
36
+ return httpx.Response(
37
+ 200,
38
+ json={
39
+ "runId": "run_race",
40
+ "status": "completed",
41
+ "output": {"text": "稍后可读"},
42
+ },
43
+ )
44
+ raise AssertionError(request.url)
45
+
46
+ client = CanonicalRunDebugClient(
47
+ OPERATION_KEY,
48
+ "https://platform.test",
49
+ "developer-token",
50
+ transport=httpx.MockTransport(handler),
51
+ result_poll_interval_seconds=0.001,
52
+ )
53
+
54
+ result = await client.run("验证竞态")
55
+
56
+ assert result["answer"] == "稍后可读"
57
+ events = result["events"]
58
+ assert isinstance(events, list) and {"type": "run.completed"} in events
59
+ assert started_runs == 1
60
+ assert result_attempts == 3
61
+
62
+
63
+ @pytest.mark.parametrize(
64
+ ("payload", "timeout_seconds", "delay_seconds", "message"),
65
+ [
66
+ (
67
+ {
68
+ "code": "RUN_CANCELLED",
69
+ "message": "secret-upstream-body",
70
+ "retryable": False,
71
+ },
72
+ 5,
73
+ 0,
74
+ "永久失败",
75
+ ),
76
+ (
77
+ {
78
+ "code": "RUN_CONFLICT",
79
+ "message": "Agent Run 尚未完成",
80
+ "retryable": True,
81
+ },
82
+ 0.01,
83
+ 0,
84
+ "等待超时",
85
+ ),
86
+ (
87
+ {
88
+ "code": "RUN_CONFLICT",
89
+ "message": "slow-secret-upstream-body",
90
+ "retryable": True,
91
+ },
92
+ 0.01,
93
+ 0.05,
94
+ "等待超时",
95
+ ),
96
+ ],
97
+ )
98
+ async def test_client_bounds_result_polling_and_hides_error_content(
99
+ payload: dict[str, object],
100
+ timeout_seconds: float,
101
+ delay_seconds: float,
102
+ message: str,
103
+ ) -> None:
104
+ requests: list[httpx.Request] = []
105
+
106
+ async def handler(request: httpx.Request) -> httpx.Response:
107
+ requests.append(request)
108
+ if request.url.path == "/api/agent/runs":
109
+ return httpx.Response(200, json={"runId": "run_terminal"})
110
+ if request.url.path.endswith("/events"):
111
+ return httpx.Response(
112
+ 200,
113
+ content=(
114
+ b"event: run.completed\ndata: "
115
+ b'{"sequence":1,"type":"run.completed"}\n\n'
116
+ ),
117
+ )
118
+ if request.url.path.endswith("/result"):
119
+ await asyncio.sleep(delay_seconds)
120
+ return httpx.Response(409, json=payload)
121
+ raise AssertionError(request.url)
122
+
123
+ client = CanonicalRunDebugClient(
124
+ OPERATION_KEY,
125
+ "https://platform.test",
126
+ "developer-token",
127
+ transport=httpx.MockTransport(handler),
128
+ result_poll_timeout_seconds=timeout_seconds,
129
+ result_poll_interval_seconds=0.001,
130
+ )
131
+
132
+ with pytest.raises(RuntimeError, match=message) as failure:
133
+ await client.run("验证结果失败")
134
+
135
+ error_message = str(failure.value)
136
+ assert "secret-upstream-body" not in error_message
137
+ assert "slow-secret-upstream-body" not in error_message
138
+ assert sum(request.url.path == "/api/agent/runs" for request in requests) == 1
139
+ assert sum(request.url.path.endswith("/result") for request in requests) >= 1
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import httpx
7
+ import pytest
8
+ from google.adk.models.llm_request import LlmRequest
9
+ from google.genai import types
10
+
11
+ from adk_web_agents.canonical_debug.agent import (
12
+ TOOL_NAME,
13
+ CanonicalDebugLlm,
14
+ build_root_agent,
15
+ )
16
+ from southwind_agent_server.adk_debug import (
17
+ MAX_EVENT_FRAME_BYTES,
18
+ CanonicalRunDebugClient,
19
+ TextOperationDebugConfig,
20
+ compact_events,
21
+ load_text_operation_debug_config,
22
+ sse_events,
23
+ )
24
+
25
+ OPERATION = TextOperationDebugConfig("customer.copilot.global")
26
+
27
+
28
+ async def test_client_runs_project_operation_through_canonical_api() -> None:
29
+ requests: list[httpx.Request] = []
30
+
31
+ async def handler(request: httpx.Request) -> httpx.Response:
32
+ requests.append(request)
33
+ if request.url.path == "/api/agent/runs":
34
+ return httpx.Response(200, json={"runId": "run_debug"})
35
+ if request.url.path.endswith("/events"):
36
+ return httpx.Response(
37
+ 200,
38
+ content=(
39
+ b"event: tool.requested\ndata: "
40
+ b'{"sequence":1,"type":"tool.requested",'
41
+ b'"toolKey":"customer.records.search"}\n\n'
42
+ b"event: run.completed\ndata: "
43
+ b'{"sequence":2,"type":"run.completed"}\n\n'
44
+ ),
45
+ )
46
+ if request.url.path.endswith("/result"):
47
+ return httpx.Response(
48
+ 200,
49
+ json={
50
+ "runId": "run_debug",
51
+ "status": "completed",
52
+ "output": {"text": "真实回答"},
53
+ },
54
+ )
55
+ raise AssertionError(request.url)
56
+
57
+ client = CanonicalRunDebugClient(
58
+ OPERATION.operation_key,
59
+ "https://platform.test",
60
+ "developer-token",
61
+ transport=httpx.MockTransport(handler),
62
+ )
63
+
64
+ result = await client.run("请查询案件")
65
+
66
+ assert result == {
67
+ "runId": "run_debug",
68
+ "answer": "真实回答",
69
+ "events": [
70
+ {
71
+ "sequence": 1,
72
+ "type": "tool.requested",
73
+ "toolKey": "customer.records.search",
74
+ },
75
+ {"sequence": 2, "type": "run.completed"},
76
+ ],
77
+ }
78
+ assert requests[0].headers["authorization"] == "Bearer developer-token"
79
+ assert json.loads(requests[0].content) == {
80
+ "operationKey": "customer.copilot.global",
81
+ "input": {"question": "请查询案件"},
82
+ }
83
+
84
+
85
+ async def test_dispatcher_sends_user_text_to_canonical_tool() -> None:
86
+ model = CanonicalDebugLlm(OPERATION)
87
+ request = LlmRequest(
88
+ contents=[types.Content(role="user", parts=[types.Part(text="查测试数据")])]
89
+ )
90
+
91
+ responses = [response async for response in model.generate_content_async(request)]
92
+
93
+ response = responses[0]
94
+ assert response.content and response.content.parts
95
+ call = response.content.parts[0].function_call
96
+ assert call
97
+ assert call.name == TOOL_NAME
98
+ assert call.args == {"question": "查测试数据"}
99
+ assert build_root_agent(OPERATION).name == "canonical_debug"
100
+ assert len(build_root_agent(OPERATION).tools) == 1
101
+
102
+
103
+ async def test_dispatcher_renders_result_and_bounded_event_summary() -> None:
104
+ model = CanonicalDebugLlm(OPERATION)
105
+ request = LlmRequest(
106
+ contents=[
107
+ types.Content(
108
+ role="user",
109
+ parts=[
110
+ types.Part(
111
+ function_response=types.FunctionResponse(
112
+ name=TOOL_NAME,
113
+ response={
114
+ "runId": "run_debug",
115
+ "answer": "真实回答",
116
+ "events": [{"type": "run.completed"}],
117
+ },
118
+ )
119
+ )
120
+ ],
121
+ )
122
+ ]
123
+ )
124
+
125
+ responses = [response async for response in model.generate_content_async(request)]
126
+
127
+ response = responses[0]
128
+ assert response.content and response.content.parts
129
+ assert response.content.parts[0].text == (
130
+ '真实回答\n\n---\n调试 Run:run_debug\n事件摘要:[{"type": "run.completed"}]'
131
+ )
132
+
133
+
134
+ async def test_unconfigured_project_fails_closed_without_tool() -> None:
135
+ model = CanonicalDebugLlm(None)
136
+ request = LlmRequest(
137
+ contents=[types.Content(role="user", parts=[types.Part(text="查测试数据")])]
138
+ )
139
+
140
+ responses = [response async for response in model.generate_content_async(request)]
141
+
142
+ response = responses[0]
143
+ assert response.content and response.content.parts
144
+ assert "尚未" in (response.content.parts[0].text or "")
145
+ assert build_root_agent(None).tools == []
146
+
147
+
148
+ def test_project_config_selects_only_operation_key(tmp_path: Path) -> None:
149
+ path = tmp_path / "adk-web-debug.json"
150
+ path.write_text(
151
+ '{"schemaVersion":1,"textOperation":'
152
+ '{"key":"customer.copilot.global","dataActorId":"user_dev_admin"}}',
153
+ encoding="utf-8",
154
+ )
155
+
156
+ assert load_text_operation_debug_config(path) == TextOperationDebugConfig(
157
+ "customer.copilot.global", "user_dev_admin"
158
+ )
159
+
160
+
161
+ def test_environment_selects_operation_without_mount(
162
+ monkeypatch: pytest.MonkeyPatch,
163
+ ) -> None:
164
+ monkeypatch.setenv("ADK_DEBUG_OPERATION_KEY", "customer.copilot.global")
165
+
166
+ assert load_text_operation_debug_config() == OPERATION
167
+
168
+
169
+ def test_missing_project_config_is_unconfigured(tmp_path: Path) -> None:
170
+ assert load_text_operation_debug_config(tmp_path / "missing.json") is None
171
+
172
+
173
+ def test_project_config_rejects_unknown_fields(tmp_path: Path) -> None:
174
+ path = tmp_path / "adk-web-debug.json"
175
+ path.write_text(
176
+ '{"schemaVersion":1,"textOperation":null,"token":"forbidden"}',
177
+ encoding="utf-8",
178
+ )
179
+
180
+ with pytest.raises(RuntimeError, match="格式无效"):
181
+ load_text_operation_debug_config(path)
182
+
183
+
184
+ def test_client_rejects_missing_user_token(
185
+ monkeypatch: pytest.MonkeyPatch,
186
+ ) -> None:
187
+ monkeypatch.delenv("ADK_DEBUG_PLATFORM_TOKEN", raising=False)
188
+
189
+ with pytest.raises(RuntimeError, match="ADK_DEBUG_PLATFORM_TOKEN"):
190
+ CanonicalRunDebugClient.from_environment(OPERATION.operation_key)
191
+
192
+
193
+ def test_event_summary_compacts_streaming_output() -> None:
194
+ events = compact_events(
195
+ [
196
+ {"sequence": 1, "type": "run.started"},
197
+ {"sequence": 2, "type": "output.delta"},
198
+ {"sequence": 3, "type": "output.delta"},
199
+ {"sequence": 4, "type": "run.completed"},
200
+ ]
201
+ )
202
+
203
+ assert events == [
204
+ {"sequence": 1, "type": "run.started"},
205
+ {"sequence": 4, "type": "run.completed"},
206
+ {"type": "output.delta", "count": 2},
207
+ ]
208
+
209
+
210
+ async def test_event_stream_rejects_oversized_frame() -> None:
211
+ response = httpx.Response(
212
+ 200,
213
+ content=b"event: output.delta\ndata: " + b"x" * MAX_EVENT_FRAME_BYTES,
214
+ )
215
+
216
+ with pytest.raises(RuntimeError, match="超过调试上限"):
217
+ _ = [event async for event in sse_events(response)]
218
+
219
+
220
+ def test_event_summary_never_exceeds_visible_limit() -> None:
221
+ events = compact_events(
222
+ [
223
+ *[
224
+ {"sequence": sequence, "type": "tool.completed"}
225
+ for sequence in range(100)
226
+ ],
227
+ {"sequence": 101, "type": "output.delta"},
228
+ ]
229
+ )
230
+
231
+ assert len(events) == 64
232
+ assert events[-2:] == [
233
+ {"type": "output.delta", "count": 1},
234
+ {"type": "events.truncated", "count": 38},
235
+ ]
@@ -0,0 +1,27 @@
1
+ import { projectedModuleUserId, type RequestActor } from "../guards.js";
2
+ import { AgentRunFacadeError, inaccessibleRun } from "./run-errors.js";
3
+
4
+ interface AgentDebugOperationRuntime {
5
+ agentDebugOperationForActor(actorId: string): string | undefined;
6
+ }
7
+
8
+ export function assertSelectedDebugOperation(
9
+ runtime: AgentDebugOperationRuntime,
10
+ actorId: string,
11
+ operationKey: string,
12
+ disclosure: "forbidden" | "hidden",
13
+ ) {
14
+ const selected = runtime.agentDebugOperationForActor(actorId);
15
+ if (!selected || selected === operationKey) return;
16
+ if (disclosure === "hidden") throw inaccessibleRun();
17
+ throw new AgentRunFacadeError(
18
+ "FORBIDDEN",
19
+ "ADK Web 调试身份只能执行项目选定的 Agent Operation",
20
+ 403,
21
+ );
22
+ }
23
+
24
+ export function debugActorAuditMetadata(actor: RequestActor) {
25
+ const userId = projectedModuleUserId(actor);
26
+ return userId ? { debugDataActorId: userId } : {};
27
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { AgentRunFacade } from "./run-facade.js";
3
+ import {
4
+ createRunFacadeFixture,
5
+ runFacadeManifest,
6
+ } from "./run-facade.test-support.js";
7
+ import { createTestRunAuthorizationStore } from "./run-authorization.test-support.js";
8
+ import { debugActorAuditMetadata } from "./run-debug-authorization.js";
9
+ import { bindModuleUserProjection } from "../guards.js";
10
+
11
+ describe("ADK 调试 Operation 边界", () => {
12
+ test("专用身份不能切换到未选定的 Agent Operation", async () => {
13
+ const fixture = await createRunFacadeFixture();
14
+ const runtime = Object.assign(fixture.runtime, {
15
+ agentDebugOperationForActor(actorId: string) {
16
+ return actorId === "actor-1" ? "business.other" : undefined;
17
+ },
18
+ });
19
+ const facade = new AgentRunFacade(
20
+ [runFacadeManifest()],
21
+ fixture.hosts,
22
+ runtime,
23
+ fixture.remote,
24
+ createTestRunAuthorizationStore(),
25
+ );
26
+
27
+ await expect(
28
+ facade.start(
29
+ "business.answer",
30
+ { question: "越权切换" },
31
+ "request-debug",
32
+ fixture.context,
33
+ ),
34
+ ).rejects.toMatchObject({ code: "FORBIDDEN", status: 403 });
35
+ await expect(
36
+ facade.authorizeRun("business.answer", "run_1", fixture.context),
37
+ ).rejects.toMatchObject({ code: "RUN_CONFLICT", status: 404 });
38
+ });
39
+
40
+ test("审计显式记录业务数据用户但不替换核心调试主体", () => {
41
+ expect(
42
+ debugActorAuditMetadata(
43
+ bindModuleUserProjection(
44
+ {
45
+ id: "service_adk_debug",
46
+ type: "service",
47
+ name: "ADK 开发调试",
48
+ roleCodes: ["adk-debug"],
49
+ permissionKeys: ["business.answer.execute"],
50
+ },
51
+ "user_dev_admin",
52
+ ),
53
+ ),
54
+ ).toEqual({ debugDataActorId: "user_dev_admin" });
55
+ });
56
+ });
@@ -0,0 +1,235 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import { featureKey } from "@southwind-ai/shared";
3
+ import { buildScopeContext } from "../data-access/context.js";
4
+ import { createRequestGuardContext } from "../guards.js";
5
+ import { initializeModuleHosts } from "../module-host.js";
6
+ import { createServerRuntime } from "../runtime.js";
7
+ import { FakeAgentRuntime } from "./fake-runtime.test-support.js";
8
+ import { AgentRunFacade } from "./run-facade.js";
9
+ import { createTestRunAuthorizationStore } from "./run-authorization.test-support.js";
10
+
11
+ export async function createRunFacadeFixture(
12
+ permissionKeys: string[] = ["business.ai.execute", "business.records.read"],
13
+ ) {
14
+ const module = runFacadeManifest();
15
+ const prepared: unknown[] = [];
16
+ const hosts = await initializeModuleHosts({
17
+ modules: [module],
18
+ installations: [
19
+ {
20
+ manifest: module,
21
+ setup(host) {
22
+ host.registerAiOperationHandlers([
23
+ {
24
+ operationKey: "business.answer",
25
+ release: {
26
+ workflowVersion: "workflow-2",
27
+ promptVersion: "prompt-3",
28
+ routeVersion: "route-4",
29
+ },
30
+ prepare(input, context) {
31
+ prepared.push({ input, actorId: context.actor.id });
32
+ const question =
33
+ typeof input === "object" &&
34
+ input !== null &&
35
+ !Array.isArray(input) &&
36
+ typeof input.question === "string"
37
+ ? input.question
38
+ : "";
39
+ return {
40
+ instructions: "只回答当前业务问题。",
41
+ userPrompt: question,
42
+ };
43
+ },
44
+ },
45
+ ]);
46
+ },
47
+ },
48
+ ],
49
+ });
50
+ const runtime = createServerRuntime(
51
+ {},
52
+ {
53
+ featureCatalog: module.features,
54
+ licenseResolver: { resolve: () => ({ status: "valid" }) },
55
+ },
56
+ );
57
+ const context = guardContext(permissionKeys);
58
+ const remote = new FakeAgentRuntime();
59
+ const facade = new AgentRunFacade(
60
+ [module],
61
+ hosts,
62
+ runtime,
63
+ remote,
64
+ createTestRunAuthorizationStore(),
65
+ );
66
+ return { context, facade, hosts, prepared, remote, runtime };
67
+ }
68
+
69
+ export function guardContext(permissionKeys: string[]) {
70
+ return createRequestGuardContext(
71
+ new Request("http://localhost/api/agent/runs"),
72
+ {
73
+ resolveActor: () => ({
74
+ id: "actor-1",
75
+ type: "user",
76
+ name: "测试用户",
77
+ roleCodes: ["user"],
78
+ permissionKeys,
79
+ scopeContext: buildScopeContext("actor-1", undefined, [
80
+ {
81
+ id: "role-user",
82
+ code: "user",
83
+ dataScope: "self",
84
+ departmentIds: [],
85
+ },
86
+ ]),
87
+ }),
88
+ resolveLicense: () => ({ status: "valid" }),
89
+ },
90
+ );
91
+ }
92
+
93
+ export function runFacadeManifest(): ModuleManifest {
94
+ return {
95
+ name: "business",
96
+ version: "0.1.0",
97
+ label: "业务",
98
+ description: "业务",
99
+ menus: [],
100
+ adminRoutes: [],
101
+ permissions: [
102
+ permission("business.ai.execute", "ai", "manage"),
103
+ permission("business.records.read", "records", "read"),
104
+ ],
105
+ apiPermissions: [],
106
+ features: [
107
+ enabledFeature("business.ai"),
108
+ enabledFeature("business.records"),
109
+ ],
110
+ migrations: [],
111
+ schemaExports: [],
112
+ jsonSchemas: [
113
+ {
114
+ key: "business.records.input.v1",
115
+ schema: { type: "object" },
116
+ },
117
+ {
118
+ key: "business.records.output.v1",
119
+ schema: { type: "object" },
120
+ },
121
+ { key: "business.input.v1", schema: { type: "object" } },
122
+ { key: "business.output.v1", schema: { type: "object" } },
123
+ ],
124
+ tasks: [],
125
+ tools: [
126
+ {
127
+ key: "business.records.list",
128
+ label: "查询记录",
129
+ description: "查询当前范围记录。",
130
+ inputSchemaKey: "business.records.input.v1",
131
+ outputSchemaKey: "business.records.output.v1",
132
+ operation: "query",
133
+ permissionKey: "business.records.read",
134
+ featureKey: "business.records",
135
+ auditAction: "business.records.read",
136
+ scopeMode: "request",
137
+ dataClassification: "internal",
138
+ idempotency: "none",
139
+ approval: "none",
140
+ limits: {
141
+ maxDurationSeconds: 10,
142
+ maxInputBytes: 4096,
143
+ maxOutputBytes: 4096,
144
+ },
145
+ },
146
+ ],
147
+ aiOperations: [agentOperation()],
148
+ providers: [],
149
+ auditActions: ["business.ai.execute", "business.records.read"],
150
+ };
151
+ }
152
+
153
+ function agentOperation() {
154
+ return {
155
+ key: "business.answer",
156
+ kind: "agent" as const,
157
+ featureKey: "business.ai",
158
+ permissionKey: "business.ai.execute",
159
+ auditAction: "business.ai.execute" as const,
160
+ inputSchemaKey: "business.input.v1",
161
+ outputSchemaKey: "business.output.v1",
162
+ requiredCapabilities: ["text-generation"],
163
+ allowedToolKeys: ["business.records.list"],
164
+ dataClassification: "internal" as const,
165
+ execution: "streaming" as const,
166
+ providerEgress: {
167
+ mode: "configured-endpoint" as const,
168
+ deploymentKey: "ai.openai-compatible",
169
+ },
170
+ providerDataPolicy: {
171
+ inputFields: [
172
+ {
173
+ jsonPointer: "/question",
174
+ classification: "internal" as const,
175
+ dlpAction: "redact" as const,
176
+ },
177
+ ],
178
+ envelopeFields: [
179
+ fieldPolicy("instructions", "reject"),
180
+ fieldPolicy("user-prompt", "redact"),
181
+ fieldPolicy("tool-definitions", "reject"),
182
+ fieldPolicy("provider-output", "redact"),
183
+ {
184
+ field: "tool-results" as const,
185
+ classification: "sensitive" as const,
186
+ dlpAction: "redact" as const,
187
+ },
188
+ ],
189
+ },
190
+ toolNetworkPolicy: { mode: "forbidden" as const },
191
+ limits: {
192
+ maxDurationSeconds: 60,
193
+ maxInputBytes: 4096,
194
+ maxOutputBytes: 4096,
195
+ },
196
+ };
197
+ }
198
+
199
+ function fieldPolicy(
200
+ field:
201
+ | "instructions"
202
+ | "user-prompt"
203
+ | "tool-definitions"
204
+ | "provider-output",
205
+ dlpAction: "reject" | "redact",
206
+ ) {
207
+ return {
208
+ field,
209
+ classification: "internal" as const,
210
+ dlpAction,
211
+ };
212
+ }
213
+
214
+ function permission(key: string, resource: string, action: "read" | "manage") {
215
+ return {
216
+ key,
217
+ label: key,
218
+ module: "business",
219
+ moduleLabel: "业务",
220
+ resource,
221
+ resourceLabel: resource,
222
+ action,
223
+ };
224
+ }
225
+
226
+ function enabledFeature(key: string) {
227
+ return {
228
+ key: featureKey(key),
229
+ label: key,
230
+ module: "business",
231
+ visible: "visible" as const,
232
+ enabled: true,
233
+ stage: "stable" as const,
234
+ };
235
+ }