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.
- package/dist/cli.js +100 -2
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +43 -0
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +2 -0
- package/template/apps/server/package.json +3 -1
- package/template/apps/server/src/agent/data-scope-routes.test.ts +20 -20
- package/template/apps/server/src/agent/remote-runtime.test.ts +59 -0
- package/template/apps/server/src/agent/remote-runtime.ts +22 -4
- package/template/apps/server/src/agent/routes.test.ts +16 -8
- package/template/apps/server/src/agent/routes.ts +103 -169
- package/template/apps/server/src/agent/run-facade.test.ts +30 -0
- package/template/apps/server/src/agent/runtime-bootstrap.ts +3 -3
- package/template/apps/server/src/agent/tool-host.test.ts +61 -8
- package/template/apps/server/src/agent/tool-host.ts +17 -73
- package/template/apps/server/src/agent/tool-registry.test.ts +117 -0
- package/template/apps/server/src/agent/tool-registry.ts +318 -0
- package/template/apps/server/src/http-app.test.ts +87 -0
- package/template/apps/server/src/http-app.ts +43 -25
- package/template/apps/server/src/index.ts +14 -2
- package/template/apps/server/src/license/runtime-service.test.ts +6 -3
- package/template/apps/server/src/mcp/adapter.test.ts +195 -0
- package/template/apps/server/src/mcp/adapter.ts +204 -0
- package/template/apps/server/src/mcp/routes.ts +21 -0
- package/template/apps/server/src/module-host/tool-handlers.ts +6 -5
- package/template/apps/server/src/observability/http-observability.test.ts +21 -0
- package/template/apps/server/src/observability/http-observability.ts +1 -0
- package/template/apps/server/src/observability/metrics.ts +5 -2
- package/template/docs/operations/observability.md +9 -1
- package/template/docs/platform/agent-runtime.md +15 -3
- package/template/docs/platform/agent-tools.md +88 -28
- package/template/packages/modules/index.ts +1 -0
- package/template/packages/modules/src/ai-operation-composition.test.ts +30 -0
- package/template/packages/modules/src/ai-operation-composition.ts +13 -0
- package/template/packages/modules/src/composition.test.ts +42 -0
- package/template/packages/modules/src/composition.ts +3 -0
- package/template/packages/modules/src/manifest.ts +20 -1
- package/template/packages/modules/src/system-agent-tools.ts +53 -1
- package/template/packages/modules/src/system-api-permissions.ts +5 -0
- package/template/packages/modules/src/system-module-catalog.ts +1 -0
- package/template/packages/modules/src/system-modules.test.ts +32 -0
- package/template/packages/modules/src/system-modules.ts +9 -1
- package/template/packages/modules/src/tool-composition.ts +86 -0
- package/template/packages/server-sdk/index.ts +1 -0
- package/template/packages/server-sdk/package.json +1 -0
- package/template/packages/server-sdk/src/ai-operation-registration.ts +2 -7
- package/template/packages/server-sdk/src/tool-registration.ts +6 -7
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/src/json.ts +8 -0
|
@@ -1,51 +1,30 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
37
|
+
return new Map([
|
|
59
38
|
[
|
|
60
39
|
"system.users.list",
|
|
61
40
|
{
|
|
62
|
-
scopeMode: "request",
|
|
63
|
-
execute: async (
|
|
64
|
-
|
|
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
|
-
|
|
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 (
|
|
82
|
-
const { page, pageSize } = paging(
|
|
83
|
-
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
{
|
|
149
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
153
|
+
input: ModuleToolInput,
|
|
215
154
|
runtime: ServerRuntime,
|
|
216
155
|
): ModuleToolResult {
|
|
217
|
-
const { page, pageSize } = paging(
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
):
|
|
233
|
-
return
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
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 {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
handlerContext:
|
|
151
|
+
input: { page?: number },
|
|
152
|
+
handlerContext: {
|
|
153
|
+
guardContext: typeof context;
|
|
154
|
+
},
|
|
152
155
|
) => {
|
|
153
156
|
called.push({
|
|
154
|
-
|
|
155
|
-
actorId: handlerContext.actor.id,
|
|
156
|
-
scopeKind:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
21
|
+
private readonly tools: GovernedToolRegistry,
|
|
24
22
|
private readonly runtime: ServerRuntime,
|
|
25
23
|
private readonly grants: ExecutionGrantStore,
|
|
26
24
|
) {
|
|
27
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
throw
|
|
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
|
-
|
|
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
|
}
|