create-windy 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/dist/cli.js +1008 -61
  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/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +2 -1
  115. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +16 -0
  116. package/template/docs/architecture/agent-workflow.md +13 -2
  117. package/template/docs/architecture/durable-jobs.md +7 -0
  118. package/template/docs/operations/agent-session-memory.md +2 -1
  119. package/template/docs/operations/agent-session-troubleshooting.md +87 -2
  120. package/template/docs/operations/agent-workflow-recovery.md +25 -14
  121. package/template/docs/platform/client-agent-session.md +148 -2
  122. package/template/packages/ai-client/package.json +2 -1
  123. package/template/packages/ai-client/src/client-agent/index.ts +1 -0
  124. package/template/packages/ai-client/src/client-agent/workflow-artifact/contracts.ts +85 -0
  125. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.test.ts +49 -0
  126. package/template/packages/ai-client/src/client-agent/workflow-artifact/http.ts +82 -0
  127. package/template/packages/ai-client/src/client-agent/workflow-artifact/index.ts +3 -0
  128. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.test.ts +154 -0
  129. package/template/packages/ai-client/src/client-agent/workflow-artifact/validation.ts +332 -0
  130. package/template/packages/database/drizzle/0062_client_agent_workflow_artifact.sql +63 -0
  131. package/template/packages/database/drizzle/meta/0062_snapshot.json +14839 -0
  132. package/template/packages/database/drizzle/meta/_journal.json +7 -0
  133. package/template/packages/database/package.json +1 -1
  134. package/template/packages/database/src/agent-runtime-schema.test.ts +177 -0
  135. package/template/packages/database/src/schema/client-agent-workflow-artifact.ts +158 -0
  136. package/template/packages/database/src/schema/index.ts +1 -0
  137. package/template/packages/jobs/package.json +1 -1
  138. package/template/packages/jobs/src/handlers.test.ts +10 -0
  139. package/template/packages/jobs/src/handlers.ts +28 -11
  140. package/template/packages/modules/package.json +1 -1
  141. package/template/packages/modules/src/ai-client-execution-policy.ts +6 -0
  142. package/template/packages/modules/src/manifest.ts +2 -0
  143. package/template/packages/modules/src/system-agent-audit-actions.ts +3 -0
  144. package/template/packages/modules/src/system-ai-api-bindings.ts +15 -0
  145. package/template/packages/modules/src/system-audit-actions.ts +15 -0
  146. package/template/packages/server-sdk/index.ts +52 -0
  147. package/template/packages/server-sdk/package.json +5 -1
  148. package/template/packages/server-sdk/src/client-agent-session.ts +13 -1
  149. package/template/packages/server-sdk/src/client-agent-workflow-artifact.contract.test.ts +113 -0
  150. package/template/packages/server-sdk/src/client-agent-workflow-artifact.ts +183 -0
  151. package/template/packages/server-sdk/src/durable-handler.ts +7 -0
  152. package/template/packages/server-sdk/src/request-context.ts +7 -0
  153. package/template/packages/server-sdk/src/workflow-input-artifact-port.ts +104 -0
  154. package/template/packages/server-sdk/src/workflow-input-artifact-validation.contract.test.ts +124 -0
  155. package/template/packages/server-sdk/src/workflow-input-artifact-validation.ts +347 -0
  156. package/template/packages/server-sdk/src/workflow-input-materialization-handler.ts +43 -0
  157. package/template/packages/storage/package.json +1 -1
  158. package/template/packages/storage/src/blob-store-put-once.test.ts +70 -0
  159. package/template/packages/storage/src/blob-store.ts +9 -0
  160. package/template/packages/storage/src/local-file-blob-put-once.ts +120 -0
  161. package/template/packages/storage/src/local-file-blob-store.ts +53 -1
  162. package/template/packages/storage/src/memory-blob-store.ts +73 -33
