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.
- package/README.md +6 -1
- package/dist/cli.js +54 -44
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/AGENTS.md +1 -1
- package/template/apps/agent-server/Dockerfile +1 -1
- package/template/apps/agent-server/README.md +1 -1
- package/template/apps/agent-server/src/southwind_agent_server/asgi.py +8 -0
- package/template/apps/agent-server/src/southwind_agent_server/runtime_logging.py +57 -0
- package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +134 -0
- package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +4 -0
- package/template/apps/agent-server/tests/test_run_operational_logging.py +114 -0
- package/template/apps/agent-server/tests/test_structured_logging.py +105 -0
- package/template/apps/server/src/agent/isolated-execution-guard.test.ts +194 -0
- package/template/apps/server/src/agent/isolated-execution-guard.ts +202 -0
- package/template/apps/server/src/agent/run-definition-support.ts +11 -1
- package/template/apps/server/src/agent/run-facade.ts +7 -11
- package/template/apps/server/src/agent/run-operation-policy.ts +11 -0
- package/template/apps/server/src/agent/workflow-durable-runtime-input.ts +8 -5
- package/template/apps/server/src/jobs/legacy-service-runtime.ts +4 -0
- package/template/apps/server/src/jobs/runtime-logging.test.ts +212 -0
- package/template/apps/server/src/jobs/runtime-logging.ts +58 -0
- package/template/apps/server/src/jobs/worker.ts +54 -12
- package/template/apps/server/src/observability/bootstrap.ts +1 -4
- package/template/apps/server/src/observability/http-observability.ts +2 -3
- package/template/apps/server/src/observability/structured-logger.test.ts +31 -0
- package/template/apps/server/src/observability/structured-logger.ts +27 -0
- package/template/apps/server/src/platform-bootstrap.ts +9 -3
- package/template/apps/server/src/startup-finalization.ts +3 -3
- package/template/apps/server/src/system/built-in-roles.test.ts +23 -0
- package/template/apps/server/src/system/built-in-roles.ts +10 -4
- package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +16 -5
- package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +4 -0
- package/template/docker-compose.yml +3 -3
- package/template/docs/architecture/log-governance-implementation.md +79 -0
- package/template/docs/architecture/log-governance-plan.md +39 -0
- package/template/docs/operations/observability.md +36 -4
- package/template/docs/platform/module-configuration.md +1 -1
- package/template/docs/platform/organization-membership.md +1 -1
- package/template/docs/platform/platform-authentication.md +1 -1
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-isolated-execution-policy.test.ts +70 -0
- package/template/packages/modules/src/ai-isolated-execution-policy.ts +76 -0
- package/template/packages/modules/src/ai-operation-composition.ts +2 -0
- package/template/packages/modules/src/manifest.ts +23 -0
- package/template/packages/modules/src/system-features.ts +11 -10
- package/template/packages/server-sdk/package.json +1 -1
- package/template/packages/shared/index.ts +1 -0
- package/template/packages/shared/package.json +3 -2
- package/template/packages/shared/src/structured-log.test.ts +147 -0
- package/template/packages/shared/src/structured-log.ts +215 -0
- 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
|
+
}
|