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.
Files changed (160) hide show
  1. package/dist/cli.js +215 -24
  2. package/package.json +1 -1
  3. package/template/.agents/skills/windy-business-module/SKILL.md +58 -8
  4. package/template/.windy-template.json +2 -2
  5. package/template/apps/server/src/agent/client-session/attempt-authority.test.ts +162 -0
  6. package/template/apps/server/src/agent/client-session/attempt-authority.ts +191 -0
  7. package/template/apps/server/src/agent/client-session/bootstrap.ts +27 -11
  8. package/template/apps/server/src/agent/client-session/completion.ts +75 -24
  9. package/template/apps/server/src/agent/client-session/contracts.ts +23 -0
  10. package/template/apps/server/src/agent/client-session/creation.ts +103 -0
  11. package/template/apps/server/src/agent/client-session/digest.ts +11 -1
  12. package/template/apps/server/src/agent/client-session/drizzle-records.ts +150 -0
  13. package/template/apps/server/src/agent/client-session/drizzle-repository.integration.test.ts +30 -3
  14. package/template/apps/server/src/agent/client-session/drizzle-repository.ts +23 -132
  15. package/template/apps/server/src/agent/client-session/finalization.ts +99 -0
  16. package/template/apps/server/src/agent/client-session/host-auth.ts +22 -10
  17. package/template/apps/server/src/agent/client-session/host-revocation-composition.test.ts +77 -0
  18. package/template/apps/server/src/agent/client-session/host-revocation-composition.ts +28 -0
  19. package/template/apps/server/src/agent/client-session/launch-authority.contract.test.ts +21 -0
  20. package/template/apps/server/src/agent/client-session/manifest-admission.ts +11 -0
  21. package/template/apps/server/src/agent/client-session/service.test-support.ts +22 -0
  22. package/template/apps/server/src/agent/client-session/service.ts +104 -134
  23. package/template/apps/server/src/agent/client-session/terminal-digest.test.ts +23 -0
  24. package/template/apps/server/src/agent/client-workflow-artifact/active-blob-references.ts +8 -0
  25. package/template/apps/server/src/agent/client-workflow-artifact/artifact-builder.ts +60 -0
  26. package/template/apps/server/src/agent/client-workflow-artifact/authority-current.ts +59 -0
  27. package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.test.ts +189 -0
  28. package/template/apps/server/src/agent/client-workflow-artifact/binding-registry.ts +153 -0
  29. package/template/apps/server/src/agent/client-workflow-artifact/bootstrap.ts +77 -0
  30. package/template/apps/server/src/agent/client-workflow-artifact/cancellation.ts +47 -0
  31. package/template/apps/server/src/agent/client-workflow-artifact/completed-recovery.ts +31 -0
  32. package/template/apps/server/src/agent/client-workflow-artifact/contracts.ts +104 -0
  33. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-locks.ts +30 -0
  34. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-mapper.ts +138 -0
  35. package/template/apps/server/src/agent/client-workflow-artifact/drizzle-repository.ts +346 -0
  36. package/template/apps/server/src/agent/client-workflow-artifact/error.ts +21 -0
  37. package/template/apps/server/src/agent/client-workflow-artifact/handler.ts +167 -0
  38. package/template/apps/server/src/agent/client-workflow-artifact/host-revocation.ts +29 -0
  39. package/template/apps/server/src/agent/client-workflow-artifact/in-memory-repository.ts +232 -0
  40. package/template/apps/server/src/agent/client-workflow-artifact/job-port.ts +101 -0
  41. package/template/apps/server/src/agent/client-workflow-artifact/presentation.ts +41 -0
  42. package/template/apps/server/src/agent/client-workflow-artifact/recovery.contract.test.ts +253 -0
  43. package/template/apps/server/src/agent/client-workflow-artifact/release-binding.ts +85 -0
  44. package/template/apps/server/src/agent/client-workflow-artifact/repository.test.ts +204 -0
  45. package/template/apps/server/src/agent/client-workflow-artifact/repository.ts +97 -0
  46. package/template/apps/server/src/agent/client-workflow-artifact/routes-handler.contract.test.ts +201 -0
  47. package/template/apps/server/src/agent/client-workflow-artifact/routes.ts +161 -0
  48. package/template/apps/server/src/agent/client-workflow-artifact/runtime-composition.ts +101 -0
  49. package/template/apps/server/src/agent/client-workflow-artifact/scope-authority.ts +39 -0
  50. package/template/apps/server/src/agent/client-workflow-artifact/service-authorization.ts +117 -0
  51. package/template/apps/server/src/agent/client-workflow-artifact/service-support.ts +28 -0
  52. package/template/apps/server/src/agent/client-workflow-artifact/service.contract.test.ts +345 -0
  53. package/template/apps/server/src/agent/client-workflow-artifact/service.ts +350 -0
  54. package/template/apps/server/src/agent/client-workflow-artifact/session-boundary.ts +39 -0
  55. package/template/apps/server/src/agent/client-workflow-artifact/source-access.contract.test.ts +98 -0
  56. package/template/apps/server/src/agent/client-workflow-artifact/source-access.ts +80 -0
  57. package/template/apps/server/src/agent/client-workflow-artifact/takeover.ts +122 -0
  58. package/template/apps/server/src/agent/client-workflow-artifact/task-authority.contract.test.ts +97 -0
  59. package/template/apps/server/src/agent/client-workflow-artifact/task-authority.ts +147 -0
  60. package/template/apps/server/src/agent/client-workflow-artifact/terminal-authority.contract.test.ts +100 -0
  61. package/template/apps/server/src/agent/client-workflow-artifact/terminal-replay.ts +31 -0
  62. package/template/apps/server/src/agent/client-workflow-artifact/terminal-seal.ts +211 -0
  63. package/template/apps/server/src/agent/client-workflow-artifact/workflow-artifact.test-support.ts +173 -0
  64. package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.contract.test.ts +254 -0
  65. package/template/apps/server/src/agent/client-workflow-artifact/workflow-loader.ts +285 -0
  66. package/template/apps/server/src/agent/run-facade.test-support.ts +17 -4
  67. package/template/apps/server/src/agent/run-facade.test.ts +16 -0
  68. package/template/apps/server/src/agent/run-session-authorization.ts +12 -10
  69. package/template/apps/server/src/agent/runtime-bootstrap-input.ts +5 -1
  70. package/template/apps/server/src/agent/runtime-bootstrap.ts +33 -33
  71. package/template/apps/server/src/agent/runtime-public-ports.ts +40 -0
  72. package/template/apps/server/src/agent/session-invalidation/contracts.ts +25 -26
  73. package/template/apps/server/src/agent/session-projection-access.ts +31 -19
  74. package/template/apps/server/src/agent/unavailable-routes.ts +21 -0
  75. package/template/apps/server/src/agent/workflow-runtime-bootstrap.ts +2 -0
  76. package/template/apps/server/src/agent-workflow/artifact-source-authorization.ts +12 -6
  77. package/template/apps/server/src/agent-workflow/command-facade.ts +70 -11
  78. package/template/apps/server/src/agent-workflow/drizzle-artifact-port.ts +9 -9
  79. package/template/apps/server/src/agent-workflow/drizzle-delegation-completion.ts +1 -1
  80. package/template/apps/server/src/agent-workflow/platform-node-adapters.ts +14 -6
  81. package/template/apps/server/src/agent-workflow/project-launch-authority.contract.test.ts +53 -0
  82. package/template/apps/server/src/agent-workflow/project-launch-authority.ts +92 -0
  83. package/template/apps/server/src/agent-workflow/runtime-composition.ts +30 -14
  84. package/template/apps/server/src/app/agent-runtime-composition.ts +6 -1
  85. package/template/apps/server/src/app/client-agent-workflow-artifact-binding.ts +14 -0
  86. package/template/apps/server/src/application-services.ts +6 -0
  87. package/template/apps/server/src/data-access/scope-containment.test.ts +31 -0
  88. package/template/apps/server/src/data-access/scope-containment.ts +48 -0
  89. package/template/apps/server/src/jobs/artifact-publication.test.ts +192 -0
  90. package/template/apps/server/src/jobs/artifact-publication.ts +56 -0
  91. package/template/apps/server/src/jobs/artifact-publisher.ts +101 -0
  92. package/template/apps/server/src/jobs/drizzle-artifact.ts +73 -1
  93. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +14 -1
  94. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +65 -0
  95. package/template/apps/server/src/jobs/drizzle-repository.integration.test.ts +1 -0
  96. package/template/apps/server/src/jobs/execution-repository.ts +28 -0
  97. package/template/apps/server/src/jobs/execution.test.ts +3 -1
  98. package/template/apps/server/src/jobs/in-memory-execution.ts +16 -0
  99. package/template/apps/server/src/jobs/in-memory-lease.ts +45 -1
  100. package/template/apps/server/src/jobs/legacy-adapter.integration.test.ts +2 -0
  101. package/template/apps/server/src/jobs/postgres-cleanup-contract.test.ts +58 -0
  102. package/template/apps/server/src/jobs/worker.ts +8 -14
  103. package/template/apps/server/src/module-host/background-ai-unavailable.ts +18 -0
  104. package/template/apps/server/src/module-host/durable-context.ts +19 -22
  105. package/template/apps/server/src/module-host/implementation-completeness.test.ts +80 -1
  106. package/template/apps/server/src/module-host/implementation-completeness.ts +28 -1
  107. package/template/apps/server/src/module-host/request-context.ts +32 -8
  108. package/template/apps/server/src/module-host/storage-port.ts +3 -0
  109. package/template/apps/server/src/module-host/workflow-input-artifact-port.test.ts +104 -0
  110. package/template/apps/server/src/module-host/workflow-input-artifact-port.ts +229 -0
  111. package/template/apps/server/src/module-host/workflow-input-materialization.test.ts +163 -0
  112. package/template/apps/server/src/module-host/workflow-input-materialization.ts +161 -0
  113. package/template/apps/server/src/workflow/drizzle-repository.integration.test.ts +1 -0
  114. package/template/docs/architecture/agent-workflow.md +13 -2
  115. package/template/docs/architecture/durable-jobs.md +7 -0
  116. package/template/docs/operations/agent-session-memory.md +2 -1
  117. package/template/docs/operations/agent-session-troubleshooting.md +87 -2
  118. package/template/docs/operations/agent-workflow-recovery.md +25 -14
  119. package/template/docs/platform/client-agent-session.md +148 -2
  120. package/template/packages/ai-client/package.json +2 -1
  121. package/template/packages/ai-client/src/client-agent/index.ts +1 -0
  122. package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
  123. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
  124. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
  125. package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
  126. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
  127. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
  128. package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
  129. package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
  130. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  131. package/template/packages/database/package.json +1 -1
  132. package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
  133. package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
  134. package/template/packages/database/src/schema/index.ts +1 -0
  135. package/template/packages/jobs/package.json +1 -1
  136. package/template/packages/jobs/src/handlers.test.ts +10 -0
  137. package/template/packages/jobs/src/handlers.ts +28 -11
  138. package/template/packages/modules/package.json +1 -1
  139. package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
  140. package/template/packages/modules/src/manifest.ts +2 -0
  141. package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
  142. package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
  143. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  144. package/template/packages/server-sdk/index.ts +52 -0
  145. package/template/packages/server-sdk/package.json +5 -1
  146. package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
  147. package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
  148. package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
  149. package/template/packages/server-sdk/src/durable-handler.ts +7 -0
  150. package/template/packages/server-sdk/src/request-context.ts +7 -0
  151. package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
  152. package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
  153. package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
  154. package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
  155. package/template/packages/storage/package.json +1 -1
  156. package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
  157. package/template/packages/storage/src/blob-store.ts +9 -0
  158. package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
  159. package/template/packages/storage/src/local-file-blob-store.ts +53 -1
  160. 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 { service: undefined, dispose: undefined };
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
- input.db
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
- const unbind = input.hostAuth.bindRevocations(async (principal) => {
79
- await service.revokeHostSession(
80
- hostSessionDigest(principal),
81
- principal.actorId,
82
- );
83
- });
84
- return { service, dispose: unbind };
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: "authorization-drift" | "ttl",
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
- const cause =
87
- input.current.expiresAt <= input.now()
88
- ? "ttl"
89
- : (await input.admission.reauthorize(
90
- input.current,
91
- input.caller.actorId,
92
- ))
93
- ? undefined
94
- : "authorization-drift";
95
- if (!cause) return;
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: await input.admission.projectProviderResult(
203
- input.reserved.operationKey,
204
- response,
205
- ),
242
+ canonicalResult,
206
243
  };
207
- if (!(await input.repository.completeAttempt(input.sessionId, receipt))) {
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(