create-windy 0.2.20 → 0.2.21

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 (58) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +84 -10
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/README.md +1 -0
  6. package/template/apps/agent-server/Dockerfile +20 -0
  7. package/template/apps/agent-server/README.md +46 -0
  8. package/template/apps/agent-server/pyproject.toml +44 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/__init__.py +5 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +39 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/app.py +263 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/config.py +141 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +99 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +52 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/engine.py +85 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +197 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +246 -0
  18. package/template/apps/agent-server/src/southwind_agent_server/service.py +180 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +317 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/sse.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/store.py +144 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +80 -0
  23. package/template/apps/agent-server/tests/test_api.py +207 -0
  24. package/template/apps/agent-server/tests/test_config.py +56 -0
  25. package/template/apps/agent-server/tests/test_openai_provider.py +224 -0
  26. package/template/apps/agent-server/tests/test_sqlite_store.py +93 -0
  27. package/template/apps/agent-server/uv.lock +925 -0
  28. package/template/apps/server/src/agent/execution-grants.ts +89 -0
  29. package/template/apps/server/src/agent/remote-runtime.ts +128 -0
  30. package/template/apps/server/src/agent/run-contracts.ts +43 -0
  31. package/template/apps/server/src/agent/run-facade.test.ts +257 -0
  32. package/template/apps/server/src/agent/run-facade.ts +190 -0
  33. package/template/apps/server/src/agent/run-routes.ts +222 -0
  34. package/template/apps/server/src/agent/runtime-bootstrap.ts +53 -0
  35. package/template/apps/server/src/agent/tool-host.test.ts +242 -0
  36. package/template/apps/server/src/agent/tool-host.ts +153 -0
  37. package/template/apps/server/src/index.ts +12 -16
  38. package/template/apps/server/src/module-composition.test.ts +9 -0
  39. package/template/apps/server/src/module-composition.ts +13 -0
  40. package/template/apps/server/src/module-host/initialize-contracts.ts +67 -0
  41. package/template/apps/server/src/module-host/initialize.test.ts +69 -0
  42. package/template/apps/server/src/module-host/initialize.ts +50 -53
  43. package/template/apps/server/src/module-host.ts +1 -0
  44. package/template/apps/server/src/module-runtime-validation.ts +40 -0
  45. package/template/docker-compose.yml +25 -0
  46. package/template/docs/architecture/ai-runtime.md +744 -0
  47. package/template/docs/platform/agent-runtime.md +128 -0
  48. package/template/docs/platform/agent-tools.md +8 -1
  49. package/template/package.json +1 -0
  50. package/template/packages/config/index.test.ts +43 -0
  51. package/template/packages/config/index.ts +1 -0
  52. package/template/packages/config/package.json +2 -1
  53. package/template/packages/config/src/ai-openai-compatible.ts +131 -0
  54. package/template/packages/server-sdk/index.ts +11 -0
  55. package/template/packages/server-sdk/package.json +4 -2
  56. package/template/packages/server-sdk/src/ai-operation-registration.ts +31 -0
  57. package/template/packages/server-sdk/src/module-host.ts +5 -0
  58. package/template/packages/server-sdk/src/tool-host-port.ts +22 -0
