create-windy 0.6.0 → 0.6.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 (57) hide show
  1. package/dist/cli.js +82 -2
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +27 -20
  5. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +111 -4
  6. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +13 -0
  7. package/template/apps/embedding-server/src/southwind_embedding_server/pin_cli.py +62 -0
  8. package/template/apps/embedding-server/src/southwind_embedding_server/readiness.py +259 -0
  9. package/template/apps/embedding-server/src/southwind_embedding_server/readiness_contracts.py +221 -0
  10. package/template/apps/embedding-server/src/southwind_embedding_server/readiness_http.py +91 -0
  11. package/template/apps/embedding-server/src/southwind_embedding_server/run_service.py +23 -3
  12. package/template/apps/embedding-server/tests/test_config.py +13 -0
  13. package/template/apps/embedding-server/tests/test_readiness.py +202 -0
  14. package/template/apps/embedding-server/tests/test_run_http.py +76 -4
  15. package/template/apps/embedding-server/tests/test_run_service.py +3 -1
  16. package/template/apps/server/src/agent/drizzle-screenshot-intake-repository.integration.test.ts +3 -0
  17. package/template/apps/server/src/agent/run-facade.ts +2 -1
  18. package/template/apps/server/src/agent/run-route-screenshot-context.test.ts +13 -0
  19. package/template/apps/server/src/agent/runtime-bootstrap.ts +2 -0
  20. package/template/apps/server/src/agent/screenshot-context-admission.test.ts +249 -0
  21. package/template/apps/server/src/agent/screenshot-context-attestation.ts +60 -0
  22. package/template/apps/server/src/agent/screenshot-intake-bootstrap.ts +2 -4
  23. package/template/apps/server/src/agent/screenshot-intake-test-support.ts +4 -0
  24. package/template/apps/server/src/agent/screenshot-intake-v2-validation.ts +24 -0
  25. package/template/apps/server/src/agent/screenshot-intake-v2.test.ts +7 -0
  26. package/template/apps/server/src/agent/screenshot-intake-v2.ts +8 -10
  27. package/template/apps/server/src/agent/screenshot-run-binding.ts +17 -2
  28. package/template/apps/server/src/agent/vision-readiness-pin.ts +6 -48
  29. package/template/apps/server/src/ai-runtime-readiness/deployment-pin.ts +69 -0
  30. package/template/apps/server/src/embedding/contracts.ts +6 -1
  31. package/template/apps/server/src/embedding/facade-readiness.test.ts +307 -0
  32. package/template/apps/server/src/embedding/facade.ts +8 -6
  33. package/template/apps/server/src/embedding/readiness-admission.ts +26 -0
  34. package/template/apps/server/src/embedding/remote-runtime.test.ts +85 -0
  35. package/template/apps/server/src/embedding/remote-runtime.ts +21 -0
  36. package/template/apps/server/src/embedding/run-fingerprint.test.ts +26 -0
  37. package/template/apps/server/src/embedding/run-fingerprint.ts +11 -2
  38. package/template/apps/server/src/module-host/ai-operation-port.test.ts +72 -0
  39. package/template/apps/server/src/module-host/ai-operation-port.ts +43 -0
  40. package/template/apps/server/src/module-host/ai-operation-speech-normalize.test.ts +186 -0
  41. package/template/docker-compose.yml +11 -0
  42. package/template/docs/architecture/ai-runtime-readiness.md +6 -0
  43. package/template/docs/platform/vision-ai-runtime.md +22 -1
  44. package/template/packages/config/package.json +1 -1
  45. package/template/packages/database/package.json +1 -1
  46. package/template/packages/jobs/package.json +1 -1
  47. package/template/packages/modules/index.ts +1 -0
  48. package/template/packages/modules/package.json +1 -1
  49. package/template/packages/modules/src/ai-operation-composition.ts +2 -0
  50. package/template/packages/modules/src/ai-screenshot-context.test.ts +72 -0
  51. package/template/packages/modules/src/ai-screenshot-context.ts +50 -0
  52. package/template/packages/modules/src/manifest.ts +8 -0
  53. package/template/packages/server-sdk/package.json +1 -1
  54. package/template/packages/shared/index.ts +3 -0
  55. package/template/packages/shared/package.json +1 -1
  56. package/template/packages/shared/src/screenshot-context-digest.test.ts +46 -0
  57. package/template/packages/shared/src/screenshot-context-digest.ts +72 -0
