create-windy 0.3.16 → 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 (65) hide show
  1. package/README.md +14 -36
  2. package/dist/cli.js +1831 -3178
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +7 -1
  6. package/template/README.md +8 -11
  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/Dockerfile +1 -1
  15. package/template/apps/server/src/agent/run-debug-authorization.ts +27 -0
  16. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +56 -0
  17. package/template/apps/server/src/agent/run-facade.test-support.ts +235 -0
  18. package/template/apps/server/src/agent/run-facade.test.ts +8 -244
  19. package/template/apps/server/src/agent/run-facade.ts +23 -48
  20. package/template/apps/server/src/agent/run-operation-registry.ts +51 -0
  21. package/template/apps/server/src/agent/runtime-bootstrap.ts +5 -2
  22. package/template/apps/server/src/agent/tool-audit.ts +3 -0
  23. package/template/apps/server/src/agent/tool-registry.test.ts +29 -0
  24. package/template/apps/server/src/application-services.ts +83 -15
  25. package/template/apps/server/src/audit/contracts.ts +8 -0
  26. package/template/apps/server/src/audit/recovery-queue.test.ts +48 -1
  27. package/template/apps/server/src/audit/recovery-queue.ts +73 -4
  28. package/template/apps/server/src/audit/reliable-writer.test.ts +120 -2
  29. package/template/apps/server/src/audit/reliable-writer.ts +98 -34
  30. package/template/apps/server/src/guards.ts +16 -0
  31. package/template/apps/server/src/index.ts +23 -20
  32. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +0 -1
  33. package/template/apps/server/src/jobs/legacy-adapter.test.ts +6 -0
  34. package/template/apps/server/src/jobs/legacy-service-runtime.ts +18 -0
  35. package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +84 -0
  36. package/template/apps/server/src/module-host/durable-context.test.ts +14 -0
  37. package/template/apps/server/src/module-host/durable-context.ts +17 -15
  38. package/template/apps/server/src/module-host/request-context.test.ts +30 -1
  39. package/template/apps/server/src/module-host/request-context.ts +15 -2
  40. package/template/apps/server/src/module-host/storage-port.ts +1 -1
  41. package/template/apps/server/src/platform-bootstrap.ts +16 -0
  42. package/template/apps/server/src/runtime-adk-debug.test.ts +246 -0
  43. package/template/apps/server/src/runtime-adk-debug.ts +158 -0
  44. package/template/apps/server/src/runtime-request-actor.test.ts +55 -0
  45. package/template/apps/server/src/runtime-request-actor.ts +22 -0
  46. package/template/apps/server/src/runtime.test.ts +3 -0
  47. package/template/apps/server/src/runtime.ts +36 -3
  48. package/template/apps/server/src/scheduler/definitions.test.ts +16 -0
  49. package/template/apps/web/Dockerfile +1 -1
  50. package/template/docker-compose.agent-debug.yml +56 -0
  51. package/template/docker-compose.yml +1 -39
  52. package/template/docs/architecture/ai-runtime.md +14 -5
  53. package/template/docs/architecture/module-manifest-composition.md +14 -0
  54. package/template/docs/development/custom-login-page.md +2 -16
  55. package/template/docs/platform/agent-runtime.md +57 -16
  56. package/template/docs/platform/agent-tool-sandbox.md +4 -3
  57. package/template/docs/platform/audit-reliability.md +33 -5
  58. package/template/docs/platform/platform-authentication.md +9 -1
  59. package/template/package.json +4 -3
  60. package/template/packages/database/drizzle/0043_activate_agent_dependencies.sql +196 -0
  61. package/template/packages/database/drizzle/meta/0043_snapshot.json +8335 -0
  62. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  63. package/template/packages/database/src/schema-notification-delivery.test.ts +15 -6
  64. package/template/packages/database/src/schema-workflow-notification.test.ts +44 -29
  65. package/template/scripts/dev-adk-web.ts +107 -0
