create-windy 0.8.0 → 0.8.2
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 +14 -8
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- 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/attempt_context.py +9 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +5 -2
- package/template/apps/agent-server/src/southwind_agent_server/provider_output_guidance.py +34 -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 +145 -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 +142 -0
- package/template/apps/agent-server/tests/test_tool_structured_output.py +347 -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/docs/architecture/log-governance-implementation.md +96 -0
- package/template/docs/architecture/log-governance-plan.md +39 -0
- package/template/docs/operations/observability.md +36 -4
- package/template/docs/platform/agent-runtime.md +18 -0
- 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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ModuleAiOperationDefinition } from "./manifest.js";
|
|
2
|
+
|
|
3
|
+
const ceilings = {
|
|
4
|
+
maxWorkspaceBytes: 256 * 1024 * 1024,
|
|
5
|
+
maxFileBytes: 64 * 1024 * 1024,
|
|
6
|
+
maxOutputBytes: 16 * 1024 * 1024,
|
|
7
|
+
maxDurationSeconds: 300,
|
|
8
|
+
maxCpuMillis: 300_000,
|
|
9
|
+
maxMemoryBytes: 1024 * 1024 * 1024,
|
|
10
|
+
maxPids: 64,
|
|
11
|
+
maxConcurrentProcesses: 4,
|
|
12
|
+
maxToolCalls: 128,
|
|
13
|
+
maxToolRounds: 32,
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
export function validateAiIsolatedExecutionPolicy(
|
|
17
|
+
operation: ModuleAiOperationDefinition,
|
|
18
|
+
): void {
|
|
19
|
+
const policy = operation.isolatedExecution;
|
|
20
|
+
if (policy === undefined) return;
|
|
21
|
+
if (
|
|
22
|
+
!policy ||
|
|
23
|
+
operation.kind !== "agent" ||
|
|
24
|
+
policy.version !== 1 ||
|
|
25
|
+
policy.mode !== "isolated" ||
|
|
26
|
+
Object.keys(policy).sort().join(",") !==
|
|
27
|
+
"inputArtifactTypes,languages,limits,mode,network,version"
|
|
28
|
+
) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`${operation.key} 隔离执行只允许 Agent Operation 的 V1 策略`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (
|
|
34
|
+
!Array.isArray(policy.languages) ||
|
|
35
|
+
!policy.languages.length ||
|
|
36
|
+
new Set(policy.languages).size !== policy.languages.length ||
|
|
37
|
+
policy.languages.some((language) => !["python", "node"].includes(language))
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`${operation.key} 隔离执行语言无效`);
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
!Array.isArray(policy.inputArtifactTypes) ||
|
|
43
|
+
!policy.inputArtifactTypes.length ||
|
|
44
|
+
new Set(policy.inputArtifactTypes).size !==
|
|
45
|
+
policy.inputArtifactTypes.length ||
|
|
46
|
+
policy.inputArtifactTypes.some(
|
|
47
|
+
(type) =>
|
|
48
|
+
typeof type !== "string" ||
|
|
49
|
+
!/^[a-z][a-z0-9.+-]*\/[a-z0-9.+-]+$/u.test(type),
|
|
50
|
+
)
|
|
51
|
+
) {
|
|
52
|
+
throw new Error(`${operation.key} 隔离执行输入 Artifact 类型无效`);
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
policy.network !== "forbidden" ||
|
|
56
|
+
operation.toolNetworkPolicy?.mode !== "forbidden"
|
|
57
|
+
) {
|
|
58
|
+
throw new Error(`${operation.key} 隔离执行必须禁止 Tool 网络`);
|
|
59
|
+
}
|
|
60
|
+
const limits = policy.limits;
|
|
61
|
+
if (
|
|
62
|
+
!limits ||
|
|
63
|
+
Object.keys(limits).length !== Object.keys(ceilings).length ||
|
|
64
|
+
Object.entries(ceilings).some(([key, ceiling]) => {
|
|
65
|
+
const value = limits[key as keyof typeof ceilings];
|
|
66
|
+
return !Number.isSafeInteger(value) || value <= 0 || value > ceiling;
|
|
67
|
+
}) ||
|
|
68
|
+
limits.maxFileBytes > limits.maxWorkspaceBytes ||
|
|
69
|
+
limits.maxOutputBytes > limits.maxWorkspaceBytes ||
|
|
70
|
+
limits.maxDurationSeconds > operation.limits.maxDurationSeconds ||
|
|
71
|
+
limits.maxOutputBytes > operation.limits.maxOutputBytes ||
|
|
72
|
+
limits.maxToolRounds > limits.maxToolCalls
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(`${operation.key} 隔离执行预算越过 Operation 或 Host 上限`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -6,6 +6,7 @@ import { validateAiNetworkPolicy } from "./ai-network-policy.js";
|
|
|
6
6
|
import { validateAiOperationReadinessContract } from "./ai-operation-readiness-contract.js";
|
|
7
7
|
import { validateAiScreenshotContext } from "./ai-screenshot-context.js";
|
|
8
8
|
import { validateClientExecution } from "./ai-client-execution-policy.js";
|
|
9
|
+
import { validateAiIsolatedExecutionPolicy } from "./ai-isolated-execution-policy.js";
|
|
9
10
|
|
|
10
11
|
interface AiOperationCatalogs {
|
|
11
12
|
permissions: ReadonlySet<string>;
|
|
@@ -107,6 +108,7 @@ function validateOperation(
|
|
|
107
108
|
validateAiNetworkPolicy(operation);
|
|
108
109
|
validateProviderDataPolicy(operation);
|
|
109
110
|
validateClientExecution(operation);
|
|
111
|
+
validateAiIsolatedExecutionPolicy(operation);
|
|
110
112
|
validateLimits(operation);
|
|
111
113
|
validateAiOperationReadinessContract(operation);
|
|
112
114
|
validateAiScreenshotContext(operation);
|
|
@@ -167,6 +167,27 @@ export interface AiOperationLimits {
|
|
|
167
167
|
maxPixelsPerArtifact?: number;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/** Opt-in policy for a future, separate Python/Node Execution Runtime. Absence disables execution. */
|
|
171
|
+
export interface AiIsolatedExecutionPolicyV1 {
|
|
172
|
+
readonly version: 1;
|
|
173
|
+
readonly mode: "isolated";
|
|
174
|
+
readonly languages: readonly ("python" | "node")[];
|
|
175
|
+
readonly inputArtifactTypes: readonly string[];
|
|
176
|
+
readonly network: "forbidden";
|
|
177
|
+
readonly limits: {
|
|
178
|
+
readonly maxWorkspaceBytes: number;
|
|
179
|
+
readonly maxFileBytes: number;
|
|
180
|
+
readonly maxOutputBytes: number;
|
|
181
|
+
readonly maxDurationSeconds: number;
|
|
182
|
+
readonly maxCpuMillis: number;
|
|
183
|
+
readonly maxMemoryBytes: number;
|
|
184
|
+
readonly maxPids: number;
|
|
185
|
+
readonly maxConcurrentProcesses: number;
|
|
186
|
+
readonly maxToolCalls: number;
|
|
187
|
+
readonly maxToolRounds: number;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
170
191
|
/**
|
|
171
192
|
* 允许受治理的宿主客户端在本地编排模型循环。
|
|
172
193
|
*
|
|
@@ -234,6 +255,8 @@ export interface ModuleAiOperationDefinition {
|
|
|
234
255
|
providerDataPolicy?: AiProviderDataPolicy;
|
|
235
256
|
/** 缺省不允许浏览器或其它宿主客户端执行本地 Agent 循环。 */
|
|
236
257
|
clientExecution?: AiClientExecutionPolicy;
|
|
258
|
+
/** Missing means no workspace or process execution capability. */
|
|
259
|
+
isolatedExecution?: AiIsolatedExecutionPolicyV1;
|
|
237
260
|
toolNetworkPolicy?: AiToolNetworkPolicy;
|
|
238
261
|
limits: AiOperationLimits;
|
|
239
262
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as shared from "@southwind-ai/shared";
|
|
2
2
|
import type { ModuleManifest } from "./manifest.js";
|
|
3
3
|
|
|
4
4
|
// @windy-module system.scheduler begin
|
|
5
|
-
const standardAndAdvanced = ["standard", "advanced"].map(
|
|
5
|
+
const standardAndAdvanced = ["standard", "advanced"].map(
|
|
6
|
+
shared.licenseVersionKey,
|
|
7
|
+
);
|
|
6
8
|
// @windy-module system.scheduler end
|
|
7
|
-
const advancedOnly = [licenseVersionKey("advanced")];
|
|
8
9
|
|
|
9
10
|
export const systemFeatures: ModuleManifest["features"] = [
|
|
10
11
|
feature(
|
|
@@ -109,7 +110,7 @@ export const systemFeatures: ModuleManifest["features"] = [
|
|
|
109
110
|
"Agent 长期会话",
|
|
110
111
|
"恢复受治理的长期 Session,并解释 Context 与 Memory 来源。",
|
|
111
112
|
),
|
|
112
|
-
dependencies: [featureKey("system.agent")],
|
|
113
|
+
dependencies: [shared.featureKey("system.agent")],
|
|
113
114
|
},
|
|
114
115
|
{
|
|
115
116
|
...feature(
|
|
@@ -117,7 +118,7 @@ export const systemFeatures: ModuleManifest["features"] = [
|
|
|
117
118
|
"AI Runtime 报表",
|
|
118
119
|
"查看受数据范围约束的 Agent Run、模型用量、缓存与失败事实。",
|
|
119
120
|
),
|
|
120
|
-
dependencies: [featureKey("system.agent")],
|
|
121
|
+
dependencies: [shared.featureKey("system.agent")],
|
|
121
122
|
},
|
|
122
123
|
{
|
|
123
124
|
...feature(
|
|
@@ -125,8 +126,8 @@ export const systemFeatures: ModuleManifest["features"] = [
|
|
|
125
126
|
"Agent Workflow",
|
|
126
127
|
"查看和运行具备预算、恢复、委派与持久审计边界的不可变 Agent Workflow Release。",
|
|
127
128
|
),
|
|
128
|
-
dependencies: [featureKey("system.agent")],
|
|
129
|
-
allowedLicenseVersions:
|
|
129
|
+
dependencies: [shared.featureKey("system.agent")],
|
|
130
|
+
allowedLicenseVersions: [shared.licenseVersionKey("advanced")],
|
|
130
131
|
},
|
|
131
132
|
// @windy-module system.agent end
|
|
132
133
|
// @windy-module system.operations begin
|
|
@@ -167,7 +168,7 @@ export const systemFeatures: ModuleManifest["features"] = [
|
|
|
167
168
|
"批量数据任务",
|
|
168
169
|
"受控的大批量导入、导出、失败明细和结果留存。",
|
|
169
170
|
),
|
|
170
|
-
allowedLicenseVersions:
|
|
171
|
+
allowedLicenseVersions: [shared.licenseVersionKey("advanced")],
|
|
171
172
|
},
|
|
172
173
|
// @windy-module system.bulk-data end
|
|
173
174
|
];
|
|
@@ -178,7 +179,7 @@ function feature(
|
|
|
178
179
|
description?: string,
|
|
179
180
|
): ModuleManifest["features"][number] {
|
|
180
181
|
return {
|
|
181
|
-
key: featureKey(key),
|
|
182
|
+
key: shared.featureKey(key),
|
|
182
183
|
label,
|
|
183
184
|
module: "system",
|
|
184
185
|
description,
|
|
@@ -195,6 +196,6 @@ function rbacFeature(
|
|
|
195
196
|
): ModuleManifest["features"][number] {
|
|
196
197
|
return {
|
|
197
198
|
...feature(key, label, description),
|
|
198
|
-
dependencies: [featureKey("system.rbac")],
|
|
199
|
+
dependencies: [shared.featureKey("system.rbac")],
|
|
199
200
|
};
|
|
200
201
|
}
|
|
@@ -17,6 +17,7 @@ export * from "./src/migration-bundle-validation.js";
|
|
|
17
17
|
export * from "./src/mcp.js";
|
|
18
18
|
export * from "./src/module-configuration.js";
|
|
19
19
|
export * from "./src/correlation.js";
|
|
20
|
+
export * from "./src/structured-log.js";
|
|
20
21
|
export * from "./src/notification.js";
|
|
21
22
|
export * from "./src/password.js";
|
|
22
23
|
export * from "./src/platform-settings.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@southwind-ai/shared",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"typecheck": "tsc --noEmit --project tsconfig.json"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
|
-
".": "./index.ts"
|
|
18
|
+
".": "./index.ts",
|
|
19
|
+
"./structured-log": "./src/structured-log.ts"
|
|
19
20
|
},
|
|
20
21
|
"publishConfig": {
|
|
21
22
|
"access": "public"
|
|
@@ -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
|
+
}
|