create-windy 0.4.1 → 0.5.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.
Files changed (174) hide show
  1. package/dist/cli.js +258 -121
  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
@@ -7,6 +7,7 @@ import {
7
7
  import { createTestRunAuthorizationStore } from "./run-authorization.test-support.js";
8
8
  import { debugActorAuditMetadata } from "./run-debug-authorization.js";
9
9
  import { bindModuleUserProjection } from "../guards.js";
10
+ import { buildScopeContext } from "../data-access/context.js";
10
11
 
11
12
  describe("ADK 调试 Operation 边界", () => {
12
13
  test("专用身份不能切换到未选定的 Agent Operation", async () => {
@@ -53,4 +54,42 @@ describe("ADK 调试 Operation 边界", () => {
53
54
  ),
54
55
  ).toEqual({ debugDataActorId: "user_dev_admin" });
55
56
  });
57
+
58
+ test("报表按 Module Actor 投影归属真实调试用户", async () => {
59
+ const fixture = await createRunFacadeFixture();
60
+ const actor = bindModuleUserProjection(
61
+ {
62
+ ...fixture.context.actor,
63
+ id: "service_adk_debug",
64
+ type: "service",
65
+ scopeContext: buildScopeContext("user_dev_admin", "dept_platform", [
66
+ {
67
+ id: "role-debug",
68
+ code: "debug",
69
+ dataScope: "self",
70
+ departmentIds: [],
71
+ },
72
+ ]),
73
+ },
74
+ "user_dev_admin",
75
+ );
76
+ const context = { ...fixture.context, actor };
77
+ Object.assign(fixture.runtime, {
78
+ agentDebugOperationForActor: (actorId: string) =>
79
+ actorId === "service_adk_debug" ? "business.answer" : undefined,
80
+ });
81
+
82
+ await fixture.facade.start(
83
+ "business.answer",
84
+ { question: "调试归属" },
85
+ "request-debug-report",
86
+ context,
87
+ );
88
+
89
+ expect(fixture.remote.started[0]?.reporting).toEqual({
90
+ actorId: "user_dev_admin",
91
+ actorType: "user",
92
+ departmentId: "dept_platform",
93
+ });
94
+ });
56
95
  });
@@ -39,6 +39,10 @@ describe("Guarded Agent Run facade", () => {
39
39
  promptVersion: "prompt-3",
40
40
  routeVersion: "route-4",
41
41
  },
42
+ reporting: {
43
+ actorId: "actor-1",
44
+ actorType: "user",
45
+ },
42
46
  });
43
47
  expect(fixture.remote.started[0]?.release.releaseId).toMatch(
44
48
  /^release_[a-f0-9]{40}$/,
@@ -109,4 +113,78 @@ describe("Guarded Agent Run facade", () => {
109
113
  message: "Agent Run 不存在或不可访问",
110
114
  });
111
115
  });
