create-windy 0.3.17 → 0.3.18

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 (57) hide show
  1. package/README.md +14 -36
  2. package/dist/cli.js +1528 -3078
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +6 -0
  6. package/template/README.md +5 -10
  7. package/template/apps/agent-server/Dockerfile +2 -1
  8. package/template/apps/agent-server/README.md +13 -5
  9. package/template/apps/agent-server/adk_web_agents/canonical_debug/__init__.py +1 -0
  10. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +125 -0
  11. package/template/apps/agent-server/project/adk-web-debug.json +4 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +315 -0
  13. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +235 -0
  14. package/template/apps/server/src/agent/run-debug-authorization.ts +27 -0
  15. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +56 -0
  16. package/template/apps/server/src/agent/run-facade.test-support.ts +235 -0
  17. package/template/apps/server/src/agent/run-facade.test.ts +8 -244
  18. package/template/apps/server/src/agent/run-facade.ts +23 -48
  19. package/template/apps/server/src/agent/run-operation-registry.ts +51 -0
  20. package/template/apps/server/src/agent/runtime-bootstrap.ts +5 -2
  21. package/template/apps/server/src/agent/tool-audit.ts +3 -0
  22. package/template/apps/server/src/agent/tool-registry.test.ts +29 -0
  23. package/template/apps/server/src/application-services.ts +83 -15
  24. package/template/apps/server/src/guards.ts +16 -0
  25. package/template/apps/server/src/index.ts +23 -20
  26. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +0 -1
  27. package/template/apps/server/src/jobs/legacy-adapter.test.ts +6 -0
  28. package/template/apps/server/src/jobs/legacy-service-runtime.ts +18 -0
  29. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +84 -0
  30. package/template/apps/server/src/module-host/durable-context.test.ts +14 -0
  31. package/template/apps/server/src/module-host/durable-context.ts +17 -15
  32. package/template/apps/server/src/module-host/request-context.test.ts +30 -1
  33. package/template/apps/server/src/module-host/request-context.ts +15 -2
  34. package/template/apps/server/src/module-host/storage-port.ts +1 -1
  35. package/template/apps/server/src/platform-bootstrap.ts +16 -0
  36. package/template/apps/server/src/runtime-adk-debug.test.ts +246 -0
  37. package/template/apps/server/src/runtime-adk-debug.ts +158 -0
  38. package/template/apps/server/src/runtime-request-actor.test.ts +55 -0
  39. package/template/apps/server/src/runtime-request-actor.ts +22 -0
  40. package/template/apps/server/src/runtime.test.ts +3 -0
  41. package/template/apps/server/src/runtime.ts +36 -3
  42. package/template/apps/server/src/scheduler/definitions.test.ts +16 -0
  43. package/template/docker-compose.agent-debug.yml +56 -0
  44. package/template/docker-compose.yml +1 -39
  45. package/template/docs/architecture/ai-runtime.md +14 -5
  46. package/template/docs/architecture/module-manifest-composition.md +14 -0
  47. package/template/docs/development/custom-login-page.md +2 -16
  48. package/template/docs/platform/agent-runtime.md +57 -16
  49. package/template/docs/platform/agent-tool-sandbox.md +4 -3
  50. package/template/docs/platform/platform-authentication.md +9 -1
  51. package/template/package.json +1 -1
  52. package/template/packages/database/drizzle/0043_activate_agent_dependencies.sql +196 -0
  53. package/template/packages/database/drizzle/meta/0043_snapshot.json +8335 -0
  54. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  55. package/template/packages/database/src/schema-notification-delivery.test.ts +15 -6
  56. package/template/packages/database/src/schema-workflow-notification.test.ts +44 -29
  57. package/template/scripts/dev-adk-web.ts +107 -0
@@ -5,7 +5,9 @@ import { buildActorScopeContexts } from "../data-access/context.js";
5
5
  import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
6
6
  import type { RequestActor } from "../guards.js";
