create-windy 0.4.0 → 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 (194) hide show
  1. package/dist/cli.js +263 -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/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/system/operations.test.ts +54 -0
  100. package/template/apps/server/src/system/operations.ts +23 -1
  101. package/template/apps/server/src/system/seed.test.ts +4 -0
  102. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  103. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  104. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  105. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  106. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  107. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  108. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  109. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  110. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  111. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  112. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  113. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  114. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  115. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  116. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  117. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  118. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  119. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  120. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  121. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  122. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  123. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  124. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  125. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  126. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  127. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  128. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  129. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  130. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  131. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  132. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  133. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  134. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  135. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  136. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  137. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  138. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  139. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  140. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  141. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  142. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  143. package/template/apps/web/src/router/error-navigation.ts +22 -0
  144. package/template/apps/web/src/router/index.ts +12 -23
  145. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  146. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  147. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  148. package/template/apps/web/src/services/operations-api.ts +28 -0
  149. package/template/docker-compose.agent-debug.yml +1 -1
  150. package/template/docs/architecture/ai-runtime.md +2 -1
  151. package/template/docs/architecture/module-host.md +3 -0
  152. package/template/docs/platform/agent-run-attestation.md +65 -0
  153. package/template/docs/platform/agent-runtime-observability.md +135 -0
  154. package/template/docs/platform/agent-runtime.md +47 -3
  155. package/template/docs/platform/operations-overview.md +13 -0
  156. package/template/docs/platform/platform-authentication.md +4 -3
  157. package/template/docs/platform/platform-shell-settings.md +15 -10
  158. package/template/docs/platform/web-system-crud.md +1 -1
  159. package/template/packages/config/package.json +1 -1
  160. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  161. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  162. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  163. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  164. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  165. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  166. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  167. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  168. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  169. package/template/packages/database/package.json +1 -1
  170. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  171. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  172. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  173. package/template/packages/jobs/package.json +1 -1
  174. package/template/packages/modules/package.json +1 -1
  175. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  176. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  177. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  178. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  179. package/template/packages/modules/src/system-features.ts +8 -0
  180. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  181. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  182. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  183. package/template/packages/modules/src/system-modules.ts +13 -0
  184. package/template/packages/modules/src/system-permissions.ts +2 -0
  185. package/template/packages/server-sdk/index.ts +11 -0
  186. package/template/packages/server-sdk/package.json +2 -1
  187. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  188. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  189. package/template/packages/server-sdk/src/module-host.ts +5 -0
  190. package/template/packages/shared/index.ts +3 -0
  191. package/template/packages/shared/package.json +1 -1
  192. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  193. package/template/packages/shared/src/platform-settings.ts +17 -0
  194. package/template/scripts/lib/postgres-regression.ts +201 -0
