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.
- package/README.md +14 -36
- package/dist/cli.js +1831 -3178
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +7 -1
- package/template/README.md +8 -11
- package/template/apps/agent-server/Dockerfile +2 -1
- package/template/apps/agent-server/README.md +13 -5
- package/template/apps/agent-server/adk_web_agents/canonical_debug/__init__.py +1 -0
- package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +125 -0
- package/template/apps/agent-server/project/adk-web-debug.json +4 -0
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +315 -0
- package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +235 -0
- package/template/apps/server/Dockerfile +1 -1
- package/template/apps/server/src/agent/run-debug-authorization.ts +27 -0
- package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +56 -0
- package/template/apps/server/src/agent/run-facade.test-support.ts +235 -0
- package/template/apps/server/src/agent/run-facade.test.ts +8 -244
- package/template/apps/server/src/agent/run-facade.ts +23 -48
- package/template/apps/server/src/agent/run-operation-registry.ts +51 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +5 -2
- package/template/apps/server/src/agent/tool-audit.ts +3 -0
- package/template/apps/server/src/agent/tool-registry.test.ts +29 -0
- package/template/apps/server/src/application-services.ts +83 -15
- package/template/apps/server/src/audit/contracts.ts +8 -0
- package/template/apps/server/src/audit/recovery-queue.test.ts +48 -1
- package/template/apps/server/src/audit/recovery-queue.ts +73 -4
- package/template/apps/server/src/audit/reliable-writer.test.ts +120 -2
- package/template/apps/server/src/audit/reliable-writer.ts +98 -34
- package/template/apps/server/src/guards.ts +16 -0
- package/template/apps/server/src/index.ts +23 -20
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +0 -1
- package/template/apps/server/src/jobs/legacy-adapter.test.ts +6 -0
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +18 -0
- package/template/apps/server/src/module-host/durable-context-unavailable.test.ts +84 -0
- package/template/apps/server/src/module-host/durable-context.test.ts +14 -0
- package/template/apps/server/src/module-host/durable-context.ts +17 -15
- package/template/apps/server/src/module-host/request-context.test.ts +30 -1
- package/template/apps/server/src/module-host/request-context.ts +15 -2
- package/template/apps/server/src/module-host/storage-port.ts +1 -1
- package/template/apps/server/src/platform-bootstrap.ts +16 -0
- package/template/apps/server/src/runtime-adk-debug.test.ts +246 -0
- package/template/apps/server/src/runtime-adk-debug.ts +158 -0
- package/template/apps/server/src/runtime-request-actor.test.ts +55 -0
- package/template/apps/server/src/runtime-request-actor.ts +22 -0
- package/template/apps/server/src/runtime.test.ts +3 -0
- package/template/apps/server/src/runtime.ts +36 -3
- package/template/apps/server/src/scheduler/definitions.test.ts +16 -0
- package/template/apps/web/Dockerfile +1 -1
- package/template/docker-compose.agent-debug.yml +56 -0
- package/template/docker-compose.yml +1 -39
- package/template/docs/architecture/ai-runtime.md +14 -5
- package/template/docs/architecture/module-manifest-composition.md +14 -0
- package/template/docs/development/custom-login-page.md +2 -16
- package/template/docs/platform/agent-runtime.md +57 -16
- package/template/docs/platform/agent-tool-sandbox.md +4 -3
- package/template/docs/platform/audit-reliability.md +33 -5
- package/template/docs/platform/platform-authentication.md +9 -1
- package/template/package.json +4 -3
- package/template/packages/database/drizzle/0043_activate_agent_dependencies.sql +196 -0
- package/template/packages/database/drizzle/meta/0043_snapshot.json +8335 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/src/schema-notification-delivery.test.ts +15 -6
- package/template/packages/database/src/schema-workflow-notification.test.ts +44 -29
- 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
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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,
|