@@ -0,0 +1,285 @@
1
+ import type { AgentWorkflowNode } from "@southwind-ai/shared";
2
+ import { CLIENT_AGENT_WORKFLOW_ARTIFACT_WORK_ID_PREFIX } from "@southwind-ai/server-sdk";
3
+ import type { DurableJobService } from "../../jobs/service.js";
4
+ import { hashAuthorizationScope } from "../../data-access/scope-snapshot.js";
5
+ import { digestJson } from "../client-session/digest.js";
6
+ import { DrizzleAgentWorkflowRepository } from "../../agent-workflow/drizzle-repository.js";
7
+ import { ReceiptAgentWorkflowNodeInputPort } from "../../agent-workflow/node-input-receipt-port.js";
8
+ import { schedulerAdapterContext } from "../../agent-workflow/scheduler-context.js";
9
+ import {
10
+ clientWorkflowBindingCommitment,
11
+ ClientAgentWorkflowArtifactBindingRegistry,
12
+ } from "./binding-registry.js";
13
+ import { assertClientAgentWorkflowReleaseBinding } from "./release-binding.js";
14
+ import {
15
+ clientAgentWorkflowArtifactTaskKey,
16
+ type ClientAgentWorkflowBeginFacts,
17
+ } from "./contracts.js";
18
+ import { requireClientWorkflowFrozenScope } from "./scope-authority.js";
19
+
20
+ export interface ClientAgentWorkflowJobPayload {
21
+ readonly commandId: string;
22
+ readonly runId: string;
23
+ readonly nodeRunId: string;
24
+ readonly effectGeneration?: number;
25
+ readonly requestDigest?: string;
26
+ }
27
+
28
+ type WorkflowSnapshot = NonNullable<
29
+ Awaited<ReturnType<DrizzleAgentWorkflowRepository["getSnapshot"]>>
30
+ >;
31
+
32
+ export class ClientAgentWorkflowFactsLoader {
33
+ readonly #inputs = new ReceiptAgentWorkflowNodeInputPort();
34
+
35
+ constructor(
36
+ private readonly workflows: DrizzleAgentWorkflowRepository,
37
+ private readonly jobs: DurableJobService,
38
+ private readonly bindings: ClientAgentWorkflowArtifactBindingRegistry,
39
+ ) {}
40
+
41
+ async load(input: {
42
+ jobId: string;
43
+ payload: ClientAgentWorkflowJobPayload;
44
+ signal: AbortSignal;
45
+ }): Promise<ClientAgentWorkflowBeginFacts> {
46
+ const [snapshot, job] = await Promise.all([
47
+ this.workflows.getSnapshot(input.payload.runId),
48
+ this.jobs.get(input.jobId),
49
+ ]);
50
+ if (!snapshot || !job) throw invalid("WORKFLOW_FACTS_NOT_FOUND");
51
+ const release = await this.workflows.getRelease(snapshot.run.releaseId);
52
+ const nodeRun = snapshot.nodeRuns.find(
53
+ ({ nodeRunId }) => nodeRunId === input.payload.nodeRunId,
54
+ );
55
+ const command = snapshot.sideEffects.find(
56
+ ({ commandId }) => commandId === input.payload.commandId,
57
+ );
58
+ const node = release?.definition.nodes.find(
59
+ ({ id }) => id === nodeRun?.nodeId,
60
+ );
61
+ if (!release || !nodeRun || !command || !node) {
62
+ throw invalid("WORKFLOW_COORDINATE_NOT_FOUND");
63
+ }
64
+ assertJobAndFence({ input, snapshot, job, nodeRun, command, node });
65
+ const binding = this.bindings.require(
66
+ release.definition.workflowKey,
67
+ node.id,
68
+ );
69
+ assertClientAgentWorkflowReleaseBinding(release, binding);
70
+ const context = schedulerAdapterContext({
71
+ snapshot,
72
+ release,
73
+ nodeRun,
74
+ node,
75
+ signal: input.signal,
76
+ });
77
+ const value = await this.#inputs.readJson({
78
+ context,
79
+ ...(node.inputSchemaKey ? { schemaKey: node.inputSchemaKey } : {}),
80
+ });
81
+ const record: ClientAgentWorkflowBeginFacts["record"] = {
82
+ workId: workflowWorkId(command.commandId),
83
+ commandId: command.commandId,
84
+ durableJobId: input.jobId,
85
+ runId: snapshot.run.runId,
86
+ nodeRunId: nodeRun.nodeRunId,
87
+ effectKind: "job",
88
+ taskKey: clientAgentWorkflowArtifactTaskKey,
89
+ operationKey: binding.operationKey,
90
+ bindingDigest: digestJson(clientWorkflowBindingCommitment(binding)),
91
+ actorId: snapshot.run.actorId,
92
+ scopeDigest: requireDigest(snapshot.run.scopeDigest),
93
+ authorizationSnapshotDigest: requireDigest(
94
+ command.authorizationSnapshotDigest,
95
+ ),
96
+ runPolicyDigest: requireDigest(command.runPolicyDigest),
97
+ requestDigest: requireDigest(command.requestDigest),
98
+ inputDigest: digestJson(value),
99
+ effectGeneration: command.generation,
100
+ effectFenceTokenDigest: requireDigest(command.fenceTokenDigest),
101
+ status: "pending",
102
+ claimGeneration: 0,
103
+ historySequence: 0,
104
+ version: 1,
105
+ createdAt: Date.now(),
106
+ updatedAt: Date.now(),
107
+ };
108
+ return {
109
+ input: value,
110
+ inputArtifacts: context.inputArtifacts,
111
+ binding,
112
+ frozenScope: requireClientWorkflowFrozenScope(record, job),
113
+ record,
114
+ };
115
+ }
116
+
117
+ async loadTerminal(input: {
118
+ record: ClientAgentWorkflowBeginFacts["record"];
119
+ signal: AbortSignal;
120
+ }): Promise<
121
+ Pick<
122
+ ClientAgentWorkflowBeginFacts,
123
+ "binding" | "input" | "inputArtifacts" | "frozenScope"
124
+ >
125
+ > {
126
+ const [snapshot, job] = await Promise.all([
127
+ this.workflows.getSnapshot(input.record.runId),
128
+ this.jobs.get(input.record.durableJobId),
129
+ ]);
130
+ if (!snapshot || !job) throw invalid("WORKFLOW_FACTS_NOT_FOUND");
131
+ const release = await this.workflows.getRelease(snapshot.run.releaseId);
132
+ const nodeRun = snapshot.nodeRuns.find(
133
+ ({ nodeRunId }) => nodeRunId === input.record.nodeRunId,
134
+ );
135
+ const command = snapshot.sideEffects.find(
136
+ ({ commandId }) => commandId === input.record.commandId,
137
+ );
138
+ const node = release?.definition.nodes.find(
139
+ ({ id }) => id === nodeRun?.nodeId,
140
+ );
141
+ if (!release || !nodeRun || !command || !node) {
142
+ throw invalid("WORKFLOW_COORDINATE_NOT_FOUND");
143
+ }
144
+ const binding = this.bindings.require(
145
+ release.definition.workflowKey,
146
+ node.id,
147
+ );
148
+ assertClientAgentWorkflowReleaseBinding(release, binding);
149
+ const context = schedulerAdapterContext({
150
+ snapshot,
151
+ release,
152
+ nodeRun,
153
+ node,
154
+ signal: input.signal,
155
+ });
156
+ const value = await this.#inputs.readJson({
157
+ context,
158
+ ...(node.inputSchemaKey ? { schemaKey: node.inputSchemaKey } : {}),
159
+ });
160
+ assertTerminalIdentity({
161
+ record: input.record,
162
+ snapshot,
163
+ release,
164
+ nodeRun,
165
+ command,
166
+ node,
167
+ binding,
168
+ inputDigest: digestJson(value),
169
+ });
170
+ return {
171
+ binding,
172
+ input: value,
173
+ inputArtifacts: context.inputArtifacts,
174
+ frozenScope: requireClientWorkflowFrozenScope(input.record, job),
175
+ };
176
+ }
177
+ }
178
+
179
+ function assertJobAndFence(input: {
180
+ input: {
181
+ jobId: string;
182
+ payload: ClientAgentWorkflowJobPayload;
183
+ };
184
+ snapshot: WorkflowSnapshot;
185
+ job: NonNullable<Awaited<ReturnType<DurableJobService["get"]>>>;
186
+ nodeRun: WorkflowSnapshot["nodeRuns"][number];
187
+ command: WorkflowSnapshot["sideEffects"][number];
188
+ node: AgentWorkflowNode;
189
+ }) {
190
+ const { payload } = input.input;
191
+ const { job } = input.job;
192
+ if (
193
+ job.id !== input.input.jobId ||
194
+ job.handlerKey !== clientAgentWorkflowArtifactTaskKey ||
195
+ job.requestedBy !== input.snapshot.run.actorId ||
196
+ !job.authorizationScope ||
197
+ `sha256:${hashAuthorizationScope(job.authorizationScope)}` !==
198
+ input.snapshot.run.scopeDigest ||
199
+ input.node.type !== "job" ||
200
+ input.node.taskKey !== clientAgentWorkflowArtifactTaskKey ||
201
+ input.nodeRun.nodeType !== "job" ||
202
+ input.nodeRun.runId !== input.snapshot.run.runId ||
203
+ input.command.runId !== input.snapshot.run.runId ||
204
+ input.command.nodeRunId !== input.nodeRun.nodeRunId ||
205
+ input.command.effectKind !== "job" ||
206
+ input.command.status !== "dispatched" ||
207
+ input.command.generation < 1 ||
208
+ !input.command.fenceTokenDigest ||
209
+ input.command.authorizationSnapshotDigest !==
210
+ input.snapshot.run.authorizationSnapshotDigest ||
211
+ input.command.runPolicyDigest !==
212
+ input.snapshot.run.executionPolicyDigest ||
213
+ (payload.effectGeneration !== undefined &&
214
+ payload.effectGeneration !== input.command.generation) ||
215
+ (payload.requestDigest !== undefined &&
216
+ payload.requestDigest !== input.command.requestDigest)
217
+ ) {
218
+ throw invalid("WORKFLOW_EFFECT_FENCE_INVALID");
219
+ }
220
+ }
221
+
222
+ function assertTerminalIdentity(input: {
223
+ record: ClientAgentWorkflowBeginFacts["record"];
224
+ snapshot: WorkflowSnapshot;
225
+ release: NonNullable<
226
+ Awaited<ReturnType<DrizzleAgentWorkflowRepository["getRelease"]>>
227
+ >;
228
+ nodeRun: WorkflowSnapshot["nodeRuns"][number];
229
+ command: WorkflowSnapshot["sideEffects"][number];
230
+ node: AgentWorkflowNode;
231
+ binding: ClientAgentWorkflowBeginFacts["binding"];
232
+ inputDigest: `sha256:${string}`;
233
+ }) {
234
+ const { record, snapshot, release, nodeRun, command, node, binding } = input;
235
+ if (
236
+ snapshot.run.releaseId !== release.releaseId ||
237
+ snapshot.run.runId !== record.runId ||
238
+ snapshot.run.actorId !== record.actorId ||
239
+ snapshot.run.scopeDigest !== record.scopeDigest ||
240
+ snapshot.run.authorizationSnapshotDigest !==
241
+ record.authorizationSnapshotDigest ||
242
+ snapshot.run.executionPolicyDigest !== record.runPolicyDigest ||
243
+ !["running", "waiting", "completed"].includes(snapshot.run.status) ||
244
+ nodeRun.nodeRunId !== record.nodeRunId ||
245
+ nodeRun.runId !== record.runId ||
246
+ nodeRun.nodeId !== binding.nodeId ||
247
+ nodeRun.nodeType !== "job" ||
248
+ !["running", "waiting", "completed"].includes(nodeRun.status) ||
249
+ node.type !== "job" ||
250
+ node.taskKey !== clientAgentWorkflowArtifactTaskKey ||
251
+ command.commandId !== record.commandId ||
252
+ command.runId !== record.runId ||
253
+ command.nodeRunId !== record.nodeRunId ||
254
+ command.effectKind !== record.effectKind ||
255
+ command.effectKey !== node.id ||
256
+ command.adapter.kind !== "durable-job" ||
257
+ !["dispatched", "committed", "reconciled"].includes(command.status) ||
258
+ command.generation !== record.effectGeneration ||
259
+ command.requestDigest !== record.requestDigest ||
260
+ command.fenceTokenDigest !== record.effectFenceTokenDigest ||
261
+ command.authorizationSnapshotDigest !==
262
+ record.authorizationSnapshotDigest ||
263
+ command.runPolicyDigest !== record.runPolicyDigest ||
264
+ record.taskKey !== clientAgentWorkflowArtifactTaskKey ||
265
+ record.operationKey !== binding.operationKey ||
266
+ record.bindingDigest !==
267
+ digestJson(clientWorkflowBindingCommitment(binding)) ||
268
+ record.inputDigest !== input.inputDigest
269
+ ) {
270
+ throw invalid("WORKFLOW_TERMINAL_IDENTITY_INVALID");
271
+ }
272
+ }
273
+
274
+ function workflowWorkId(commandId: string) {
275
+ return `${CLIENT_AGENT_WORKFLOW_ARTIFACT_WORK_ID_PREFIX}${digestJson({ commandId }).slice("sha256:".length, 53)}`;
276
+ }
277
+
278
+ function requireDigest(value: string | undefined): `sha256:${string}` {
279
+ if (!value?.startsWith("sha256:")) throw invalid("DIGEST_INVALID");
280
+ return value as `sha256:${string}`;
281
+ }
282
+
283
+ function invalid(code: string) {
284
+ return new Error(code);
285
+ }
@@ -10,8 +10,9 @@ import { createTestRunAuthorizationStore } from "./run-authorization.test-suppor
10
10
 
