create-windy 0.2.23 → 0.2.25

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 (49) hide show
  1. package/dist/cli.js +100 -2
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +43 -0
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +2 -0
  6. package/template/apps/server/package.json +3 -1
  7. package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -20
  8. package/template/apps/server/src/agent/remote-runtime.test.ts +59 -0
  9. package/template/apps/server/src/agent/remote-runtime.ts +22 -4
  10. package/template/apps/server/src/agent/routes.test.ts +16 -8
  11. package/template/apps/server/src/agent/routes.ts +103 -169
  12. package/template/apps/server/src/agent/run-facade.test.ts +30 -0
  13. package/template/apps/server/src/agent/runtime-bootstrap.ts +3 -3
  14. package/template/apps/server/src/agent/tool-host.test.ts +61 -8
  15. package/template/apps/server/src/agent/tool-host.ts +17 -73
  16. package/template/apps/server/src/agent/tool-registry.test.ts +117 -0
  17. package/template/apps/server/src/agent/tool-registry.ts +318 -0
  18. package/template/apps/server/src/http-app.test.ts +87 -0
  19. package/template/apps/server/src/http-app.ts +43 -25
  20. package/template/apps/server/src/index.ts +14 -2
  21. package/template/apps/server/src/license/runtime-service.test.ts +6 -3
  22. package/template/apps/server/src/mcp/adapter.test.ts +195 -0
  23. package/template/apps/server/src/mcp/adapter.ts +204 -0
  24. package/template/apps/server/src/mcp/routes.ts +21 -0
  25. package/template/apps/server/src/module-host/tool-handlers.ts +6 -5
  26. package/template/apps/server/src/observability/http-observability.test.ts +21 -0
  27. package/template/apps/server/src/observability/http-observability.ts +1 -0
  28. package/template/apps/server/src/observability/metrics.ts +5 -2
  29. package/template/docs/operations/observability.md +9 -1
  30. package/template/docs/platform/agent-runtime.md +15 -3
  31. package/template/docs/platform/agent-tools.md +88 -28
  32. package/template/packages/modules/index.ts +1 -0
  33. package/template/packages/modules/src/ai-operation-composition.test.ts +30 -0
  34. package/template/packages/modules/src/ai-operation-composition.ts +13 -0
  35. package/template/packages/modules/src/composition.test.ts +42 -0
  36. package/template/packages/modules/src/composition.ts +3 -0
  37. package/template/packages/modules/src/manifest.ts +20 -1
  38. package/template/packages/modules/src/system-agent-tools.ts +53 -1
  39. package/template/packages/modules/src/system-api-permissions.ts +5 -0
  40. package/template/packages/modules/src/system-module-catalog.ts +1 -0
  41. package/template/packages/modules/src/system-modules.test.ts +32 -0
  42. package/template/packages/modules/src/system-modules.ts +9 -1
  43. package/template/packages/modules/src/tool-composition.ts +86 -0
  44. package/template/packages/server-sdk/index.ts +1 -0
  45. package/template/packages/server-sdk/package.json +1 -0
  46. package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -7
  47. package/template/packages/server-sdk/src/tool-registration.ts +6 -7
  48. package/template/packages/shared/index.ts +1 -0
  49. package/template/packages/shared/src/json.ts +8 -0
@@ -1,51 +1,30 @@
1
- import { randomUUIDv7 } from "bun";
2
- import type { ModuleManifest, ModuleToolDefinition } from "@southwind-ai/modules";
3
- import type { AuditEvent, FeatureFlagDefinition } from "@southwind-ai/shared";
4
- import { evaluateAccessGuards } from "../access-guards.js";
5
- import { type RequestGuardContext } from "../guards.js";
6
- import { findFeature, systemFeatureKeyForPermission } from "../route-guards.js";
1
+ import type {
2
+ ModuleToolInput,
3
+ ModuleToolResult,
4
+ } from "@southwind-ai/server-sdk";
7
5
  import type { createServerRuntime } from "../runtime.js";