@@ -0,0 +1,222 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
+ import type { RequestGuardContext } from "../guards.js";
4
+ import type { RouteApp } from "../system/routes.js";
5
+ import type { AgentRunFacade } from "./run-facade.js";
6
+ import { AgentRunFacadeError } from "./run-facade.js";
7
+ import type { AgentRuntimePort } from "./run-contracts.js";
8
+ import {
9
+ AgentToolHostError,
10
+ attachExecutionGrant,
11
+ type RestrictedAgentToolHost,
12
+ } from "./tool-host.js";
13
+
14
+ interface RouteContext {
15
+ body?: unknown;
16
+ params: Record<string, string | undefined>;
17
+ query: Record<string, string | undefined>;
18
+ request: Request;
19
+ guardContext: RequestGuardContext;
20
+ }
21
+
22
+ export function registerAgentRunRoutes(
23
+ app: RouteApp,
24
+ facade: AgentRunFacade,
25
+ remote: AgentRuntimePort,
26
+ toolHost: RestrictedAgentToolHost,
27
+ internalToken: string,
28
+ ) {
29
+ app.post("/agent/runs", async (raw) => {
30
+ const context = raw as RouteContext;
31
+ try {
32
+ const request = parseStartRequest(context.body);
33
+ const idempotencyKey = context.request.headers.get("idempotency-key");
34
+ if (!idempotencyKey?.trim()) {
35
+ return errorResponse("INVALID_INPUT", "幂等键不能为空", 400);
36
+ }
37
+ return await facade.start(
38
+ request.operationKey,
39
+ request.input,
40
+ idempotencyKey,
41
+ context.guardContext,
42
+ );
43
+ } catch (error) {
44
+ return facadeError(error);
45
+ }
46
+ });
47
+
48
+ app.get("/agent/runs/:runId/events", async (raw) => {
49
+ const context = raw as RouteContext;
50
+ try {
51
+ const runId = required(context.params.runId);
52
+ facade.authorizeRun(runId, context.guardContext);
53
+ const cursor = parseCursor(
54
+ context.query.after,
55
+ context.request.headers.get("last-event-id"),
56
+ );
57
+ return await remote.events(runId, cursor, context.request.signal);
58
+ } catch (error) {
59
+ return facadeError(error);
60
+ }
61
+ });
62
+
63
+ app.get("/agent/runs/:runId/result", async (raw) => {
64
+ const context = raw as RouteContext;
65
+ try {
66
+ const runId = required(context.params.runId);
67
+ facade.authorizeRun(runId, context.guardContext);
68
+ return await remote.result(runId, context.request.signal);
69
+ } catch (error) {
70
+ return facadeError(error);
71
+ }
72
+ });
73
+
74
+ app.post("/agent/runs/:runId/cancel", async (raw) => {
75
+ const context = raw as RouteContext;
76
+ try {
77
+ const runId = required(context.params.runId);
78
+ facade.authorizeRun(runId, context.guardContext);
79
+ const reason =
80
+ isRecord(context.body) && typeof context.body.reason === "string"
81
+ ? context.body.reason.slice(0, 200)
82
+ : undefined;
83
+ return await remote.cancel(runId, reason, context.request.signal);
84
+ } catch (error) {
85
+ return facadeError(error);
86
+ }
87
+ });
88
+
89
+ app.post("/internal/agent/tools/execute", async (raw) => {
90
+ const context = raw as RouteContext;
91
+ if (!validInternalToken(context.request, internalToken)) {
92
+ return errorResponse("FORBIDDEN", "内部调用认证失败", 403);
93
+ }
94
+ try {
95
+ const parsed = parseToolRequest(context.body);
96
+ return await toolHost.execute(
97
+ attachExecutionGrant(parsed.invocation, parsed.executionGrant),
98
+ );
99
+ } catch (error) {
100
+ if (error instanceof AgentToolHostError) {
101
+ return errorResponse(error.code, error.message, 403);
102
+ }
103
+ return errorResponse("TOOL_DENIED", "Agent Tool 执行失败", 403);
104
+ }
105
+ });
106
+
107
+ return app;
108
+ }
109
+
110
+ function parseStartRequest(body: unknown): {
111
+ operationKey: string;
112
+ input: ModuleAiJsonValue;
113
+ } {
114
+ if (
115
+ !isRecord(body) ||
116
+ Object.keys(body).some((key) => !["operationKey", "input"].includes(key))
117
+ ) {
118
+ throw new AgentRunFacadeError("INVALID_INPUT", "请求格式无效", 400);
119
+ }
120
+ if (typeof body.operationKey !== "string" || !body.operationKey.trim()) {
121
+ throw new AgentRunFacadeError("INVALID_INPUT", "Operation Key 无效", 400);
122
+ }
123
+ assertJson(body.input);
124
+ return { operationKey: body.operationKey, input: body.input };
125
+ }
126
+
127
+ function parseToolRequest(body: unknown) {
128
+ const allowed = [
129
+ "executionGrant",
130
+ "runId",
131
+ "toolCallId",
132
+ "toolKey",
133
+ "arguments",
134
+ ];
135
+ if (
136
+ !isRecord(body) ||
137
+ Object.keys(body).some((key) => !allowed.includes(key))
138
+ ) {
139
+ throw new Error("Tool 请求格式无效");
140
+ }
141
+ if (
142
+ typeof body.executionGrant !== "string" ||
143
+ typeof body.runId !== "string" ||
144
+ typeof body.toolCallId !== "string" ||
145
+ typeof body.toolKey !== "string" ||
146
+ !isRecord(body.arguments)
147
+ ) {
148
+ throw new Error("Tool 请求字段无效");
149
+ }
150
+ assertJson(body.arguments);
151
+ return {
152
+ executionGrant: body.executionGrant,
153
+ invocation: {
154
+ runId: body.runId,
155
+ toolCallId: body.toolCallId,
156
+ toolKey: body.toolKey,
157
+ arguments: body.arguments,
158
+ },
159
+ };
160
+ }
161
+
162
+ function parseCursor(after: string | undefined, header: string | null): number {
163
+ const values = [after, header].filter(
164
+ (value): value is string => value !== undefined && value !== null,
165
+ );
166
+ if (values.some((value) => !/^\d+$/.test(value))) {
167
+ throw new AgentRunFacadeError("INVALID_INPUT", "事件游标无效", 400);
168
+ }
169
+ return Math.max(0, ...values.map(Number));
170
+ }
171
+
172
+ function validInternalToken(request: Request, expected: string): boolean {
173
+ const provided = request.headers
174
+ .get("authorization")
175
+ ?.replace(/^Bearer /, "");
176
+ if (!provided || !expected) return false;
177
+ const left = Buffer.from(provided);
178
+ const right = Buffer.from(expected);
179
+ return left.length === right.length && timingSafeEqual(left, right);
180
+ }
181
+
182
+ function facadeError(error: unknown): Response {
183
+ if (error instanceof AgentRunFacadeError) {
184
+ return errorResponse(error.code, error.message, error.status);
185
+ }
186
+ return errorResponse(
187
+ "RUNTIME_UNAVAILABLE",
188
+ "Agent Runtime 暂不可用",
189
+ 503,
190
+ true,
191
+ );
192
+ }
193
+
194
+ function errorResponse(
195
+ code: string,
196
+ message: string,
197
+ status: number,
198
+ retryable = false,
199
+ ): Response {
200
+ return Response.json(
201
+ { code, message, requestId: crypto.randomUUID(), retryable },
202
+ { status },
203
+ );
204
+ }
205
+
206
+ function required(value: string | undefined): string {
207
+ if (!value)
208
+ throw new AgentRunFacadeError("INVALID_INPUT", "Run ID 无效", 400);
209
+ return value;
210
+ }
211
+
212
+ function isRecord(value: unknown): value is Record<string, unknown> {
213
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
214
+ }
215
+
216
+ function assertJson(value: unknown): asserts value is ModuleAiJsonValue {
217
+ try {
218
+ if (JSON.stringify(value) === undefined) throw new Error();
219
+ } catch {
220
+ throw new AgentRunFacadeError("INVALID_INPUT", "请求必须是 JSON", 400);
221
+ }
222
+ }
@@ -0,0 +1,53 @@
1
+ import type { PlatformConfig } from "@southwind-ai/config";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import type { InitializedModuleHosts } from "../module-host.js";
4
+ import type { createServerRuntime } from "../runtime.js";
5
+ import type { RouteApp } from "../system/routes.js";
6
+ import { ExecutionGrantStore } from "./execution-grants.js";
7
+ import { RemoteAgentRuntime } from "./remote-runtime.js";
8
+ import type { ModuleToolHandlers } from "./routes.js";
9
+ import { AgentRunFacade } from "./run-facade.js";
10
+ import { registerAgentRunRoutes } from "./run-routes.js";
11
+ import { RestrictedAgentToolHost } from "./tool-host.js";
12
+
13
+ export function registerAgentPlatformRuntime(input: {
14
+ app: RouteApp;
15
+ platformConfig: PlatformConfig;
16
+ modules: readonly ModuleManifest[];
17
+ hosts: InitializedModuleHosts;
18
+ runtime: ReturnType<typeof createServerRuntime>;
19
+ toolHandlers: ModuleToolHandlers;
20
+ }): void {
21
+ if (!input.platformConfig.enableAI) return;
22
+ const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
23
+ const remote = new RemoteAgentRuntime({
24
+ baseUrl: process.env.AGENT_SERVER_URL || "http://agent-server:8080",
25
+ internalToken,
26
+ toolHostUrl: process.env.AGENT_TOOL_HOST_URL || "http://server:9747",
27
+ });
28
+ const grants = new ExecutionGrantStore();
29
+ registerAgentRunRoutes(
30
+ input.app,
31
+ new AgentRunFacade(
32
+ input.modules,
33
+ input.hosts,
34
+ input.runtime,
35
+ remote,
36
+ grants,
37
+ ),
38
+ remote,
39
+ new RestrictedAgentToolHost(
40
+ input.modules,
41
+ input.toolHandlers,
42
+ input.runtime,
43
+ grants,
44
+ ),
45
+ internalToken,
46
+ );
47
+ }
48
+
49
+ function requiredEnvironment(key: string): string {
50
+ const value = process.env[key]?.trim();
51
+ if (!value) throw new Error(`启用 AI Runtime 时缺少 ${key}`);
52
+ return value;
53
+ }
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type {
3
+ ModuleAiOperationDefinition,
4
+ ModuleManifest,
5
+ } from "@southwind-ai/modules";
6
+ import { featureKey } from "@southwind-ai/shared";
7
+ import { buildScopeContext } from "../data-access/context.js";
8
+ import { createRequestGuardContext } from "../guards.js";
9
+ import { createServerRuntime } from "../runtime.js";
10
+ import { ExecutionGrantStore } from "./execution-grants.js";
11
+ import {
12
+ AgentToolHostError,
13
+ attachExecutionGrant,
14
+ RestrictedAgentToolHost,
15
+ } from "./tool-host.js";
16
+
17
+ const operation: ModuleAiOperationDefinition = {
18
+ key: "business.answer",
19
+ kind: "agent",
20
+ featureKey: "business.ai",
21
+ permissionKey: "business.ai.execute",
22
+ auditAction: "business.ai.execute",
23
+ inputSchemaKey: "business.input.v1",
24
+ outputSchemaKey: "business.output.v1",
25
+ requiredCapabilities: ["text-generation"],
26
+ allowedToolKeys: ["business.records.list"],
27
+ dataClassification: "internal",
28
+ execution: "streaming",
29
+ publicNetworkAccess: "forbidden",
30
+ limits: {
31
+ maxDurationSeconds: 60,
32
+ maxInputBytes: 4096,
33
+ maxOutputBytes: 4096,
34
+ },
35
+ };
36
+
37
+ describe("受限 Agent Tool Host", () => {
38
+ test("只接受 Grant 绑定 Operation 的 allowedToolKeys", async () => {
39
+ const fixture = createFixture();
40
+ const invocation = attachExecutionGrant(
41
+ {
42
+ runId: "run_1",
43
+ toolCallId: "call_1",
44
+ toolKey: "business.admin.delete",
45
+ arguments: {},
46
+ },
47
+ fixture.grant.token,
48
+ );
49
+
50
+ await expect(fixture.host.execute(invocation)).rejects.toMatchObject({
51
+ code: "TOOL_DENIED",
52
+ message: "Operation 未授权该 Tool",
53
+ });
54
+ expect(fixture.called).toEqual([]);
55
+ });
56
+
57
+ test("未知 Tool fail closed 且客户端不能提供权限事实", async () => {
58
+ const fixture = createFixture({
59
+ allowedToolKeys: ["business.unknown"],
60
+ });
61
+ const invocation = attachExecutionGrant(
62
+ {
63
+ runId: "run_1",
64
+ toolCallId: "call_1",
65
+ toolKey: "business.unknown",
66
+ arguments: {
67
+ permissionKeys: ["business.admin"],
68
+ },
69
+ },
70
+ fixture.grant.token,
71
+ );
72
+
73
+ await expect(fixture.host.execute(invocation)).rejects.toBeInstanceOf(
74
+ AgentToolHostError,
75
+ );
76
+ expect(fixture.called).toEqual([]);
77
+ });
78
+
79
+ test("使用宿主颁发 Actor 与 Scope 执行 Handler", async () => {
80
+ const fixture = createFixture();
81
+ const result = await fixture.host.execute(
82
+ attachExecutionGrant(
83
+ {
84
+ runId: "run_1",
85
+ toolCallId: "call_1",
86
+ toolKey: "business.records.list",
87
+ arguments: { page: 2 },
88
+ },
89
+ fixture.grant.token,
90
+ ),
91
+ );
92
+
93
+ expect(result.value).toEqual({
94
+ items: [{ id: "record-1" }],
95
+ total: 1,
96
+ page: 2,
97
+ pageSize: 10,
98
+ });
99
+ expect(fixture.called).toEqual([
100
+ {
101
+ query: { page: "2" },
102
+ actorId: "actor-1",
103
+ scopeKind: "self",
104
+ },
105
+ ]);
106
+ });
107
+ });
108
+
109
+ function createFixture(overrides: Partial<ModuleAiOperationDefinition> = {}) {
110
+ const currentOperation = { ...operation, ...overrides };
111
+ const module = manifest(currentOperation);
112
+ const runtime = createServerRuntime(
113
+ {},
114
+ {
115
+ featureCatalog: module.features,
116
+ licenseResolver: { resolve: () => ({ status: "valid" }) },
117
+ },
118
+ );
119
+ const context = createRequestGuardContext(
120
+ new Request("http://localhost/api/agent/runs"),
121
+ {
122
+ resolveActor: () => ({
123
+ id: "actor-1",
124
+ type: "user",
125
+ name: "测试用户",
126
+ roleCodes: ["user"],
127
+ permissionKeys: ["business.ai.execute", "business.records.read"],
128
+ scopeContext: buildScopeContext("actor-1", undefined, [
129
+ {
130
+ id: "role-user",
131
+ code: "user",
132
+ dataScope: "self",
133
+ departmentIds: [],
134
+ },
135
+ ]),
136
+ }),
137
+ resolveLicense: () => ({ status: "valid" }),
138
+ },
139
+ );
140
+ const grants = new ExecutionGrantStore();
141
+ const grant = grants.issue(currentOperation, context);
142
+ grants.bindRun(grant.token, "run_1");
143
+ const called: unknown[] = [];
144
+ const handlers = new Map([
145
+ [
146
+ "business.records.list",
147
+ {
148
+ scopeMode: "request" as const,
149
+ execute: (
150
+ query: Record<string, string | undefined>,
151
+ handlerContext: typeof context,
152
+ ) => {
153
+ called.push({
154
+ query,
155
+ actorId: handlerContext.actor.id,
156
+ scopeKind: handlerContext.actor.scopeContext?.grants[0]?.scope,
157
+ });
158
+ return {
159
+ items: [{ id: "record-1" }],
160
+ total: 1,
161
+ page: Number(query.page),
162
+ pageSize: 10,
163
+ };
164
+ },
165
+ },
166
+ ],
167
+ ]);
168
+ return {
169
+ called,
170
+ grant,
171
+ grants,
172
+ host: new RestrictedAgentToolHost([module], handlers, runtime, grants),
173
+ };
174
+ }
175
+
176
+ function manifest(
177
+ currentOperation: ModuleAiOperationDefinition,
178
+ ): ModuleManifest {
179
+ return {
180
+ name: "business",
181
+ version: "0.1.0",
182
+ label: "业务",
183
+ description: "业务",
184
+ menus: [],
185
+ adminRoutes: [],
186
+ permissions: [
187
+ {
188
+ key: "business.ai.execute",
189
+ label: "AI",
190
+ module: "business",
191
+ moduleLabel: "业务",
192
+ resource: "ai",
193
+ resourceLabel: "AI",
194
+ action: "manage",
195
+ },
196
+ {
197
+ key: "business.records.read",
198
+ label: "记录",
199
+ module: "business",
200
+ moduleLabel: "业务",
201
+ resource: "records",
202
+ resourceLabel: "记录",
203
+ action: "read",
204
+ },
205
+ ],
206
+ apiPermissions: [],
207
+ features: [
208
+ {
209
+ key: featureKey("business.ai"),
210
+ label: "AI",
211
+ module: "business",
212
+ visible: "visible",
213
+ enabled: true,
214
+ stage: "stable",
215
+ },
216
+ {
217
+ key: featureKey("business.records"),
218
+ label: "记录",
219
+ module: "business",
220
+ visible: "visible",
221
+ enabled: true,
222
+ stage: "stable",
223
+ },
224
+ ],
225
+ migrations: [],
226
+ schemaExports: [],
227
+ tasks: [],
228
+ tools: [
229
+ {
230
+ key: "business.records.list",
231
+ label: "查询记录",
232
+ description: "查询当前范围记录。",
233
+ permissionKey: "business.records.read",
234
+ featureKey: "business.records",
235
+ auditAction: "business.records.read",
236
+ },
237
+ ],
238
+ aiOperations: [currentOperation],
239
+ providers: [],
240
+ auditActions: ["business.ai.execute", "business.records.read"],
241
+ };
242
+ }
@@ -0,0 +1,153 @@
1
+ import type { ModuleManifest } from "@southwind-ai/modules";
2
+ import type {
3
+ ModuleAgentToolHostPort,
4
+ ModuleAgentToolInvocation,
5
+ ModuleAiJsonValue,
6
+ } from "@southwind-ai/server-sdk";
7
+ import type { FeatureFlagDefinition } from "@southwind-ai/shared";
8
+ import { evaluateAccessGuards } from "../access-guards.js";
9
+ import { isServerScopeContext } from "../data-access/context.js";
10
+ import type { createServerRuntime } from "../runtime.js";
11
+ import { findFeature, systemFeatureKeyForPermission } from "../route-guards.js";
12
+ import type { ModuleToolHandlers } from "./routes.js";
13
+ import type { ExecutionGrantStore } from "./execution-grants.js";
14
+
15
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
16
+
17
+ export class RestrictedAgentToolHost implements ModuleAgentToolHostPort {
18
+ readonly #tools: Map<string, ToolDefinition>;
19
+ readonly #features: FeatureFlagDefinition[];
20
+
21
+ constructor(
22
+ modules: readonly ModuleManifest[],
23
+ private readonly handlers: ModuleToolHandlers,
24
+ private readonly runtime: ServerRuntime,
25
+ private readonly grants: ExecutionGrantStore,
26
+ ) {
27
+ const features = modules.flatMap(({ features }) => features);
28
+ this.#features = features;
29
+ this.#tools = new Map(
30
+ modules.flatMap((module) =>
31
+ module.tools.map(
32
+ (tool) =>
33
+ [
34
+ tool.key,
35
+ {
36
+ ...tool,
37
+ moduleName: module.name,
38
+ feature: findFeature(
39
+ features,
40
+ tool.featureKey ||
41
+ systemFeatureKeyForPermission(tool.permissionKey),
42
+ ),
43
+ },
44
+ ] as const,
45
+ ),
46
+ ),
47
+ );
48
+ }
49
+
50
+ async execute(invocation: ModuleAgentToolInvocation) {
51
+ const grant = this.grants.require(
52
+ readGrantToken(invocation),
53
+ invocation.runId,
54
+ );
55
+ if (!grant.operation.allowedToolKeys.includes(invocation.toolKey)) {
56
+ throw new AgentToolHostError("TOOL_DENIED", "Operation 未授权该 Tool");
57
+ }
58
+ const tool = this.#tools.get(invocation.toolKey);
59
+ const handler = this.handlers.get(invocation.toolKey);
60
+ if (!tool || !handler) {
61
+ throw new AgentToolHostError("TOOL_DENIED", "Agent Tool 不存在");
62
+ }
63
+ const context = await this.runtime.createGuardContextForActor(
64
+ new Request("http://agent-tool.internal"),
65
+ grant.context.actor,
66
+ );
67
+ const operationDecision = await evaluateAccessGuards(
68
+ this.runtime,
69
+ context,
70
+ {
71
+ permission: { permissionKey: grant.operation.permissionKey },
72
+ feature: findFeature(this.#features, grant.operation.featureKey),
73
+ },
74
+ );
75
+ if (!operationDecision.decision.allowed) {
76
+ throw new AgentToolHostError(
77
+ "TOOL_DENIED",
78
+ "Agent Operation 当前无权继续执行",
79
+ );
80
+ }
81
+ const decision = await evaluateAccessGuards(this.runtime, context, {
82
+ permission: { permissionKey: tool.permissionKey },
83
+ feature: tool.feature,
84
+ });
85
+ if (!decision.decision.allowed) {
86
+ throw new AgentToolHostError("TOOL_DENIED", "Agent Tool 当前无权执行");
87
+ }
88
+ if (
89
+ handler.scopeMode === "request" &&
90
+ !isServerScopeContext(context.actor.scopeContext)
91
+ ) {
92
+ throw new AgentToolHostError("TOOL_DENIED", "Agent Tool 数据范围无效");
93
+ }
94
+ const result = await handler.execute(
95
+ toQuery(invocation.arguments),
96
+ context,
97
+ );
98
+ return { value: toJsonValue(result) };
99
+ }
100
+ }
101
+
102
+ interface ToolDefinition {
103
+ key: string;
104
+ permissionKey: string;
105
+ feature?: ReturnType<typeof findFeature>;
106
+ moduleName: string;
107
+ }
108
+
109
+ export class AgentToolHostError extends Error {
110
+ constructor(
111
+ readonly code: "TOOL_DENIED",
112
+ message: string,
113
+ ) {
114
+ super(message);
115
+ }
116
+ }
117
+
118
+ const grantTokens = new WeakMap<object, string>();
119
+
120
+ export function attachExecutionGrant<T extends ModuleAgentToolInvocation>(
121
+ invocation: T,
122
+ token: string,
123
+ ): T {
124
+ grantTokens.set(invocation, token);
125
+ return invocation;
126
+ }
127
+
128
+ function readGrantToken(invocation: ModuleAgentToolInvocation): string {
129
+ const token = grantTokens.get(invocation);
130
+ if (!token) throw new AgentToolHostError("TOOL_DENIED", "缺少执行授权");
131
+ return token;
132
+ }
133
+
134
+ function toQuery(
135
+ value: Readonly<Record<string, ModuleAiJsonValue>>,
136
+ ): Record<string, string | undefined> {
137
+ return Object.fromEntries(
138
+ Object.entries(value).map(([key, item]) => [
139
+ key,
140
+ item === null
141
+ ? undefined
142
+ : typeof item === "object"
143
+ ? JSON.stringify(item)
144
+ : String(item),
145
+ ]),
146
+ );
147
+ }
148
+
149
+ function toJsonValue(value: unknown): ModuleAiJsonValue {
150
+ const encoded = JSON.stringify(value);
151
+ if (encoded === undefined) throw new Error("Agent Tool 返回值不是 JSON");
152
+ return JSON.parse(encoded) as ModuleAiJsonValue;
153
+ }