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
@@ -2,24 +2,33 @@ import {
2
2
  clientAgentReceipts,
3
3
  clientAgentSessions,
4
4
  } from "@southwind-ai/database";
5
- import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
6
- import { and, asc, eq, sql } from "drizzle-orm";
5
+ import { and, eq, sql } from "drizzle-orm";
7
6
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
8
7
  import type {
9
- ClientSessionAdmission,
10
- ClientSessionFinalResult,
11
8
  ClientSessionReceiptRecord,
12
9
  ClientSessionRecord,
13
10
  ClientSessionStatus,
14
11
  } from "./contracts.js";
15
12
  import { receiptMatches, type ClientSessionRepository } from "./repository.js";
16
13
  import { sameSecret } from "./digest.js";
17
-
18
- type SessionRow = typeof clientAgentSessions.$inferSelect;
19
- type ReceiptRow = typeof clientAgentReceipts.$inferSelect;
14
+ import {
15
+ clientSessionAttemptAuthorityFacts,
16
+ type ClientSessionAttemptAuthorityBinding,
17
+ } from "./attempt-authority.js";
18
+ import {
19
+ clientSessionReceiptValues as receiptValues,
20
+ clientSessionValues as sessionValues,
21
+ findClientSessionRecord as findRecord,
22
+ hydrateClientSessionRecord as hydrateRecord,
23
+ mapClientSessionRecord as mapRecord,
24
+ } from "./drizzle-records.js";
20
25
 