116
+
117
+ test("服务端可验证 canonical 输入,且同幂等键的不同输入 fail closed", async () => {
118
+ const fixture = await createRunFacadeFixture();
119
+ await fixture.facade.start(
120
+ "business.answer",
121
+ { question: "原始问题", ignored: "不出网" },
122
+ "request-attested",
123
+ fixture.context,
124
+ );
125
+
126
+ await expect(
127
+ fixture.facade.verifyRunInput(
128
+ "business.answer",
129
+ "run_1",
130
+ { question: "原始问题" },
131
+ fixture.context,
132
+ ),
133
+ ).resolves.toBeUndefined();
134
+ await expect(
135
+ fixture.facade.verifyRunInput(
136
+ "business.answer",
137
+ "run_1",
138
+ { question: "伪造问题" },
139
+ fixture.context,
140
+ ),
141
+ ).rejects.toMatchObject({ code: "RUN_CONFLICT", status: 404 });
142
+ await expect(
143
+ fixture.facade.start(
144
+ "business.answer",
145
+ { question: "伪造问题" },
146
+ "request-attested",
147
+ fixture.context,
148
+ ),
149
+ ).rejects.toMatchObject({ message: "Agent Run 不存在或不可访问" });
150
+ expect(fixture.remote.started).toHaveLength(1);
151
+ });
152
+
153
+ test("敏感原文使用 DLP 前承诺且不会与脱敏占位符碰撞", async () => {
154
+ const fixture = await createRunFacadeFixture();
155
+ await fixture.facade.start(
156
+ "business.answer",
157
+ { question: "联系 13800138000" },
158
+ "request-sensitive",
159
+ fixture.context,
160
+ );
161
+
162
+ expect(fixture.remote.started[0]?.input).toEqual({
163
+ question: "联系 [REDACTED]",
164
+ });
165
+ await expect(
166
+ fixture.facade.verifyRunInput(
167
+ "business.answer",
168
+ "run_1",
169
+ { question: "联系 13800138000" },
170
+ fixture.context,
171
+ ),
172
+ ).resolves.toBeUndefined();
173
+ await expect(
174
+ fixture.facade.verifyRunInput(
175
+ "business.answer",
176
+ "run_1",
177
+ { question: "联系 [REDACTED]" },
178
+ fixture.context,
179
+ ),
180
+ ).rejects.toMatchObject({ code: "RUN_CONFLICT", status: 404 });
181
+ await expect(
182
+ fixture.facade.start(
183
+ "business.answer",
184
+ { question: "联系 [REDACTED]" },
185
+ "request-sensitive",
186
+ fixture.context,
187
+ ),
188
+ ).rejects.toMatchObject({ message: "Agent Run 不存在或不可访问" });
189
+ });
112
190
  });
