create-windy 0.6.14 → 0.6.16

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 (162) hide show
  1. package/dist/cli.js +1008 -61
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +58 -8
  4. package/template/.windy-template.json +2 -2
  5. package/template/apps/server/src/agent/client-session/attempt-authority.test.ts +162 -0
  6. package/template/apps/server/src/agent/client-session/attempt-authority.ts +191 -0
  7. package/template/apps/server/src/agent/client-session/bootstrap.ts +27 -11
  8. package/template/apps/server/src/agent/client-session/completion.ts +75 -24
  9. package/template/apps/server/src/agent/client-session/contracts.ts +23 -0
  10. package/template/apps/server/src/agent/client-session/creation.ts +103 -0
  11. package/template/apps/server/src/agent/client-session/digest.ts +11 -1
  12. package/template/apps/server/src/agent/client-session/drizzle-records.ts +150 -0
  13. package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts +30 -3
  14. package/template/apps/server/src/agent/client-session/drizzle-repository.ts +23 -132
  15. package/template/apps/server/src/agent/client-session/finalization.ts +99 -0
  16. package/template/apps/server/src/agent/client-session/host-auth.ts +22 -10
  17. package/template/apps/server/src/agent/client-session/host-revocation-composition.test.ts +77 -0
  18. package/template/apps/server/src/agent/client-session/host-revocation-composition.ts +28 -0
  19. package/template/apps/server/src/agent/client-session/launch-authority.contract.test.ts +21 -0
  20. package/template/apps/server/src/agent/client-session/manifest-admission.ts +11 -0
  21. package/template/apps/server/src/agent/client-session/service.test-support.ts +22 -0
  22. package/template/apps/server/src/agent/client-session/service.ts +104 -134
  23. package/template/apps/server/src/agent/client-session/terminal-digest.test.ts +23 -0
  24. package/template/apps/server/src/agent/client-workflow-artifact/active-blob-references.ts +8 -0
  25. package/template/apps/server/src/agent/client-workflow-artifact/artifact-builder.ts +60 -0
  26. package/template/apps/server/src/agent/client-workflow-artifact/authority-current.ts +59 -0
  27. package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.test.ts +189 -0
  28. package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.ts +153 -0
  29. package/template/apps/server/src/agent/client-workflow-artifact/bootstrap.ts +77 -0
  30. package/template/apps/server/src/agent/client-workflow-artifact/cancellation.ts +47 -0
  31. package/template/apps/server/src/agent/client-workflow-artifact/completed-recovery.ts +31 -0
  32. package/template/apps/server/src/agent/client-workflow-artifact/contracts.ts +104 -0
  33. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-locks.ts +30 -0
  34. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-mapper.ts +138 -0
  35. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-repository.ts +346 -0
  36. package/template/apps/server/src/agent/client-workflow-artifact/error.ts +21 -0
  37. package/template/apps/server/src/agent/client-workflow-artifact/handler.ts +167 -0
  38. package/template/apps/server/src/agent/client-workflow-artifact/host-revocation.ts +29 -0
  39. package/template/apps/server/src/agent/client-workflow-artifact/in-memory-repository.ts +232 -0
  40. package/template/apps/server/src/agent/client-workflow-artifact/job-port.ts +101 -0
  41. package/template/apps/server/src/agent/client-workflow-artifact/presentation.ts +41 -0
  42. package/template/apps/server/src/agent/client-workflow-artifact/recovery.contract.test.ts +253 -0
  43. package/template/apps/server/src/agent/client-workflow-artifact/release-binding.ts +85 -0
  44. package/template/apps/server/src/agent/client-workflow-artifact/repository.test.ts +204 -0
  45. package/template/apps/server/src/agent/client-workflow-artifact/repository.ts +97 -0
  46. package/template/apps/server/src/agent/client-workflow-artifact/routes-handler.contract.test.ts +201 -0
  47. package/template/apps/server/src/agent/client-workflow-artifact/routes.ts +161 -0
  48. package/template/apps/server/src/agent/client-workflow-artifact/runtime-composition.ts +101 -0
  49. package/template/apps/server/src/agent/client-workflow-artifact/scope-authority.ts +39 -0
  50. package/template/apps/server/src/agent/client-workflow-artifact/service-authorization.ts +117 -0
  51. package/template/apps/server/src/agent/client-workflow-artifact/service-support.ts +28 -0
  52. package/template/apps/server/src/agent/client-workflow-artifact/service.contract.test.ts +345 -0
  53. package/template/apps/server/src/agent/client-workflow-artifact/service.ts +350 -0
  54. package/template/apps/server/src/agent/client-workflow-artifact/session-boundary.ts +39 -0
  55. package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts +98 -0
  56. package/template/apps/server/src/agent/client-workflow-artifact/source-access.ts +80 -0
  57. package/template/apps/server/src/agent/client-workflow-artifact/takeover.ts +122 -0
  58. package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts +97 -0
  59. package/template/apps/server/src/agent/client-workflow-artifact/task-authority.ts +147 -0
  60. package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts +100 -0
  61. package/template/apps/server/src/agent/client-workflow-artifact/terminal-replay.ts +31 -0
  62. package/template/apps/server/src/agent/client-workflow-artifact/terminal-seal.ts +211 -0
  63. package/template/apps/server/src/agent/client-workflow-artifact/workflow-artifact.test-support.ts +173 -0
  64. package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.contract.test.ts +254 -0
  65. package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.ts +285 -0
  66. package/template/apps/server/src/agent/run-facade.test-support.ts +17 -4
  67. package/template/apps/server/src/agent/run-facade.test.ts +16 -0
  68. package/template/apps/server/src/agent/run-session-authorization.ts +12 -10
  69. package/template/apps/server/src/agent/runtime-bootstrap-input.ts +5 -1
  70. package/template/apps/server/src/agent/runtime-bootstrap.ts +33 -33
  71. package/template/apps/server/src/agent/runtime-public-ports.ts +40 -0
  72. package/template/apps/server/src/agent/session-invalidation/contracts.ts +25 -26
  73. package/template/apps/server/src/agent/session-projection-access.ts +31 -19
  74. package/template/apps/server/src/agent/unavailable-routes.ts +21 -0
  75. package/template/apps/server/src/agent/workflow-runtime-bootstrap.ts +2 -0
  76. package/template/apps/server/src/agent-workflow/artifact-source-authorization.ts +12 -6
  77. package/template/apps/server/src/agent-workflow/command-facade.ts +70 -11
  78. package/template/apps/server/src/agent-workflow/drizzle-artifact-port.ts +9 -9
  79. package/template/apps/server/src/agent-workflow/drizzle-delegation-completion.ts +1 -1
  80. package/template/apps/server/src/agent-workflow/platform-node-adapters.ts +14 -6
  81. package/template/apps/server/src/agent-workflow/project-launch-authority.contract.test.ts +53 -0
  82. package/template/apps/server/src/agent-workflow/project-launch-authority.ts +92 -0
  83. package/template/apps/server/src/agent-workflow/runtime-composition.ts +30 -14
  84. package/template/apps/server/src/app/agent-runtime-composition.ts +6 -1
  85. package/template/apps/server/src/app/client-agent-workflow-artifact-binding.ts +14 -0
  86. package/template/apps/server/src/application-services.ts +6 -0
  87. package/template/apps/server/src/data-access/scope-containment.test.ts +31 -0
  88. package/template/apps/server/src/data-access/scope-containment.ts +48 -0
  89. package/template/apps/server/src/jobs/artifact-publication.test.ts +192 -0
  90. package/template/apps/server/src/jobs/artifact-publication.ts +56 -0
  91. package/template/apps/server/src/jobs/artifact-publisher.ts +101 -0
  92. package/template/apps/server/src/jobs/drizzle-artifact.ts +73 -1
  93. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +14 -1
  94. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +65 -0
  95. package/template/apps/server/src/jobs/drizzle-repository.integration.test.ts +1 -0
  96. package/template/apps/server/src/jobs/execution-repository.ts +28 -0
  97. package/template/apps/server/src/jobs/execution.test.ts +3 -1
  98. package/template/apps/server/src/jobs/in-memory-execution.ts +16 -0
  99. package/template/apps/server/src/jobs/in-memory-lease.ts +45 -1
  100. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +2 -0
  101. package/template/apps/server/src/jobs/postgres-cleanup-contract.test.ts +58 -0
  102. package/template/apps/server/src/jobs/worker.ts +8 -14
  103. package/template/apps/server/src/module-host/background-ai-unavailable.ts +18 -0
  104. package/template/apps/server/src/module-host/durable-context.ts +19 -22
  105. package/template/apps/server/src/module-host/implementation-completeness.test.ts +80 -1
  106. package/template/apps/server/src/module-host/implementation-completeness.ts +28 -1
  107. package/template/apps/server/src/module-host/request-context.ts +32 -8
  108. package/template/apps/server/src/module-host/storage-port.ts +3 -0
  109. package/template/apps/server/src/module-host/workflow-input-artifact-port.test.ts +104 -0
  110. package/template/apps/server/src/module-host/workflow-input-artifact-port.ts +229 -0
  111. package/template/apps/server/src/module-host/workflow-input-materialization.test.ts +163 -0
  112. package/template/apps/server/src/module-host/workflow-input-materialization.ts +161 -0
  113. package/template/apps/server/src/workflow/drizzle-repository.integration.test.ts +1 -0
  114. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +2 -1
  115. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +16 -0
  116. package/template/docs/architecture/agent-workflow.md +13 -2
  117. package/template/docs/architecture/durable-jobs.md +7 -0
  118. package/template/docs/operations/agent-session-memory.md +2 -1
  119. package/template/docs/operations/agent-session-troubleshooting.md +87 -2
  120. package/template/docs/operations/agent-workflow-recovery.md +25 -14
  121. package/template/docs/platform/client-agent-session.md +148 -2
  122. package/template/packages/ai-client/package.json +2 -1
  123. package/template/packages/ai-client/src/client-agent/index.ts +1 -0
  124. package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
  125. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
  126. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
  127. package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
  128. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
  129. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
  130. package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
  131. package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
  132. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  133. package/template/packages/database/package.json +1 -1
  134. package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
  135. package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
  136. package/template/packages/database/src/schema/index.ts +1 -0
  137. package/template/packages/jobs/package.json +1 -1
  138. package/template/packages/jobs/src/handlers.test.ts +10 -0
  139. package/template/packages/jobs/src/handlers.ts +28 -11
  140. package/template/packages/modules/package.json +1 -1
  141. package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
  142. package/template/packages/modules/src/manifest.ts +2 -0
  143. package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
  144. package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
  145. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  146. package/template/packages/server-sdk/index.ts +52 -0
  147. package/template/packages/server-sdk/package.json +5 -1
  148. package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
  149. package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
  150. package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
  151. package/template/packages/server-sdk/src/durable-handler.ts +7 -0
  152. package/template/packages/server-sdk/src/request-context.ts +7 -0
  153. package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
  154. package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
  155. package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
  156. package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
  157. package/template/packages/storage/package.json +1 -1
  158. package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
  159. package/template/packages/storage/src/blob-store.ts +9 -0
  160. package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
  161. package/template/packages/storage/src/local-file-blob-store.ts +53 -1
  162. package/template/packages/storage/src/memory-blob-store.ts +73 -33