7
7
  import { createServerRuntime } from "../runtime.js";
8
+ // @windy-module system.storage begin
8
9
  import { createStorageRuntime } from "../storage/runtime.js";
10
+ // @windy-module system.storage end
9
11
  import { createModuleDurableContextualizer } from "./durable-context.js";
10
12
  import { initializeModuleHosts } from "./initialize.js";
11
13
  import { manifest } from "./test-fixtures.js";
@@ -82,6 +84,7 @@ describe("模块 Durable Handler 授权上下文", () => {
82
84
  ]);
83
85
  });
84
86
 
87
+ // @windy-module system.storage begin
85
88
  test("重启后按 requestedBy 重建 Actor 并允许同案跨上传者读取", async () => {
86
89
  const { contextualize, storage, actor, resolverCalls, runtime } =
87
90
  await setup();
@@ -131,7 +134,9 @@ describe("模块 Durable Handler 授权上下文", () => {
131
134
  }),
132
135
  );
133
136
  });
137
+ // @windy-module system.storage end
134
138
 
139
+ // @windy-module system.storage begin
135
140
  test("Scope 变化、权限撤销与资源绑定漂移均 fail-closed", async () => {
136
141
  const stable = currentActor();
137
142
  const scopeChanged = currentActor("self");
@@ -182,6 +187,7 @@ describe("模块 Durable Handler 授权上下文", () => {
182
187
  }),
183
188
  );
184
189
  });
190
+ // @windy-module system.storage end
185
191
  });
186
192
 
187
193
  async function setup(
@@ -220,7 +226,9 @@ async function setup(
220
226
  },
221
227
  ],
222
228
  });
229
+ // @windy-module system.storage begin
223
230
  const storage = createStorageRuntime({}, undefined, hosts.uploadPolicies);
