create-windy 0.8.2 → 0.8.3
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 +18 -1
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
- package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
- package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
- package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
- package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
- package/template/apps/server/src/agent/operation-catalog.ts +135 -0
- package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
- package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
- package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
- package/template/apps/server/src/agent/run-route-response.ts +35 -0
- package/template/apps/server/src/agent/run-routes.ts +46 -43
- package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
- package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
- package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
- package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
- package/template/apps/server/src/agent/session-projection-view.ts +19 -16
- package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
- package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
- package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
- package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
- package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
- package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
- package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
- package/template/apps/server/src/agent/tool-catalog.ts +44 -0
- package/template/apps/server/src/agent/tool-registry.ts +45 -14
- package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
- package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
- package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
- package/template/apps/server/src/agent/workbench-facade.ts +339 -0
- package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
- package/template/apps/server/src/agent/workbench-routes.ts +97 -0
- package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
- package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
- package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
- package/template/apps/server/src/agent/workbench-sse.ts +50 -0
- package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
- package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
- package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
- package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
- package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
- package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
- package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
- package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
- package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
- package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
- package/template/apps/web/src/router/index.ts +4 -0
- package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
- package/template/apps/web/src/services/agent-admin-api.ts +155 -0
- package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
- package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
- package/template/docs/architecture/agent-harness.md +4 -0
- package/template/packages/modules/src/ai-operation-composition.ts +9 -0
- package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
- package/template/packages/modules/src/manifest.ts +2 -0
- package/template/packages/modules/src/system-admin-routes.ts +16 -0
- package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
- package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
- package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
- package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
- package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
- package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
- package/template/packages/modules/src/system-audit-actions.ts +25 -0
- package/template/packages/modules/src/system-features.ts +19 -0
- package/template/packages/modules/src/system-module-catalog.ts +14 -12
- package/template/packages/modules/src/system-module-ownership.ts +12 -0
- package/template/packages/modules/src/system-modules.ts +2 -0
- package/template/packages/modules/src/system-permissions.ts +5 -0
- package/template/packages/server-sdk/index.ts +2 -0
- package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
- package/template/packages/shared/index.ts +2 -0
- package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
- package/template/packages/shared/src/agent-workbench.ts +100 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
facadeError,
|
|
3
|
+
errorResponse,
|
|
4
|
+
required,
|
|
5
|
+
isRecord,
|
|
6
|
+
} from "./run-route-response.js";
|
|
1
7
|
import { timingSafeEqual } from "node:crypto";
|
|
2
8
|
import type { RequestGuardContext } from "../guards.js";
|
|
3
9
|
import type { RouteApp } from "../system/routes.js";
|
|
@@ -7,6 +13,8 @@ import type { AgentRuntimePort } from "./run-contracts.js";
|
|
|
7
13
|
import type { AgentRunSessionLinkPort } from "./session-run-link.js";
|
|
8
14
|
import type { SessionRunResultMemoryPort } from "./session-result-memory.js";
|
|
9
15
|
import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
|
|
16
|
+
import { projectPublicRunEvents } from "./workbench-sse.js";
|
|
17
|
+
import { assertBrowserVisibleOperation } from "./run-browser-visibility.js";
|
|
10
18
|
import {
|
|
11
19
|
AgentToolHostError,
|
|
12
20
|
attachExecutionGrant,
|
|
@@ -46,6 +54,7 @@ export function registerAgentRunRoutes(
|
|
|
46
54
|
const context = raw as RouteContext;
|
|
47
55
|
try {
|
|
48
56
|
const request = parseStartRequest(context.body);
|
|
57
|
+
assertBrowserVisibleOperation(facade, request.operationKey);
|
|
49
58
|
const idempotencyKey = context.request.headers.get("idempotency-key");
|
|
50
59
|
if (!idempotencyKey?.trim()) {
|
|
51
60
|
return errorResponse("INVALID_INPUT", "幂等键不能为空", 400);
|
|
@@ -70,12 +79,15 @@ export function registerAgentRunRoutes(
|
|
|
70
79
|
try {
|
|
71
80
|
const runId = required(context.params.runId);
|
|
72
81
|
const operationKey = parseOperationKey(context.query.operationKey);
|
|
82
|
+
assertBrowserVisibleOperation(facade, operationKey);
|
|
73
83
|
await facade.authorizeRun(operationKey, runId, context.guardContext);
|
|
74
84
|
const cursor = parseCursor(
|
|
75
85
|
context.query.after,
|
|
76
86
|
context.request.headers.get("last-event-id"),
|
|
77
87
|
);
|
|
78
|
-
return
|
|
88
|
+
return projectPublicRunEvents(
|
|
89
|
+
await remote.events(runId, cursor, context.request.signal),
|
|
90
|
+
);
|
|
79
91
|
} catch (error) {
|
|
80
92
|
return facadeError(error);
|
|
81
93
|
}
|
|
@@ -86,6 +98,7 @@ export function registerAgentRunRoutes(
|
|
|
86
98
|
try {
|
|
87
99
|
const runId = required(context.params.runId);
|
|
88
100
|
const operationKey = parseOperationKey(context.query.operationKey);
|
|
101
|
+
assertBrowserVisibleOperation(facade, operationKey);
|
|
89
102
|
const sessionId = context.query.sessionId
|
|
90
103
|
? parseSessionId(context.query.sessionId)
|
|
91
104
|
: undefined;
|
|
@@ -105,8 +118,14 @@ export function registerAgentRunRoutes(
|
|
|
105
118
|
status.status === "cancelled"
|
|
106
119
|
) {
|
|
107
120
|
if (status.status === "completed") {
|
|
108
|
-
await projectResult(
|
|
109
|
-
|
|
121
|
+
await projectResult(
|
|
122
|
+
resultMemory,
|
|
123
|
+
boundSessionId,
|
|
124
|
+
runId,
|
|
125
|
+
status.lastSequence,
|
|
126
|
+
response,
|
|
127
|
+
context,
|
|
128
|
+
);
|
|
110
129
|
}
|
|
111
130
|
await completeSessionRun(
|
|
112
131
|
sessionLinks,
|
|
@@ -128,6 +147,7 @@ export function registerAgentRunRoutes(
|
|
|
128
147
|
try {
|
|
129
148
|
const runId = required(context.params.runId);
|
|
130
149
|
const operationKey = parseOperationKey(context.query.operationKey);
|
|
150
|
+
assertBrowserVisibleOperation(facade, operationKey);
|
|
131
151
|
const sessionId = context.query.sessionId
|
|
132
152
|
? parseSessionId(context.query.sessionId)
|
|
133
153
|
: undefined;
|
|
@@ -141,7 +161,11 @@ export function registerAgentRunRoutes(
|
|
|
141
161
|
isRecord(context.body) && typeof context.body.reason === "string"
|
|
142
162
|
? context.body.reason.slice(0, 200)
|
|
143
163
|
: undefined;
|
|
144
|
-
const response = await remote.cancel(
|
|
164
|
+
const response = await remote.cancel(
|
|
165
|
+
runId,
|
|
166
|
+
reason,
|
|
167
|
+
context.request.signal,
|
|
168
|
+
);
|
|
145
169
|
if (authorized.sessionId && response.ok) {
|
|
146
170
|
const status = await remote.status(runId, context.request.signal);
|
|
147
171
|
if (!["completed", "failed", "cancelled"].includes(status.status)) {
|
|
@@ -149,8 +173,14 @@ export function registerAgentRunRoutes(
|
|
|
149
173
|
}
|
|
150
174
|
if (status.status === "completed") {
|
|
151
175
|
const result = await remote.result(runId, context.request.signal);
|
|
152
|
-
await projectResult(
|
|
153
|
-
|
|
176
|
+
await projectResult(
|
|
177
|
+
resultMemory,
|
|
178
|
+
authorized.sessionId,
|
|
179
|
+
runId,
|
|
180
|
+
status.lastSequence,
|
|
181
|
+
result,
|
|
182
|
+
context,
|
|
183
|
+
);
|
|
154
184
|
}
|
|
155
185
|
await completeSessionRun(
|
|
156
186
|
sessionLinks,
|
|
@@ -171,6 +201,7 @@ export function registerAgentRunRoutes(
|
|
|
171
201
|
try {
|
|
172
202
|
const runId = required(context.params.runId);
|
|
173
203
|
const request = parseAttachRequest(context.body);
|
|
204
|
+
assertBrowserVisibleOperation(facade, request.operationKey);
|
|
174
205
|
const authorized = await facade.authorizeRunMutation(
|
|
175
206
|
request.operationKey,
|
|
176
207
|
runId,
|
|
@@ -188,8 +219,14 @@ export function registerAgentRunRoutes(
|
|
|
188
219
|
if (authorized.sessionId && attached.terminalEvent) {
|
|
189
220
|
if (attached.status === "completed") {
|
|
190
221
|
const response = await remote.result(runId, context.request.signal);
|
|
191
|
-
await projectResult(
|
|
192
|
-
|
|
222
|
+
await projectResult(
|
|
223
|
+
resultMemory,
|
|
224
|
+
authorized.sessionId,
|
|
225
|
+
runId,
|
|
226
|
+
attached.terminalEvent.sequence,
|
|
227
|
+
response,
|
|
228
|
+
context,
|
|
229
|
+
);
|
|
193
230
|
}
|
|
194
231
|
await completeSessionRun(
|
|
195
232
|
sessionLinks,
|
|
@@ -267,7 +304,7 @@ async function projectResult(
|
|
|
267
304
|
sessionId,
|
|
268
305
|
runId,
|
|
269
306
|
terminalSequence,
|
|
270
|
-
result: await response.clone().json() as ModuleAiJsonValue,
|
|
307
|
+
result: (await response.clone().json()) as ModuleAiJsonValue,
|
|
271
308
|
context: context.guardContext,
|
|
272
309
|
signal: context.request.signal,
|
|
273
310
|
});
|
|
@@ -302,37 +339,3 @@ export function validInternalToken(
|
|
|
302
339
|
const right = Buffer.from(expected);
|
|
303
340
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
304
341
|
}
|
|
305
|
-
|
|
306
|
-
function facadeError(error: unknown): Response {
|
|
307
|
-
if (error instanceof AgentRunFacadeError) {
|
|
308
|
-
return errorResponse(error.code, error.message, error.status);
|
|
309
|
-
}
|
|
310
|
-
return errorResponse(
|
|
311
|
-
"RUNTIME_UNAVAILABLE",
|
|
312
|
-
"Agent Runtime 暂不可用",
|
|
313
|
-
503,
|
|
314
|
-
true,
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function errorResponse(
|
|
319
|
-
code: string,
|
|
320
|
-
message: string,
|
|
321
|
-
status: number,
|
|
322
|
-
retryable = false,
|
|
323
|
-
): Response {
|
|
324
|
-
return Response.json(
|
|
325
|
-
{ code, message, requestId: crypto.randomUUID(), retryable },
|
|
326
|
-
{ status },
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function required(value: string | undefined): string {
|
|
331
|
-
if (!value)
|
|
332
|
-
throw new AgentRunFacadeError("INVALID_INPUT", "Run ID 无效", 400);
|
|
333
|
-
return value;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
337
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
338
|
-
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { resolveCurrentSystemActor } from "../system/actor.js";
|
|
2
|
+
import type { AgentAdminReadinessRef } from "./admin-catalog-bootstrap.js";
|
|
3
|
+
import { registerAgentAdminCatalogRuntime } from "./admin-catalog-bootstrap.js";
|
|
2
4
|
import { AiArtifactCodec } from "../ai-artifact/codec.js";
|
|
3
5
|
import { registerAiArtifactRoutes } from "../ai-artifact/routes.js";
|
|
4
6
|
import { AiArtifactRuntime } from "../ai-artifact/runtime.js";
|
|
@@ -41,7 +43,7 @@ import { InMemoryAgentRunBindingRepository } from "./run-binding-repository.js";
|
|
|
41
43
|
import { AgentBackgroundOperationExecutor } from "./background-operation.js";
|
|
42
44
|
import { createModuleAiPorts } from "./module-ai-port-factory.js";
|
|
43
45
|
import { createManifestAuthorizedRouteApp } from "../module-host/static-route-authorization.js";
|
|
44
|
-
import {
|
|
46
|
+
import { disableAgentRuntime } from "./runtime-disabled-composition.js";
|
|
45
47
|
import { createClientAgentSessionRuntime } from "./client-session/bootstrap.js";
|
|
46
48
|
import { attachClientAgentWorkflowArtifactRuntime } from "./client-workflow-artifact/runtime-composition.js";
|
|
47
49
|
import { bindClientAgentHostRevocations } from "./client-session/host-revocation-composition.js";
|
|
@@ -52,6 +54,7 @@ import { registerScreenshotIntakeRoutes } from "./screenshot-intake-routes.js";
|
|
|
52
54
|
import { createScreenshotIntakeRuntimeV2 } from "./screenshot-intake-bootstrap.js";
|
|
53
55
|
import { requiredEnvironment } from "./required-environment.js";
|
|
54
56
|
import { composeAgentLongSessionRuntime } from "./session-runtime-composition.js";
|
|
57
|
+
import { registerAgentWorkbenchRuntime } from "./workbench-bootstrap.js";
|
|
55
58
|
import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
|
|
56
59
|
import { composeAgentToolRuntime } from "./runtime-tool-composition.js";
|
|
57
60
|
import { registerProductionAgentWorkflowRuntime } from "./workflow-runtime-bootstrap.js";
|
|
@@ -62,33 +65,23 @@ import {
|
|
|
62
65
|
export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
|
|
63
66
|
const resolveAgentActor = async (actorId: string) =>
|
|
64
67
|
resolveCurrentSystemActor(input.systemRepositories, actorId);
|
|
65
|
-
const
|
|
66
|
-
|
|
68
|
+
const tools = composeAgentToolRuntime(input);
|
|
69
|
+
const { features, toolHandlers, toolRegistry, routeAuthorizer } = tools;
|
|
70
|
+
const catalogReadiness: AgentAdminReadinessRef = {};
|
|
71
|
+
registerAgentAdminCatalogRuntime(input, tools, catalogReadiness);
|
|
67
72
|
const aiRouteApp = createManifestAuthorizedRouteApp(
|
|
68
73
|
input.app,
|
|
69
74
|
routeAuthorizer,
|
|
70
75
|
);
|
|
71
76
|
if (!input.platformConfig.enableAI) {
|
|
72
|
-
|
|
73
|
-
registerUnavailableAiRoutes(
|
|
74
|
-
createManifestAuthorizedRouteApp(input.app, routeAuthorizer),
|
|
75
|
-
);
|
|
76
|
-
return {
|
|
77
|
-
toolHandlers,
|
|
78
|
-
aiArtifacts: undefined,
|
|
79
|
-
aiOperations: undefined,
|
|
80
|
-
aiReadiness: undefined,
|
|
81
|
-
clientAgentSessions: undefined,
|
|
82
|
-
agentWorkflowProjects: undefined,
|
|
83
|
-
sessionMemory: undefined,
|
|
84
|
-
sourceInvalidation: undefined,
|
|
85
|
-
invalidateAuthorization: undefined,
|
|
86
|
-
disposeAgentRuntime: undefined,
|
|
87
|
-
};
|
|
77
|
+
return disableAgentRuntime(input, aiRouteApp, tools);
|
|
88
78
|
}
|
|
89
79
|
const sessionSecret = requiredEnvironment("AI_ARTIFACT_ENCRYPTION_SECRET");
|
|
90
80
|
const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
|
|
91
|
-
registerAgentSessions({
|
|
81
|
+
const businessSessions = registerAgentSessions({
|
|
82
|
+
...input,
|
|
83
|
+
secret: internalToken,
|
|
84
|
+
});
|
|
92
85
|
const aiArtifacts = new AiArtifactRuntime(
|
|
93
86
|
new AiArtifactCodec(sessionSecret),
|
|
94
87
|
input.hosts,
|
|
@@ -203,6 +196,7 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
|
|
|
203
196
|
trustStore: readinessTrustStore,
|
|
204
197
|
runtime: input.runtime,
|
|
205
198
|
});
|
|
199
|
+
catalogReadiness.current = aiReadiness;
|
|
206
200
|
const outputRuntime = createDocumentOutputRuntime({
|
|
207
201
|
modules: input.modules,
|
|
208
202
|
readiness: aiReadiness,
|
|
@@ -286,6 +280,13 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
|
|
|
286
280
|
if (agentWorkflowRuntime?.artifacts) {
|
|
287
281
|
clientWorkflowArtifacts.bindTaskArtifacts?.(agentWorkflowRuntime.artifacts);
|
|
288
282
|
}
|
|
283
|
+
registerAgentWorkbenchRuntime(input, aiRouteApp, {
|
|
284
|
+
agentFacade,
|
|
285
|
+
remote,
|
|
286
|
+
business: businessSessions,
|
|
287
|
+
sessions: longSessions?.facade,
|
|
288
|
+
workflows: agentWorkflowRuntime?.query,
|
|
289
|
+
});
|
|
289
290
|
registerScreenshotIntakeRoutes(aiRouteApp, screenshotIntake);
|
|
290
291
|
registerAgentRunRoutes(
|
|
291
292
|
aiRouteApp,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { RouteApp } from "../system/routes.js";
|
|
2
|
+
import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
|
|
3
|
+
import type { composeAgentToolRuntime } from "./runtime-tool-composition.js";
|
|
4
|
+
import { registerAgentWorkbenchRuntime } from "./workbench-bootstrap.js";
|
|
5
|
+
import { registerProductionAgentWorkflowRuntime } from "./workflow-runtime-bootstrap.js";
|
|
6
|
+
import { registerUnavailableAiRoutes } from "./unavailable-routes.js";
|
|
7
|
+
|
|
8
|
+
export function disableAgentRuntime(
|
|
9
|
+
input: AgentPlatformRuntimeInput,
|
|
10
|
+
app: RouteApp,
|
|
11
|
+
tools: ReturnType<typeof composeAgentToolRuntime>,
|
|
12
|
+
) {
|
|
13
|
+
registerAgentWorkbenchRuntime(input, app);
|
|
14
|
+
registerProductionAgentWorkflowRuntime(input, app, tools.toolRegistry);
|
|
15
|
+
registerUnavailableAiRoutes(app);
|
|
16
|
+
return {
|
|
17
|
+
toolHandlers: tools.toolHandlers,
|
|
18
|
+
aiArtifacts: undefined,
|
|
19
|
+
aiOperations: undefined,
|
|
20
|
+
aiReadiness: undefined,
|
|
21
|
+
clientAgentSessions: undefined,
|
|
22
|
+
agentWorkflowProjects: undefined,
|
|
23
|
+
sessionMemory: undefined,
|
|
24
|
+
sourceInvalidation: undefined,
|
|
25
|
+
invalidateAuthorization: undefined,
|
|
26
|
+
disposeAgentRuntime: undefined,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { projectSessionRecording } from "./session-recording-reader.js";
|
|
2
|
+
import { sessionProjectionGovernance as governance } from "./session-projection-governance.js";
|
|
2
3
|
import type { RequestGuardContext } from "../guards.js";
|
|
3
4
|
import type { MemoryObjectAuthorizationPort } from "./context-memory/contracts.js";
|
|
4
5
|
import {
|
|
@@ -89,6 +90,12 @@ export class AgentSessionProjectionFacade {
|
|
|
89
90
|
};
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
async recording(sessionId: string, context: RequestGuardContext) {
|
|
94
|
+
return projectSessionRecording(
|
|
95
|
+
await this.replay(sessionId, undefined, context),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
92
99
|
async contextVersions(sessionId: string, context: RequestGuardContext) {
|
|
93
100
|
const session = await this.requireSession(sessionId, context);
|
|
94
101
|
const rows = await this.contexts.listForSession(session.sessionId);
|
|
@@ -339,11 +346,3 @@ export class AgentSessionProjectionFacade {
|
|
|
339
346
|
return session;
|
|
340
347
|
}
|
|
341
348
|
}
|
|
342
|
-
|
|
343
|
-
function governance(context: RequestGuardContext) {
|
|
344
|
-
return {
|
|
345
|
-
actorId: context.actor.id,
|
|
346
|
-
scopeDigest:
|
|
347
|
-
`sha256:${captureAuthorizationScope(context.actor.scopeContext).hash}` as const,
|
|
348
|
-
};
|
|
349
|
-
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
|
|
2
|
+
import type { RequestGuardContext } from "../guards.js";
|
|
3
|
+
|
|
4
|
+
export function sessionProjectionGovernance(context: RequestGuardContext) {
|
|
5
|
+
return {
|
|
6
|
+
actorId: context.actor.id,
|
|
7
|
+
scopeDigest:
|
|
8
|
+
`sha256:${captureAuthorizationScope(context.actor.scopeContext).hash}` as const,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
@@ -27,9 +27,7 @@ export function projectAgentSession(session: SessionRecord) {
|
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function projectAgentSessionTimelineEvent(
|
|
31
|
-
event: SessionEventEnvelope,
|
|
32
|
-
) {
|
|
30
|
+
export function projectAgentSessionTimelineEvent(event: SessionEventEnvelope) {
|
|
33
31
|
const data = event.data;
|
|
34
32
|
return {
|
|
35
33
|
sequence: event.sequence,
|
|
@@ -39,6 +37,9 @@ export function projectAgentSessionTimelineEvent(
|
|
|
39
37
|
...("runId" in data && data.runId ? { runId: data.runId } : {}),
|
|
40
38
|
...("turnId" in data ? { turnId: data.turnId } : {}),
|
|
41
39
|
...("stepId" in data ? { stepId: data.stepId } : {}),
|
|
40
|
+
...("toolCallId" in data ? { toolCallId: data.toolCallId } : {}),
|
|
41
|
+
...("toolKey" in data ? { toolKey: data.toolKey } : {}),
|
|
42
|
+
...("outcome" in data ? { outcome: data.outcome } : {}),
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -121,17 +122,19 @@ export function projectableMemory(
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function timelineSummary(type: SessionEventEnvelope["data"]["type"]) {
|
|
124
|
-
return (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
125
|
+
return (
|
|
126
|
+
{
|
|
127
|
+
"session.created": "Session 已创建",
|
|
128
|
+
"session.archived": "Session 已归档",
|
|
129
|
+
"turn.started": "Turn 已开始",
|
|
130
|
+
"turn.completed": "Turn 已结束",
|
|
131
|
+
"run.linked": "Run 已关联",
|
|
132
|
+
"step.started": "Step 已开始",
|
|
133
|
+
"step.completed": "Step 已结束",
|
|
134
|
+
"tool.execution-recorded": "Tool 执行事实已记录",
|
|
135
|
+
"checkpoint.created": "Checkpoint 已创建",
|
|
136
|
+
"fork.created": "Fork 已创建",
|
|
137
|
+
"write-tool.reexecuted": "写 Tool 已经显式授权重新执行",
|
|
138
|
+
} satisfies Record<SessionEventEnvelope["data"]["type"], string>
|
|
139
|
+
)[type];
|
|
137
140
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { SessionEventLogService } from "./session-log/service.js";
|
|
3
|
+
import { InMemorySessionEventRepository } from "./session-log/in-memory-repository.js";
|
|
4
|
+
import { projectSessionRecording } from "./session-recording-reader.js";
|
|
5
|
+
|
|
6
|
+
test("fork replay reuses AH1 recorded-only segments and original parent/child sequences", async () => {
|
|
7
|
+
const repository = new InMemorySessionEventRepository();
|
|
8
|
+
const service = new SessionEventLogService(repository);
|
|
9
|
+
const governance = {
|
|
10
|
+
actorId: "actor-1",
|
|
11
|
+
scopeDigest: `sha256:${"a".repeat(64)}` as const,
|
|
12
|
+
};
|
|
13
|
+
await service.create({
|
|
14
|
+
sessionId: "session_parent",
|
|
15
|
+
createdEventId: "session_event_parent",
|
|
16
|
+
governance,
|
|
17
|
+
});
|
|
18
|
+
await service.append({
|
|
19
|
+
sessionId: "session_parent",
|
|
20
|
+
expectedSequence: 1,
|
|
21
|
+
events: [
|
|
22
|
+
service.newEvent({
|
|
23
|
+
type: "turn.started",
|
|
24
|
+
turnId: "session_turn_parent",
|
|
25
|
+
cause: "user-input",
|
|
26
|
+
input: {
|
|
27
|
+
sourceType: "agent-run-input",
|
|
28
|
+
sourceId: "binding_1",
|
|
29
|
+
sourceVersion: "business.answer",
|
|
30
|
+
contentDigest: `sha256:${"b".repeat(64)}`,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
service.newEvent({
|
|
34
|
+
type: "run.linked",
|
|
35
|
+
turnId: "session_turn_parent",
|
|
36
|
+
runId: "run_parent",
|
|
37
|
+
}),
|
|
38
|
+
service.newEvent({
|
|
39
|
+
type: "turn.completed",
|
|
40
|
+
turnId: "session_turn_parent",
|
|
41
|
+
outcome: "completed",
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
await service.checkpoint({
|
|
46
|
+
sessionId: "session_parent",
|
|
47
|
+
expectedSequence: 4,
|
|
48
|
+
checkpointId: "session_checkpoint_parent",
|
|
49
|
+
eventId: "session_event_checkpoint",
|
|
50
|
+
});
|
|
51
|
+
await service.fork({
|
|
52
|
+
parentSessionId: "session_parent",
|
|
53
|
+
expectedParentSequence: 5,
|
|
54
|
+
checkpointId: "session_checkpoint_parent",
|
|
55
|
+
childSessionId: "session_child",
|
|
56
|
+
parentEventId: "session_event_fork",
|
|
57
|
+
childCreatedEventId: "session_event_child",
|
|
58
|
+
governance,
|
|
59
|
+
});
|
|
60
|
+
await service.append({
|
|
61
|
+
sessionId: "session_child",
|
|
62
|
+
expectedSequence: 1,
|
|
63
|
+
events: [
|
|
64
|
+
service.newEvent({
|
|
65
|
+
type: "turn.started",
|
|
66
|
+
turnId: "session_turn_child",
|
|
67
|
+
cause: "user-input",
|
|
68
|
+
input: {
|
|
69
|
+
sourceType: "agent-run-input",
|
|
70
|
+
sourceId: "binding_2",
|
|
71
|
+
sourceVersion: "business.answer",
|
|
72
|
+
contentDigest: `sha256:${"c".repeat(64)}`,
|
|
73
|
+
},
|
|
74
|
+
}),
|
|
75
|
+
service.newEvent({
|
|
76
|
+
type: "run.linked",
|
|
77
|
+
turnId: "session_turn_child",
|
|
78
|
+
runId: "run_child",
|
|
79
|
+
}),
|
|
80
|
+
service.newEvent({
|
|
81
|
+
type: "turn.completed",
|
|
82
|
+
turnId: "session_turn_child",
|
|
83
|
+
outcome: "completed",
|
|
84
|
+
}),
|
|
85
|
+
],
|
|
86
|
+
});
|
|
87
|
+
const replay = await service.replay({ sessionId: "session_child" });
|
|
88
|
+
const projected = projectSessionRecording({
|
|
89
|
+
...replay,
|
|
90
|
+
segments: await Promise.all(
|
|
91
|
+
replay.segments.map(async (segment) => ({
|
|
92
|
+
...segment,
|
|
93
|
+
session: (await repository.get(segment.sessionId))!,
|
|
94
|
+
})),
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
expect(replay.sideEffectPolicy).toBe("recorded-only");
|
|
98
|
+
expect(projected.segments.map(({ sessionId }) => sessionId)).toEqual([
|
|
99
|
+
"session_parent",
|
|
100
|
+
"session_child",
|
|
101
|
+
]);
|
|
102
|
+
expect(
|
|
103
|
+
projected.segments.map(({ timeline }) =>
|
|
104
|
+
timeline.map(({ sequence }) => sequence),
|
|
105
|
+
),
|
|
106
|
+
).toEqual([
|
|
107
|
+
[1, 2, 3, 4],
|
|
108
|
+
[1, 2, 3, 4],
|
|
109
|
+
]);
|
|
110
|
+
expect(
|
|
111
|
+
projected.runRefs.map(({ runId, sessionId, operationKey }) => ({
|
|
112
|
+
runId,
|
|
113
|
+
sessionId,
|
|
114
|
+
operationKey,
|
|
115
|
+
})),
|
|
116
|
+
).toEqual([
|
|
117
|
+
{
|
|
118
|
+
runId: "run_parent",
|
|
119
|
+
sessionId: "session_parent",
|
|
120
|
+
operationKey: "business.answer",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
runId: "run_child",
|
|
124
|
+
sessionId: "session_child",
|
|
125
|
+
operationKey: "business.answer",
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
expect(projected.truncated).toBe(false);
|
|
129
|
+
expect((await repository.get("session_parent"))?.headSequence).toBe(6);
|
|
130
|
+
expect((await repository.get("session_child"))?.headSequence).toBe(4);
|
|
131
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { SessionRecord } from "./session-log/domain.js";
|
|
2
|
+
import type { ReadOnlySessionReplay } from "./session-log/replay-policy.js";
|
|
3
|
+
import {
|
|
4
|
+
projectAgentSession,
|
|
5
|
+
projectAgentSessionTimelineEvent,
|
|
6
|
+
} from "./session-projection-view.js";
|
|
7
|
+
|
|
8
|
+
/** Already-authorized replay segments preserve parent/child sequence domains. */
|
|
9
|
+
export function projectSessionRecording(
|
|
10
|
+
replay: Omit<ReadOnlySessionReplay, "segments"> & {
|
|
11
|
+
segments: readonly {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
throughSequence: number;
|
|
14
|
+
events: ReadOnlySessionReplay["segments"][number]["events"];
|
|
15
|
+
session: SessionRecord;
|
|
16
|
+
}[];
|
|
17
|
+
},
|
|
18
|
+
) {
|
|
19
|
+
const current = replay.segments.find(
|
|
20
|
+
({ sessionId }) => sessionId === replay.sessionId,
|
|
21
|
+
);
|
|
22
|
+
if (!current) throw new Error("Current replay segment missing");
|
|
23
|
+
const segments = replay.segments.map((segment) => ({
|
|
24
|
+
...segment,
|
|
25
|
+
events: segment.events.slice(0, 10_000),
|
|
26
|
+
}));
|
|
27
|
+
const runRefs = segments.flatMap(({ sessionId, events }) => {
|
|
28
|
+
const operations = new Map(
|
|
29
|
+
events.flatMap(({ data }) =>
|
|
30
|
+
data.type === "turn.started" &&
|
|
31
|
+
data.input.sourceType === "agent-run-input"
|
|
32
|
+
? [[data.turnId, data.input.sourceVersion] as const]
|
|
33
|
+
: [],
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
return events.flatMap(({ data }) =>
|
|
37
|
+
data.type === "run.linked"
|
|
38
|
+
? [
|
|
39
|
+
{
|
|
40
|
+
runId: data.runId,
|
|
41
|
+
sessionId,
|
|
42
|
+
turnId: data.turnId,
|
|
43
|
+
operationKey: operations.get(data.turnId) ?? "",
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [],
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
session: projectAgentSession(current.session),
|
|
51
|
+
truncated: replay.segments.some(({ events }) => events.length > 10_000),
|
|
52
|
+
segments: segments.map(({ sessionId, throughSequence, events }) => ({
|
|
53
|
+
sessionId,
|
|
54
|
+
throughSequence,
|
|
55
|
+
timeline: events.map(projectAgentSessionTimelineEvent),
|
|
56
|
+
})),
|
|
57
|
+
timeline: segments
|
|
58
|
+
.find(({ sessionId }) => sessionId === replay.sessionId)!
|
|
59
|
+
.events.map(projectAgentSessionTimelineEvent),
|
|
60
|
+
runRefs,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { randomUUIDv7 } from "bun";
|
|
2
|
+
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
3
|
+
import type { createServerRuntime } from "../runtime.js";
|
|
4
|
+
|
|
5
|
+
export function auditAgentAdminCatalogRead(
|
|
6
|
+
runtime: ReturnType<typeof createServerRuntime>,
|
|
7
|
+
context: RequestGuardContext,
|
|
8
|
+
kind: "tools" | "operations",
|
|
9
|
+
itemCount: number,
|
|
10
|
+
): void {
|
|
11
|
+
runtime.auditSink.recordEvent({
|
|
12
|
+
id: randomUUIDv7(),
|
|
13
|
+
action:
|
|
14
|
+
kind === "tools" ? "system.agent-tools.read" : "system.agent-debug.read",
|
|
15
|
+
actor: auditActor(context),
|
|
16
|
+
outcome: "success",
|
|
17
|
+
target: { type: "agent-admin-catalog", id: kind },
|
|
18
|
+
requestId: context.requestId,
|
|
19
|
+
metadata: { itemCount },
|
|
20
|
+
occurredAt: new Date().toISOString(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AgentAdminAvailabilityReason } from "@southwind-ai/shared";
|
|
2
|
+
import type { GuardDecisionReason } from "../guards.js";
|
|
3
|
+
|
|
4
|
+
export function catalogGuardReason(
|
|
5
|
+
reason: GuardDecisionReason,
|
|
6
|
+
): AgentAdminAvailabilityReason {
|
|
7
|
+
switch (reason) {
|
|
8
|
+
case "allowed":
|
|
9
|
+
case "anonymous":
|
|
10
|
+
return "allowed";
|
|
11
|
+
case "feature-disabled":
|
|
12
|
+
case "dependency-missing":
|
|
13
|
+
return "feature-unavailable";
|
|
14
|
+
case "license-restricted":
|
|
15
|
+
case "license-version-not-allowed":
|
|
16
|
+
case "entitlement-not-granted":
|
|
17
|
+
return "license-unavailable";
|
|
18
|
+
default:
|
|
19
|
+
return "permission-denied";
|
|
20
|
+
}
|
|
21
|
+
}
|