create-windy 0.6.0 → 0.6.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.
Files changed (61) hide show
  1. package/dist/cli.js +142 -37
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/_bunfig.toml +5 -0
  5. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +27 -20
  6. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +111 -4
  7. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +13 -0
  8. package/template/apps/embedding-server/src/southwind_embedding_server/pin_cli.py +62 -0
  9. package/template/apps/embedding-server/src/southwind_embedding_server/readiness.py +259 -0
  10. package/template/apps/embedding-server/src/southwind_embedding_server/readiness_contracts.py +221 -0
  11. package/template/apps/embedding-server/src/southwind_embedding_server/readiness_http.py +91 -0
  12. package/template/apps/embedding-server/src/southwind_embedding_server/run_service.py +23 -3
  13. package/template/apps/embedding-server/tests/test_config.py +13 -0
  14. package/template/apps/embedding-server/tests/test_readiness.py +202 -0
  15. package/template/apps/embedding-server/tests/test_run_http.py +76 -4
  16. package/template/apps/embedding-server/tests/test_run_service.py +3 -1
  17. package/template/apps/server/Dockerfile +10 -5
  18. package/template/apps/server/src/agent/drizzle-screenshot-intake-repository.integration.test.ts +3 -0
  19. package/template/apps/server/src/agent/run-facade.ts +2 -1
  20. package/template/apps/server/src/agent/run-route-screenshot-context.test.ts +13 -0
  21. package/template/apps/server/src/agent/runtime-bootstrap.ts +2 -0
  22. package/template/apps/server/src/agent/screenshot-context-admission.test.ts +249 -0
  23. package/template/apps/server/src/agent/screenshot-context-attestation.ts +60 -0
  24. package/template/apps/server/src/agent/screenshot-intake-bootstrap.ts +2 -4
  25. package/template/apps/server/src/agent/screenshot-intake-test-support.ts +4 -0
  26. package/template/apps/server/src/agent/screenshot-intake-v2-validation.ts +24 -0
  27. package/template/apps/server/src/agent/screenshot-intake-v2.test.ts +7 -0
  28. package/template/apps/server/src/agent/screenshot-intake-v2.ts +8 -10
  29. package/template/apps/server/src/agent/screenshot-run-binding.ts +17 -2
  30. package/template/apps/server/src/agent/vision-readiness-pin.ts +6 -48
  31. package/template/apps/server/src/ai-runtime-readiness/deployment-pin.ts +69 -0
  32. package/template/apps/server/src/embedding/contracts.ts +6 -1
  33. package/template/apps/server/src/embedding/facade-readiness.test.ts +307 -0
  34. package/template/apps/server/src/embedding/facade.ts +8 -6
  35. package/template/apps/server/src/embedding/readiness-admission.ts +26 -0
  36. package/template/apps/server/src/embedding/remote-runtime.test.ts +85 -0
  37. package/template/apps/server/src/embedding/remote-runtime.ts +21 -0
  38. package/template/apps/server/src/embedding/run-fingerprint.test.ts +26 -0
  39. package/template/apps/server/src/embedding/run-fingerprint.ts +11 -2
  40. package/template/apps/server/src/module-host/ai-operation-port.test.ts +72 -0
  41. package/template/apps/server/src/module-host/ai-operation-port.ts +43 -0
  42. package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +186 -0
  43. package/template/apps/web/Dockerfile +1 -1
  44. package/template/docker-compose.yml +11 -0
  45. package/template/docs/architecture/ai-runtime-readiness.md +6 -0
  46. package/template/docs/platform/vision-ai-runtime.md +22 -1
  47. package/template/package.json +2 -0
  48. package/template/packages/config/package.json +1 -1
  49. package/template/packages/database/package.json +1 -1
  50. package/template/packages/jobs/package.json +1 -1
  51. package/template/packages/modules/index.ts +1 -0
  52. package/template/packages/modules/package.json +1 -1
  53. package/template/packages/modules/src/ai-operation-composition.ts +2 -0
  54. package/template/packages/modules/src/ai-screenshot-context.test.ts +72 -0
  55. package/template/packages/modules/src/ai-screenshot-context.ts +50 -0
  56. package/template/packages/modules/src/manifest.ts +8 -0
  57. package/template/packages/server-sdk/package.json +1 -1
  58. package/template/packages/shared/index.ts +3 -0
  59. package/template/packages/shared/package.json +1 -1
  60. package/template/packages/shared/src/screenshot-context-digest.test.ts +46 -0
  61. package/template/packages/shared/src/screenshot-context-digest.ts +72 -0
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import { computeAiRuntimeOffersDigestV1 } from "@southwind-ai/server-sdk";
2
3
  import {
3
4
  RemoteEmbeddingRuntime,
4
5
  unavailableEmbeddingRuntime,
@@ -81,4 +82,88 @@ describe("RemoteEmbeddingRuntime", () => {
81
82
  runtime.start(operation, "run-key", "embedding_server_bound"),
82
83
  ).resolves.toEqual({ runId: "embedding_server_bound" });
83
84
  });
