create-windy 0.6.14 → 0.6.15
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 +215 -24
- 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/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
|
@@ -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(
|
|
@@ -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
|
|