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
package/dist/cli.js
CHANGED
|
@@ -325,6 +325,15 @@ var agentOptionalModule = {
|
|
|
325
325
|
"apps/server/src/module-host/tool-handlers.test.ts",
|
|
326
326
|
"apps/server/src/module-host/module-secret-tool-boundary.test.ts",
|
|
327
327
|
"apps/web/src/pages/system/AiRuntimePage.vue",
|
|
328
|
+
"apps/web/src/pages/system/AgentToolsPage.vue",
|
|
329
|
+
"apps/web/src/pages/system/AgentDebugPage.vue",
|
|
330
|
+
"apps/web/src/pages/system/components/agent-workbench",
|
|
331
|
+
"apps/web/src/pages/system/composables/useAgentWorkbench.ts",
|
|
332
|
+
"apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts",
|
|
333
|
+
"apps/web/src/pages/system/composables/useSessionReplay.ts",
|
|
334
|
+
"apps/web/src/services/agent-admin-api.ts",
|
|
335
|
+
"apps/web/src/services/agent-admin-api.webtest.ts",
|
|
336
|
+
"apps/web/src/services/agent-workbench-event.ts",
|
|
328
337
|
"apps/web/src/pages/system/AgentSessionPage.vue",
|
|
329
338
|
"apps/web/src/pages/system/AgentSessionPage.webtest.ts",
|
|
330
339
|
"apps/web/src/pages/system/AgentWorkflowPage.vue",
|
|
@@ -344,6 +353,9 @@ var agentOptionalModule = {
|
|
|
344
353
|
"apps/web/src/services/ai-runtime-api.ts",
|
|
345
354
|
"apps/web/src/services/ai-runtime-api.webtest.ts",
|
|
346
355
|
"scripts/provision-speech-readiness.ts",
|
|
356
|
+
"packages/modules/src/system-agent-admin-registration.test.ts",
|
|
357
|
+
"packages/modules/src/system-agent-admin-menus.ts",
|
|
358
|
+
"packages/modules/src/system-agent-admin-bindings.ts",
|
|
347
359
|
"packages/modules/src/system-agent-audit-actions.ts",
|
|
348
360
|
"packages/modules/src/system-agent-tools.ts",
|
|
349
361
|
"packages/modules/src/system-agent-supervisor.ts",
|
|
@@ -359,6 +371,8 @@ var agentOptionalModule = {
|
|
|
359
371
|
"packages/server-sdk/src/workflow-input-materialization-handler.ts",
|
|
360
372
|
"packages/database/src/agent-runtime-schema.test.ts",
|
|
361
373
|
"packages/database/src/ai-output-artifacts.test.ts",
|
|
374
|
+
"packages/shared/src/agent-admin-catalog.ts",
|
|
375
|
+
"packages/shared/src/agent-workbench.ts",
|
|
362
376
|
"packages/shared/src/agent-workflow-definition.ts",
|
|
363
377
|
"packages/shared/src/agent-workflow-delegation.ts",
|
|
364
378
|
"packages/shared/src/agent-workflow-evaluation.ts",
|
|
@@ -371,6 +385,8 @@ var agentOptionalModule = {
|
|
|
371
385
|
],
|
|
372
386
|
documentation: [
|
|
373
387
|
"docs/adr/0003-agent-session-event-source.md",
|
|
388
|
+
"docs/architecture/agent-admin-workbench-plan.md",
|
|
389
|
+
"docs/architecture/agent-admin-workbench-implementation.md",
|
|
374
390
|
"docs/architecture/agent-harness.md",
|
|
375
391
|
"docs/architecture/agent-runtime-compatibility.md",
|
|
376
392
|
"docs/architecture/agent-workflow.md",
|
|
@@ -8530,7 +8546,8 @@ var recipes = [
|
|
|
8530
8546
|
to: "0.8.0"
|
|
8531
8547
|
},
|
|
8532
8548
|
{ id: "starter-0.8.0-to-0.8.1", from: "0.8.0", to: "0.8.1" },
|
|
8533
|
-
{ id: "starter-0.8.1-to-0.8.2", from: "0.8.1", to: "0.8.2" }
|
|
8549
|
+
{ id: "starter-0.8.1-to-0.8.2", from: "0.8.1", to: "0.8.2" },
|
|
8550
|
+
{ id: "starter-0.8.2-to-0.8.3", from: "0.8.2", to: "0.8.3" }
|
|
8534
8551
|
];
|
|
8535
8552
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
|
8536
8553
|
if (compareVersions2(sourceVersion, minimumSupportedUpdateVersion) < 0) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ModuleAiOperationReadinessPortV1 } from "@southwind-ai/server-sdk";
|
|
2
|
+
import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
|
|
3
|
+
import type { composeAgentToolRuntime } from "./runtime-tool-composition.js";
|
|
4
|
+
import { registerAgentAdminCatalogRoutes } from "./tool-catalog-routes.js";
|
|
5
|
+
|
|
6
|
+
export interface AgentAdminReadinessRef {
|
|
7
|
+
current?: ModuleAiOperationReadinessPortV1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function registerAgentAdminCatalogRuntime(
|
|
11
|
+
input: AgentPlatformRuntimeInput,
|
|
12
|
+
tools: ReturnType<typeof composeAgentToolRuntime>,
|
|
13
|
+
readiness: AgentAdminReadinessRef,
|
|
14
|
+
) {
|
|
15
|
+
registerAgentAdminCatalogRoutes(input.app, {
|
|
16
|
+
modules: input.modules,
|
|
17
|
+
hosts: input.hosts,
|
|
18
|
+
runtime: input.runtime,
|
|
19
|
+
registry: tools.toolRegistry,
|
|
20
|
+
authorizer: tools.routeAuthorizer,
|
|
21
|
+
runtimeAvailable: () => input.platformConfig.enableAI,
|
|
22
|
+
operationReadiness: async (operationKey) =>
|
|
23
|
+
(await readiness.current?.inspect({ operationKey }))?.status === "ready",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -11,13 +11,12 @@ export function registerAgentSessions(input: {
|
|
|
11
11
|
runtime: ReturnType<typeof createServerRuntime>;
|
|
12
12
|
secret: string;
|
|
13
13
|
}) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
input.
|
|
17
|
-
new
|
|
18
|
-
input.hosts,
|
|
19
|
-
input.runtime,
|
|
20
|
-
new AgentSessionIdCodecV1(input.secret),
|
|
21
|
-
),
|
|
14
|
+
const facade = new AgentSessionFacadeV1(
|
|
15
|
+
input.hosts,
|
|
16
|
+
input.runtime,
|
|
17
|
+
new AgentSessionIdCodecV1(input.secret),
|
|
22
18
|
);
|
|
19
|
+
if (!input.hosts.agentSessionProviders.length) return facade;
|
|
20
|
+
registerAgentSessionRoutesV1(input.app, facade);
|
|
21
|
+
return facade;
|
|
23
22
|
}
|
|
@@ -2,6 +2,8 @@ import { randomUUIDv7 } from "bun";
|
|
|
2
2
|
import {
|
|
3
3
|
MODULE_AGENT_SESSION_SCHEMA_VERSION,
|
|
4
4
|
type ModuleAgentSessionSummaryV1,
|
|
5
|
+
type ModuleAgentSessionRunReferenceV1,
|
|
6
|
+
type ModuleAgentSessionWorkflowReferenceV1,
|
|
5
7
|
} from "@southwind-ai/server-sdk";
|
|
6
8
|
import { evaluateAccessGuards } from "../access-guards.js";
|
|
7
9
|
import { auditActor, type RequestGuardContext } from "../guards.js";
|
|
@@ -33,12 +35,15 @@ export interface AgentSessionSummaryDtoV1 {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export interface AgentSessionDetailDtoV1 extends AgentSessionSummaryDtoV1 {
|
|
38
|
+
readonly truncated?: boolean;
|
|
36
39
|
readonly messages: readonly {
|
|
37
40
|
sequence: number;
|
|
38
41
|
role: "user" | "assistant";
|
|
39
42
|
content: string;
|
|
40
43
|
createdAt: string;
|
|
41
44
|
}[];
|
|
45
|
+
readonly runRefs?: readonly ModuleAgentSessionRunReferenceV1[];
|
|
46
|
+
readonly workflowRefs?: readonly ModuleAgentSessionWorkflowReferenceV1[];
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
export class AgentSessionFacadeError extends Error {
|
|
@@ -156,6 +161,13 @@ export class AgentSessionFacadeV1 {
|
|
|
156
161
|
session: {
|
|
157
162
|
...this.project(entry, actor.id, validated),
|
|
158
163
|
messages: validated.messages,
|
|
164
|
+
...(validated.truncated !== undefined
|
|
165
|
+
? { truncated: validated.truncated }
|
|
166
|
+
: {}),
|
|
167
|
+
...(validated.runRefs ? { runRefs: validated.runRefs } : {}),
|
|
168
|
+
...(validated.workflowRefs
|
|
169
|
+
? { workflowRefs: validated.workflowRefs }
|
|
170
|
+
: {}),
|
|
159
171
|
} satisfies AgentSessionDetailDtoV1,
|
|
160
172
|
};
|
|
161
173
|
} catch (error) {
|
|
@@ -66,6 +66,49 @@ export function validateAgentSessionDetail(
|
|
|
66
66
|
previousSequence = message.sequence;
|
|
67
67
|
}
|
|
68
68
|
if (totalChars > MAX_TOTAL_MESSAGE_CHARS) invalidProvider();
|
|
69
|
+
if (value.truncated !== undefined && typeof value.truncated !== "boolean")
|
|
70
|
+
invalidProvider();
|
|
71
|
+
const messageSequences = new Set(
|
|
72
|
+
value.messages.map(({ sequence }) => sequence),
|
|
73
|
+
);
|
|
74
|
+
if (value.runRefs !== undefined) {
|
|
75
|
+
if (!Array.isArray(value.runRefs) || value.runRefs.length > 200)
|
|
76
|
+
invalidProvider();
|
|
77
|
+
const ids = new Set<string>();
|
|
78
|
+
for (const ref of value.runRefs) {
|
|
79
|
+
if (
|
|
80
|
+
!ref ||
|
|
81
|
+
!validText(ref.runId, 200) ||
|
|
82
|
+
!validText(ref.operationKey, 200) ||
|
|
83
|
+
!/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/.test(ref.operationKey) ||
|
|
84
|
+
ids.has(ref.runId) ||
|
|
85
|
+
(ref.sessionId !== undefined && !validText(ref.sessionId, 200)) ||
|
|
86
|
+
(ref.turnId !== undefined && !validText(ref.turnId, 200)) ||
|
|
87
|
+
(ref.workflowRunId !== undefined &&
|
|
88
|
+
!validText(ref.workflowRunId, 200)) ||
|
|
89
|
+
(ref.messageSequence !== undefined &&
|
|
90
|
+
!messageSequences.has(ref.messageSequence))
|
|
91
|
+
)
|
|
92
|
+
invalidProvider();
|
|
93
|
+
ids.add(ref.runId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (value.workflowRefs !== undefined) {
|
|
97
|
+
if (!Array.isArray(value.workflowRefs) || value.workflowRefs.length > 100)
|
|
98
|
+
invalidProvider();
|
|
99
|
+
const ids = new Set<string>();
|
|
100
|
+
for (const ref of value.workflowRefs) {
|
|
101
|
+
if (
|
|
102
|
+
!ref ||
|
|
103
|
+
!validText(ref.workflowRunId, 200) ||
|
|
104
|
+
ids.has(ref.workflowRunId) ||
|
|
105
|
+
(ref.messageSequence !== undefined &&
|
|
106
|
+
!messageSequences.has(ref.messageSequence))
|
|
107
|
+
)
|
|
108
|
+
invalidProvider();
|
|
109
|
+
ids.add(ref.workflowRunId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
69
112
|
return value;
|
|
70
113
|
}
|
|
71
114
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createServerRuntime } from "../runtime.js";
|
|
3
|
+
import { AgentAdminOperationCatalog } from "./operation-catalog.js";
|
|
4
|
+
import {
|
|
5
|
+
createRunFacadeFixture,
|
|
6
|
+
runFacadeManifest,
|
|
7
|
+
} from "./run-facade.test-support.js";
|
|
8
|
+
|
|
9
|
+
async function fixture(
|
|
10
|
+
options: { workflowOnly?: boolean; runtimeAvailable?: boolean } = {},
|
|
11
|
+
) {
|
|
12
|
+
const run = await createRunFacadeFixture(undefined, {
|
|
13
|
+
workflowOnly: options.workflowOnly,
|
|
14
|
+
});
|
|
15
|
+
const module = runFacadeManifest(options.workflowOnly);
|
|
16
|
+
const catalog = new AgentAdminOperationCatalog({
|
|
17
|
+
modules: [module],
|
|
18
|
+
hosts: run.hosts,
|
|
19
|
+
runtime: run.runtime,
|
|
20
|
+
runtimeAvailable: () => options.runtimeAvailable ?? true,
|
|
21
|
+
});
|
|
22
|
+
return { ...run, catalog, module };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("Admin Operation catalogue uses production registration", () => {
|
|
26
|
+
test("Registered and governed agent exposes real Schema/limits/tools, without preparing or running", async () => {
|
|
27
|
+
const { catalog, context, prepared, module, remote } = await fixture();
|
|
28
|
+
const [operation] = await catalog.list(context);
|
|
29
|
+
expect(operation).toMatchObject({
|
|
30
|
+
key: "business.answer",
|
|
31
|
+
module: "business",
|
|
32
|
+
inputSchemaKey: "business.input.v1",
|
|
33
|
+
inputSchema: module.jsonSchemas!.find(
|
|
34
|
+
({ key }) => key === "business.input.v1",
|
|
35
|
+
)!.schema,
|
|
36
|
+
outputSchemaKey: "business.output.v1",
|
|
37
|
+
allowedToolKeys: ["business.records.list"],
|
|
38
|
+
limits: module.aiOperations![0]!.limits,
|
|
39
|
+
selectable: true,
|
|
40
|
+
availabilityReason: "allowed",
|
|
41
|
+
});
|
|
42
|
+
expect(prepared).toHaveLength(0);
|
|
43
|
+
expect(remote.started).toHaveLength(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("Catalog read capability cannot start an unauthorized operation", async () => {
|
|
47
|
+
const { catalog, context, facade } = await fixture();
|
|
48
|
+
context.actor.permissionKeys = ["system.agent-debug.read"];
|
|
49
|
+
const [operation] = await catalog.list(context);
|
|
50
|
+
expect(operation).toMatchObject({
|
|
51
|
+
selectable: false,
|
|
52
|
+
availabilityReason: "permission-denied",
|
|
53
|
+
});
|
|
54
|
+
await expect(
|
|
55
|
+
facade.start(
|
|
56
|
+
"business.answer",
|
|
57
|
+
{ question: "测试" },
|
|
58
|
+
"catalog-no-grant",
|
|
59
|
+
context,
|
|
60
|
+
),
|
|
61
|
+
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("Workflow-only operation and unavailable Runtime are unselectable", async () => {
|
|
65
|
+
const workflow = await fixture({ workflowOnly: true });
|
|
66
|
+
expect((await workflow.catalog.list(workflow.context))[0]).toMatchObject({
|
|
67
|
+
selectable: false,
|
|
68
|
+
availabilityReason: "workflow-only",
|
|
69
|
+
launchAuthority: "workflow-artifact",
|
|
70
|
+
});
|
|
71
|
+
const unavailable = await fixture({ runtimeAvailable: false });
|
|
72
|
+
expect(
|
|
73
|
+
(await unavailable.catalog.list(unavailable.context))[0],
|
|
74
|
+
).toMatchObject({
|
|
75
|
+
selectable: false,
|
|
76
|
+
availabilityReason: "runtime-unavailable",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("Declared readiness needs a current successful projection", async () => {
|
|
81
|
+
const { module, hosts, runtime, context } = await fixture();
|
|
82
|
+
module.aiOperations![0]!.readiness = {
|
|
83
|
+
contractVersion: 1,
|
|
84
|
+
variant: "test",
|
|
85
|
+
inputModalities: ["text"],
|
|
86
|
+
outputModalities: ["text"],
|
|
87
|
+
expectedDeployment: {
|
|
88
|
+
key: "test",
|
|
89
|
+
identity: "test",
|
|
90
|
+
releaseDigest: `sha256:${"a".repeat(64)}`,
|
|
91
|
+
offersDigest: `sha256:${"b".repeat(64)}`,
|
|
92
|
+
},
|
|
93
|
+
minConcurrentRuns: 1,
|
|
94
|
+
minQueueDepth: 1,
|
|
95
|
+
};
|
|
96
|
+
const input = {
|
|
97
|
+
modules: [module],
|
|
98
|
+
hosts,
|
|
99
|
+
runtime,
|
|
100
|
+
runtimeAvailable: () => true,
|
|
101
|
+
};
|
|
102
|
+
expect(
|
|
103
|
+
(await new AgentAdminOperationCatalog(input).list(context))[0],
|
|
104
|
+
).toMatchObject({
|
|
105
|
+
selectable: false,
|
|
106
|
+
availabilityReason: "runtime-unavailable",
|
|
107
|
+
});
|
|
108
|
+
expect(
|
|
109
|
+
(
|
|
110
|
+
await new AgentAdminOperationCatalog({
|
|
111
|
+
...input,
|
|
112
|
+
operationReadiness: async () => false,
|
|
113
|
+
}).list(context)
|
|
114
|
+
)[0],
|
|
115
|
+
).toMatchObject({
|
|
116
|
+
selectable: false,
|
|
117
|
+
availabilityReason: "runtime-unavailable",
|
|
118
|
+
});
|
|
119
|
+
expect(
|
|
120
|
+
(
|
|
121
|
+
await new AgentAdminOperationCatalog({
|
|
122
|
+
...input,
|
|
123
|
+
operationReadiness: async () => true,
|
|
124
|
+
}).list(context)
|
|
125
|
+
)[0],
|
|
126
|
+
).toMatchObject({ selectable: true });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("Unregistered handler, unsupported kind, invalid scope fail closed", async () => {
|
|
130
|
+
const { module, hosts, runtime, context } = await fixture();
|
|
131
|
+
const catalog = (current = module, currentHosts = hosts) =>
|
|
132
|
+
new AgentAdminOperationCatalog({
|
|
133
|
+
modules: [current],
|
|
134
|
+
hosts: currentHosts,
|
|
135
|
+
runtime,
|
|
136
|
+
runtimeAvailable: () => true,
|
|
137
|
+
});
|
|
138
|
+
expect(
|
|
139
|
+
(
|
|
140
|
+
await catalog(module, { ...hosts, aiOperationHandlers: [] }).list(
|
|
141
|
+
context,
|
|
142
|
+
)
|
|
143
|
+
)[0],
|
|
144
|
+
).toMatchObject({
|
|
145
|
+
selectable: false,
|
|
146
|
+
availabilityReason: "handler-unavailable",
|
|
147
|
+
});
|
|
148
|
+
const speech = structuredClone(module);
|
|
149
|
+
speech.aiOperations![0]!.kind = "speech";
|
|
150
|
+
expect((await catalog(speech).list(context))[0]).toMatchObject({
|
|
151
|
+
selectable: false,
|
|
152
|
+
availabilityReason: "unsupported-operation",
|
|
153
|
+
});
|
|
154
|
+
delete context.actor.scopeContext;
|
|
155
|
+
expect((await catalog().list(context))[0]).toMatchObject({
|
|
156
|
+
selectable: false,
|
|
157
|
+
availabilityReason: "scope-unavailable",
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("Feature closure and missing registered output Schema fail closed", async () => {
|
|
162
|
+
const { module, hosts, runtime, context } = await fixture();
|
|
163
|
+
const missingSchema = structuredClone(module);
|
|
164
|
+
missingSchema.jsonSchemas = missingSchema.jsonSchemas!.filter(
|
|
165
|
+
({ key }) => key !== "business.output.v1",
|
|
166
|
+
);
|
|
167
|
+
expect(
|
|
168
|
+
() =>
|
|
169
|
+
new AgentAdminOperationCatalog({
|
|
170
|
+
modules: [missingSchema],
|
|
171
|
+
hosts,
|
|
172
|
+
runtime,
|
|
173
|
+
runtimeAvailable: () => true,
|
|
174
|
+
}),
|
|
175
|
+
).toThrow();
|
|
176
|
+
const disabledModule = structuredClone(module);
|
|
177
|
+
disabledModule.features = disabledModule.features.map((feature) => ({
|
|
178
|
+
...feature,
|
|
179
|
+
enabled: false,
|
|
180
|
+
}));
|
|
181
|
+
const disabledRuntime = createServerRuntime(
|
|
182
|
+
{},
|
|
183
|
+
{ featureCatalog: disabledModule.features },
|
|
184
|
+
);
|
|
185
|
+
const missing = new AgentAdminOperationCatalog({
|
|
186
|
+
modules: [disabledModule],
|
|
187
|
+
hosts,
|
|
188
|
+
runtime: disabledRuntime,
|
|
189
|
+
runtimeAvailable: () => true,
|
|
190
|
+
});
|
|
191
|
+
expect((await missing.list(context))[0]).toMatchObject({
|
|
192
|
+
selectable: false,
|
|
193
|
+
availabilityReason: "feature-unavailable",
|
|
194
|
+
});
|
|
195
|
+
context.license = { status: "expired", restricted: true };
|
|
196
|
+
context.licenseEnforced = true;
|
|
197
|
+
expect((await missing.list(context))[0]).toMatchObject({
|
|
198
|
+
selectable: false,
|
|
199
|
+
availabilityReason: "license-unavailable",
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ModuleManifest } from "@southwind-ai/modules";
|
|
2
|
+
import type {
|
|
3
|
+
AgentAdminAvailabilityReason,
|
|
4
|
+
AgentAdminOperationCatalogItem,
|
|
5
|
+
} from "@southwind-ai/shared";
|
|
6
|
+
import { evaluateAccessGuards } from "../access-guards.js";
|
|
7
|
+
import { isServerScopeContext } from "../data-access/context.js";
|
|
8
|
+
import type { RequestGuardContext } from "../guards.js";
|
|
9
|
+
import type { InitializedModuleHosts } from "../module-host.js";
|
|
10
|
+
import type { createServerRuntime } from "../runtime.js";
|
|
11
|
+
import {
|
|
12
|
+
buildRegisteredOperations,
|
|
13
|
+
type RegisteredOperation,
|
|
14
|
+
} from "./run-operation-registry.js";
|
|
15
|
+
import { catalogGuardReason } from "./tool-catalog-availability.js";
|
|
16
|
+
|
|
17
|
+
export interface AgentAdminOperationCatalogInput {
|
|
18
|
+
readonly modules: readonly ModuleManifest[];
|
|
19
|
+
readonly hosts: InitializedModuleHosts;
|
|
20
|
+
readonly runtime: ReturnType<typeof createServerRuntime>;
|
|
21
|
+
readonly runtimeAvailable: () => boolean;
|
|
22
|
+
/** Optional current readiness projection; no Provider execution or grants. */
|
|
23
|
+
readonly operationReadiness?: (
|
|
24
|
+
operationKey: string,
|
|
25
|
+
context: RequestGuardContext,
|
|
26
|
+
) => Promise<boolean>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class AgentAdminOperationCatalog {
|
|
30
|
+
readonly #operations: ReadonlyMap<string, RegisteredOperation>;
|
|
31
|
+
readonly #schemas: ReadonlyMap<
|
|
32
|
+
string,
|
|
33
|
+
NonNullable<ModuleManifest["jsonSchemas"]>[number]["schema"]
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly input: AgentAdminOperationCatalogInput) {
|
|
37
|
+
this.#operations = buildRegisteredOperations(input.modules, input.hosts);
|
|
38
|
+
this.#schemas = new Map(
|
|
39
|
+
input.modules.flatMap(({ jsonSchemas }) =>
|
|
40
|
+
(jsonSchemas ?? []).map(({ key, schema }) => [key, schema] as const),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async list(
|
|
46
|
+
context: RequestGuardContext,
|
|
47
|
+
): Promise<readonly AgentAdminOperationCatalogItem[]> {
|
|
48
|
+
return Promise.all(
|
|
49
|
+
[...this.#operations.values()].map(async (operation) => {
|
|
50
|
+
const definition = operation.definition;
|
|
51
|
+
const module = this.input.modules.find(({ aiOperations }) =>
|
|
52
|
+
aiOperations?.some(({ key }) => key === definition.key),
|
|
53
|
+
)!;
|
|
54
|
+
const inputSchema = this.#schemas.get(definition.inputSchemaKey);
|
|
55
|
+
const outputSchema = this.#schemas.get(definition.outputSchemaKey);
|
|
56
|
+
const availabilityReason = await this.availability(operation, context);
|
|
57
|
+
return {
|
|
58
|
+
key: definition.key,
|
|
59
|
+
module: module.name,
|
|
60
|
+
moduleLabel: module.label,
|
|
61
|
+
kind: definition.kind,
|
|
62
|
+
visibility: definition.visibility ?? "public",
|
|
63
|
+
execution: definition.execution,
|
|
64
|
+
featureKey: definition.featureKey,
|
|
65
|
+
permissionKey: definition.permissionKey,
|
|
66
|
+
auditAction: definition.auditAction,
|
|
67
|
+
dataClassification: definition.dataClassification,
|
|
68
|
+
requiredCapabilities: [...definition.requiredCapabilities],
|
|
69
|
+
inputModalities: [...(definition.inputModalities ?? ["text"])],
|
|
70
|
+
inputSchemaKey: definition.inputSchemaKey,
|
|
71
|
+
outputSchemaKey: definition.outputSchemaKey,
|
|
72
|
+
...(inputSchema ? { inputSchema: structuredClone(inputSchema) } : {}),
|
|
73
|
+
...(outputSchema
|
|
74
|
+
? { outputSchema: structuredClone(outputSchema) }
|
|
75
|
+
: {}),
|
|
76
|
+
allowedToolKeys: [...definition.allowedToolKeys],
|
|
77
|
+
limits: { ...definition.limits },
|
|
78
|
+
launchAuthority:
|
|
79
|
+
definition.clientExecution?.launchAuthority ?? "direct",
|
|
80
|
+
selectable: availabilityReason === "allowed",
|
|
81
|
+
availabilityReason,
|
|
82
|
+
};
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private async availability(
|
|
88
|
+
operation: RegisteredOperation,
|
|
89
|
+
context: RequestGuardContext,
|
|
90
|
+
): Promise<AgentAdminAvailabilityReason> {
|
|
91
|
+
const { definition } = operation;
|
|
92
|
+
if (!operation.feature) return "governance-unavailable";
|
|
93
|
+
const access = await evaluateAccessGuards(
|
|
94
|
+
this.input.runtime,
|
|
95
|
+
context,
|
|
96
|
+
{
|
|
97
|
+
feature: operation.feature,
|
|
98
|
+
permission: { permissionKey: definition.permissionKey },
|
|
99
|
+
},
|
|
100
|
+
{ record: false },
|
|
101
|
+
);
|
|
102
|
+
if (!access.decision.allowed)
|
|
103
|
+
return catalogGuardReason(access.decision.reason);
|
|
104
|
+
if (definition.visibility === "internal") return "internal-operation";
|
|
105
|
+
if (definition.kind !== "agent") return "unsupported-operation";
|
|
106
|
+
if (definition.clientExecution?.launchAuthority === "workflow-artifact")
|
|
107
|
+
return "workflow-only";
|
|
108
|
+
if (!operation.registration?.prepare || !operation.moduleName)
|
|
109
|
+
return "handler-unavailable";
|
|
110
|
+
if (!isServerScopeContext(context.actor.scopeContext))
|
|
111
|
+
return "scope-unavailable";
|
|
112
|
+
if (
|
|
113
|
+
!this.#schemas.has(definition.inputSchemaKey) ||
|
|
114
|
+
!this.#schemas.has(definition.outputSchemaKey)
|
|
115
|
+
)
|
|
116
|
+
return "governance-unavailable";
|
|
117
|
+
if (
|
|
118
|
+
definition.providerEgress?.mode === "configured-endpoint" &&
|
|
119
|
+
!definition.providerDataPolicy
|
|
120
|
+
)
|
|
121
|
+
return "governance-unavailable";
|
|
122
|
+
if (definition.isolatedExecution || !this.input.runtimeAvailable())
|
|
123
|
+
return "runtime-unavailable";
|
|
124
|
+
if (definition.readiness) {
|
|
125
|
+
if (!this.input.operationReadiness) return "runtime-unavailable";
|
|
126
|
+
try {
|
|
127
|
+
if (!(await this.input.operationReadiness(definition.key, context)))
|
|
128
|
+
return "runtime-unavailable";
|
|
129
|
+
} catch {
|
|
130
|
+
return "runtime-unavailable";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return "allowed";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { AgentRunFacade } from "./run-facade.js";
|
|
3
|
+
import { assertBrowserVisibleOperation } from "./run-browser-visibility.js";
|
|
4
|
+
import {
|
|
5
|
+
createRunFacadeFixture,
|
|
6
|
+
runFacadeManifest,
|
|
7
|
+
} from "./run-facade.test-support.js";
|
|
8
|
+
import { createTestRunAuthorizationStore } from "./run-authorization.test-support.js";
|
|
9
|
+
import { AgentAdminOperationCatalog } from "./operation-catalog.js";
|
|
10
|
+
|
|
11
|
+
describe("Internal operations remain private to trusted orchestration", () => {
|
|
12
|
+
test("Public/default operations remain browser-visible", async () => {
|
|
13
|
+
const { facade } = await createRunFacadeFixture();
|
|
14
|
+
expect(facade.operationPolicy("business.answer")?.visibility).toBe(
|
|
15
|
+
"public",
|
|
16
|
+
);
|
|
17
|
+
expect(() =>
|
|
18
|
+
assertBrowserVisibleOperation(facade, "business.answer"),
|
|
19
|
+
).not.toThrow();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("Internal declaration blocks browser access and Admin selection without blocking governed server orchestration", async () => {
|
|
23
|
+
const { hosts, runtime, remote, context } = await createRunFacadeFixture();
|
|
24
|
+
const module = runFacadeManifest();
|
|
25
|
+
module.aiOperations![0]!.visibility = "internal";
|
|
26
|
+
const facade = new AgentRunFacade(
|
|
27
|
+
[module],
|
|
28
|
+
hosts,
|
|
29
|
+
runtime,
|
|
30
|
+
remote,
|
|
31
|
+
createTestRunAuthorizationStore(),
|
|
32
|
+
);
|
|
33
|
+
expect(() =>
|
|
34
|
+
assertBrowserVisibleOperation(facade, "business.answer"),
|
|
35
|
+
).toThrow("Agent Run 不存在或不可访问");
|
|
36
|
+
const catalog = new AgentAdminOperationCatalog({
|
|
37
|
+
modules: [module],
|
|
38
|
+
hosts,
|
|
39
|
+
runtime,
|
|
40
|
+
runtimeAvailable: () => true,
|
|
41
|
+
});
|
|
42
|
+
expect((await catalog.list(context))[0]).toMatchObject({
|
|
43
|
+
visibility: "internal",
|
|
44
|
+
selectable: false,
|
|
45
|
+
availabilityReason: "internal-operation",
|
|
46
|
+
});
|
|
47
|
+
await facade.start(
|
|
48
|
+
"business.answer",
|
|
49
|
+
{ question: "可信服务端编排" },
|
|
50
|
+
"server-orchestration",
|
|
51
|
+
context,
|
|
52
|
+
);
|
|
53
|
+
expect(remote.started).toHaveLength(1);
|
|
54
|
+
context.actor.permissionKeys = [];
|
|
55
|
+
await expect(
|
|
56
|
+
facade.start("business.answer", { question: "无权" }, "denied", context),
|
|
57
|
+
).rejects.toMatchObject({ code: "FORBIDDEN" });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentRunFacade } from "./run-facade.js";
|
|
2
|
+
import { inaccessibleRun } from "./run-errors.js";
|
|
3
|
+
|
|
4
|
+
export function assertBrowserVisibleOperation(
|
|
5
|
+
facade: AgentRunFacade,
|
|
6
|
+
operationKey: string,
|
|
7
|
+
) {
|
|
8
|
+
if (facade.operationPolicy(operationKey)?.visibility === "internal") {
|
|
9
|
+
throw inaccessibleRun();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -12,6 +12,7 @@ export function publicAgentOperationPolicy(
|
|
|
12
12
|
allowedToolKeys: [...operation.definition.allowedToolKeys],
|
|
13
13
|
featureKey: operation.definition.featureKey,
|
|
14
14
|
permissionKey: operation.definition.permissionKey,
|
|
15
|
+
visibility: operation.definition.visibility ?? "public",
|
|
15
16
|
deploymentKey:
|
|
16
17
|
provider.mode === "configured-endpoint"
|
|
17
18
|
? provider.deploymentKey
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AgentRunFacadeError } from "./run-errors.js";
|
|
2
|
+
|
|
3
|
+
export function facadeError(error: unknown): Response {
|
|
4
|
+
if (error instanceof AgentRunFacadeError) {
|
|
5
|
+
return errorResponse(error.code, error.message, error.status);
|
|
6
|
+
}
|
|
7
|
+
return errorResponse(
|
|
8
|
+
"RUNTIME_UNAVAILABLE",
|
|
9
|
+
"Agent Runtime 暂不可用",
|
|
10
|
+
503,
|
|
11
|
+
true,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function errorResponse(
|
|
16
|
+
code: string,
|
|
17
|
+
message: string,
|
|
18
|
+
status: number,
|
|
19
|
+
retryable = false,
|
|
20
|
+
): Response {
|
|
21
|
+
return Response.json(
|
|
22
|
+
{ code, message, requestId: crypto.randomUUID(), retryable },
|
|
23
|
+
{ status },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function required(value: string | undefined): string {
|
|
28
|
+
if (!value)
|
|
29
|
+
throw new AgentRunFacadeError("INVALID_INPUT", "Run ID 无效", 400);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
35
|
+
}
|