create-windy 0.2.23 → 0.2.24

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 +21 -1
  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
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleToolHandlers } from "./tool-registry.js";
3
+ import {
4
+ createFoundationSnapshot,
5
+ loadFoundationModules,
6
+ } from "../foundation.js";
7
+ import { createServerRuntime } from "../runtime.js";
8
+ import { createSystemRepositories } from "../system/seed.js";
9
+ import { createSystemToolHandlers } from "./routes.js";
10
+ import { GovernedToolRegistry } from "./tool-registry.js";
11
+
12
+ describe("Governed Tool Registry", () => {
13
+ test("输入 Schema 在 Handler 前校验并去除非契约字段", async () => {
14
+ const fixture = await createFixture();
15
+ await expect(
16
+ fixture.registry.execute(
17
+ "system.features.list",
18
+ { page: 0 },
19
+ fixture.context,
20
+ ),
21
+ ).rejects.toMatchObject({ code: "TOOL_INPUT_INVALID" });
22
+
23
+ let received: unknown;
24
+ const handlers = new Map(fixture.handlers);
25
+ handlers.set("system.features.list", {
26
+ scopeMode: "platform",
27
+ execute: (input) => {
28
+ received = input;
29
+ return { items: [], total: 0, page: 1, pageSize: 10 };
30
+ },
31
+ });
32
+ const registry = new GovernedToolRegistry(
33
+ loadFoundationModules(),
34
+ handlers,
35
+ fixture.runtime,
36
+ );
37
+ await registry.execute(
38
+ "system.features.list",
39
+ { page: 1, permissionKeys: ["system.admin"] },
40
+ fixture.context,
41
+ );
42
+
43
+ expect(received).toEqual({ page: 1, pageSize: 10 });
44
+ });
45
+
46
+ test("输出漂移和 Handler scopeMode 漂移均 fail closed", async () => {
47
+ const fixture = await createFixture();
48
+ const invalidOutput = new Map(fixture.handlers);
49
+ invalidOutput.set("system.features.list", {
50
+ scopeMode: "platform",
51
+ execute: () => ({
52
+ items: [],
53
+ total: "wrong",
54
+ page: 1,
55
+ pageSize: 10,
56
+ }),
57
+ });
58
+ const registry = new GovernedToolRegistry(
59
+ loadFoundationModules(),
60
+ invalidOutput,
61
+ fixture.runtime,
62
+ );
63
+ await expect(
64
+ registry.execute("system.features.list", {}, fixture.context),
65
+ ).rejects.toMatchObject({ code: "TOOL_OUTPUT_INVALID" });
66
+
67
+ const mismatchedScope = new Map(fixture.handlers);
68
+ mismatchedScope.set("system.users.list", {
69
+ ...mismatchedScope.get("system.users.list")!,
70
+ scopeMode: "platform",
71
+ });
72
+ expect(
73
+ () =>
74
+ new GovernedToolRegistry(
75
+ loadFoundationModules(),
76
+ mismatchedScope,
77
+ fixture.runtime,
78
+ ),
79
+ ).toThrow("scopeMode 与 Manifest 不一致");
80
+ });
81
+
82
+ test("审计只记录受控字节计数,不记录 Tool 输入原值", async () => {
83
+ const fixture = await createFixture();
84
+ await fixture.registry.execute(
85
+ "system.features.list",
86
+ { page: 1 },
87
+ fixture.context,
88
+ );
89
+
90
+ const event = fixture.runtime.auditSink.list().at(-1);
91
+ expect(event?.action).toBe("agent.tool.execute");
92
+ expect(event?.metadata).toMatchObject({
93
+ inputBytes: expect.any(Number),
94
+ outputBytes: expect.any(Number),
95
+ });
96
+ expect(JSON.stringify(event)).not.toContain("permissionKeys");
97
+ });
98
+ });
99
+
100
+ async function createFixture() {
101
+ const runtime = createServerRuntime({ WINDY_DEV_ADMIN_TOKEN: "token_1" });
102
+ const handlers: ModuleToolHandlers = createSystemToolHandlers(
103
+ createSystemRepositories(createFoundationSnapshot()),
104
+ runtime,
105
+ );
106
+ const registry = new GovernedToolRegistry(
107
+ loadFoundationModules(),
108
+ handlers,
109
+ runtime,
110
+ );
111
+ const context = await runtime.createGuardContext(
112
+ new Request("http://localhost/api/agent/tools", {
113
+ headers: { authorization: "Bearer token_1" },
114
+ }),
115
+ );
116
+ return { context, handlers, registry, runtime };
117
+ }
@@ -0,0 +1,318 @@
1
+ import type {
2
+ ModuleJsonSchemaDefinition,
3
+ ModuleManifest,
4
+ ModuleToolDefinition,
5
+ } from "@southwind-ai/modules";
6
+ import type {
7
+ ModuleToolInput,
8
+ ModuleToolResult,
9
+ } from "@southwind-ai/server-sdk";
10
+ import type { AuditEvent, FeatureFlagDefinition } from "@southwind-ai/shared";
11
+ import Ajv, { type ValidateFunction } from "ajv";
12
+ import { evaluateAccessGuards } from "../access-guards.js";
13
+ import { isServerScopeContext } from "../data-access/context.js";
14
+ import type { RequestGuardContext } from "../guards.js";
15
+ import { findFeature } from "../route-guards.js";
16
+ import type { createServerRuntime } from "../runtime.js";
17
+
18
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
19
+
20
+ export interface PlatformToolExecutionContext {
21
+ readonly guardContext: RequestGuardContext;
22
+ readonly signal: AbortSignal;
23
+ }
24
+
25
+ export interface PlatformToolHandlerRegistration {
26
+ readonly scopeMode: "platform" | "request";
27
+ execute(
28
+ input: ModuleToolInput,
29
+ context: PlatformToolExecutionContext,
30
+ ): Promise<ModuleToolResult> | ModuleToolResult;
31
+ }
32
+
33
+ export type ModuleToolHandlers = ReadonlyMap<
34
+ string,
35
+ PlatformToolHandlerRegistration
36
+ >;
37
+
38
+ export interface RegisteredTool extends ModuleToolDefinition {
39
+ readonly module: string;
40
+ readonly feature: FeatureFlagDefinition;
41
+ readonly inputSchema: ModuleJsonSchemaDefinition["schema"];
42
+ readonly outputSchema: ModuleJsonSchemaDefinition["schema"];
43
+ }
44
+
45
+ interface CompiledTool {
46
+ readonly definition: RegisteredTool;
47
+ readonly handler: PlatformToolHandlerRegistration;
48
+ readonly validateInput: ValidateFunction<unknown>;
49
+ readonly validateOutput: ValidateFunction<unknown>;
50
+ }
51
+
52
+ export class GovernedToolRegistry {
53
+ readonly #tools: ReadonlyMap<string, CompiledTool>;
54
+
55
+ constructor(
56
+ modules: readonly ModuleManifest[],
57
+ handlers: ModuleToolHandlers,
58
+ private readonly runtime: ServerRuntime,
59
+ ) {
60
+ const definitions = collectTools(modules);
61
+ validateHandlerContract(definitions, handlers);
62
+ const inputAjv = new Ajv({
63
+ allErrors: true,
64
+ coerceTypes: true,
65
+ removeAdditional: "all",
66
+ strict: true,
67
+ useDefaults: true,
68
+ });
69
+ const outputAjv = new Ajv({ allErrors: true, strict: true });
70
+ this.#tools = new Map(
71
+ definitions.map((definition) => {
72
+ const handler = handlers.get(definition.key)!;
73
+ if (handler.scopeMode !== definition.scopeMode) {
74
+ throw new Error(
75
+ `Agent Tool ${definition.key} 的 Handler scopeMode 与 Manifest 不一致`,
76
+ );
77
+ }
78
+ return [
79
+ definition.key,
80
+ {
81
+ definition,
82
+ handler,
83
+ validateInput: inputAjv.compile(definition.inputSchema),
84
+ validateOutput: outputAjv.compile(definition.outputSchema),
85
+ },
86
+ ];
87
+ }),
88
+ );
89
+ }
90
+
91
+ definitions(): readonly RegisteredTool[] {
92
+ return [...this.#tools.values()].map(({ definition }) => definition);
93
+ }
94
+
95
+ async allowed(key: string, context: RequestGuardContext): Promise<boolean> {
96
+ const tool = this.#tools.get(key);
97
+ if (!tool) return false;
98
+ return (
99
+ await evaluateAccessGuards(
100
+ this.runtime,
101
+ context,
102
+ {
103
+ permission: { permissionKey: tool.definition.permissionKey },
104
+ feature: tool.definition.feature,
105
+ },
106
+ { record: false },
107
+ )
108
+ ).decision.allowed;
109
+ }
110
+
111
+ async execute(
112
+ key: string,
113
+ rawInput: ModuleToolInput,
114
+ context: RequestGuardContext,
115
+ ): Promise<ModuleToolResult> {
116
+ const tool = this.#tools.get(key);
117
+ if (!tool) {
118
+ throw new ToolExecutionError("TOOL_NOT_FOUND", "Agent Tool 不存在");
119
+ }
120
+ const access = await evaluateAccessGuards(this.runtime, context, {
121
+ permission: { permissionKey: tool.definition.permissionKey },
122
+ feature: tool.definition.feature,
123
+ });
124
+ if (!access.decision.allowed) {
125
+ throw new ToolExecutionError(
126
+ "TOOL_FORBIDDEN",
127
+ "Agent Tool 当前无权执行",
128
+ access.decision.reason,
129
+ );
130
+ }
131
+ if (
132
+ tool.definition.scopeMode === "request" &&
133
+ !isServerScopeContext(context.actor.scopeContext)
134
+ ) {
135
+ throw new ToolExecutionError(
136
+ "TOOL_FORBIDDEN",
137
+ "Agent Tool 数据范围无效",
138
+ "data-scope-denied",
139
+ );
140
+ }
141
+ const input = cloneJsonObject(rawInput);
142
+ enforceSize(
143
+ input,
144
+ tool.definition.limits.maxInputBytes,
145
+ "TOOL_INPUT_INVALID",
146
+ );
147
+ if (!tool.validateInput(input)) {
148
+ throw new ToolExecutionError(
149
+ "TOOL_INPUT_INVALID",
150
+ validationMessage(tool.validateInput),
151
+ );
152
+ }
153
+ const result = await executeWithTimeout(
154
+ tool,
155
+ input,
156
+ context,
157
+ tool.definition.limits.maxDurationSeconds,
158
+ );
159
+ enforceSize(
160
+ result,
161
+ tool.definition.limits.maxOutputBytes,
162
+ "TOOL_OUTPUT_INVALID",
163
+ );
164
+ if (!tool.validateOutput(result)) {
165
+ throw new ToolExecutionError(
166
+ "TOOL_OUTPUT_INVALID",
167
+ validationMessage(tool.validateOutput),
168
+ );
169
+ }
170
+ this.runtime.auditSink.recordEvent(
171
+ toolEvent(context, tool.definition, input, result),
172
+ );
173
+ return result;
174
+ }
175
+ }
176
+
177
+ export type ToolExecutionErrorCode =
178
+ | "TOOL_NOT_FOUND"
179
+ | "TOOL_FORBIDDEN"
180
+ | "TOOL_INPUT_INVALID"
181
+ | "TOOL_OUTPUT_INVALID"
182
+ | "TOOL_TIMEOUT";
183
+
184
+ export class ToolExecutionError extends Error {
185
+ constructor(
186
+ readonly code: ToolExecutionErrorCode,
187
+ message: string,
188
+ readonly reason?: string,
189
+ ) {
190
+ super(message);
191
+ }
192
+ }
193
+
194
+ function collectTools(modules: readonly ModuleManifest[]): RegisteredTool[] {
195
+ const features = modules.flatMap(({ features }) => features);
196
+ const schemas = new Map(
197
+ modules.flatMap((module) =>
198
+ (module.jsonSchemas ?? []).map((schema) => [schema.key, schema] as const),
199
+ ),
200
+ );
201
+ return modules.flatMap((module) =>
202
+ module.tools.map((tool) => ({
203
+ ...tool,
204
+ module: module.name,
205
+ feature: required(
206
+ findFeature(features, tool.featureKey),
207
+ `Agent Tool ${tool.key} 缺少 Feature`,
208
+ ),
209
+ inputSchema: required(
210
+ schemas.get(tool.inputSchemaKey),
211
+ `Agent Tool ${tool.key} 缺少输入 Schema`,
212
+ ).schema,
213
+ outputSchema: required(
214
+ schemas.get(tool.outputSchemaKey),
215
+ `Agent Tool ${tool.key} 缺少输出 Schema`,
216
+ ).schema,
217
+ })),
218
+ );
219
+ }
220
+
221
+ function validateHandlerContract(
222
+ tools: readonly RegisteredTool[],
223
+ handlers: ModuleToolHandlers,
224
+ ): void {
225
+ const declared = new Set(tools.map(({ key }) => key));
226
+ for (const key of declared) {
227
+ if (!handlers.has(key)) throw new Error(`Agent Tool ${key} 缺少 Handler`);
228
+ }
229
+ for (const key of handlers.keys()) {
230
+ if (!declared.has(key)) {
231
+ throw new Error(`Agent Tool Handler 未声明:${key}`);
232
+ }
233
+ }
234
+ }
235
+
236
+ async function executeWithTimeout(
237
+ tool: CompiledTool,
238
+ input: ModuleToolInput,
239
+ context: RequestGuardContext,
240
+ seconds: number,
241
+ ): Promise<ModuleToolResult> {
242
+ const controller = new AbortController();
243
+ let timeout: ReturnType<typeof setTimeout> | undefined;
244
+ const expired = new Promise<never>((_, reject) => {
245
+ timeout = setTimeout(() => {
246
+ controller.abort();
247
+ reject(new ToolExecutionError("TOOL_TIMEOUT", "Agent Tool 执行超时"));
248
+ }, seconds * 1_000);
249
+ });
250
+ try {
251
+ return await Promise.race([
252
+ tool.handler.execute(input, {
253
+ guardContext: context,
254
+ signal: controller.signal,
255
+ }),
256
+ expired,
257
+ ]);
258
+ } finally {
259
+ if (timeout) clearTimeout(timeout);
260
+ }
261
+ }
262
+
263
+ function cloneJsonObject(value: ModuleToolInput): ModuleToolInput {
264
+ return structuredClone(value);
265
+ }
266
+
267
+ function enforceSize(
268
+ value: ModuleToolInput | ModuleToolResult,
269
+ maximum: number,
270
+ code: "TOOL_INPUT_INVALID" | "TOOL_OUTPUT_INVALID",
271
+ ): void {
272
+ const bytes = jsonBytes(value);
273
+ if (bytes > maximum) {
274
+ throw new ToolExecutionError(code, `Agent Tool JSON 超过 ${maximum} 字节`);
275
+ }
276
+ }
277
+
278
+ function validationMessage(validate: ValidateFunction<unknown>): string {
279
+ const detail = validate.errors
280
+ ?.map(({ instancePath, message }) => `${instancePath || "/"} ${message}`)
281
+ .join(";");
282
+ return `Agent Tool JSON 不符合 Schema${detail ? `:${detail}` : ""}`;
283
+ }
284
+
285
+ function toolEvent(
286
+ context: RequestGuardContext,
287
+ tool: RegisteredTool,
288
+ input: ModuleToolInput,
289
+ result: ModuleToolResult,
290
+ ): AuditEvent {
291
+ return {
292
+ id: crypto.randomUUID(),
293
+ action: tool.auditAction,
294
+ outcome: "success",
295
+ actor: {
296
+ id: context.actor.id,
297
+ type: context.actor.type === "anonymous" ? "user" : context.actor.type,
298
+ name: context.actor.name,
299
+ },
300
+ target: { type: "agent-tool", id: tool.key, label: tool.label },
301
+ module: tool.module,
302
+ requestId: context.requestId,
303
+ metadata: {
304
+ inputBytes: jsonBytes(input),
305
+ outputBytes: jsonBytes(result),
306
+ },
307
+ occurredAt: new Date().toISOString(),
308
+ };
309
+ }
310
+
311
+ function required<T>(value: T | undefined, message: string): T {
312
+ if (value === undefined) throw new Error(message);
313
+ return value;
314
+ }
315
+
316
+ function jsonBytes(value: ModuleToolInput | ModuleToolResult): number {
317
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
318
+ }
@@ -4,6 +4,92 @@ import { createPlatformObservability } from "./observability/bootstrap.js";
4
4
  import { createServerRuntime } from "./runtime.js";
