create-windy 0.7.0 → 0.8.1

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 (52) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +54 -44
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +1 -1
  6. package/template/apps/agent-server/Dockerfile +1 -1
  7. package/template/apps/agent-server/README.md +1 -1
  8. package/template/apps/agent-server/src/southwind_agent_server/asgi.py +8 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/runtime_logging.py +57 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +134 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +4 -0
  12. package/template/apps/agent-server/tests/test_run_operational_logging.py +114 -0
  13. package/template/apps/agent-server/tests/test_structured_logging.py +105 -0
  14. package/template/apps/server/src/agent/isolated-execution-guard.test.ts +194 -0
  15. package/template/apps/server/src/agent/isolated-execution-guard.ts +202 -0
  16. package/template/apps/server/src/agent/run-definition-support.ts +11 -1
  17. package/template/apps/server/src/agent/run-facade.ts +7 -11
  18. package/template/apps/server/src/agent/run-operation-policy.ts +11 -0
  19. package/template/apps/server/src/agent/workflow-durable-runtime-input.ts +8 -5
  20. package/template/apps/server/src/jobs/legacy-service-runtime.ts +4 -0
  21. package/template/apps/server/src/jobs/runtime-logging.test.ts +212 -0
  22. package/template/apps/server/src/jobs/runtime-logging.ts +58 -0
  23. package/template/apps/server/src/jobs/worker.ts +54 -12
  24. package/template/apps/server/src/observability/bootstrap.ts +1 -4
  25. package/template/apps/server/src/observability/http-observability.ts +2 -3
  26. package/template/apps/server/src/observability/structured-logger.test.ts +31 -0
  27. package/template/apps/server/src/observability/structured-logger.ts +27 -0
  28. package/template/apps/server/src/platform-bootstrap.ts +9 -3
  29. package/template/apps/server/src/startup-finalization.ts +3 -3
  30. package/template/apps/server/src/system/built-in-roles.test.ts +23 -0
  31. package/template/apps/server/src/system/built-in-roles.ts +10 -4
  32. package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +16 -5
  33. package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +4 -0
  34. package/template/docker-compose.yml +3 -3
  35. package/template/docs/architecture/log-governance-implementation.md +79 -0
  36. package/template/docs/architecture/log-governance-plan.md +39 -0
  37. package/template/docs/operations/observability.md +36 -4
  38. package/template/docs/platform/module-configuration.md +1 -1
  39. package/template/docs/platform/organization-membership.md +1 -1
  40. package/template/docs/platform/platform-authentication.md +1 -1
  41. package/template/packages/modules/package.json +1 -1
  42. package/template/packages/modules/src/ai-isolated-execution-policy.test.ts +70 -0
  43. package/template/packages/modules/src/ai-isolated-execution-policy.ts +76 -0
  44. package/template/packages/modules/src/ai-operation-composition.ts +2 -0
  45. package/template/packages/modules/src/manifest.ts +23 -0
  46. package/template/packages/modules/src/system-features.ts +11 -10
  47. package/template/packages/server-sdk/package.json +1 -1
  48. package/template/packages/shared/index.ts +1 -0
  49. package/template/packages/shared/package.json +3 -2
  50. package/template/packages/shared/src/structured-log.test.ts +147 -0
  51. package/template/packages/shared/src/structured-log.ts +215 -0
  52. package/template/scripts/lib/postgres-regression.ts +1 -1
