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
@@ -3,6 +3,7 @@ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
3
  import type {
4
4
  ClientAgentProviderPort,
5
5
  ClientSessionAdmissionPort,
6
+ ClientSessionAttemptAuthorityChecker,
6
7
  ClientSessionCaller,
7
8
  ClientSessionRecord,
8
9
  ClientSessionReceiptRecord,
@@ -21,6 +22,11 @@ import {
21
22
  parseClientAgentCompletion,
22
23
  } from "./request.js";
23
24
  import { ProviderInputGovernanceError } from "../provider-input-governance.js";
25
+ import {
26
+ enforceClientSessionAttemptAuthority,
27
+ enforceClientSessionProviderBoundary,
28
+ type ClientSessionAttemptRevocationCause,
29
+ } from "./attempt-authority.js";
24
30
 
25
31
  export type ClientAgentCompletionEvent =
26
32
  | { readonly type: "chunk"; readonly value: ModuleAiJsonValue }
@@ -45,6 +51,7 @@ export async function runClientAgentCompletion(input: {
45
51
  caller: ClientSessionCaller;
46
52
  signal?: AbortSignal;
47
53
  current: ClientSessionRecord;
54
+ attemptAuthority: ClientSessionAttemptAuthorityChecker;
48
55
  repository: ClientSessionRepository;
49
56
  admission: ClientSessionAdmissionPort;
50
57
  provider: ClientAgentProviderPort;
@@ -57,10 +64,18 @@ export async function runClientAgentCompletion(input: {
57
64
  ) => Promise<void> | void;
58
65
  revokeAudit: (
59
66
  record: ClientSessionRecord,
60
- cause: "authorization-drift" | "ttl",
67
+ cause: ClientSessionAttemptRevocationCause,
61
68
  ) => Promise<void> | void;
62
69
  }) {
63
70
  const parsed = parseCompletion(input.body, input.current);
71
+ const workflowOwned = await enforceClientSessionAttemptAuthority({
72
+ checker: input.attemptAuthority,
73
+ record: input.current,
74
+ repository: input.repository,
75
+ ...(input.signal ? { signal: input.signal } : {}),
76
+ workflowOwned: false,
77
+ audit: input.revokeAudit,
78
+ });
64
79
  const reserved = await input.repository.reserveAttempt({
65
80
  sessionId: input.sessionId,
66
81
  tokenHash: digestText(input.caller.token),
@@ -83,24 +98,16 @@ export async function runClientAgentCompletion(input: {
83
98
  if (checking || controller.signal.aborted) return;
84
99
  checking = true;
85
100
  try {
86
- const cause =
87
- input.current.expiresAt <= input.now()
88
- ? "ttl"
89
- : (await input.admission.reauthorize(
90
- input.current,
91
- input.caller.actorId,
92
- ))
93
- ? undefined
94
- : "authorization-drift";
95
- if (!cause) return;
96
- const status = cause === "ttl" ? "expired" : "revoked";
97
- const closed = await input.repository.transition(
98
- input.sessionId,
99
- "active",
100
- status,
101
- );
102
- if (closed) await input.revokeAudit(closed, cause);
103
- controller.abort(cause);
101
+ await enforceClientSessionProviderBoundary({
102
+ admission: input.admission,
103
+ checker: input.attemptAuthority,
104
+ record: reserved,
105
+ repository: input.repository,
106
+ now: input.now(),
107
+ signal: controller.signal,
108
+ workflowOwned,
109
+ audit: input.revokeAudit,
110
+ });
104
111
  } catch {
105
112
  controller.abort("authorization-check-failed");
106
113
  } finally {
@@ -112,6 +119,7 @@ export async function runClientAgentCompletion(input: {
112
119
  abort: () => controller.abort(),
113
120
  events: completionEvents({
114
121
  ...input,
122
+ workflowOwned,
115
123
  parsed,
116
124
  reserved,
117
125
  attempt,
@@ -135,6 +143,8 @@ async function* completionEvents(input: {
135
143
  watcher: ReturnType<typeof setInterval>;
136
144
  repository: ClientSessionRepository;
137
145
  admission: ClientSessionAdmissionPort;
146
+ attemptAuthority: ClientSessionAttemptAuthorityChecker;
147
+ workflowOwned: boolean;
138
148
  provider: ClientAgentProviderPort;
139
149
  controllers: Map<string, AbortController>;
140
150
  now: () => number;
@@ -143,6 +153,10 @@ async function* completionEvents(input: {
143
153
  outcome: "success" | "failure",
144
154
  metadata: Readonly<Record<string, string | number | boolean>>,
145
155
  ) => Promise<void> | void;
156
+ revokeAudit: (
157
+ record: ClientSessionRecord,
158
+ cause: ClientSessionAttemptRevocationCause,
159
+ ) => Promise<void> | void;
146
160
  }): AsyncGenerator<ClientAgentCompletionEvent> {
147
161
  let settled = false;
148
162
  let audited = false;
@@ -151,6 +165,17 @@ async function* completionEvents(input: {
151
165
  let deploymentDigest: `sha256:${string}` | undefined;
152
166
  let responseBytes = 0;
153
167
  let usage: Readonly<Record<string, number>> = {};
168
+ await enforceClientSessionProviderBoundary({
169
+ admission: input.admission,
170
+ checker: input.attemptAuthority,
171
+ record: input.reserved,
172
+ repository: input.repository,
173
+ attempt: input.attempt,
174
+ now: input.now(),
175
+ signal: input.controller.signal,
176
+ workflowOwned: input.workflowOwned,
177
+ audit: input.revokeAudit,
178
+ });
154
179
  for await (const event of input.provider.stream(
155
180
  input.parsed.request,
156
181
  {
@@ -187,6 +212,21 @@ async function* completionEvents(input: {
187
212
  }
188
213
  if (!deploymentDigest) throw new Error("Provider meta 缺失");
189
214
  const response = accumulator.result();
215
+ await enforceClientSessionProviderBoundary({
216
+ admission: input.admission,
217
+ checker: input.attemptAuthority,
218
+ record: input.reserved,
219
+ repository: input.repository,
220
+ attempt: input.attempt,
221
+ now: input.now(),
222
+ signal: input.controller.signal,
223
+ workflowOwned: input.workflowOwned,
224
+ audit: input.revokeAudit,
225
+ });
226
+ const canonicalResult = await input.admission.projectProviderResult(
227
+ input.reserved.operationKey,
228
+ response,
229
+ );
190
230
  const receipt: ClientSessionReceiptRecord = {
191
231
  receiptId: `receipt_${randomUUID()}`,
192
232
  clientRunId: input.reserved.clientRunId,
@@ -199,12 +239,23 @@ async function* completionEvents(input: {
199
239
  responseDigest: digestJson(response),
200
240
  deploymentDigest,
201
241
  issuedAt: input.now(),
202
- canonicalResult: await input.admission.projectProviderResult(
203
- input.reserved.operationKey,
204
- response,
205
- ),
242
+ canonicalResult,
206
243
  };
207
- if (!(await input.repository.completeAttempt(input.sessionId, receipt))) {
244
+ const committed = await input.repository.completeAttempt(
245
+ input.sessionId,
246
+ receipt,
247
+ );
248
+ if (!committed) {
249
+ if (input.workflowOwned) {
250
+ await enforceClientSessionAttemptAuthority({
251
+ checker: input.attemptAuthority,
252
+ record: input.reserved,
253
+ repository: input.repository,
254
+ signal: input.controller.signal,
255
+ workflowOwned: true,
256
+ audit: input.revokeAudit,
257
+ });
258
+ }
208
259
  throw clientSessionConflict("模型响应到达时会话已关闭");
209
260
  }
210
261
  settled = true;
@@ -76,6 +76,28 @@ export interface ClientSessionCaller {
76
76
  readonly hostSessionDigest: `sha256:${string}`;
77
77
  }
78
78
 
79
+ export type ClientSessionAttemptAuthorityDecision =
80
+ | "ordinary"
81
+ | "current"
82
+ | "revoked";
83
+
84
+ export interface ClientSessionAttemptAuthorityFacts {
85
+ readonly sessionId: string;
86
+ readonly clientRunId: string;
87
+ readonly actorId: string;
88
+ readonly operationKey: string;
89
+ readonly clientBindingDigest: `sha256:${string}`;
90
+ readonly hostSessionDigest: `sha256:${string}`;
91
+ }
92
+
93
+ /** Host checker 必须从 canonical Workflow/command/fence 事实判定,不接受浏览器声明。 */
94
+ export interface ClientSessionAttemptAuthorityChecker {
95
+ check(
96
+ facts: ClientSessionAttemptAuthorityFacts,
97
+ signal?: AbortSignal,
98
+ ): Promise<ClientSessionAttemptAuthorityDecision>;
99
+ }
100
+
79
101
  export interface ClientAgentProviderPort {
80
102
  readonly modelAlias: string;
81
103
  ready(signal?: AbortSignal): Promise<boolean>;
@@ -105,6 +127,7 @@ export interface ClientSessionAdmissionPort {
105
127
  readonly input: ModuleAiJsonValue;
106
128
  readonly clientBindingDigest: `sha256:${string}`;
107
129
  readonly actorId: string;
130
+ readonly launchAuthority: "direct" | "workflow-artifact";
108
131
  }): Promise<ClientSessionAdmission>;
109
132
  reauthorize(session: ClientSessionRecord, actorId: string): Promise<boolean>;
110
133
  validateResult(
@@ -0,0 +1,103 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
+ import type {
4
+ ClientAgentProviderPort,
5
+ ClientSessionAdmissionPort,
6
+ ClientSessionRecord,
7
+ } from "./contracts.js";
8
+ import { digestText, validDigest } from "./digest.js";
9
+ import { ClientAgentSessionError, clientSessionInvalid } from "./errors.js";
10
+ import { ensureTrailingSlash, publicLimits } from "./presentation.js";
11
+ import type { ClientSessionRepository } from "./repository.js";
12
+
13
+ export async function createClientAgentSession(input: {
14
+ readonly request: {
15
+ operationKey: string;
16
+ input: ModuleAiJsonValue;
17
+ clientBindingDigest: string;
18
+ hostSessionDigest: `sha256:${string}`;
19
+ actorId: string;
20
+ };
21
+ readonly launchAuthority: "direct" | "workflow-artifact";
22
+ readonly recovery: Promise<number>;
23
+ readonly provider: ClientAgentProviderPort;
24
+ readonly admission: ClientSessionAdmissionPort;
25
+ readonly repository: ClientSessionRepository;
26
+ readonly trustedBaseUrl: string;
27
+ readonly now: () => number;
28
+ readonly maxTtlMs: number;
29
+ readonly audit: (
30
+ record: ClientSessionRecord,
31
+ metadata: Readonly<Record<string, string>>,
32
+ ) => Promise<void>;
33
+ }) {
34
+ await input.recovery;
35
+ if (!(await input.provider.ready())) {
36
+ throw new ClientAgentSessionError(
37
+ "PROVIDER_FAILED",
38
+ "Client Agent Provider 尚未就绪",
39
+ 503,
40
+ );
41
+ }
42
+ const request = input.request;
43
+ if (
44
+ !validDigest(request.clientBindingDigest) ||
45
+ !validDigest(request.hostSessionDigest) ||
46
+ !request.actorId.trim()
47
+ ) {
48
+ throw clientSessionInvalid("Client Agent binding 或 Actor 无效");
49
+ }
50
+ const admission = await input.admission.admit({
51
+ operationKey: request.operationKey,
52
+ input: request.input,
53
+ actorId: request.actorId,
54
+ clientBindingDigest: request.clientBindingDigest,
55
+ launchAuthority: input.launchAuthority,
56
+ });
57
+ if (
58
+ admission.actorId !== request.actorId ||
59
+ admission.operationKey !== request.operationKey ||
60
+ admission.clientBindingDigest !== request.clientBindingDigest
61
+ ) {
62
+ throw new Error("Client Agent admission 返回了不一致的绑定");
63
+ }
64
+ const createdAt = input.now();
65
+ const expiresAt =
66
+ createdAt +
67
+ Math.min(admission.limits.maxDurationSeconds * 1_000, input.maxTtlMs);
68
+ const accessToken = `car_${randomBytes(32).toString("base64url")}`;
69
+ const record: ClientSessionRecord = {
70
+ ...admission,
71
+ sessionId: `cas_${randomUUID()}`,
72
+ clientRunId: `carun_${randomUUID()}`,
73
+ tokenHash: digestText(accessToken),
74
+ hostSessionDigest: request.hostSessionDigest,
75
+ modelAlias: input.provider.modelAlias,
76
+ createdAt,
77
+ expiresAt,
78
+ status: "active",
79
+ attemptCount: 0,
80
+ activeRequests: 0,
81
+ pendingAttempts: [],
82
+ receipts: [],
83
+ };
84
+ await input.repository.create(record);
85
+ await input.audit(record, {
86
+ inputCommitment: record.inputCommitment,
87
+ authorizationDigest: record.authorizationDigest,
88
+ bindingDigest: record.clientBindingDigest,
89
+ });
90
+ return {
91
+ clientRunId: record.clientRunId,
92
+ sessionId: record.sessionId,
93
+ trustedBaseUrl: new URL(
94
+ `${encodeURIComponent(record.sessionId)}/v1/`,
95
+ ensureTrailingSlash(input.trustedBaseUrl),
96
+ ).toString(),
97
+ modelAlias: record.modelAlias,
98
+ limits: publicLimits(record),
99
+ expiresAt: new Date(record.expiresAt).toISOString(),
100
+ accessToken,
101
+ capabilities: ["pageAgentClientRun"] as const,
102
+ };
103
+ }
@@ -5,6 +5,16 @@ export function digestJson(value: ModuleAiJsonValue): `sha256:${string}` {
5
5
  return digestText(canonicalJson(value));
6
6
  }
7
7
 
8
+ export function clientSessionTerminalDigest(
9
+ receiptIds: readonly string[],
10
+ clientResult?: ModuleAiJsonValue,
11
+ ): `sha256:${string}` {
12
+ return digestJson({
13
+ receiptIds: [...receiptIds],
14
+ ...(clientResult === undefined ? {} : { result: clientResult }),
15
+ });
16
+ }
17
+
8
18
  export function digestText(value: string): `sha256:${string}` {
9
19
  return `sha256:${createHash("sha256").update(value).digest("hex")}`;
10
20
  }
@@ -22,7 +32,7 @@ export function sameSecret(left: string, right: string): boolean {
22
32
  );
23
33
  }
24
34
 
25
- function canonicalJson(value: ModuleAiJsonValue): string {
35
+ export function canonicalJson(value: ModuleAiJsonValue): string {
26
36
  if (value === null || typeof value !== "object") {
27
37
  if (typeof value === "number" && !Number.isFinite(value)) {
28
38
  throw new TypeError("Client Agent JSON 不能包含非有限数值");
@@ -0,0 +1,150 @@
1
+ import {
2
+ clientAgentReceipts,
3
+ clientAgentSessions,
4
+ } from "@southwind-ai/database";
5
+ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
6
+ import { asc, eq } from "drizzle-orm";
7
+ import type { NodePgDatabase } from "drizzle-orm/node-postgres";
8
+ import type {
9
+ ClientSessionAdmission,
10
+ ClientSessionFinalResult,
11
+ ClientSessionReceiptRecord,
12
+ ClientSessionRecord,
13
+ ClientSessionStatus,
14
+ } from "./contracts.js";
15
+
16
+ type SessionRow = typeof clientAgentSessions.$inferSelect;
17
+ type ReceiptRow = typeof clientAgentReceipts.$inferSelect;
18
+
19
+ export async function findClientSessionRecord(
20
+ db: NodePgDatabase,
21
+ sessionId: string,
22
+ ) {
23
+ const [row] = await db
24
+ .select()
25
+ .from(clientAgentSessions)
26
+ .where(eq(clientAgentSessions.sessionId, sessionId))
27
+ .limit(1);
28
+ return row ? hydrateClientSessionRecord(db, row) : undefined;
29
+ }
30
+
31
+ export async function hydrateClientSessionRecord(
32
+ db: NodePgDatabase,
33
+ row: SessionRow,
34
+ ) {
35
+ const receipts = await db
36
+ .select()
37
+ .from(clientAgentReceipts)
38
+ .where(eq(clientAgentReceipts.sessionId, row.sessionId))
39
+ .orderBy(asc(clientAgentReceipts.attempt));
40
+ return mapClientSessionRecord(row, receipts.map(mapReceipt));
41
+ }
42
+
43
+ export function clientSessionValues(record: ClientSessionRecord) {
44
+ const admission: ClientSessionAdmission = {
45
+ actorId: record.actorId,
46
+ operationKey: record.operationKey,
47
+ releaseId: record.releaseId,
48
+ inputCommitment: record.inputCommitment,
49
+ authorizationDigest: record.authorizationDigest,
50
+ clientBindingDigest: record.clientBindingDigest,
51
+ deploymentKey: record.deploymentKey,
52
+ providerDataPolicy: record.providerDataPolicy,
53
+ limits: record.limits,
54
+ };
55
+ return {
56
+ sessionId: record.sessionId,
57
+ clientRunId: record.clientRunId,
58
+ actorId: record.actorId,
59
+ operationKey: record.operationKey,
60
+ releaseId: record.releaseId,
61
+ tokenHash: record.tokenHash,
62
+ clientBindingDigest: record.clientBindingDigest,
63
+ hostSessionDigest: record.hostSessionDigest,
64
+ modelAlias: record.modelAlias,
65
+ status: record.status,
66
+ attemptCount: record.attemptCount,
67
+ activeRequests: record.activeRequests,
68
+ pendingAttempts: [...record.pendingAttempts],
69
+ admissionJson: admission as unknown as Record<string, unknown>,
70
+ terminalDigest: record.terminalDigest,
71
+ finalResultJson: record.finalResult as Record<string, unknown> | undefined,
72
+ createdAt: record.createdAt,
73
+ expiresAt: record.expiresAt,
74
+ };
75
+ }
76
+
77
+ export function mapClientSessionRecord(
78
+ row: SessionRow,
79
+ receipts: readonly ClientSessionReceiptRecord[],
80
+ ): ClientSessionRecord {
81
+ const admission = row.admissionJson as unknown as ClientSessionAdmission;
82
+ return {
83
+ ...admission,
84
+ sessionId: row.sessionId,
85
+ clientRunId: row.clientRunId,
86
+ actorId: row.actorId,
87
+ operationKey: row.operationKey,
88
+ releaseId: row.releaseId,
89
+ tokenHash: row.tokenHash as `sha256:${string}`,
90
+ clientBindingDigest: row.clientBindingDigest as `sha256:${string}`,
91
+ hostSessionDigest: row.hostSessionDigest as `sha256:${string}`,
92
+ modelAlias: row.modelAlias,
93
+ status: row.status as ClientSessionStatus,
94
+ attemptCount: row.attemptCount,
95
+ activeRequests: row.activeRequests,
96
+ pendingAttempts: row.pendingAttempts,
97
+ receipts,
98
+ createdAt: row.createdAt,
99
+ expiresAt: row.expiresAt,
100
+ ...(row.terminalDigest
101
+ ? { terminalDigest: row.terminalDigest as `sha256:${string}` }
102
+ : {}),
103
+ ...(row.finalResultJson
104
+ ? {
105
+ finalResult:
106
+ row.finalResultJson as unknown as ClientSessionFinalResult,
107
+ }
108
+ : {}),
109
+ };
110
+ }
111
+
112
+ export function clientSessionReceiptValues(
113
+ sessionId: string,
114
+ receipt: ClientSessionReceiptRecord,
115
+ ) {
116
+ return {
117
+ receiptId: receipt.receiptId,
118
+ sessionId,
119
+ clientRunId: receipt.clientRunId,
120
+ actorId: receipt.actorId,
121
+ operationKey: receipt.operationKey,
122
+ releaseId: receipt.releaseId,
123
+ clientBindingDigest: receipt.clientBindingDigest,
124
+ attempt: receipt.attempt,
125
+ requestDigest: receipt.requestDigest,
126
+ responseDigest: receipt.responseDigest,
127
+ deploymentDigest: receipt.deploymentDigest,
128
+ canonicalResultJson: receipt.canonicalResult,
129
+ issuedAt: receipt.issuedAt,
130
+ };
131
+ }
132
+
133
+ function mapReceipt(row: ReceiptRow): ClientSessionReceiptRecord {
134
+ return {
135
+ receiptId: row.receiptId,
136
+ clientRunId: row.clientRunId,
137
+ actorId: row.actorId,
138
+ operationKey: row.operationKey,
139
+ releaseId: row.releaseId,
140
+ clientBindingDigest: row.clientBindingDigest as `sha256:${string}`,
141
+ attempt: row.attempt,
142
+ requestDigest: row.requestDigest as `sha256:${string}`,
143
+ responseDigest: row.responseDigest as `sha256:${string}`,
144
+ deploymentDigest: row.deploymentDigest as `sha256:${string}`,
145
+ issuedAt: row.issuedAt,
146
+ ...(row.canonicalResultJson === null
147
+ ? {}
148
+ : { canonicalResult: row.canonicalResultJson as ModuleAiJsonValue }),
149
+ };
150
+ }
@@ -8,6 +8,7 @@ import type {
8
8
  ClientSessionRecord,
9
9
  } from "./contracts.js";
10
10
  import { DrizzleClientSessionRepository } from "./drizzle-repository.js";
11
+ import { ClientSessionAttemptAuthorityBinding } from "./attempt-authority.js";
11
12
 
12
13
  const databaseUrl = process.env.CLIENT_AGENT_TEST_DATABASE_URL;
13
14
  const describePostgres = databaseUrl ? describe : describe.skip;
@@ -38,7 +39,25 @@ describePostgres("PostgreSQL Client Agent Session repository", () => {
38
39
  });
39
40
 
40
41
  test("重启恢复 session/receipt,且并发预留与终态保持原子", async () => {
41
- const first = new DrizzleClientSessionRepository(drizzle(pool));
42
+ let transactionAuthorityChecks = 0;
43
+ const commitNow = 3_000;
44
+ const authority = new ClientSessionAttemptAuthorityBinding();
45
+ authority.bind({
46
+ async check() {
47
+ return "ordinary";
48
+ },
49
+ async checkTransaction(transaction, _facts, now) {
50
+ expect(transaction).toBeDefined();
51
+ expect(now).toBe(commitNow);
52
+ transactionAuthorityChecks += 1;
53
+ return "ordinary";
54
+ },
55
+ });
56
+ const first = new DrizzleClientSessionRepository(
57
+ drizzle(pool),
58
+ authority,
59
+ () => commitNow,
60
+ );
42
61
  await first.create(session());
43
62
  const [accepted, denied] = await Promise.all([
44
63
  first.reserveAttempt(caller()),
@@ -47,7 +66,11 @@ describePostgres("PostgreSQL Client Agent Session repository", () => {
47
66
  expect([accepted, denied].filter(Boolean)).toHaveLength(1);
48
67
  expect([accepted, denied].filter((value) => !value)).toHaveLength(1);
49
68
 
50
- const restarted = new DrizzleClientSessionRepository(drizzle(pool));
69
+ const restarted = new DrizzleClientSessionRepository(
70
+ drizzle(pool),
71
+ authority,
72
+ () => commitNow,
73
+ );
51
74
  expect(await restarted.find("cas_1")).toMatchObject({
52
75
  clientRunId: "carun_1",
53
76
  attemptCount: 1,
@@ -75,8 +98,12 @@ describePostgres("PostgreSQL Client Agent Session repository", () => {
75
98
  });
76
99
 
77
100
  const completed = await restarted.completeAttempt("cas_1", receipt());
101
+ expect(transactionAuthorityChecks).toBe(1);
78
102
  expect(completed?.receipts).toHaveLength(1);
79
- const afterReceipt = new DrizzleClientSessionRepository(drizzle(pool));
103
+ const afterReceipt = new DrizzleClientSessionRepository(
104
+ drizzle(pool),
105
+ authority,
106
+ );
80
107
  expect((await afterReceipt.find("cas_1"))?.receipts[0]).toEqual(receipt());
81
108
 
82
109
  const terminal = await afterReceipt.transition(