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.
- package/dist/cli.js +1008 -61
- package/package.json +1 -1
- package/template/.agents/skills/windy-business-module/SKILL.md +58 -8
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/agent/client-session/attempt-authority.test.ts +162 -0
- package/template/apps/server/src/agent/client-session/attempt-authority.ts +191 -0
- package/template/apps/server/src/agent/client-session/bootstrap.ts +27 -11
- package/template/apps/server/src/agent/client-session/completion.ts +75 -24
- package/template/apps/server/src/agent/client-session/contracts.ts +23 -0
- package/template/apps/server/src/agent/client-session/creation.ts +103 -0
- package/template/apps/server/src/agent/client-session/digest.ts +11 -1
- package/template/apps/server/src/agent/client-session/drizzle-records.ts +150 -0
- package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts +30 -3
- package/template/apps/server/src/agent/client-session/drizzle-repository.ts +23 -132
- package/template/apps/server/src/agent/client-session/finalization.ts +99 -0
- package/template/apps/server/src/agent/client-session/host-auth.ts +22 -10
- package/template/apps/server/src/agent/client-session/host-revocation-composition.test.ts +77 -0
- package/template/apps/server/src/agent/client-session/host-revocation-composition.ts +28 -0
- package/template/apps/server/src/agent/client-session/launch-authority.contract.test.ts +21 -0
- package/template/apps/server/src/agent/client-session/manifest-admission.ts +11 -0
- package/template/apps/server/src/agent/client-session/service.test-support.ts +22 -0
- package/template/apps/server/src/agent/client-session/service.ts +104 -134
- package/template/apps/server/src/agent/client-session/terminal-digest.test.ts +23 -0
- package/template/apps/server/src/agent/client-workflow-artifact/active-blob-references.ts +8 -0
- package/template/apps/server/src/agent/client-workflow-artifact/artifact-builder.ts +60 -0
- package/template/apps/server/src/agent/client-workflow-artifact/authority-current.ts +59 -0
- package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.test.ts +189 -0
- package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.ts +153 -0
- package/template/apps/server/src/agent/client-workflow-artifact/bootstrap.ts +77 -0
- package/template/apps/server/src/agent/client-workflow-artifact/cancellation.ts +47 -0
- package/template/apps/server/src/agent/client-workflow-artifact/completed-recovery.ts +31 -0
- package/template/apps/server/src/agent/client-workflow-artifact/contracts.ts +104 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-locks.ts +30 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-mapper.ts +138 -0
- package/template/apps/server/src/agent/client-workflow-artifact/drizzle-repository.ts +346 -0
- package/template/apps/server/src/agent/client-workflow-artifact/error.ts +21 -0
- package/template/apps/server/src/agent/client-workflow-artifact/handler.ts +167 -0
- package/template/apps/server/src/agent/client-workflow-artifact/host-revocation.ts +29 -0
- package/template/apps/server/src/agent/client-workflow-artifact/in-memory-repository.ts +232 -0
- package/template/apps/server/src/agent/client-workflow-artifact/job-port.ts +101 -0
- package/template/apps/server/src/agent/client-workflow-artifact/presentation.ts +41 -0
- package/template/apps/server/src/agent/client-workflow-artifact/recovery.contract.test.ts +253 -0
- package/template/apps/server/src/agent/client-workflow-artifact/release-binding.ts +85 -0
- package/template/apps/server/src/agent/client-workflow-artifact/repository.test.ts +204 -0
- package/template/apps/server/src/agent/client-workflow-artifact/repository.ts +97 -0
- package/template/apps/server/src/agent/client-workflow-artifact/routes-handler.contract.test.ts +201 -0
- package/template/apps/server/src/agent/client-workflow-artifact/routes.ts +161 -0
- package/template/apps/server/src/agent/client-workflow-artifact/runtime-composition.ts +101 -0
- package/template/apps/server/src/agent/client-workflow-artifact/scope-authority.ts +39 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service-authorization.ts +117 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service-support.ts +28 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service.contract.test.ts +345 -0
- package/template/apps/server/src/agent/client-workflow-artifact/service.ts +350 -0
- package/template/apps/server/src/agent/client-workflow-artifact/session-boundary.ts +39 -0
- package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts +98 -0
- package/template/apps/server/src/agent/client-workflow-artifact/source-access.ts +80 -0
- package/template/apps/server/src/agent/client-workflow-artifact/takeover.ts +122 -0
- package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts +97 -0
- package/template/apps/server/src/agent/client-workflow-artifact/task-authority.ts +147 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts +100 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-replay.ts +31 -0
- package/template/apps/server/src/agent/client-workflow-artifact/terminal-seal.ts +211 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-artifact.test-support.ts +173 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.contract.test.ts +254 -0
- package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.ts +285 -0
- package/template/apps/server/src/agent/run-facade.test-support.ts +17 -4
- package/template/apps/server/src/agent/run-facade.test.ts +16 -0
- package/template/apps/server/src/agent/run-session-authorization.ts +12 -10
- package/template/apps/server/src/agent/runtime-bootstrap-input.ts +5 -1
- package/template/apps/server/src/agent/runtime-bootstrap.ts +33 -33
- package/template/apps/server/src/agent/runtime-public-ports.ts +40 -0
- package/template/apps/server/src/agent/session-invalidation/contracts.ts +25 -26
- package/template/apps/server/src/agent/session-projection-access.ts +31 -19
- package/template/apps/server/src/agent/unavailable-routes.ts +21 -0
- package/template/apps/server/src/agent/workflow-runtime-bootstrap.ts +2 -0
- package/template/apps/server/src/agent-workflow/artifact-source-authorization.ts +12 -6
- package/template/apps/server/src/agent-workflow/command-facade.ts +70 -11
- package/template/apps/server/src/agent-workflow/drizzle-artifact-port.ts +9 -9
- package/template/apps/server/src/agent-workflow/drizzle-delegation-completion.ts +1 -1
- package/template/apps/server/src/agent-workflow/platform-node-adapters.ts +14 -6
- package/template/apps/server/src/agent-workflow/project-launch-authority.contract.test.ts +53 -0
- package/template/apps/server/src/agent-workflow/project-launch-authority.ts +92 -0
- package/template/apps/server/src/agent-workflow/runtime-composition.ts +30 -14
- package/template/apps/server/src/app/agent-runtime-composition.ts +6 -1
- package/template/apps/server/src/app/client-agent-workflow-artifact-binding.ts +14 -0
- package/template/apps/server/src/application-services.ts +6 -0
- package/template/apps/server/src/data-access/scope-containment.test.ts +31 -0
- package/template/apps/server/src/data-access/scope-containment.ts +48 -0
- package/template/apps/server/src/jobs/artifact-publication.test.ts +192 -0
- package/template/apps/server/src/jobs/artifact-publication.ts +56 -0
- package/template/apps/server/src/jobs/artifact-publisher.ts +101 -0
- package/template/apps/server/src/jobs/drizzle-artifact.ts +73 -1
- package/template/apps/server/src/jobs/drizzle-execution-repository.ts +14 -1
- package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +65 -0
- package/template/apps/server/src/jobs/drizzle-repository.integration.test.ts +1 -0
- package/template/apps/server/src/jobs/execution-repository.ts +28 -0
- package/template/apps/server/src/jobs/execution.test.ts +3 -1
- package/template/apps/server/src/jobs/in-memory-execution.ts +16 -0
- package/template/apps/server/src/jobs/in-memory-lease.ts +45 -1
- package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +2 -0
- package/template/apps/server/src/jobs/postgres-cleanup-contract.test.ts +58 -0
- package/template/apps/server/src/jobs/worker.ts +8 -14
- package/template/apps/server/src/module-host/background-ai-unavailable.ts +18 -0
- package/template/apps/server/src/module-host/durable-context.ts +19 -22
- package/template/apps/server/src/module-host/implementation-completeness.test.ts +80 -1
- package/template/apps/server/src/module-host/implementation-completeness.ts +28 -1
- package/template/apps/server/src/module-host/request-context.ts +32 -8
- package/template/apps/server/src/module-host/storage-port.ts +3 -0
- package/template/apps/server/src/module-host/workflow-input-artifact-port.test.ts +104 -0
- package/template/apps/server/src/module-host/workflow-input-artifact-port.ts +229 -0
- package/template/apps/server/src/module-host/workflow-input-materialization.test.ts +163 -0
- package/template/apps/server/src/module-host/workflow-input-materialization.ts +161 -0
- package/template/apps/server/src/workflow/drizzle-repository.integration.test.ts +1 -0
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +2 -1
- package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +16 -0
- package/template/docs/architecture/agent-workflow.md +13 -2
- package/template/docs/architecture/durable-jobs.md +7 -0
- package/template/docs/operations/agent-session-memory.md +2 -1
- package/template/docs/operations/agent-session-troubleshooting.md +87 -2
- package/template/docs/operations/agent-workflow-recovery.md +25 -14
- package/template/docs/platform/client-agent-session.md +148 -2
- package/template/packages/ai-client/package.json +2 -1
- package/template/packages/ai-client/src/client-agent/index.ts +1 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
- package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
- package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
- package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
- package/template/packages/database/drizzle/meta/_journal.json +7 -0
- package/template/packages/database/package.json +1 -1
- package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
- package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
- package/template/packages/database/src/schema/index.ts +1 -0
- package/template/packages/jobs/package.json +1 -1
- package/template/packages/jobs/src/handlers.test.ts +10 -0
- package/template/packages/jobs/src/handlers.ts +28 -11
- package/template/packages/modules/package.json +1 -1
- package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
- package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
- package/template/packages/modules/src/system-audit-actions.ts +15 -0
- package/template/packages/server-sdk/index.ts +52 -0
- package/template/packages/server-sdk/package.json +5 -1
- package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
- package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
- package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
- package/template/packages/server-sdk/src/durable-handler.ts +7 -0
- package/template/packages/server-sdk/src/request-context.ts +7 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
- package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
- package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
- package/template/packages/storage/package.json +1 -1
- package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
- package/template/packages/storage/src/blob-store.ts +9 -0
- package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
- package/template/packages/storage/src/local-file-blob-store.ts +53 -1
- 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:
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
203
|
-
input.reserved.operationKey,
|
|
204
|
-
response,
|
|
205
|
-
),
|
|
242
|
+
canonicalResult,
|
|
206
243
|
};
|
|
207
|
-
|
|
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
|
+
}
|
package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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(
|