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
@@ -1,31 +1,25 @@
1
- import { randomBytes, randomUUID } from "node:crypto";
2
1
  import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
2
  import type {
4
3
  ClientSessionAdmissionPort,
4
+ ClientSessionAttemptAuthorityChecker,
5
5
  ClientSessionAuditPort,
6
6
  ClientSessionCaller,
7
- ClientSessionFinalResult,
8
7
  ClientSessionRecord,
9
8
  } from "./contracts.js";
10
- import { digestJson, digestText, validDigest } from "./digest.js";
9
+ import { digestText } from "./digest.js";
11
10
  import type { ClientSessionRepository } from "./repository.js";
12
11
  import { clientAgentCapabilities } from "./capabilities.js";
13
12
  import { authorizeClientSession } from "./authorization.js";
14
13
  import { abortClientSession, auditRevokedSessions } from "./revocation.js";
15
- import {
16
- ensureTrailingSlash,
17
- publicLimits,
18
- sessionView,
19
- } from "./presentation.js";
20
- import {
21
- ClientAgentSessionError,
22
- clientSessionConflict as conflict,
23
- clientSessionInvalid as invalid,
24
- } from "./errors.js";
14
+ import { sessionView } from "./presentation.js";
15
+ import { clientSessionConflict as conflict } from "./errors.js";
25
16
  import {
26
17
  consumeClientAgentCompletion,
27
18
  runClientAgentCompletion,
28
19
  } from "./completion.js";
