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
|
@@ -2,24 +2,33 @@ import {
|
|
|
2
2
|
clientAgentReceipts,
|
|
3
3
|
clientAgentSessions,
|
|
4
4
|
} from "@southwind-ai/database";
|
|
5
|
-
import
|
|
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
|
-
|
|
19
|
-
type
|
|
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(
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
88
|
-
const header =
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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,
|