21
26
  export class DrizzleClientSessionRepository implements ClientSessionRepository {
22
- constructor(private readonly db: NodePgDatabase) {}
27
+ constructor(
28
+ private readonly db: NodePgDatabase,
29
+ private readonly attemptAuthority?: ClientSessionAttemptAuthorityBinding,
30
+ private readonly now: () => number = Date.now,
31
+ ) {}
23
32
 
24
33
  async recoverInterrupted(now: number): Promise<number> {
25
34
  return this.db.transaction(async (transaction) => {
@@ -101,6 +110,12 @@ export class DrizzleClientSessionRepository implements ClientSessionRepository {
101
110
  ) {
102
111
  return undefined;
103
112
  }
113
+ const authority = await this.attemptAuthority?.checkTransaction(
114
+ db,
115
+ clientSessionAttemptAuthorityFacts(record),
116
+ this.now(),
117
+ );
118
+ if (authority === "revoked") return undefined;
104
119
  await db
105
120
  .insert(clientAgentReceipts)
106
121
  .values(receiptValues(sessionId, receipt));
@@ -219,127 +234,3 @@ export class DrizzleClientSessionRepository implements ClientSessionRepository {
219
234
  });
220
235
  }
221
236
  }
222
-
223
- async function findRecord(db: NodePgDatabase, sessionId: string) {
224
- const [row] = await db
225
- .select()
226
- .from(clientAgentSessions)
227
- .where(eq(clientAgentSessions.sessionId, sessionId))
228
- .limit(1);
229
- return row ? hydrateRecord(db, row) : undefined;
230
- }
231
-
232
- async function hydrateRecord(db: NodePgDatabase, row: SessionRow) {
233
- const receipts = await db
234
- .select()
235
- .from(clientAgentReceipts)
236
- .where(eq(clientAgentReceipts.sessionId, row.sessionId))
237
- .orderBy(asc(clientAgentReceipts.attempt));
238
- return mapRecord(row, receipts.map(mapReceipt));
239
- }
240
-
241
- function sessionValues(record: ClientSessionRecord) {
242
- const admission: ClientSessionAdmission = {
243
- actorId: record.actorId,
244
- operationKey: record.operationKey,
245
- releaseId: record.releaseId,
246
- inputCommitment: record.inputCommitment,
247
- authorizationDigest: record.authorizationDigest,
248
- clientBindingDigest: record.clientBindingDigest,
249
- deploymentKey: record.deploymentKey,
250
- providerDataPolicy: record.providerDataPolicy,
251
- limits: record.limits,
252
- };
253
- return {
254
- sessionId: record.sessionId,
255
- clientRunId: record.clientRunId,
256
- actorId: record.actorId,
257
- operationKey: record.operationKey,
258
- releaseId: record.releaseId,
259
- tokenHash: record.tokenHash,
260
- clientBindingDigest: record.clientBindingDigest,
261
- hostSessionDigest: record.hostSessionDigest,
262
- modelAlias: record.modelAlias,
263
- status: record.status,
264
- attemptCount: record.attemptCount,
265
- activeRequests: record.activeRequests,
266
- pendingAttempts: [...record.pendingAttempts],
267
- admissionJson: admission as unknown as Record<string, unknown>,
268
- terminalDigest: record.terminalDigest,
269
- finalResultJson: record.finalResult as Record<string, unknown> | undefined,
270
- createdAt: record.createdAt,
271
- expiresAt: record.expiresAt,
272
- };
273
- }
274
-
275
- function mapRecord(
276
- row: SessionRow,
277
- receipts: readonly ClientSessionReceiptRecord[],
278
- ): ClientSessionRecord {
279
- const admission = row.admissionJson as unknown as ClientSessionAdmission;
280
- return {
281
- ...admission,
282
- sessionId: row.sessionId,
283
- clientRunId: row.clientRunId,
284
- actorId: row.actorId,
285
- operationKey: row.operationKey,
286
- releaseId: row.releaseId,
287
- tokenHash: row.tokenHash as `sha256:${string}`,
288
- clientBindingDigest: row.clientBindingDigest as `sha256:${string}`,
289
- hostSessionDigest: row.hostSessionDigest as `sha256:${string}`,
290
- modelAlias: row.modelAlias,
291
- status: row.status as ClientSessionStatus,
292
- attemptCount: row.attemptCount,
293
- activeRequests: row.activeRequests,
294
- pendingAttempts: row.pendingAttempts,
295
- receipts,
296
- createdAt: row.createdAt,
297
- expiresAt: row.expiresAt,
298
- ...(row.terminalDigest
299
- ? { terminalDigest: row.terminalDigest as `sha256:${string}` }
300
- : {}),
301
- ...(row.finalResultJson
302
- ? {
303
- finalResult:
304
- row.finalResultJson as unknown as ClientSessionFinalResult,
305
- }
306
- : {}),
307
- };
308
- }
309
-
310
- function receiptValues(sessionId: string, receipt: ClientSessionReceiptRecord) {
311
- return {
312
- receiptId: receipt.receiptId,
313
- sessionId,
314
- clientRunId: receipt.clientRunId,
315
- actorId: receipt.actorId,
316
- operationKey: receipt.operationKey,
317
- releaseId: receipt.releaseId,
318
- clientBindingDigest: receipt.clientBindingDigest,
319
- attempt: receipt.attempt,
320
- requestDigest: receipt.requestDigest,
321
- responseDigest: receipt.responseDigest,
322
- deploymentDigest: receipt.deploymentDigest,
323
- canonicalResultJson: receipt.canonicalResult,
324
- issuedAt: receipt.issuedAt,
325
- };
326
- }
327
-
328
- function mapReceipt(row: ReceiptRow): ClientSessionReceiptRecord {
329
- return {
330
- receiptId: row.receiptId,
331
- clientRunId: row.clientRunId,
332
- actorId: row.actorId,
333
- operationKey: row.operationKey,
334
- releaseId: row.releaseId,
335
- clientBindingDigest: row.clientBindingDigest as `sha256:${string}`,
336
- attempt: row.attempt,
337
- requestDigest: row.requestDigest as `sha256:${string}`,
338
- responseDigest: row.responseDigest as `sha256:${string}`,
339
- deploymentDigest: row.deploymentDigest as `sha256:${string}`,
340
- issuedAt: row.issuedAt,
341
- ...(row.canonicalResultJson === null
342
- ? {}
343
- : { canonicalResult: row.canonicalResultJson as ModuleAiJsonValue }),
344
- };
345
- }
@@ -0,0 +1,99 @@
1
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
2
+ import type {
3
+ ClientSessionAdmissionPort,
4
+ ClientSessionFinalResult,
5
+ ClientSessionRecord,
6
+ } from "./contracts.js";
7
+ import { clientSessionTerminalDigest } from "./digest.js";
8
+ import {
9
+ clientSessionConflict as conflict,
10
+ clientSessionInvalid as invalid,
11
+ } from "./errors.js";
12
+ import { sessionView } from "./presentation.js";
13
+ import type { ClientSessionRepository } from "./repository.js";
14
+
15
+ export async function finalizeClientSession(input: {
16
+ record: ClientSessionRecord;
17
+ repository: ClientSessionRepository;
18
+ admission: ClientSessionAdmissionPort;
19
+ receiptIds: readonly string[];
20
+ clientResult?: ModuleAiJsonValue;
21
+ canonicalizeResult?: (
22
+ proposedResult: ModuleAiJsonValue,
23
+ ) => Promise<ModuleAiJsonValue>;
24
+ audit(
25
+ record: ClientSessionRecord,
26
+ metadata: Readonly<Record<string, string | number | boolean>>,
27
+ ): Promise<void> | void;
28
+ }) {
29
+ const terminalDigest = clientSessionTerminalDigest(
30
+ input.receiptIds,
31
+ input.clientResult,
32
+ );
33
+ if (input.record.status === "completed") {
34
+ if (input.record.terminalDigest === terminalDigest) {
35
+ return sessionView(input.record);
36
+ }
37
+ throw conflict("会话已完成,终态不可改写");
38
+ }
39
+ if (input.record.status !== "active" || input.record.activeRequests) {
40
+ throw conflict("会话当前不能 finalize");
41
+ }
42
+ const receipts = [...input.record.receipts].sort(
43
+ (left, right) => left.attempt - right.attempt,
44
+ );
45
+ if (
46
+ !receipts.length ||
47
+ input.receiptIds.length !== receipts.length ||
48
+ receipts.some(
49
+ (receipt, index) => receipt.receiptId !== input.receiptIds[index],
50
+ )
51
+ ) {
52
+ throw invalid("Finalize 必须提交本 Run 的完整终末 receipt 链");
53
+ }
54
+ const terminal = receipts.at(-1)!;
55
+ const projected = terminal.canonicalResult;
56
+ const proposedResult = projected ?? input.clientResult;
57
+ if (proposedResult === undefined) throw invalid("Finalize 缺少可公开结果");
58
+ await input.admission.validateResult(
59
+ input.record.operationKey,
60
+ proposedResult,
61
+ );
62
+ const result = input.canonicalizeResult
63
+ ? await input.canonicalizeResult(proposedResult)
64
+ : proposedResult;
65
+ await input.admission.validateResult(input.record.operationKey, result);
66
+ const finalResult: ClientSessionFinalResult = {
67
+ clientRunId: input.record.clientRunId,
68
+ status: "completed",
69
+ result,
70
+ attestation: {
71
+ source:
72
+ projected === undefined ? "client-projected" : "provider-observed",
73
+ terminalReceiptId: terminal.receiptId,
74
+ providerAttested: projected !== undefined,
75
+ clientProjected: projected === undefined,
76
+ },
77
+ };
78
+ const completed = await input.repository.transition(
79
+ input.record.sessionId,
80
+ "active",
81
+ "completed",
82
+ { terminalDigest, finalResult },
83
+ );
84
+ if (!completed) {
85
+ const concurrent = await input.repository.find(input.record.sessionId);
86
+ if (
87
+ concurrent?.status === "completed" &&
88
+ concurrent.terminalDigest === terminalDigest
89
+ ) {
90
+ return sessionView(concurrent);
91
+ }
92
+ throw conflict("Finalize 与其它终态操作冲突");
93
+ }
94
+ await input.audit(completed, {
95
+ terminalReceiptId: terminal.receiptId,
96
+ clientProjected: projected === undefined,
97
+ });
98
+ return sessionView(completed);
99
+ }
@@ -39,11 +39,22 @@ export function createClientAgentHostAuthRouteApp(input: {
39
39
  };
40
40
  if (!(await input.adapter.ready(context.request.signal)))
41
41
  throw unavailable();
42
- const principal = await input.adapter.authenticate({
43
- token: hostToken(context.request, path),
44
- purpose: isCompletion(path) ? "completion" : "control",
45
- signal: context.request.signal,
46
- });
42
+ const purpose = isCompletion(path) ? "completion" : "control";
43
+ const token = hostToken(context.request);
44
+ const principal = token
45
+ ? await input.adapter.authenticate({
46
+ token,
47
+ purpose,
48
+ signal: context.request.signal,
49
+ })
50
+ : context.guardContext.actor.type !== "anonymous"
51
+ ? await input.adapter.authenticateCurrentSession?.({
52
+ request: context.request,
53
+ actorId: context.guardContext.actor.id,
54
+ purpose,
55
+ signal: context.request.signal,
56
+ })
57
+ : undefined;
47
58
  const actor = principal
48
59
  ? await input.resolveActor(principal.actorId)
49
60
  : undefined;
@@ -84,12 +95,13 @@ export function createClientAgentHostAuthRouteApp(input: {
84
95
  };
85
96
  }
86
97
 
87
- function hostToken(request: Request, path: string): string {
88
- const header = path.includes(":sessionId")
89
- ? request.headers.get("x-windy-host-authorization")
90
- : request.headers.get("authorization");
98
+ function hostToken(request: Request): string | undefined {
99
+ const header =
100
+ request.headers.get("x-windy-host-authorization") ??
101
+ request.headers.get("authorization");
91
102
  const token = header?.match(/^Bearer\s+(.+)$/iu)?.[1];
92
- if (!token?.startsWith("bax_")) throw hidden();
103
+ if (!token || token.startsWith("car_")) return undefined;
104
+ if (!token.startsWith("bax_")) throw hidden();
93
105
  return token;
94
106
  }
95
107
 
@@ -0,0 +1,77 @@
1
+ import { expect, test } from "bun:test";
2
+ import type {
3
+ ModuleClientAgentHostAuthAdapter,
4
+ ModuleClientAgentHostPrincipal,
5
+ } from "@southwind-ai/server-sdk";
6
+ import { bindClientAgentHostRevocations } from "./host-revocation-composition.js";
7
+
8
+ test("单监听 Host Auth Adapter 通过 Composition fan-out 撤销 Session 与 Workflow", async () => {
9
+ let listener:
10
+ | ((principal: ModuleClientAgentHostPrincipal) => Promise<void> | void)
11
+ | undefined;
12
+ const adapter = {
13
+ ready: async () => true,
14
+ authenticate: async () => undefined,
15
+ bindRevocations(next) {
16
+ listener = next;
17
+ return () => {
18
+ listener = undefined;
19
+ };
20
+ },
21
+ } satisfies ModuleClientAgentHostAuthAdapter;
22
+ const calls: string[] = [];
23
+ const dispose = bindClientAgentHostRevocations({
24
+ adapter,
25
+ revokeSessionHost: async () => {
26
+ calls.push("session");
27
+ },
28
+ revokeWorkflowHost: async () => {
29
+ calls.push("workflow");
30
+ },
31
+ });
32
+
33
+ await listener?.({
34
+ actorId: "actor-1",
35
+ installationId: "installation-1",
36
+ sessionId: "host-session-1",
37
+ });
38
+ expect(calls).toEqual(["session", "workflow"]);
39
+ dispose?.();
40
+ expect(listener).toBeUndefined();
41
+ });
42
+
43
+ test("Session 撤销失败时仍尝试 Workflow 撤销并汇总失败", async () => {
44
+ let listener:
45
+ | ((principal: ModuleClientAgentHostPrincipal) => Promise<void> | void)
46
+ | undefined;
47
+ const adapter = {
48
+ ready: async () => true,
49
+ authenticate: async () => undefined,
50
+ bindRevocations(next) {
51
+ listener = next;
52
+ return () => {
53
+ listener = undefined;
54
+ };
55
+ },
56
+ } satisfies ModuleClientAgentHostAuthAdapter;
57
+ const calls: string[] = [];
58
+ bindClientAgentHostRevocations({
59
+ adapter,
60
+ revokeSessionHost: async () => {
61
+ calls.push("session");
62
+ throw new Error("session revoke failed");
63
+ },
64
+ revokeWorkflowHost: async () => {
65
+ calls.push("workflow");
66
+ },
67
+ });
68
+
69
+ await expect(
70
+ listener?.({
71
+ actorId: "actor-1",
72
+ installationId: "installation-1",
73
+ sessionId: "host-session-1",
74
+ }),
75
+ ).rejects.toBeInstanceOf(AggregateError);
76
+ expect(calls).toEqual(["session", "workflow"]);
77
+ });
@@ -0,0 +1,28 @@
1
+ import type {
2
+ ModuleClientAgentHostAuthAdapter,
3
+ ModuleClientAgentHostPrincipal,
4
+ } from "@southwind-ai/server-sdk";
5
+
6
+ export function bindClientAgentHostRevocations(input: {
7
+ adapter?: ModuleClientAgentHostAuthAdapter;
8
+ revokeSessionHost?(
9
+ principal: ModuleClientAgentHostPrincipal,
10
+ ): Promise<unknown>;
11
+ revokeWorkflowHost?(
12
+ principal: ModuleClientAgentHostPrincipal,
13
+ ): Promise<unknown>;
14
+ }) {
15
+ return input.adapter?.bindRevocations(async (principal) => {
16
+ const errors: unknown[] = [];
17
+ for (const revoke of [input.revokeSessionHost, input.revokeWorkflowHost]) {
18
+ try {
19
+ await revoke?.(principal);
20
+ } catch (error) {
21
+ errors.push(error);
22
+ }
23
+ }
24
+ if (errors.length) {
25
+ throw new AggregateError(errors, "Client Agent Host 撤销未完全收敛");
26
+ }
27
+ });
28
+ }
@@ -0,0 +1,21 @@
1
+ import { expect, test } from "bun:test";
2
+ import { createFixture } from "./service.test-support.js";
3
+ import { binding, hostSession } from "./service.test-support.js";
4
+
5
+ test("普通 Client Session create 不能启动 workflow-only Operation", async () => {
6
+ const fixture = await createFixture();
7
+ const input = {
8
+ operationKey: "analysis.page-agent",
9
+ input: { question: "绕过 Workflow" },
10
+ clientBindingDigest: binding,
11
+ hostSessionDigest: hostSession,
12
+ actorId: "actor-1",
13
+ } as const;
14
+
15
+ await expect(fixture.service.create(input)).rejects.toMatchObject({
16
+ code: "FORBIDDEN",
17
+ });
18
+ await expect(fixture.service.createWorkflow(input)).resolves.toMatchObject({
19
+ capabilities: ["pageAgentClientRun"],
20
+ });
21
+ });
@@ -63,8 +63,19 @@ export class ManifestClientSessionAdmission implements ClientSessionAdmissionPor
63
63
  input: ModuleAiJsonValue;
64
64
  clientBindingDigest: `sha256:${string}`;
65
65
  actorId: string;
66
+ launchAuthority: "direct" | "workflow-artifact";
66
67
  }) {
67
68
  const operation = this.operation(input.operationKey);
69
+ if (
70
+ (operation.definition.clientExecution!.launchAuthority ?? "direct") !==
71
+ input.launchAuthority
72
+ ) {
73
+ throw new ClientAgentSessionError(
74
+ "FORBIDDEN",
75
+ "Operation 不允许从当前入口启动 Client Agent Session",
76
+ 403,
77
+ );
78
+ }
68
79
  const context = await this.resolveContext(input.actorId);
69
80
  await this.authorize(operation, context, input.actorId);
70
81
  const inputBytes = new TextEncoder().encode(
@@ -2,6 +2,7 @@ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
2
2
  import type {
3
3
  ClientAgentProviderPort,
4
4
  ClientSessionAdmissionPort,
5
+ ClientSessionAttemptAuthorityChecker,
5
6
  ClientSessionAuditPort,
6
7
  ClientSessionCaller,
7
8
  ClientSessionRecord,
@@ -10,6 +11,7 @@ import { digestJson } from "./digest.js";
10
11
  import { ClientAgentSessionError } from "./errors.js";
11
12
  import { InMemoryClientSessionRepository } from "./repository.js";
12
13
  import { GovernedClientAgentSessionService } from "./service.js";
14
+ import { ClientSessionAttemptAuthorityBinding } from "./attempt-authority.js";
13
15
 
14
16
  export const binding = `sha256:${"b".repeat(64)}` as const;
15
17
  export const hostSession = `sha256:${"e".repeat(64)}` as const;
@@ -26,6 +28,7 @@ export async function createFixture(
26
28
  signal: AbortSignal,
27
29
  ) => Promise<ModuleAiJsonValue>;
28
30
  providerReady?: boolean;
31
+ attemptAuthority?: ClientSessionAttemptAuthorityChecker;
29
32
  } = {},
30
33
  ) {
31
34
  const clock = { value: options.now ?? 1_000 };
@@ -39,6 +42,10 @@ export async function createFixture(
39
42
  options.providerReady ?? true,
40
43
  );
41
44
  const audits: Parameters<ClientSessionAuditPort["record"]>[0][] = [];
45
+ const attemptAuthority = new ClientSessionAttemptAuthorityBinding();
46
+ if (options.attemptAuthority) {
47
+ attemptAuthority.bind(options.attemptAuthority);
48
+ }
42
49
  const service = new GovernedClientAgentSessionService(
43
50
  repository,
44
51
  admission,
@@ -49,6 +56,7 @@ export async function createFixture(
49
56
  },
50
57
  },
51
58
  "https://windy.example/api/agent/client-sessions/",
59
+ attemptAuthority,
52
60
  () => clock.value,
53
61
  options.maxTtlMs,
54
62
  );
@@ -70,6 +78,7 @@ export async function createFixture(
70
78
  repository,
71
79
  admission,
72
80
  provider,
81
+ attemptAuthority,
73
82
  audits,
74
83
  grant,
75
84
  caller,
@@ -90,7 +99,19 @@ class FakeAdmission implements ClientSessionAdmissionPort {
90
99
  input: ModuleAiJsonValue;
91
100
  clientBindingDigest: `sha256:${string}`;
92
101
  actorId: string;
102
+ launchAuthority: "direct" | "workflow-artifact";
93
103
  }) {
104
+ const expectedAuthority: "direct" | "workflow-artifact" =
105
+ input.operationKey === "analysis.page-agent"
106
+ ? "workflow-artifact"
107
+ : "direct";
108
+ if (input.launchAuthority !== expectedAuthority) {
109
+ throw new ClientAgentSessionError(
110
+ "FORBIDDEN",
111
+ "launch authority mismatch",
112
+ 403,
113
+ );
114
+ }
94
115
  return {
95
116
  actorId: input.actorId,
96
117
  operationKey: input.operationKey,
@@ -121,6 +142,7 @@ class FakeAdmission implements ClientSessionAdmissionPort {
121
142
  },
122
143
  limits: {
123
144
  mode: "governed-session" as const,
145
+ launchAuthority: expectedAuthority,
124
146
  maxSteps: this.maxSteps,
125
147
  maxRequestBytes: 16_384,
126
148
  maxResponseBytes: 16_384,