231
+ // @windy-module system.storage end
224
232
  const runtime = createServerRuntime(
225
233
  { NODE_ENV: "test" },
226
234
  { featureCatalog: [evidenceFeature] },
@@ -228,14 +236,18 @@ async function setup(
228
236
  let calls = 0;
229
237
  return {
230
238
  actor: currentActor(),
239
+ // @windy-module system.storage begin
231
240
  storage,
241
+ // @windy-module system.storage end
232
242
  runtime,
233
243
  resolverCalls: () => calls,
234
244
  contextualize: createModuleDurableContextualizer({
235
245
  hosts,
236
246
  runtime,
237
247
  features: [evidenceFeature],
248
+ // @windy-module system.storage begin
238
249
  storage,
250
+ // @windy-module system.storage end
239
251
  backgroundAi,
240
252
  async resolveActor() {
241
253
  calls += 1;
@@ -283,6 +295,7 @@ function baseContext(attemptNumber: number): DurableHandlerContext {
283
295
  };
284
296
  }
285
297
 
298
+ // @windy-module system.storage begin
286
299
  async function seedFile(storage: ReturnType<typeof createStorageRuntime>) {
287
300
  const bytes = new TextEncoder().encode("evidence");
288
301
  const sha256 = new Bun.CryptoHasher("sha256").update(bytes).digest("hex");
@@ -310,3 +323,4 @@ async function seedFile(storage: ReturnType<typeof createStorageRuntime>) {
310
323
  });
311
324
  return storage.uploads.complete(session.id, "user-uploader");
312
325
  }
326
+ // @windy-module system.storage end
@@ -21,6 +21,7 @@ import type { InitializedModuleHosts } from "./initialize.js";
21
21
  import {
22
22
  createModuleStoragePort,
23
23
  type ModuleStorageServices,
24
+ unavailableModuleStoragePort,
24
25
  } from "./storage-port.js";
25
26
  import { createModuleStorageScope } from "./storage-scope.js";
26
27
  import { toModuleActor } from "./request-context.js";
@@ -29,7 +30,7 @@ export interface ModuleDurableContextServices {
29
30
  readonly hosts: InitializedModuleHosts;
30
31
  readonly runtime: ReturnType<typeof createServerRuntime>;
31
32
  readonly features: readonly FeatureFlagDefinition[];
32
- readonly storage: ModuleStorageServices;
33
+ readonly storage?: ModuleStorageServices;
33
34
  readonly backgroundAi?: {
34
35
  create(
35
36
  moduleName: string,
@@ -75,20 +76,21 @@ export function createModuleDurableContextualizer(
75
76
  request,
76
77
  actor,
77
78
  );
78
- const storage = createModuleStoragePort(
79
- services.storage,
80
- createModuleStorageScope({
81
- moduleName: input.moduleName,
82
- purposes: services.hosts.uploadPurposesOf(input.moduleName),
83
- resourceFileAccessPolicies: services.hosts.resourceFileAccessPoliciesOf(
84
- input.moduleName,
85
- ),
86
- guardContext,
87
- runtime: services.runtime,
88
- features: services.features,
89
- signal: input.context.signal,
90
- }),
91
- );
79
+ const storage = services.storage
80
+ ? createModuleStoragePort(
81
+ services.storage,
82
+ createModuleStorageScope({
83
+ moduleName: input.moduleName,
84
+ purposes: services.hosts.uploadPurposesOf(input.moduleName),
85
+ resourceFileAccessPolicies:
86
+ services.hosts.resourceFileAccessPoliciesOf(input.moduleName),
87
+ guardContext,
88
+ runtime: services.runtime,
89
+ features: services.features,
90
+ signal: input.context.signal,
91
+ }),
92
+ )
93
+ : unavailableModuleStoragePort(input.moduleName);
92
94
  const moduleContext: ModuleDurableHandlerContext = {
93
95
  ...input.context,
94
96
  actor: toModuleActor(actor),
@@ -3,7 +3,12 @@ import {
3
3
  buildActorScopeContexts,
4
4
  buildScopeContext,
5
5
  } from "../data-access/context.js";
6
- import type { RequestActor } from "../guards.js";
6
+ import {
7
+ // @windy-module system.agent begin
8
+ bindModuleUserProjection,
9
+ // @windy-module system.agent end
10
+ type RequestActor,
11
+ } from "../guards.js";
7
12
  import { createServerRuntime } from "../runtime.js";
8
13
  import { manifest } from "./test-fixtures.js";
9
14
  import {
@@ -73,6 +78,30 @@ describe("模块 Actor 运行时边界", () => {
73
78
  ).toThrow("不是由当前宿主颁发");
74
79
  });
75
80
 
81
+ // @windy-module system.agent begin
82
+ test("开发调试委托只向业务 Module 投影真实用户,核心主体保持不变", () => {
83
+ const requestActor = bindModuleUserProjection(
84
+ {
85
+ id: "service_adk_debug",
86
+ type: "service",
87
+ name: "ADK 开发调试",
88
+ roleCodes: ["adk-debug"],
89
+ permissionKeys: ["analysis.copilot.execute"],
90
+ },
91
+ "user_dev_admin",
92
+ );
93
+
94
+ const actor = toModuleActor(requestActor);
95
+
96
+ expect(actor).toMatchObject({
97
+ id: "user_dev_admin",
98
+ type: "user",
99
+ name: "ADK 开发调试",
100
+ });
101
+ expect(requestActorFromModule(actor)).toBe(requestActor);
102
+ });
103
+ // @windy-module system.agent end
104
+
76
105
  test("模块提交任务时由宿主持久化 Actor 与 nominal Scope 快照", async () => {
77
106
  const submissions: unknown[] = [];
78
107
  const actor: RequestActor = {
@@ -19,6 +19,9 @@ import type { AuditEvent } from "@southwind-ai/shared";
19
19
  import type { ModuleManifest } from "@southwind-ai/modules";
20
20
  import {
21
21
  auditActor,
22
+ // @windy-module system.agent begin
23
+ projectedModuleUserId,
24
+ // @windy-module system.agent end
22
25
  type RequestActor,
23
26
  type RequestGuardContext,
24
27
  } from "../guards.js";
@@ -38,9 +41,9 @@ const requestActorByModuleView = new WeakMap<object, RequestActor>();
38
41
  * 联系方式与数据范围令牌在运行时通过属性枚举泄漏给业务模块。
39
42
  */
40
43
  export function toModuleActor(actor: RequestActor): ModuleActor {
44
+ const identity = moduleActorIdentity(actor);
41
45
  const view: ModuleActor = Object.freeze({
42
- id: actor.id,
43
- type: actor.type,
46
+ ...identity,
44
47
  name: actor.name,
45
48
  ...(actor.username ? { username: actor.username } : {}),
46
49
  ...(actor.displayName ? { displayName: actor.displayName } : {}),
@@ -51,6 +54,16 @@ export function toModuleActor(actor: RequestActor): ModuleActor {
51
54
  return view;
52
55
  }
53
56
 
57
+ function moduleActorIdentity(
58
+ actor: RequestActor,
59
+ ): Pick<ModuleActor, "id" | "type"> {
60
+ // @windy-module system.agent begin
61
+ const userId = projectedModuleUserId(actor);
62
+ if (userId) return { id: userId, type: "user" };
63
+ // @windy-module system.agent end
64
+ return { id: actor.id, type: actor.type };
65
+ }
66
+
54
67
  /**
55
68
  * Host 内部适配器通过对象身份恢复原始 Actor,使平台内置模块仍能复用宿主的数据
56
69
  * 范围执行链;原始 Actor 不作为 SDK 字段下发,外部业务包也无法访问该映射。
@@ -180,7 +180,7 @@ function forbiddenResourceFile(): UploadError {
180
180
  export function unavailableModuleStoragePort(
181
181
  moduleName: string,
182
182
  ): ModuleStoragePort {
183
- const fail = (): never => {
183
+ const fail = async (): Promise<never> => {
184
184
  throw new Error(`模块 ${moduleName} 的存储 Port 未挂载`);
185
185
  };
186
186
  return {
@@ -0,0 +1,16 @@
1
+ import { loadPlatformConfig } from "@southwind-ai/config";
2
+ import pino from "pino";
3
+ import { createServerPersistence } from "./persistence.js";
4
+
5
+ export function createPlatformBootstrap() {
6
+ const logger = pino({ name: "windy-server" });
7
+ const platformConfig = loadPlatformConfig();
8
+ const persistence = createServerPersistence(
9
+ process.env,
10
+ (status) => {
11
+ logger.error({ audit: status }, "审计持久化进入降级状态");
12
+ },
13
+ platformConfig,
14
+ );
15
+ return { logger, persistence, platformConfig };
16
+ }
@@ -0,0 +1,246 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { buildActorScopeContexts } from "./data-access/context.js";
4
+ import { projectedModuleUserId, type RequestActor } from "./guards.js";
5
+ import { createAdkDebugRuntime } from "./runtime-adk-debug.js";
6
+
7
+ const token = "debug-token-012345678901234567890123";
8
+
9
+ describe("ADK Web 专用调试身份", () => {
10
+ test("只在 canonical Run 路径委托真实用户并收窄权限", async () => {
11
+ const runtime = createRuntime();
12
+ expect(
13
+ runtime.claimsRequest(request("/api/foundation", "GET", token)),
14
+ ).toBeTrue();
15
+ const actor = await runtime.resolveRequest(
16
+ request("/api/agent/runs", "POST", token),
17
+ );
18
+
19
+ expect(actor).toMatchObject({
20
+ id: "service_adk_debug",
21
+ type: "service",
22
+ roleCodes: ["adk-debug"],
23
+ permissionKeys: ["customer.copilot.execute", "customer.records.read"],
24
+ });
25
+ expect(actor && projectedModuleUserId(actor)).toBe("user_dev_admin");
26
+ expect(actor?.scopeContext?.actorId).toBe("user_dev_admin");
27
+ expect(actor?.permissionKeys).not.toContain("customer.admin.manage");
28
+ expect(
29
+ await runtime.resolveRequest(request("/api/foundation", "GET", token)),
30
+ ).toBeUndefined();
31
+ expect(
32
+ await runtime.resolveRequest(
33
+ request("/api/agent/runs/run_1/result", "GET"),
34
+ ),
35
+ ).toBeUndefined();
36
+ });
37
+
38
+ test("持久 Run 的 Tool Host 可恢复同一个受限身份", async () => {
39
+ const runtime = createRuntime();
40
+
41
+ expect(
42
+ (await runtime.resolveActor("service_adk_debug"))?.permissionKeys,
43
+ ).toEqual(["customer.copilot.execute", "customer.records.read"]);
44
+ expect(runtime.operationForActor("service_adk_debug")).toBe(
45
+ "customer.copilot.global",
46
+ );
47
+ expect(await runtime.resolveActor("user_dev_admin")).toBeUndefined();
48
+ });
49
+
50
+ test("数据用户不存在或本身缺少闭包权限时拒绝委托", async () => {
51
+ expect(
52
+ await createRuntime({ dataActor: undefined }).resolveRequest(
53
+ request("/api/agent/runs", "POST", token),
54
+ ),
55
+ ).toBeUndefined();
56
+ expect(
57
+ await createRuntime({
58
+ dataActor: actor(["customer.copilot.execute"]),
59
+ }).resolveRequest(request("/api/agent/runs", "POST", token)),
60
+ ).toBeUndefined();
61
+ expect(
62
+ await createRuntime({
63
+ dataActor: { ...actor(), id: "user_other" },
64
+ }).resolveRequest(request("/api/agent/runs", "POST", token)),
65
+ ).toBeUndefined();
66
+ });
67
+
68
+ test("非开发环境、短 Token、未知 Operation 和未知 Tool 均 fail closed", async () => {
69
+ expect(
70
+ await createAdkDebugRuntime(
71
+ {
72
+ NODE_ENV: "production",
73
+ WINDY_ADK_DEBUG_OPERATION_KEY: "customer.copilot.global",
74
+ WINDY_ADK_DEBUG_DATA_ACTOR_ID: "user_dev_admin",
75
+ WINDY_ADK_DEBUG_TOKEN: token,
76
+ },
77
+ [manifest()],
78
+ ).resolveRequest(request("/api/agent/runs", "POST", token)),
79
+ ).toBeUndefined();
80
+ expect(() =>
81
+ createAdkDebugRuntime(
82
+ {
83
+ NODE_ENV: "development",
84
+ WINDY_ADK_DEBUG_OPERATION_KEY: "customer.copilot.global",
85
+ WINDY_ADK_DEBUG_DATA_ACTOR_ID: "user_dev_admin",
86
+ WINDY_ADK_DEBUG_TOKEN: "short",
87
+ },
88
+ [manifest()],
89
+ () => actor(),
90
+ ),
91
+ ).toThrow("至少 32");
92
+ expect(() =>
93
+ createAdkDebugRuntime(
94
+ {
95
+ NODE_ENV: "development",
96
+ WINDY_ADK_DEBUG_OPERATION_KEY: "customer.copilot.global",
97
+ WINDY_ADK_DEBUG_DATA_ACTOR_ID: "user_dev_admin",
98
+ WINDY_ADK_DEBUG_TOKEN: token,
99
+ AGENT_SERVER_INTERNAL_TOKEN: token,
100
+ },
101
+ [manifest()],
102
+ () => actor(),
103
+ ),
104
+ ).toThrow("内部服务 Token");
105
+ expect(() => createRuntime({ operationKey: "customer.unknown" })).toThrow(
106
+ "Operation 必须唯一存在",
107
+ );
108
+ expect(() =>
109
+ createRuntime({ allowedToolKeys: ["customer.unknown"] }),
110
+ ).toThrow("Tool 必须唯一存在");
111
+ });
112
+ });
113
+
114
+ function createRuntime(
115
+ overrides: {
116
+ operationKey?: string;
117
+ allowedToolKeys?: string[];
118
+ dataActor?: RequestActor;
119
+ } = {},
120
+ ) {
121
+ const dataActor = Object.hasOwn(overrides, "dataActor")
122
+ ? overrides.dataActor
123
+ : actor();
124
+ return createAdkDebugRuntime(
125
+ {
126
+ NODE_ENV: "development",
127
+ WINDY_ADK_DEBUG_OPERATION_KEY:
128
+ overrides.operationKey ?? "customer.copilot.global",
129
+ WINDY_ADK_DEBUG_DATA_ACTOR_ID: "user_dev_admin",
130
+ WINDY_ADK_DEBUG_TOKEN: token,
131
+ },
132
+ [manifest(overrides.allowedToolKeys)],
133
+ (actorId) => (actorId === "user_dev_admin" ? dataActor : undefined),
134
+ );
135
+ }
136
+
137
+ function actor(
138
+ permissionKeys = [
139
+ "customer.copilot.execute",
140
+ "customer.records.read",
141
+ "customer.admin.manage",
142
+ ],
143
+ ): RequestActor {
144
+ return {
145
+ id: "user_dev_admin",
146
+ type: "user",
147
+ name: "开发管理员",
148
+ displayName: "开发管理员",
149
+ roleCodes: ["super-admin"],
150
+ permissionKeys,
151
+ ...buildActorScopeContexts("user_dev_admin", "dept_platform", [
152
+ {
153
+ id: "role_super_admin",
154
+ code: "super-admin",
155
+ dataScope: "all",
156
+ departmentIds: [],
157
+ },
158
+ ]),
159
+ };
160
+ }
161
+
162
+ function request(path: string, method: string, bearer = "") {
163
+ return new Request(`http://localhost${path}`, {
164
+ method,
165
+ headers: bearer ? { authorization: `Bearer ${bearer}` } : undefined,
166
+ });
167
+ }
168
+
169
+ function manifest(
170
+ allowedToolKeys = ["customer.records.search"],
171
+ ): ModuleManifest {
172
+ return {
173
+ name: "customer",
174
+ version: "0.1.0",
175
+ label: "客户",
176
+ description: "客户",
177
+ menus: [],
178
+ adminRoutes: [],
179
+ permissions: [
180
+ permission("customer.copilot.execute"),
181
+ permission("customer.records.read"),
182
+ permission("customer.admin.manage"),
183
+ ],
184
+ apiPermissions: [],
185
+ features: [],
186
+ migrations: [],
187
+ schemaExports: [],
188
+ tasks: [],
189
+ tools: [
190
+ {
191
+ key: "customer.records.search",
192
+ label: "查询记录",
193
+ description: "查询记录",
194
+ inputSchemaKey: "customer.records.input.v1",
195
+ outputSchemaKey: "customer.records.output.v1",
196
+ operation: "query",
197
+ permissionKey: "customer.records.read",
198
+ featureKey: "customer.records",
199
+ auditAction: "customer.records.read",
200
+ scopeMode: "request",
201
+ dataClassification: "internal",
202
+ idempotency: "none",
203
+ approval: "none",
204
+ limits: {
205
+ maxDurationSeconds: 10,
206
+ maxInputBytes: 4096,
207
+ maxOutputBytes: 4096,
208
+ },
209
+ },
210
+ ],
211
+ aiOperations: [
212
+ {
213
+ key: "customer.copilot.global",
214
+ kind: "agent",
215
+ featureKey: "customer.copilot",
216
+ permissionKey: "customer.copilot.execute",
217
+ auditAction: "customer.copilot.execute",
218
+ inputSchemaKey: "customer.copilot.input.v1",
219
+ outputSchemaKey: "customer.copilot.output.v1",
220
+ requiredCapabilities: ["text-generation"],
221
+ allowedToolKeys,
222
+ dataClassification: "internal",
223
+ execution: "streaming",
224
+ limits: {
225
+ maxDurationSeconds: 60,
226
+ maxInputBytes: 4096,
227
+ maxOutputBytes: 4096,
228
+ },
229
+ },
230
+ ],
231
+ providers: [],
232
+ auditActions: [],
233
+ };
234
+ }
235
+
236
+ function permission(key: string) {
237
+ return {
238
+ key,
239
+ label: key,
240
+ module: "customer",
241
+ moduleLabel: "客户",
242
+ resource: key,
243
+ resourceLabel: key,
244
+ action: "manage" as const,
245
+ };
246
+ }
@@ -0,0 +1,158 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { readBearerToken } from "./auth/bearer-token.js";
4
+ import { isServerScopeContext } from "./data-access/context.js";
5
+ import { loadFoundationModules } from "./foundation.js";
6
+ import { bindModuleUserProjection, type RequestActor } from "./guards.js";
7
+
8
+ const debugActorId = "service_adk_debug";
9
+ const minimumTokenBytes = 32;
10
+
11
+ export type AdkDebugActorResolver = (
12
+ actorId: string,
13
+ ) => Promise<RequestActor | undefined> | RequestActor | undefined;
14
+
15
+ export function createAdkDebugRuntime(
16
+ env: Record<string, string | undefined>,
17
+ modules: readonly ModuleManifest[] = loadFoundationModules(),
18
+ actorResolver?: AdkDebugActorResolver,
19
+ ) {
20
+ const operationKey = env.WINDY_ADK_DEBUG_OPERATION_KEY?.trim();
21
+ if (env.NODE_ENV !== "development" || !operationKey) {
22
+ return disabledRuntime();
23
+ }
24
+ const dataActorId = env.WINDY_ADK_DEBUG_DATA_ACTOR_ID?.trim();
25
+ if (!dataActorId) {
26
+ throw new Error("ADK Web 调试必须选择真实开发数据用户");
27
+ }
28
+ if (!actorResolver) {
29
+ throw new Error("ADK Web 调试数据用户解析器未配置");
30
+ }
31
+ const token = env.WINDY_ADK_DEBUG_TOKEN?.trim();
32
+ if (!token || Buffer.byteLength(token) < minimumTokenBytes) {
33
+ throw new Error("ADK Web 调试 Token 必须至少 32 字节");
34
+ }
35
+ const internalToken = env.AGENT_SERVER_INTERNAL_TOKEN?.trim();
36
+ if (internalToken && secureEqual(token, internalToken)) {
37
+ throw new Error("ADK Web 调试 Token 不能复用 Agent 内部服务 Token");
38
+ }
39
+ const permissionKeys = resolvePermissionClosure(modules, operationKey);
40
+ const claimsRequest = (request: Request) => {
41
+ const provided = readBearerToken(request);
42
+ return Boolean(provided && secureEqual(provided, token));
43
+ };
44
+
45
+ return {
46
+ claimsRequest,
47
+ async resolveRequest(request: Request): Promise<RequestActor | undefined> {
48
+ if (!claimsRequest(request) || !isCanonicalRunRequest(request)) {
49
+ return undefined;
50
+ }
51
+ return resolveDelegatedActor(actorResolver, dataActorId, permissionKeys);
52
+ },
53
+ async resolveActor(actorId: string): Promise<RequestActor | undefined> {
54
+ return actorId === debugActorId
55
+ ? resolveDelegatedActor(actorResolver, dataActorId, permissionKeys)
56
+ : undefined;
57
+ },
58
+ operationForActor(actorId: string): string | undefined {
59
+ return actorId === debugActorId ? operationKey : undefined;
60
+ },
61
+ usesToken(candidate: string | undefined): boolean {
62
+ return Boolean(candidate && secureEqual(candidate, token));
63
+ },
64
+ };
65
+ }
66
+
67
+ async function resolveDelegatedActor(
68
+ resolver: AdkDebugActorResolver,
69
+ dataActorId: string,
70
+ permissionKeys: string[],
71
+ ): Promise<RequestActor | undefined> {
72
+ const user = await resolver(dataActorId);
73
+ if (
74
+ !user ||
75
+ user.id !== dataActorId ||
76
+ user.type !== "user" ||
77
+ !isServerScopeContext(user.scopeContext) ||
78
+ user.scopeContext.actorId !== user.id ||
79
+ !permissionKeys.every((key) => user.permissionKeys.includes(key))
80
+ ) {
81
+ return undefined;
82
+ }
83
+ return bindModuleUserProjection(
84
+ {
85
+ id: debugActorId,
86
+ type: "service",
87
+ name: "ADK 开发调试",
88
+ displayName: `ADK 开发调试(${user.displayName || user.name})`,
89
+ roleCodes: ["adk-debug"],
90
+ permissionKeys: [...permissionKeys],
91
+ dataAccess: user.dataAccess,
92
+ scopeContext: user.scopeContext,
93
+ },
94
+ user.id,
95
+ );
96
+ }
97
+
98
+ function resolvePermissionClosure(
99
+ modules: readonly ModuleManifest[],
100
+ operationKey: string,
101
+ ): string[] {
102
+ const operations = modules.flatMap((module) =>
103
+ (module.aiOperations ?? [])
104
+ .filter(({ key }) => key === operationKey)
105
+ .map((operation) => ({ module, operation })),
106
+ );
107
+ if (operations.length !== 1) {
108
+ throw new Error(`ADK Web 调试 Operation 必须唯一存在:${operationKey}`);
109
+ }
110
+ const [{ operation }] = operations;
111
+ if (operation.kind !== "agent") {
112
+ throw new Error(`ADK Web 只能调试 Agent Operation:${operationKey}`);
113
+ }
114
+ const toolPermissions = operation.allowedToolKeys.map((toolKey) => {
115
+ const tools = modules.flatMap((module) =>
116
+ module.tools.filter(({ key }) => key === toolKey),
117
+ );
118
+ if (tools.length !== 1) {
119
+ throw new Error(`ADK Web 调试 Tool 必须唯一存在:${toolKey}`);
120
+ }
121
+ return tools[0]!.permissionKey;
122
+ });
123
+ return [...new Set([operation.permissionKey, ...toolPermissions])];
124
+ }
125
+
126
+ function isCanonicalRunRequest(request: Request): boolean {
127
+ const path = new URL(request.url).pathname.replace(/\/+$/, "");
128
+ if (request.method === "POST" && path === "/api/agent/runs") return true;
129
+ const match = path.match(
130
+ /^\/api\/agent\/runs\/[^/]+\/(events|result|cancel|attach)$/,
131
+ );
132
+ if (!match) return false;
133
+ return match[1] === "events" || match[1] === "result"
134
+ ? request.method === "GET"
135
+ : request.method === "POST";
136
+ }
137
+
138
+ function secureEqual(left: string, right: string): boolean {
139
+ const leftBytes = Buffer.from(left);
140
+ const rightBytes = Buffer.from(right);
141
+ return (
142
+ leftBytes.length === rightBytes.length &&
143
+ timingSafeEqual(leftBytes, rightBytes)
144
+ );
145
+ }
146
+
147
+ function disabledRuntime() {
148
+ return {
149
+ claimsRequest: (_request: Request): boolean => false,
150
+ resolveRequest: async (
151
+ _request: Request,
152
+ ): Promise<RequestActor | undefined> => undefined,
153
+ resolveActor: async (_actorId: string): Promise<RequestActor | undefined> =>
154
+ undefined,
155
+ operationForActor: (_actorId: string): string | undefined => undefined,
156
+ usesToken: (_candidate: string | undefined) => false,
157
+ };
158
+ }