@@ -0,0 +1,54 @@
1
+ import type {
2
+ ModuleActor,
3
+ ModuleDebugSessionProviderV1,
4
+ ModuleDebugSessionSummaryV1,
5
+ } from "@southwind-ai/server-sdk";
6
+ import {
7
+ DebugSessionProviderContractError,
8
+ validateDebugSessionPage,
9
+ } from "./debug-session-validation.js";
10
+
11
+ const PAGE_SIZE = 100;
12
+
13
+ /**
14
+ * Drain one Provider with bounded keyset pages. The native ADK session API has
15
+ * no pagination surface, so returning a partial page would make older sessions
16
+ * permanently undiscoverable.
17
+ */
18
+ export async function readAllDebugSessionSummaries(input: {
19
+ readonly provider: ModuleDebugSessionProviderV1;
20
+ readonly actor: ModuleActor;
21
+ readonly operationKey: string;
22
+ readonly signal: AbortSignal;
23
+ }): Promise<readonly ModuleDebugSessionSummaryV1[]> {
24
+ const sessions: ModuleDebugSessionSummaryV1[] = [];
25
+ const sourceIds = new Set<string>();
26
+ const cursors = new Set<string>();
27
+ let cursor: string | undefined;
28
+ for (;;) {
29
+ input.signal.throwIfAborted();
30
+ const page = validateDebugSessionPage(
31
+ await input.provider.list({
32
+ actor: input.actor,
33
+ operationKey: input.operationKey,
34
+ limit: PAGE_SIZE,
35
+ ...(cursor ? { cursor } : {}),
36
+ signal: input.signal,
37
+ }),
38
+ );
39
+ for (const session of page.sessions) {
40
+ if (sourceIds.has(session.sourceId)) invalidProvider();
41
+ sourceIds.add(session.sourceId);
42
+ sessions.push(session);
43
+ }
44
+ if (!page.truncated) return sessions;
45
+ const next = page.next;
46
+ if (!next || cursors.has(next)) invalidProvider();
47
+ cursors.add(next);
48
+ cursor = next;
49
+ }
50
+ }
51
+
52
+ function invalidProvider(): never {
53
+ throw new DebugSessionProviderContractError("调试会话 Provider 分页游标无效");
54
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { createRequestGuardContext } from "../guards.js";
3
+ import { FakeRouteApp } from "../system/route-test-app.js";
4
+ import {
5
+ DebugSessionFacadeError,
6
+ type DebugSessionFacadeV1,
7
+ } from "./debug-session-facade.js";
8
+ import { registerDebugSessionRoutesV1 } from "./debug-session-routes.js";
9
+
10
+ describe("ADK 调试会话 V1 routes", () => {
11
+ test("只注册 GET 并在错误响应复用 guard requestId", async () => {
12
+ const app = new FakeRouteApp();
13
+ registerDebugSessionRoutesV1(app, {
14
+ list: async () => {
15
+ throw new DebugSessionFacadeError(
16
+ "PROVIDER_UNAVAILABLE",
17
+ "暂不可用",
18
+ 503,
19
+ );
20
+ },
21
+ } as unknown as DebugSessionFacadeV1);
22
+ const request = new Request("http://localhost/api/agent/debug/sessions/v1");
23
+ const guardContext = createRequestGuardContext(request);
24
+
25
+ const response = (await app.invoke("GET", "/agent/debug/sessions/v1", {
26
+ request,
27
+ guardContext,
28
+ params: {},
29
+ })) as Response;
30
+
31
+ expect([...app.handlers.keys()]).toEqual([
32
+ "GET /agent/debug/sessions/v1",
33
+ "GET /agent/debug/sessions/v1/:sessionId",
34
+ ]);
35
+ expect(response.status).toBe(503);
36
+ expect(response.headers.get("cache-control")).toBe("no-store");
37
+ expect(await response.json()).toMatchObject({
38
+ code: "PROVIDER_UNAVAILABLE",
39
+ requestId: guardContext.requestId,
40
+ retryable: true,
41
+ });
42
+ });
43
+
44
+ test("成功响应同样禁止缓存", async () => {
45
+ const app = new FakeRouteApp();
46
+ registerDebugSessionRoutesV1(app, {
47
+ list: async () => ({
48
+ schemaVersion: 1,
49
+ sessions: [],
50
+ truncated: false,
51
+ }),
52
+ } as unknown as DebugSessionFacadeV1);
53
+ const request = new Request("http://localhost/api/agent/debug/sessions/v1");
54
+
55
+ const response = (await app.invoke("GET", "/agent/debug/sessions/v1", {
56
+ request,
57
+ guardContext: createRequestGuardContext(request),
58
+ params: {},
59
+ })) as Response;
60
+
61
+ expect(response.status).toBe(200);
62
+ expect(response.headers.get("cache-control")).toBe("no-store");
63
+ });
64
+ });
@@ -0,0 +1,73 @@
1
+ import type { RequestGuardContext } from "../guards.js";
2
+ import type { RouteApp } from "../system/routes.js";
3
+ import {
4
+ DebugSessionFacadeError,
5
+ type DebugSessionFacadeV1,
6
+ } from "./debug-session-facade.js";
7
+
8
+ interface DebugSessionRouteContext {
9
+ params: Record<string, string | undefined>;
10
+ request: Request;
11
+ guardContext: RequestGuardContext;
12
+ }
13
+
14
+ export function registerDebugSessionRoutesV1(
15
+ app: RouteApp,
16
+ facade: DebugSessionFacadeV1,
17
+ ) {
18
+ app.get("/agent/debug/sessions/v1", async (raw) => {
19
+ const context = raw as DebugSessionRouteContext;
20
+ try {
21
+ return json(
22
+ await facade.list(context.guardContext, context.request.signal),
23
+ );
24
+ } catch (error) {
25
+ return facadeError(error, context.guardContext.requestId);
26
+ }
27
+ });
28
+ app.get("/agent/debug/sessions/v1/:sessionId", async (raw) => {
29
+ const context = raw as DebugSessionRouteContext;
30
+ try {
31
+ return json(
32
+ await facade.get(
33
+ context.params.sessionId ?? "",
34
+ context.guardContext,
35
+ context.request.signal,
36
+ ),
37
+ );
38
+ } catch (error) {
39
+ return facadeError(error, context.guardContext.requestId);
40
+ }
41
+ });
42
+ return app;
43
+ }
44
+
45
+ function json(body: unknown, status = 200): Response {
46
+ return Response.json(body, {
47
+ status,
48
+ headers: { "cache-control": "no-store" },
49
+ });
50
+ }
51
+
52
+ function facadeError(error: unknown, requestId: string): Response {
53
+ if (error instanceof DebugSessionFacadeError) {
54
+ return json(
55
+ {
56
+ code: error.code,
57
+ message: error.message,
58
+ requestId,
59
+ retryable: error.status >= 500,
60
+ },
61
+ error.status,
62
+ );
63
+ }
64
+ return json(
65
+ {
66
+ code: "PROVIDER_UNAVAILABLE",
67
+ message: "调试会话 Provider 暂不可用",
68
+ requestId,
69
+ retryable: true,
70
+ },
71
+ 503,
72
+ );
73
+ }
@@ -0,0 +1,106 @@
1
+ import type {
2
+ ModuleDebugSessionSummaryV1,
3
+ ModuleDebugSessionV1,
4
+ } from "@southwind-ai/server-sdk";
5
+
6
+ const MAX_SESSIONS = 100;
7
+ const MAX_MESSAGES = 64;
8
+ const MAX_MESSAGE_CHARS = 16_000;
9
+ const MAX_TOTAL_MESSAGE_CHARS = 128_000;
10
+
11
+ export class DebugSessionProviderContractError extends Error {}
12
+
13
+ export function validateDebugSessionPage(value: {
14
+ sessions: readonly ModuleDebugSessionSummaryV1[];
15
+ truncated: boolean;
16
+ next?: string;
17
+ }): {
18
+ sessions: readonly ModuleDebugSessionSummaryV1[];
19
+ truncated: boolean;
20
+ next?: string;
21
+ } {
22
+ if (
23
+ !value ||
24
+ !Array.isArray(value.sessions) ||
25
+ typeof value.truncated !== "boolean" ||
26
+ value.sessions.length > MAX_SESSIONS ||
27
+ (value.truncated ? !validText(value.next, 2_000) : value.next !== undefined)
28
+ ) {
29
+ invalidProvider();
30
+ }
31
+ const sourceIds = new Set<string>();
32
+ for (const session of value.sessions) {
33
+ validateSummary(session);
34
+ if (sourceIds.has(session.sourceId)) invalidProvider();
35
+ sourceIds.add(session.sourceId);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ export function validateDebugSessionDetail(
41
+ value: ModuleDebugSessionV1,
42
+ expectedSourceId: string,
43
+ ): ModuleDebugSessionV1 {
44
+ validateSummary(value);
45
+ if (
46
+ value.sourceId !== expectedSourceId ||
47
+ !Array.isArray(value.messages) ||
48
+ value.messages.length > MAX_MESSAGES
49
+ ) {
50
+ invalidProvider();
51
+ }
52
+ let previousSequence = -1;
53
+ let totalChars = 0;
54
+ for (const message of value.messages) {
55
+ if (
56
+ !message ||
57
+ !Number.isSafeInteger(message.sequence) ||
58
+ message.sequence <= previousSequence ||
59
+ (message.role !== "user" && message.role !== "assistant") ||
60
+ !validText(message.content, MAX_MESSAGE_CHARS, false) ||
61
+ !validDate(message.createdAt)
62
+ ) {
63
+ invalidProvider();
64
+ }
65
+ totalChars += message.content.length;
66
+ previousSequence = message.sequence;
67
+ }
68
+ if (totalChars > MAX_TOTAL_MESSAGE_CHARS) invalidProvider();
69
+ return value;
70
+ }
71
+
72
+ function validateSummary(value: ModuleDebugSessionSummaryV1): void {
73
+ if (
74
+ !value ||
75
+ !validText(value.sourceId, 200) ||
76
+ !validText(value.title, 200) ||
77
+ (value.scopeLabel !== undefined && !validText(value.scopeLabel, 120)) ||
78
+ !validDate(value.updatedAt) ||
79
+ !validText(value.revision, 200)
80
+ ) {
81
+ invalidProvider();
82
+ }
83
+ }
84
+
85
+ function validText(value: unknown, max: number, trim = true): value is string {
86
+ return (
87
+ typeof value === "string" &&
88
+ value.length <= max &&
89
+ value.trim().length > 0 &&
90
+ (!trim || value === value.trim())
91
+ );
92
+ }
93
+
94
+ function validDate(value: unknown): value is string {
95
+ return (
96
+ typeof value === "string" &&
97
+ value === value.trim() &&
98
+ value.length <= 40 &&
99
+ /(?:Z|[+-]\d{2}:\d{2})$/.test(value) &&
100
+ Number.isFinite(Date.parse(value))
101
+ );
102
+ }
103
+
104
+ function invalidProvider(): never {
105
+ throw new DebugSessionProviderContractError("调试会话 Provider 返回无效数据");
106
+ }
@@ -85,6 +85,7 @@ function values(binding: AgentRunBinding) {
85
85
  actorId: binding.actorId,
86
86
  idempotencyHash: binding.idempotencyHash,
87
87
  tokenHash: binding.tokenHash,
88
+ inputDigestCommitment: binding.inputDigestCommitment,
88
89
  scopeHash: binding.scopeHash,
89
90
  scopeSnapshot: binding.scopeSnapshot,
90
91
  authorizationSnapshot: binding.authorizationSnapshot,
@@ -103,6 +104,7 @@ function map(row: typeof agentRunBindings.$inferSelect): AgentRunBinding {
103
104
  actorId: row.actorId,
104
105
  idempotencyHash: row.idempotencyHash,
105
106
  tokenHash: row.tokenHash,
107
+ inputDigestCommitment: row.inputDigestCommitment,
106
108
  scopeHash: row.scopeHash,
107
109
  scopeSnapshot: row.scopeSnapshot,
108
110
  authorizationSnapshot: row.authorizationSnapshot,
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import type { AiProviderDataPolicy } from "@southwind-ai/modules";
3
3
  import {
4
4
  governProviderInput,
5
+ projectProviderInput,
5
6
  ProviderInputGovernanceError,
6
7
  } from "./provider-input-governance.js";
7
8
 
@@ -48,6 +49,23 @@ describe("Provider 输入字段治理", () => {
48
49
  expect(JSON.stringify(governed)).not.toContain("case-secret");
49
50
  });
50
51
 
52
+ test("承诺视图保留 allowlist 原值且不与脱敏占位符碰撞", () => {
53
+ expect(
54
+ projectProviderInput(
55
+ {
56
+ question: "联系 13800138000",
57
+ internalCaseId: "case-secret",
58
+ },
59
+ policy,
60
+ ),
61
+ ).toEqual({ question: "联系 13800138000" });
62
+ expect(
63
+ projectProviderInput({ question: "联系 [REDACTED]" }, policy),
64
+ ).not.toEqual(
65
+ projectProviderInput({ question: "联系 13800138000" }, policy),
66
+ );
67
+ });
68
+
51
69
  test.each([
52
70
  "password=do-not-send",
53
71
  "Bearer abcdefghijklmnop",
@@ -25,6 +25,32 @@ const dlpPatterns = [
25
25
  export function governProviderInput(
26
26
  input: ModuleAiJsonValue,
27
27
  policy: AiProviderDataPolicy,
28
+ ): ModuleAiJsonValue {
29
+ return selectProviderInput(input, policy, (value, action) =>
30
+ governValue(value, action),
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Build the stable execution-relevant input view without applying lossy DLP.
36
+ *
37
+ * Run attestation commits this view so exact caller input remains
38
+ * distinguishable even when two values redact to the same provider payload.
39
+ */
40
+ export function projectProviderInput(
41
+ input: ModuleAiJsonValue,
42
+ policy: AiProviderDataPolicy,
43
+ ): ModuleAiJsonValue {
44
+ return selectProviderInput(input, policy, (value) => cloneValue(value));
45
+ }
46
+
47
+ function selectProviderInput(
48
+ input: ModuleAiJsonValue,
49
+ policy: AiProviderDataPolicy,
50
+ transform: (
51
+ value: ModuleAiJsonValue,
52
+ action: AiProviderDlpAction,
53
+ ) => ModuleAiJsonValue,
28
54
  ): ModuleAiJsonValue {
29
55
  if (!isRecord(input)) {
30
56
  throw new ProviderInputGovernanceError(
@@ -37,11 +63,21 @@ export function governProviderInput(
37
63
  const segments = parseJsonPointer(rule.jsonPointer);
38
64
  const result = readPath(input, segments);
39
65
  if (!result.found) continue;
40
- writePath(governed, segments, governValue(result.value, rule.dlpAction));
66
+ writePath(governed, segments, transform(result.value, rule.dlpAction));
41
67
  }
42
68
  return governed;
43
69
  }
44
70
 
71
+ function cloneValue(value: ModuleAiJsonValue): ModuleAiJsonValue {
72
+ if (Array.isArray(value)) return value.map(cloneValue);
73
+ if (isRecord(value)) {
74
+ return Object.fromEntries(
75
+ Object.entries(value).map(([key, item]) => [key, cloneValue(item)]),
76
+ );
77
+ }
78
+ return value;
79
+ }
80
+
45
81
  function governValue(
46
82
  value: ModuleAiJsonValue,
47
83
  action: AiProviderDlpAction,
@@ -105,6 +105,11 @@ describe("RemoteAgentRuntime", () => {
105
105
  executionGrant: "grant_1",
106
106
  release: runtimeRelease(),
107
107
  traceId: "trace_request_1",
108
+ reporting: {
109
+ actorId: "user_1",
110
+ actorType: "user",
111
+ departmentId: "dept_1",
112
+ },
108
113
  };
109
114
  const runtime = new RemoteAgentRuntime({
110
115
  baseUrl: server.url.toString(),
@@ -128,6 +133,7 @@ describe("RemoteAgentRuntime", () => {
128
133
  toolNetworkPolicy: { mode: "forbidden" },
129
134
  release: operation.release,
130
135
  traceId: operation.traceId,
136
+ reporting: operation.reporting,
131
137
  });
132
138
  expect(
133
139
  (received as { limits: Record<string, unknown> }).limits,
@@ -185,6 +191,7 @@ describe("RemoteAgentRuntime", () => {
185
191
  executionGrant: "grant_1",
186
192
  release: runtimeRelease(),
187
193
  traceId: "trace_request_vision",
194
+ reporting: { actorId: "user_1", actorType: "user" },
188
195
  };
189
196
 
190
197
  await expect(
@@ -64,6 +64,7 @@ export class RemoteAgentRuntime implements AgentRuntimePort {
64
64
  executionGrant: request.executionGrant,
65
65
  release: request.release,
66
66
  traceId: request.traceId,
67
+ reporting: request.reporting,
67
68
  toolHostUrl: this.options.toolHostUrl,
68
69
  };
69
70
  const response = await this.request("/api/agent/runs", {
@@ -20,6 +20,7 @@ describe("Agent Run 持久授权绑定", () => {
20
20
  operationKey: "business.answer",
21
21
  releaseId: "release-1",
22
22
  idempotencyKey: "request-1",
23
+ input: { question: "same" },
23
24
  authorizationSnapshot: snapshot,
24
25
  context: context("actor-1", "role-1"),
25
26
  });
@@ -30,6 +31,7 @@ describe("Agent Run 持久授权绑定", () => {
30
31
  operationKey: "business.answer",
31
32
  releaseId: "release-1",
32
33
  idempotencyKey: "request-1",
34
+ input: { question: "same" },
33
35
  authorizationSnapshot: snapshot,
34
36
  context: context("actor-1", "role-1"),
35
37
  });
@@ -52,6 +54,7 @@ describe("Agent Run 持久授权绑定", () => {
52
54
  operationKey: "business.answer",
53
55
  releaseId: "release-1",
54
56
  idempotencyKey: "request-fast-tool",
57
+ input: { question: "fast" },
55
58
  authorizationSnapshot: snapshot,
56
59
  context: context("actor-1", "role-1"),
57
60
  });
@@ -75,6 +78,7 @@ describe("Agent Run 持久授权绑定", () => {
75
78
  operationKey: "business.answer",
76
79
  releaseId: "release-1",
77
80
  idempotencyKey: "request-denied",
81
+ input: { question: "denied" },
78
82
  authorizationSnapshot: snapshot,
79
83
  context: context("actor-1", "role-1"),
80
84
  });
@@ -102,6 +106,100 @@ describe("Agent Run 持久授权绑定", () => {
102
106
  message: "Agent Run 不存在或不可访问",
103
107
  });
104
108
  });
109
+
110
+ test("输入对象键序不影响承诺,但相同幂等键不能绑定不同输入", async () => {
111
+ const store = new AgentRunAuthorizationStore(
112
+ new InMemoryAgentRunBindingRepository(),
113
+ secret,
114
+ );
115
+ const currentContext = context("actor-1", "role-1");
116
+ const first = await store.issue({
117
+ operationKey: "business.answer",
118
+ releaseId: "release-1",
119
+ idempotencyKey: "request-input",
120
+ input: { question: "same", nested: { b: 2, a: 1 } },
121
+ authorizationSnapshot: snapshot,
122
+ context: currentContext,
123
+ });
124
+ await store.bind(first, "run-input");
125
+
126
+ await expect(
127
+ store.issue({
128
+ operationKey: "business.answer",
129
+ releaseId: "release-1",
130
+ idempotencyKey: "request-input",
131
+ input: { nested: { a: 1, b: 2 }, question: "same" },
132
+ authorizationSnapshot: snapshot,
133
+ context: currentContext,
134
+ }),
135
+ ).resolves.toMatchObject({ token: first.token });
136
+ await expect(
137
+ store.issue({
138
+ operationKey: "business.answer",
139
+ releaseId: "release-1",
140
+ idempotencyKey: "request-input",
141
+ input: { question: "different" },
142
+ authorizationSnapshot: snapshot,
143
+ context: currentContext,
144
+ }),
145
+ ).rejects.toMatchObject({ message: "Agent Run 不存在或不可访问" });
146
+ await expect(
147
+ store.verifyRunInput(
148
+ "business.answer",
149
+ "run-input",
150
+ { nested: { b: 2, a: 1 }, question: "same" },
151
+ currentContext,
152
+ ),
153
+ ).resolves.toBeDefined();
154
+ await expect(
155
+ store.verifyRunInput(
156
+ "business.answer",
157
+ "run-input",
158
+ { question: "different" },
159
+ currentContext,
160
+ ),
161
+ ).rejects.toMatchObject({ message: "Agent Run 不存在或不可访问" });
162
+ });
163
+
164
+ test("升级前绑定保留普通授权,但零值承诺不能通过输入验证", async () => {
165
+ const sourceStore = new AgentRunAuthorizationStore(
166
+ new InMemoryAgentRunBindingRepository(),
167
+ secret,
168
+ );
169
+ const currentContext = context("actor-1", "role-1");
170
+ const issued = await sourceStore.issue({
171
+ operationKey: "business.answer",
172
+ releaseId: "release-1",
173
+ idempotencyKey: "legacy-binding",
174
+ input: { question: "legacy" },
175
+ authorizationSnapshot: snapshot,
176
+ context: currentContext,
177
+ });
178
+ const repository = new InMemoryAgentRunBindingRepository();
179
+ await repository.createOrGet({
180
+ ...issued.binding,
181
+ runId: "run-legacy",
182
+ boundAt: Date.now(),
183
+ inputDigestCommitment: "0".repeat(64),
184
+ });
185
+ const upgradedStore = new AgentRunAuthorizationStore(repository, secret);
186
+
187
+ await expect(
188
+ upgradedStore.authorizeBrowser(
189
+ "business.answer",
190
+ "run-legacy",
191
+ currentContext,
192
+ ),
193
+ ).resolves.toBeDefined();
194
+ await expect(
195
+ upgradedStore.verifyRunInput(
196
+ "business.answer",
197
+ "run-legacy",
198
+ { question: "legacy" },
199
+ currentContext,
200
+ ),
201
+ ).rejects.toMatchObject({ message: "Agent Run 不存在或不可访问" });
202
+ });
105
203
  });
106
204
 
107
205
  function context(actorId: string, roleId: string) {
@@ -2,10 +2,15 @@ import { createHash, createHmac } from "node:crypto";
2
2
  import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
3
3
  import type { RequestGuardContext } from "../guards.js";
4
4
  import type { AgentRunAuthorizationSnapshot } from "@southwind-ai/shared";
5
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
5
6
  import type {
6
7
  AgentRunBinding,
7
8
  AgentRunBindingRepository,
8
9
  } from "./run-binding-repository.js";
10
+ import {
11
+ commitAgentRunInput,
12
+ sameInputCommitment,
13
+ } from "./run-input-commitment.js";
9
14
 
10
15
  export interface AgentExecutionAuthorization {
11
16
  readonly binding: AgentRunBinding;
@@ -28,6 +33,7 @@ export class AgentRunAuthorizationStore {
28
33
  operationKey: string;
29
34
  releaseId: string;
30
35
  idempotencyKey: string;
36
+ input: ModuleAiJsonValue;
31
37
  authorizationSnapshot: AgentRunAuthorizationSnapshot;
32
38
  context: RequestGuardContext;
33
39
  }): Promise<AgentExecutionAuthorization> {
@@ -41,12 +47,18 @@ export class AgentRunAuthorizationStore {
41
47
  ].join("\u0000");
42
48
  const idempotencyHash = this.hmac(`idempotency\u0000${coordinate}`);
43
49
  const token = `grant_${this.hmac(`token\u0000${coordinate}`)}`;
50
+ const tokenHash = sha256(token);
44
51
  const candidate: AgentRunBinding = {
45
52
  bindingId: `binding_${idempotencyHash.slice(0, 40)}`,
46
53
  operationKey: input.operationKey,
47
54
  actorId: input.context.actor.id,
48
55
  idempotencyHash,
49
- tokenHash: sha256(token),
56
+ tokenHash,
57
+ inputDigestCommitment: commitAgentRunInput(
58
+ this.secret,
59
+ tokenHash,
60
+ input.input,
61
+ ),
50
62
  scopeHash: scope.hash,
51
63
  scopeSnapshot: scope.snapshot,
52
64
  authorizationSnapshot: structuredClone(input.authorizationSnapshot),
@@ -117,6 +129,24 @@ export class AgentRunAuthorizationStore {
117
129
  return binding;
118
130
  }
119
131
 
132
+ async verifyRunInput(
133
+ operationKey: string,
134
+ runId: string,
135
+ input: ModuleAiJsonValue,
136
+ context: RequestGuardContext,
137
+ ) {
138
+ const binding = await this.authorizeBrowser(operationKey, runId, context);
139
+ const commitment = commitAgentRunInput(
140
+ this.secret,
141
+ binding.tokenHash,
142
+ input,
143
+ );
144
+ if (!sameInputCommitment(binding.inputDigestCommitment, commitment)) {
145
+ throw new AgentRunAuthorizationError();
146
+ }
147
+ return binding;
148
+ }
149
+
120
150
  assertCurrentScope(
121
151
  binding: AgentRunBinding,
122
152
  context: RequestGuardContext,
@@ -151,6 +181,7 @@ function sameAuthorization(
151
181
  existing.operationKey === candidate.operationKey &&
152
182
  existing.actorId === candidate.actorId &&
153
183
  existing.tokenHash === candidate.tokenHash &&
184
+ existing.inputDigestCommitment === candidate.inputDigestCommitment &&
154
185
  existing.scopeHash === candidate.scopeHash &&
155
186
  sameSnapshot(
156
187
  existing.authorizationSnapshot,
@@ -10,6 +10,7 @@ export interface AgentRunBinding {
10
10
  readonly actorId: string;
11
11
  readonly idempotencyHash: string;
12
12
  readonly tokenHash: string;
13
+ readonly inputDigestCommitment: string;
13
14
  readonly scopeHash: string;
14
15
  readonly scopeSnapshot: AuthorizationScopeSnapshot;
15
16
  readonly authorizationSnapshot: AgentRunAuthorizationSnapshot;
@@ -38,6 +38,11 @@ export interface AgentRuntimeOperation {
38
38
  executionGrant: string;
39
39
  release: AgentRuntimeRelease;
40
40
  traceId: string;
41
+ reporting: {
42
+ actorId: string;
43
+ actorType: "anonymous" | "user" | "service";
44
+ departmentId?: string;
45
+ };
41
46
  }
42
47
 
43
48
  export interface AgentRuntimeRelease {