20
+ import { finalizeClientSession } from "./finalization.js";
21
+ import { createClientAgentSession } from "./creation.js";
22
+
29
23
  export class GovernedClientAgentSessionService {
30
24
  readonly #attemptControllers = new Map<string, AbortController>();
31
25
  readonly #recovery: Promise<number>;
@@ -35,6 +29,7 @@ export class GovernedClientAgentSessionService {
35
29
  private readonly provider: import("./contracts.js").ClientAgentProviderPort,
36
30
  private readonly audit: ClientSessionAuditPort,
37
31
  private readonly trustedBaseUrl: string,
32
+ private readonly attemptAuthority: ClientSessionAttemptAuthorityChecker,
38
33
  private readonly now: () => number = Date.now,
39
34
  private readonly maxTtlMs = 5 * 60_000,
40
35
  ) {
@@ -60,73 +55,44 @@ export class GovernedClientAgentSessionService {
60
55
  hostSessionDigest: `sha256:${string}`;
61
56
  actorId: string;
62
57
  }) {
63
- await this.#recovery;
64
- if (!(await this.provider.ready())) {
65
- throw new ClientAgentSessionError(
66
- "PROVIDER_FAILED",
67
- "Client Agent Provider 尚未就绪",
68
- 503,
69
- );
70
- }
71
- if (
72
- !validDigest(input.clientBindingDigest) ||
73
- !validDigest(input.hostSessionDigest) ||
74
- !input.actorId.trim()
75
- ) {
76
- throw invalid("Client Agent binding 或 Actor 无效");
77
- }
78
- const admission = await this.admission.admit({
79
- operationKey: input.operationKey,
80
- input: input.input,
81
- actorId: input.actorId,
82
- clientBindingDigest: input.clientBindingDigest,
83
- });
84
- if (
85
- admission.actorId !== input.actorId ||
86
- admission.operationKey !== input.operationKey ||
87
- admission.clientBindingDigest !== input.clientBindingDigest
88
- ) {
89
- throw new Error("Client Agent admission 返回了不一致的绑定");
90
- }
91
- const createdAt = this.now();
92
- const expiresAt =
93
- createdAt +
94
- Math.min(admission.limits.maxDurationSeconds * 1_000, this.maxTtlMs);
95
- const accessToken = `car_${randomBytes(32).toString("base64url")}`;
96
- const record: ClientSessionRecord = {
97
- ...admission,
98
- sessionId: `cas_${randomUUID()}`,
99
- clientRunId: `carun_${randomUUID()}`,
100
- tokenHash: digestText(accessToken),
101
- hostSessionDigest: input.hostSessionDigest,
102
- modelAlias: this.provider.modelAlias,
103
- createdAt,
104
- expiresAt,
105
- status: "active",
106
- attemptCount: 0,
107
- activeRequests: 0,
108
- pendingAttempts: [],
109
- receipts: [],
110
- };
111
- await this.repository.create(record);
112
- await this.recordAudit("create", record, "success", {
113
- inputCommitment: record.inputCommitment,
114
- authorizationDigest: record.authorizationDigest,
115
- bindingDigest: record.clientBindingDigest,
58
+ return this.createWithAuthority(input, "direct");
59
+ }
60
+
61
+ /** 仅供固定 Workflow Artifact service 使用;路由不得暴露。 */
62
+ async createWorkflow(input: {
63
+ operationKey: string;
64
+ input: ModuleAiJsonValue;
65
+ clientBindingDigest: string;
66
+ hostSessionDigest: `sha256:${string}`;
67
+ actorId: string;
68
+ }) {
69
+ return this.createWithAuthority(input, "workflow-artifact");
70
+ }
71
+
72
+ private async createWithAuthority(
73
+ input: {
74
+ operationKey: string;
75
+ input: ModuleAiJsonValue;
76
+ clientBindingDigest: string;
77
+ hostSessionDigest: `sha256:${string}`;
78
+ actorId: string;
79
+ },
80
+ launchAuthority: "direct" | "workflow-artifact",
81
+ ) {
82
+ return createClientAgentSession({
83
+ request: input,
84
+ launchAuthority,
85
+ recovery: this.#recovery,
86
+ provider: this.provider,
87
+ admission: this.admission,
88
+ repository: this.repository,
89
+ trustedBaseUrl: this.trustedBaseUrl,
90
+ now: this.now,
91
+ maxTtlMs: this.maxTtlMs,
92
+ audit: async (record, metadata) => {
93
+ await this.recordAudit("create", record, "success", metadata);
94
+ },
116
95
  });
117
- return {
118
- clientRunId: record.clientRunId,
119
- sessionId: record.sessionId,
120
- trustedBaseUrl: new URL(
121
- `${encodeURIComponent(record.sessionId)}/v1/`,
122
- ensureTrailingSlash(this.trustedBaseUrl),
123
- ).toString(),
124
- modelAlias: record.modelAlias,
125
- limits: publicLimits(record),
126
- expiresAt: new Date(record.expiresAt).toISOString(),
127
- accessToken,
128
- capabilities: ["pageAgentClientRun"] as const,
129
- };
130
96
  }
131
97
 
132
98
  async complete(
@@ -152,6 +118,7 @@ export class GovernedClientAgentSessionService {
152
118
  caller,
153
119
  signal,
154
120
  current: await this.authorize(sessionId, caller, true),
121
+ attemptAuthority: this.attemptAuthority,
155
122
  repository: this.repository,
156
123
  admission: this.admission,
157
124
  provider: this.provider,
@@ -172,66 +139,49 @@ export class GovernedClientAgentSessionService {
172
139
  ) {
173
140
  await this.#recovery;
174
141
  const record = await this.authorize(sessionId, caller, false);
175
- const terminalDigest = digestJson({
176
- receiptIds: [...receiptIds],
177
- ...(clientResult === undefined ? {} : { result: clientResult }),
142
+ return finalizeClientSession({
143
+ record,
144
+ repository: this.repository,
145
+ admission: this.admission,
146
+ receiptIds,
147
+ clientResult,
148
+ audit: (completed, metadata) =>
149
+ this.recordAudit("finalize", completed, "success", metadata),
178
150
  });
179
- if (record.status === "completed") {
180
- if (record.terminalDigest === terminalDigest) return sessionView(record);
181
- throw conflict("会话已完成,终态不可改写");
182
- }
183
- if (record.status !== "active" || record.activeRequests) {
184
- throw conflict("会话当前不能 finalize");
185
- }
186
- const receipts = [...record.receipts].sort((a, b) => a.attempt - b.attempt);
151
+ }
152
+
153
+ async finalizeWorkflow(
154
+ sessionId: string,
155
+ caller: Omit<ClientSessionCaller, "token">,
156
+ receiptIds: readonly string[],
157
+ clientResult: ModuleAiJsonValue | undefined,
158
+ canonicalizeResult: (
159
+ proposedResult: ModuleAiJsonValue,
160
+ ) => Promise<ModuleAiJsonValue>,
161
+ ) {
162
+ await this.#recovery;
163
+ const record = await this.repository.find(sessionId);
187
164
  if (
188
- !receipts.length ||
189
- receiptIds.length !== receipts.length ||
190
- receipts.some((receipt, index) => receipt.receiptId !== receiptIds[index])
165
+ !record ||
166
+ record.actorId !== caller.actorId ||
167
+ record.clientBindingDigest !== caller.clientBindingDigest ||
168
+ record.hostSessionDigest !== caller.hostSessionDigest ||
169
+ (record.status !== "completed" &&
170
+ (record.status !== "active" || record.expiresAt <= this.now())) ||
171
+ !(await this.admission.reauthorize(record, caller.actorId))
191
172
  ) {
192
- throw invalid("Finalize 必须提交本 Run 的完整终末 receipt 链");
173
+ throw conflict("Workflow Client Agent Session 绑定已失效");
193
174
  }
194
- const terminal = receipts.at(-1)!;
195
- const projected = terminal.canonicalResult;
196
- const result = projected ?? clientResult;
197
- if (result === undefined) throw invalid("Finalize 缺少可公开结果");
198
- await this.admission.validateResult(record.operationKey, result);
199
- const finalResult: ClientSessionFinalResult = {
200
- clientRunId: record.clientRunId,
201
- status: "completed",
202
- result,
203
- attestation: {
204
- source:
205
- projected === undefined ? "client-projected" : "provider-observed",
206
- terminalReceiptId: terminal.receiptId,
207
- providerAttested: projected !== undefined,
208
- clientProjected: projected === undefined,
209
- },
210
- };
211
- const completed = await this.repository.transition(
212
- sessionId,
213
- "active",
214
- "completed",
215
- {
216
- terminalDigest,
217
- finalResult,
218
- },
219
- );
220
- if (!completed) {
221
- const concurrent = await this.repository.find(sessionId);
222
- if (
223
- concurrent?.status === "completed" &&
224
- concurrent.terminalDigest === terminalDigest
225
- ) {
226
- return sessionView(concurrent);
227
- }
228
- throw conflict("Finalize 与其它终态操作冲突");
229
- }
230
- await this.recordAudit("finalize", completed, "success", {
231
- terminalReceiptId: terminal.receiptId,
232
- clientProjected: projected === undefined,
175
+ return finalizeClientSession({
176
+ record,
177
+ repository: this.repository,
178
+ admission: this.admission,
179
+ receiptIds,
180
+ clientResult,
181
+ canonicalizeResult,
182
+ audit: (completed, metadata) =>
183
+ this.recordAudit("finalize", completed, "success", metadata),
233
184
  });
234
- return sessionView(completed);
235
185
  }
236
186
 
237
187
  async cancel(
@@ -310,6 +260,26 @@ export class GovernedClientAgentSessionService {
310
260
  });
311
261
  }
312
262
 
263
+ /** Host-internal revocation for a fenced Workflow claim; no browser token is accepted. */
264
+ async revokeSession(sessionId: string, cause: string) {
265
+ await this.#recovery;
266
+ const record = await this.repository.find(sessionId);
267
+ if (!record || record.status === "revoked") return;
268
+ if (record.status !== "active") return;
269
+ abortClientSession(this.#attemptControllers, sessionId);
270
+ const revoked = await this.repository.transition(
271
+ sessionId,
272
+ "active",
273
+ "revoked",
274
+ { terminalDigest: digestText(`revoke\0${cause.slice(0, 120)}`) },
275
+ );
276
+ if (revoked) {
277
+ await this.recordAudit("revoke", revoked, "success", {
278
+ cause: cause.slice(0, 120),
279
+ });
280
+ }
281
+ }
282
+
313
283
  private async authorize(
314
284
  sessionId: string,
315
285
  caller: ClientSessionCaller,
@@ -0,0 +1,23 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { clientSessionTerminalDigest } from "./digest.js";
3
+
4
+ describe("Client Session terminal digest", () => {
5
+ test("canonical result key 顺序不改变 digest,receipt 顺序仍参与绑定", () => {
6
+ const left = clientSessionTerminalDigest(["receipt_1", "receipt_2"], {
7
+ answer: "done",
8
+ evidence: { b: 2, a: 1 },
9
+ });
10
+ const same = clientSessionTerminalDigest(["receipt_1", "receipt_2"], {
11
+ evidence: { a: 1, b: 2 },
12
+ answer: "done",
13
+ });
14
+ const reordered = clientSessionTerminalDigest(["receipt_2", "receipt_1"], {
15
+ answer: "done",
16
+ evidence: { a: 1, b: 2 },
17
+ });
18
+
19
+ expect(left).toBe(same);
20
+ expect(left).toMatch(/^sha256:[a-f0-9]{64}$/u);
21
+ expect(reordered).not.toBe(left);
22
+ });
23
+ });
@@ -0,0 +1,8 @@
1
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
2
+ import { DrizzleClientAgentWorkflowArtifactRepository } from "./drizzle-repository.js";
3
+
4
+ export function clientWorkflowArtifactActiveBlobReferences(
5
+ db: NodePgDatabase | undefined,
6
+ ) {
7
+ return db ? [new DrizzleClientAgentWorkflowArtifactRepository(db)] : [];
8
+ }
@@ -0,0 +1,60 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
+ import { artifactRef, type BlobStore } from "@southwind-ai/storage";
4
+ import { canonicalJson, digestJson } from "../client-session/digest.js";
5
+ import type {
6
+ ClientAgentWorkflowArtifactBinding,
7
+ ClientAgentWorkflowArtifactRecord,
8
+ } from "./contracts.js";
9
+ import { ClientAgentWorkflowArtifactError } from "./error.js";
10
+
11
+ export function clientWorkflowTerminalReceipt(
12
+ record: ClientAgentWorkflowArtifactRecord,
13
+ historyHeadDigest: string,
14
+ ) {
15
+ return `cawr_${digestJson({
16
+ workId: record.workId,
17
+ claimGeneration: record.claimGeneration,
18
+ historyHeadDigest,
19
+ }).slice("sha256:".length, 53)}`;
20
+ }
21
+
22
+ export async function storeClientWorkflowArtifact(input: {
23
+ blobs: BlobStore;
24
+ result: ModuleAiJsonValue;
25
+ binding: ClientAgentWorkflowArtifactBinding;
26
+ terminalReceiptDigest: `sha256:${string}`;
27
+ }) {
28
+ const bytes = new TextEncoder().encode(canonicalJson(input.result));
29
+ if (bytes.byteLength > input.binding.artifact.maxByteSize) {
30
+ throw new ClientAgentWorkflowArtifactError(
31
+ "ARTIFACT_TOO_LARGE",
32
+ "Client Agent Workflow 产物超过策略上限",
33
+ 413,
34
+ );
35
+ }
36
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
37
+ if (!input.blobs.putOnce) {
38
+ throw new ClientAgentWorkflowArtifactError(
39
+ "ARTIFACT_STORE_UNAVAILABLE",
40
+ "Client Agent Workflow 稳定产物存储未装配",
41
+ 503,
42
+ );
43
+ }
44
+ const stableRef = artifactRef(
45
+ `blob_${input.terminalReceiptDigest.slice("sha256:".length, 39)}`,
46
+ );
47
+ const stored = await input.blobs.putOnce({
48
+ ref: stableRef,
49
+ body: new Blob([bytes]).stream(),
50
+ expectedByteSize: bytes.byteLength,
51
+ expectedContentHash: { algorithm: "sha256", value: sha256 },
52
+ });
53
+ return {
54
+ artifactRef: stored.ref,
55
+ filename: input.binding.artifact.filename,
56
+ contentType: input.binding.artifact.contentType,
57
+ byteSize: stored.byteSize,
58
+ sha256,
59
+ };
60
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ agentWorkflowNodeRuns,
3
+ agentWorkflowRuns,
4
+ agentWorkflowSideEffectCommands,
5
+ } from "@southwind-ai/database";
6
+ import { eq } from "drizzle-orm";
7
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
8
+ import type { ClientAgentWorkflowArtifactRecord } from "./contracts.js";
9
+
10
+ export async function clientWorkflowAuthorityCurrent(
11
+ db: NodePgDatabase,
12
+ record: ClientAgentWorkflowArtifactRecord,
13
+ now: number,
14
+ ) {
15
+ const [command] = await db
16
+ .select()
17
+ .from(agentWorkflowSideEffectCommands)
18
+ .where(eq(agentWorkflowSideEffectCommands.commandId, record.commandId))
19
+ .for("update")
20
+ .limit(1);
21
+ if (!command) return false;
22
+ const [run] = await db
23
+ .select()
24
+ .from(agentWorkflowRuns)
25
+ .where(eq(agentWorkflowRuns.runId, record.runId))
26
+ .for("update")
27
+ .limit(1);
28
+ if (!run) return false;
29
+ const [nodeRun] = await db
30
+ .select()
31
+ .from(agentWorkflowNodeRuns)
32
+ .where(eq(agentWorkflowNodeRuns.nodeRunId, record.nodeRunId))
33
+ .for("update")
34
+ .limit(1);
35
+ return Boolean(
36
+ nodeRun &&
37
+ command.status === "dispatched" &&
38
+ command.runId === record.runId &&
39
+ command.nodeRunId === record.nodeRunId &&
40
+ command.effectKind === record.effectKind &&
41
+ command.adapterKind === "durable-job" &&
42
+ command.generation === record.effectGeneration &&
43
+ command.requestDigest === record.requestDigest &&
44
+ command.fenceTokenDigest === record.effectFenceTokenDigest &&
45
+ command.authorizationSnapshotDigest ===
46
+ record.authorizationSnapshotDigest &&
47
+ command.runPolicyDigest === record.runPolicyDigest &&
48
+ ["accepted", "running", "waiting"].includes(run.status) &&
49
+ !run.cancellationRequestedAt &&
50
+ run.deadlineAt > now &&
51
+ run.actorId === record.actorId &&
52
+ run.scopeDigest === record.scopeDigest &&
53
+ run.authorizationSnapshotDigest === record.authorizationSnapshotDigest &&
54
+ run.executionPolicyDigest === record.runPolicyDigest &&
55
+ nodeRun.runId === record.runId &&
56
+ nodeRun.nodeType === "job" &&
57
+ ["running", "waiting"].includes(nodeRun.status),
58
+ );
59
+ }
@@ -0,0 +1,189 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ CLIENT_AGENT_WORKFLOW_ARTIFACT_BINDING_LIMITS as limits,
4
+ type ModuleClientAgentWorkflowArtifactProjectBinding,
5
+ } from "@southwind-ai/server-sdk";
6
+ import {
7
+ clientWorkflowBindingCommitment,
8
+ ClientAgentWorkflowArtifactBindingRegistry,
9
+ } from "./binding-registry.js";
10
+
11
+ test("Composition 启动时拒绝超出 DB/DTO 边界与路径型文件名", () => {
12
+ for (const binding of [
13
+ value({ workflowKey: "w".repeat(limits.workflowKey + 1) }),
14
+ value({ nodeId: "n".repeat(limits.nodeId + 1) }),
15
+ value({ operationKey: "o".repeat(limits.operationKey + 1) }),
16
+ value({ releaseDigest: `sha256:${"d".repeat(65)}` }),
17
+ value({
18
+ terminalResultAuthority: {
19
+ ...value().terminalResultAuthority,
20
+ version: "v".repeat(limits.terminalAuthorityVersion + 1),
21
+ },
22
+ }),
23
+ value({
24
+ terminalResultAuthority: {
25
+ ...value().terminalResultAuthority,
26
+ digest: `sha256:${"e".repeat(65)}`,
27
+ },
28
+ }),
29
+ value({
30
+ artifact: {
31
+ ...value().artifact,
32
+ kind: "k".repeat(limits.artifactKind + 1),
33
+ },
34
+ }),
35
+ value({
36
+ artifact: {
37
+ ...value().artifact,
38
+ filename: `${"f".repeat(limits.artifactFilename)}x`,
39
+ },
40
+ }),
41
+ value({
42
+ artifact: {
43
+ ...value().artifact,
44
+ retentionPolicyKey: "r".repeat(limits.retentionPolicyKey + 1),
45
+ },
46
+ }),
47
+ value({
48
+ artifact: { ...value().artifact, maxByteSize: limits.maxByteSize + 1 },
49
+ }),
50
+ value({ artifact: { ...value().artifact, filename: "nested/out.json" } }),
51
+ value({ artifact: { ...value().artifact, filename: "nested\\out.json" } }),
52
+ value({ workflowKey: "workflow\0key" }),
53
+ value({ nodeId: "page-agent\0artifact" }),
54
+ value({ operationKey: "analysis\0page-agent" }),
55
+ value({ releaseDigest: `sha256:${"d".repeat(63)}\0` }),
56
+ value({
57
+ artifact: { ...value().artifact, kind: "analysis\0report" },
58
+ }),
59
+ invalidContentTypeBinding(),
60
+ value({
61
+ artifact: { ...value().artifact, filename: "analysis\0.json" },
62
+ }),
63
+ value({
64
+ artifact: {
65
+ ...value().artifact,
66
+ retentionPolicyKey: "analysis\0case-artifact",
67
+ },
68
+ }),
69
+ ]) {
70
+ expect(
71
+ () => new ClientAgentWorkflowArtifactBindingRegistry([binding]),
72
+ ).toThrow("CLIENT_AGENT_WORKFLOW_BINDING_INVALID");
73
+ }
74
+ });
75
+
76
+ function invalidContentTypeBinding(): ModuleClientAgentWorkflowArtifactProjectBinding {
77
+ const binding = value();
78
+ Object.defineProperty(binding.artifact, "contentType", {
79
+ value: "application/json\0",
80
+ enumerable: true,
81
+ });
82
+ return binding;
83
+ }
84
+
85
+ test("每个 Workflow 只允许一个固定 PageAgent Job binding", () => {
86
+ expect(
87
+ () =>
88
+ new ClientAgentWorkflowArtifactBindingRegistry([
89
+ value(),
90
+ value({ nodeId: "other-node" }),
91
+ ]),
92
+ ).toThrow("CLIENT_AGENT_WORKFLOW_BINDING_DUPLICATE");
93
+ });
94
+
95
+ test("binding commitment 只承诺 authority version 与 digest", () => {
96
+ const first = value();
97
+ const second = value({
98
+ terminalResultAuthority: {
99
+ ...first.terminalResultAuthority,
100
+ async canonicalize() {
101
+ return { changed: true };
102
+ },
103
+ },
104
+ clientTaskAuthority: {
105
+ version: "sharp.client-task.v1",
106
+ digest: `sha256:${"f".repeat(64)}`,
107
+ async build({ workflowInput }) {
108
+ return { taskKey: "sharp.full-review", input: workflowInput };
109
+ },
110
+ },
111
+ launchAuthority: {
112
+ version: "sharp.workflow-launch.v1",
113
+ digest: `sha256:${"a".repeat(64)}`,
114
+ async authorizeRelease() {},
115
+ async authorizeRun({ actorId }) {
116
+ return {
117
+ authorizationScope: {
118
+ tenantId: "single-tenant",
119
+ actorId,
120
+ grants: [{ scope: "self" }],
121
+ roleIds: ["role-analyst"],
122
+ },
123
+ };
124
+ },
125
+ },
126
+ });
127
+ expect(clientWorkflowBindingCommitment(first)).toEqual(
128
+ clientWorkflowBindingCommitment(second),
129
+ );
130
+ expect(clientWorkflowBindingCommitment(first)).not.toEqual(
131
+ clientWorkflowBindingCommitment(
132
+ value({
133
+ terminalResultAuthority: {
134
+ ...first.terminalResultAuthority,
135
+ version: "sharp.analysis-result.v2",
136
+ },
137
+ }),
138
+ ),
139
+ );
140
+ });
141
+
142
+ function value(
143
+ patch: Partial<ModuleClientAgentWorkflowArtifactProjectBinding> = {},
144
+ ): ModuleClientAgentWorkflowArtifactProjectBinding {
145
+ return {
146
+ workflowKey: "analysis.cross-case.full-review",
147
+ nodeId: "page-agent-artifact",
148
+ operationKey: "analysis.cross-case.page-agent",
149
+ releaseDigest: `sha256:${"d".repeat(64)}`,
150
+ scopeContainmentContractVersion: 1,
151
+ terminalResultAuthority: {
152
+ version: "sharp.analysis-result.v1",
153
+ digest: `sha256:${"e".repeat(64)}`,
154
+ async canonicalize({ proposedResult }) {
155
+ return proposedResult;
156
+ },
157
+ },
158
+ clientTaskAuthority: {
159
+ version: "sharp.client-task.v1",
160
+ digest: `sha256:${"f".repeat(64)}`,
161
+ async build({ workflowInput }) {
162
+ return { taskKey: "sharp.full-review", input: workflowInput };
163
+ },
164
+ },
165
+ launchAuthority: {
166
+ version: "sharp.workflow-launch.v1",
167
+ digest: `sha256:${"a".repeat(64)}`,
168
+ async authorizeRelease() {},
169
+ async authorizeRun({ actorId }) {
170
+ return {
171
+ authorizationScope: {
172
+ tenantId: "single-tenant",
173
+ actorId,
174
+ grants: [{ scope: "self" }],
175
+ roleIds: ["role-analyst"],
176
+ },
177
+ };
178
+ },
179
+ },
180
+ artifact: {
181
+ kind: "analysis-report",
182
+ contentType: "application/json",
183
+ filename: "analysis.json",
184
+ retentionPolicyKey: "analysis.case-artifact",
185
+ maxByteSize: 4 * 1024 * 1024,
186
+ },
187
+ ...patch,
188
+ };
189
+ }