@@ -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
+ }
@@ -0,0 +1,55 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { RequestActor } from "./guards.js";
3
+ import { resolveRequestActor } from "./runtime-request-actor.js";
4
+
5
+ const debugActor: RequestActor = {
6
+ id: "service_adk_debug",
7
+ type: "service",
8
+ name: "ADK 开发调试",
9
+ roleCodes: ["adk-debug"],
10
+ permissionKeys: ["analysis.copilot.execute"],
11
+ };
12
+
13
+ test("保留凭据命中后不回退普通认证", async () => {
14
+ let standardCalls = 0;
15
+ const request = new Request("http://localhost/api/foundation");
16
+ const actor = await resolveRequestActor(
17
+ request,
18
+ () => {
19
+ standardCalls += 1;
20
+ return {
21
+ id: "user_regular",
22
+ type: "user",
23
+ name: "普通用户",
24
+ roleCodes: ["user"],
25
+ permissionKeys: [],
26
+ };
27
+ },
28
+ {
29
+ claimsRequest: () => true,
30
+ resolveRequest: () => undefined,
31
+ },
32
+ );
33
+
34
+ expect(actor).toBeUndefined();
35
+ expect(standardCalls).toBe(0);
36
+ });
37
+
38
+ test("保留凭据在 canonical 路径始终解析为专用主体", async () => {
39
+ const actor = await resolveRequestActor(
40
+ new Request("http://localhost/api/agent/runs", { method: "POST" }),
41
+ () => ({
42
+ id: "user_regular",
43
+ type: "user",
44
+ name: "普通用户",
45
+ roleCodes: ["user"],
46
+ permissionKeys: ["analysis.copilot.execute"],
47
+ }),
48
+ {
49
+ claimsRequest: () => true,
50
+ resolveRequest: () => debugActor,
51
+ },
52
+ );
53
+
54
+ expect(actor).toBe(debugActor);
55
+ });
@@ -0,0 +1,22 @@
1
+ import type { RequestActor } from "./guards.js";
2
+
3
+ export interface ReservedRequestActorResolver {
4
+ claimsRequest(request: Request): boolean;
5
+ resolveRequest(
6
+ request: Request,
7
+ ): Promise<RequestActor | undefined> | RequestActor | undefined;
8
+ }
9
+
10
+ export async function resolveRequestActor(
11
+ request: Request,
12
+ resolveStandard: () =>
13
+ | Promise<RequestActor | undefined>
14
+ | RequestActor
15
+ | undefined,
16
+ reserved?: ReservedRequestActorResolver,
17
+ ): Promise<RequestActor | undefined> {
18
+ if (reserved?.claimsRequest(request)) {
19
+ return reserved.resolveRequest(request);
20
+ }
21
+ return resolveStandard();
22
+ }
@@ -22,6 +22,9 @@ describe("server runtime", () => {
22
22
 
23
23
  expect(context.actor.id).toBe("user_dev_admin");
24
24
  expect(context.actor.permissionKeys).toContain("system.menu.read");
25
+ // @windy-module work-order begin
26
+ expect(context.actor.permissionKeys).toContain("work-order.ticket.read");
27
+ // @windy-module work-order end
25
28
  });
26
29
 
27
30
  test("生产环境强制关闭开发管理员 token", async () => {
@@ -31,6 +31,13 @@ import {
31
31
  requestSourcePolicyFromEnv,
32
32
  resolveRequestSource,
33
33
  } from "./http/request-source.js";
34
+ import { resolveRequestActor } from "./runtime-request-actor.js";
35
+ // @windy-module system.agent begin
36
+ import {
37
+ createAdkDebugRuntime,
38
+ type AdkDebugActorResolver,
39
+ } from "./runtime-adk-debug.js";
40
+ // @windy-module system.agent end
34
41
 
35
42
  export type {
36
43
  FeatureFlagRuntimeResolver,
@@ -78,6 +85,9 @@ export interface ServerRuntimeOptions {
78
85
  featureCatalog?: readonly FeatureFlagDefinition[];
79
86
  auditWriter?: AuditEventWriter;
80
87
  auditReader?: AuditEventReader;
88
+ // @windy-module system.agent begin
89
+ adkDebugActorResolver?: AdkDebugActorResolver;
90
+ // @windy-module system.agent end
81
91
  }
82
92
 
83
93
  export class InMemorySessionStore {
@@ -186,6 +196,19 @@ export function createServerRuntime(
186
196
  options: ServerRuntimeOptions = {},
187
197
  ) {
188
198
  const developmentAuth = readDevelopmentAuthBypass(env);
199
+ // @windy-module system.agent begin
200
+ const adkDebug = createAdkDebugRuntime(
201
+ env,
202
+ undefined,
203
+ options.adkDebugActorResolver,
204
+ );
205
+ if (adkDebug.usesToken(developmentAuth.token)) {
206
+ throw new Error("ADK Web 调试 Token 不能复用开发管理员 Token");
207
+ }
208
+ if (adkDebug.usesToken(env.AGENT_SERVER_INTERNAL_TOKEN?.trim())) {
209
+ throw new Error("ADK Web 调试 Token 不能复用 Agent 内部服务 Token");
210
+ }
211
+ // @windy-module system.agent end
189
212
  const sessionStore = new InMemorySessionStore(
190
213
  developmentAuth.token
191
214
  ? [{ token: developmentAuth.token, actor: developmentAdminActor() }]
@@ -216,9 +239,15 @@ export function createServerRuntime(
216
239
  metadata.clientIp,
217
240
  requestSourcePolicy,
218
241
  );
219
- const actor =
220
- (await options.actorResolver?.resolve(request)) ||
221
- sessionStore.resolve(request);
242
+ const actor = await resolveRequestActor(
243
+ request,
244
+ async () =>
245
+ (await options.actorResolver?.resolve(request)) ||
246
+ sessionStore.resolve(request),
247
+ // @windy-module system.agent begin
248
+ adkDebug,
249
+ // @windy-module system.agent end
250
+ );
222
251
  const license =
223
252
  (await options.licenseResolver?.resolve()) || licenseStore.resolve();
224
253
  return createRequestGuardContext(request, {
@@ -227,6 +256,10 @@ export function createServerRuntime(
227
256
  resolveLicense: () => license,
228
257
  });
229
258
  },
259
+ // @windy-module system.agent begin
260
+ resolveEphemeralActor: adkDebug.resolveActor,
261
+ agentDebugOperationForActor: adkDebug.operationForActor,
262
+ // @windy-module system.agent end
230
263
  async createGuardContextForActor(
231
264
  request: Request,
232
265
  actor: RequestActor,