@@ -14,6 +14,7 @@ import {
14
14
  type UploadAuthorizationBinding,
15
15
  type FileMetadata,
16
16
  type FileService,
17
+ type BlobStore,
17
18
  type UploadSession,
18
19
  type UploadService,
19
20
  } from "@southwind-ai/storage";
@@ -21,6 +22,8 @@ import {
21
22
  export interface ModuleStorageServices {
22
23
  uploads: UploadService;
23
24
  files: FileService;
25
+ /** Host-only Durable publication seam;ModuleStoragePort 不暴露 BlobStore。 */
26
+ blobStore?: BlobStore;
24
27
  }
25
28
 
26
29
  export interface ModuleStorageScope {
@@ -0,0 +1,104 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { DurableHandlerArtifactInput } from "@southwind-ai/jobs";
3
+ import { MemoryBlobStore } from "@southwind-ai/storage";
4
+ import { createModuleWorkflowInputArtifactPublicationPort } from "./workflow-input-artifact-port.js";
5
+
6
+ test("Storage resource 经真实 Durable publication 后才成为 Workflow artifact ref", async () => {
7
+ const blobs = new MemoryBlobStore();
8
+ const published: DurableHandlerArtifactInput[] = [];
9
+ const port = createModuleWorkflowInputArtifactPublicationPort({
10
+ moduleName: "case",
11
+ actorId: "actor-1",
12
+ blobs,
13
+ storage: {
14
+ async openResourceFile() {
15
+ const bytes = new TextEncoder().encode('{"caseId":"case-1"}');
16
+ return {
17
+ id: "file_storage_only",
18
+ purpose: "case-evidence",
19
+ filename: "case.json",
20
+ mimeType: "application/json",
21
+ byteSize: bytes.byteLength,
22
+ sha256:
23
+ "b749cb42fc25c341604392803be93cd89343609feddd5d93d5b6596e6d7d31e9",
24
+ body: new Blob([bytes]).stream(),
25
+ };
26
+ },
27
+ },
28
+ durable: {
29
+ jobId: "job-1",
30
+ stageKey: "prepare",
31
+ attemptNumber: 1,
32
+ signal: new AbortController().signal,
33
+ heartbeat: async () => undefined,
34
+ progress: async () => undefined,
35
+ publishArtifact: async () => undefined,
36
+ async publishArtifactOnce(publicationKey, artifact) {
37
+ published.push(artifact);
38
+ return {
39
+ schemaVersion: 1,
40
+ publicationKey,
41
+ artifactId: "dap_real_artifact",
42
+ jobId: "job-1",
43
+ stageKey: "prepare",
44
+ kind: artifact.kind,
45
+ artifactRef: artifact.artifactRef,
46
+ sha256: artifact.sha256,
47
+ };
48
+ },
49
+ },
50
+ });
51
+
52
+ const result = await port.publishResource({
53
+ publicationKey: "case-input-v1",
54
+ kind: "case-snapshot",
55
+ filename: "case.json",
56
+ retentionPolicyKey: "case.analysis",
57
+ resource: {
58
+ fileId: "file_storage_only",
59
+ purpose: "case-evidence",
60
+ resourceType: "case",
61
+ resourceId: "case-1",
62
+ },
63
+ source: {
64
+ kind: "case",
65
+ id: "case-1",
66
+ version: "42",
67
+ sequence: 42,
68
+ digest: `sha256:${"a".repeat(64)}`,
69
+ },
70
+ });
71
+
72
+ expect(result.artifactId).toBe("dap_real_artifact");
73
+ expect(result.artifactId).not.toBe("file_storage_only");
74
+ expect(result.source).toEqual({
75
+ moduleKey: "case",
76
+ kind: "case",
77
+ id: "case-1",
78
+ version: "42",
79
+ sequence: 42,
80
+ digest: `sha256:${"a".repeat(64)}`,
81
+ });
82
+ expect(published[0]?.metadata).toMatchObject({
83
+ sourceModuleKey: "case",
84
+ sourceId: "case-1",
85
+ retentionPolicyKey: "case.analysis",
86
+ });
87
+ expect(blobs.size).toBe(1);
88
+ await expect(
89
+ port.publishJson({
90
+ publicationKey: "invalid-json",
91
+ kind: "case-snapshot",
92
+ filename: "invalid.json",
93
+ retentionPolicyKey: "case.analysis",
94
+ source: {
95
+ kind: "case",
96
+ id: "case-1",
97
+ version: "42",
98
+ sequence: 42,
99
+ },
100
+ value: undefined as never,
101
+ }),
102
+ ).rejects.toMatchObject({ code: "WORKFLOW_INPUT_ARTIFACT_INVALID" });
103
+ expect(blobs.size).toBe(1);
104
+ });
@@ -0,0 +1,229 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ DurableHandlerError,
4
+ type DurableHandlerContext,
5
+ } from "@southwind-ai/jobs";
6
+ import {
7
+ parseModuleWorkflowInputJsonArtifactV1,
8
+ parseModuleWorkflowInputResourceArtifactV1,
9
+ type ModuleDurableStoragePort,
10
+ type ModuleWorkflowInputArtifactPublicationPortV1,
11
+ type ModuleWorkflowInputArtifactSourceV1,
12
+ } from "@southwind-ai/server-sdk";
13
+ import type { AgentWorkflowArtifactRef, JsonValue } from "@southwind-ai/shared";
14
+ import {
15
+ artifactRef,
16
+ type BlobStore,
17
+ type ContentHash,
18
+ } from "@southwind-ai/storage";
19
+
20
+ export function createModuleWorkflowInputArtifactPublicationPort(input: {
21
+ readonly moduleName: string;
22
+ readonly actorId: string;
23
+ readonly storage: ModuleDurableStoragePort;
24
+ readonly blobs: BlobStore;
25
+ readonly durable: DurableHandlerContext;
26
+ }): ModuleWorkflowInputArtifactPublicationPortV1 {
27
+ return {
28
+ async publishResource(spec) {
29
+ const parsed = parseResource(spec);
30
+ const opened = await input.storage.openResourceFile(parsed.resource);
31
+ return publish(input, {
32
+ ...parsed,
33
+ mediaType: opened.mimeType,
34
+ byteSize: opened.byteSize,
35
+ sha256: opened.sha256,
36
+ body: opened.body,
37
+ });
38
+ },
39
+ async publishJson(spec) {
40
+ const parsed = parseJsonArtifact(spec);
41
+ const bytes = new TextEncoder().encode(stableJson(parsed.value));
42
+ return publish(input, {
43
+ ...parsed,
44
+ mediaType: "application/json",
45
+ byteSize: bytes.byteLength,
46
+ sha256: sha256(bytes),
47
+ body: new Blob([bytes]).stream(),
48
+ });
49
+ },
50
+ };
51
+ }
52
+
53
+ export function moduleWorkflowInputArtifactContext(
54
+ moduleName: string,
55
+ actorId: string,
56
+ storage: ModuleDurableStoragePort,
57
+ blobs: BlobStore | undefined,
58
+ durable: DurableHandlerContext,
59
+ ) {
60
+ return blobs
61
+ ? {
62
+ workflowInputArtifacts:
63
+ createModuleWorkflowInputArtifactPublicationPort({
64
+ moduleName,
65
+ actorId,
66
+ storage,
67
+ blobs,
68
+ durable,
69
+ }),
70
+ }
71
+ : {};
72
+ }
73
+
74
+ async function publish(
75
+ host: {
76
+ moduleName: string;
77
+ actorId: string;
78
+ blobs: BlobStore;
79
+ durable: DurableHandlerContext;
80
+ },
81
+ artifact: {
82
+ publicationKey: string;
83
+ kind: string;
84
+ filename: string;
85
+ retentionPolicyKey: string;
86
+ source: ModuleWorkflowInputArtifactSourceV1;
87
+ mediaType: string;
88
+ byteSize: number;
89
+ sha256: string;
90
+ body: ReadableStream<Uint8Array>;
91
+ },
92
+ ): Promise<AgentWorkflowArtifactRef> {
93
+ assertCoordinate(artifact);
94
+ if (!host.blobs.putOnce || !host.durable.publishArtifactOnce) {
95
+ throw unavailable();
96
+ }
97
+ const ref = artifactRef(
98
+ `blob_${sha256Text(
99
+ JSON.stringify([
100
+ host.durable.jobId,
101
+ host.durable.stageKey,
102
+ artifact.publicationKey,
103
+ ]),
104
+ ).slice(0, 32)}`,
105
+ );
106
+ const expectedContentHash: ContentHash = {
107
+ algorithm: "sha256",
108
+ value: artifact.sha256,
109
+ };
110
+ const stored = await host.blobs.putOnce({
111
+ ref,
112
+ body: artifact.body,
113
+ expectedByteSize: artifact.byteSize,
114
+ expectedContentHash,
115
+ });
116
+ const receipt = await host.durable.publishArtifactOnce(
117
+ artifact.publicationKey,
118
+ {
119
+ kind: artifact.kind,
120
+ artifactRef: stored.ref,
121
+ filename: artifact.filename,
122
+ contentType: artifact.mediaType,
123
+ byteSize: stored.byteSize,
124
+ sha256: stored.contentHash.value,
125
+ ownerId: host.actorId,
126
+ metadata: {
127
+ publicationKey: artifact.publicationKey,
128
+ retentionPolicyKey: artifact.retentionPolicyKey,
129
+ sourceModuleKey: host.moduleName,
130
+ sourceKind: artifact.source.kind,
131
+ sourceId: artifact.source.id,
132
+ sourceVersion: artifact.source.version,
133
+ sourceSequence: artifact.source.sequence,
134
+ ...(artifact.source.digest
135
+ ? { sourceDigest: artifact.source.digest }
136
+ : {}),
137
+ },
138
+ },
139
+ );
140
+ return {
141
+ artifactId: receipt.artifactId,
142
+ kind: artifact.kind,
143
+ mediaType: artifact.mediaType,
144
+ byteSize: stored.byteSize,
145
+ sha256: stored.contentHash.value,
146
+ retentionPolicyKey: artifact.retentionPolicyKey,
147
+ source: { moduleKey: host.moduleName, ...artifact.source },
148
+ };
149
+ }
150
+
151
+ function assertCoordinate(input: {
152
+ publicationKey: string;
153
+ kind: string;
154
+ filename: string;
155
+ retentionPolicyKey: string;
156
+ source: ModuleWorkflowInputArtifactSourceV1;
157
+ }) {
158
+ const text = [
159
+ input.publicationKey,
160
+ input.kind,
161
+ input.filename,
162
+ input.retentionPolicyKey,
163
+ input.source.kind,
164
+ input.source.id,
165
+ input.source.version,
166
+ ];
167
+ if (
168
+ text.some((value) => !value.trim() || value !== value.trim()) ||
169
+ !Number.isSafeInteger(input.source.sequence) ||
170
+ input.source.sequence < 1
171
+ ) {
172
+ throw new DurableHandlerError(
173
+ "WORKFLOW_INPUT_ARTIFACT_INVALID",
174
+ "Workflow input artifact 坐标无效",
175
+ false,
176
+ );
177
+ }
178
+ }
179
+
180
+ function stableJson(value: JsonValue): string {
181
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
182
+ if (value && typeof value === "object") {
183
+ return `{${Object.keys(value)
184
+ .sort()
185
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key]!)}`)
186
+ .join(",")}}`;
187
+ }
188
+ return JSON.stringify(value);
189
+ }
190
+
191
+ function sha256(bytes: Uint8Array) {
192
+ return createHash("sha256").update(bytes).digest("hex");
193
+ }
194
+
195
+ function sha256Text(value: string) {
196
+ return createHash("sha256").update(value).digest("hex");
197
+ }
198
+
199
+ function unavailable() {
200
+ return new DurableHandlerError(
201
+ "WORKFLOW_INPUT_ARTIFACT_UNAVAILABLE",
202
+ "Workflow input artifact publication 未装配",
203
+ false,
204
+ );
205
+ }
206
+
207
+ function parseResource(value: unknown) {
208
+ try {
209
+ return parseModuleWorkflowInputResourceArtifactV1(value);
210
+ } catch {
211
+ throw invalid();
212
+ }
213
+ }
214
+
215
+ function parseJsonArtifact(value: unknown) {
216
+ try {
217
+ return parseModuleWorkflowInputJsonArtifactV1(value);
218
+ } catch {
219
+ throw invalid();
220
+ }
221
+ }
222
+
223
+ function invalid() {
224
+ return new DurableHandlerError(
225
+ "WORKFLOW_INPUT_ARTIFACT_INVALID",
226
+ "Workflow input artifact 输入无效",
227
+ false,
228
+ );
229
+ }
@@ -0,0 +1,163 @@
1
+ import { expect, test } from "bun:test";
2
+ import type { DurableJobSnapshot, DurableJobs } from "@southwind-ai/jobs";
3
+ import type { ModuleJobPort } from "@southwind-ai/server-sdk";
4
+ import { artifactRef } from "@southwind-ai/storage";
5
+ import { buildScopeContext } from "../data-access/context.js";
6
+ import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
7
+ import type { RequestGuardContext } from "../guards.js";
8
+ import { createModuleWorkflowInputMaterializationPort } from "./workflow-input-materialization.js";
9
+
10
+ test("两阶段 coordinator 只在真实 module Job succeeded 后返回正式 artifact refs", async () => {
11
+ const context = guard();
12
+ const source = {
13
+ mode: "json" as const,
14
+ publicationKey: "case-1-v7",
15
+ kind: "structured-data",
16
+ filename: "case-1.json",
17
+ retentionPolicyKey: "case.analysis",
18
+ source: {
19
+ kind: "case",
20
+ id: "case-1",
21
+ version: "7",
22
+ sequence: 7,
23
+ digest: `sha256:${"a".repeat(64)}`,
24
+ },
25
+ value: { caseId: "case-1" },
26
+ };
27
+ let status: "running" | "succeeded" = "running";
28
+ let submittedPayload: Record<string, unknown> | undefined;
29
+ const snapshot = (): DurableJobSnapshot => ({
30
+ job: {
31
+ id: "job-real",
32
+ handlerKey: "case.workflow-input-materialize",
33
+ queue: "case",
34
+ status,
35
+ idempotencyKey: "materialize-case-1",
36
+ payload: submittedPayload!,
37
+ requestedBy: "actor-1",
38
+ authorizationScope: captureAuthorizationScope(context.actor.scopeContext)
39
+ .snapshot,
40
+ priority: 0,
41
+ maxAttempts: 3,
42
+ timeoutSeconds: 900,
43
+ availableAt: "2026-08-28T00:00:00.000Z",
44
+ createdAt: "2026-08-28T00:00:00.000Z",
45
+ updatedAt: "2026-08-28T00:00:00.000Z",
46
+ },
47
+ stages: [],
48
+ attempts: [],
49
+ leases: [],
50
+ progress: [],
51
+ failures: [],
52
+ recoveries: [],
53
+ artifacts:
54
+ status === "succeeded"
55
+ ? [
56
+ {
57
+ id: "dap_real",
58
+ jobId: "job-real",
59
+ kind: source.kind,
60
+ artifactRef: artifactRef(`blob_${"b".repeat(32)}`),
61
+ filename: source.filename,
62
+ contentType: "application/json",
63
+ byteSize: 19,
64
+ sha256: "c".repeat(64),
65
+ ownerId: "actor-1",
66
+ metadata: {
67
+ publicationKey: source.publicationKey,
68
+ retentionPolicyKey: source.retentionPolicyKey,
69
+ sourceModuleKey: "case",
70
+ sourceKind: source.source.kind,
71
+ sourceId: source.source.id,
72
+ sourceVersion: source.source.version,
73
+ sourceSequence: source.source.sequence,
74
+ sourceDigest: source.source.digest,
75
+ },
76
+ createdAt: "2026-08-28T00:00:00.000Z",
77
+ },
78
+ ]
79
+ : [],
80
+ });
81
+ const moduleJobs = {
82
+ async submit(input) {
83
+ submittedPayload = input.payload;
84
+ return { jobId: "job-real", created: true };
85
+ },
86
+ async get() {
87
+ return undefined;
88
+ },
89
+ async cancel() {
90
+ throw new Error("not used");
91
+ },
92
+ async recover() {
93
+ throw new Error("not used");
94
+ },
95
+ } satisfies ModuleJobPort;
96
+ const jobs = {
97
+ async submit() {
98
+ throw new Error("coordinator must use scoped module job port");
99
+ },
100
+ async get() {
101
+ return snapshot();
102
+ },
103
+ async list() {
104
+ return [];
105
+ },
106
+ } satisfies DurableJobs;
107
+ const port = createModuleWorkflowInputMaterializationPort({
108
+ moduleName: "case",
109
+ guardContext: context,
110
+ jobs,
111
+ moduleJobs,
112
+ });
113
+
114
+ const handle = await port.begin({
115
+ idempotencyKey: "materialize-case-1",
116
+ sources: [source],
117
+ });
118
+ await expect(port.resolve(handle)).resolves.toEqual({
119
+ ...handle,
120
+ status: "pending",
121
+ });
122
+ status = "succeeded";
123
+ await expect(port.resolve(handle)).resolves.toMatchObject({
124
+ status: "ready",
125
+ artifacts: [{ artifactId: "dap_real", source: { moduleKey: "case" } }],
126
+ });
127
+ expect(submittedPayload).toEqual({ schemaVersion: 1, sources: [source] });
128
+ await expect(
129
+ port.begin({
130
+ idempotencyKey: "materialize-invalid",
131
+ sources: [{ ...source, value: Number.POSITIVE_INFINITY }],
132
+ }),
133
+ ).rejects.toMatchObject({ code: "MATERIALIZATION_RESULT_INVALID" });
134
+ });
135
+
136
+ function guard(): RequestGuardContext {
137
+ const scope = buildScopeContext(
138
+ "actor-1",
139
+ undefined,
140
+ [
141
+ {
142
+ id: "analyst",
143
+ code: "analyst",
144
+ dataScope: "custom",
145
+ departmentIds: ["case-1", "case-2"],
146
+ },
147
+ ],
148
+ [{ scope: "custom", dimension: "caseId", values: ["case-1", "case-2"] }],
149
+ );
150
+ return {
151
+ requestId: "request-1",
152
+ actor: {
153
+ id: "actor-1",
154
+ type: "user",
155
+ name: "analyst",
156
+ roleCodes: ["analyst"],
157
+ permissionKeys: ["case.analysis"],
158
+ dataAccess: scope,
159
+ scopeContext: scope,
160
+ },
161
+ startedAt: "2026-08-28T00:00:00.000Z",
162
+ };
163
+ }
@@ -0,0 +1,161 @@
1
+ import type { DurableJobArtifact, DurableJobs } from "@southwind-ai/jobs";
2
+ import {
3
+ ModuleWorkflowInputMaterializationError,
4
+ moduleWorkflowInputMaterializationHandlerKey,
5
+ parseModuleWorkflowInputMaterializationBeginV1,
6
+ parseModuleWorkflowInputMaterializationPayloadV1,
7
+ type ModuleJobPort,
8
+ type ModuleWorkflowInputMaterializationPortV1,
9
+ type ModuleWorkflowInputMaterializationSourceV1,
10
+ } from "@southwind-ai/server-sdk";
11
+ import type { AgentWorkflowArtifactRef } from "@southwind-ai/shared";
12
+ import type { RequestGuardContext } from "../guards.js";
13
+ import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
14
+ import { authorizationScopeContains } from "../data-access/scope-containment.js";
15
+
16
+ export function createModuleWorkflowInputMaterializationPort(input: {
17
+ readonly moduleName: string;
18
+ readonly guardContext: RequestGuardContext;
19
+ readonly jobs: DurableJobs;
20
+ readonly moduleJobs: ModuleJobPort;
21
+ readonly reauthorize?: () => Promise<boolean>;
22
+ }): ModuleWorkflowInputMaterializationPortV1 {
23
+ const handlerKey = moduleWorkflowInputMaterializationHandlerKey(
24
+ input.moduleName,
25
+ );
26
+ return {
27
+ async begin(request) {
28
+ let parsed;
29
+ try {
30
+ parsed = parseModuleWorkflowInputMaterializationBeginV1(request);
31
+ } catch {
32
+ throw invalid();
33
+ }
34
+ const submitted = await input.moduleJobs.submit({
35
+ handlerKey,
36
+ idempotencyKey: parsed.idempotencyKey,
37
+ payload: {
38
+ schemaVersion: 1,
39
+ sources: parsed.sources,
40
+ },
41
+ });
42
+ return { materializationId: submitted.jobId };
43
+ },
44
+ async resolve(handle) {
45
+ const snapshot = await currentSnapshot(input, handle.materializationId);
46
+ if (["queued", "running"].includes(snapshot.job.status)) {
47
+ return { ...handle, status: "pending" };
48
+ }
49
+ if (snapshot.job.status !== "succeeded") throw failed();
50
+ const sources = payloadSources(snapshot.job.payload);
51
+ if (snapshot.artifacts.length !== sources.length) throw invalid();
52
+ const artifacts = sources.map((source) =>
53
+ projectArtifact(snapshot.artifacts, source, input),
54
+ );
55
+ if (
56
+ new Set(artifacts.map(({ artifactId }) => artifactId)).size !==
57
+ artifacts.length
58
+ ) {
59
+ throw invalid();
60
+ }
61
+ return { ...handle, status: "ready", artifacts };
62
+ },
63
+ async cancel(handle) {
64
+ const current = await input.moduleJobs.get(handle.materializationId);
65
+ if (!current || current.handlerKey !== handlerKey) throw unavailable();
66
+ if (current.status === "cancelled") return;
67
+ if (!["queued", "running"].includes(current.status)) throw unavailable();
68
+ await input.moduleJobs.cancel({
69
+ jobId: handle.materializationId,
70
+ reason: handle.reason,
71
+ expectedStatus: current.status,
72
+ expectedVersion: current.version,
73
+ });
74
+ },
75
+ };
76
+ }
77
+
78
+ async function currentSnapshot(
79
+ input: Parameters<typeof createModuleWorkflowInputMaterializationPort>[0],
80
+ jobId: string,
81
+ ) {
82
+ if (input.reauthorize && !(await input.reauthorize())) throw unavailable();
83
+ const snapshot = await input.jobs.get(jobId);
84
+ const current = captureAuthorizationScope(
85
+ input.guardContext.actor.scopeContext,
86
+ ).snapshot;
87
+ const job = snapshot?.job;
88
+ if (
89
+ !snapshot ||
90
+ !job?.authorizationScope ||
91
+ job.queue !== input.moduleName ||
92
+ job.handlerKey !==
93
+ moduleWorkflowInputMaterializationHandlerKey(input.moduleName) ||
94
+ job.requestedBy !== input.guardContext.actor.id ||
95
+ !authorizationScopeContains(current, job.authorizationScope)
96
+ ) {
97
+ throw unavailable();
98
+ }
99
+ return snapshot;
100
+ }
101
+
102
+ function payloadSources(payload: Record<string, unknown>) {
103
+ try {
104
+ return parseModuleWorkflowInputMaterializationPayloadV1(payload).sources;
105
+ } catch {
106
+ throw invalid();
107
+ }
108
+ }
109
+
110
+ function projectArtifact(
111
+ artifacts: readonly DurableJobArtifact[],
112
+ source: ModuleWorkflowInputMaterializationSourceV1,
113
+ input: { moduleName: string; guardContext: RequestGuardContext },
114
+ ): AgentWorkflowArtifactRef {
115
+ const artifact = artifacts.find(
116
+ ({ metadata }) => metadata?.publicationKey === source.publicationKey,
117
+ );
118
+ const metadata = artifact?.metadata;
119
+ if (
120
+ !artifact ||
121
+ artifact.ownerId !== input.guardContext.actor.id ||
122
+ metadata?.sourceModuleKey !== input.moduleName ||
123
+ metadata.sourceKind !== source.source.kind ||
124
+ metadata.sourceId !== source.source.id ||
125
+ metadata.sourceVersion !== source.source.version ||
126
+ metadata.sourceSequence !== source.source.sequence ||
127
+ metadata.sourceDigest !== source.source.digest ||
128
+ metadata.retentionPolicyKey !== source.retentionPolicyKey ||
129
+ artifact.kind !== source.kind
130
+ ) {
131
+ throw invalid();
132
+ }
133
+ return {
134
+ artifactId: artifact.id,
135
+ kind: artifact.kind,
136
+ mediaType: artifact.contentType,
137
+ byteSize: artifact.byteSize,
138
+ sha256: artifact.sha256,
139
+ retentionPolicyKey: source.retentionPolicyKey,
140
+ source: {
141
+ moduleKey: input.moduleName,
142
+ ...source.source,
143
+ },
144
+ };
145
+ }
146
+
147
+ function unavailable() {
148
+ return new ModuleWorkflowInputMaterializationError(
149
+ "MATERIALIZATION_UNAVAILABLE",
150
+ );
151
+ }
152
+
153
+ function failed() {
154
+ return new ModuleWorkflowInputMaterializationError("MATERIALIZATION_FAILED");
155
+ }
156
+
157
+ function invalid() {
158
+ return new ModuleWorkflowInputMaterializationError(
159
+ "MATERIALIZATION_RESULT_INVALID",
160
+ );
161
+ }
@@ -32,6 +32,7 @@ describePostgres("PostgreSQL 最小审批 Repository", () => {
32
32
  await pool.query(`truncate table
33
33
  governed_export_requests,
34
34
  bulk_data_artifacts, bulk_data_jobs, scheduled_task_runs,
35
+ client_agent_workflow_artifacts,
35
36
  durable_job_recoveries, durable_job_failures, durable_job_artifacts,
36
37
  durable_job_progress, durable_job_leases, durable_job_attempts,
37
38
  durable_job_stages, durable_jobs,
@@ -55,7 +55,8 @@ function searchActors() {
55
55
  :key="item.value"
56
56
  size="sm"
57
57
  class="h-8 px-3"
58
- :variant="range === item.value ? 'secondary' : 'ghost'"
58
+ :variant="range === item.value ? 'default' : 'ghost'"
59
+ :aria-pressed="range === item.value"
59
60
  :disabled="disabled"
60
61
  @click="emit('update:range', item.value)"
61
62
  >