create-windy 0.4.1 → 0.5.1

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 (174) hide show
  1. package/dist/cli.js +292 -125
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/system/operations.test.ts +54 -0
  96. package/template/apps/server/src/system/operations.ts +23 -1
  97. package/template/apps/server/src/system/seed.test.ts +4 -0
  98. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  99. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  100. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  101. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  102. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  103. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  104. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  105. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  106. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  107. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  108. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  109. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  110. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  111. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  112. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  113. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  114. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  115. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  116. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  117. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  118. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  119. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  120. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  121. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  122. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  123. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  124. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  125. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  126. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  127. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  128. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  129. package/template/apps/web/src/router/error-navigation.ts +22 -0
  130. package/template/apps/web/src/router/index.ts +12 -23
  131. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  132. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  133. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  134. package/template/apps/web/src/services/operations-api.ts +28 -0
  135. package/template/docker-compose.agent-debug.yml +1 -1
  136. package/template/docs/architecture/ai-runtime.md +2 -1
  137. package/template/docs/architecture/module-host.md +3 -0
  138. package/template/docs/platform/agent-run-attestation.md +65 -0
  139. package/template/docs/platform/agent-runtime-observability.md +135 -0
  140. package/template/docs/platform/agent-runtime.md +47 -3
  141. package/template/docs/platform/operations-overview.md +13 -0
  142. package/template/docs/platform/platform-authentication.md +4 -3
  143. package/template/packages/config/package.json +1 -1
  144. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  145. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  146. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  147. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  148. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  149. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  150. package/template/packages/database/drizzle/meta/_journal.json +21 -0
  151. package/template/packages/database/package.json +1 -1
  152. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  153. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  154. package/template/packages/jobs/package.json +1 -1
  155. package/template/packages/modules/package.json +1 -1
  156. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  157. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  158. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  159. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  160. package/template/packages/modules/src/system-features.ts +8 -0
  161. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  162. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  163. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  164. package/template/packages/modules/src/system-modules.ts +13 -0
  165. package/template/packages/modules/src/system-permissions.ts +2 -0
  166. package/template/packages/server-sdk/index.ts +11 -0
  167. package/template/packages/server-sdk/package.json +2 -1
  168. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  169. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  170. package/template/packages/server-sdk/src/module-host.ts +5 -0
  171. package/template/packages/shared/index.ts +3 -0
  172. package/template/packages/shared/package.json +1 -1
  173. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  174. package/template/scripts/lib/postgres-regression.ts +201 -0
