create-windy 0.8.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 (45) hide show
  1. package/dist/cli.js +46 -41
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/Dockerfile +1 -1
  5. package/template/apps/agent-server/README.md +1 -1
  6. package/template/apps/agent-server/src/southwind_agent_server/asgi.py +8 -0
  7. package/template/apps/agent-server/src/southwind_agent_server/runtime_logging.py +57 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +134 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +4 -0
  10. package/template/apps/agent-server/tests/test_run_operational_logging.py +114 -0
  11. package/template/apps/agent-server/tests/test_structured_logging.py +105 -0
  12. package/template/apps/server/src/agent/isolated-execution-guard.test.ts +194 -0
  13. package/template/apps/server/src/agent/isolated-execution-guard.ts +202 -0
  14. package/template/apps/server/src/agent/run-definition-support.ts +11 -1
  15. package/template/apps/server/src/agent/run-facade.ts +7 -11
  16. package/template/apps/server/src/agent/run-operation-policy.ts +11 -0
  17. package/template/apps/server/src/agent/workflow-durable-runtime-input.ts +8 -5
  18. package/template/apps/server/src/jobs/legacy-service-runtime.ts +4 -0
  19. package/template/apps/server/src/jobs/runtime-logging.test.ts +212 -0
  20. package/template/apps/server/src/jobs/runtime-logging.ts +58 -0
  21. package/template/apps/server/src/jobs/worker.ts +54 -12
  22. package/template/apps/server/src/observability/bootstrap.ts +1 -4
  23. package/template/apps/server/src/observability/http-observability.ts +2 -3
  24. package/template/apps/server/src/observability/structured-logger.test.ts +31 -0
  25. package/template/apps/server/src/observability/structured-logger.ts +27 -0
  26. package/template/apps/server/src/platform-bootstrap.ts +9 -3
  27. package/template/apps/server/src/startup-finalization.ts +3 -3
  28. package/template/apps/server/src/system/built-in-roles.test.ts +23 -0
  29. package/template/apps/server/src/system/built-in-roles.ts +10 -4
  30. package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +16 -5
  31. package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +4 -0
  32. package/template/docs/architecture/log-governance-implementation.md +79 -0
  33. package/template/docs/architecture/log-governance-plan.md +39 -0
  34. package/template/docs/operations/observability.md +36 -4
  35. package/template/packages/modules/package.json +1 -1
  36. package/template/packages/modules/src/ai-isolated-execution-policy.test.ts +70 -0
  37. package/template/packages/modules/src/ai-isolated-execution-policy.ts +76 -0
  38. package/template/packages/modules/src/ai-operation-composition.ts +2 -0
  39. package/template/packages/modules/src/manifest.ts +23 -0
  40. package/template/packages/modules/src/system-features.ts +11 -10
  41. package/template/packages/server-sdk/package.json +1 -1
  42. package/template/packages/shared/index.ts +1 -0
  43. package/template/packages/shared/package.json +3 -2
  44. package/template/packages/shared/src/structured-log.test.ts +147 -0
  45. package/template/packages/shared/src/structured-log.ts +215 -0