@@ -0,0 +1,60 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import {
3
+ parseAiScreenshotContextInputPointer,
4
+ type ModuleAiOperationDefinition,
5
+ } from "@southwind-ai/modules";
6
+ import {
7
+ computeScreenshotContextDigestV1,
8
+ isScreenshotContextDigestV1,
9
+ type JsonValue,
10
+ type ScreenshotContextDigestV1,
11
+ } from "@southwind-ai/shared";
12
+
13
+ /** null 表示 legacy 未 opt-in;undefined 表示已 opt-in 但上下文不能形成证明。 */
14
+ export async function expectedScreenshotContextDigest(
15
+ definition: ModuleAiOperationDefinition,
16
+ input: JsonValue,
17
+ ): Promise<ScreenshotContextDigestV1 | null | undefined> {
18
+ const binding = definition.screenshotContext;
19
+ if (!binding) return null;
20
+ try {
21
+ const context = readPointer(
22
+ input,
23
+ parseAiScreenshotContextInputPointer(binding.contextInputPointer),
24
+ );
25
+ if (!isRecord(context)) return undefined;
26
+ return await computeScreenshotContextDigestV1(context);
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ }
31
+
32
+ export function sameScreenshotContextDigest(
33
+ actual: string,
34
+ expected: string | undefined,
35
+ ): boolean {
36
+ if (
37
+ !isScreenshotContextDigestV1(actual) ||
38
+ !isScreenshotContextDigestV1(expected)
39
+ ) {
40
+ return false;
41
+ }
42
+ const actualBytes = Buffer.from(actual.slice("sha256:".length), "hex");
43
+ const expectedBytes = Buffer.from(expected.slice("sha256:".length), "hex");
44
+ return timingSafeEqual(actualBytes, expectedBytes);
45
+ }
46
+
47
+ function readPointer(value: JsonValue, segments: readonly string[]): JsonValue {
48
+ let current = value;
49
+ for (const segment of segments) {
50
+ if (!isRecord(current) || !Object.hasOwn(current, segment)) {
51
+ throw new Error("Screenshot context input 缺失");
52
+ }
53
+ current = current[segment] as JsonValue;
54
+ }
55
+ return current;
56
+ }
57
+
58
+ function isRecord(value: JsonValue): value is Record<string, JsonValue> {
59
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
60
+ }
@@ -9,10 +9,8 @@ import {
9
9
  PersistentScreenshotIntakeProducer,
10
10
  type ScreenshotPageRegistryV2,
11
11
  } from "./persistent-screenshot-intake-producer.js";
12
- import {
13
- ScreenshotIntakeRuntimeV2,
14
- type ProjectAiScreenshotIntakeFactoryV2,
15
- } from "./screenshot-intake-v2.js";
12
+ import { type ProjectAiScreenshotIntakeFactoryV2 } from "./screenshot-intake-durable.js";
13
+ import { ScreenshotIntakeRuntimeV2 } from "./screenshot-intake-v2.js";
16
14
 
17
15
  export function createScreenshotIntakeRuntimeV2(input: {
18
16
  readonly modules: readonly ModuleManifest[];
@@ -81,6 +81,10 @@ export function screenshotVisionManifest(): ModuleManifest {
81
81
  minConcurrentRuns: 1,
82
82
  minQueueDepth: 0,
83
83
  },
84
+ screenshotContext: {
85
+ contractVersion: 1,
86
+ contextInputPointer: "/uiContext",
87
+ },
84
88
  },
85
89
  ],
86
90
  };
@@ -8,6 +8,8 @@ import type {
8
8
  ModuleAiScreenshotPlanV2,
9
9
  } from "@southwind-ai/server-sdk";
10
10
  import type { RequestGuardContext } from "../guards.js";
11
+ import { sameScreenshotContextDigest } from "./screenshot-context-attestation.js";
12
+ import type { ScreenshotIntakeRecordV2 } from "./screenshot-intake-record.js";
11
13
 
12
14
  const MAX_BYTES = 8 * 1_024 * 1_024;
13
15
  const MAX_PIXELS = 20_000_000;
@@ -114,6 +116,28 @@ export function sameScreenshotDeployment(
114
116
  );
115
117
  }
116
118
 