@@ -27,6 +27,7 @@ import type {
27
27
  import { AgentRuntimeError } from "./run-contracts.js";
28
28
  import {
29
29
  governProviderInput,
30
+ projectProviderInput,
30
31
  ProviderInputGovernanceError,
31
32
  } from "./provider-input-governance.js";
32
33
  import { rotateVisionArtifacts, validateOutputSchema } from "./vision-input.js";
@@ -38,6 +39,7 @@ import {
38
39
  } from "./run-definition-support.js";
39
40
  import { AgentRunFacadeError, inaccessibleRun } from "./run-errors.js";
40
41
  import { scopedRuntimeIdempotencyKey } from "./idempotency-key.js";
42
+ import { verifyCommittedRunInput } from "./run-input-verification.js";
41
43
  import {
42
44
  assertSelectedDebugOperation,
43
45
  debugActorAuditMetadata,
@@ -52,7 +54,6 @@ type ServerRuntime = ReturnType<typeof createServerRuntime>;
52
54
 
53
55
  export class AgentRunFacade {
54
56
  readonly #operations: Map<string, RegisteredOperation>;
55
-
56
57
  constructor(
57
58
  modules: readonly ModuleManifest[],
58
59
  hosts: InitializedModuleHosts,
@@ -63,7 +64,6 @@ export class AgentRunFacade {
63
64
  ) {
64
65
  this.#operations = buildRegisteredOperations(modules, hosts);
65
66
  }
66
-
67
67
  async start(
68
68
  operationKey: string,
69
69
  input: ModuleAiJsonValue,
@@ -116,7 +116,11 @@ export class AgentRunFacade {
116
116
  );
117
117
  }
118
118
  let governedInput = input;
119
+ let committedInput = input;
119
120
  try {
121
+ committedInput = providerPolicy
122
+ ? projectProviderInput(input, providerPolicy)
123
+ : input;
120
124
  governedInput = providerPolicy
121
125
  ? governProviderInput(input, providerPolicy)
122
126
  : input;
@@ -130,8 +134,9 @@ export class AgentRunFacade {
130
134
  }
131
135
  throw error;
132
136
  }
137
+ const moduleActor = toModuleActor(context.actor);
133
138
  const plan = await operation.registration.prepare(governedInput, {
134
- actor: toModuleActor(context.actor),
139
+ actor: moduleActor,
135
140
  requestId: context.requestId,
136
141
  });
137
142
  if (!plan.instructions.trim() || !plan.userPrompt.trim()) {
@@ -161,6 +166,7 @@ export class AgentRunFacade {
161
166
  operationKey,
162
167
  releaseId: release.releaseId,
163
168
  idempotencyKey,
169
+ input: committedInput,
164
170
  authorizationSnapshot: {
165
171
  permissionKey: operation.definition.permissionKey,
166
172
  featureKey: operation.definition.featureKey,
@@ -194,6 +200,13 @@ export class AgentRunFacade {
194
200
  executionGrant: authorization.token,
195
201
  release,
196
202
  traceId,
203
+ reporting: {
204
+ actorId: moduleActor.id,
205
+ actorType: moduleActor.type,
206
+ ...(context.actor.scopeContext?.departmentId
207
+ ? { departmentId: context.actor.scopeContext.departmentId }
208
+ : {}),
209
+ },
197
210
  },
198
211
  scopedRuntimeIdempotencyKey(
199
212
  context.actor.id,
@@ -282,6 +295,23 @@ export class AgentRunFacade {
282
295
  return operation;
283
296
  }
284
297
 
298
+ async verifyRunInput(
299
+ operationKey: string,
300
+ runId: string,
301
+ input: ModuleAiJsonValue,
302
+ context: RequestGuardContext,
303
+ ): Promise<void> {
304
+ const operation = await this.authorizeRun(operationKey, runId, context);
305
+ await verifyCommittedRunInput({
306
+ operationKey,
307
+ runId,
308
+ value: input,
309
+ policy: operation.definition.providerDataPolicy,
310
+ context,
311
+ authorizations: this.authorizations,
312
+ });
313
+ }
314
+
285
315
  async attach(
286
316
  operationKey: string,
287
317
  runId: string,
@@ -0,0 +1,50 @@
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
+
4
+ export function commitAgentRunInput(
5
+ secret: string,
6
+ tokenHash: string,
7
+ input: ModuleAiJsonValue,
8
+ ): string {
9
+ const digest = createHash("sha256")
10
+ .update(canonicalizeJson(input))
11
+ .digest("hex");
12
+ return createHmac("sha256", secret)
13
+ .update(`input\u0000${tokenHash}\u0000${digest}`)
14
+ .digest("hex");
15
+ }
16
+
17
+ export function sameInputCommitment(left: string, right: string): boolean {
18
+ const leftBytes = Buffer.from(left, "hex");
19
+ const rightBytes = Buffer.from(right, "hex");
20
+ return (
21
+ leftBytes.byteLength === 32 &&
22
+ rightBytes.byteLength === 32 &&
23
+ timingSafeEqual(leftBytes, rightBytes)
24
+ );
25
+ }
26
+
27
+ export function canonicalizeJson(value: ModuleAiJsonValue): string {
28
+ if (
29
+ value === null ||
30
+ typeof value === "string" ||
31
+ typeof value === "boolean"
32
+ ) {
33
+ return JSON.stringify(value);
34
+ }
35
+ if (typeof value === "number") {
36
+ if (!Number.isFinite(value))
37
+ throw new Error("Agent Run 输入包含非有限数值");
38
+ return JSON.stringify(value);
39
+ }
40
+ if (Array.isArray(value)) {
41
+ return `[${value.map(canonicalizeJson).join(",")}]`;
42
+ }
43
+ return `{${Object.keys(value)
44
+ .sort()
45
+ .map(
46
+ (key) =>
47
+ `${JSON.stringify(key)}:${canonicalizeJson(value[key] as ModuleAiJsonValue)}`,
48
+ )
49
+ .join(",")}}`;
50
+ }
@@ -0,0 +1,41 @@
1
+ import type { AiProviderDataPolicy } from "@southwind-ai/modules";
2
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
+ import type { RequestGuardContext } from "../guards.js";
4
+ import {
5
+ projectProviderInput,
6
+ ProviderInputGovernanceError,
7
+ } from "./provider-input-governance.js";
8
+ import {
9
+ AgentRunAuthorizationError,
10
+ type AgentRunAuthorizationStore,
11
+ } from "./run-authorization.js";
12
+ import { inaccessibleRun } from "./run-errors.js";
13
+
14
+ export async function verifyCommittedRunInput(input: {
15
+ readonly operationKey: string;
16
+ readonly runId: string;
17
+ readonly value: ModuleAiJsonValue;
18
+ readonly policy?: AiProviderDataPolicy;
19
+ readonly context: RequestGuardContext;
20
+ readonly authorizations: AgentRunAuthorizationStore;
21
+ }): Promise<void> {
22
+ try {
23
+ const committed = input.policy
24
+ ? projectProviderInput(input.value, input.policy)
25
+ : input.value;
26
+ await input.authorizations.verifyRunInput(
27
+ input.operationKey,
28
+ input.runId,
29
+ committed,
30
+ input.context,
31
+ );
32
+ } catch (error) {
33
+ if (
34
+ error instanceof AgentRunAuthorizationError ||
35
+ error instanceof ProviderInputGovernanceError
36
+ ) {
37
+ throw inaccessibleRun();
38
+ }
39
+ throw error;
40
+ }
41
+ }
@@ -0,0 +1,136 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+ import { createFoundationSnapshot, loadSystemModules } from "../foundation.js";
3
+ import { createRequestGuardContext } from "../guards.js";
4
+ import { buildScopeContext } from "../data-access/context.js";
5
+ import { createServerRuntime } from "../runtime.js";
6
+ import { createSystemRepositories } from "../system/seed.js";
7
+ import { FakeRouteApp } from "../system/route-test-app.js";
8
+ import { registerAiRuntimeReporting } from "./runtime-bootstrap.js";
9
+ import { registerUnavailableAiRoutes } from "./unavailable-routes.js";
10
+
11
+ const originalFetch = globalThis.fetch;
12
+
13
+ afterEach(() => {
14
+ globalThis.fetch = originalFetch;
15
+ });
16
+
17
+ describe("AI Runtime 报表生产组合", () => {
18
+ test("注册受权限保护的路由并复用 Agent Server 地址与内部 Token", async () => {
19
+ const snapshot = createFoundationSnapshot();
20
+ const modules = loadSystemModules();
21
+ const features = modules.flatMap(({ features }) => features);
22
+ const runtime = createServerRuntime(
23
+ { WINDY_DEV_ADMIN_TOKEN: "admin-token" },
24
+ { featureCatalog: features },
25
+ );
26
+ const app = new FakeRouteApp();
27
+ const remote = mock(
28
+ async (input: string | URL | Request, init?: RequestInit) => {
29
+ const url = new URL(
30
+ input instanceof Request ? input.url : input.toString(),
31
+ );
32
+ expect(url.origin).toBe("http://agent-report.test");
33
+ expect(url.pathname).toBe("/api/internal/ai-runtime-report/summary");
34
+ expect(url.searchParams.get("scopeMode")).toBe("all");
35
+ expect(new Headers(init?.headers).get("authorization")).toBe(
36
+ "Bearer internal-token-for-tests",
37
+ );
38
+ return Response.json(emptySummary());
39
+ },
40
+ );
41
+ globalThis.fetch = remote as unknown as typeof globalThis.fetch;
42
+ registerAiRuntimeReporting({
43
+ app,
44
+ features,
45
+ runtime,
46
+ systemRepositories: createSystemRepositories(snapshot),
47
+ agentServerUrl: "http://agent-report.test",
48
+ internalToken: "internal-token-for-tests",
49
+ });
50
+ const guardContext = await createRequestGuardContext(
51
+ new Request("http://server.test/api/system/ai-runtime/summary"),
52
+ {
53
+ resolveActor: () => ({
54
+ id: "report-admin",
55
+ type: "user",
56
+ name: "报表管理员",
57
+ roleCodes: ["report-admin"],
58
+ permissionKeys: ["system.ai-runtime.read"],
59
+ scopeContext: buildScopeContext("report-admin", "dept_platform", [
60
+ {
61
+ id: "role-report-admin",
62
+ code: "report-admin",
63
+ dataScope: "all",
64
+ departmentIds: [],
65
+ },
66
+ ]),
67
+ }),
68
+ },
69
+ );
70
+
71
+ const result = await app.invoke("GET", "/system/ai-runtime/summary", {
72
+ guardContext,
73
+ params: {},
74
+ query: { range: "day" },
75
+ });
76
+
77
+ expect(result).toEqual(emptySummary());
78
+ expect(remote).toHaveBeenCalledTimes(1);
79
+ expect(runtime.auditSink.list().at(-1)?.action).toBe(
80
+ "agent.runtime.report.read",
81
+ );
82
+ });
83
+
84
+ test("AI disabled 时四条报表接口明确返回 503", async () => {
85
+ const app = new FakeRouteApp();
86
+ registerUnavailableAiRoutes(app);
87
+
88
+ for (const path of [
89
+ "/system/ai-runtime/summary",
90
+ "/system/ai-runtime/runs",
91
+ "/system/ai-runtime/runs/:runId",
92
+ "/system/ai-runtime/actors",
93
+ ]) {
94
+ const response = (await app.invoke("GET", path, {})) as Response;
95
+ expect(response.status).toBe(503);
96
+ await expect(response.json()).resolves.toEqual({
97
+ code: "RUNTIME_UNAVAILABLE",
98
+ message: "AI Runtime 未启用",
99
+ });
100
+ }
101
+ });
102
+ });
103
+
104
+ function emptySummary() {
105
+ return {
106
+ generatedAt: "2026-07-31T00:00:00Z",
107
+ reportingAvailableFrom: null,
108
+ retentionDays: 90,
109
+ runs: {
110
+ total: 0,
111
+ completed: 0,
112
+ failed: 0,
113
+ cancelled: 0,
114
+ inProgress: 0,
115
+ successRate: null,
116
+ },
117
+ cache: {
118
+ hit: 0,
119
+ miss: 0,
120
+ unknown: 0,
121
+ hitRate: null,
122
+ tokenRate: null,
123
+ coverageRate: 0,
124
+ },
125
+ tokens: {
126
+ input: 0,
127
+ output: 0,
128
+ cachedInput: 0,
129
+ reasoning: 0,
130
+ total: 0,
131
+ coverageRate: 0,
132
+ },
133
+ byModel: [],
134
+ activity: [],
135
+ };
136
+ }
@@ -1,5 +1,11 @@
1
1
  import type { PlatformConfig } from "@southwind-ai/config";
2
2
  import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
4
+ import { SystemAiRuntimeActorDirectory } from "../ai-runtime-report/actor-directory.js";
5
+ import { RemoteAiRuntimeReportPort } from "../ai-runtime-report/remote-port.js";
6
+ import { registerAiRuntimeReportRoutes } from "../ai-runtime-report/routes.js";
7
+ import { AiRuntimeReportService } from "../ai-runtime-report/service.js";
8
+ import { departmentDescendantResolver } from "../data-access/department-tree.js";
3
9
  import type { InitializedModuleHosts } from "../module-host.js";
4
10
  import type { createServerRuntime } from "../runtime.js";
5
11
  import type { RouteApp } from "../system/routes.js";
@@ -51,6 +57,9 @@ import {
51
57
  createStaticRouteAuthorizer,
52
58
  } from "../module-host/static-route-authorization.js";
53
59
  import { registerUnavailableAiRoutes } from "./unavailable-routes.js";
60
+ import { DebugSessionFacadeV1 } from "./debug-session-facade.js";
61
+ import { DebugSessionIdCodecV1 } from "./debug-session-id.js";
62
+ import { registerDebugSessionRoutesV1 } from "./debug-session-routes.js";
54
63
 
55
64
  export function registerAgentPlatformRuntime(input: {
56
65
  app: RouteApp;
@@ -107,6 +116,21 @@ export function registerAgentPlatformRuntime(input: {
107
116
  input.app,
108
117
  routeAuthorizer,
109
118
  );
119
+ const debugToken =
120
+ process.env.NODE_ENV === "development" &&
121
+ process.env.WINDY_ADK_DEBUG_OPERATION_KEY?.trim()
122
+ ? process.env.WINDY_ADK_DEBUG_TOKEN?.trim()
123
+ : undefined;
124
+ if (debugToken) {
125
+ registerDebugSessionRoutesV1(
126
+ input.app,
127
+ new DebugSessionFacadeV1(
128
+ input.hosts,
129
+ input.runtime,
130
+ new DebugSessionIdCodecV1(debugToken),
131
+ ),
132
+ );
133
+ }
110
134
  const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
111
135
  const aiArtifacts = new AiArtifactRuntime(
112
136
  new AiArtifactCodec(requiredEnvironment("AI_ARTIFACT_ENCRYPTION_SECRET")),
@@ -117,11 +141,21 @@ export function registerAgentPlatformRuntime(input: {
117
141
  resolveAgentActor,
118
142
  );
119
143
  registerAiArtifactRoutes(aiRouteApp, aiArtifacts, internalToken);
144
+ const agentServerUrl =
145
+ process.env.AGENT_SERVER_URL || "http://agent-server:8080";
120
146
  const remote = new RemoteAgentRuntime({
121
- baseUrl: process.env.AGENT_SERVER_URL || "http://agent-server:8080",
147
+ baseUrl: agentServerUrl,
122
148
  internalToken,
123
149
  toolHostUrl: process.env.AGENT_TOOL_HOST_URL || "http://server:9747",
124
150
  });
151
+ registerAiRuntimeReporting({
152
+ app: aiRouteApp,
153
+ features,
154
+ runtime: input.runtime,
155
+ systemRepositories: input.systemRepositories,
156
+ agentServerUrl,
157
+ internalToken,
158
+ });
125
159
  const grants = new ExecutionGrantStore();
126
160
  const runAuthorizations = new AgentRunAuthorizationStore(
127
161
  input.db
@@ -221,6 +255,37 @@ export function registerAgentPlatformRuntime(input: {
221
255
  return { toolHandlers, aiArtifacts, aiOperations };
222
256
  }
223
257
 
258
+ export function registerAiRuntimeReporting(input: {
259
+ app: RouteApp;
260
+ features: FeatureFlagDefinition[];
261
+ runtime: ReturnType<typeof createServerRuntime>;
262
+ systemRepositories: SystemRepositories;
263
+ agentServerUrl: string;
264
+ internalToken: string;
265
+ }): void {
266
+ registerAiRuntimeReportRoutes(
267
+ input.app,
268
+ new AiRuntimeReportService(
269
+ new RemoteAiRuntimeReportPort({
270
+ baseUrl: input.agentServerUrl,
271
+ internalToken: input.internalToken,
272
+ }),
273
+ new SystemAiRuntimeActorDirectory(
274
+ input.systemRepositories,
275
+ input.internalToken,
276
+ ),
277
+ {
278
+ descendants: departmentDescendantResolver(
279
+ input.systemRepositories.departments,
280
+ ),
281
+ },
282
+ input.runtime,
283
+ ),
284
+ input.runtime,
285
+ input.features,
286
+ );
287
+ }
288
+
224
289
  function requiredEnvironment(key: string): string {
225
290
  const value = process.env[key]?.trim();
226
291
  if (!value) throw new Error(`启用 AI Runtime 时缺少 ${key}`);
@@ -180,6 +180,7 @@ async function createFixture(
180
180
  undefined,
181
181
  ).releaseId,
182
182
  idempotencyKey: "tool-host-test",
183
+ input: { question: "tool host" },
183
184
  authorizationSnapshot: {
184
185
  permissionKey: currentOperation.permissionKey,
185
186
  featureKey: currentOperation.featureKey,
@@ -18,6 +18,10 @@ const unavailableRoutes = [
18
18
  ["POST", "/agent/runs/:runId/cancel"],
19
19
  ["POST", "/agent/runs/:runId/attach"],
20
20
  ["POST", "/internal/agent/tools/execute"],
21
+ ["GET", "/system/ai-runtime/summary"],
22
+ ["GET", "/system/ai-runtime/runs"],
23
+ ["GET", "/system/ai-runtime/runs/:runId"],
24
+ ["GET", "/system/ai-runtime/actors"],
21
25
  ] as const;
22
26
 
23
27
  export function registerUnavailableAiRoutes(app: RouteApp): void {
@@ -0,0 +1,94 @@
1
+ import type {
2
+ AiRuntimeActorPage,
3
+ AiRuntimeActorSummary,
4
+ } from "@southwind-ai/shared";
5
+ import {
6
+ DataAccessDeniedError,
7
+ DataScopedResourceService,
8
+ } from "../data-access/scoped-resource.js";
9
+ import { departmentDescendantResolver } from "../data-access/department-tree.js";
10
+ import type { RequestActor } from "../guards.js";
11
+ import type { ManagedUser } from "../system/entities.js";
12
+ import type { SystemRepositories } from "../system/seed.js";
13
+ import { AiRuntimeActorHandleCodec } from "./actor-handle.js";
14
+ import type { AiRuntimeActorDirectory } from "./contracts.js";
15
+
16
+ export class SystemAiRuntimeActorDirectory implements AiRuntimeActorDirectory {
17
+ readonly #access: DataScopedResourceService<ManagedUser>;
18
+ readonly #handles: AiRuntimeActorHandleCodec;
19
+
20
+ constructor(
21
+ repositories: Pick<SystemRepositories, "users" | "departments">,
22
+ handleSecret: string,
23
+ ) {
24
+ this.#access = new DataScopedResourceService(
25
+ repositories.users,
26
+ departmentDescendantResolver(repositories.departments),
27
+ );
28
+ this.#handles = new AiRuntimeActorHandleCodec(handleSecret);
29
+ }
30
+
31
+ async list(
32
+ query: { q?: string; page: number; pageSize: number },
33
+ actor: RequestActor,
34
+ ): Promise<AiRuntimeActorPage> {
35
+ const result = await this.#access.list(
36
+ {
37
+ ...query,
38
+ status: "active",
39
+ sort: [{ field: "displayName", order: "asc" }],
40
+ },
41
+ ["displayName", "username"],
42
+ actor,
43
+ );
44
+ return {
45
+ ...result,
46
+ items: result.items.map((user) => this.#actorSummary(user)),
47
+ };
48
+ }
49
+
50
+ async resolve(
51
+ actorIds: readonly string[],
52
+ actor: RequestActor,
53
+ ): Promise<Map<string, AiRuntimeActorSummary>> {
54
+ const result = new Map<string, AiRuntimeActorSummary>();
55
+ await Promise.all(
56
+ [...new Set(actorIds)].map(async (id) => {
57
+ try {
58
+ const user = await this.#access.get(id, actor);
59
+ if (user) result.set(id, this.#actorSummary(user));
60
+ } catch (error) {
61
+ if (!(error instanceof DataAccessDeniedError)) throw error;
62
+ }
63
+ }),
64
+ );
65
+ return result;
66
+ }
67
+
68
+ async resolveHandle(
69
+ handle: string,
70
+ actor: RequestActor,
71
+ ): Promise<string | undefined> {
72
+ const actorId = this.#handles.verify(handle);
73
+ if (!actorId) return undefined;
74
+ try {
75
+ const user = await this.#access.get(actorId, actor);
76
+ return user?.status === "active" ? user.id : undefined;
77
+ } catch (error) {
78
+ if (error instanceof DataAccessDeniedError) return undefined;
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ #actorSummary(user: {
84
+ id: string;
85
+ username?: string;
86
+ displayName?: string;
87
+ }): AiRuntimeActorSummary {
88
+ return {
89
+ handle: this.#handles.issue(user.id),
90
+ displayName: user.displayName?.trim() || user.username || "未命名用户",
91
+ ...(user.username ? { username: user.username } : {}),
92
+ };
93
+ }
94
+ }