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,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,
|
|
@@ -1,31 +1,25 @@
|
|
|
1
|
-
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
1
|
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
3
2
|
import type {
|
|
4
3
|
ClientSessionAdmissionPort,
|
|
4
|
+
ClientSessionAttemptAuthorityChecker,
|
|
5
5
|
ClientSessionAuditPort,
|
|
6
6
|
ClientSessionCaller,
|
|
7
|
-
ClientSessionFinalResult,
|
|
8
7
|
ClientSessionRecord,
|
|
9
8
|
} from "./contracts.js";
|
|
10
|
-
import {
|
|
9
|
+
import { digestText } from "./digest.js";
|
|
11
10
|
import type { ClientSessionRepository } from "./repository.js";
|
|
12
11
|
import { clientAgentCapabilities } from "./capabilities.js";
|
|
13
12
|
import { authorizeClientSession } from "./authorization.js";
|
|
14
13
|
import { abortClientSession, auditRevokedSessions } from "./revocation.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
publicLimits,
|
|
18
|
-
sessionView,
|
|
19
|
-
} from "./presentation.js";
|
|
20
|
-
import {
|
|
21
|
-
ClientAgentSessionError,
|
|
22
|
-
clientSessionConflict as conflict,
|
|
23
|
-
clientSessionInvalid as invalid,
|
|
24
|
-
} from "./errors.js";
|
|
14
|
+
import { sessionView } from "./presentation.js";
|
|
15
|
+
import { clientSessionConflict as conflict } from "./errors.js";
|
|
25
16
|
import {
|
|
26
17
|
consumeClientAgentCompletion,
|
|
27
18
|
runClientAgentCompletion,
|
|
28
19
|
} from "./completion.js";
|
|
20
|
+
import { finalizeClientSession } from "./finalization.js";
|
|
21
|
+
import { createClientAgentSession } from "./creation.js";
|
|
22
|
+
|
|
29
23
|
export class GovernedClientAgentSessionService {
|
|
30
24
|
readonly #attemptControllers = new Map<string, AbortController>();
|
|
31
25
|
readonly #recovery: Promise<number>;
|
|
@@ -35,6 +29,7 @@ export class GovernedClientAgentSessionService {
|
|
|
35
29
|
private readonly provider: import("./contracts.js").ClientAgentProviderPort,
|
|
36
30
|
private readonly audit: ClientSessionAuditPort,
|
|
37
31
|
private readonly trustedBaseUrl: string,
|
|
32
|
+
private readonly attemptAuthority: ClientSessionAttemptAuthorityChecker,
|
|
38
33
|
private readonly now: () => number = Date.now,
|
|
39
34
|
private readonly maxTtlMs = 5 * 60_000,
|
|
40
35
|
) {
|
|
@@ -60,73 +55,44 @@ export class GovernedClientAgentSessionService {
|
|
|
60
55
|
hostSessionDigest: `sha256:${string}`;
|
|
61
56
|
actorId: string;
|
|
62
57
|
}) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
operationKey:
|
|
80
|
-
input:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
tokenHash: digestText(accessToken),
|
|
101
|
-
hostSessionDigest: input.hostSessionDigest,
|
|
102
|
-
modelAlias: this.provider.modelAlias,
|
|
103
|
-
createdAt,
|
|
104
|
-
expiresAt,
|
|
105
|
-
status: "active",
|
|
106
|
-
attemptCount: 0,
|
|
107
|
-
activeRequests: 0,
|
|
108
|
-
pendingAttempts: [],
|
|
109
|
-
receipts: [],
|
|
110
|
-
};
|
|
111
|
-
await this.repository.create(record);
|
|
112
|
-
await this.recordAudit("create", record, "success", {
|
|
113
|
-
inputCommitment: record.inputCommitment,
|
|
114
|
-
authorizationDigest: record.authorizationDigest,
|
|
115
|
-
bindingDigest: record.clientBindingDigest,
|
|
58
|
+
return this.createWithAuthority(input, "direct");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 仅供固定 Workflow Artifact service 使用;路由不得暴露。 */
|
|
62
|
+
async createWorkflow(input: {
|
|
63
|
+
operationKey: string;
|
|
64
|
+
input: ModuleAiJsonValue;
|
|
65
|
+
clientBindingDigest: string;
|
|
66
|
+
hostSessionDigest: `sha256:${string}`;
|
|
67
|
+
actorId: string;
|
|
68
|
+
}) {
|
|
69
|
+
return this.createWithAuthority(input, "workflow-artifact");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async createWithAuthority(
|
|
73
|
+
input: {
|
|
74
|
+
operationKey: string;
|
|
75
|
+
input: ModuleAiJsonValue;
|
|
76
|
+
clientBindingDigest: string;
|
|
77
|
+
hostSessionDigest: `sha256:${string}`;
|
|
78
|
+
actorId: string;
|
|
79
|
+
},
|
|
80
|
+
launchAuthority: "direct" | "workflow-artifact",
|
|
81
|
+
) {
|
|
82
|
+
return createClientAgentSession({
|
|
83
|
+
request: input,
|
|
84
|
+
launchAuthority,
|
|
85
|
+
recovery: this.#recovery,
|
|
86
|
+
provider: this.provider,
|
|
87
|
+
admission: this.admission,
|
|
88
|
+
repository: this.repository,
|
|
89
|
+
trustedBaseUrl: this.trustedBaseUrl,
|
|
90
|
+
now: this.now,
|
|
91
|
+
maxTtlMs: this.maxTtlMs,
|
|
92
|
+
audit: async (record, metadata) => {
|
|
93
|
+
await this.recordAudit("create", record, "success", metadata);
|
|
94
|
+
},
|
|
116
95
|
});
|
|
117
|
-
return {
|
|
118
|
-
clientRunId: record.clientRunId,
|
|
119
|
-
sessionId: record.sessionId,
|
|
120
|
-
trustedBaseUrl: new URL(
|
|
121
|
-
`${encodeURIComponent(record.sessionId)}/v1/`,
|
|
122
|
-
ensureTrailingSlash(this.trustedBaseUrl),
|
|
123
|
-
).toString(),
|
|
124
|
-
modelAlias: record.modelAlias,
|
|
125
|
-
limits: publicLimits(record),
|
|
126
|
-
expiresAt: new Date(record.expiresAt).toISOString(),
|
|
127
|
-
accessToken,
|
|
128
|
-
capabilities: ["pageAgentClientRun"] as const,
|
|
129
|
-
};
|
|
130
96
|
}
|
|
131
97
|
|
|
132
98
|
async complete(
|
|
@@ -152,6 +118,7 @@ export class GovernedClientAgentSessionService {
|
|
|
152
118
|
caller,
|
|
153
119
|
signal,
|
|
154
120
|
current: await this.authorize(sessionId, caller, true),
|
|
121
|
+
attemptAuthority: this.attemptAuthority,
|
|
155
122
|
repository: this.repository,
|
|
156
123
|
admission: this.admission,
|
|
157
124
|
provider: this.provider,
|
|
@@ -172,66 +139,49 @@ export class GovernedClientAgentSessionService {
|
|
|
172
139
|
) {
|
|
173
140
|
await this.#recovery;
|
|
174
141
|
const record = await this.authorize(sessionId, caller, false);
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
142
|
+
return finalizeClientSession({
|
|
143
|
+
record,
|
|
144
|
+
repository: this.repository,
|
|
145
|
+
admission: this.admission,
|
|
146
|
+
receiptIds,
|
|
147
|
+
clientResult,
|
|
148
|
+
audit: (completed, metadata) =>
|
|
149
|
+
this.recordAudit("finalize", completed, "success", metadata),
|
|
178
150
|
});
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async finalizeWorkflow(
|
|
154
|
+
sessionId: string,
|
|
155
|
+
caller: Omit<ClientSessionCaller, "token">,
|
|
156
|
+
receiptIds: readonly string[],
|
|
157
|
+
clientResult: ModuleAiJsonValue | undefined,
|
|
158
|
+
canonicalizeResult: (
|
|
159
|
+
proposedResult: ModuleAiJsonValue,
|
|
160
|
+
) => Promise<ModuleAiJsonValue>,
|
|
161
|
+
) {
|
|
162
|
+
await this.#recovery;
|
|
163
|
+
const record = await this.repository.find(sessionId);
|
|
187
164
|
if (
|
|
188
|
-
!
|
|
189
|
-
|
|
190
|
-
|
|
165
|
+
!record ||
|
|
166
|
+
record.actorId !== caller.actorId ||
|
|
167
|
+
record.clientBindingDigest !== caller.clientBindingDigest ||
|
|
168
|
+
record.hostSessionDigest !== caller.hostSessionDigest ||
|
|
169
|
+
(record.status !== "completed" &&
|
|
170
|
+
(record.status !== "active" || record.expiresAt <= this.now())) ||
|
|
171
|
+
!(await this.admission.reauthorize(record, caller.actorId))
|
|
191
172
|
) {
|
|
192
|
-
throw
|
|
173
|
+
throw conflict("Workflow Client Agent Session 绑定已失效");
|
|
193
174
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
attestation: {
|
|
204
|
-
source:
|
|
205
|
-
projected === undefined ? "client-projected" : "provider-observed",
|
|
206
|
-
terminalReceiptId: terminal.receiptId,
|
|
207
|
-
providerAttested: projected !== undefined,
|
|
208
|
-
clientProjected: projected === undefined,
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
const completed = await this.repository.transition(
|
|
212
|
-
sessionId,
|
|
213
|
-
"active",
|
|
214
|
-
"completed",
|
|
215
|
-
{
|
|
216
|
-
terminalDigest,
|
|
217
|
-
finalResult,
|
|
218
|
-
},
|
|
219
|
-
);
|
|
220
|
-
if (!completed) {
|
|
221
|
-
const concurrent = await this.repository.find(sessionId);
|
|
222
|
-
if (
|
|
223
|
-
concurrent?.status === "completed" &&
|
|
224
|
-
concurrent.terminalDigest === terminalDigest
|
|
225
|
-
) {
|
|
226
|
-
return sessionView(concurrent);
|
|
227
|
-
}
|
|
228
|
-
throw conflict("Finalize 与其它终态操作冲突");
|
|
229
|
-
}
|
|
230
|
-
await this.recordAudit("finalize", completed, "success", {
|
|
231
|
-
terminalReceiptId: terminal.receiptId,
|
|
232
|
-
clientProjected: projected === undefined,
|
|
175
|
+
return finalizeClientSession({
|
|
176
|
+
record,
|
|
177
|
+
repository: this.repository,
|
|
178
|
+
admission: this.admission,
|
|
179
|
+
receiptIds,
|
|
180
|
+
clientResult,
|
|
181
|
+
canonicalizeResult,
|
|
182
|
+
audit: (completed, metadata) =>
|
|
183
|
+
this.recordAudit("finalize", completed, "success", metadata),
|
|
233
184
|
});
|
|
234
|
-
return sessionView(completed);
|
|
235
185
|
}
|
|
236
186
|
|
|
237
187
|
async cancel(
|
|
@@ -310,6 +260,26 @@ export class GovernedClientAgentSessionService {
|
|
|
310
260
|
});
|
|
311
261
|
}
|
|
312
262
|
|
|
263
|
+
/** Host-internal revocation for a fenced Workflow claim; no browser token is accepted. */
|
|
264
|
+
async revokeSession(sessionId: string, cause: string) {
|
|
265
|
+
await this.#recovery;
|
|
266
|
+
const record = await this.repository.find(sessionId);
|
|
267
|
+
if (!record || record.status === "revoked") return;
|
|
268
|
+
if (record.status !== "active") return;
|
|
269
|
+
abortClientSession(this.#attemptControllers, sessionId);
|
|
270
|
+
const revoked = await this.repository.transition(
|
|
271
|
+
sessionId,
|
|
272
|
+
"active",
|
|
273
|
+
"revoked",
|
|
274
|
+
{ terminalDigest: digestText(`revoke\0${cause.slice(0, 120)}`) },
|
|
275
|
+
);
|
|
276
|
+
if (revoked) {
|
|
277
|
+
await this.recordAudit("revoke", revoked, "success", {
|
|
278
|
+
cause: cause.slice(0, 120),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
313
283
|
private async authorize(
|
|
314
284
|
sessionId: string,
|
|
315
285
|
caller: ClientSessionCaller,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { clientSessionTerminalDigest } from "./digest.js";
|
|
3
|
+
|
|
4
|
+
describe("Client Session terminal digest", () => {
|
|
5
|
+
test("canonical result key 顺序不改变 digest,receipt 顺序仍参与绑定", () => {
|
|
6
|
+
const left = clientSessionTerminalDigest(["receipt_1", "receipt_2"], {
|
|
7
|
+
answer: "done",
|
|
8
|
+
evidence: { b: 2, a: 1 },
|
|
9
|
+
});
|
|
10
|
+
const same = clientSessionTerminalDigest(["receipt_1", "receipt_2"], {
|
|
11
|
+
evidence: { a: 1, b: 2 },
|
|
12
|
+
answer: "done",
|
|
13
|
+
});
|
|
14
|
+
const reordered = clientSessionTerminalDigest(["receipt_2", "receipt_1"], {
|
|
15
|
+
answer: "done",
|
|
16
|
+
evidence: { a: 1, b: 2 },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(left).toBe(same);
|
|
20
|
+
expect(left).toMatch(/^sha256:[a-f0-9]{64}$/u);
|
|
21
|
+
expect(reordered).not.toBe(left);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { DrizzleClientAgentWorkflowArtifactRepository } from "./drizzle-repository.js";
|
|
3
|
+
|
|
4
|
+
export function clientWorkflowArtifactActiveBlobReferences(
|
|
5
|
+
db: NodePgDatabase | undefined,
|
|
6
|
+
) {
|
|
7
|
+
return db ? [new DrizzleClientAgentWorkflowArtifactRepository(db)] : [];
|
|
8
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
3
|
+
import { artifactRef, type BlobStore } from "@southwind-ai/storage";
|
|
4
|
+
import { canonicalJson, digestJson } from "../client-session/digest.js";
|
|
5
|
+
import type {
|
|
6
|
+
ClientAgentWorkflowArtifactBinding,
|
|
7
|
+
ClientAgentWorkflowArtifactRecord,
|
|
8
|
+
} from "./contracts.js";
|
|
9
|
+
import { ClientAgentWorkflowArtifactError } from "./error.js";
|
|
10
|
+
|
|
11
|
+
export function clientWorkflowTerminalReceipt(
|
|
12
|
+
record: ClientAgentWorkflowArtifactRecord,
|
|
13
|
+
historyHeadDigest: string,
|
|
14
|
+
) {
|
|
15
|
+
return `cawr_${digestJson({
|
|
16
|
+
workId: record.workId,
|
|
17
|
+
claimGeneration: record.claimGeneration,
|
|
18
|
+
historyHeadDigest,
|
|
19
|
+
}).slice("sha256:".length, 53)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function storeClientWorkflowArtifact(input: {
|
|
23
|
+
blobs: BlobStore;
|
|
24
|
+
result: ModuleAiJsonValue;
|
|
25
|
+
binding: ClientAgentWorkflowArtifactBinding;
|
|
26
|
+
terminalReceiptDigest: `sha256:${string}`;
|
|
27
|
+
}) {
|
|
28
|
+
const bytes = new TextEncoder().encode(canonicalJson(input.result));
|
|
29
|
+
if (bytes.byteLength > input.binding.artifact.maxByteSize) {
|
|
30
|
+
throw new ClientAgentWorkflowArtifactError(
|
|
31
|
+
"ARTIFACT_TOO_LARGE",
|
|
32
|
+
"Client Agent Workflow 产物超过策略上限",
|
|
33
|
+
413,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
37
|
+
if (!input.blobs.putOnce) {
|
|
38
|
+
throw new ClientAgentWorkflowArtifactError(
|
|
39
|
+
"ARTIFACT_STORE_UNAVAILABLE",
|
|
40
|
+
"Client Agent Workflow 稳定产物存储未装配",
|
|
41
|
+
503,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const stableRef = artifactRef(
|
|
45
|
+
`blob_${input.terminalReceiptDigest.slice("sha256:".length, 39)}`,
|
|
46
|
+
);
|
|
47
|
+
const stored = await input.blobs.putOnce({
|
|
48
|
+
ref: stableRef,
|
|
49
|
+
body: new Blob([bytes]).stream(),
|
|
50
|
+
expectedByteSize: bytes.byteLength,
|
|
51
|
+
expectedContentHash: { algorithm: "sha256", value: sha256 },
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
artifactRef: stored.ref,
|
|
55
|
+
filename: input.binding.artifact.filename,
|
|
56
|
+
contentType: input.binding.artifact.contentType,
|
|
57
|
+
byteSize: stored.byteSize,
|
|
58
|
+
sha256,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentWorkflowNodeRuns,
|
|
3
|
+
agentWorkflowRuns,
|
|
4
|
+
agentWorkflowSideEffectCommands,
|
|
5
|
+
} from "@southwind-ai/database";
|
|
6
|
+
import { eq } from "drizzle-orm";
|
|
7
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
8
|
+
import type { ClientAgentWorkflowArtifactRecord } from "./contracts.js";
|
|
9
|
+
|
|
10
|
+
export async function clientWorkflowAuthorityCurrent(
|
|
11
|
+
db: NodePgDatabase,
|
|
12
|
+
record: ClientAgentWorkflowArtifactRecord,
|
|
13
|
+
now: number,
|
|
14
|
+
) {
|
|
15
|
+
const [command] = await db
|
|
16
|
+
.select()
|
|
17
|
+
.from(agentWorkflowSideEffectCommands)
|
|
18
|
+
.where(eq(agentWorkflowSideEffectCommands.commandId, record.commandId))
|
|
19
|
+
.for("update")
|
|
20
|
+
.limit(1);
|
|
21
|
+
if (!command) return false;
|
|
22
|
+
const [run] = await db
|
|
23
|
+
.select()
|
|
24
|
+
.from(agentWorkflowRuns)
|
|
25
|
+
.where(eq(agentWorkflowRuns.runId, record.runId))
|
|
26
|
+
.for("update")
|
|
27
|
+
.limit(1);
|
|
28
|
+
if (!run) return false;
|
|
29
|
+
const [nodeRun] = await db
|
|
30
|
+
.select()
|
|
31
|
+
.from(agentWorkflowNodeRuns)
|
|
32
|
+
.where(eq(agentWorkflowNodeRuns.nodeRunId, record.nodeRunId))
|
|
33
|
+
.for("update")
|
|
34
|
+
.limit(1);
|
|
35
|
+
return Boolean(
|
|
36
|
+
nodeRun &&
|
|
37
|
+
command.status === "dispatched" &&
|
|
38
|
+
command.runId === record.runId &&
|
|
39
|
+
command.nodeRunId === record.nodeRunId &&
|
|
40
|
+
command.effectKind === record.effectKind &&
|
|
41
|
+
command.adapterKind === "durable-job" &&
|
|
42
|
+
command.generation === record.effectGeneration &&
|
|
43
|
+
command.requestDigest === record.requestDigest &&
|
|
44
|
+
command.fenceTokenDigest === record.effectFenceTokenDigest &&
|
|
45
|
+
command.authorizationSnapshotDigest ===
|
|
46
|
+
record.authorizationSnapshotDigest &&
|
|
47
|
+
command.runPolicyDigest === record.runPolicyDigest &&
|
|
48
|
+
["accepted", "running", "waiting"].includes(run.status) &&
|
|
49
|
+
!run.cancellationRequestedAt &&
|
|
50
|
+
run.deadlineAt > now &&
|
|
51
|
+
run.actorId === record.actorId &&
|
|
52
|
+
run.scopeDigest === record.scopeDigest &&
|
|
53
|
+
run.authorizationSnapshotDigest === record.authorizationSnapshotDigest &&
|
|
54
|
+
run.executionPolicyDigest === record.runPolicyDigest &&
|
|
55
|
+
nodeRun.runId === record.runId &&
|
|
56
|
+
nodeRun.nodeType === "job" &&
|
|
57
|
+
["running", "waiting"].includes(nodeRun.status),
|
|
58
|
+
);
|
|
59
|
+
}
|