11
11
  export async function createRunFacadeFixture(
12
12
  permissionKeys: string[] = ["business.ai.execute", "business.records.read"],
13
+ options: { workflowOnly?: boolean } = {},
13
14
  ) {
14
- const module = runFacadeManifest();
15
+ const module = runFacadeManifest(options.workflowOnly);
15
16
  const prepared: unknown[] = [];
16
17
  const hosts = await initializeModuleHosts({
17
18
  modules: [module],
@@ -90,7 +91,7 @@ export function guardContext(permissionKeys: string[]) {
90
91
  );
91
92
  }
92
93
 
93
- export function runFacadeManifest(): ModuleManifest {
94
+ export function runFacadeManifest(workflowOnly = false): ModuleManifest {
94
95
  return {
95
96
  name: "business",
96
97
  version: "0.1.0",
@@ -144,13 +145,13 @@ export function runFacadeManifest(): ModuleManifest {
144
145
  },
145
146
  },
146
147
  ],
147
- aiOperations: [agentOperation()],
148
+ aiOperations: [agentOperation(workflowOnly)],
148
149
  providers: [],
149
150
  auditActions: ["business.ai.execute", "business.records.read"],
150
151
  };
151
152
  }
152
153
 
153
- function agentOperation() {
154
+ function agentOperation(workflowOnly: boolean) {
154
155
  return {
155
156
  key: "business.answer",
156
157
  kind: "agent" as const,
@@ -187,6 +188,18 @@ function agentOperation() {
187
188
  },
188
189
  ],
189
190
  },
191
+ ...(workflowOnly
192
+ ? {
193
+ clientExecution: {
194
+ mode: "governed-session" as const,
195
+ launchAuthority: "workflow-artifact" as const,
196
+ maxSteps: 4,
197
+ maxRequestBytes: 4096,
198
+ maxResponseBytes: 4096,
199
+ maxConcurrentRequests: 1,
200
+ },
201
+ }
202
+ : {}),
190
203
  toolNetworkPolicy: { mode: "forbidden" as const },
191
204
  limits: {
192
205
  maxDurationSeconds: 60,
@@ -96,6 +96,22 @@ describe("Guarded Agent Run facade", () => {
96
96
  expect(fixture.prepared).toEqual([]);
97
97
  });
98
98
 
99
+ test("workflow-only Client Operation 不允许普通 Agent Run 直接启动", async () => {
100
+ const fixture = await createRunFacadeFixture(undefined, {
101
+ workflowOnly: true,
102
+ });
103
+ await expect(
104
+ fixture.facade.start(
105
+ "business.answer",
106
+ { question: "绕过 Workflow" },
107
+ "request-workflow-bypass",
108
+ fixture.context,
109
+ ),
110
+ ).rejects.toMatchObject({ code: "OPERATION_NOT_FOUND", status: 404 });
111
+ expect(fixture.remote.started).toEqual([]);
112
+ expect(fixture.prepared).toEqual([]);
113
+ });
114
+
99
115
  test("恢复时权限已收回会返回不泄露 Run 存在性的 404", async () => {
100
116
  const fixture = await createRunFacadeFixture();
101
117
  await fixture.facade.start(
@@ -41,11 +41,7 @@ export class AgentRunSessionBindingAuthorizer {
41
41
  feature: this.#feature,
42
42
  });
43
43
  if (!access.decision.allowed) {
44
- throw new AgentRunFacadeError(
45
- "FORBIDDEN",
46
- "无权写入长期 Session",
47
- 403,
48
- );
44
+ throw new AgentRunFacadeError("FORBIDDEN", "无权写入长期 Session", 403);
49
45
  }
50
46
  }
51
47
  }
@@ -56,6 +52,16 @@ export async function authorizeAgentRunStart(input: {
56
52
  readonly context: RequestGuardContext;
57
53
  readonly sessionBinding?: AgentRunSessionBindingAuthorizer;
58
54
  }): Promise<void> {
55
+ if (
56
+ input.operation.definition.clientExecution?.launchAuthority ===
57
+ "workflow-artifact"
58
+ ) {
59
+ throw new AgentRunFacadeError(
60
+ "OPERATION_NOT_FOUND",
61
+ "Agent Operation 不存在",
62
+ 404,
63
+ );
64
+ }
59
65
  const decision = await evaluateAccessGuards(input.runtime, input.context, {
60
66
  permission: {
61
67
  permissionKey: input.operation.definition.permissionKey,
@@ -63,11 +69,7 @@ export async function authorizeAgentRunStart(input: {
63
69
  feature: input.operation.feature,
64
70
  });
65
71
  if (!decision.decision.allowed) {
66
- throw new AgentRunFacadeError(
67
- "FORBIDDEN",
68
- "无权执行 Agent Operation",
69
- 403,
70
- );
72
+ throw new AgentRunFacadeError("FORBIDDEN", "无权执行 Agent Operation", 403);
71
73
  }
72
74
  await input.sessionBinding?.requireManage(input.context);
73
75
  }
@@ -1,7 +1,10 @@
1
1
  import type { PlatformConfig } from "@southwind-ai/config";
2
2
  import type { DurableHandlerRegistry } from "@southwind-ai/jobs";
3
3
  import type { ModuleManifest } from "@southwind-ai/modules";
4
- import type { ModuleClientAgentHostAuthAdapter } from "@southwind-ai/server-sdk";
4
+ import type {
5
+ ModuleClientAgentHostAuthAdapter,
6
+ ModuleClientAgentWorkflowArtifactProjectBinding,
7
+ } from "@southwind-ai/server-sdk";
5
8
  import type { NodePgDatabase } from "drizzle-orm/node-postgres";
6
9
  import type { InitializedModuleHosts } from "../module-host.js";
7
10
  import type { createServerRuntime } from "../runtime.js";
@@ -36,6 +39,7 @@ export interface AgentPlatformRuntimeInput {
36
39
  backgroundAi: DeferredAgentBackgroundOperationPortFactory;
37
40
  screenshotPageRegistry?: ScreenshotPageRegistryV2;
38
41
  clientAgentHostAuth?: ModuleClientAgentHostAuthAdapter;
42
+ clientAgentWorkflowArtifactBindings?: readonly ModuleClientAgentWorkflowArtifactProjectBinding[];
39
43
  authoritativeSessionSources?: readonly ModuleAuthoritativeSourceAdapterRegistration[];
40
44
  sessionMemoryObjects?: readonly ModuleMemoryObjectAdapterRegistration[];
41
45
  agentWorkflowWorkspacePolicies?: readonly AgentWorkflowWorkspacePolicyDefinition[];
@@ -43,6 +43,8 @@ import { createModuleAiPorts } from "./module-ai-port-factory.js";
43
43
  import { createManifestAuthorizedRouteApp } from "../module-host/static-route-authorization.js";
44
44
  import { registerUnavailableAiRoutes } from "./unavailable-routes.js";
45
45
  import { createClientAgentSessionRuntime } from "./client-session/bootstrap.js";
46
+ import { attachClientAgentWorkflowArtifactRuntime } from "./client-workflow-artifact/runtime-composition.js";
47
+ import { bindClientAgentHostRevocations } from "./client-session/host-revocation-composition.js";
46
48
  import { registerAgentSessions } from "./agent-session-bootstrap.js";
47
49
  import { createAiRuntimeReadinessSources } from "./runtime-readiness-sources.js";
48
50
  import { registerAiRuntimeReporting } from "./runtime-reporting-bootstrap.js";
@@ -51,10 +53,12 @@ import { createScreenshotIntakeRuntimeV2 } from "./screenshot-intake-bootstrap.j
51
53
  import { requiredEnvironment } from "./required-environment.js";
52
54
  import { composeAgentLongSessionRuntime } from "./session-runtime-composition.js";
53
55
  import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
54
- import { createModuleAgentSessionMemoryPort } from "../module-host/agent-session-memory-port.js";
55
- import { createModuleAuthoritativeSourceInvalidationPort } from "../module-host/agent-source-invalidation-port.js";
56
56
  import { composeAgentToolRuntime } from "./runtime-tool-composition.js";
57
57
  import { registerProductionAgentWorkflowRuntime } from "./workflow-runtime-bootstrap.js";
58
+ import {
59
+ createSessionMemoryPortFactory,
60
+ createSourceInvalidationPortFactory,
61
+ } from "./runtime-public-ports.js";
58
62
  export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
59
63
  const resolveAgentActor = async (actorId: string) =>
60
64
  resolveCurrentSystemActor(input.systemRepositories, actorId);
@@ -75,6 +79,7 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
75
79
  aiOperations: undefined,
76
80
  aiReadiness: undefined,
77
81
  clientAgentSessions: undefined,
82
+ agentWorkflowProjects: undefined,
78
83
  sessionMemory: undefined,
79
84
  sourceInvalidation: undefined,
80
85
  invalidateAuthorization: undefined,
@@ -113,6 +118,20 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
113
118
  resolveActor: resolveAgentActor,
114
119
  });
115
120
  const clientAgentSessions = clientAgentSessionRuntime?.service;
121
+ const clientWorkflowArtifacts = attachClientAgentWorkflowArtifactRuntime({
122
+ platform: input,
123
+ aiRouteApp,
124
+ hostApp: clientAgentSessionRuntime?.hostApp,
125
+ routeAuthorizer,
126
+ sessions: clientAgentSessionRuntime?.service,
127
+ sessionRepository: clientAgentSessionRuntime?.repository,
128
+ attemptAuthority: clientAgentSessionRuntime?.attemptAuthority,
129
+ });
130
+ const unbindClientAgentHostRevocations = bindClientAgentHostRevocations({
131
+ adapter: input.clientAgentHostAuth,
132
+ revokeSessionHost: clientAgentSessionRuntime.revokeHost,
133
+ revokeWorkflowHost: clientWorkflowArtifacts.revokeHost,
134
+ });
116
135
  const screenshotIntake = createScreenshotIntakeRuntimeV2({
117
136
  modules: input.modules,
118
137
  runtime: input.runtime,
@@ -146,7 +165,10 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
146
165
  remote,
147
166
  authorizations: runAuthorizations,
148
167
  resolveActor: resolveAgentActor,
149
- authoritativeSources: input.authoritativeSessionSources,
168
+ authoritativeSources: [
169
+ ...clientWorkflowArtifacts.authoritativeSources,
170
+ ...(input.authoritativeSessionSources ?? []),
171
+ ],
150
172
  memoryObjects: input.sessionMemoryObjects,
151
173
  });
152
174
  const documentRemote = process.env.DOCUMENT_SERVER_URL?.trim()
@@ -260,6 +282,9 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
260
282
  resolveActor: resolveAgentActor,
261
283
  },
262
284
  );
285
+ if (agentWorkflowRuntime?.artifacts) {
286
+ clientWorkflowArtifacts.bindTaskArtifacts?.(agentWorkflowRuntime.artifacts);
287
+ }
263
288
  registerScreenshotIntakeRoutes(aiRouteApp, screenshotIntake);
264
289
  registerAgentRunRoutes(
265
290
  aiRouteApp,
@@ -309,39 +334,14 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
309
334
  aiOperations: moduleAi.operations,
310
335
  aiReadiness: moduleAi.readiness,
311
336
  clientAgentSessions,
312
- sessionMemory: longSessions
313
- ? (
314
- moduleName: string,
315
- context: Parameters<typeof createModuleAgentSessionMemoryPort>[1],
316
- signal: AbortSignal,
317
- ) =>
318
- createModuleAgentSessionMemoryPort(
319
- moduleName,
320
- context,
321
- signal,
322
- longSessions.memoryProjector,
323
- )
324
- : undefined,
325
- sourceInvalidation: longSessions
326
- ? (
327
- moduleName: string,
328
- context: Parameters<
329
- typeof createModuleAuthoritativeSourceInvalidationPort
330
- >[1],
331
- signal: AbortSignal,
332
- ) =>
333
- createModuleAuthoritativeSourceInvalidationPort(
334
- moduleName,
335
- context,
336
- signal,
337
- longSessions.authoritativeSources,
338
- longSessions.invalidationOutbox,
339
- )
340
- : undefined,
337
+ agentWorkflowProjects: agentWorkflowRuntime?.projectCommands,
338
+ sessionMemory: createSessionMemoryPortFactory(longSessions),
339
+ sourceInvalidation: createSourceInvalidationPortFactory(longSessions),
341
340
  invalidateAuthorization: longSessions?.invalidateAuthorization,
342
341
  disposeAgentRuntime() {
343
342
  screenshotIntake?.dispose();
344
- clientAgentSessionRuntime.dispose?.();
343
+ unbindClientAgentHostRevocations?.();
344
+ clientWorkflowArtifacts.dispose?.();
345
345
  longSessions?.dispose();
346
346
  },
347
347
  };
@@ -0,0 +1,40 @@
1
+ import { createModuleAgentSessionMemoryPort } from "../module-host/agent-session-memory-port.js";
2
+ import { createModuleAuthoritativeSourceInvalidationPort } from "../module-host/agent-source-invalidation-port.js";
3
+ import type { composeAgentLongSessionRuntime } from "./session-runtime-composition.js";
4
+
5
+ type LongSessionRuntime = ReturnType<typeof composeAgentLongSessionRuntime>;
6
+
7
+ export function createSessionMemoryPortFactory(runtime: LongSessionRuntime) {
8
+ if (!runtime) return undefined;
9
+ return (
10
+ moduleName: string,
11
+ context: Parameters<typeof createModuleAgentSessionMemoryPort>[1],
12
+ signal: AbortSignal,
13
+ ) =>
14
+ createModuleAgentSessionMemoryPort(
15
+ moduleName,
16
+ context,
17
+ signal,
18
+ runtime.memoryProjector,
19
+ );
20
+ }
21
+
22
+ export function createSourceInvalidationPortFactory(
23
+ runtime: LongSessionRuntime,
24
+ ) {
25
+ if (!runtime) return undefined;
26
+ return (
27
+ moduleName: string,
28
+ context: Parameters<
29
+ typeof createModuleAuthoritativeSourceInvalidationPort
30
+ >[1],
31
+ signal: AbortSignal,
32
+ ) =>
33
+ createModuleAuthoritativeSourceInvalidationPort(
34
+ moduleName,
35
+ context,
36
+ signal,
37
+ runtime.authoritativeSources,
38
+ runtime.invalidationOutbox,
39
+ );
40
+ }
@@ -57,6 +57,8 @@ export interface SourceAuthorizationCoordinate {
57
57
  readonly actorId: string;
58
58
  readonly scopeDigest: string;
59
59
  readonly generation: number;
60
+ /** Optional server-derived scope; authorities that need containment must require it. */
61
+ readonly scopeSnapshot?: import("@southwind-ai/shared").AuthorizationScopeSnapshot;
60
62
  }
61
63
 
62
64
  type SourceInvalidationEnvelopeBase = {
@@ -102,26 +104,26 @@ export type SessionDerivedInvalidationEnvelopeV1 =
102
104
 
103
105
  export type SourceInvalidationEnvelopeV1 =
104
106
  | (SourceInvalidationEnvelopeBase & {
105
- readonly causedBy: SourceInvalidationCausation;
106
- readonly transition: {
107
- readonly reason: "authorization-revoked";
108
- readonly targetState: "revoked";
109
- };
110
- readonly scope: {
111
- /** Sinks converge Session-derived projections for this Actor; source matching alone is insufficient. */
112
- readonly kind: "authorization";
113
- readonly authorization: SourceAuthorizationCoordinate;
114
- };
115
- })
107
+ readonly causedBy: SourceInvalidationCausation;
108
+ readonly transition: {
109
+ readonly reason: "authorization-revoked";
110
+ readonly targetState: "revoked";
111
+ };
112
+ readonly scope: {
113
+ /** Sinks converge Session-derived projections for this Actor; source matching alone is insufficient. */
114
+ readonly kind: "authorization";
115
+ readonly authorization: SourceAuthorizationCoordinate;
116
+ };
117
+ })
116
118
  | (SourceInvalidationEnvelopeBase & {
117
- readonly causedBy: SourceInvalidationCausation;
118
- readonly transition: Exclude<
119
- SourceInvalidationTransition,
120
- | { readonly reason: "authorization-revoked" }
121
- | { readonly reason: "source-deleted" }
122
- >;
123
- readonly scope: { readonly kind: "resource" };
124
- })
119
+ readonly causedBy: SourceInvalidationCausation;
120
+ readonly transition: Exclude<
121
+ SourceInvalidationTransition,
122
+ | { readonly reason: "authorization-revoked" }
123
+ | { readonly reason: "source-deleted" }
124
+ >;
125
+ readonly scope: { readonly kind: "resource" };
126
+ })
125
127
  | AuthoritativeSourceDeletionEnvelopeV1