@@ -0,0 +1,335 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import {
4
+ MODULE_DEBUG_SESSION_SCHEMA_VERSION,
5
+ type ModuleDebugSessionProviderV1,
6
+ } from "@southwind-ai/server-sdk";
7
+ import { featureKey } from "@southwind-ai/shared";
8
+ import {
9
+ bindModuleUserProjection,
10
+ createRequestGuardContext,
11
+ type RequestActor,
12
+ } from "../guards.js";
13
+ import { initializeModuleHosts } from "../module-host.js";
14
+ import { manifest } from "../module-host/test-fixtures.js";
15
+ import { createServerRuntime } from "../runtime.js";
16
+ import {
17
+ DebugSessionFacadeError,
18
+ DebugSessionFacadeV1,
19
+ } from "./debug-session-facade.js";
20
+ import { DebugSessionIdCodecV1 } from "./debug-session-id.js";
21
+
22
+ const OPERATION_KEY = "known.copilot.global";
23
+ const SOURCE_ID = "conversation-private-42";
24
+
25
+ describe("ADK 调试会话 V1 facade", () => {
26
+ test("投影真实业务用户并只返回不透明 ID 与有界通用 DTO", async () => {
27
+ const seenActors: string[] = [];
28
+ const fixture = await createFixture({
29
+ list: async ({ actor }) => {
30
+ seenActors.push(actor.id);
31
+ return { sessions: [summary()], truncated: false };
32
+ },
33
+ get: async ({ actor }) => {
34
+ seenActors.push(actor.id);
35
+ return {
36
+ ...summary(),
37
+ messages: [
38
+ {
39
+ sequence: 1,
40
+ role: "user",
41
+ content: "历史问题",
42
+ createdAt: "2026-07-31T01:00:00.000Z",
43
+ },
44
+ {
45
+ sequence: 2,
46
+ role: "assistant",
47
+ content: "历史回答",
48
+ createdAt: "2026-07-31T01:00:01.000Z",
49
+ },
50
+ ],
51
+ };
52
+ },
53
+ });
54
+
55
+ const listed = await fixture.facade.list(fixture.context, signal());
56
+ const id = listed.sessions[0]?.id;
57
+ expect(id).toMatch(/^wdbg1_/);
58
+ expect(JSON.stringify(listed)).not.toContain(SOURCE_ID);
59
+ expect(listed.sessions[0]).toMatchObject({
60
+ title: "历史研判",
61
+ scopeLabel: "测试范围",
62
+ revision: "2",
63
+ });
64
+
65
+ const detail = await fixture.facade.get(
66
+ id ?? "",
67
+ fixture.context,
68
+ signal(),
69
+ );
70
+ expect(detail.session.messages.map(({ role }) => role)).toEqual([
71
+ "user",
72
+ "assistant",
73
+ ]);
74
+ expect(seenActors).toEqual([
75
+ "user_dev_admin",
76
+ "user_dev_admin",
77
+ "user_dev_admin",
78
+ ]);
79
+ const audits = fixture.runtime.auditSink
80
+ .list()
81
+ .filter(({ action }) => action === "known.copilot.history.read");
82
+ expect(audits).toHaveLength(2);
83
+ expect(audits[0]?.metadata).toMatchObject({
84
+ debugDataActorId: "user_dev_admin",
85
+ mode: "list",
86
+ });
87
+ });
88
+
89
+ test("每次详情读取重新 list 授权,未知和非法 ID 同形安全失败", async () => {
90
+ let visible = true;
91
+ const fixture = await createFixture({
92
+ list: async () => ({
93
+ sessions: visible ? [summary()] : [],
94
+ truncated: false,
95
+ }),
96
+ get: async () => ({ ...summary(), messages: [] }),
97
+ });
98
+ const listed = await fixture.facade.list(fixture.context, signal());
99
+ visible = false;
100
+
101
+ await expect(
102
+ fixture.facade.get(
103
+ listed.sessions[0]?.id ?? "",
104
+ fixture.context,
105
+ signal(),
106
+ ),
107
+ ).rejects.toMatchObject({
108
+ code: "SESSION_NOT_FOUND",
109
+ status: 404,
110
+ });
111
+ await expect(
112
+ fixture.facade.get(SOURCE_ID, fixture.context, signal()),
113
+ ).rejects.toMatchObject({
114
+ code: "INVALID_SESSION_ID",
115
+ status: 400,
116
+ });
117
+ });
118
+
119
+ test("分页读完超过 100 条会话且旧会话详情不会被新会话挤出", async () => {
120
+ const values = Array.from({ length: 101 }, (_, index) => ({
121
+ sourceId: `conversation-${index}`,
122
+ title: `历史研判 ${index}`,
123
+ scopeLabel: "测试范围",
124
+ updatedAt: new Date(Date.UTC(2026, 6, 31, 1, 0, index)).toISOString(),
125
+ revision: "1",
126
+ }));
127
+ const fixture = await createFixture({
128
+ list: async ({ limit, cursor }) => {
129
+ const offset = Number(cursor ?? 0);
130
+ const sessions = values.slice(offset, offset + limit);
131
+ const nextOffset = offset + sessions.length;
132
+ const truncated = nextOffset < values.length;
133
+ return {
134
+ sessions,
135
+ truncated,
136
+ ...(truncated ? { next: String(nextOffset) } : {}),
137
+ };
138
+ },
139
+ get: async ({ sourceId }) => {
140
+ const value = values.find((item) => item.sourceId === sourceId);
141
+ return value ? { ...value, messages: [] } : undefined;
142
+ },
143
+ });
144
+
145
+ const listed = await fixture.facade.list(fixture.context, signal());
146
+ expect(listed.sessions).toHaveLength(101);
147
+ expect(listed.truncated).toBe(false);
148
+ const oldest = listed.sessions.find(({ title }) => title === "历史研判 0");
149
+ expect(oldest).toBeDefined();
150
+ await expect(
151
+ fixture.facade.get(oldest?.id ?? "", fixture.context, signal()),
152
+ ).resolves.toMatchObject({
153
+ session: { title: "历史研判 0" },
154
+ });
155
+ });
156
+
157
+ test("非调试身份、缺少权限与 Provider 越界响应均 fail closed", async () => {
158
+ const fixture = await createFixture({
159
+ list: async () => ({ sessions: [summary()], truncated: false }),
160
+ get: async () => ({
161
+ ...summary(),
162
+ messages: Array.from({ length: 65 }, (_, sequence) => ({
163
+ sequence,
164
+ role: "assistant" as const,
165
+ content: "x",
166
+ createdAt: "2026-07-31T01:00:01.000Z",
167
+ })),
168
+ }),
169
+ });
170
+ const listed = await fixture.facade.list(fixture.context, signal());
171
+
172
+ await expect(
173
+ fixture.facade.get(
174
+ listed.sessions[0]?.id ?? "",
175
+ fixture.context,
176
+ signal(),
177
+ ),
178
+ ).rejects.toMatchObject({ code: "PROVIDER_UNAVAILABLE", status: 503 });
179
+
180
+ const forbidden = await createFixture(
181
+ {
182
+ list: async () => ({ sessions: [], truncated: false }),
183
+ get: async () => undefined,
184
+ },
185
+ [],
186
+ );
187
+ await expect(
188
+ forbidden.facade.list(forbidden.context, signal()),
189
+ ).rejects.toBeInstanceOf(DebugSessionFacadeError);
190
+
191
+ const ordinaryContext = createContext(
192
+ { ...actor(), id: "user_ordinary", type: "user" },
193
+ false,
194
+ );
195
+ await expect(
196
+ fixture.facade.list(ordinaryContext, signal()),
197
+ ).rejects.toMatchObject({ code: "FORBIDDEN", status: 403 });
198
+ });
199
+ });
200
+
201
+ async function createFixture(
202
+ methods: Pick<ModuleDebugSessionProviderV1, "list" | "get">,
203
+ permissionKeys = ["known.copilot.execute"],
204
+ ) {
205
+ const module = debugManifest();
206
+ const provider: ModuleDebugSessionProviderV1 = {
207
+ schemaVersion: MODULE_DEBUG_SESSION_SCHEMA_VERSION,
208
+ key: "known.copilot.history",
209
+ operationKeys: [OPERATION_KEY],
210
+ featureKey: "known.copilot",
211
+ permissionKey: "known.copilot.execute",
212
+ auditAction: "known.copilot.history.read",
213
+ ...methods,
214
+ };
215
+ const hosts = await initializeModuleHosts({
216
+ modules: [module],
217
+ installations: [
218
+ {
219
+ manifest: module,
220
+ setup(host) {
221
+ host.registerDebugSessionProviders([provider]);
222
+ },
223
+ },
224
+ ],
225
+ });
226
+ const base = createServerRuntime(
227
+ {},
228
+ {
229
+ featureCatalog: module.features,
230
+ licenseResolver: { resolve: () => ({ status: "valid" }) },
231
+ },
232
+ );
233
+ const runtime = {
234
+ ...base,
235
+ agentDebugOperationForActor: (actorId: string) =>
236
+ actorId === "service_adk_debug" ? OPERATION_KEY : undefined,
237
+ };
238
+ const context = createContext(actor(permissionKeys), true);
239
+ return {
240
+ context,
241
+ facade: new DebugSessionFacadeV1(
242
+ hosts,
243
+ runtime,
244
+ new DebugSessionIdCodecV1("test-secret-012345678901234567890123456789"),
245
+ ),
246
+ runtime,
247
+ };
248
+ }
249
+
250
+ function createContext(value: RequestActor, projected: boolean) {
251
+ const actorValue = projected
252
+ ? bindModuleUserProjection(value, "user_dev_admin")
253
+ : value;
254
+ return createRequestGuardContext(
255
+ new Request("http://localhost/api/agent/debug/sessions/v1"),
256
+ {
257
+ resolveActor: () => actorValue,
258
+ resolveLicense: () => ({ status: "valid" }),
259
+ licenseEnforced: false,
260
+ },
261
+ );
262
+ }
263
+
264
+ function actor(
265
+ permissionKeys: string[] = ["known.copilot.execute"],
266
+ ): RequestActor {
267
+ return {
268
+ id: "service_adk_debug",
269
+ type: "service",
270
+ name: "ADK Debug",
271
+ roleCodes: ["adk-debug"],
272
+ permissionKeys,
273
+ };
274
+ }
275
+
276
+ function summary() {
277
+ return {
278
+ sourceId: SOURCE_ID,
279
+ title: "历史研判",
280
+ scopeLabel: "测试范围",
281
+ updatedAt: "2026-07-31T01:00:01.000Z",
282
+ revision: "2",
283
+ };
284
+ }
285
+
286
+ function signal(): AbortSignal {
287
+ return new AbortController().signal;
288
+ }
289
+
290
+ function debugManifest(): ModuleManifest {
291
+ return manifest("known", {
292
+ features: [
293
+ {
294
+ key: featureKey("known.copilot"),
295
+ label: "Copilot",
296
+ module: "known",
297
+ visible: "visible",
298
+ enabled: true,
299
+ stage: "stable",
300
+ },
301
+ ],
302
+ permissions: [
303
+ {
304
+ key: "known.copilot.execute",
305
+ label: "使用 Copilot",
306
+ module: "known",
307
+ moduleLabel: "Known",
308
+ resource: "copilot",
309
+ resourceLabel: "Copilot",
310
+ action: "execute",
311
+ },
312
+ ],
313
+ aiOperations: [
314
+ {
315
+ key: OPERATION_KEY,
316
+ kind: "agent",
317
+ featureKey: "known.copilot",
318
+ permissionKey: "known.copilot.execute",
319
+ auditAction: "known.copilot.execute",
320
+ inputSchemaKey: "known.copilot.input.v1",
321
+ outputSchemaKey: "known.copilot.output.v1",
322
+ requiredCapabilities: ["text-generation"],
323
+ allowedToolKeys: [],
324
+ dataClassification: "internal",
325
+ execution: "streaming",
326
+ limits: {
327
+ maxDurationSeconds: 60,
328
+ maxInputBytes: 4096,
329
+ maxOutputBytes: 4096,
330
+ },
331
+ },
332
+ ],
333
+ auditActions: ["known.copilot.execute", "known.copilot.history.read"],
334
+ });
335
+ }
@@ -0,0 +1,302 @@
1
+ import { randomUUIDv7 } from "bun";
2
+ import {
3
+ MODULE_DEBUG_SESSION_SCHEMA_VERSION,
4
+ type ModuleDebugSessionSummaryV1,
5
+ } from "@southwind-ai/server-sdk";
6
+ import { evaluateAccessGuards } from "../access-guards.js";
7
+ import { auditActor, type RequestGuardContext } from "../guards.js";
8
+ import type {
9
+ InitializedModuleHosts,
10
+ RegisteredModuleDebugSessionProvider,
11
+ } from "../module-host.js";
12
+ import { toModuleActor } from "../module-host.js";
13
+ import type { createServerRuntime } from "../runtime.js";
14
+ import { debugActorAuditMetadata } from "./run-debug-authorization.js";
15
+ import { DebugSessionIdCodecV1 } from "./debug-session-id.js";
16
+ import { readAllDebugSessionSummaries } from "./debug-session-provider-reader.js";
17
+ import {
18
+ DebugSessionProviderContractError,
19
+ validateDebugSessionDetail,
20
+ } from "./debug-session-validation.js";
21
+
22
+ type ServerRuntime = Pick<
23
+ ReturnType<typeof createServerRuntime>,
24
+ | "agentDebugOperationForActor"
25
+ | "auditSink"
26
+ | "recordDecisions"
27
+ | "resolveFeature"
28
+ >;
29
+
30
+ export interface DebugSessionSummaryDtoV1 {
31
+ readonly id: string;
32
+ readonly title: string;
33
+ readonly scopeLabel?: string;
34
+ readonly updatedAt: string;
35
+ readonly revision: string;
36
+ }
37
+
38
+ export interface DebugSessionDetailDtoV1 extends DebugSessionSummaryDtoV1 {
39
+ readonly messages: readonly {
40
+ sequence: number;
41
+ role: "user" | "assistant";
42
+ content: string;
43
+ createdAt: string;
44
+ }[];
45
+ }
46
+
47
+ export class DebugSessionFacadeError extends Error {
48
+ constructor(
49
+ readonly code:
50
+ | "FORBIDDEN"
51
+ | "INVALID_SESSION_ID"
52
+ | "SESSION_NOT_FOUND"
53
+ | "PROVIDER_INVALID"
54
+ | "PROVIDER_UNAVAILABLE",
55
+ message: string,
56
+ readonly status: number,
57
+ ) {
58
+ super(message);
59
+ }
60
+ }
61
+
62
+ export class DebugSessionFacadeV1 {
63
+ constructor(
64
+ private readonly hosts: InitializedModuleHosts,
65
+ private readonly runtime: ServerRuntime,
66
+ private readonly ids: DebugSessionIdCodecV1,
67
+ ) {}
68
+
69
+ async list(context: RequestGuardContext, signal: AbortSignal) {
70
+ const authorized = await this.authorizedProviders(context);
71
+ const actor = toModuleActor(context.actor);
72
+ const results = await Promise.all(
73
+ authorized.map(async (entry) => {
74
+ try {
75
+ const sessions = await readAllDebugSessionSummaries({
76
+ provider: entry.provider,
77
+ actor,
78
+ operationKey: entry.operationKey,
79
+ signal,
80
+ });
81
+ this.audit(entry, context, "list", undefined, sessions.length);
82
+ return { ok: true as const, entry, sessions };
83
+ } catch (error) {
84
+ this.auditFailure(entry, context, "list", "list", error);
85
+ return { ok: false as const };
86
+ }
87
+ }),
88
+ );
89
+ if (results.some((result) => !result.ok)) throw providerUnavailable();
90
+ const projected = results
91
+ .flatMap((result) => (result.ok ? [result] : []))
92
+ .flatMap(({ entry, sessions }) =>
93
+ sessions.map((session) => this.project(entry, actor.id, session)),
94
+ )
95
+ .sort(
96
+ (left, right) =>
97
+ Date.parse(right.updatedAt) - Date.parse(left.updatedAt),
98
+ );
99
+ return {
100
+ schemaVersion: MODULE_DEBUG_SESSION_SCHEMA_VERSION,
101
+ sessions: projected,
102
+ truncated: false,
103
+ };
104
+ }
105
+
106
+ async get(
107
+ sessionId: string,
108
+ context: RequestGuardContext,
109
+ signal: AbortSignal,
110
+ ) {
111
+ if (!this.ids.isWellFormed(sessionId)) {
112
+ throw new DebugSessionFacadeError(
113
+ "INVALID_SESSION_ID",
114
+ "调试会话 ID 无效",
115
+ 400,
116
+ );
117
+ }
118
+ const authorized = await this.authorizedProviders(context);
119
+ const actor = toModuleActor(context.actor);
120
+ let providerFailed = false;
121
+ for (const entry of authorized) {
122
+ let summaries: readonly ModuleDebugSessionSummaryV1[];
123
+ try {
124
+ summaries = await readAllDebugSessionSummaries({
125
+ provider: entry.provider,
126
+ actor,
127
+ operationKey: entry.operationKey,
128
+ signal,
129
+ });
130
+ } catch (error) {
131
+ providerFailed = true;
132
+ this.auditFailure(entry, context, "get", "list", error);
133
+ continue;
134
+ }
135
+ for (const summary of summaries) {
136
+ const projected = this.project(entry, actor.id, summary);
137
+ if (projected.id !== sessionId) continue;
138
+ try {
139
+ const detail = await entry.provider.get({
140
+ actor,
141
+ operationKey: entry.operationKey,
142
+ sourceId: summary.sourceId,
143
+ signal,
144
+ });
145
+ if (!detail) break;
146
+ const validated = validateDebugSessionDetail(
147
+ detail,
148
+ summary.sourceId,
149
+ );
150
+ this.audit(
151
+ entry,
152
+ context,
153
+ "get",
154
+ sessionId,
155
+ validated.messages.length,
156
+ );
157
+ return {
158
+ schemaVersion: MODULE_DEBUG_SESSION_SCHEMA_VERSION,
159
+ session: {
160
+ ...this.project(entry, actor.id, validated),
161
+ messages: validated.messages,
162
+ } satisfies DebugSessionDetailDtoV1,
163
+ };
164
+ } catch (error) {
165
+ providerFailed = true;
166
+ this.auditFailure(entry, context, "get", "get", error);
167
+ break;
168
+ }
169
+ }
170
+ }
171
+ if (providerFailed) throw providerUnavailable();
172
+ throw new DebugSessionFacadeError(
173
+ "SESSION_NOT_FOUND",
174
+ "调试会话不存在或不可访问",
175
+ 404,
176
+ );
177
+ }
178
+
179
+ private async authorizedProviders(context: RequestGuardContext) {
180
+ const operationKey = this.runtime.agentDebugOperationForActor(
181
+ context.actor.id,
182
+ );
183
+ if (!operationKey) {
184
+ throw new DebugSessionFacadeError("FORBIDDEN", "调试会话访问被拒绝", 403);
185
+ }
186
+ const candidates = this.hosts.debugSessionProviders.filter(({ provider }) =>
187
+ provider.operationKeys.includes(operationKey),
188
+ );
189
+ for (const { moduleName, provider } of candidates) {
190
+ const manifest = this.hosts.manifestOf(moduleName);
191
+ const feature = manifest.features.find(
192
+ ({ key }) => key === provider.featureKey,
193
+ );
194
+ const access = await evaluateAccessGuards(this.runtime, context, {
195
+ permission: { permissionKey: provider.permissionKey },
196
+ feature,
197
+ });
198
+ if (!access.decision.allowed) {
199
+ throw new DebugSessionFacadeError(
200
+ "FORBIDDEN",
201
+ "调试会话访问被拒绝",
202
+ 403,
203
+ );
204
+ }
205
+ }
206
+ return candidates.map((entry) => ({ ...entry, operationKey }));
207
+ }
208
+
209
+ private project(
210
+ entry: AuthorizedProvider,
211
+ actorId: string,
212
+ summary: ModuleDebugSessionSummaryV1,
213
+ ): DebugSessionSummaryDtoV1 {
214
+ return {
215
+ id: this.ids.issue({
216
+ actorId,
217
+ operationKey: entry.operationKey,
218
+ providerKey: entry.provider.key,
219
+ sourceId: summary.sourceId,
220
+ }),
221
+ title: summary.title,
222
+ ...(summary.scopeLabel ? { scopeLabel: summary.scopeLabel } : {}),
223
+ updatedAt: summary.updatedAt,
224
+ revision: summary.revision,
225
+ };
226
+ }
227
+
228
+ private audit(
229
+ entry: AuthorizedProvider,
230
+ context: RequestGuardContext,
231
+ mode: "list" | "get",
232
+ sessionId: string | undefined,
233
+ resultCount: number,
234
+ ): void {
235
+ this.runtime.auditSink.recordEvent({
236
+ id: randomUUIDv7(),
237
+ action: entry.provider.auditAction,
238
+ outcome: "success",
239
+ actor: auditActor(context),
240
+ target: {
241
+ type: "agent-debug-session",
242
+ id: sessionId ?? entry.provider.key,
243
+ },
244
+ module: entry.moduleName,
245
+ requestId: context.requestId,
246
+ metadata: {
247
+ ...debugActorAuditMetadata(context.actor),
248
+ mode,
249
+ resultCount,
250
+ },
251
+ occurredAt: new Date().toISOString(),
252
+ });
253
+ }
254
+
255
+ private auditFailure(
256
+ entry: AuthorizedProvider,
257
+ context: RequestGuardContext,
258
+ mode: "list" | "get",
259
+ phase: "list" | "get",
260
+ error: unknown,
261
+ ): void {
262
+ this.runtime.auditSink.recordEvent({
263
+ id: randomUUIDv7(),
264
+ action: entry.provider.auditAction,
265
+ outcome: "failure",
266
+ actor: auditActor(context),
267
+ target: {
268
+ type: "agent-debug-session-provider",
269
+ id: entry.provider.key,
270
+ },
271
+ module: entry.moduleName,
272
+ requestId: context.requestId,
273
+ metadata: {
274
+ ...debugActorAuditMetadata(context.actor),
275
+ mode,
276
+ phase,
277
+ failureCode: providerFailureCode(error),
278
+ },
279
+ occurredAt: new Date().toISOString(),
280
+ });
281
+ }
282
+ }
283
+
284
+ type AuthorizedProvider = RegisteredModuleDebugSessionProvider & {
285
+ readonly operationKey: string;
286
+ };
287
+
288
+ function providerFailureCode(
289
+ error: unknown,
290
+ ): "PROVIDER_INVALID" | "PROVIDER_UNAVAILABLE" {
291
+ return error instanceof DebugSessionProviderContractError
292
+ ? "PROVIDER_INVALID"
293
+ : "PROVIDER_UNAVAILABLE";
294
+ }
295
+
296
+ function providerUnavailable(): DebugSessionFacadeError {
297
+ return new DebugSessionFacadeError(
298
+ "PROVIDER_UNAVAILABLE",
299
+ "调试会话 Provider 暂不可用",
300
+ 503,
301
+ );
302
+ }
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DebugSessionIdCodecV1 } from "./debug-session-id.js";
3
+
4
+ describe("ADK 调试会话不透明 ID", () => {
5
+ test("稳定绑定业务用户、Operation、Provider 与 sourceId", () => {
6
+ const codec = new DebugSessionIdCodecV1(
7
+ "test-secret-012345678901234567890123456789",
8
+ );
9
+ const identity = {
10
+ actorId: "user_dev_admin",
11
+ operationKey: "known.copilot.global",
12
+ providerKey: "known.copilot.history",
13
+ sourceId: "private-conversation-id",
14
+ };
15
+ const issued = codec.issue(identity);
16
+
17
+ expect(codec.issue(identity)).toBe(issued);
18
+ expect(issued).not.toContain(identity.sourceId);
19
+ expect(codec.isWellFormed(issued)).toBeTrue();
20
+ for (const changed of [
21
+ { ...identity, actorId: "user_other" },
22
+ { ...identity, operationKey: "known.copilot.case" },
23
+ { ...identity, providerKey: "known.other.history" },
24
+ { ...identity, sourceId: "other-conversation" },
25
+ ]) {
26
+ expect(codec.issue(changed)).not.toBe(issued);
27
+ }
28
+ });
29
+
30
+ test("短 Secret 与可逆业务 ID fail closed", () => {
31
+ expect(() => new DebugSessionIdCodecV1("short")).toThrow("至少 32");
32
+ const codec = new DebugSessionIdCodecV1(
33
+ "test-secret-012345678901234567890123456789",
34
+ );
35
+ expect(codec.isWellFormed("private-conversation-id")).toBeFalse();
36
+ });
37
+ });
@@ -0,0 +1,37 @@
1
+ import { createHmac } from "node:crypto";
2
+
3
+ export const DEBUG_SESSION_ID_PREFIX = "wdbg1_";
4
+
5
+ export interface DebugSessionIdentityV1 {
6
+ readonly actorId: string;
7
+ readonly operationKey: string;
8
+ readonly providerKey: string;
9
+ readonly sourceId: string;
10
+ }
11
+
12
+ export class DebugSessionIdCodecV1 {
13
+ constructor(private readonly secret: string) {
14
+ if (Buffer.byteLength(secret) < 32) {
15
+ throw new Error("调试会话 ID Secret 必须至少 32 字节");
16
+ }
17
+ }
18
+
19
+ issue(identity: DebugSessionIdentityV1): string {
20
+ const digest = createHmac("sha256", this.secret)
21
+ .update(
22
+ JSON.stringify([
23
+ 1,
24
+ identity.actorId,
25
+ identity.operationKey,
26
+ identity.providerKey,
27
+ identity.sourceId,
28
+ ]),
29
+ )
30
+ .digest("base64url");
31
+ return `${DEBUG_SESSION_ID_PREFIX}${digest}`;
32
+ }
33
+
34
+ isWellFormed(value: string): boolean {
35
+ return /^wdbg1_[A-Za-z0-9_-]{43}$/.test(value);
36
+ }
37
+ }