85
+
86
+ test("deployment pin 漂移时在创建远端 Run 前失败关闭", async () => {
87
+ const pin = embeddingPin();
88
+ let starts = 0;
89
+ const runtime = new RemoteEmbeddingRuntime({
90
+ baseUrl: "http://embedding.internal",
91
+ internalToken: "token",
92
+ fetch: async (input) => {
93
+ const url = String(input);
94
+ if (url.endsWith("/health/ready")) {
95
+ return Response.json(
96
+ readinessSnapshot(pin, `sha256:${"f".repeat(64)}`),
97
+ );
98
+ }
99
+ starts += 1;
100
+ return Response.json({ runId: "embedding_server_bound" });
101
+ },
102
+ });
103
+
104
+ await expect(
105
+ runtime.start(
106
+ { ...operation, readinessPin: pin },
107
+ "run-key",
108
+ "embedding_server_bound",
109
+ ),
110
+ ).rejects.toMatchObject({ code: "RUNTIME_UNAVAILABLE" });
111
+ expect(starts).toBe(0);
112
+ });
84
113
  });
114
+
115
+ function embeddingPin() {
116
+ const offersDigest = computeAiRuntimeOffersDigestV1([embeddingOffer()]);
117
+ return {
118
+ key: "embedding.primary",
119
+ identity: "windy-deployment-v1_0f26eb34-f384-4ec9-98f3-caf8d582291f",
120
+ releaseDigest: `sha256:${"a".repeat(64)}` as const,
121
+ offersDigest,
122
+ };
123
+ }
124
+
125
+ function readinessSnapshot(
126
+ pin: ReturnType<typeof embeddingPin>,
127
+ offersDigest = pin.offersDigest,
128
+ ) {
129
+ const now = new Date();
130
+ const checkedAt = now.toISOString();
131
+ const validUntil = new Date(now.getTime() + 60_000).toISOString();
132
+ const deployment = { ...pin, offersDigest };
133
+ return {
134
+ schemaVersion: 1,
135
+ status: "ready",
136
+ configured: true,
137
+ runtimeKind: "embedding",
138
+ observedAt: checkedAt,
139
+ downstreamProbe: {
140
+ status: "passed",
141
+ checkedAt,
142
+ validUntil,
143
+ latencyMs: 1,
144
+ deploymentIdentity: pin.identity,
145
+ releaseDigest: pin.releaseDigest,
146
+ offersDigest,
147
+ },
148
+ deployment,
149
+ offers: [embeddingOffer()],
150
+ unavailableReasons: [],
151
+ };
152
+ }
153
+
154
+ function embeddingOffer() {
155
+ return {
156
+ variant: "text",
157
+ capabilities: ["embedding.text"],
158
+ inputModalities: ["text"] as const,
159
+ outputModalities: ["embedding"] as const,
160
+ limits: {
161
+ maxDurationSeconds: 30,
162
+ maxInputBytes: 1024,
163
+ maxOutputBytes: 1024,
164
+ maxUnits: 10,
165
+ maxConcurrentRuns: 1,
166
+ maxQueueDepth: 0,
167
+ },
168
+ };
169
+ }
@@ -4,6 +4,7 @@ import {
4
4
  type EmbeddingRuntimeOperation,
5
5
  type EmbeddingRuntimePort,
6
6
  } from "./contracts.js";
7
+ import { assertAiRuntimeReadinessPin } from "../ai-runtime-readiness/deployment-pin.js";
7
8
 