8
6
  import type { RouteApp } from "../system/routes.js";
9
7
  import type { SystemRepositories } from "../system/seed.js";
10
8
  import { DataScopedResourceService } from "../data-access/scoped-resource.js";
11
9
  import { departmentDescendantResolver } from "../data-access/department-tree.js";
12
- import { isServerScopeContext } from "../data-access/context.js";
13
10
  import { projectMaskedUser } from "../system/user-projection.js";
11
+ import {
12
+ GovernedToolRegistry,
13
+ ToolExecutionError,
14
+ type ModuleToolHandlers,
15
+ type PlatformToolExecutionContext,
16
+ } from "./tool-registry.js";
14
17
 
15
18
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
16
19
 
17
20
  interface RouteContext {
18
- guardContext: RequestGuardContext;
21
+ guardContext: Parameters<GovernedToolRegistry["allowed"]>[1];
19
22
  params: Record<string, string | undefined>;
20
23
  query: Record<string, string | undefined>;
24
+ body?: unknown;
21
25
  }
22
26
 
23
- interface RegisteredTool extends ModuleToolDefinition {
24
- module: string;
25
- feature?: FeatureFlagDefinition;
26
- }
27
-
28
- export type ModuleToolResult = {
29
- items: unknown[];
30
- total: number;
31
- page: number;
32
- pageSize: number;
33
- };
34
-
35
- export type ModuleToolHandler = (
36
- query: RouteContext["query"],
37
- context: RequestGuardContext,
38
- ) => Promise<ModuleToolResult> | ModuleToolResult;
39
-
40
- export interface ModuleToolHandlerRegistration {
41
- readonly scopeMode: "platform" | "request";
42
- readonly execute: ModuleToolHandler;
43
- }
44
-
45
- export type ModuleToolHandlers = ReadonlyMap<
46
- string,
47
- ModuleToolHandlerRegistration
48
- >;
27
+ export type { ModuleToolHandlers } from "./tool-registry.js";
49
28
 
