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,162 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createFixture, validCompletion } from "./service.test-support.js";
|
|
3
|
+
import { consumeClientAgentCompletion } from "./completion.js";
|
|
4
|
+
|
|
5
|
+
describe("Client Session Provider attempt authority", () => {
|
|
6
|
+
test("普通 Session 经 Host checker 明确分类后保持原 completion 行为", async () => {
|
|
7
|
+
const fixture = await createFixture({
|
|
8
|
+
attemptAuthority: {
|
|
9
|
+
async check() {
|
|
10
|
+
return "ordinary";
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
await expect(fixture.complete(validCompletion())).resolves.toBeDefined();
|
|
16
|
+
expect(fixture.provider.requests).toHaveLength(1);
|
|
17
|
+
expect(
|
|
18
|
+
(await fixture.repository.find(fixture.grant.sessionId))?.status,
|
|
19
|
+
).toBe("active");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("每个 Workflow Session attempt 出网前复核,漂移先撤销且不调用 Provider", async () => {
|
|
23
|
+
let decision = "current" as "current" | "revoked";
|
|
24
|
+
let checks = 0;
|
|
25
|
+
const fixture = await createFixture({
|
|
26
|
+
attemptAuthority: {
|
|
27
|
+
async check(facts) {
|
|
28
|
+
checks += 1;
|
|
29
|
+
expect(facts).toMatchObject({
|
|
30
|
+
actorId: "actor-1",
|
|
31
|
+
operationKey: "support.answer",
|
|
32
|
+
});
|
|
33
|
+
return decision;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await fixture.complete(validCompletion());
|
|
39
|
+
decision = "revoked";
|
|
40
|
+
await expect(fixture.complete(validCompletion())).rejects.toMatchObject({
|
|
41
|
+
code: "CONFLICT",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
expect(checks).toBe(4);
|
|
45
|
+
expect(fixture.provider.requests).toHaveLength(1);
|
|
46
|
+
expect(
|
|
47
|
+
(await fixture.repository.find(fixture.grant.sessionId))?.status,
|
|
48
|
+
).toBe("revoked");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("reserve 后 authority 漂移仍在真正 Provider 出网紧前阻断", async () => {
|
|
52
|
+
let checks = 0;
|
|
53
|
+
const fixture = await createFixture({
|
|
54
|
+
attemptAuthority: {
|
|
55
|
+
async check() {
|
|
56
|
+
checks += 1;
|
|
57
|
+
return checks === 1 ? "current" : "revoked";
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await expect(fixture.complete(validCompletion())).rejects.toMatchObject({
|
|
63
|
+
code: "CONFLICT",
|
|
64
|
+
});
|
|
65
|
+
expect(checks).toBe(2);
|
|
66
|
+
expect(fixture.provider.requests).toHaveLength(0);
|
|
67
|
+
expect(
|
|
68
|
+
(await fixture.repository.find(fixture.grant.sessionId))?.status,
|
|
69
|
+
).toBe("revoked");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("reserve 后当前授权漂移也在真正 Provider 出网紧前阻断", async () => {
|
|
73
|
+
let revokeCurrentAuthorization: () => void = () => undefined;
|
|
74
|
+
const fixture = await createFixture({
|
|
75
|
+
attemptAuthority: {
|
|
76
|
+
async check() {
|
|
77
|
+
revokeCurrentAuthorization();
|
|
78
|
+
return "current";
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
revokeCurrentAuthorization = () => {
|
|
83
|
+
fixture.admission.allowed = false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await expect(fixture.complete(validCompletion())).rejects.toMatchObject({
|
|
87
|
+
code: "CONFLICT",
|
|
88
|
+
});
|
|
89
|
+
expect(fixture.provider.requests).toHaveLength(0);
|
|
90
|
+
expect(
|
|
91
|
+
(await fixture.repository.find(fixture.grant.sessionId))?.status,
|
|
92
|
+
).toBe("revoked");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("reserve 后 Session 被 takeover 撤销时,generator 不得开始 Provider", async () => {
|
|
96
|
+
const fixture = await createFixture({
|
|
97
|
+
attemptAuthority: {
|
|
98
|
+
async check() {
|
|
99
|
+
return "current";
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const completion = await fixture.service.startCompletion(
|
|
104
|
+
fixture.grant.sessionId,
|
|
105
|
+
validCompletion(),
|
|
106
|
+
fixture.caller,
|
|
107
|
+
);
|
|
108
|
+
await fixture.repository.transition(
|
|
109
|
+
fixture.grant.sessionId,
|
|
110
|
+
"active",
|
|
111
|
+
"revoked",
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
await expect(
|
|
115
|
+
consumeClientAgentCompletion(completion.events),
|
|
116
|
+
).rejects.toMatchObject({ code: "CONFLICT" });
|
|
117
|
+
expect(fixture.provider.requests).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("Provider 返回后 fence 漂移时不提交迟到 receipt", async () => {
|
|
121
|
+
let decision = "current" as "current" | "revoked";
|
|
122
|
+
const fixture = await createFixture({
|
|
123
|
+
attemptAuthority: {
|
|
124
|
+
async check() {
|
|
125
|
+
return decision;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
execute: async () => {
|
|
129
|
+
decision = "revoked";
|
|
130
|
+
return {
|
|
131
|
+
choices: [{ message: { role: "assistant", content: "late" } }],
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await expect(fixture.complete(validCompletion())).rejects.toMatchObject({
|
|
137
|
+
code: "CONFLICT",
|
|
138
|
+
});
|
|
139
|
+
expect(
|
|
140
|
+
(await fixture.repository.find(fixture.grant.sessionId))?.receipts,
|
|
141
|
+
).toHaveLength(0);
|
|
142
|
+
expect(fixture.provider.requests).toHaveLength(1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("authority checker 异常同样 fail closed 且不产生 Provider attempt", async () => {
|
|
146
|
+
const fixture = await createFixture({
|
|
147
|
+
attemptAuthority: {
|
|
148
|
+
async check() {
|
|
149
|
+
throw new Error("database unavailable");
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await expect(fixture.complete(validCompletion())).rejects.toMatchObject({
|
|
155
|
+
code: "CONFLICT",
|
|
156
|
+
});
|
|
157
|
+
expect(fixture.provider.requests).toHaveLength(0);
|
|
158
|
+
expect(
|
|
159
|
+
(await fixture.repository.find(fixture.grant.sessionId))?.status,
|
|
160
|
+
).toBe("revoked");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClientSessionAdmissionPort,
|
|
3
|
+
ClientSessionAttemptAuthorityChecker,
|
|
4
|
+
ClientSessionAttemptAuthorityDecision,
|
|
5
|
+
ClientSessionRecord,
|
|
6
|
+
} from "./contracts.js";
|
|
7
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
8
|
+
import { clientSessionConflict } from "./errors.js";
|
|
9
|
+
import type { ClientSessionRepository } from "./repository.js";
|
|
10
|
+
|
|
11
|
+
export type ClientSessionAttemptRevocationCause =
|
|
12
|
+
| "authorization-drift"
|
|
13
|
+
| "authorization-check-failed"
|
|
14
|
+
| "ttl"
|
|
15
|
+
| "workflow-authority"
|
|
16
|
+
| "workflow-authority-check-failed";
|
|
17
|
+
|
|
18
|
+
export interface ClientSessionAttemptAuthorityHostChecker extends ClientSessionAttemptAuthorityChecker {
|
|
19
|
+
checkTransaction?(
|
|
20
|
+
transaction: NodePgDatabase,
|
|
21
|
+
facts: Parameters<ClientSessionAttemptAuthorityChecker["check"]>[0],
|
|
22
|
+
now: number,
|
|
23
|
+
): Promise<ClientSessionAttemptAuthorityDecision>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 启动时先创建 binding,Workflow runtime 完成装配后再绑定唯一 Host checker。 */
|
|
27
|
+
export class ClientSessionAttemptAuthorityBinding implements ClientSessionAttemptAuthorityChecker {
|
|
28
|
+
#checker?: ClientSessionAttemptAuthorityHostChecker;
|
|
29
|
+
|
|
30
|
+
bind(checker: ClientSessionAttemptAuthorityHostChecker): () => void {
|
|
31
|
+
if (this.#checker) {
|
|
32
|
+
throw new Error("Client Session attempt authority checker 重复绑定");
|
|
33
|
+
}
|
|
34
|
+
this.#checker = checker;
|
|
35
|
+
return () => {
|
|
36
|
+
if (this.#checker === checker) this.#checker = undefined;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
check(
|
|
41
|
+
facts: Parameters<ClientSessionAttemptAuthorityChecker["check"]>[0],
|
|
42
|
+
signal?: AbortSignal,
|
|
43
|
+
): Promise<ClientSessionAttemptAuthorityDecision> {
|
|
44
|
+
return this.#checker?.check(facts, signal) ?? Promise.resolve("ordinary");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
checkTransaction(
|
|
48
|
+
transaction: NodePgDatabase,
|
|
49
|
+
facts: Parameters<ClientSessionAttemptAuthorityChecker["check"]>[0],
|
|
50
|
+
now: number,
|
|
51
|
+
) {
|
|
52
|
+
return this.#checker?.checkTransaction
|
|
53
|
+
? this.#checker.checkTransaction(transaction, facts, now)
|
|
54
|
+
: (this.#checker?.check(facts) ?? Promise.resolve("ordinary" as const));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function enforceClientSessionAttemptAuthority(input: {
|
|
59
|
+
checker: ClientSessionAttemptAuthorityChecker;
|
|
60
|
+
record: ClientSessionRecord;
|
|
61
|
+
repository: ClientSessionRepository;
|
|
62
|
+
signal?: AbortSignal;
|
|
63
|
+
workflowOwned: boolean;
|
|
64
|
+
audit(
|
|
65
|
+
record: ClientSessionRecord,
|
|
66
|
+
cause: ClientSessionAttemptRevocationCause,
|
|
67
|
+
): Promise<void> | void;
|
|
68
|
+
}): Promise<boolean> {
|
|
69
|
+
let decision: ClientSessionAttemptAuthorityDecision;
|
|
70
|
+
try {
|
|
71
|
+
decision = await input.checker.check(
|
|
72
|
+
clientSessionAttemptAuthorityFacts(input.record),
|
|
73
|
+
input.signal,
|
|
74
|
+
);
|
|
75
|
+
} catch {
|
|
76
|
+
await revoke(input, "workflow-authority-check-failed");
|
|
77
|
+
throw clientSessionConflict("Client Agent 会话 authority 校验失败");
|
|
78
|
+
}
|
|
79
|
+
if (decision === "current") return true;
|
|
80
|
+
if (decision === "ordinary" && !input.workflowOwned) return false;
|
|
81
|
+
await revoke(input, "workflow-authority");
|
|
82
|
+
throw clientSessionConflict("Client Agent 会话 authority 已失效");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function enforceClientSessionCurrentAuthorization(input: {
|
|
86
|
+
admission: ClientSessionAdmissionPort;
|
|
87
|
+
record: ClientSessionRecord;
|
|
88
|
+
repository: ClientSessionRepository;
|
|
89
|
+
attempt?: number;
|
|
90
|
+
now: number;
|
|
91
|
+
audit(
|
|
92
|
+
record: ClientSessionRecord,
|
|
93
|
+
cause: ClientSessionAttemptRevocationCause,
|
|
94
|
+
): Promise<void> | void;
|
|
95
|
+
}) {
|
|
96
|
+
const canonical = await input.repository.find(input.record.sessionId);
|
|
97
|
+
if (!canonical || canonical.status !== "active") {
|
|
98
|
+
throw clientSessionConflict("Client Agent 会话已关闭");
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
input.attempt !== undefined &&
|
|
102
|
+
!canonical.pendingAttempts.includes(input.attempt)
|
|
103
|
+
) {
|
|
104
|
+
await revokeCurrentAuthorization(input, canonical);
|
|
105
|
+
throw clientSessionConflict("Client Agent attempt 已失效");
|
|
106
|
+
}
|
|
107
|
+
let cause: ClientSessionAttemptRevocationCause | undefined;
|
|
108
|
+
if (canonical.expiresAt <= input.now) {
|
|
109
|
+
cause = "ttl";
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
cause = (await input.admission.reauthorize(canonical, canonical.actorId))
|
|
113
|
+
? undefined
|
|
114
|
+
: "authorization-drift";
|
|
115
|
+
} catch {
|
|
116
|
+
cause = "authorization-check-failed";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!cause) return;
|
|
120
|
+
const status = cause === "ttl" ? "expired" : "revoked";
|
|
121
|
+
const closed = await input.repository.transition(
|
|
122
|
+
canonical.sessionId,
|
|
123
|
+
"active",
|
|
124
|
+
status,
|
|
125
|
+
);
|
|
126
|
+
if (closed) await input.audit(closed, cause);
|
|
127
|
+
throw clientSessionConflict("Client Agent 会话当前授权已失效");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function enforceClientSessionProviderBoundary(input: {
|
|
131
|
+
admission: ClientSessionAdmissionPort;
|
|
132
|
+
checker: ClientSessionAttemptAuthorityChecker;
|
|
133
|
+
record: ClientSessionRecord;
|
|
134
|
+
repository: ClientSessionRepository;
|
|
135
|
+
attempt?: number;
|
|
136
|
+
now: number;
|
|
137
|
+
signal?: AbortSignal;
|
|
138
|
+
workflowOwned: boolean;
|
|
139
|
+
audit(
|
|
140
|
+
record: ClientSessionRecord,
|
|
141
|
+
cause: ClientSessionAttemptRevocationCause,
|
|
142
|
+
): Promise<void> | void;
|
|
143
|
+
}) {
|
|
144
|
+
await enforceClientSessionCurrentAuthorization(input);
|
|
145
|
+
if (!input.workflowOwned) return;
|
|
146
|
+
await enforceClientSessionAttemptAuthority({
|
|
147
|
+
checker: input.checker,
|
|
148
|
+
record: input.record,
|
|
149
|
+
repository: input.repository,
|
|
150
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
151
|
+
workflowOwned: true,
|
|
152
|
+
audit: input.audit,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function revokeCurrentAuthorization(
|
|
157
|
+
input: Parameters<typeof enforceClientSessionCurrentAuthorization>[0],
|
|
158
|
+
canonical: ClientSessionRecord,
|
|
159
|
+
) {
|
|
160
|
+
const revoked = await input.repository.transition(
|
|
161
|
+
canonical.sessionId,
|
|
162
|
+
"active",
|
|
163
|
+
"revoked",
|
|
164
|
+
);
|
|
165
|
+
if (revoked) await input.audit(revoked, "authorization-check-failed");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function clientSessionAttemptAuthorityFacts(
|
|
169
|
+
record: ClientSessionRecord,
|
|
170
|
+
) {
|
|
171
|
+
return {
|
|
172
|
+
sessionId: record.sessionId,
|
|
173
|
+
clientRunId: record.clientRunId,
|
|
174
|
+
actorId: record.actorId,
|
|
175
|
+
operationKey: record.operationKey,
|
|
176
|
+
clientBindingDigest: record.clientBindingDigest,
|
|
177
|
+
hostSessionDigest: record.hostSessionDigest,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function revoke(
|
|
182
|
+
input: Parameters<typeof enforceClientSessionAttemptAuthority>[0],
|
|
183
|
+
cause: ClientSessionAttemptRevocationCause,
|
|
184
|
+
) {
|
|
185
|
+
const revoked = await input.repository.transition(
|
|
186
|
+
input.record.sessionId,
|
|
187
|
+
"active",
|
|
188
|
+
"revoked",
|
|
189
|
+
);
|
|
190
|
+
if (revoked) await input.audit(revoked, cause);
|
|
191
|
+
}
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
createClientAgentHostAuthRouteApp,
|
|
22
22
|
hostSessionDigest,
|
|
23
23
|
} from "./host-auth.js";
|
|
24
|
+
import { ClientSessionAttemptAuthorityBinding } from "./attempt-authority.js";
|
|
24
25
|
|
|
25
26
|
export function createClientAgentSessionRuntime(input: {
|
|
26
27
|
app: RouteApp;
|
|
@@ -38,12 +39,21 @@ export function createClientAgentSessionRuntime(input: {
|
|
|
38
39
|
registerUnavailableClientAgentSessionRoutes(
|
|
39
40
|
createManifestAuthorizedRouteApp(input.app, input.routeAuthorizer),
|
|
40
41
|
);
|
|
41
|
-
return {
|
|
42
|
+
return {
|
|
43
|
+
service: undefined,
|
|
44
|
+
repository: undefined,
|
|
45
|
+
hostApp: undefined,
|
|
46
|
+
attemptAuthority: undefined,
|
|
47
|
+
revokeHost: undefined,
|
|
48
|
+
dispose: undefined,
|
|
49
|
+
};
|
|
42
50
|
}
|
|
51
|
+
const attemptAuthority = new ClientSessionAttemptAuthorityBinding();
|
|
52
|
+
const repository = input.db
|
|
53
|
+
? new DrizzleClientSessionRepository(input.db, attemptAuthority)
|
|
54
|
+
: new InMemoryClientSessionRepository();
|
|
43
55
|
const service = new GovernedClientAgentSessionService(
|
|
44
|
-
|
|
45
|
-
? new DrizzleClientSessionRepository(input.db)
|
|
46
|
-
: new InMemoryClientSessionRepository(),
|
|
56
|
+
repository,
|
|
47
57
|
new ManifestClientSessionAdmission(
|
|
48
58
|
input.modules,
|
|
49
59
|
input.hosts,
|
|
@@ -64,6 +74,7 @@ export function createClientAgentSessionRuntime(input: {
|
|
|
64
74
|
}),
|
|
65
75
|
new RuntimeClientSessionAudit(input.runtime),
|
|
66
76
|
trustedBaseUrl(),
|
|
77
|
+
attemptAuthority,
|
|
67
78
|
);
|
|
68
79
|
const hostApp = createClientAgentHostAuthRouteApp({
|
|
69
80
|
app: input.app,
|
|
@@ -75,13 +86,18 @@ export function createClientAgentSessionRuntime(input: {
|
|
|
75
86
|
createManifestAuthorizedRouteApp(hostApp, input.routeAuthorizer),
|
|
76
87
|
service,
|
|
77
88
|
);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
return {
|
|
90
|
+
service,
|
|
91
|
+
repository,
|
|
92
|
+
hostApp,
|
|
93
|
+
attemptAuthority,
|
|
94
|
+
revokeHost: (principal: Parameters<typeof hostSessionDigest>[0]) =>
|
|
95
|
+
service.revokeHostSession(
|
|
96
|
+
hostSessionDigest(principal),
|
|
97
|
+
principal.actorId,
|
|
98
|
+
),
|
|
99
|
+
dispose: undefined,
|
|
100
|
+
};
|
|
85
101
|
}
|
|
86
102
|
|
|
87
103
|
function trustedBaseUrl(): string {
|
|
@@ -3,6 +3,7 @@ import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
|
3
3
|
import type {
|
|
4
4
|
ClientAgentProviderPort,
|
|
5
5
|
ClientSessionAdmissionPort,
|
|
6
|
+
ClientSessionAttemptAuthorityChecker,
|
|
6
7
|
ClientSessionCaller,
|
|
7
8
|
ClientSessionRecord,
|
|
8
9
|
ClientSessionReceiptRecord,
|
|
@@ -21,6 +22,11 @@ import {
|
|
|
21
22
|
parseClientAgentCompletion,
|
|
22
23
|
} from "./request.js";
|
|
23
24
|
import { ProviderInputGovernanceError } from "../provider-input-governance.js";
|
|
25
|
+
import {
|
|
26
|
+
enforceClientSessionAttemptAuthority,
|
|
27
|
+
enforceClientSessionProviderBoundary,
|
|
28
|
+
type ClientSessionAttemptRevocationCause,
|
|
29
|
+
} from "./attempt-authority.js";
|
|
24
30
|
|
|
25
31
|
export type ClientAgentCompletionEvent =
|
|
26
32
|
| { readonly type: "chunk"; readonly value: ModuleAiJsonValue }
|
|
@@ -45,6 +51,7 @@ export async function runClientAgentCompletion(input: {
|
|
|
45
51
|
caller: ClientSessionCaller;
|
|
46
52
|
signal?: AbortSignal;
|
|
47
53
|
current: ClientSessionRecord;
|
|
54
|
+
attemptAuthority: ClientSessionAttemptAuthorityChecker;
|
|
48
55
|
repository: ClientSessionRepository;
|
|
49
56
|
admission: ClientSessionAdmissionPort;
|
|
50
57
|
provider: ClientAgentProviderPort;
|
|
@@ -57,10 +64,18 @@ export async function runClientAgentCompletion(input: {
|
|
|
57
64
|
) => Promise<void> | void;
|
|
58
65
|
revokeAudit: (
|
|
59
66
|
record: ClientSessionRecord,
|
|
60
|
-
cause:
|
|
67
|
+
cause: ClientSessionAttemptRevocationCause,
|
|
61
68
|
) => Promise<void> | void;
|
|
62
69
|
}) {
|
|
63
70
|
const parsed = parseCompletion(input.body, input.current);
|
|
71
|
+
const workflowOwned = await enforceClientSessionAttemptAuthority({
|
|
72
|
+
checker: input.attemptAuthority,
|
|
73
|
+
record: input.current,
|
|
74
|
+
repository: input.repository,
|
|
75
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
76
|
+
workflowOwned: false,
|
|
77
|
+
audit: input.revokeAudit,
|
|
78
|
+
});
|
|
64
79
|
const reserved = await input.repository.reserveAttempt({
|
|
65
80
|
sessionId: input.sessionId,
|
|
66
81
|
tokenHash: digestText(input.caller.token),
|
|
@@ -83,24 +98,16 @@ export async function runClientAgentCompletion(input: {
|
|
|
83
98
|
if (checking || controller.signal.aborted) return;
|
|
84
99
|
checking = true;
|
|
85
100
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const status = cause === "ttl" ? "expired" : "revoked";
|
|
97
|
-
const closed = await input.repository.transition(
|
|
98
|
-
input.sessionId,
|
|
99
|
-
"active",
|
|
100
|
-
status,
|
|
101
|
-
);
|
|
102
|
-
if (closed) await input.revokeAudit(closed, cause);
|
|
103
|
-
controller.abort(cause);
|
|
101
|
+
await enforceClientSessionProviderBoundary({
|
|
102
|
+
admission: input.admission,
|
|
103
|
+
checker: input.attemptAuthority,
|
|
104
|
+
record: reserved,
|
|
105
|
+
repository: input.repository,
|
|
106
|
+
now: input.now(),
|
|
107
|
+
signal: controller.signal,
|
|
108
|
+
workflowOwned,
|
|
109
|
+
audit: input.revokeAudit,
|
|
110
|
+
});
|
|
104
111
|
} catch {
|
|
105
112
|
controller.abort("authorization-check-failed");
|
|
106
113
|
} finally {
|
|
@@ -112,6 +119,7 @@ export async function runClientAgentCompletion(input: {
|
|
|
112
119
|
abort: () => controller.abort(),
|
|
113
120
|
events: completionEvents({
|
|
114
121
|
...input,
|
|
122
|
+
workflowOwned,
|
|
115
123
|
parsed,
|
|
116
124
|
reserved,
|
|
117
125
|
attempt,
|
|
@@ -135,6 +143,8 @@ async function* completionEvents(input: {
|
|
|
135
143
|
watcher: ReturnType<typeof setInterval>;
|
|
136
144
|
repository: ClientSessionRepository;
|
|
137
145
|
admission: ClientSessionAdmissionPort;
|
|
146
|
+
attemptAuthority: ClientSessionAttemptAuthorityChecker;
|
|
147
|
+
workflowOwned: boolean;
|
|
138
148
|
provider: ClientAgentProviderPort;
|
|
139
149
|
controllers: Map<string, AbortController>;
|
|
140
150
|
now: () => number;
|
|
@@ -143,6 +153,10 @@ async function* completionEvents(input: {
|
|
|
143
153
|
outcome: "success" | "failure",
|
|
144
154
|
metadata: Readonly<Record<string, string | number | boolean>>,
|
|
145
155
|
) => Promise<void> | void;
|
|
156
|
+
revokeAudit: (
|
|
157
|
+
record: ClientSessionRecord,
|
|
158
|
+
cause: ClientSessionAttemptRevocationCause,
|
|
159
|
+
) => Promise<void> | void;
|
|
146
160
|
}): AsyncGenerator<ClientAgentCompletionEvent> {
|
|
147
161
|
let settled = false;
|
|
148
162
|
let audited = false;
|
|
@@ -151,6 +165,17 @@ async function* completionEvents(input: {
|
|
|
151
165
|
let deploymentDigest: `sha256:${string}` | undefined;
|
|
152
166
|
let responseBytes = 0;
|
|
153
167
|
let usage: Readonly<Record<string, number>> = {};
|
|
168
|
+
await enforceClientSessionProviderBoundary({
|
|
169
|
+
admission: input.admission,
|
|
170
|
+
checker: input.attemptAuthority,
|
|
171
|
+
record: input.reserved,
|
|
172
|
+
repository: input.repository,
|
|
173
|
+
attempt: input.attempt,
|
|
174
|
+
now: input.now(),
|
|
175
|
+
signal: input.controller.signal,
|
|
176
|
+
workflowOwned: input.workflowOwned,
|
|
177
|
+
audit: input.revokeAudit,
|
|
178
|
+
});
|
|
154
179
|
for await (const event of input.provider.stream(
|
|
155
180
|
input.parsed.request,
|
|
156
181
|
{
|
|
@@ -187,6 +212,21 @@ async function* completionEvents(input: {
|
|
|
187
212
|
}
|
|
188
213
|
if (!deploymentDigest) throw new Error("Provider meta 缺失");
|
|
189
214
|
const response = accumulator.result();
|
|
215
|
+
await enforceClientSessionProviderBoundary({
|
|
216
|
+
admission: input.admission,
|
|
217
|
+
checker: input.attemptAuthority,
|
|
218
|
+
record: input.reserved,
|
|
219
|
+
repository: input.repository,
|
|
220
|
+
attempt: input.attempt,
|
|
221
|
+
now: input.now(),
|
|
222
|
+
signal: input.controller.signal,
|
|
223
|
+
workflowOwned: input.workflowOwned,
|
|
224
|
+
audit: input.revokeAudit,
|
|
225
|
+
});
|
|
226
|
+
const canonicalResult = await input.admission.projectProviderResult(
|
|
227
|
+
input.reserved.operationKey,
|
|
228
|
+
response,
|
|
229
|
+
);
|
|
190
230
|
const receipt: ClientSessionReceiptRecord = {
|
|
191
231
|
receiptId: `receipt_${randomUUID()}`,
|
|
192
232
|
clientRunId: input.reserved.clientRunId,
|
|
@@ -199,12 +239,23 @@ async function* completionEvents(input: {
|
|
|
199
239
|
responseDigest: digestJson(response),
|
|
200
240
|
deploymentDigest,
|
|
201
241
|
issuedAt: input.now(),
|
|
202
|
-
canonicalResult
|
|
203
|
-
input.reserved.operationKey,
|
|
204
|
-
response,
|
|
205
|
-
),
|
|
242
|
+
canonicalResult,
|
|
206
243
|
};
|
|
207
|
-
|
|
244
|
+
const committed = await input.repository.completeAttempt(
|
|
245
|
+
input.sessionId,
|
|
246
|
+
receipt,
|
|
247
|
+
);
|
|
248
|
+
if (!committed) {
|
|
249
|
+
if (input.workflowOwned) {
|
|
250
|
+
await enforceClientSessionAttemptAuthority({
|
|
251
|
+
checker: input.attemptAuthority,
|
|
252
|
+
record: input.reserved,
|
|
253
|
+
repository: input.repository,
|
|
254
|
+
signal: input.controller.signal,
|
|
255
|
+
workflowOwned: true,
|
|
256
|
+
audit: input.revokeAudit,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
208
259
|
throw clientSessionConflict("模型响应到达时会话已关闭");
|
|
209
260
|
}
|
|
210
261
|
settled = true;
|
|
@@ -76,6 +76,28 @@ export interface ClientSessionCaller {
|
|
|
76
76
|
readonly hostSessionDigest: `sha256:${string}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export type ClientSessionAttemptAuthorityDecision =
|
|
80
|
+
| "ordinary"
|
|
81
|
+
| "current"
|
|
82
|
+
| "revoked";
|
|
83
|
+
|
|
84
|
+
export interface ClientSessionAttemptAuthorityFacts {
|
|
85
|
+
readonly sessionId: string;
|
|
86
|
+
readonly clientRunId: string;
|
|
87
|
+
readonly actorId: string;
|
|
88
|
+
readonly operationKey: string;
|
|
89
|
+
readonly clientBindingDigest: `sha256:${string}`;
|
|
90
|
+
readonly hostSessionDigest: `sha256:${string}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Host checker 必须从 canonical Workflow/command/fence 事实判定,不接受浏览器声明。 */
|
|
94
|
+
export interface ClientSessionAttemptAuthorityChecker {
|
|
95
|
+
check(
|
|
96
|
+
facts: ClientSessionAttemptAuthorityFacts,
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
): Promise<ClientSessionAttemptAuthorityDecision>;
|
|
99
|
+
}
|
|
100
|
+
|
|
79
101
|
export interface ClientAgentProviderPort {
|
|
80
102
|
readonly modelAlias: string;
|
|
81
103
|
ready(signal?: AbortSignal): Promise<boolean>;
|
|
@@ -105,6 +127,7 @@ export interface ClientSessionAdmissionPort {
|
|
|
105
127
|
readonly input: ModuleAiJsonValue;
|
|
106
128
|
readonly clientBindingDigest: `sha256:${string}`;
|
|
107
129
|
readonly actorId: string;
|
|
130
|
+
readonly launchAuthority: "direct" | "workflow-artifact";
|
|
108
131
|
}): Promise<ClientSessionAdmission>;
|
|
109
132
|
reauthorize(session: ClientSessionRecord, actorId: string): Promise<boolean>;
|
|
110
133
|
validateResult(
|