119
+ export function canReserveScreenshot(
120
+ record: ScreenshotIntakeRecordV2,
121
+ operationKey: string,
122
+ deployment: AiRuntimeDeploymentV1 | undefined,
123
+ expectedContextDigest: string | null | undefined,
124
+ ): record is ScreenshotIntakeRecordV2 & { artifact: ModuleAiArtifactRef } {
125
+ return Boolean(
126
+ record.artifact &&
127
+ (record.durable
128
+ ? ["uploaded", "reserving", "bound"].includes(record.state)
129
+ : record.state === "uploaded") &&
130
+ record.operationKey === operationKey &&
131
+ (expectedContextDigest === null ||
132
+ sameScreenshotContextDigest(
133
+ record.contextDigest,
134
+ expectedContextDigest,
135
+ )) &&
136
+ deployment &&
137
+ sameScreenshotDeployment(record.deployment, deployment),
138
+ );
139
+ }
140
+
117
141
  export function sameScreenshotArtifact(
118
142
  value: ModuleAiArtifactRef,
119
143
  record: {
@@ -35,6 +35,7 @@ describe("Screenshot Intake V2", () => {
35
35
  ...deployment,
36
36
  offersDigest: `sha256:${"b".repeat(64)}` as `sha256:${string}`,
37
37
  },
38
+ declaration().contextDigest,
38
39
  context,
39
40
  ),
40
41
  ).rejects.toThrow("Screenshot Intake");
@@ -50,6 +51,7 @@ describe("Screenshot Intake V2", () => {
50
51
  second.intakeToken,
51
52
  "evidence.image.inspect",
52
53
  deployment,
54
+ declaration().contextDigest,
53
55
  context,
54
56
  );
55
57
  expect(reserved.artifact.value).toBe("opaque-screenshot-artifact");
@@ -59,6 +61,7 @@ describe("Screenshot Intake V2", () => {
59
61
  second.intakeToken,
60
62
  "evidence.image.inspect",
61
63
  deployment,
64
+ declaration().contextDigest,
62
65
  context,
63
66
  ),
64
67
  ).rejects.toThrow("Screenshot Intake");
@@ -332,6 +335,10 @@ function manifest(): ModuleManifest {
332
335
  minConcurrentRuns: 1,
333
336
  minQueueDepth: 0,
334
337
  },
338
+ screenshotContext: {
339
+ contractVersion: 1,
340
+ contextInputPointer: "/uiContext",
341
+ },
335
342
  },
336
343
  ],
337
344
  };
@@ -31,9 +31,9 @@ import {
31
31
  bindingForScreenshot,
32
32
  assertScreenshotOperation,
33
33
  assertScreenshotProducerPlan,
34
+ canReserveScreenshot,
34
35
  sameScreenshotArtifact,
35
36
  sameScreenshotBinding,
36
- sameScreenshotDeployment,
37
37
  screenshotModuleName,
38
38
  screenshotPurpose,
39
39
  } from "./screenshot-intake-v2-validation.js";
@@ -42,8 +42,6 @@ const MAX_ENTRIES = 1_024;
42
42
  const MAX_TTL_MS = 60_000;
43
43
  const SWEEP_INTERVAL_MS = 10_000;
44
44
 