50
29
  export function createSystemToolHandlers(
51
30
  repositories: SystemRepositories,
@@ -55,44 +34,49 @@ export function createSystemToolHandlers(
55
34
  repositories.users,
56
35
  departmentDescendantResolver(repositories.departments),
57
36
  );
58
- return new Map<string, ModuleToolHandlerRegistration>([
37
+ return new Map([
59
38
  [
60
39
  "system.users.list",
61
40
  {
62
- scopeMode: "request",
63
- execute: async (query, context) => {
64
- const { page, pageSize } = paging(query);
41
+ scopeMode: "request" as const,
42
+ execute: async (
43
+ input: ModuleToolInput,
44
+ { guardContext }: PlatformToolExecutionContext,
45
+ ) => {
46
+ const { page, pageSize } = paging(input);
65
47
  const result = await userAccess.list(
66
48
  { page, pageSize },
67
49
  ["username", "displayName"],
68
- context.actor,
50
+ guardContext.actor,
69
51
  );
70
- return {
52
+ return jsonResult({
71
53
  ...result,
72
54
  items: result.items.map((user) => projectMaskedUser(user)),
73
- };
55
+ });
74
56
  },
75
57
  },
76
58
  ],
77
59
  [
78
60
  "system.features.list",
79
61
  {
80
- scopeMode: "platform",
81
- execute: async (query) => {
82
- const { page, pageSize } = paging(query);
83
- return repositories.features.list({ page, pageSize }, [
84
- "key",
85
- "label",
86
- "module",
87
- ]);
62
+ scopeMode: "platform" as const,
63
+ execute: async (input: ModuleToolInput) => {
64
+ const { page, pageSize } = paging(input);
65
+ return jsonResult(
66
+ await repositories.features.list({ page, pageSize }, [
67
+ "key",
68
+ "label",
69
+ "module",
70
+ ]),
71
+ );
88
72
  },
89
73
  },
90
74
  ],
91
75
  [
92
76
  "system.audit.list",
93
77
  {
94
- scopeMode: "platform",
95
- execute: (query) => pagedAuditResult(query, runtime),
78
+ scopeMode: "platform" as const,
79
+ execute: (input: ModuleToolInput) => pagedAuditResult(input, runtime),
96
80
  },
97
81
  ],
98
82
  ]);
@@ -100,158 +84,108 @@ export function createSystemToolHandlers(
100
84
 
101
85
  export function registerAgentRoutes(
102
86
  app: RouteApp,
103
- modules: ModuleManifest[],
104
- handlers: ModuleToolHandlers,
105
- runtime: ServerRuntime,
87
+ registry: GovernedToolRegistry,
106
88
  ) {
107
- const tools = collectTools(modules);
108
- validateToolHandlerContract(tools, handlers);
109
-
110
89
  app.get("/agent/tools", async (rawContext) => {
111
90
  const { guardContext } = rawContext as RouteContext;
112
91
  return Promise.all(
113
- tools.map(async (tool) => ({
114
- ...tool,
115
- allowed: (
116
- await evaluateToolGuards(runtime, guardContext, tool, {
117
- record: false,
118
- })
119
- ).allowed,
92
+ registry.definitions().map(async (tool) => ({
93
+ key: tool.key,
94
+ label: tool.label,
95
+ description: tool.description,
96
+ operation: tool.operation,
97
+ inputSchema: tool.inputSchema,
98
+ outputSchema: tool.outputSchema,
99
+ allowed: await registry.allowed(tool.key, guardContext),
120
100
  })),
121
101
  );
122
102
  });
123
103
 
124
104
  app.post("/agent/tools/:key/execute", async (rawContext) => {
125
- const { guardContext, params, query } = rawContext as RouteContext;
126
- const tool = tools.find((item) => item.key === params.key);
127
- if (!tool) {
128
- return Response.json(
129
- { error: "TOOL_NOT_FOUND", key: params.key },
130
- { status: 404 },
131
- );
132
- }
133
-
134
- const decision = await evaluateToolGuards(runtime, guardContext, tool);
135
- if (!decision.allowed) {
136
- return Response.json(
137
- { error: "FORBIDDEN", reason: decision.reason },
138
- { status: 403 },
105
+ const context = rawContext as RouteContext;
106
+ try {
107
+ return await registry.execute(
108
+ context.params.key ?? "",
109
+ readInput(context),
110
+ context.guardContext,
139
111
  );
140
- }
141
-
142
- const handler = handlers.get(tool.key)!;
143
- if (
144
- handler.scopeMode === "request" &&
145
- !isServerScopeContext(guardContext.actor.scopeContext)
146
- ) {
112
+ } catch (error) {
113
+ if (!(error instanceof ToolExecutionError)) throw error;
147
114
  return Response.json(
148
- { error: "FORBIDDEN", reason: "data-scope-denied" },
149
- { status: 403 },
115
+ {
116
+ error: error.code === "TOOL_FORBIDDEN" ? "FORBIDDEN" : error.code,
117
+ reason: error.reason,
118
+ message: error.message,
119
+ },
120
+ { status: errorStatus(error) },
150
121
  );
151
122
  }
152
- const result = await handler.execute(query, guardContext);
153
- runtime.auditSink.recordEvent(toolEvent(guardContext, tool, result.total));
154
- return result;
155
123
  });
156
124
 
157
125
  return app;
158
126
  }
159
127
 
160
- function validateToolHandlerContract(
161
- tools: RegisteredTool[],
162
- handlers: ModuleToolHandlers,
163
- ): void {
164
- const declared = new Set(tools.map(({ key }) => key));
165
- for (const key of declared) {
166
- if (!handlers.has(key)) throw new Error(`Agent Tool ${key} 缺少 Handler`);
167
- }
168
- for (const key of handlers.keys()) {
169
- if (!declared.has(key))
170
- throw new Error(`Agent Tool Handler 未声明:${key}`);
128
+ function readInput(context: RouteContext): ModuleToolInput {
129
+ if (context.body !== undefined) {
130
+ if (
131
+ context.body === null ||
132
+ typeof context.body !== "object" ||
133
+ Array.isArray(context.body)
134
+ ) {
135
+ throw new ToolExecutionError(
136
+ "TOOL_INPUT_INVALID",
137
+ "Agent Tool 输入必须是 JSON object",
138
+ );
139
+ }
140
+ return jsonInput(context.body);
171
141
  }
142
+ return jsonInput(context.query);
172
143
  }
173
144
 
174
- function collectTools(modules: ModuleManifest[]): RegisteredTool[] {
175
- const features = modules.flatMap((module) => module.features);
176
- return modules.flatMap((module) =>
177
- module.tools.map((tool) => ({
178
- ...tool,
179
- module: module.name,
180
- feature: findFeature(
181
- features,
182
- tool.featureKey || systemFeatureKeyForPermission(tool.permissionKey),
183
- ),
184
- })),
185
- );
186
- }
187
-
188
- function evaluateToolGuards(
189
- runtime: ServerRuntime,
190
- context: RequestGuardContext,
191
- tool: RegisteredTool,
192
- options: { record?: boolean } = {},
193
- ) {
194
- return evaluateAccessGuards(
195
- runtime,
196
- context,
197
- {
198
- permission: {
199
- permissionKey: tool.permissionKey,
200
- },
201
- feature: tool.feature,
202
- },
203
- options,
204
- ).then((result) => result.decision);
205
- }
206
-
207
- function paging(query: RouteContext["query"]) {
208
- const page = toPositiveInt(query.page, 1);
209
- const pageSize = Math.min(50, toPositiveInt(query.pageSize, 10));
210
- return { page, pageSize };
145
+ function paging(input: ModuleToolInput) {
146
+ return {
147
+ page: toPositiveInt(input.page, 1),
148
+ pageSize: Math.min(50, toPositiveInt(input.pageSize, 10)),
149
+ };
211
150
  }
212
151
 
213
152
  function pagedAuditResult(
214
- query: RouteContext["query"],
153
+ input: ModuleToolInput,
215
154
  runtime: ServerRuntime,
216
155
  ): ModuleToolResult {
217
- const { page, pageSize } = paging(query);
156
+ const { page, pageSize } = paging(input);
218
157
  const events = runtime.auditSink.list();
219
158
  const start = (page - 1) * pageSize;
220
- return {
159
+ return jsonResult({
221
160
  items: events.slice(start, start + pageSize),
222
161
  total: events.length,
223
162
  page,
224
163
  pageSize,
225
- };
164
+ });
226
165
  }
227
166
 
228
- function toolEvent(
229
- context: RequestGuardContext,
230
- tool: RegisteredTool,
231
- total: number,
232
- ): AuditEvent {
233
- return {
234
- id: randomUUIDv7(),
235
- action: "agent.tool.execute",
236
- outcome: "success",
237
- actor: {
238
- id: context.actor.id,
239
- type: context.actor.type === "anonymous" ? "user" : context.actor.type,
240
- name: context.actor.name,
241
- },
242
- target: {
243
- type: "agent-tool",
244
- id: tool.key,
245
- label: tool.label,
246
- },
247
- module: tool.module,
248
- requestId: context.requestId,
249
- metadata: { total },
250
- occurredAt: new Date().toISOString(),
251
- };
167
+ function jsonInput(value: unknown): ModuleToolInput {
168
+ return JSON.parse(JSON.stringify(value)) as ModuleToolInput;
169
+ }
170
+
171
+ function jsonResult(value: unknown): ModuleToolResult {
172
+ return JSON.parse(JSON.stringify(value)) as ModuleToolResult;
173
+ }
174
+
175
+ function toPositiveInt(value: ModuleToolInput[string], fallback: number) {
176
+ const parsed =
177
+ typeof value === "number"
178
+ ? value
179
+ : typeof value === "string"
180
+ ? Number.parseInt(value, 10)
181
+ : fallback;
182
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
252
183
  }
253
184
 
254
- function toPositiveInt(value: string | undefined, fallback: number): number {
255
- const parsed = value ? Number.parseInt(value, 10) : fallback;
256
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
185
+ function errorStatus(error: ToolExecutionError): number {
186
+ if (error.code === "TOOL_NOT_FOUND") return 404;
187
+ if (error.code === "TOOL_FORBIDDEN") return 403;
188
+ if (error.code === "TOOL_TIMEOUT") return 504;
189
+ if (error.code === "TOOL_INPUT_INVALID") return 400;
190
+ return 500;
257
191
  }
@@ -196,15 +196,45 @@ function manifest(): ModuleManifest {
196
196
  ],
197
197
  migrations: [],
198
198
  schemaExports: [],
199
+ jsonSchemas: [
200
+ {
201
+ key: "business.records.input.v1",
202
+ schema: { type: "object" },
203
+ },
204
+ {
205
+ key: "business.records.output.v1",
206
+ schema: { type: "object" },
207
+ },
208
+ {
209
+ key: "business.input.v1",
210
+ schema: { type: "object" },
211
+ },
212
+ {
213
+ key: "business.output.v1",
214
+ schema: { type: "object" },
215
+ },
216
+ ],
199
217
  tasks: [],
200
218
  tools: [
201
219
  {
202
220
  key: "business.records.list",
203
221
  label: "查询记录",
204
222
  description: "查询当前范围记录。",
223
+ inputSchemaKey: "business.records.input.v1",
224
+ outputSchemaKey: "business.records.output.v1",
225
+ operation: "query",
205
226
  permissionKey: "business.records.read",
206
227
  featureKey: "business.records",
207
228
  auditAction: "business.records.read",
229
+ scopeMode: "request",
230
+ dataClassification: "internal",
231
+ idempotency: "none",
232
+ approval: "none",
233
+ limits: {
234
+ maxDurationSeconds: 10,
235
+ maxInputBytes: 4096,
236
+ maxOutputBytes: 4096,
237
+ },
208
238
  },
209
239
  ],
210
240
  aiOperations: [
@@ -5,7 +5,7 @@ import type { createServerRuntime } from "../runtime.js";
5
5
  import type { RouteApp } from "../system/routes.js";
6
6
  import { ExecutionGrantStore } from "./execution-grants.js";
7
7
  import { RemoteAgentRuntime } from "./remote-runtime.js";
8
- import type { ModuleToolHandlers } from "./routes.js";
8
+ import type { GovernedToolRegistry } from "./tool-registry.js";
9
9
  import { AgentRunFacade } from "./run-facade.js";
10
10
  import { registerAgentRunRoutes } from "./run-routes.js";
11
11
  import { RestrictedAgentToolHost } from "./tool-host.js";
@@ -16,7 +16,7 @@ export function registerAgentPlatformRuntime(input: {
16
16
  modules: readonly ModuleManifest[];
17
17
  hosts: InitializedModuleHosts;
18
18
  runtime: ReturnType<typeof createServerRuntime>;
19
- toolHandlers: ModuleToolHandlers;
19
+ toolRegistry: GovernedToolRegistry;
20
20
  }): void {
21
21
  if (!input.platformConfig.enableAI) return;
22
22
  const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
@@ -38,7 +38,7 @@ export function registerAgentPlatformRuntime(input: {
38
38
  remote,
39
39
  new RestrictedAgentToolHost(
40
40
  input.modules,
41
- input.toolHandlers,
41
+ input.toolRegistry,
42
42
  input.runtime,
43
43
  grants,
44
44
  ),
@@ -13,6 +13,7 @@ import {
13
13
  attachExecutionGrant,
14
14
  RestrictedAgentToolHost,
15
15
  } from "./tool-host.js";
16
+ import { GovernedToolRegistry } from "./tool-registry.js";
16
17
 
17
18
  const operation: ModuleAiOperationDefinition = {
18
19
  key: "business.answer",
@@ -98,7 +99,7 @@ describe("受限 Agent Tool Host", () => {
98
99
  });
99
100
  expect(fixture.called).toEqual([
100
101
  {
101
- query: { page: "2" },
102
+ input: { page: 2 },
102
103
  actorId: "actor-1",
103
104
  scopeKind: "self",
104
105
  },
@@ -147,18 +148,21 @@ function createFixture(overrides: Partial<ModuleAiOperationDefinition> = {}) {
147
148
  {
148
149
  scopeMode: "request" as const,
149
150
  execute: (
150
- query: Record<string, string | undefined>,
151
- handlerContext: typeof context,
151
+ input: { page?: number },
152
+ handlerContext: {
153
+ guardContext: typeof context;
154
+ },
152
155
  ) => {
153
156
  called.push({
154
- query,
155
- actorId: handlerContext.actor.id,
156
- scopeKind: handlerContext.actor.scopeContext?.grants[0]?.scope,
157
+ input,
158
+ actorId: handlerContext.guardContext.actor.id,
159
+ scopeKind:
160
+ handlerContext.guardContext.actor.scopeContext?.grants[0]?.scope,
157
161
  });
158
162
  return {
159
163
  items: [{ id: "record-1" }],
160
164
  total: 1,
161
- page: Number(query.page),
165
+ page: input.page ?? 1,
162
166
  pageSize: 10,
163
167
  };
164
168
  },
@@ -169,7 +173,12 @@ function createFixture(overrides: Partial<ModuleAiOperationDefinition> = {}) {
169
173
  called,
170
174
  grant,
171
175
  grants,
172
- host: new RestrictedAgentToolHost([module], handlers, runtime, grants),
176
+ host: new RestrictedAgentToolHost(
177
+ [module],
178
+ new GovernedToolRegistry([module], handlers, runtime),
179
+ runtime,
180
+ grants,
181
+ ),
173
182
  };
174
183
  }
175
184
 
@@ -224,15 +233,59 @@ function manifest(
224
233
  ],
225
234
  migrations: [],
226
235
  schemaExports: [],
236
+ jsonSchemas: [
237
+ {
238
+ key: "business.records.input.v1",
239
+ schema: {
240
+ type: "object",
241
+ properties: { page: { type: "integer", minimum: 1 } },
242
+ additionalProperties: false,
243
+ },
244
+ },
245
+ {
246
+ key: "business.records.output.v1",
247
+ schema: {
248
+ type: "object",
249
+ properties: {
250
+ items: { type: "array", items: { type: "object" } },
251
+ total: { type: "integer" },
252
+ page: { type: "integer" },
253
+ pageSize: { type: "integer" },
254
+ },
255
+ required: ["items", "total", "page", "pageSize"],
256
+ additionalProperties: false,
257
+ },
258
+ },
259
+ {
260
+ key: "business.input.v1",
261
+ schema: { type: "object" },
262
+ },
263
+ {
264
+ key: "business.output.v1",
265
+ schema: { type: "object" },
266
+ },
267
+ ],
227
268
  tasks: [],
228
269
  tools: [
229
270
  {
230
271
  key: "business.records.list",
231
272
  label: "查询记录",
232
273
  description: "查询当前范围记录。",
274
+ inputSchemaKey: "business.records.input.v1",
275
+ outputSchemaKey: "business.records.output.v1",
276
+ operation: "query",
233
277
  permissionKey: "business.records.read",
234
278
  featureKey: "business.records",
235
279
  auditAction: "business.records.read",
280
+ scopeMode: "request",
281
+ dataClassification: "internal",
282
+ idempotency: "none",
283
+ approval: "none",
284
+ limits: {
285
+ maxDurationSeconds: 10,
286
+ maxInputBytes: 4096,
287
+ maxOutputBytes: 4096,
288
+ },
236
289
  },
237
290
  ],
238
291
  aiOperations: [currentOperation],
@@ -6,45 +6,23 @@ import type {
6
6
  } from "@southwind-ai/server-sdk";
7
7
  import type { FeatureFlagDefinition } from "@southwind-ai/shared";
8
8
  import { evaluateAccessGuards } from "../access-guards.js";
9
- import { isServerScopeContext } from "../data-access/context.js";
10
9
  import type { createServerRuntime } from "../runtime.js";
11
- import { findFeature, systemFeatureKeyForPermission } from "../route-guards.js";
12
- import type { ModuleToolHandlers } from "./routes.js";
10
+ import { findFeature } from "../route-guards.js";
13
11
  import type { ExecutionGrantStore } from "./execution-grants.js";
12
+ import { GovernedToolRegistry, ToolExecutionError } from "./tool-registry.js";
14
13
 
15
14
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
16
15
 
17
16
  export class RestrictedAgentToolHost implements ModuleAgentToolHostPort {
18
- readonly #tools: Map<string, ToolDefinition>;
19
17
  readonly #features: FeatureFlagDefinition[];
20
18
 
21
19
  constructor(
22
20
  modules: readonly ModuleManifest[],
23
- private readonly handlers: ModuleToolHandlers,
21
+ private readonly tools: GovernedToolRegistry,
24
22
  private readonly runtime: ServerRuntime,
25
23
  private readonly grants: ExecutionGrantStore,
26
24
  ) {
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
- );
25
+ this.#features = modules.flatMap(({ features }) => features);
48
26
  }
49
27
 
50
28
  async execute(invocation: ModuleAgentToolInvocation) {
@@ -55,11 +33,6 @@ export class RestrictedAgentToolHost implements ModuleAgentToolHostPort {
55
33
  if (!grant.operation.allowedToolKeys.includes(invocation.toolKey)) {
56
34
  throw new AgentToolHostError("TOOL_DENIED", "Operation 未授权该 Tool");
57
35
  }
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
36
  const context = await this.runtime.createGuardContextForActor(
64
37
  new Request("http://agent-tool.internal"),
65
38
  grant.context.actor,
@@ -78,34 +51,22 @@ export class RestrictedAgentToolHost implements ModuleAgentToolHostPort {
78
51
  "Agent Operation 当前无权继续执行",
79
52
  );
80
53
  }
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 数据范围无效");
54
+ try {
55
+ const result = await this.tools.execute(
56
+ invocation.toolKey,
57
+ invocation.arguments,
58
+ context,
59
+ );
60
+ return { value: toJsonValue(result) };
61
+ } catch (error) {
62
+ if (error instanceof ToolExecutionError) {
63
+ throw new AgentToolHostError("TOOL_DENIED", error.message);
64
+ }
65
+ throw error;
93
66
  }
94
- const result = await handler.execute(
95
- toQuery(invocation.arguments),
96
- context,
97
- );
98
- return { value: toJsonValue(result) };
99
67
  }
100
68
  }
101
69
 
102
- interface ToolDefinition {
103
- key: string;
104
- permissionKey: string;
105
- feature?: ReturnType<typeof findFeature>;
106
- moduleName: string;
107
- }
108
-
109
70
  export class AgentToolHostError extends Error {
110
71
  constructor(
111
72
  readonly code: "TOOL_DENIED",
@@ -131,23 +92,6 @@ function readGrantToken(invocation: ModuleAgentToolInvocation): string {
131
92
  return token;
132
93
  }
133
94
 
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
95
  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;
96
+ return JSON.parse(JSON.stringify(value)) as ModuleAiJsonValue;
153
97
  }