@@ -0,0 +1,147 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ createStructuredLogger,
4
+ STRUCTURED_LOG_MAX_BYTES,
5
+ } from "./structured-log.js";
6
+
7
+ describe("structured operational log", () => {
8
+ test("保留跨服务关联标识与稳定排障字段", () => {
9
+ const records: Record<string, unknown>[] = [];
10
+ const logger = createStructuredLogger(
11
+ {
12
+ info: (record) => records.push(record),
13
+ warn: (record) => records.push(record),
14
+ error: (record) => records.push(record),
15
+ },
16
+ "windy-server",
17
+ () => new Date("2026-09-29T00:00:00.000Z"),
18
+ );
19
+ logger.error({
20
+ event: "agent.workflow.continuation.failed",
21
+ requestId: "request-1",
22
+ runId: "run-1",
23
+ jobId: "job-1",
24
+ traceId: "trace-1",
25
+ diagnosticId: "diag-1",
26
+ route: "/api/agent/runs/:runId/events",
27
+ outcome: "failure",
28
+ errorCode: "AGENT_WORKFLOW_CONTINUATION_FAILED",
29
+ stageKey: "dispatch",
30
+ handlerKey: "system.scheduler.dispatch",
31
+ queue: "default",
32
+ attemptNumber: 2,
33
+ count: 10,
34
+ });
35
+ expect(records).toEqual([
36
+ {
37
+ timestamp: "2026-09-29T00:00:00.000Z",
38
+ level: "error",
39
+ component: "windy-server",
40
+ event: "agent.workflow.continuation.failed",
41
+ retentionClass: "operational",
42
+ requestId: "request-1",
43
+ runId: "run-1",
44
+ jobId: "job-1",
45
+ traceId: "trace-1",
46
+ diagnosticId: "diag-1",
47
+ route: "/api/agent/runs/:runId/events",
48
+ outcome: "failure",
49
+ errorCode: "AGENT_WORKFLOW_CONTINUATION_FAILED",
50
+ stageKey: "dispatch",
51
+ handlerKey: "system.scheduler.dispatch",
52
+ queue: "default",
53
+ attemptNumber: 2,
54
+ count: 10,
55
+ },
56
+ ]);
57
+ });
58
+
59
+ test("拒绝未列字段、原始错误和不安全关联标识", () => {
60
+ const records: Record<string, unknown>[] = [];
61
+ const logger = createStructuredLogger(
62
+ {
63
+ info: (record) => records.push(record),
64
+ warn: (record) => records.push(record),
65
+ error: (record) => records.push(record),
66
+ },
67
+ "windy-server",
68
+ );
69
+ logger.error(
70
+ {
71
+ event: "unsafe\nrequest",
72
+ requestId: "bad\nsecret-request",
73
+ runId: "x".repeat(200),
74
+ errorCode: "provider secret payload",
75
+ handlerKey: "payload secret-handler",
76
+ queue: "secret\nqueue",
77
+ attemptNumber: -1,
78
+ diagnosticId: "secret\ndiag",
79
+ route: "/api/users/123?secret-route",
80
+ completedUnits: -1,
81
+ totalUnits: Number.POSITIVE_INFINITY,
82
+ count: -1,
83
+ body: "secret-body",
84
+ authorization: "Bearer secret-token",
85
+ error: new Error("secret-stack"),
86
+ } as unknown as Parameters<typeof logger.error>[0],
87
+ "secret-message",
88
+ "secret-handler",
89
+ "secret-route",
90
+ "secret\\nqueue",
91
+ );
92
+ const serialized = JSON.stringify(records);
93
+ expect(records[0]?.event).toBe("logging.event.invalid");
94
+ for (const secret of [
95
+ "secret-request",
96
+ "secret-body",
97
+ "secret-token",
98
+ "secret-stack",
99
+ "secret-message",
100
+ ]) {
101
+ expect(serialized).not.toContain(secret);
102
+ }
103
+ expect(Buffer.byteLength(serialized)).toBeLessThanOrEqual(
104
+ STRUCTURED_LOG_MAX_BYTES + 2,
105
+ );
106
+ });
107
+
108
+ test("长字段不撑破单行预算", () => {
109
+ const records: Record<string, unknown>[] = [];
110
+ const logger = createStructuredLogger(
111
+ {
112
+ info: (record) => records.push(record),
113
+ warn: (record) => records.push(record),
114
+ error: (record) => records.push(record),
115
+ },
116
+ "windy-server",
117
+ );
118
+ logger.info({
119
+ event: "http.request.completed",
120
+ requestId: "x".repeat(128),
121
+ runId: "x".repeat(128),
122
+ jobId: "x".repeat(128),
123
+ traceId: "x".repeat(128),
124
+ diagnosticId: "x".repeat(128),
125
+ stageKey: "x".repeat(64),
126
+ handlerKey: "x".repeat(64),
127
+ queue: "x".repeat(64),
128
+ attemptNumber: 1_000_000_000,
129
+ });
130
+ expect(Buffer.byteLength(JSON.stringify(records[0]))).toBeLessThanOrEqual(
131
+ STRUCTURED_LOG_MAX_BYTES,
132
+ );
133
+ });
134
+
135
+ test("输出端故障不影响业务调用", () => {
136
+ const fail = () => {
137
+ throw new Error("stdout unavailable");
138
+ };
139
+ const logger = createStructuredLogger(
140
+ { info: fail, warn: fail, error: fail },
141
+ "windy-server",
142
+ );
143
+ expect(() =>
144
+ logger.error({ event: "agent.workflow.continuation.failed" }),
145
+ ).not.toThrow();
146
+ });
147
+ });
@@ -0,0 +1,215 @@
1
+ /** 运行日志只承载排障线索;业务审计和 Agent Run 事实使用各自的持久化入口。 */
2
+ export const STRUCTURED_LOG_MAX_BYTES = 2048;
3
+
4
+ export type StructuredLogLevel = "info" | "warn" | "error";
5
+ export type StructuredLogOutcome = "success" | "failure" | "denied";
6
+
7
+ export interface StructuredLogInput {
8
+ event: string;
9
+ requestId?: string;
10
+ runId?: string;
11
+ jobId?: string;
12
+ traceId?: string;
13
+ diagnosticId?: string;
14
+ outcome?: StructuredLogOutcome;
15
+ errorCode?: string;
16
+ stageKey?: string;
17
+ handlerKey?: string;
18
+ queue?: string;
19
+ attemptNumber?: number;
20
+ method?: string;
21
+ /** 只接受框架登记的路由模板,不能传请求 URL 或实际对象路径。 */
22
+ route?: string;
23
+ statusCode?: number;
24
+ durationMs?: number;
25
+ actorType?: string;
26
+ authority?: string;
27
+ port?: number;
28
+ created?: number;
29
+ metadataUpdated?: number;
30
+ archived?: number;
31
+ unchanged?: number;
32
+ queued?: number;
33
+ dropped?: number;
34
+ completedUnits?: number;
35
+ totalUnits?: number;
36
+ count?: number;
37
+ /** 兼容现有 Logger 接口;未知字段在序列化时一律丢弃。 */
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ export interface StructuredLogSink {
42
+ info(record: Record<string, unknown>): void;
43
+ warn(record: Record<string, unknown>): void;
44
+ error(record: Record<string, unknown>): void;
45
+ }
46
+
47
+ export interface StructuredLogger {
48
+ info(input: StructuredLogInput, message?: string): void;
49
+ warn(input: StructuredLogInput, message?: string): void;
50
+ error(input: StructuredLogInput, message?: string): void;
51
+ }
52
+
53
+ const stableKey = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
54
+ const correlationKey = /^[A-Za-z0-9._-]{1,128}$/;
55
+ const errorKey = /^[A-Z][A-Z0-9_]{0,95}$/;
56
+ const methodKey = /^[A-Z]{1,12}$/;
57
+ const routeTemplate =
58
+ /^\/(?:[A-Za-z][A-Za-z0-9_-]*|:[A-Za-z][A-Za-z0-9_]*|\*)(?:\/(?:[A-Za-z][A-Za-z0-9_-]*|:[A-Za-z][A-Za-z0-9_]*|\*))*$/;
59
+
60
+ /** Adapter 可接 Pino、stdout 或测试 Sink;自由文本 message 不进入日志。 */
61
+ export function createStructuredLogger(
62
+ sink: StructuredLogSink,
63
+ component: string,
64
+ now: () => Date = () => new Date(),
65
+ ): StructuredLogger {
66
+ if (!stableKey.test(component) || component.length > 64) {
67
+ throw new Error("运行日志 component 必须是有界稳定 Key");
68
+ }
69
+ const emit = (level: StructuredLogLevel, input: StructuredLogInput) => {
70
+ const record = structuredLogRecord(level, component, input, now());
71
+ try {
72
+ sink[level](record);
73
+ } catch {
74
+ // 排障日志是旁路输出;审计和业务结果由各自的可靠存储决定。
75
+ }
76
+ };
77
+ return {
78
+ info: (input) => emit("info", input),
79
+ warn: (input) => emit("warn", input),
80
+ error: (input) => emit("error", input),
81
+ };
82
+ }
83
+
84
+ export function structuredLogRecord(
85
+ level: StructuredLogLevel,
86
+ component: string,
87
+ input: StructuredLogInput,
88
+ at: Date,
89
+ ): Record<string, unknown> {
90
+ const record: Record<string, unknown> = {
91
+ timestamp: at.toISOString(),
92
+ level,
93
+ component,
94
+ event:
95
+ stableKey.test(input.event) && input.event.length <= 96
96
+ ? input.event
97
+ : "logging.event.invalid",
98
+ retentionClass: "operational",
99
+ };
100
+ for (const key of [
101
+ "requestId",
102
+ "runId",
103
+ "jobId",
104
+ "traceId",
105
+ "diagnosticId",
106
+ ] as const) {
107
+ const value = input[key];
108
+ if (typeof value === "string" && correlationKey.test(value)) {
109
+ record[key] = value;
110
+ }
111
+ }
112
+ if (
113
+ input.outcome === "success" ||
114
+ input.outcome === "failure" ||
115
+ input.outcome === "denied"
116
+ )
117
+ record.outcome = input.outcome;
118
+ if (typeof input.errorCode === "string" && errorKey.test(input.errorCode)) {
119
+ record.errorCode = input.errorCode;
120
+ }
121
+ for (const key of [
122
+ "stageKey",
123
+ "handlerKey",
124
+ "queue",
125
+ "actorType",
126
+ "authority",
127
+ ] as const) {
128
+ const value = input[key];
129
+ if (
130
+ typeof value === "string" &&
131
+ value.length <= 64 &&
132
+ stableKey.test(value)
133
+ ) {
134
+ record[key] = value;
135
+ }
136
+ }
137
+ if (typeof input.method === "string" && methodKey.test(input.method)) {
138
+ record.method = input.method;
139
+ }
140
+ if (
141
+ typeof input.route === "string" &&
142
+ input.route.length <= 160 &&
143
+ (input.route === "/" || routeTemplate.test(input.route))
144
+ )
145
+ record.route = input.route;
146
+ if (validNumber(input.statusCode, 100, 599, true))
147
+ record.statusCode = input.statusCode;
148
+ if (validNumber(input.durationMs, 0, 3_600_000))
149
+ record.durationMs = input.durationMs;
150
+ if (validNumber(input.port, 1, 65535, true)) record.port = input.port;
151
+ if (validNumber(input.attemptNumber, 1, 1_000_000_000, true))
152
+ record.attemptNumber = input.attemptNumber;
153
+ for (const key of [
154
+ "created",
155
+ "metadataUpdated",
156
+ "archived",
157
+ "unchanged",
158
+ "queued",
159
+ "dropped",
160
+ "completedUnits",
161
+ "totalUnits",
162
+ "count",
163
+ ] as const) {
164
+ const value = input[key];
165
+ if (validNumber(value, 0, 1_000_000_000, true)) record[key] = value;
166
+ }
167
+ for (const key of [
168
+ "dropped",
169
+ "completedUnits",
170
+ "totalUnits",
171
+ "count",
172
+ "route",
173
+ "queued",
174
+ "unchanged",
175
+ "archived",
176
+ "metadataUpdated",
177
+ "created",
178
+ "port",
179
+ "attemptNumber",
180
+ "queue",
181
+ "handlerKey",
182
+ "authority",
183
+ "actorType",
184
+ "stageKey",
185
+ "diagnosticId",
186
+ "traceId",
187
+ "jobId",
188
+ "runId",
189
+ "requestId",
190
+ ] as const) {
191
+ if (
192
+ new TextEncoder().encode(JSON.stringify(record)).length <=
193
+ STRUCTURED_LOG_MAX_BYTES
194
+ )
195
+ break;
196
+ delete record[key];
197
+ record.truncated = true;
198
+ }
199
+ return record;
200
+ }
201
+
202
+ function validNumber(
203
+ value: unknown,
204
+ min: number,
205
+ max: number,
206
+ integer = false,
207
+ ): value is number {
208
+ return (
209
+ typeof value === "number" &&
210
+ Number.isFinite(value) &&
211
+ value >= min &&
212
+ value <= max &&
213
+ (!integer || Number.isInteger(value))
214
+ );
215
+ }
@@ -69,7 +69,7 @@ export class PostgresRegressionHarness {
69
69
  "POSTGRES_PASSWORD=windy",
70
70
  "--publish",
71
71
  "127.0.0.1::5432",
72
- "postgres:17-alpine",
72
+ "pgvector/pgvector:pg18",
73
73
  ]);
74
74
  await this.waitUntilReady();
75
75
  const portResult = await runCommand([