45
- export type { ProjectAiScreenshotIntakeFactoryV2 } from "./screenshot-intake-durable.js";
46
-
47
45
  export class ScreenshotIntakeRuntimeV2 {
48
46
  readonly #records = new Map<string, ScreenshotIntakeRecordV2>();
49
47
  readonly #sweeper: ReturnType<typeof setInterval>;
@@ -211,6 +209,7 @@ export class ScreenshotIntakeRuntimeV2 {
211
209
  token: string,
212
210
  operationKey: string,
213
211
  deployment: AiRuntimeDeploymentV1 | undefined,
212
+ expectedContextDigest: string | null | undefined,
214
213
  context: RequestGuardContext,
215
214
  signal: AbortSignal = AbortSignal.timeout(10_000),
216
215
  ): Promise<{
@@ -226,13 +225,12 @@ export class ScreenshotIntakeRuntimeV2 {
226
225
  context,
227
226
  );
228
227
  if (
229
- !record.artifact ||
230
- (!record.durable && record.state !== "uploaded") ||
231
- (record.durable &&
232
- !["uploaded", "reserving", "bound"].includes(record.state)) ||
233
- record.operationKey !== operationKey ||
234
- !deployment ||
235
- !sameScreenshotDeployment(record.deployment, deployment)
228
+ !canReserveScreenshot(
229
+ record,
230
+ operationKey,
231
+ deployment,
232
+ expectedContextDigest,
233
+ )
236
234
  ) {
237
235
  await this.cancel(record);
238
236
  throw unavailable();
@@ -1,9 +1,12 @@
1
1
  import type {
2
2
  AiRuntimeDeploymentV1,
3
3
  ModuleAiArtifactRef,
4
+ ModuleAiJsonValue,
4
5
  } from "@southwind-ai/server-sdk";
6
+ import type { ModuleAiOperationDefinition } from "@southwind-ai/modules";
5
7
  import type { RequestGuardContext } from "../guards.js";
6
8
  import { AgentRunFacadeError } from "./run-errors.js";
9
+ import { expectedScreenshotContextDigest } from "./screenshot-context-attestation.js";
7
10
  import type { ScreenshotIntakeRuntimeV2 } from "./screenshot-intake-v2.js";
8
11
 
9
12
  export interface ScreenshotRunBinding {
@@ -15,14 +18,26 @@ export interface ScreenshotRunBinding {
15
18
  export async function reserveScreenshotForRun(
16
19
  intake: ScreenshotIntakeRuntimeV2 | undefined,
17
20
  token: string | undefined,
18
- operationKey: string,
21
+ definition: ModuleAiOperationDefinition,
22
+ input: ModuleAiJsonValue,
19
23
  deployment: AiRuntimeDeploymentV1 | undefined,
20
24
  context: RequestGuardContext,
21
25
  signal: AbortSignal,
22
26
  ): Promise<ScreenshotRunBinding | undefined> {
23
27
  if (!token) return undefined;
24
28
  if (!intake) throw unavailable();
25
- return await intake.reserve(token, operationKey, deployment, context, signal);
29
+ const contextDigest = await expectedScreenshotContextDigest(
30
+ definition,
31
+ input,
32
+ );
33
+ return await intake.reserve(
34
+ token,
35
+ definition.key,
36
+ deployment,
37
+ contextDigest,
38
+ context,
39
+ signal,
40
+ );
26
41
  }
27
42
 
28
43
  function unavailable(): AgentRunFacadeError {
@@ -1,19 +1,15 @@
1
+ import type { AiRuntimeDeploymentV1 } from "@southwind-ai/server-sdk";
1
2
  import {
2
- parseAiRuntimeReadinessV1,
3
- type AiRuntimeDeploymentV1,
4
- } from "@southwind-ai/server-sdk";
3
+ assertAiRuntimeReadinessPin,
4
+ sameAiRuntimeDeployment,
5
+ } from "../ai-runtime-readiness/deployment-pin.js";
5
6
  import { AgentRuntimeError } from "./run-contracts.js";
6
7
 
7
8
  export function sameVisionReadinessDeployment(
8
9
  actual: AiRuntimeDeploymentV1,
9
10
  expected: AiRuntimeDeploymentV1,
10
11
  ): boolean {
11
- return (
12
- actual.key === expected.key &&
13
- actual.identity === expected.identity &&
14
- actual.releaseDigest === expected.releaseDigest &&
15
- actual.offersDigest === expected.offersDigest
16
- );
12
+ return sameAiRuntimeDeployment(actual, expected);
17
13
  }
18
14
 
19
15
  export function unavailableVisionReadiness(): AgentRuntimeError {
@@ -28,47 +24,9 @@ export async function assertVisionReadinessPin(
28
24
  expected: AiRuntimeDeploymentV1,
29
25
  health: () => Promise<Response>,
30
26
  ): Promise<void> {
31
- const response = await boundedHealth(health);
32
- if (response.status !== 200) throw unavailableVisionReadiness();
33
- if (!response.headers.get("content-type")?.includes("application/json")) {
34
- throw unavailableVisionReadiness();
35
- }
36
- const length = Number(response.headers.get("content-length") ?? "0");
37
- if (!Number.isSafeInteger(length) || length < 0 || length > 64 * 1_024) {
38
- throw unavailableVisionReadiness();
39
- }
40
- let snapshot: unknown;
41
27
  try {
42
- const text = await response.text();
43
- if (new TextEncoder().encode(text).byteLength > 64 * 1_024) {
44
- throw new Error("health body too large");
45
- }
46
- snapshot = JSON.parse(text);
28
+ await assertAiRuntimeReadinessPin(expected, health);
47
29
  } catch {
48
30
  throw unavailableVisionReadiness();
49
31
  }
50
- const parsed = parseAiRuntimeReadinessV1(snapshot, new Date().toISOString());
51
- if (
52
- !parsed.ok ||
53
- parsed.value.status !== "ready" ||
54
- !sameVisionReadinessDeployment(parsed.value.deployment, expected)
55
- ) {
56
- throw unavailableVisionReadiness();
57
- }
58
- }
59
-
60
- async function boundedHealth(
61
- health: () => Promise<Response>,
62
- ): Promise<Response> {
63
- let timeout: ReturnType<typeof setTimeout> | undefined;
64
- try {
65
- return await Promise.race([
66
- health(),
67
- new Promise<Response>((_, reject) => {
68
- timeout = setTimeout(() => reject(unavailableVisionReadiness()), 5_000);
69
- }),
70
- ]);
71
- } finally {
72
- if (timeout) clearTimeout(timeout);
73
- }
74
32
  }
@@ -0,0 +1,69 @@
1
+ import {
2
+ parseAiRuntimeReadinessV1,
3
+ type AiRuntimeDeploymentV1,
4
+ } from "@southwind-ai/server-sdk";
5
+
6
+ export function sameAiRuntimeDeployment(
7
+ actual: AiRuntimeDeploymentV1,
8
+ expected: AiRuntimeDeploymentV1,
9
+ ): boolean {
10
+ return (
11
+ actual.key === expected.key &&
12
+ actual.identity === expected.identity &&
13
+ actual.releaseDigest === expected.releaseDigest &&
14
+ actual.offersDigest === expected.offersDigest
15
+ );
16
+ }
17
+
18
+ export async function assertAiRuntimeReadinessPin(
19
+ expected: AiRuntimeDeploymentV1,
20
+ health: () => Promise<Response>,
21
+ ): Promise<void> {
22
+ const response = await boundedHealth(health);
23
+ if (response.status !== 200) throw unavailable();
24
+ if (!response.headers.get("content-type")?.includes("application/json")) {
25
+ throw unavailable();
26
+ }
27
+ const length = Number(response.headers.get("content-length") ?? "0");
28
+ if (!Number.isSafeInteger(length) || length < 0 || length > 64 * 1_024) {
29
+ throw unavailable();
30
+ }
31
+ let snapshot: unknown;
32
+ try {
33
+ const text = await response.text();
34
+ if (new TextEncoder().encode(text).byteLength > 64 * 1_024) {
35
+ throw new Error("health body too large");
36
+ }
37
+ snapshot = JSON.parse(text);
38
+ } catch {
39
+ throw unavailable();
40
+ }
41
+ const parsed = parseAiRuntimeReadinessV1(snapshot, new Date().toISOString());
42
+ if (
43
+ !parsed.ok ||
44
+ parsed.value.status !== "ready" ||
45
+ !sameAiRuntimeDeployment(parsed.value.deployment, expected)
46
+ ) {
47
+ throw unavailable();
48
+ }
49
+ }
50
+
51
+ async function boundedHealth(
52
+ health: () => Promise<Response>,
53
+ ): Promise<Response> {
54
+ let timeout: ReturnType<typeof setTimeout> | undefined;
55
+ try {
56
+ return await Promise.race([
57
+ health(),
58
+ new Promise<Response>((_, reject) => {
59
+ timeout = setTimeout(() => reject(unavailable()), 5_000);
60
+ }),
61
+ ]);
62
+ } finally {
63
+ if (timeout) clearTimeout(timeout);
64
+ }
65
+ }
66
+
67
+ function unavailable(): Error {
68
+ return new Error("AI Runtime readiness pin mismatch");
69
+ }
@@ -4,7 +4,10 @@ import type {
4
4
  AiProviderDataPolicy,
5
5
  AiProviderEgressPolicy,
6
6
  } from "@southwind-ai/modules";
7
- import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
7
+ import type {
8
+ AiRuntimeDeploymentV1,
9
+ ModuleAiJsonValue,
10
+ } from "@southwind-ai/server-sdk";
8
11
 
9
12
  export type EmbeddingRuntimeItem =
10
13
  | { readonly id: string; readonly text: string }
@@ -36,6 +39,8 @@ export interface EmbeddingRuntimeOperation {
36
39
  };
37
40
  readonly providerEgress: AiProviderEgressPolicy;
38
41
  readonly providerDataPolicy?: AiProviderDataPolicy;
42
+ /** Host-projected immutable deployment facts; never supplied by a Module. */
43
+ readonly readinessPin?: AiRuntimeDeploymentV1;
39
44
  }
40
45
 
41
46
  export interface EmbeddingBatchResponse {