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
|
@@ -5,6 +5,11 @@ import type {
|
|
|
5
5
|
ManualJobRecovery,
|
|
6
6
|
} from "./execution-repository.js";
|
|
7
7
|
import { createDurableArtifactPublisher } from "./artifact-publisher.js";
|
|
8
|
+
import type { StructuredLogger } from "@southwind-ai/shared";
|
|
9
|
+
import {
|
|
10
|
+
logJobAttemptCompleted,
|
|
11
|
+
logJobAttemptStarted,
|
|
12
|
+
} from "./runtime-logging.js";
|
|
8
13
|
import {
|
|
9
14
|
DurableHandlerError,
|
|
10
15
|
type DurableHandlerContext,
|
|
@@ -25,6 +30,7 @@ export interface DurableWorkerOptions {
|
|
|
25
30
|
leaseDurationMs?: number;
|
|
26
31
|
retryDelayMs?: number;
|
|
27
32
|
now?: () => Date;
|
|
33
|
+
logger?: StructuredLogger;
|
|
28
34
|
contextualize?: (input: {
|
|
29
35
|
claim: ClaimedDurableJob;
|
|
30
36
|
moduleName?: string;
|
|
@@ -37,6 +43,7 @@ export class DurableJobWorker {
|
|
|
37
43
|
private readonly retryDelayMs: number;
|
|
38
44
|
private readonly now: () => Date;
|
|
39
45
|
private readonly contextualize?: DurableWorkerOptions["contextualize"];
|
|
46
|
+
private readonly logger?: StructuredLogger;
|
|
40
47
|
private readonly activeRuns = new Map<string, AbortController>();
|
|
41
48
|
|
|
42
49
|
constructor(
|
|
@@ -48,6 +55,7 @@ export class DurableJobWorker {
|
|
|
48
55
|
this.retryDelayMs = options.retryDelayMs || 1_000;
|
|
49
56
|
this.now = options.now || (() => new Date());
|
|
50
57
|
this.contextualize = options.contextualize;
|
|
58
|
+
this.logger = options.logger;
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
async runNext(queue: string, workerId: string): Promise<DurableRunResult> {
|
|
@@ -59,20 +67,54 @@ export class DurableJobWorker {
|
|
|
59
67
|
leaseDurationMs: this.leaseDurationMs,
|
|
60
68
|
});
|
|
61
69
|
if (!claim) return { status: "idle" };
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
70
|
+
const startedAt = performance.now();
|
|
71
|
+
logJobAttemptStarted(this.logger, claim);
|
|
72
|
+
try {
|
|
73
|
+
const handler = this.handlers.get(claim.job.handlerKey);
|
|
74
|
+
if (!handler) {
|
|
75
|
+
await this.repository.fail(
|
|
76
|
+
claim,
|
|
77
|
+
{
|
|
78
|
+
code: "HANDLER_NOT_REGISTERED",
|
|
79
|
+
message: `任务 handler 未注册:${claim.job.handlerKey}`,
|
|
80
|
+
retryable: false,
|
|
81
|
+
},
|
|
82
|
+
this.now(),
|
|
83
|
+
);
|
|
84
|
+
logJobAttemptCompleted(
|
|
85
|
+
this.logger,
|
|
86
|
+
claim,
|
|
87
|
+
"failed",
|
|
88
|
+
performance.now() - startedAt,
|
|
89
|
+
"HANDLER_NOT_REGISTERED",
|
|
90
|
+
);
|
|
91
|
+
return { status: "failed", jobId: claim.jobId };
|
|
92
|
+
}
|
|
93
|
+
const result = await this.execute(claim, handler);
|
|
94
|
+
logJobAttemptCompleted(
|
|
95
|
+
this.logger,
|
|
65
96
|
claim,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
result.status,
|
|
98
|
+
performance.now() - startedAt,
|
|
99
|
+
result.status === "failed"
|
|
100
|
+
? "JOB_ATTEMPT_FAILED"
|
|
101
|
+
: result.status === "cancelled"
|
|
102
|
+
? "JOB_CANCELLED"
|
|
103
|
+
: result.status === "lease-lost"
|
|
104
|
+
? "JOB_LEASE_LOST"
|
|
105
|
+
: undefined,
|
|
106
|
+
);
|
|
107
|
+
return result;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logJobAttemptCompleted(
|
|
110
|
+
this.logger,
|
|
111
|
+
claim,
|
|
112
|
+
"failed",
|
|
113
|
+
performance.now() - startedAt,
|
|
114
|
+
"JOB_WORKER_ERROR",
|
|
72
115
|
);
|
|
73
|
-
|
|
116
|
+
throw error;
|
|
74
117
|
}
|
|
75
|
-
return this.execute(claim, handler);
|
|
76
118
|
}
|
|
77
119
|
|
|
78
120
|
async cancel(jobId: string) {
|
|
@@ -104,7 +146,7 @@ export class DurableJobWorker {
|
|
|
104
146
|
private async execute(
|
|
105
147
|
claim: ClaimedDurableJob,
|
|
106
148
|
handler: RegisteredDurableHandler,
|
|
107
|
-
): Promise<DurableRunResult
|
|
149
|
+
): Promise<Exclude<DurableRunResult, { status: "idle" }>> {
|
|
108
150
|
const abort = new AbortController();
|
|
109
151
|
this.activeRuns.set(claim.jobId, abort);
|
|
110
152
|
const identity = identityOf(claim);
|
|
@@ -49,8 +49,5 @@ export function logPlatformStartup(
|
|
|
49
49
|
{ event: "feature.manifest.synchronized", ...input.featureSync },
|
|
50
50
|
"Feature Manifest 已同步",
|
|
51
51
|
);
|
|
52
|
-
logger.info(
|
|
53
|
-
{ event: "service.started", component: "windy-server", port: input.port },
|
|
54
|
-
"服务已启动",
|
|
55
|
-
);
|
|
52
|
+
logger.info({ event: "service.started", port: input.port }, "服务已启动");
|
|
56
53
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import type { StructuredLogger } from "@southwind-ai/shared";
|
|
1
2
|
import type { RequestGuardContext } from "../guards.js";
|
|
2
3
|
import type { RequestObservation, RuntimeMetrics } from "./metrics.js";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
-
info(fields: Record<string, unknown>, message: string): void;
|
|
6
|
-
}
|
|
5
|
+
export type StructuredRequestLogger = Pick<StructuredLogger, "info">;
|
|
7
6
|
|
|
8
7
|
export class HttpObservability {
|
|
9
8
|
constructor(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createServerLogger } from "./structured-logger.js";
|
|
3
|
+
|
|
4
|
+
describe("Server Pino operational logger", () => {
|
|
5
|
+
test("实际输出单行 JSON 且没有 Pino 隐含字段或自由文本", () => {
|
|
6
|
+
const lines: string[] = [];
|
|
7
|
+
const logger = createServerLogger({ write: (line) => lines.push(line) });
|
|
8
|
+
logger.error(
|
|
9
|
+
{
|
|
10
|
+
event: "agent.workflow.continuation.failed",
|
|
11
|
+
outcome: "failure",
|
|
12
|
+
errorCode: "AGENT_WORKFLOW_CONTINUATION_FAILED",
|
|
13
|
+
requestId: "request-1",
|
|
14
|
+
error: new Error("secret-stack"),
|
|
15
|
+
} as unknown as Parameters<typeof logger.error>[0],
|
|
16
|
+
"secret-message",
|
|
17
|
+
);
|
|
18
|
+
expect(lines).toHaveLength(1);
|
|
19
|
+
const record = JSON.parse(lines[0] ?? "") as Record<string, unknown>;
|
|
20
|
+
expect(record).toMatchObject({
|
|
21
|
+
level: "error",
|
|
22
|
+
component: "windy-server",
|
|
23
|
+
event: "agent.workflow.continuation.failed",
|
|
24
|
+
requestId: "request-1",
|
|
25
|
+
});
|
|
26
|
+
expect(record).not.toHaveProperty("time");
|
|
27
|
+
expect(record).not.toHaveProperty("msg");
|
|
28
|
+
expect(lines[0]).not.toContain("secret-stack");
|
|
29
|
+
expect(lines[0]).not.toContain("secret-message");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createStructuredLogger } from "@southwind-ai/shared";
|
|
2
|
+
import pino, { type DestinationStream } from "pino";
|
|
3
|
+
|
|
4
|
+
/** Pino 只负责写出;字段选择和大小限制由共享契约完成。 */
|
|
5
|
+
export function createServerLogger(destination?: DestinationStream) {
|
|
6
|
+
const options = {
|
|
7
|
+
base: null,
|
|
8
|
+
timestamp: false,
|
|
9
|
+
formatters: { level: (level: string) => ({ level }) },
|
|
10
|
+
};
|
|
11
|
+
const sink = destination ? pino(options, destination) : pino(options);
|
|
12
|
+
const write = (
|
|
13
|
+
level: "info" | "warn" | "error",
|
|
14
|
+
record: Record<string, unknown>,
|
|
15
|
+
) => {
|
|
16
|
+
const { level: _level, ...fields } = record;
|
|
17
|
+
sink[level](fields);
|
|
18
|
+
};
|
|
19
|
+
return createStructuredLogger(
|
|
20
|
+
{
|
|
21
|
+
info: (record) => write("info", record),
|
|
22
|
+
warn: (record) => write("warn", record),
|
|
23
|
+
error: (record) => write("error", record),
|
|
24
|
+
},
|
|
25
|
+
"windy-server",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import { loadPlatformConfig } from "@southwind-ai/config";
|
|
2
|
-
import
|
|
2
|
+
import { createServerLogger } from "./observability/structured-logger.js";
|
|
3
3
|
import { createServerPersistence } from "./persistence.js";
|
|
4
4
|
|
|
5
5
|
export function createPlatformBootstrap() {
|
|
6
|
-
const logger =
|
|
6
|
+
const logger = createServerLogger();
|
|
7
7
|
const platformConfig = loadPlatformConfig();
|
|
8
8
|
const persistence = createServerPersistence(
|
|
9
9
|
process.env,
|
|
10
10
|
(status) => {
|
|
11
|
-
logger.error({
|
|
11
|
+
logger.error({
|
|
12
|
+
event: "audit.persistence.degraded",
|
|
13
|
+
outcome: "failure",
|
|
14
|
+
errorCode: "AUDIT_PERSISTENCE_DEGRADED",
|
|
15
|
+
queued: status.queued,
|
|
16
|
+
dropped: status.dropped,
|
|
17
|
+
});
|
|
12
18
|
},
|
|
13
19
|
platformConfig,
|
|
14
20
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { StructuredLogger } from "@southwind-ai/shared";
|
|
1
2
|
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
2
3
|
import type { InitializedModuleHosts } from "./module-host.js";
|
|
3
4
|
import { validateInstalledRuntime } from "./module-runtime-validation.js";
|
|
@@ -16,9 +17,8 @@ export function finalizePlatformStartup(input: {
|
|
|
16
17
|
providers?: readonly string[];
|
|
17
18
|
schedulerWorker?: { start(): void; stop(): void };
|
|
18
19
|
disposeAgentRuntime?: () => Promise<unknown> | unknown;
|
|
19
|
-
logger: Parameters<typeof logServerStartup>[0] &
|
|
20
|
-
|
|
21
|
-
};
|
|
20
|
+
logger: Parameters<typeof logServerStartup>[0] &
|
|
21
|
+
Pick<StructuredLogger, "error">;
|
|
22
22
|
featureSync: Parameters<typeof logServerStartup>[1];
|
|
23
23
|
persistenceEnabled: boolean;
|
|
24
24
|
}): void {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { createFoundationSnapshot } from "../foundation.js";
|
|
3
|
+
import { createBuiltInRoleSeeds } from "./built-in-roles.js";
|
|
4
|
+
|
|
5
|
+
test("built-in roles only grant permissions in the installed capability catalog", () => {
|
|
6
|
+
const snapshot = createFoundationSnapshot();
|
|
7
|
+
const coreKeys = new Set([
|
|
8
|
+
"system.menu.read",
|
|
9
|
+
"system.profile.read",
|
|
10
|
+
"system.user.read",
|
|
11
|
+
]);
|
|
12
|
+
snapshot.permissions = snapshot.permissions.filter(({ key }) =>
|
|
13
|
+
coreKeys.has(key),
|
|
14
|
+
);
|
|
15
|
+
const roles = createBuiltInRoleSeeds(snapshot);
|
|
16
|
+
for (const role of roles) {
|
|
17
|
+
expect(role.permissionKeys.every((key) => coreKeys.has(key))).toBe(true);
|
|
18
|
+
}
|
|
19
|
+
expect(roles.find(({ code }) => code === "user")?.permissionKeys).toEqual([
|
|
20
|
+
"system.menu.read",
|
|
21
|
+
"system.profile.read",
|
|
22
|
+
]);
|
|
23
|
+
});
|
|
@@ -25,17 +25,23 @@ export function createBuiltInRoleSeeds(
|
|
|
25
25
|
snapshot: FoundationSnapshot,
|
|
26
26
|
): RoleSeed[] {
|
|
27
27
|
const allPermissions = snapshot.permissions.map(({ key }) => key).sort();
|
|
28
|
+
const registeredPermissions = new Set<string>(allPermissions);
|
|
29
|
+
const supportedOrdinaryPermissions = ordinaryPermissions.filter((key) =>
|
|
30
|
+
registeredPermissions.has(key),
|
|
31
|
+
);
|
|
28
32
|
const developerPermissions = snapshot.permissions
|
|
29
33
|
.filter(({ accessRisk }) => !accessRisk || accessRisk === "standard")
|
|
30
34
|
.map(({ key }) => key)
|
|
31
35
|
.sort();
|
|
32
36
|
const allMenus = collectNavigableMenuKeys(snapshot.menus);
|
|
33
37
|
const departmentManagerPermissions = [
|
|
34
|
-
...
|
|
38
|
+
...supportedOrdinaryPermissions,
|
|
35
39
|
"system.department.read",
|
|
36
40
|
"system.user.read",
|
|
37
41
|
"system.user-directory.read",
|
|
38
|
-
]
|
|
42
|
+
]
|
|
43
|
+
.filter((key) => registeredPermissions.has(key))
|
|
44
|
+
.sort();
|
|
39
45
|
return [
|
|
40
46
|
role(
|
|
41
47
|
"role_super_admin",
|
|
@@ -58,8 +64,8 @@ export function createBuiltInRoleSeeds(
|
|
|
58
64
|
"user",
|
|
59
65
|
"普通用户",
|
|
60
66
|
"self",
|
|
61
|
-
|
|
62
|
-
menuKeysForPermissions(snapshot,
|
|
67
|
+
supportedOrdinaryPermissions,
|
|
68
|
+
menuKeysForPermissions(snapshot, supportedOrdinaryPermissions),
|
|
63
69
|
),
|
|
64
70
|
role(
|
|
65
71
|
"role_developer",
|
|
@@ -33,6 +33,17 @@ export function useResourceEditor(options: {
|
|
|
33
33
|
try {
|
|
34
34
|
const normalized = { ...payload };
|
|
35
35
|
delete normalized._transitionTargetDepartmentId;
|
|
36
|
+
if (
|
|
37
|
+
options.key === "users" &&
|
|
38
|
+
!item.value &&
|
|
39
|
+
(typeof payload.temporaryPassword !== "string" ||
|
|
40
|
+
!payload.temporaryPassword)
|
|
41
|
+
) {
|
|
42
|
+
toast.error("请设置一次性初始密码", {
|
|
43
|
+
description: "密码至少 12 个字符,创建后首次登录即可修改。",
|
|
44
|
+
});
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
36
47
|
if (
|
|
37
48
|
options.key === "departments" &&
|
|
38
49
|
item.value &&
|
|
@@ -54,12 +65,12 @@ export function useResourceEditor(options: {
|
|
|
54
65
|
? await saveUserAccount(
|
|
55
66
|
normalized,
|
|
56
67
|
savedAccountId.value,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
async (accountPayload, id) => {
|
|
69
|
+
const record = await options.save(accountPayload, id);
|
|
70
|
+
if (record) savedAccountId.value = record.id;
|
|
71
|
+
return record;
|
|
61
72
|
},
|
|
62
|
-
|
|
73
|
+
toast,
|
|
63
74
|
)
|
|
64
75
|
: await options.save(normalized, item.value?.id);
|
|
65
76
|
if (saved) options.mutated();
|
|
@@ -40,6 +40,10 @@ describe("账户编辑保存闭环", () => {
|
|
|
40
40
|
expect(editor.item.value).toBeUndefined();
|
|
41
41
|
expect(save.mock.calls[0]?.[1]).toBeUndefined();
|
|
42
42
|
expect(mutated).not.toHaveBeenCalled();
|
|
43
|
+
expect(await editor.submit({ ...payload, temporaryPassword: "" })).toBe(
|
|
44
|
+
false,
|
|
45
|
+
);
|
|
46
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
43
47
|
expect(await editor.submit(payload)).toBe(true);
|
|
44
48
|
expect(save.mock.calls[1]?.[1]).toBe("user_created");
|
|
45
49
|
expect(api.reset).toHaveBeenLastCalledWith(
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# 结构化运行日志技术实现
|
|
2
|
+
|
|
3
|
+
状态:SDK 与 E1 已随 Windy `0.8.1` 正式发布,利剑完成官方更新和正式 SDK 四条链定向联调;
|
|
4
|
+
完整隔离执行、真实 Provider、生产采集留存及集中查询/告警平台未验收。
|
|
5
|
+
对应业务方案见
|
|
6
|
+
[日志治理方案](./log-governance-plan.md)。
|
|
7
|
+
|
|
8
|
+
## 当前调用链
|
|
9
|
+
|
|
10
|
+
- `packages/shared/src/structured-log.ts` 提供跨服务字段契约与 TypeScript SDK。调用者提交稳定
|
|
11
|
+
`event`、关联 ID、结果、错误码和少量明确允许的数值字段;序列化器生成时间、等级、组件和
|
|
12
|
+
`retentionClass=operational`,只输出白名单字段,并限制单行 2048 UTF-8 字节。任意 message、
|
|
13
|
+
原始 Error、请求内容、Token 与未知字段不会进入记录。
|
|
14
|
+
公共入口 `@southwind-ai/shared/structured-log` 可独立消费日志 SDK;利剑用真实打包候选的
|
|
15
|
+
同一入口验证,保留原 workspace shared,避免同名同版本 package identity 的类型去重冲突。
|
|
16
|
+
字段同时支持 `diagnosticId`、登记的 `route` 模板、`handlerKey/queue/attemptNumber` 及
|
|
17
|
+
有界 `completedUnits/totalUnits/count`。路由不能传实际 URL、query、fragment 或对象路径;
|
|
18
|
+
SDK 做词法校验,调用方负责从框架登记模板取得它。
|
|
19
|
+
- `apps/server/src/observability/structured-logger.ts` 将该契约接入 Pino。
|
|
20
|
+
`platform-bootstrap.ts` 创建同一 logger,HTTP 请求完成、启动、审计降级及 Workflow continuation
|
|
21
|
+
的现有日志路径继续使用它。Workflow continuation 只把原始异常字段改为稳定错误码,不改派发和
|
|
22
|
+
错误处理行为。
|
|
23
|
+
- `createLegacyServiceRuntime` 将统一 Logger 注入 `DurableJobWorker`;每次实际 claim 记录
|
|
24
|
+
`job.attempt.started` 和唯一的 `job.attempt.completed`,包括未注册 Handler、失败、取消和租约
|
|
25
|
+
丢失。只读 canonical claim 的 Job/阶段/重试/Handler/Queue 坐标,不扫描 payload 或记录租约
|
|
26
|
+
Token。idle 不产生日志。
|
|
27
|
+
- `apps/agent-server/src/southwind_agent_server/structured_logging.py` 为 Python 包级日志安装对等
|
|
28
|
+
适配器。`asgi.py` 保留原 FastAPI app 对象,在生产 ASGI 入口安装 handler;Dockerfile 与开发
|
|
29
|
+
启动命令均指向该入口。恢复轮询和报表写入失败映射为稳定事件/错误码,不输出异常堆栈或任意
|
|
30
|
+
`extra`。Uvicorn 自身 access/error 日志仍由 Uvicorn 配置管理。
|
|
31
|
+
`AgentRunTrace.start/finish` 同时调用 `AgentOperationalAttempt`,记录固定的
|
|
32
|
+
`agent.run.attempt.started/completed`、Run/Trace、耗时及安全结果;幂等重放不重新启动 attempt。
|
|
33
|
+
进程停止等非终态结束标为 interrupted,不能伪装成 canonical Run failed。详细输入输出仍由
|
|
34
|
+
Run/Artifact 受管查询负责。
|
|
35
|
+
|
|
36
|
+
业务审计继续走 `system.audit` 的 Manifest 与 Audit Port;Agent Run/Provider/Workflow 的持久
|
|
37
|
+
事实仍由各自 canonical store 维护。运行日志只用于排障,不能替代这两类记录。
|
|
38
|
+
|
|
39
|
+
## 利剑候选消费与四条链
|
|
40
|
+
|
|
41
|
+
利剑先 fetch 并 fast-forward `origin/main` 至 `3a51d6edf`,再创建
|
|
42
|
+
`rayjason/logging-integration`。变更仅在日志、Host 装配、候选依赖和验证文档;矛盾分析和聊天
|
|
43
|
+
记录分析的业务文件没有修改。利剑方案在 `docs/architecture/logging-integration-plan.md`,
|
|
44
|
+
最终实现和证据在 `docs/operations/windy-logging-integration.md`。
|
|
45
|
+
|
|
46
|
+
| 链路 | 当前本地联调证据 | 业务结果 |
|
|
47
|
+
| ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
48
|
+
| HTTP 写请求 | 真实 `createPlatformHttpApp` / Elysia 生命周期与候选 SDK/Pino | 一次完成记录、请求关联、正文和身份明细不入日志 |
|
|
49
|
+
| 慢请求与失败 | 慢 GET、错误诊断 Hook 和完成去重 | 保留阈值采样、诊断编号与登记路由模板 |
|
|
50
|
+
| Durable Job | 真实 Worker + 隔离内存 Repository | 开始、进度、终态按 Job/阶段/重试串联;Sink 故障不改状态机 |
|
|
51
|
+
| Agent | 利剑 Remote Runtime 到本地 HTTP 协议 fixture;独立安装的 Python 候选 | Host 请求到 Trace/Run 的关联、当前进程采样去重、canonical 结果保持 |
|
|
52
|
+
|
|
53
|
+
利剑 `ObservedAgentRuntime` 的终态观测最多保留 1000 个 Run,仅用于进程内采样,重启或淘汰
|
|
54
|
+
后可能再次观测同一 Run;它不承诺全局 exactly-once。Run store 和业务审计仍是权威事实。
|
|
55
|
+
本地 fixture 不调用真实模型 Provider,也没有写入生产案件数据。
|
|
56
|
+
|
|
57
|
+
## 验证与限制
|
|
58
|
+
|
|
59
|
+
- Bun 定向测试:共享契约、Server Pino 输出与 HTTP 观测共 8 项通过,包含 Sink 故障不影响业务。
|
|
60
|
+
- Agent Server Python 定向测试:4 项通过,包含日志输出失败不影响 Run;ASGI 导入确认复用原
|
|
61
|
+
app 对象。
|
|
62
|
+
- Server/Shared 类型检查及相关格式、Lint 检查通过;提交钩子的工作区类型检查作为静态证据。
|
|
63
|
+
- SDK 新字段及 Job 旁路定向检查各 4 项通过;Agent 日志、真实 ASGI Run、坏 Sink 与幂等重放
|
|
64
|
+
共 11 项 Python 定向检查通过,Ruff/Mypy 通过。
|
|
65
|
+
平台最终合并的 SDK/Job/Pino/HTTP 定向检查共 12 项通过。
|
|
66
|
+
- Python 候选源码 `8c045684` 用 `uv build apps/agent-server --wheel` 打包,安装到独立 venv,
|
|
67
|
+
依赖由 `uv export --locked --no-dev --no-emit-project` 固定。`scripts/agent-log-wheel-consumer.py`
|
|
68
|
+
用独立解释器的 `-I` 模式在仓库外执行,验证导入来自 site-packages,正常和坏 stdout 两种
|
|
69
|
+
场景均完成真实 ASGI Run、canonical 结果、幂等重放、关联/脱敏与大小预算。
|
|
70
|
+
该 wheel SHA256 为 `8fd5bbb3fb5f6d92cc6c4dd9d7237df8c3dd1d9afe425448c0b61f7997ad2a95`。
|
|
71
|
+
|
|
72
|
+
利剑消费 SDK 源码 `11df0eb4`,归档 SHA256 为
|
|
73
|
+
`aa48ef04927edd9245ce7dde2b6a6415d35ac4c911c3535fd10192f859010e3b`。实际 Elysia/Worker/Remote
|
|
74
|
+
协议链及既有授权定向检查 26 项通过;交付构建输入/增量角色映射检查 22 项通过。候选包的
|
|
75
|
+
本地依赖必须在 Docker 的 frozen install 前复制,Server/Web/Seeder 已补齐该输入及 SDK 导入。
|
|
76
|
+
容器依赖层安装和已有静态失败在利剑最终文档分别记录。
|
|
77
|
+
候选 SDK 用真实本地归档消费,没有改 `.windy` 元数据或手改版本号冒充正式升级。
|
|
78
|
+
|
|
79
|
+
## 正式发布与利剑升级
|
|
80
|
+
|
|
81
|
+
2026-10-01 核验 `create-windy@0.8.1`、`@southwind-ai/shared@0.3.1` 已在 npm 发布,
|
|
82
|
+
`create-windy-v0.8.1` tag 指向 `066d8018c0ec0c82fbf01d26013b3b303aaa9c22`。
|
|
83
|
+
[发布记录](https://github.com/RayJason/windy-platform/releases/tag/create-windy-v0.8.1) 已附
|
|
84
|
+
candidate/published evidence、distribution manifest 与 checksum;两次发布核验均成功。
|
|
85
|
+
利剑官方更新账本绑定同一 SHA 和 `0.8.1`,当前使用正常的
|
|
86
|
+
`@southwind-ai/shared/structured-log`,已删除候选别名及归档。详细更新及验收记录在利剑
|
|
87
|
+
`docs/operations/windy-logging-integration.md`。
|
|
88
|
+
|
|
89
|
+
未完成 E2—E4 集成发布、真实 Provider/完整 Agent 部署、生产采集留存、集中查询或告警验收。
|
|
90
|
+
日志 SDK 正式接入不能当作完整 Agent 能力已经交付的证据。
|
|
91
|
+
|
|
92
|
+
利剑合并验收另发现旧 Bootstrap 借用 Uvicorn Handler 后再追加 SDK 会形成双出口。
|
|
93
|
+
`configure_agent_logging()` 已回馈上游:保留唯一现存结构化 Handler,移除包级其它 Handler 引用,
|
|
94
|
+
不关闭与 Uvicorn 共享的 Handler;重复初始化保留原实例和 Stream。5 项适配定向检查通过。
|
|
95
|
+
该修复是 `0.8.1` 发布后的源码补丁,没有改写发布版本;利剑同时修复旧 Bootstrap 调用点,并用
|
|
96
|
+
真实 Uvicorn 配置先于 ASGI 导入的回归检查证明单条 JSON 输出及独立访问日志仍有效。
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# 日志治理方案
|
|
2
|
+
|
|
3
|
+
状态:运行日志 SDK 和平台适配随 Windy `0.8.1` 正式发布,利剑已通过官方更新接入并完成四条链
|
|
4
|
+
定向联调。部署留存与真实 Provider 另行验收;集中管理界面尚未实施。
|
|
5
|
+
|
|
6
|
+
## 业务需求先行
|
|
7
|
+
|
|
8
|
+
| 场景 | 使用者与问题 | 权威记录 | 成功标准 |
|
|
9
|
+
| -------- | --------------------------------------------- | ------------------------ | -------------------------------------- |
|
|
10
|
+
| 故障定位 | 运维定位写请求、慢请求、Job 或 Agent Run 失败 | 有界结构化运行日志与指标 | 用关联 ID 找到失败阶段,不暴露敏感输入 |
|
|
11
|
+
| 责任追溯 | 审计人员确认谁在什么权限下执行了业务动作 | `system.audit` 审计事实 | 权限控制、完整性和导出可验证 |
|
|
12
|
+
| 合规取证 | 管理人员按部署约定查询、留存和导出 | 审计事实及受管保留策略 | 保留期、访问授权和交付责任明确 |
|
|
13
|
+
|
|
14
|
+
## 模块边界
|
|
15
|
+
|
|
16
|
+
`system.audit` 已有 Manifest、Module Audit Port、可靠写入、权限控制及管理端查询/导出。
|
|
17
|
+
业务/安全审计继续由它维护;普通 stdout、HTTP 访问日志和 Agent Trace 不写成它的审计事实。
|
|
18
|
+
Agent Run/Provider/Workflow 仍保留各自 canonical 事实,借关联 ID 串联。
|
|
19
|
+
|
|
20
|
+
Windy 平台层提供结构化运行日志契约和 TypeScript SDK;Python Agent Server 使用对等适配器。
|
|
21
|
+
它们统一 `timestamp`、`level`、`component`、`event`、`requestId`、`runId`、`jobId`、
|
|
22
|
+
`traceId`、`outcome` 和稳定错误码,限定字段白名单、脱敏、采样、大小预算与 retention 分类。
|
|
23
|
+
日志内容不成为业务授权来源,也不替代 Trace Event。对客户数据、密钥、模型输入输出、Tool 参数、
|
|
24
|
+
stdout/stderr 采用默认不记录的规则;确需详查时走当前权限复核和有 TTL 的受管 payload/Artifact。
|
|
25
|
+
|
|
26
|
+
集中采集、索引、备份和保留由部署层管理。只有在运维确有统一查询、告警配置和多租户隔离的
|
|
27
|
+
需求与责任人时,新增独立可选 `system.observability` module,提供受权的控制面和查询代理;
|
|
28
|
+
不把日志引擎塞进 `system.audit`,也不复制一份日志事实数据库。
|
|
29
|
+
|
|
30
|
+
## 迁移与验证
|
|
31
|
+
|
|
32
|
+
1. 清点 Windy 的 Server Pino/HTTP、Job、Agent 日志及利剑已有写请求、慢请求和 Docker 轮转字段。
|
|
33
|
+
2. 固定跨语言 schema、上下文传播、等级/采样、脱敏和稳定错误码;SDK 只负责产出,不控制业务审计。
|
|
34
|
+
3. 对接 Windy 后,用利剑的四条真实链路做兼容映射:写请求、慢请求、Durable Job、Agent Run。
|
|
35
|
+
检查敏感字段泄漏、重复事件、丢关联 ID 和日志系统不可用时业务是否按既定审计策略处理。
|
|
36
|
+
4. 和 Agent 平台候选共同发布;利剑在官方更新事务中升级,分别验收排障、审计导出和部署保留。
|
|
37
|
+
|
|
38
|
+
集中管理平台是否实施以这些业务验收后的缺口决定。当前任务状态见
|
|
39
|
+
[上游任务清单](https://github.com/RayJason/windy-platform/blob/main/docs/product/tasks.md)。
|
|
@@ -29,14 +29,41 @@ Web、Server、License 和 Signing 接受 `x-request-id`。只保留 1 至 128
|
|
|
29
29
|
|
|
30
30
|
## 结构化日志
|
|
31
31
|
|
|
32
|
-
应用启动、运行模式警告和 HTTP 完成事件使用单行 JSON。
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
32
|
+
应用启动、运行模式警告和 HTTP 完成事件使用单行 JSON。Server 使用
|
|
33
|
+
`@southwind-ai/shared` 的结构化日志契约清洗字段,再交给 Pino 输出;Agent Server
|
|
34
|
+
生产 ASGI 入口使用同一契约的 Python 适配器处理运行时异常。单行上限为 2048 UTF-8
|
|
35
|
+
字节,自由文本 message 和原始异常不会输出。`retentionClass=operational` 表示排障日志,
|
|
36
|
+
不赋予它审计事实或六个月留存语义。
|
|
37
|
+
|
|
38
|
+
通用稳定字段是 `timestamp`、`level`、`component`、`event`、`retentionClass`;
|
|
39
|
+
可选 `requestId`、`runId`、`jobId`、`traceId`、`diagnosticId`、`outcome`、`errorCode` 和
|
|
40
|
+
`stageKey`。关联 ID 只接受 1 至 128 位安全字符,不合法就省略,不猜测或生成假的关联。
|
|
41
|
+
`event`、`errorCode` 和阶段只接受有界稳定 Key。未列字段包括 body、headers、Token、
|
|
42
|
+
Prompt、Tool 参数和原始 Error 均被丢弃;超出大小预算时先丢弃次要关联字段。
|
|
43
|
+
运行日志输出失败时由 SDK/适配器吞掉写出异常,不改变原请求、Run 或审计结果;
|
|
44
|
+
日志丢失需由部署层的采集健康和告警发现。
|
|
45
|
+
HTTP 完成事件另外允许:
|
|
46
|
+
|
|
47
|
+
- `method`、`statusCode`、`durationMs`;
|
|
48
|
+
- `route` 只允许框架登记的模板,由调用方提供;SDK 拒绝 query、fragment、编码或纯数字路径段,
|
|
49
|
+
不以词法校验代替模板来源保证;
|
|
36
50
|
- Server 可额外记录粗粒度 `actorType`,不记录用户或资源 ID。
|
|
37
51
|
|
|
38
52
|
请求完成日志禁止记录完整 URL、query、请求或响应 body、Cookie、Authorization、Secret、连接串和原始异常。需要增加字段时,必须先判断其基数和敏感性,并补充防泄漏测试。生产日志采集器应按 JSON 字段解析,按 `requestId` 关联,不依赖自由文本。
|
|
39
53
|
|
|
54
|
+
Agent Server 的生产入口为 `southwind_agent_server.asgi:app`,它不改动 FastAPI
|
|
55
|
+
应用对象,而是在现有包级 Python logging 上安装结构化输出。当前恢复轮询和报表写入失败
|
|
56
|
+
映射到稳定事件/错误码;报表失败可携带合法的 `runId` 与 `stageKey`。实际执行的
|
|
57
|
+
`AgentRunTrace` 同时发出 `agent.run.attempt.started/completed`、Run/Trace、耗时与安全结果,
|
|
58
|
+
幂等重放不重发,非终态执行中断标记 interrupted。异常堆栈、
|
|
59
|
+
Python `extra` 中的任意字段和报表正文都不进入 stdout。直接使用旧的
|
|
60
|
+
`southwind_agent_server.app:app` 入口不会安装该适配器。
|
|
61
|
+
|
|
62
|
+
后台 Durable Worker 每次实际 claim 发出 `job.attempt.started/completed`,字段只取 canonical
|
|
63
|
+
`jobId/stageKey/attemptNumber/handlerKey/queue`;失败只记录稳定码。`completedUnits/totalUnits/count`
|
|
64
|
+
支持进度或清理计数,均限制为 0 至 10 亿的整数。日志不记录 payload、租约 Token 或进度正文,
|
|
65
|
+
也不从业务 payload 猜测关联 ID;Worker idle 不产生日志。
|
|
66
|
+
|
|
40
67
|
请求观测在全局 `onRequest` 建立,在响应完成时原子取出并结束。未匹配路由、Guard
|
|
41
68
|
拒绝和 handler 异常与正常请求遵循同一生命周期;即使框架同时触发 error 与
|
|
42
69
|
after-response 路径,同一个请求也只能扣减一次活跃计数并写一条完成日志。业务路由
|
|
@@ -58,5 +85,10 @@ after-response 路径,同一个请求也只能扣减一次活跃计数并写
|
|
|
58
85
|
Server/License 请求完成日志字段、Web 到 Server 的标识透传,以及 Server 未匹配路由、
|
|
59
86
|
异常路由只完成一次观测且不会终止进程,均有 Bun 单元或边界测试;工作区 typecheck 和
|
|
60
87
|
Oxlint 已通过。Dockerfile 已声明 readiness `HEALTHCHECK`。
|
|
88
|
+
新增结构化日志切片的共享契约、Pino 输出和 HTTP 观测定向测试共 8 项通过;Agent Server
|
|
89
|
+
Python 适配定向测试 4 项通过,ASGI 导入复用原 app 对象。完整记录见
|
|
90
|
+
[结构化运行日志技术实现](../architecture/log-governance-implementation.md)。
|
|
91
|
+
利剑四条真实适配链已完成候选包本地定向联调;平台 Job 定向检查 4 项、Agent 日志/ASGI
|
|
92
|
+
检查 11 项及独立 wheel 消费通过。实际 Provider、正式更新事务和部署采集留存仍须独立验收。
|
|
61
93
|
|
|
62
94
|
尚未由本页证明:生产 Compose、新环境部署、备份恢复、升级回滚、原生 Linux amd64 runner、告警规则、长稳运行、多实例和大容量。这些项必须由后续生产恢复演练和目标环境记录单独闭环;Docker Desktop 或 QEMU 上的 amd64 运行不能写成原生 Linux amd64 已认证。
|
|
@@ -52,6 +52,24 @@ Provider 调用前会执行:
|
|
|
52
52
|
Tool 调用继续由 Windy Server 执行。Agent Server 只收到 opaque Execution Grant;每次 Tool
|
|
53
53
|
都重新校验当前权限、Scope、审批和 receipt,不把 Run 创建时的权限快照当长期授权。
|
|
54
54
|
|
|
55
|
+
### Tool 与结构化业务结果
|
|
56
|
+
|
|
57
|
+
需要先查询业务数据再返回结构化结果的 Operation,同时保留真实 function calling 与最终
|
|
58
|
+
JSON Schema 验证。为兼容同时发送 `tools` 和 `response_format` 时只生成正文的 Provider,
|
|
59
|
+
存在 Tool 时请求只发送 `tools`,把经过授权和 DLP 的输出 Schema 及最终 JSON 指导合并到
|
|
60
|
+
首条系统指令;没有 Tool 时继续发送严格 `response_format: json_schema`。这是默认序列化
|
|
61
|
+
规则,无新增配置。正文中的工具名称或伪造调用不执行,只有协议中的 `tool_calls` 执行。
|
|
62
|
+
|
|
63
|
+
合并不增加消息、移动 Context 来源序号或放宽 Guard。治理后的同一消息用于 Context 提交、
|
|
64
|
+
输入预算和实际出网请求;Schema 指导也计入 Provider Context 与保守 Token 预算、出网审计
|
|
65
|
+
字节数。`maxInputBytes` 继续约束原始输入和 Artifact,原有输出预算与有界 Tool 轮次继续生效。
|
|
66
|
+
最终回复仍必须通过严格 Schema 验证,
|
|
67
|
+
不接受 Markdown 围栏或额外字段;Provider 未生成合规结果时返回 `OUTPUT_VALIDATION_FAILED`。
|
|
68
|
+
|
|
69
|
+
Context 的首条指令摘要与 `requestDigest` 覆盖完整、脱敏后的 Schema 指导。兼容请求没有
|
|
70
|
+
`response_format`,因此 `outputSchemaDigest` 是实际空字段的摘要;它不能单独用来恢复
|
|
71
|
+
输出 Schema。首条指令的来源与版本规则保持不变,业务来源仍按原始序号追踪。
|
|
72
|
+
|
|
55
73
|
## 配置
|
|
56
74
|
|
|
57
75
|
选择 `system.agent` 的项目在本机 `.env` 配置:
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AiIsolatedExecutionPolicyV1 } from "./manifest.js";
|
|
3
|
+
import { validateModuleComposition } from "./composition.js";
|
|
4
|
+
import { fixture, operation } from "./ai-operation-composition.test-support.js";
|
|
5
|
+
|
|
6
|
+
const policy: AiIsolatedExecutionPolicyV1 = {
|
|
7
|
+
version: 1,
|
|
8
|
+
mode: "isolated",
|
|
9
|
+
languages: ["python", "node"],
|
|
10
|
+
inputArtifactTypes: ["text/plain", "application/json"],
|
|
11
|
+
network: "forbidden",
|
|
12
|
+
limits: {
|
|
13
|
+
maxWorkspaceBytes: 8_388_608,
|
|
14
|
+
maxFileBytes: 2_097_152,
|
|
15
|
+
maxOutputBytes: 1_048_576,
|
|
16
|
+
maxDurationSeconds: 30,
|
|
17
|
+
maxCpuMillis: 30_000,
|
|
18
|
+
maxMemoryBytes: 268_435_456,
|
|
19
|
+
maxPids: 8,
|
|
20
|
+
maxConcurrentProcesses: 1,
|
|
21
|
+
maxToolCalls: 8,
|
|
22
|
+
maxToolRounds: 4,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe("Agent 隔离执行 Manifest", () => {
|
|
27
|
+
test("缺省关闭;显式策略可通过组合校验", () => {
|
|
28
|
+
const module = fixture("business");
|
|
29
|
+
module.aiOperations = [operation("business.answer")];
|
|
30
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
31
|
+
module.aiOperations = [
|
|
32
|
+
{
|
|
33
|
+
...module.aiOperations[0]!,
|
|
34
|
+
toolNetworkPolicy: { mode: "forbidden" },
|
|
35
|
+
isolatedExecution: policy,
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
expect(() => validateModuleComposition([module])).not.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("未知语言、网络和越界预算在装配时拒绝", () => {
|
|
42
|
+
const module = fixture("business");
|
|
43
|
+
const base = operation("business.answer");
|
|
44
|
+
const check = (candidate: AiIsolatedExecutionPolicyV1) => {
|
|
45
|
+
module.aiOperations = [
|
|
46
|
+
{
|
|
47
|
+
...base,
|
|
48
|
+
toolNetworkPolicy: { mode: "forbidden" },
|
|
49
|
+
isolatedExecution: candidate,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
expect(() => validateModuleComposition([module])).toThrow();
|
|
53
|
+
};
|
|
54
|
+
check({
|
|
55
|
+
...policy,
|
|
56
|
+
languages: [
|
|
57
|
+
"ruby",
|
|
58
|
+
] as unknown as AiIsolatedExecutionPolicyV1["languages"],
|
|
59
|
+
});
|
|
60
|
+
check({
|
|
61
|
+
...policy,
|
|
62
|
+
network: "allowed" as AiIsolatedExecutionPolicyV1["network"],
|
|
63
|
+
});
|
|
64
|
+
check({
|
|
65
|
+
...policy,
|
|
66
|
+
limits: { ...policy.limits, maxMemoryBytes: 2_147_483_648 },
|
|
67
|
+
});
|
|
68
|
+
check({ ...policy, limits: { ...policy.limits, maxToolRounds: 9 } });
|
|
69
|
+
});
|
|
70
|
+
});
|