8
9
  export class RemoteEmbeddingRuntime implements EmbeddingRuntimePort {
9
10
  readonly #baseUrl: string;
@@ -32,6 +33,7 @@ export class RemoteEmbeddingRuntime implements EmbeddingRuntimePort {
32
33
  idempotencyKey: string,
33
34
  signal?: AbortSignal,
34
35
  ): Promise<EmbeddingBatchResponse> {
36
+ await this.assertReadiness(request, signal);
35
37
  const response = await this.request("/api/embeddings", {
36
38
  method: "POST",
37
39
  headers: {
@@ -55,6 +57,7 @@ export class RemoteEmbeddingRuntime implements EmbeddingRuntimePort {
55
57
  runId: string,
56
58
  signal?: AbortSignal,
57
59
  ) {
60
+ await this.assertReadiness(request, signal);
58
61
  const response = await this.request("/api/embeddings/runs", {
59
62
  method: "POST",
60
63
  headers: {
@@ -125,6 +128,24 @@ export class RemoteEmbeddingRuntime implements EmbeddingRuntimePort {
125
128
  return response;
126
129
  });
127
130
  }
131
+
132
+ private async assertReadiness(
133
+ request: EmbeddingRuntimeOperation,
134
+ signal?: AbortSignal,
135
+ ) {
136
+ if (!request.readinessPin) return;
137
+ try {
138
+ await assertAiRuntimeReadinessPin(request.readinessPin, () =>
139
+ this.health(signal),
140
+ );
141
+ } catch {
142
+ throw new EmbeddingRuntimeError(
143
+ "RUNTIME_UNAVAILABLE",
144
+ "Embedding Runtime readiness 已漂移",
145
+ true,
146
+ );
147
+ }
148
+ }
128
149
  }
129
150
 
130
151
  export const unavailableEmbeddingRuntime: EmbeddingRuntimePort = {
@@ -2,6 +2,12 @@ import { expect, test } from "bun:test";
2
2
  import { embeddingRunFingerprint } from "./run-fingerprint.js";
3
3
 
4
4
  test("Run fingerprint 排除旋转后的 Artifact token,保留稳定输入与 options", async () => {
5
+ const pin = {
6
+ key: "embedding.primary",
7
+ identity: "windy-deployment-v1_0f26eb34-f384-4ec9-98f3-caf8d582291f",
8
+ releaseDigest: `sha256:${"a".repeat(64)}` as const,
9
+ offersDigest: `sha256:${"b".repeat(64)}` as const,
10
+ };
5
11
  const first = await embeddingRunFingerprint(
6
12
  "analysis.embedding.image",
7
13
  [
@@ -16,6 +22,7 @@ test("Run fingerprint 排除旋转后的 Artifact token,保留稳定输入与
16
22
  },
17
23
  ],
18
24
  { nested: { limit: 2 } },
25
+ pin,
19
26
  );
20
27
  const retry = await embeddingRunFingerprint(
21
28
  "analysis.embedding.image",
@@ -31,6 +38,7 @@ test("Run fingerprint 排除旋转后的 Artifact token,保留稳定输入与
31
38
  },
32
39
  ],
33
40
  { nested: { limit: 2 } },
41
+ pin,
34
42
  );
35
43
  const changed = await embeddingRunFingerprint(
36
44
  "analysis.embedding.image",
@@ -46,7 +54,25 @@ test("Run fingerprint 排除旋转后的 Artifact token,保留稳定输入与
46
54
  },
47
55
  ],
48
56
  { nested: { limit: 2 } },
57
+ { ...pin, offersDigest: `sha256:${"c".repeat(64)}` },
58
+ );
59
+ const deploymentChanged = await embeddingRunFingerprint(
60
+ "analysis.embedding.image",
61
+ [
62
+ {
63
+ id: "image-1",
64
+ source: {
65
+ value: "rotated-token-four",
66
+ mediaType: "image/png",
67
+ byteSize: 12,
68
+ sha256: "a".repeat(64),
69
+ },
70
+ },
71
+ ],
72
+ { nested: { limit: 2 } },
73
+ { ...pin, offersDigest: `sha256:${"c".repeat(64)}` },
49
74
  );
50
75
  expect(retry).toBe(first);
51
76
  expect(changed).not.toBe(first);
77
+ expect(deploymentChanged).not.toBe(first);
52
78
  });
@@ -1,15 +1,24 @@
1
- import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
1
+ import type {
2
+ AiRuntimeDeploymentV1,
3
+ ModuleAiJsonValue,
4
+ } from "@southwind-ai/server-sdk";
2
5
  import type { EmbeddingRuntimeItem } from "./contracts.js";
3
6
 
4
7
  export async function embeddingRunFingerprint(
5
8
  operationKey: string,
6
9
  items: readonly EmbeddingRuntimeItem[],
7
10
  options: ModuleAiJsonValue,
11
+ readinessPin?: AiRuntimeDeploymentV1,
8
12
  ) {
9
13
  const digest = await crypto.subtle.digest(
10
14
  "SHA-256",
11
15
  new TextEncoder().encode(
12
- stableJson({ operationKey, items: items.map(stableItem), options }),
16
+ stableJson({
17
+ operationKey,
18
+ items: items.map(stableItem),
19
+ options,
20
+ ...(readinessPin ? { readinessPin } : {}),
21
+ }),
13
22
  ),
14
23
  );
15
24
  return [...new Uint8Array(digest)]
@@ -2,6 +2,65 @@ import { describe, expect, test } from "bun:test";
2
2
  import { createModuleAiOperationPort } from "./ai-operation-port.js";
3
3
 
4
4
  describe("Module AI Operation canonical result", () => {
5
+ test("Speech contract 只从 Manifest 重建 normalize 变体", async () => {
6
+ let received: unknown;
7
+ const port = createModuleAiOperationPort(
8
+ "analysis",
9
+ {} as never,
10
+ AbortSignal.timeout(1_000),
11
+ {
12
+ modules: [speechModule("normalize")],
13
+ speechFacade: {
14
+ async start(...args: unknown[]) {
15
+ received = args.at(-1);
16
+ return {
17
+ runId: "speech-1",
18
+ status: "accepted",
19
+ eventsUrl: "/events",
20
+ };
21
+ },
22
+ },
23
+ } as never,
24
+ );
25
+
26
+ await port.startSpeech({
27
+ operationKey: "analysis.audio.normalize",
28
+ source: {} as never,
29
+ options: { contractVersion: 2, variant: "translate" },
30
+ idempotencyKey: "speech-1",
31
+ });
32
+
33
+ expect(received).toEqual({ contractVersion: 2, variant: "normalize" });
34
+ });
35
+
36
+ test("Manifest Speech 变体未知时不触达 facade", async () => {
37
+ let called = false;
38
+ const port = createModuleAiOperationPort(
39
+ "analysis",
40
+ {} as never,
41
+ AbortSignal.timeout(1_000),
42
+ {
43
+ modules: [speechModule("forged")],
44
+ speechFacade: {
45
+ async start() {
46
+ called = true;
47
+ throw new Error("unreachable");
48
+ },
49
+ },
50
+ } as never,
51
+ );
52
+
53
+ await expect(
54
+ port.startSpeech({
55
+ operationKey: "analysis.audio.normalize",
56
+ source: {} as never,
57
+ options: {},
58
+ idempotencyKey: "speech-1",
59
+ }),
60
+ ).rejects.toMatchObject({ code: "AI_OPERATION_UNAVAILABLE" });
61
+ expect(called).toBe(false);
62
+ });
63
+
5
64
  test("只把尚未完成的 RUN_CONFLICT 映射为 pending", async () => {
6
65
  const port = agentPort(() =>
7
66
  Response.json(
@@ -50,6 +109,19 @@ describe("Module AI Operation canonical result", () => {
50
109
  });
51
110
  });
52
111
 
112
+ function speechModule(variant: string) {
113
+ return {
114
+ name: "analysis",
115
+ aiOperations: [
116
+ {
117
+ key: "analysis.audio.normalize",
118
+ kind: "speech",
119
+ readiness: { contractVersion: 1, variant },
120
+ },
121
+ ],
122
+ } as never;
123
+ }
124
+
53
125
  function agentPort(result: () => Response) {
54
126
  return createModuleAiOperationPort(
55
127
  "analysis",
@@ -1,4 +1,8 @@
1
1
  // @windy-module system.agent begin
2
+ import type {
3
+ ModuleAiOperationDefinition,
4
+ ModuleManifest,
5
+ } from "@southwind-ai/modules";
2
6
  import type {
3
7
  ModuleAiJsonValue,
4
8
  ModuleAiOperationPort,
@@ -21,6 +25,7 @@ export interface ModuleAiRuntimeServices {
21
25
  readonly speechFacade: SpeechRunFacade;
22
26
  readonly speechRemote: SpeechRuntimePort;
23
27
  readonly embeddingFacade: EmbeddingFacade;
28
+ readonly modules: readonly ModuleManifest[];
24
29
  }
25
30
 
26
31
  export function createModuleAiOperationPort(
@@ -71,6 +76,11 @@ export function createModuleAiOperationPort(
71
76
  },
72
77
  async startSpeech(input) {
73
78
  own(input.operationKey);
79
+ const definition = ownSpeechDefinition(
80
+ services.modules,
81
+ moduleName,
82
+ input.operationKey,
83
+ );
74
84
  return services.speechFacade.start(
75
85
  input.operationKey,
76
86
  input.source,
@@ -78,6 +88,7 @@ export function createModuleAiOperationPort(
78
88
  input.idempotencyKey,
79
89
  context,
80
90
  signal,
91
+ speechContract(definition),
81
92
  );
82
93
  },
83
94
  async embed(input) {
@@ -150,6 +161,38 @@ export function createModuleAiOperationPort(
150
161
  };
151
162
  }
152
163
 
164
+ function ownSpeechDefinition(
165
+ modules: readonly ModuleManifest[],
166
+ moduleName: string,
167
+ operationKey: string,
168
+ ): ModuleAiOperationDefinition {
169
+ const definition = modules
170
+ .find((module) => module.name === moduleName)
171
+ ?.aiOperations?.find((operation) => operation.key === operationKey);
172
+ if (!definition || definition.kind !== "speech") {
173
+ throw new ModuleAiOperationError(
174
+ "AI_OPERATION_UNAVAILABLE",
175
+ "Speech Operation 当前不可用",
176
+ );
177
+ }
178
+ return definition;
179
+ }
180
+
181
+ function speechContract(definition: ModuleAiOperationDefinition) {
182
+ const variant = definition.readiness?.variant;
183
+ if (!variant) return {};
184
+ if (!["normalize", "transcribe", "translate"].includes(variant)) {
185
+ throw new ModuleAiOperationError(
186
+ "AI_OPERATION_UNAVAILABLE",
187
+ "Speech Operation contract 当前不可用",
188
+ );
189
+ }
190
+ return {
191
+ contractVersion: 2 as const,
192
+ variant: variant as "normalize" | "transcribe" | "translate",
193
+ };
194
+ }
195
+
153
196
  async function responseJson(response: Response): Promise<ModuleAiJsonValue> {
154
197
  if (!response.ok) throw await responseError(response);
155
198
  return jsonValue(await response.json());
@@ -0,0 +1,186 @@
1
+ // @windy-module system.agent begin
2
+ import { expect, test } from "bun:test";
3
+ import type { ModuleManifest } from "@southwind-ai/modules";
4
+ import type { ModuleAiOperationReadinessV1 } from "@southwind-ai/server-sdk";
5
+ import { ExecutionGrantStore } from "../agent/execution-grants.js";
6
+ import { createRequestGuardContext } from "../guards.js";
7
+ import { initializeModuleHosts } from "../module-host.js";
8
+ import { createServerRuntime } from "../runtime.js";
9
+ import type { SpeechRuntimeOperation } from "../speech/contracts.js";
10
+ import { SpeechRunFacade } from "../speech/facade.js";
11
+ import { speechOutputManifest } from "../speech/output-artifact-host.test-support.js";
12
+ import type { SpeechOutputArtifactHost } from "../speech/output-artifact-host.js";
13
+ import { createModuleAiOperationPort } from "./ai-operation-port.js";
14
+
15
+ const operationKey = "evidence.audio.normalize";
16
+ const deployment = {
17
+ key: "speech.primary",
18
+ identity: "windy-deployment-v1_7db61d8e-e50d-4f8a-9f3f-9b7a1d932e01",
19
+ releaseDigest: `sha256:${"b".repeat(64)}` as const,
20
+ offersDigest: `sha256:${"c".repeat(64)}` as const,
21
+ };
22
+
23
+ test("Module Speech normalize 启用 Output Artifact 且调用方不能切换变体", async () => {
24
+ const module = normalizeModule();
25
+ const hosts = await initializeModuleHosts({
26
+ modules: [module],
27
+ installations: [
28
+ {
29
+ manifest: module,
30
+ setup(host) {
31
+ host.registerAiOperationHandlers([
32
+ { operationKey, prepareSpeech: (options) => ({ options }) },
33
+ ]);
34
+ },
35
+ },
36
+ ],
37
+ });
38
+ let invocation: SpeechRuntimeOperation | undefined;
39
+ let begun = 0;
40
+ const remote = {
41
+ async start(value: SpeechRuntimeOperation) {
42
+ invocation = value;
43
+ return { runId: value.runId!, status: "accepted", eventsUrl: "/events" };
44
+ },
45
+ } as never;
46
+ const output = {
47
+ hasBinding: () => true,
48
+ async begin(input: { invocation: SpeechRuntimeOperation }) {
49
+ begun += 1;
50
+ return {
51
+ runId: input.invocation.runId!,
52
+ scopeId: `outscope_${"a".repeat(32)}`,
53
+ grantGeneration: 1,
54
+ grant: "output-grant",
55
+ descriptor: {
56
+ contractVersion: 1,
57
+ scope: `outscope_${"a".repeat(32)}`,
58
+ outputKind: "normalized-audio",
59
+ deploymentIdentity: deployment.identity,
60
+ releaseDigest: deployment.releaseDigest,
61
+ offersDigest: deployment.offersDigest,
62
+ policyDigest: "d".repeat(64),
63
+ grantGeneration: 1,
64
+ allowedMediaTypes: ["audio/wav"],
65
+ expiresAt: "2030-01-01T00:00:00.000Z",
66
+ limits: {
67
+ maxArtifactBytes: 4_096,
68
+ maxArtifacts: 1,
69
+ maxTotalBytes: 4_096,
70
+ },
71
+ },
72
+ };
73
+ },
74
+ async confirmGrant() {
75
+ return true;
76
+ },
77
+ } as unknown as SpeechOutputArtifactHost;
78
+ const facade = new SpeechRunFacade(
79
+ [module],
80
+ hosts,
81
+ createServerRuntime({}, { featureCatalog: module.features }),
82
+ remote,
83
+ {
84
+ async rotate() {
85
+ return source("rotated");
86
+ },
87
+ } as never,
88
+ new ExecutionGrantStore(),
89
+ { inspect: async () => readyProjection() },
90
+ output,
91
+ );
92
+ const context = createRequestGuardContext(new Request("http://localhost"), {
93
+ resolveActor: () => ({
94
+ id: "actor-1",
95
+ type: "user",
96
+ name: "分析员",
97
+ roleCodes: [],
98
+ permissionKeys: [`${operationKey}.execute`],
99
+ }),
100
+ resolveLicense: () => ({ status: "valid" }),
101
+ });
102
+ const port = createModuleAiOperationPort(
103
+ "evidence",
104
+ context,
105
+ AbortSignal.timeout(1_000),
106
+ { modules: [module], speechFacade: facade } as never,
107
+ );
108
+
109
+ await port.startSpeech({
110
+ operationKey,
111
+ source: source("submitted"),
112
+ options: { contractVersion: 2, variant: "translate" },
113
+ idempotencyKey: "normalize-1",
114
+ });
115
+
116
+ expect(begun).toBe(1);
117
+ expect(invocation).toMatchObject({
118
+ contractVersion: 2,
119
+ variant: "normalize",
120
+ outputArtifactSink: { outputKind: "normalized-audio" },
121
+ });
122
+ });
123
+
124
+ function normalizeModule(): ModuleManifest {
125
+ const base = speechOutputManifest(operationKey);
126
+ const operation = base.aiOperations![0]!;
127
+ return {
128
+ ...base,
129
+ aiOperations: [
130
+ {
131
+ ...operation,
132
+ readiness: {
133
+ contractVersion: 1,
134
+ variant: "normalize",
135
+ inputModalities: ["audio"],
136
+ outputModalities: ["audio"],
137
+ expectedDeployment: deployment,
138
+ minConcurrentRuns: 1,
139
+ minQueueDepth: 0,
140
+ },
141
+ },
142
+ ],
143
+ };
144
+ }
145
+
146
+ function source(value: string) {
147
+ return {
148
+ value,
149
+ mediaType: "audio/wav",
150
+ byteSize: 128,
151
+ sha256: "e".repeat(64),
152
+ expiresAt: "2030-01-01T00:00:00.000Z",
153
+ };
154
+ }
155
+
156
+ function readyProjection(): ModuleAiOperationReadinessV1 {
157
+ return {
158
+ schemaVersion: 1,
159
+ operationKey,
160
+ runtimeKind: "speech",
161
+ observedAt: new Date().toISOString(),
162
+ status: "ready",
163
+ validUntil: new Date(Date.now() + 60_000).toISOString(),
164
+ deployment,
165
+ offers: [
166
+ {
167
+ variant: "normalize",
168
+ capabilities: ["speech.normalize", "speech.output-artifact.v1"],
169
+ inputModalities: ["audio"],
170
+ outputModalities: ["audio"],
171
+ limits: {
172
+ maxDurationSeconds: 60,
173
+ maxInputBytes: 4_096,
174
+ maxOutputBytes: 4_096,
175
+ maxUnits: 1,
176
+ maxMediaDurationSeconds: 60,
177
+ maxArtifacts: 1,
178
+ maxConcurrentRuns: 1,
179
+ maxQueueDepth: 0,
180
+ },
181
+ },
182
+ ],
183
+ unavailableReasons: [],
184
+ };
185
+ }
186
+ // @windy-module system.agent end
@@ -23,7 +23,7 @@ COPY packages/server-sdk/package.json packages/server-sdk/package.json
23
23
  COPY packages/shared/package.json packages/shared/package.json
24
24
  COPY packages/storage/package.json packages/storage/package.json
25
25
  # windy-copy:deps end
26
- RUN --mount=type=cache,id=windy-bun-1.3.14,target=/root/.bun/install/cache,sharing=shared bun -e 'const child=Bun.spawn(["bun","install","--frozen-lockfile","--no-progress"],{stdin:"inherit",stdout:"inherit",stderr:"inherit"});const heartbeat=setInterval(()=>console.log("[windy] Bun 依赖安装仍在进行"),15000);const exitCode=await child.exited;clearInterval(heartbeat);process.exit(exitCode)'
26
+ RUN --mount=type=cache,id=windy-bun-1.3.14,target=/root/.bun/install/cache,sharing=shared bun -e 'const child=Bun.spawn(["bun","install","--frozen-lockfile","--no-progress","--backend=copyfile","--linker=hoisted"],{stdin:"inherit",stdout:"inherit",stderr:"inherit"});const heartbeat=setInterval(()=>console.log("[windy] Bun 依赖安装仍在进行"),15000);const exitCode=await child.exited;clearInterval(heartbeat);process.exit(exitCode)'
27
27
 
28
28
  FROM deps AS build
29
29
  # windy-copy:build begin
@@ -249,6 +249,9 @@ services:
249
249
  EMBEDDING_ADAPTER_URL: ${EMBEDDING_ADAPTER_URL:-}
250
250
  EMBEDDING_ADAPTER_TOKEN: ${EMBEDDING_ADAPTER_TOKEN:-}
251
251
  EMBEDDING_DEPLOYMENT_KEY: ${EMBEDDING_DEPLOYMENT_KEY:-}
252
+ EMBEDDING_DEPLOYMENT_IDENTITY: ${EMBEDDING_DEPLOYMENT_IDENTITY:-}
253
+ EMBEDDING_RELEASE_DIGEST: ${EMBEDDING_RELEASE_DIGEST:-}
254
+ EMBEDDING_OFFERS_DIGEST: ${EMBEDDING_OFFERS_DIGEST:-}
252
255
  EMBEDDING_DEPLOYMENT_ALIAS: ${EMBEDDING_DEPLOYMENT_ALIAS:-}
253
256
  EMBEDDING_MODEL: ${EMBEDDING_MODEL:-}
254
257
  EMBEDDING_MODEL_VERSION: ${EMBEDDING_MODEL_VERSION:-}
@@ -256,8 +259,16 @@ services:
256
259
  EMBEDDING_NORMALIZED: ${EMBEDDING_NORMALIZED:-true}
257
260
  EMBEDDING_CAPABILITIES: ${EMBEDDING_CAPABILITIES:-embedding.text}
258
261
  EMBEDDING_MAX_UNITS: ${EMBEDDING_MAX_UNITS:-256}
262
+ EMBEDDING_MAX_DURATION_SECONDS: ${EMBEDDING_MAX_DURATION_SECONDS:-600}
263
+ EMBEDDING_MAX_INPUT_BYTES: ${EMBEDDING_MAX_INPUT_BYTES:-268435456}
264
+ EMBEDDING_MAX_OUTPUT_BYTES: ${EMBEDDING_MAX_OUTPUT_BYTES:-268435456}
265
+ EMBEDDING_MAX_ARTIFACTS: ${EMBEDDING_MAX_ARTIFACTS:-16}
266
+ EMBEDDING_MAX_PIXELS_PER_ARTIFACT: ${EMBEDDING_MAX_PIXELS_PER_ARTIFACT:-100000000}
259
267
  EMBEDDING_RUN_ENCRYPTION_SECRET: ${EMBEDDING_RUN_ENCRYPTION_SECRET:-}
260
268
  EMBEDDING_MAX_CONCURRENT_RUNS: ${EMBEDDING_MAX_CONCURRENT_RUNS:-1}
269
+ EMBEDDING_MAX_QUEUE_DEPTH: ${EMBEDDING_MAX_QUEUE_DEPTH:-0}
270
+ EMBEDDING_READINESS_TIMEOUT_SECONDS: ${EMBEDDING_READINESS_TIMEOUT_SECONDS:-5}
271
+ EMBEDDING_READINESS_TTL_SECONDS: ${EMBEDDING_READINESS_TTL_SECONDS:-60}
261
272
  EMBEDDING_RUN_TERMINAL_TTL_SECONDS: ${EMBEDDING_RUN_TERMINAL_TTL_SECONDS:-604800}
262
273
  volumes:
263
274
  - embedding-run-data:/app/data/embedding
@@ -376,6 +376,12 @@ pin。Run admission 在 `remote.start` 前取得新的 VLM readiness pin;只
376
376
  以真实 canonical Run ID 终结绑定。上传重放、取消、未使用 TTL、scope/Operation/pin 漂移、
377
377
  factory 缺失或 producer 异常均会取消并失败关闭。
378
378
 
379
+ Operation 可 additive 声明 Screenshot context V1 binding,把一个固定 JSON Pointer 指向
380
+ schema-owned 的结构化 Run input。开启后,Host 使用 Web-safe canonical digest helper 对该值
381
+ 重算 `sha256:<64 lowercase hex>`,并与 intake 中持久化 digest 恒时比较;客户端单独提交的
382
+ digest 或“verified”标志都不构成证明。缺失、非 object、漂移或重放只返回稳定安全 code,
383
+ 不会把页面或输入内容写入日志/审计。未 opt-in 的既有 V2 Operation 保留原安全边界。
384
+
379
385
  ## 四类 Runtime 的统一投影
380
386
 
381
387
  ### Document
@@ -1,6 +1,6 @@
1
1
  # Vision AI Runtime
2
2
 
3
- 更新时间:2026-07-27。
3
+ 更新时间:2026-08-06。
4
4
 
5
5
  Windy 的首个 Vision 切片在现有 Agent canonical Run 上增加受治理的图片 Artifact 输入,
6
6
  用于图片内容理解和结构化视觉提取。它不新增由业务选择模型的 `vision` kind:
@@ -28,6 +28,27 @@ Operation。Windy 不解析业务 DOM、Modal、表单正文或 URL,也不接
28
28
  Artifact、Run ID、文件路径或 Provider 信息;未知页面、上下文摘要漂移及未声明
29
29
  Operation 一律失败关闭。
30
30
 
31
+ 新 Operation 可通过 `screenshotContext.contractVersion=1` 和固定
32
+ `contextInputPointer` 显式启用上下文同源证明。Web 对锁定时冻结的结构化 JSON 调用
33
+ `@southwind-ai/shared` 的 `computeScreenshotContextDigestV1` 创建 intake;Run admission 不读取客户端自报的
34
+ “已验证”字段,而是从 Manifest pointer 指向的 Run input 取值,用同一套 key 排序、array
35
+ 保序、UTF-8、SHA-256 算法重算,再与 token 中的 digest 恒时比较。pointer 缺失、值不是
36
+ 结构化 object、格式无效或内容漂移都会消费计划并返回稳定不可用错误;日志与审计不记录
37
+ page、Modal、form 或 input 内容。
38
+
39
+ ```ts
40
+ {
41
+ screenshotContext: {
42
+ contractVersion: 1,
43
+ contextInputPointer: "/uiContext"
44
+ }
45
+ }
46
+ ```
47
+
48
+ 该字段是 additive opt-in:未声明它的 0.6.0 Screenshot Operation 仍保留原有
49
+ Actor/scope/purpose/pin/TTL/一次性 token 边界;声明后的 Operation 才额外强制结构化上下文
50
+ 证明。项目迁移时应先让 Web 与 Run input 复用同一个冻结对象,再打开 Manifest binding。
51
+
31
52
  Host 以 60 秒 TTL 持久化 token 哈希、Actor/scope 绑定、Operation、专用 purpose、
32
53
  Deployment pin 与 Storage file binding。数据库不保存截图字节、Blob locator、Secret、
33
54
  页面正文或表单值。Server/Route 重启后,Host 会按当前 Actor/scope、Feature、Permission
@@ -83,10 +83,12 @@
83
83
  "@types/bun": "^1.3.14",
84
84
  "@types/pg": "^8.20.0",
85
85
  "drizzle-kit": "^0.31.10",
86
+ "drizzle-orm": "^0.45.2",
86
87
  "husky": "^9.1.7",
87
88
  "lint-staged": "^17.1.1",
88
89
  "oxfmt": "^0.59.0",
89
90
  "oxlint": "^1.74.0",
91
+ "pg": "^8.22.0",
90
92
  "typescript": "5.9.3"
91
93
  },
92
94
  "dependencies": {}
@@ -13,7 +13,7 @@
13
13
  "!src/**/*.test.ts"
14
14
  ],
15
15
  "dependencies": {
16
- "@southwind-ai/shared": "workspace:^",
16
+ "@southwind-ai/shared": "workspace:^0.2.5",
17
17
  "zod": "^4.4.3"
18
18
  },
19
19
  "exports": {