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.
- package/README.md +14 -36
- package/dist/cli.js +1528 -3078
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +6 -0
- package/template/README.md +5 -10
- 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/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/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/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/platform-authentication.md +9 -1
- package/template/package.json +1 -1
- 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
|
@@ -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
|
|
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 =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
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
|
+
}
|