5
5
 
6
6
  describe("platform HTTP app observability", () => {
7
+ test("未匹配路由不会因缺少请求观测而崩溃", async () => {
8
+ const records: Record<string, unknown>[] = [];
9
+ const runtime = createServerRuntime({ NODE_ENV: "test" });
10
+ const observability = createPlatformObservability({
11
+ databaseRequired: false,
12
+ auditStatus: () => runtime.auditSink.status(),
13
+ logger: {
14
+ info(fields) {
15
+ records.push(fields);
16
+ },
17
+ },
18
+ });
19
+ const app = createPlatformHttpApp(runtime, observability).get(
20
+ "/probe",
21
+ () => ({ ok: true }),
22
+ );
23
+
24
+ const outsidePrefix = await app.handle(
25
+ new Request("http://localhost/health/live"),
26
+ );
27
+ const insidePrefix = await app.handle(
28
+ new Request("http://localhost/api/missing"),
29
+ );
30
+ const subsequent = await app.handle(
31
+ new Request("http://localhost/api/probe"),
32
+ );
33
+ await Bun.sleep(0);
34
+
35
+ expect(outsidePrefix.status).toBe(404);
36
+ expect(insidePrefix.status).toBe(404);
37
+ expect(subsequent.status).toBe(200);
38
+ expect(outsidePrefix.headers.get("x-request-id")).toMatch(
39
+ /^[A-Za-z0-9._-]{1,128}$/,
40
+ );
41
+ expect(
42
+ records.filter(({ event }) => event === "http.request.completed"),
43
+ ).toHaveLength(3);
44
+ expect(observability.metrics.render()).toContain(
45
+ 'windy_http_requests_total{method="GET",status_class="4xx"} 2',
46
+ );
47
+ expect(observability.metrics.render()).toContain(
48
+ 'windy_http_requests_total{method="GET",status_class="2xx"} 1',
49
+ );
50
+ expect(observability.metrics.render()).toContain(
51
+ "windy_http_requests_active 0",
52
+ );
53
+ });
54
+
55
+ test("handler 异常只完成一次请求观测", async () => {
56
+ const records: Record<string, unknown>[] = [];
57
+ const runtime = createServerRuntime({ NODE_ENV: "test" });
58
+ const observability = createPlatformObservability({
59
+ databaseRequired: false,
60
+ auditStatus: () => runtime.auditSink.status(),
61
+ logger: {
62
+ info(fields) {
63
+ records.push(fields);
64
+ },
65
+ },
66
+ });
67
+ const app = createPlatformHttpApp(runtime, observability)
68
+ .onError(({ set }) => {
69
+ set.status = 500;
70
+ return { error: "INTERNAL_ERROR" };
71
+ })
72
+ .get("/failure", () => {
73
+ throw new Error("failure");
74
+ });
75
+
76
+ const response = await app.handle(
77
+ new Request("http://localhost/api/failure"),
78
+ );
79
+ await Bun.sleep(0);
80
+
81
+ expect(response.status).toBe(500);
82
+ expect(
83
+ records.filter(({ event }) => event === "http.request.completed"),
84
+ ).toHaveLength(1);
85
+ expect(observability.metrics.render()).toContain(
86
+ 'windy_http_requests_total{method="GET",status_class="5xx"} 1',
87
+ );
88
+ expect(observability.metrics.render()).toContain(
89
+ "windy_http_requests_active 0",
90
+ );
91
+ });
92
+
7
93
  test("校验并回传 request id,日志与指标不包含 URL 和查询值", async () => {
8
94
  const records: Record<string, unknown>[] = [];
9
95
  const runtime = createServerRuntime({ NODE_ENV: "test" });
@@ -25,6 +111,7 @@ describe("platform HTTP app observability", () => {
25
111
  headers: { "x-request-id": "invalid request id" },
26
112
  }),
27
113
  );
114
+ await Bun.sleep(0);
28
115
 
29
116
  expect(response.status).toBe(200);
30
117
  expect(response.headers.get("x-request-id")).toMatch(
@@ -1,6 +1,8 @@
1
1
  import { swagger } from "@elysiajs/swagger";
2
2
  import { Elysia } from "elysia";
3
+ import type { RequestGuardContext } from "./guards.js";
3
4
  import type { createPlatformObservability } from "./observability/bootstrap.js";
5
+ import type { RequestObservation } from "./observability/metrics.js";
4
6
  // @windy-module system.license begin
5
7
  import { restrictedRequestResponse } from "./request-restriction.js";
6
8
  // @windy-module system.license end
@@ -8,38 +10,18 @@ import type { createServerRuntime } from "./runtime.js";
8
10
 
9
11
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
10
12
  type PlatformObservability = ReturnType<typeof createPlatformObservability>;
13
+ interface HttpRequestState {
14
+ guardContext: RequestGuardContext;
15
+ requestObservation: RequestObservation;
16
+ }
11
17
 
12
18
  export function createPlatformHttpApp(
13
19
  runtime: ServerRuntime,
14
20
  observability: PlatformObservability,
15
21
  ) {
22
+ const requestStates = new WeakMap<Request, HttpRequestState>();
16
23
  return (
17
24
  new Elysia({ prefix: "/api" })
18
- .derive(async ({ request, server }) => ({
19
- guardContext: await runtime.createGuardContext(request, {
20
- clientIp: server?.requestIP(request)?.address,
21
- }),
22
- requestObservation: observability.http.begin(request.method),
23
- }))
24
- // @windy-module system.license begin
25
- .onBeforeHandle(({ request, guardContext }) =>
26
- restrictedRequestResponse(runtime, guardContext, request),
27
- )
28
- // @windy-module system.license end
29
- .onAfterHandle(({ guardContext, set }) => {
30
- set.headers["x-request-id"] = guardContext.requestId;
31
- })
32
- .onAfterResponse(
33
- ({ request, guardContext, requestObservation, responseValue, set }) => {
34
- observability.http.complete({
35
- request,
36
- guardContext,
37
- observation: requestObservation,
38
- responseValue,
39
- status: set.status,
40
- });
41
- },
42
- )
43
25
  .use(
44
26
  swagger({
45
27
  documentation: {
@@ -47,5 +29,41 @@ export function createPlatformHttpApp(
47
29
  },
48
30
  }),
49
31
  )
32
+ .onRequest(async ({ request, server, set }) => {
33
+ if (requestStates.has(request)) return;
34
+ const guardContext = await runtime.createGuardContext(request, {
35
+ clientIp: server?.requestIP(request)?.address,
36
+ });
37
+ requestStates.set(request, {
38
+ guardContext,
39
+ requestObservation: observability.http.begin(request.method),
40
+ });
41
+ set.headers["x-request-id"] = guardContext.requestId;
42
+ })
43
+ .derive(({ request }) => {
44
+ const state = requestStates.get(request);
45
+ if (!state) throw new Error("HTTP_REQUEST_STATE_UNAVAILABLE");
46
+ return {
47
+ guardContext: state.guardContext,
48
+ requestObservation: state.requestObservation,
49
+ };
50
+ })
51
+ // @windy-module system.license begin
52
+ .onBeforeHandle(({ request, guardContext }) =>
53
+ restrictedRequestResponse(runtime, guardContext, request),
54
+ )
55
+ // @windy-module system.license end
56
+ .onAfterResponse(({ request, responseValue, set }) => {
57
+ const state = requestStates.get(request);
58
+ if (!state) return;
59
+ requestStates.delete(request);
60
+ observability.http.complete({
61
+ request,
62
+ guardContext: state.guardContext,
63
+ observation: state.requestObservation,
64
+ responseValue,
65
+ status: set.status,
66
+ });
67
+ })
50
68
  );
51
69
  }
@@ -22,6 +22,9 @@ import {
22
22
  registerAgentRoutes,
23
23
  } from "./agent/routes.js";
24
24
  import { registerAgentPlatformRuntime } from "./agent/runtime-bootstrap.js";
25
+ import { GovernedToolRegistry } from "./agent/tool-registry.js";
26
+ import { PlatformMcpAdapter } from "./mcp/adapter.js";
27
+ import { registerMcpRoutes } from "./mcp/routes.js";
25
28
  // @windy-module system.agent end
26
29
  import { registerAuthRoutes, registerAuthSessionRoute } from "./auth/routes.js";
27
30
  import { createAuthRuntime } from "./auth/runtime.js";
@@ -292,14 +295,23 @@ const toolHandlers = new Map([
292
295
  ...createSystemToolHandlers(systemRepositories, runtime),
293
296
  ...createModuleToolHandlerMap(moduleHosts),
294
297
  ]);
295
- registerAgentRoutes(app, foundationModules, toolHandlers, runtime);
298
+ const toolRegistry = new GovernedToolRegistry(
299
+ foundationModules,
300
+ toolHandlers,
301
+ runtime,
302
+ );
303
+ registerAgentRoutes(app, toolRegistry);
304
+ registerMcpRoutes(
305
+ app,
306
+ new PlatformMcpAdapter(toolRegistry, runtime, foundationSnapshot.features),
307
+ );
296
308
  registerAgentPlatformRuntime({
297
309
  app,
298
310
  platformConfig,
299
311
  modules: foundationModules,
300
312
  hosts: moduleHosts,
301
313
  runtime,
302
- toolHandlers,
314
+ toolRegistry,
303
315
  });
304
316
  // @windy-module system.agent end
305
317
  // @windy-module system.scheduler begin
@@ -16,6 +16,7 @@ import {
16
16
  createSystemToolHandlers,
17
17
  registerAgentRoutes,
18
18
  } from "../agent/routes.js";
19
+ import { GovernedToolRegistry } from "../agent/tool-registry.js";
19
20
  import { runGuardedBackgroundTask } from "../background/task-guard.js";
20
21
  import {
21
22
  createFoundationSnapshot,
@@ -264,9 +265,11 @@ function createFixture(
264
265
  registerSystemRoutes(app, repositories, runtime, snapshot.features);
265
266
  registerAgentRoutes(
266
267
  app,
267
- loadFoundationModules(),
268
- createSystemToolHandlers(repositories, runtime),
269
- runtime,
268
+ new GovernedToolRegistry(
269
+ loadFoundationModules(),
270
+ createSystemToolHandlers(repositories, runtime),
271
+ runtime,
272
+ ),
270
273
  );
271
274
  return { app, runtime, licenseRuntime };
272
275
  }