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
@@ -0,0 +1,85 @@
1
+ import {
2
+ CLIENT_AGENT_WORKFLOW_ARTIFACT_TASK_KEY,
3
+ type ModuleClientAgentWorkflowArtifactProjectBinding,
4
+ } from "@southwind-ai/server-sdk";
5
+ import type {
6
+ AgentWorkflowDefinition,
7
+ AgentWorkflowNode,
8
+ AgentWorkflowRelease,
9
+ } from "@southwind-ai/shared";
10
+
11
+ /** PageAgent 权限只属于显式审核的 Immutable Release 与固定三节点拓扑。 */
12
+ export function assertClientAgentWorkflowReleaseBinding(
13
+ release: AgentWorkflowRelease,
14
+ binding: ModuleClientAgentWorkflowArtifactProjectBinding,
15
+ ) {
16
+ const definition = release.definition;
17
+ const pageAgentJobs = definition.nodes.filter(
18
+ (node) =>
19
+ node.type === "job" &&
20
+ node.taskKey === CLIENT_AGENT_WORKFLOW_ARTIFACT_TASK_KEY,
21
+ );
22
+ const job = pageAgentJobs[0];
23
+ if (
24
+ release.releaseDigest !== binding.releaseDigest ||
25
+ definition.workflowKey !== binding.workflowKey ||
26
+ pageAgentJobs.length !== 1 ||
27
+ !job ||
28
+ job.id !== binding.nodeId ||
29
+ !failClosed(job)
30
+ ) {
31
+ invalid();
32
+ }
33
+ const artifact = uniqueSuccessor(definition, job, "artifact");
34
+ if (
35
+ artifact.artifactKind !== binding.artifact.kind ||
36
+ artifact.policyKey !== binding.artifact.retentionPolicyKey ||
37
+ !failClosed(artifact)
38
+ ) {
39
+ invalid();
40
+ }
41
+ const final = uniqueSuccessor(definition, artifact, "final");
42
+ if (!failClosed(final) || outgoing(definition, final.id).length) invalid();
43
+ }
44
+
45
+ function uniqueSuccessor<T extends "artifact" | "final">(
46
+ definition: AgentWorkflowDefinition,
47
+ source: AgentWorkflowNode,
48
+ type: T,
49
+ ): Extract<AgentWorkflowNode, { type: T }> {
50
+ const edges = outgoing(definition, source.id);
51
+ const edge = edges[0];
52
+ const node = definition.nodes.find(({ id }) => id === edge?.to);
53
+ const incoming = node
54
+ ? definition.edges.filter(({ to }) => to === node.id)
55
+ : [];
56
+ if (
57
+ edges.length !== 1 ||
58
+ !edge ||
59
+ edge.branch !== undefined ||
60
+ !node ||
61
+ node.type !== type ||
62
+ incoming.length !== 1 ||
63
+ incoming[0]?.from !== source.id ||
64
+ incoming[0]?.branch !== undefined
65
+ ) {
66
+ invalid();
67
+ }
68
+ return node as Extract<AgentWorkflowNode, { type: T }>;
69
+ }
70
+
71
+ function outgoing(definition: AgentWorkflowDefinition, nodeId: string) {
72
+ return definition.edges.filter(({ from }) => from === nodeId);
73
+ }
74
+
75
+ function failClosed(node: AgentWorkflowNode) {
76
+ return (
77
+ node.failurePolicy.mode === "fail-run" ||
78
+ (node.failurePolicy.mode === "retry" &&
79
+ node.failurePolicy.exhausted === "fail-run")
80
+ );
81
+ }
82
+
83
+ function invalid(): never {
84
+ throw new Error("CLIENT_AGENT_WORKFLOW_RELEASE_BINDING_INVALID");
85
+ }
@@ -0,0 +1,204 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ArtifactRef } from "@southwind-ai/storage";
3
+ import { InMemoryClientAgentWorkflowArtifactRepository } from "./in-memory-repository.js";
4
+ import type { ClientAgentWorkflowArtifactRecord } from "./contracts.js";
5
+
6
+ describe("Client Agent Workflow Artifact lifecycle", () => {
7
+ test("pending 先持久化,claim generation 使旧领取失效", async () => {
8
+ const repository = new InMemoryClientAgentWorkflowArtifactRepository();
9
+ const pending = await repository.begin(record());
10
+ expect(pending.status).toBe("pending");
11
+ const first = await repository.claim({
12
+ workId: pending.workId,
13
+ expectedVersion: pending.version,
14
+ binding: claim("sha256:first", 200),
15
+ });
16
+ expect(first.outcome).toBe("claimed");
17
+ if (first.outcome !== "claimed") throw new Error("claim failed");
18
+ const busy = await repository.claim({
19
+ workId: pending.workId,
20
+ expectedVersion: first.record.version,
21
+ binding: claim("sha256:busy", 199),
22
+ });
23
+ expect(busy.outcome).toBe("busy");
24
+ const expiredWithoutFence = await repository.claim({
25
+ workId: pending.workId,
26
+ expectedVersion: first.record.version,
27
+ binding: claim("sha256:second", 201),
28
+ });
29
+ expect(expiredWithoutFence.outcome).toBe("busy");
30
+ const takeover = await repository.claim({
31
+ workId: pending.workId,
32
+ expectedVersion: first.record.version,
33
+ allowActiveTakeover: true,
34
+ binding: claim("sha256:second", 201),
35
+ });
36
+ expect(takeover.outcome).toBe("claimed");
37
+ if (takeover.outcome !== "claimed") throw new Error("takeover failed");
38
+ expect(takeover.record.claimGeneration).toBe(2);
39
+ });
40
+
41
+ test("completed 与 cancelled 为吸收态,迟到写 fail closed", async () => {
42
+ const repository = new InMemoryClientAgentWorkflowArtifactRepository();
43
+ const pending = await repository.begin(record());
44
+ const claimed = await repository.claim({
45
+ workId: pending.workId,
46
+ expectedVersion: pending.version,
47
+ binding: claim("sha256:claim", 200),
48
+ });
49
+ if (claimed.outcome !== "claimed") throw new Error("claim failed");
50
+ const bound = await repository.bindClientSession({
51
+ workId: pending.workId,
52
+ claimGeneration: 1,
53
+ clientSessionId: "cas_1",
54
+ now: 101,
55
+ });
56
+ expect(bound?.clientSessionId).toBe("cas_1");
57
+ const completed = await repository.complete({
58
+ workId: pending.workId,
59
+ claimTokenHash: "sha256:claim",
60
+ terminal: {
61
+ claimGeneration: 1,
62
+ clientSessionId: "cas_1",
63
+ historySequence: 1,
64
+ historyHeadDigest: "sha256:history",
65
+ terminalReceiptId: "cawr_terminal",
66
+ terminalReceiptDigest: "sha256:terminal",
67
+ terminalRequestDigest: "sha256:request-terminal",
68
+ terminalResult: { result: "ok" },
69
+ artifact: {
70
+ artifactRef: "blob_11111111111111111111111111111111" as ArtifactRef,
71
+ filename: "result.json",
72
+ contentType: "application/json",
73
+ byteSize: 2,
74
+ sha256: "1".repeat(64),
75
+ },
76
+ now: 102,
77
+ },
78
+ });
79
+ expect(completed?.status).toBe("completed");
80
+ expect(
81
+ await repository.requestCancel({ workId: pending.workId, now: 103 }),
82
+ ).toBe(undefined);
83
+ expect(
84
+ await repository.complete({
85
+ workId: pending.workId,
86
+ claimTokenHash: "sha256:stale",
87
+ terminal: {
88
+ claimGeneration: 1,
89
+ clientSessionId: "cas_1",
90
+ historySequence: 1,
91
+ historyHeadDigest: "sha256:stale",
92
+ terminalReceiptId: "cawr_stale",
93
+ terminalReceiptDigest: "sha256:stale",
94
+ terminalRequestDigest: "sha256:stale-request",
95
+ terminalResult: { result: "late" },
96
+ artifact: completed!.artifact!,
97
+ now: 104,
98
+ },
99
+ }),
100
+ ).toBe(undefined);
101
+ });
102
+
103
+ test("取消先持久 intent,再进入吸收终态", async () => {
104
+ const repository = new InMemoryClientAgentWorkflowArtifactRepository();
105
+ const pending = await repository.begin(record());
106
+ const requested = await repository.requestCancel({
107
+ workId: pending.workId,
108
+ now: 101,
109
+ });
110
+ expect(requested?.status).toBe("cancellation-requested");
111
+ const cancelled = await repository.finalizeCancel({
112
+ workId: pending.workId,
113
+ now: 102,
114
+ });
115
+ expect(cancelled?.status).toBe("cancelled");
116
+ expect(
117
+ await repository.finalizeCancel({ workId: pending.workId, now: 103 }),
118
+ ).toEqual(cancelled);
119
+ });
120
+
121
+ test("attempt authority 绑定 Session、Actor、Operation、client 与 Host digest", async () => {
122
+ const repository = new InMemoryClientAgentWorkflowArtifactRepository();
123
+ const pending = await repository.begin(record());
124
+ const claimed = await repository.claim({
125
+ workId: pending.workId,
126
+ expectedVersion: pending.version,
127
+ binding: claim("sha256:claim", 101),
128
+ });
129
+ if (claimed.outcome !== "claimed") throw new Error("claim failed");
130
+ await repository.bindClientSession({
131
+ workId: pending.workId,
132
+ claimGeneration: claimed.record.claimGeneration,
133
+ clientSessionId: "cas_authority",
134
+ now: 102,
135
+ });
136
+ const facts = {
137
+ clientSessionId: "cas_authority",
138
+ actorId: "actor-1",
139
+ operationKey: "analysis.page-agent",
140
+ clientBindingDigest: "sha256:binding" as const,
141
+ hostSessionDigest: "sha256:host" as const,
142
+ now: 103,
143
+ };
144
+
145
+ await expect(repository.authorizeClientSessionAttempt(facts)).resolves.toBe(
146
+ "current",
147
+ );
148
+ await expect(
149
+ repository.authorizeClientSessionAttempt({
150
+ ...facts,
151
+ actorId: "actor-drift",
152
+ }),
153
+ ).resolves.toBe("revoked");
154
+ await expect(
155
+ repository.authorizeClientSessionAttempt({
156
+ ...facts,
157
+ clientSessionId: "cas_ordinary",
158
+ }),
159
+ ).resolves.toBe("ordinary");
160
+ await repository.requestCancel({ workId: pending.workId, now: 104 });
161
+ await expect(repository.authorizeClientSessionAttempt(facts)).resolves.toBe(
162
+ "revoked",
163
+ );
164
+ });
165
+ });
166
+
167
+ function record(): ClientAgentWorkflowArtifactRecord {
168
+ return {
169
+ workId: "cawa_work",
170
+ commandId: "command-1",
171
+ durableJobId: "job-1",
172
+ runId: "run-1",
173
+ nodeRunId: "node-run-1",
174
+ effectKind: "job",
175
+ taskKey: "system.agent-workflow.client-agent-artifact",
176
+ operationKey: "analysis.page-agent",
177
+ bindingDigest: "sha256:binding-policy",
178
+ actorId: "actor-1",
179
+ scopeDigest: "sha256:scope",
180
+ authorizationSnapshotDigest: "sha256:authorization",
181
+ runPolicyDigest: "sha256:policy",
182
+ requestDigest: "sha256:request",
183
+ inputDigest: "sha256:input",
184
+ effectGeneration: 1,
185
+ effectFenceTokenDigest: "sha256:fence",
186
+ status: "pending",
187
+ claimGeneration: 0,
188
+ historySequence: 0,
189
+ version: 1,
190
+ createdAt: 100,
191
+ updatedAt: 100,
192
+ };
193
+ }
194
+
195
+ function claim(token: `sha256:${string}`, now: number) {
196
+ return {
197
+ clientBindingDigest: "sha256:binding" as const,
198
+ installationId: "installation-1",
199
+ hostSessionDigest: "sha256:host" as const,
200
+ claimTokenHash: token,
201
+ claimExpiresAt: 200,
202
+ now,
203
+ };
204
+ }
@@ -0,0 +1,97 @@
1
+ import type {
2
+ ClientAgentWorkflowArtifactRecord,
3
+ ClientAgentWorkflowClaimBinding,
4
+ ClientAgentWorkflowTerminalInput,
5
+ } from "./contracts.js";
6
+ import type { ArtifactRef } from "@southwind-ai/storage";
7
+
8
+ export type ClientAgentWorkflowClaimResult =
9
+ | {
10
+ readonly outcome: "claimed";
11
+ readonly record: ClientAgentWorkflowArtifactRecord;
12
+ readonly previousClientSessionId?: string;
13
+ }
14
+ | {
15
+ readonly outcome: "busy" | "completed" | "cancelled" | "conflict";
16
+ readonly record?: ClientAgentWorkflowArtifactRecord;
17
+ };
18
+
19
+ export interface ClientAgentWorkflowArtifactRepository {
20
+ begin(
21
+ record: ClientAgentWorkflowArtifactRecord,
22
+ ): Promise<ClientAgentWorkflowArtifactRecord>;
23
+ find(workId: string): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
24
+ findByJob(
25
+ durableJobId: string,
26
+ ): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
27
+ authorizeClientSessionAttempt(input: {
28
+ clientSessionId: string;
29
+ actorId: string;
30
+ operationKey: string;
31
+ clientBindingDigest: `sha256:${string}`;
32
+ hostSessionDigest: `sha256:${string}`;
33
+ now: number;
34
+ }): Promise<"ordinary" | "current" | "revoked">;
35
+ findForActorRun(input: {
36
+ actorId: string;
37
+ runId: string;
38
+ }): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
39
+ claim(input: {
40
+ workId: string;
41
+ expectedVersion: number;
42
+ allowActiveTakeover?: boolean;
43
+ binding: ClientAgentWorkflowClaimBinding;
44
+ }): Promise<ClientAgentWorkflowClaimResult>;
45
+ bindClientSession(input: {
46
+ workId: string;
47
+ claimGeneration: number;
48
+ clientSessionId: string;
49
+ now: number;
50
+ }): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
51
+ complete(input: {
52
+ workId: string;
53
+ claimTokenHash: `sha256:${string}`;
54
+ terminal: ClientAgentWorkflowTerminalInput;
55
+ }): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
56
+ requestCancel(input: {
57
+ workId: string;
58
+ now: number;
59
+ }): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
60
+ finalizeCancel(input: {
61
+ workId: string;
62
+ now: number;
63
+ }): Promise<ClientAgentWorkflowArtifactRecord | undefined>;
64
+ listClaimedByHost(input: {
65
+ actorId: string;
66
+ hostSessionDigest: `sha256:${string}`;
67
+ }): Promise<readonly ClientAgentWorkflowArtifactRecord[]>;
68
+ listCancellationRequested(): Promise<
69
+ readonly ClientAgentWorkflowArtifactRecord[]
70
+ >;
71
+ listActiveArtifactRefs(now: Date): Promise<ArtifactRef[]>;
72
+ }
73
+
74
+ export function sameWorkflowArtifactIdentity(
75
+ left: ClientAgentWorkflowArtifactRecord,
76
+ right: ClientAgentWorkflowArtifactRecord,
77
+ ) {
78
+ return (
79
+ left.workId === right.workId &&
80
+ left.commandId === right.commandId &&
81
+ left.durableJobId === right.durableJobId &&
82
+ left.runId === right.runId &&
83
+ left.nodeRunId === right.nodeRunId &&
84
+ left.effectKind === right.effectKind &&
85
+ left.taskKey === right.taskKey &&
86
+ left.operationKey === right.operationKey &&
87
+ left.bindingDigest === right.bindingDigest &&
88
+ left.actorId === right.actorId &&
89
+ left.scopeDigest === right.scopeDigest &&
90
+ left.authorizationSnapshotDigest === right.authorizationSnapshotDigest &&
91
+ left.runPolicyDigest === right.runPolicyDigest &&
92
+ left.requestDigest === right.requestDigest &&
93
+ left.inputDigest === right.inputDigest &&
94
+ left.effectGeneration === right.effectGeneration &&
95
+ left.effectFenceTokenDigest === right.effectFenceTokenDigest
96
+ );
97
+ }
@@ -0,0 +1,201 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ DurableHandlerRegistry,
4
+ type DurableHandlerContext,
5
+ } from "@southwind-ai/jobs";
6
+ import { artifactRef } from "@southwind-ai/storage";
7
+ import type { RouteApp } from "../../system/routes.js";
8
+ import { hostSessionDigest } from "../client-session/host-auth.js";
9
+ import { registerClientAgentWorkflowArtifactHandler } from "./handler.js";
10
+ import { registerClientAgentWorkflowArtifactRoutes } from "./routes.js";
11
+ import type { GovernedClientAgentWorkflowArtifactService } from "./service.js";
12
+ import {
13
+ clientBindingDigest,
14
+ guardContext,
15
+ principal,
16
+ workflowBinding,
17
+ workflowInput,
18
+ workflowRecord,
19
+ } from "./workflow-artifact.test-support.js";
20
+
21
+ describe("Governed Client Agent Workflow Artifact routes", () => {
22
+ test("Route 拒绝 Provider 坐标注入,且只把 canonical Host principal 交给 Service", async () => {
23
+ const calls: unknown[] = [];
24
+ const service = {
25
+ claim: async (input: unknown) => {
26
+ calls.push(input);
27
+ return { workId: "cawa_work-1", status: "running", claimVersion: 1 };
28
+ },
29
+ } as unknown as GovernedClientAgentWorkflowArtifactService;
30
+ const app = captureRoutes();
31
+ registerClientAgentWorkflowArtifactRoutes(app, service);
32
+ const handler = app.handlers.get("/agent/client-workflow-artifacts/claim")!;
33
+ const context = {
34
+ request: new Request("https://windy.example/claim", { method: "POST" }),
35
+ guardContext,
36
+ clientAgentHost: {
37
+ principal,
38
+ hostSessionDigest: hostSessionDigest(principal),
39
+ },
40
+ };
41
+
42
+ const rejected = await handler({
43
+ ...context,
44
+ body: {
45
+ workflowRunId: "run-1",
46
+ clientBindingDigest,
47
+ providerUrl: "https://attacker.example/v1",
48
+ },
49
+ });
50
+ expect(rejected).toBeInstanceOf(Response);
51
+ expect((rejected as Response).status).toBe(400);
52
+ expect(calls).toHaveLength(0);
53
+
54
+ await expect(
55
+ handler({
56
+ ...context,
57
+ body: { workflowRunId: "run-1", clientBindingDigest },
58
+ }),
59
+ ).resolves.toMatchObject({ status: "running", claimVersion: 1 });
60
+ expect(calls).toHaveLength(1);
61
+ expect(calls[0]).toMatchObject({
62
+ workflowRunId: "run-1",
63
+ clientBindingDigest,
64
+ principal,
65
+ guardContext,
66
+ });
67
+ });
68
+ });
69
+
70
+ describe("Governed Client Agent Workflow Artifact durable publication", () => {
71
+ test("completed retry 与 Worker 重启只按 terminal receipt publishArtifactOnce", async () => {
72
+ const { handler, beginCalls } = completedHandler();
73
+ const publications = new Map<
74
+ string,
75
+ { readonly artifactRef: string; readonly sha256: string }
76
+ >();
77
+ let publishOnceCalls = 0;
78
+ const context = durableContext({
79
+ publishArtifactOnce: async (publicationKey, artifact) => {
80
+ publishOnceCalls++;
81
+ const existing = publications.get(publicationKey);
82
+ const value = {
83
+ artifactRef: artifact.artifactRef,
84
+ sha256: artifact.sha256,
85
+ };
86
+ if (existing && JSON.stringify(existing) !== JSON.stringify(value)) {
87
+ throw new Error("publication identity conflict");
88
+ }
89
+ publications.set(publicationKey, existing ?? value);
90
+ return {
91
+ schemaVersion: 1,
92
+ publicationKey,
93
+ artifactId: "artifact-stable-1",
94
+ jobId: context.jobId,
95
+ stageKey: context.stageKey,
96
+ kind: artifact.kind,
97
+ artifactRef: artifact.artifactRef,
98
+ sha256: artifact.sha256,
99
+ };
100
+ },
101
+ });
102
+
103
+ await handler.execute(jobPayload(), context);
104
+ await handler.execute(jobPayload(), context);
105
+
106
+ expect(beginCalls.value).toBe(2);
107
+ expect(publishOnceCalls).toBe(2);
108
+ expect(publications.size).toBe(1);
109
+ expect([...publications.keys()]).toEqual(["cawr_terminal-1"]);
110
+ });
111
+
112
+ test("stable publication 未装配时 fail closed,不回退到可重放 publishArtifact", async () => {
113
+ const { handler } = completedHandler();
114
+ let fallbackCalls = 0;
115
+ const context = durableContext({
116
+ publishArtifact: async () => {
117
+ fallbackCalls++;
118
+ },
119
+ });
120
+
121
+ await expect(handler.execute(jobPayload(), context)).rejects.toMatchObject({
122
+ code: "STABLE_ARTIFACT_PUBLICATION_UNAVAILABLE",
123
+ retryable: false,
124
+ });
125
+ expect(fallbackCalls).toBe(0);
126
+ });
127
+ });
128
+
129
+ function completedHandler() {
130
+ const beginCalls = { value: 0 };
131
+ const record = workflowRecord({
132
+ status: "completed",
133
+ claimGeneration: 1,
134
+ historySequence: 1,
135
+ historyHeadDigest: "sha256:history-1",
136
+ terminalReceiptId: "cawr_terminal-1",
137
+ terminalReceiptDigest: "sha256:terminal-1",
138
+ terminalResult: { result: { answer: "完成" } },
139
+ artifact: {
140
+ artifactRef: artifactRef(`blob_${"a".repeat(32)}`),
141
+ filename: workflowBinding.artifact.filename,
142
+ contentType: workflowBinding.artifact.contentType,
143
+ byteSize: 18,
144
+ sha256: "a".repeat(64),
145
+ },
146
+ finishedAt: 1_100,
147
+ });
148
+ const service = {
149
+ begin: async () => {
150
+ beginCalls.value++;
151
+ return { record, input: workflowInput, binding: workflowBinding };
152
+ },
153
+ } as unknown as GovernedClientAgentWorkflowArtifactService;
154
+ const registry = new DurableHandlerRegistry();
155
+ registerClientAgentWorkflowArtifactHandler({ registry, service });
156
+ const handler = registry.get(record.taskKey);
157
+ if (!handler) throw new Error("handler registration failed");
158
+ return { handler, beginCalls };
159
+ }
160
+
161
+ function jobPayload() {
162
+ const record = workflowRecord();
163
+ return {
164
+ commandId: record.commandId,
165
+ runId: record.runId,
166
+ nodeRunId: record.nodeRunId,
167
+ effectGeneration: record.effectGeneration,
168
+ requestDigest: record.requestDigest,
169
+ };
170
+ }
171
+
172
+ function durableContext(
173
+ patch: Partial<DurableHandlerContext>,
174
+ ): DurableHandlerContext {
175
+ return {
176
+ jobId: "job-1",
177
+ stageKey: "main",
178
+ attemptNumber: 1,
179
+ signal: new AbortController().signal,
180
+ heartbeat: async () => undefined,
181
+ progress: async () => undefined,
182
+ publishArtifact: async () => {
183
+ throw new Error("unstable publication must not be used");
184
+ },
185
+ ...patch,
186
+ };
187
+ }
188
+
189
+ function captureRoutes() {
190
+ const handlers = new Map<string, (context: unknown) => Promise<unknown>>();
191
+ const app = {
192
+ handlers,
193
+ get: () => undefined,
194
+ post: (path: string, handler: (context: unknown) => unknown) => {
195
+ handlers.set(path, async (context) => handler(context));
196
+ },
197
+ put: () => undefined,
198
+ delete: () => undefined,
199
+ } as unknown as RouteApp & { readonly handlers: typeof handlers };
200
+ return app;
201
+ }