126
128
  | SessionDerivedInvalidationEnvelopeV1;
127
129
 
@@ -133,10 +135,7 @@ export interface SourceInvalidationDelivery {
133
135
  readonly leaseToken: string;
134
136
  }
135
137
 
136
- export type SourceInvalidationAppendResult =
137
- | "appended"
138
- | "duplicate"
139
- | "stale";
138
+ export type SourceInvalidationAppendResult = "appended" | "duplicate" | "stale";
140
139
 
141
140
  /**
142
141
  * 由持久化 Adapter 将 append 与触发它的 Session/领域事实写入同一事务。
@@ -171,7 +170,8 @@ export type SourceInvalidationProjectionResult =
171
170
  | "stale";
172
171
 
173
172
  export interface SourceInvalidationProjectionPort<
174
- TTarget extends SourceInvalidationProjectionTarget = SourceInvalidationProjectionTarget,
173
+ TTarget extends SourceInvalidationProjectionTarget =
174
+ SourceInvalidationProjectionTarget,
175
175
  > {
176
176
  readonly target: TTarget;
177
177
  invalidate(input: {
@@ -181,8 +181,7 @@ export interface SourceInvalidationProjectionPort<
181
181
  }
182
182
 
183
183
  export type SourceInvalidationProjectionPorts = {
184
- readonly [TTarget in SourceInvalidationProjectionTarget]:
185
- SourceInvalidationProjectionPort<TTarget>;
184
+ readonly [TTarget in SourceInvalidationProjectionTarget]: SourceInvalidationProjectionPort<TTarget>;
186
185
  };
187
186
 
188
187
  export interface AuthoritativeSourceAccessQuery {