@@ -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,79 @@
1
+ # 结构化运行日志技术实现
2
+
3
+ 状态:SDK、平台适配与利剑四条链的本地定向联调已实现;正式发布/升级及集中查询/告警平台未验收。
4
+ 对应业务方案见
5
+ [日志治理方案](./log-governance-plan.md)。
6
+
7
+ ## 当前调用链
8
+
9
+ - `packages/shared/src/structured-log.ts` 提供跨服务字段契约与 TypeScript SDK。调用者提交稳定
10
+ `event`、关联 ID、结果、错误码和少量明确允许的数值字段;序列化器生成时间、等级、组件和
11
+ `retentionClass=operational`,只输出白名单字段,并限制单行 2048 UTF-8 字节。任意 message、
12
+ 原始 Error、请求内容、Token 与未知字段不会进入记录。
13
+ 公共入口 `@southwind-ai/shared/structured-log` 可独立消费日志 SDK;利剑用真实打包候选的
14
+ 同一入口验证,保留原 workspace shared,避免同名同版本 package identity 的类型去重冲突。
15
+ 字段同时支持 `diagnosticId`、登记的 `route` 模板、`handlerKey/queue/attemptNumber` 及
16
+ 有界 `completedUnits/totalUnits/count`。路由不能传实际 URL、query、fragment 或对象路径;
17
+ SDK 做词法校验,调用方负责从框架登记模板取得它。
18
+ - `apps/server/src/observability/structured-logger.ts` 将该契约接入 Pino。
19
+ `platform-bootstrap.ts` 创建同一 logger,HTTP 请求完成、启动、审计降级及 Workflow continuation
20
+ 的现有日志路径继续使用它。Workflow continuation 只把原始异常字段改为稳定错误码,不改派发和
21
+ 错误处理行为。
22
+ - `createLegacyServiceRuntime` 将统一 Logger 注入 `DurableJobWorker`;每次实际 claim 记录
23
+ `job.attempt.started` 和唯一的 `job.attempt.completed`,包括未注册 Handler、失败、取消和租约
24
+ 丢失。只读 canonical claim 的 Job/阶段/重试/Handler/Queue 坐标,不扫描 payload 或记录租约
25
+ Token。idle 不产生日志。
26
+ - `apps/agent-server/src/southwind_agent_server/structured_logging.py` 为 Python 包级日志安装对等
27
+ 适配器。`asgi.py` 保留原 FastAPI app 对象,在生产 ASGI 入口安装 handler;Dockerfile 与开发
28
+ 启动命令均指向该入口。恢复轮询和报表写入失败映射为稳定事件/错误码,不输出异常堆栈或任意
29
+ `extra`。Uvicorn 自身 access/error 日志仍由 Uvicorn 配置管理。
30
+ `AgentRunTrace.start/finish` 同时调用 `AgentOperationalAttempt`,记录固定的
31
+ `agent.run.attempt.started/completed`、Run/Trace、耗时及安全结果;幂等重放不重新启动 attempt。
32
+ 进程停止等非终态结束标为 interrupted,不能伪装成 canonical Run failed。详细输入输出仍由
33
+ Run/Artifact 受管查询负责。
34
+
35
+ 业务审计继续走 `system.audit` 的 Manifest 与 Audit Port;Agent Run/Provider/Workflow 的持久
36
+ 事实仍由各自 canonical store 维护。运行日志只用于排障,不能替代这两类记录。
37
+
38
+ ## 利剑候选消费与四条链
39
+
40
+ 利剑先 fetch 并 fast-forward `origin/main` 至 `3a51d6edf`,再创建
41
+ `rayjason/logging-integration`。变更仅在日志、Host 装配、候选依赖和验证文档;矛盾分析和聊天
42
+ 记录分析的业务文件没有修改。利剑方案在 `docs/architecture/logging-integration-plan.md`,
43
+ 最终实现和证据在 `docs/operations/windy-logging-integration.md`。
44
+
45
+ | 链路 | 当前本地联调证据 | 业务结果 |
46
+ | ------------ | -------------------------------------------------------------------- | ------------------------------------------------------------------ |
47
+ | HTTP 写请求 | 真实 `createPlatformHttpApp` / Elysia 生命周期与候选 SDK/Pino | 一次完成记录、请求关联、正文和身份明细不入日志 |
48
+ | 慢请求与失败 | 慢 GET、错误诊断 Hook 和完成去重 | 保留阈值采样、诊断编号与登记路由模板 |
49
+ | Durable Job | 真实 Worker + 隔离内存 Repository | 开始、进度、终态按 Job/阶段/重试串联;Sink 故障不改状态机 |
50
+ | Agent | 利剑 Remote Runtime 到本地 HTTP 协议 fixture;独立安装的 Python 候选 | Host 请求到 Trace/Run 的关联、当前进程采样去重、canonical 结果保持 |
51
+
52
+ 利剑 `ObservedAgentRuntime` 的终态观测最多保留 1000 个 Run,仅用于进程内采样,重启或淘汰
53
+ 后可能再次观测同一 Run;它不承诺全局 exactly-once。Run store 和业务审计仍是权威事实。
54
+ 本地 fixture 不调用真实模型 Provider,也没有写入生产案件数据。
55
+
56
+ ## 验证与限制
57
+
58
+ - Bun 定向测试:共享契约、Server Pino 输出与 HTTP 观测共 8 项通过,包含 Sink 故障不影响业务。
59
+ - Agent Server Python 定向测试:4 项通过,包含日志输出失败不影响 Run;ASGI 导入确认复用原
60
+ app 对象。
61
+ - Server/Shared 类型检查及相关格式、Lint 检查通过;提交钩子的工作区类型检查作为静态证据。
62
+ - SDK 新字段及 Job 旁路定向检查各 4 项通过;Agent 日志、真实 ASGI Run、坏 Sink 与幂等重放
63
+ 共 11 项 Python 定向检查通过,Ruff/Mypy 通过。
64
+ 平台最终合并的 SDK/Job/Pino/HTTP 定向检查共 12 项通过。
65
+ - Python 候选源码 `8c045684` 用 `uv build apps/agent-server --wheel` 打包,安装到独立 venv,
66
+ 依赖由 `uv export --locked --no-dev --no-emit-project` 固定。`scripts/agent-log-wheel-consumer.py`
67
+ 用独立解释器的 `-I` 模式在仓库外执行,验证导入来自 site-packages,正常和坏 stdout 两种
68
+ 场景均完成真实 ASGI Run、canonical 结果、幂等重放、关联/脱敏与大小预算。
69
+ 该 wheel SHA256 为 `8fd5bbb3fb5f6d92cc6c4dd9d7237df8c3dd1d9afe425448c0b61f7997ad2a95`。
70
+
71
+ 利剑消费 SDK 源码 `11df0eb4`,归档 SHA256 为
72
+ `aa48ef04927edd9245ce7dde2b6a6415d35ac4c911c3535fd10192f859010e3b`。实际 Elysia/Worker/Remote
73
+ 协议链及既有授权定向检查 26 项通过;交付构建输入/增量角色映射检查 22 项通过。候选包的
74
+ 本地依赖必须在 Docker 的 frozen install 前复制,Server/Web/Seeder 已补齐该输入及 SDK 导入。
75
+ 容器依赖层安装和已有静态失败在利剑最终文档分别记录。
76
+ 候选 SDK 用真实本地归档消费,没有改 `.windy` 元数据或手改版本号冒充正式升级。
77
+
78
+ 本轮完成日志契约与四条真实适配链的本地定向联调;未完成真实 Provider/完整 Agent 部署、正式
79
+ Windy 发布和 `windy update` 事务、生产采集留存、集中查询或告警验收。这些不能由定向检查替代。
@@ -0,0 +1,39 @@
1
+ # 日志治理方案
2
+
3
+ 状态:运行日志 SDK、平台适配及利剑四条链的本地定向联调已实现;正式发布、下游升级与部署留存
4
+ 另行验收。集中管理界面尚未实施。
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。HTTP 完成事件的稳定字段为:
33
-
34
- - `timestamp`、`level`、`event`、`component`;
35
- - `requestId`、`method`、`statusCode`、`durationMs`;
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 已认证。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/modules",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
+ });
@@ -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 { featureKey, licenseVersionKey } from "@southwind-ai/shared";
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(licenseVersionKey);
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: advancedOnly,
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: advancedOnly,
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
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@southwind-ai/server-sdk",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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.0",
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
+ });