create-windy 0.7.0 → 0.8.1

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 (52) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +54 -44
  3. package/package.json +1 -1
  4. package/template/.windy-template.json +2 -2
  5. package/template/AGENTS.md +1 -1
  6. package/template/apps/agent-server/Dockerfile +1 -1
  7. package/template/apps/agent-server/README.md +1 -1
  8. package/template/apps/agent-server/src/southwind_agent_server/asgi.py +8 -0
  9. package/template/apps/agent-server/src/southwind_agent_server/runtime_logging.py +57 -0
  10. package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +134 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +4 -0
  12. package/template/apps/agent-server/tests/test_run_operational_logging.py +114 -0
  13. package/template/apps/agent-server/tests/test_structured_logging.py +105 -0
  14. package/template/apps/server/src/agent/isolated-execution-guard.test.ts +194 -0
  15. package/template/apps/server/src/agent/isolated-execution-guard.ts +202 -0
  16. package/template/apps/server/src/agent/run-definition-support.ts +11 -1
  17. package/template/apps/server/src/agent/run-facade.ts +7 -11
  18. package/template/apps/server/src/agent/run-operation-policy.ts +11 -0
  19. package/template/apps/server/src/agent/workflow-durable-runtime-input.ts +8 -5
  20. package/template/apps/server/src/jobs/legacy-service-runtime.ts +4 -0
  21. package/template/apps/server/src/jobs/runtime-logging.test.ts +212 -0
  22. package/template/apps/server/src/jobs/runtime-logging.ts +58 -0
  23. package/template/apps/server/src/jobs/worker.ts +54 -12
  24. package/template/apps/server/src/observability/bootstrap.ts +1 -4
  25. package/template/apps/server/src/observability/http-observability.ts +2 -3
  26. package/template/apps/server/src/observability/structured-logger.test.ts +31 -0
  27. package/template/apps/server/src/observability/structured-logger.ts +27 -0
  28. package/template/apps/server/src/platform-bootstrap.ts +9 -3
  29. package/template/apps/server/src/startup-finalization.ts +3 -3
  30. package/template/apps/server/src/system/built-in-roles.test.ts +23 -0
  31. package/template/apps/server/src/system/built-in-roles.ts +10 -4
  32. package/template/apps/web/src/pages/system/composables/useResourceEditor.ts +16 -5
  33. package/template/apps/web/src/pages/system/composables/useResourceEditor.webtest.ts +4 -0
  34. package/template/docker-compose.yml +3 -3
  35. package/template/docs/architecture/log-governance-implementation.md +79 -0
  36. package/template/docs/architecture/log-governance-plan.md +39 -0
  37. package/template/docs/operations/observability.md +36 -4
  38. package/template/docs/platform/module-configuration.md +1 -1
  39. package/template/docs/platform/organization-membership.md +1 -1
  40. package/template/docs/platform/platform-authentication.md +1 -1
  41. package/template/packages/modules/package.json +1 -1
  42. package/template/packages/modules/src/ai-isolated-execution-policy.test.ts +70 -0
  43. package/template/packages/modules/src/ai-isolated-execution-policy.ts +76 -0
  44. package/template/packages/modules/src/ai-operation-composition.ts +2 -0
  45. package/template/packages/modules/src/manifest.ts +23 -0
  46. package/template/packages/modules/src/system-features.ts +11 -10
  47. package/template/packages/server-sdk/package.json +1 -1
  48. package/template/packages/shared/index.ts +1 -0
  49. package/template/packages/shared/package.json +3 -2
  50. package/template/packages/shared/src/structured-log.test.ts +147 -0
  51. package/template/packages/shared/src/structured-log.ts +215 -0
  52. package/template/scripts/lib/postgres-regression.ts +1 -1
@@ -0,0 +1,114 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import io
5
+ import json
6
+ import logging
7
+ from collections.abc import Iterator
8
+ from typing import TextIO, cast
9
+
10
+ import httpx
11
+ import pytest
12
+
13
+ from southwind_agent_server.app import create_app
14
+ from southwind_agent_server.deterministic import DeterministicEngineAdapter
15
+ from southwind_agent_server.runtime_logging import AgentOperationalAttempt, logger
16
+ from southwind_agent_server.service import AgentRunService
17
+ from southwind_agent_server.store import RunRecord
18
+ from southwind_agent_server.structured_logging import AgentStructuredLogHandler
19
+
20
+
21
+ @pytest.fixture(autouse=True)
22
+ def operational_log_level() -> Iterator[None]:
23
+ previous = logger.level
24
+ logger.setLevel(logging.INFO)
25
+ try:
26
+ yield
27
+ finally:
28
+ logger.setLevel(previous)
29
+
30
+
31
+ @pytest.mark.parametrize("broken_sink", [False, True])
32
+ async def test_real_run_completion_and_replay_with_operational_logs(
33
+ monkeypatch: pytest.MonkeyPatch,
34
+ broken_sink: bool,
35
+ ) -> None:
36
+ class Stream(io.StringIO):
37
+ def write(self, value: str) -> int:
38
+ written = super().write(value)
39
+ if broken_sink:
40
+ raise OSError("sink-secret")
41
+ return written
42
+
43
+ stream = Stream()
44
+ monkeypatch.setattr(logger, "handlers", [AgentStructuredLogHandler(stream)])
45
+ monkeypatch.setattr(logger, "propagate", False)
46
+ app = create_app(DeterministicEngineAdapter(), heartbeat_seconds=0.01)
47
+ service = cast(AgentRunService, app.state.run_service)
48
+ async with httpx.AsyncClient(
49
+ transport=httpx.ASGITransport(app=app),
50
+ base_url="http://agent.test",
51
+ ) as client:
52
+
53
+ async def start() -> httpx.Response:
54
+ return await client.post(
55
+ "/api/agent/runs",
56
+ headers={"Idempotency-Key": "operational-replay"},
57
+ json={
58
+ "operationKey": "test.echo",
59
+ "input": {"text": "input-secret"},
60
+ "traceId": "1" * 32,
61
+ },
62
+ )
63
+
64
+ response = await start()
65
+ assert response.status_code == 200
66
+ run_id = str(response.json()["runId"])
67
+ await asyncio.gather(*service._tasks)
68
+ replay = await start()
69
+ assert replay.json()["runId"] == run_id
70
+ events = await client.get(f"/api/agent/runs/{run_id}/events")
71
+ result = await client.get(f"/api/agent/runs/{run_id}/result")
72
+ assert result.json()["status"] == "completed"
73
+ assert result.json()["output"] == {"text": "input-secret"}
74
+ assert "run.failed" not in events.text
75
+ records = [json.loads(line) for line in stream.getvalue().splitlines()]
76
+ assert [record["event"] for record in records] == [
77
+ "agent.run.attempt.started",
78
+ "agent.run.attempt.completed",
79
+ ]
80
+ assert all(record["runId"] == run_id for record in records)
81
+ assert all(record["traceId"] == "1" * 32 for record in records)
82
+ assert records[-1]["outcome"] == "success"
83
+ assert records[-1]["durationMs"] >= 0
84
+ assert "input-secret" not in stream.getvalue()
85
+ assert "sink-secret" not in stream.getvalue()
86
+
87
+
88
+ @pytest.mark.parametrize("status", ["failed", "cancelled", "running"])
89
+ def test_attempt_terminal_mapping_and_duplicate_finish_are_safe(
90
+ monkeypatch: pytest.MonkeyPatch,
91
+ status: str,
92
+ ) -> None:
93
+ stream = io.StringIO()
94
+ monkeypatch.setattr(logger, "handlers", [AgentStructuredLogHandler(stream)])
95
+ monkeypatch.setattr(logger, "propagate", False)
96
+ record = RunRecord("run-1", "test.echo", {"secret": "input"}, "key", "digest")
97
+ attempt = AgentOperationalAttempt(record)
98
+ attempt.start()
99
+ attempt.start()
100
+ attempt.finish(status)
101
+ attempt.finish(status)
102
+ records = [json.loads(line) for line in stream.getvalue().splitlines()]
103
+ assert len(records) == 2
104
+ assert records[-1]["outcome"] == "failure"
105
+ expected = status if status in {"failed", "cancelled"} else "interrupted"
106
+ assert records[-1]["errorCode"] == f"AGENT_RUN_{expected.upper()}"
107
+ assert "secret" not in stream.getvalue()
108
+
109
+
110
+ def test_unknown_info_message_does_not_expand_log_content() -> None:
111
+ stream = io.StringIO()
112
+ handler = AgentStructuredLogHandler(cast(TextIO, stream))
113
+ handler.emit(logging.makeLogRecord({"levelno": logging.INFO, "msg": "secret"}))
114
+ assert stream.getvalue() == ""
@@ -0,0 +1,105 @@
1
+ import io
2
+ import json
3
+ import logging
4
+ from importlib import import_module
5
+ from typing import TextIO, cast
6
+
7
+ from southwind_agent_server.structured_logging import (
8
+ MAX_LOG_BYTES,
9
+ AgentStructuredLogHandler,
10
+ agent_log_record,
11
+ configure_agent_logging,
12
+ )
13
+
14
+
15
+ def test_agent_report_failure_keeps_only_safe_correlation() -> None:
16
+ stream = io.StringIO()
17
+ handler = AgentStructuredLogHandler(stream)
18
+ logger = logging.getLogger("southwind_agent_server.test_structured_logging")
19
+ logger.addHandler(handler)
20
+ logger.setLevel(logging.WARNING)
21
+ logger.propagate = False
22
+ try:
23
+ try:
24
+ raise ValueError("secret-error")
25
+ except ValueError:
26
+ logger.exception(
27
+ "Agent Runtime 报表写入失败",
28
+ extra={
29
+ "run_id": "run-1",
30
+ "report_stage": "run_terminal",
31
+ "request_id": "bad\nrequest-secret",
32
+ "body": "secret-body",
33
+ "authorization": "Bearer secret-token",
34
+ },
35
+ )
36
+ finally:
37
+ logger.removeHandler(handler)
38
+ line = stream.getvalue().strip()
39
+ parsed = json.loads(line)
40
+ assert parsed["event"] == "agent.report.write.failed"
41
+ assert parsed["errorCode"] == "AGENT_REPORT_WRITE_FAILED"
42
+ assert parsed["runId"] == "run-1"
43
+ assert parsed["stageKey"] == "run_terminal"
44
+ assert parsed["retentionClass"] == "operational"
45
+ assert len(line.encode("utf-8")) <= MAX_LOG_BYTES
46
+ assert all(
47
+ secret not in line
48
+ for secret in (
49
+ "request-secret",
50
+ "secret-body",
51
+ "secret-token",
52
+ "secret-error",
53
+ )
54
+ )
55
+
56
+
57
+ def test_unknown_message_and_long_fields_are_bounded() -> None:
58
+ record = logging.makeLogRecord(
59
+ {
60
+ "name": "southwind_agent_server.service",
61
+ "levelno": logging.ERROR,
62
+ "msg": "secret-message",
63
+ "run_id": "x" * 500,
64
+ "report_stage": "payload secret",
65
+ }
66
+ )
67
+ safe = agent_log_record(record)
68
+ assert safe["event"] == "agent.runtime.failed"
69
+ assert "runId" not in safe
70
+ assert "stageKey" not in safe
71
+ assert "secret-message" not in json.dumps(safe)
72
+ assert len(json.dumps(safe).encode("utf-8")) <= MAX_LOG_BYTES
73
+
74
+
75
+ def test_asgi_entrypoint_installs_handler_without_changing_app() -> None:
76
+ from southwind_agent_server.app import app
77
+
78
+ asgi = import_module("southwind_agent_server.asgi")
79
+ configure_agent_logging()
80
+ handlers = [
81
+ handler
82
+ for handler in logging.getLogger("southwind_agent_server").handlers
83
+ if isinstance(handler, AgentStructuredLogHandler)
84
+ ]
85
+ assert asgi.app is app
86
+ assert len(handlers) == 1
87
+
88
+
89
+ def test_stdout_failure_does_not_interrupt_runtime() -> None:
90
+ class BrokenStream:
91
+ def write(self, _value: str) -> None:
92
+ raise OSError("stdout unavailable")
93
+
94
+ def flush(self) -> None:
95
+ raise OSError("stdout unavailable")
96
+
97
+ handler = AgentStructuredLogHandler(cast(TextIO, BrokenStream()))
98
+ record = logging.makeLogRecord(
99
+ {
100
+ "name": "southwind_agent_server.service",
101
+ "levelno": logging.ERROR,
102
+ "msg": "Agent Run 恢复轮询失败,将继续重试",
103
+ }
104
+ )
105
+ handler.emit(record)
@@ -0,0 +1,194 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { ModuleManifest } from "@southwind-ai/modules";
3
+ import { buildScopeContext } from "../data-access/context.js";
4
+ import { createRequestGuardContext } from "../guards.js";
5
+ import type { InitializedModuleHosts } from "../module-host.js";
6
+ import { createServerRuntime } from "../runtime.js";
7
+ import { IsolatedExecutionGuard } from "./isolated-execution-guard.js";
8
+ import { createTestRunAuthorizationStore } from "./run-authorization.test-support.js";
9
+ import { buildAgentRuntimeRelease } from "./run-release.js";
10
+
11
+ const definition: NonNullable<ModuleManifest["aiOperations"]>[number] = {
12
+ key: "business.answer",
13
+ kind: "agent",
14
+ featureKey: "business.ai",
15
+ permissionKey: "business.ai.execute",
16
+ auditAction: "business.ai.execute",
17
+ inputSchemaKey: "business.input.v1",
18
+ outputSchemaKey: "business.output.v1",
19
+ requiredCapabilities: ["text-generation"],
20
+ allowedToolKeys: [],
21
+ dataClassification: "internal",
22
+ execution: "streaming",
23
+ toolNetworkPolicy: { mode: "forbidden" },
24
+ limits: { maxDurationSeconds: 60, maxInputBytes: 4096, maxOutputBytes: 4096 },
25
+ isolatedExecution: {
26
+ version: 1,
27
+ mode: "isolated",
28
+ languages: ["python"],
29
+ inputArtifactTypes: ["text/plain"],
30
+ network: "forbidden",
31
+ limits: {
32
+ maxWorkspaceBytes: 4096,
33
+ maxFileBytes: 2048,
34
+ maxOutputBytes: 1024,
35
+ maxDurationSeconds: 30,
36
+ maxCpuMillis: 30_000,
37
+ maxMemoryBytes: 134_217_728,
38
+ maxPids: 4,
39
+ maxConcurrentProcesses: 1,
40
+ maxToolCalls: 3,
41
+ maxToolRounds: 2,
42
+ },
43
+ },
44
+ };
45
+
46
+ const module = {
47
+ name: "business",
48
+ version: "0.1.0",
49
+ features: [
50
+ {
51
+ key: "business.ai",
52
+ enabled: true,
53
+ module: "business",
54
+ label: "AI",
55
+ visible: "visible",
56
+ stage: "stable",
57
+ },
58
+ ],
59
+ jsonSchemas: [
60
+ { key: "business.input.v1", schema: { type: "object" } },
61
+ { key: "business.output.v1", schema: { type: "object" } },
62
+ ],
63
+ tools: [],
64
+ aiOperations: [definition],
65
+ } as unknown as ModuleManifest;
66
+ const hosts = { aiOperationHandlers: [] } as unknown as InitializedModuleHosts;
67
+
68
+ describe("隔离执行逐次准入", () => {
69
+ test("运行时或持久预算端口缺失时拒绝,绝不执行宿主脚本", async () => {
70
+ const fixture = await setup();
71
+ await expect(
72
+ fixture.guard().admit(fixture.invocation),
73
+ ).rejects.toMatchObject({
74
+ code: "RUNTIME_UNAVAILABLE",
75
+ });
76
+ });
77
+
78
+ test("当前授权、Run 状态及预算每次复核", async () => {
79
+ const fixture = await setup();
80
+ const calls: unknown[] = [];
81
+ const guard = fixture.guard({
82
+ state: { status: async () => "running" },
83
+ budget: {
84
+ reserve: async (input) => {
85
+ calls.push(input);
86
+ return true;
87
+ },
88
+ },
89
+ available: true,
90
+ });
91
+ await expect(guard.admit(fixture.invocation)).resolves.toEqual(
92
+ definition.isolatedExecution!,
93
+ );
94
+ expect(calls).toEqual([
95
+ {
96
+ runId: "run_1",
97
+ callId: "call_1",
98
+ maxToolCalls: 3,
99
+ maxToolRounds: 2,
100
+ maxConcurrentProcesses: 1,
101
+ },
102
+ ]);
103
+ await expect(
104
+ guard.admit({ ...fixture.invocation, language: "node" }),
105
+ ).rejects.toMatchObject({ code: "EXECUTION_DENIED" });
106
+
107
+ const denied = fixture.guard({
108
+ state: { status: async () => "terminal" },
109
+ budget: {
110
+ reserve: async () => {
111
+ throw new Error("terminal Run must not reserve");
112
+ },
113
+ },
114
+ available: true,
115
+ });
116
+ await expect(denied.admit(fixture.invocation)).rejects.toMatchObject({
117
+ code: "EXECUTION_DENIED",
118
+ });
119
+ fixture.actor.permissionKeys = [];
120
+ await expect(guard.admit(fixture.invocation)).rejects.toMatchObject({
121
+ code: "EXECUTION_DENIED",
122
+ });
123
+ expect(calls).toHaveLength(1);
124
+ });
125
+ });
126
+
127
+ async function setup() {
128
+ const actor = {
129
+ id: "actor-1",
130
+ type: "user" as const,
131
+ name: "测试用户",
132
+ roleCodes: ["user"],
133
+ permissionKeys: ["business.ai.execute"],
134
+ scopeContext: buildScopeContext("actor-1", undefined, [
135
+ { id: "role-user", code: "user", dataScope: "self", departmentIds: [] },
136
+ ]),
137
+ };
138
+ const runtime = createServerRuntime(
139
+ {},
140
+ {
141
+ featureCatalog: module.features,
142
+ licenseResolver: { resolve: () => ({ status: "valid" }) },
143
+ },
144
+ );
145
+ const context = createRequestGuardContext(
146
+ new Request("http://localhost/api/agent/runs"),
147
+ {
148
+ resolveActor: () => actor,
149
+ resolveLicense: () => ({ status: "valid" }),
150
+ },
151
+ );
152
+ const authorizations = createTestRunAuthorizationStore();
153
+ const grant = await authorizations.issue({
154
+ operationKey: definition.key,
155
+ releaseId: buildAgentRuntimeRelease([module], module, definition, undefined)
156
+ .releaseId,
157
+ idempotencyKey: "isolated-execution-test",
158
+ input: { question: "analyze" },
159
+ authorizationSnapshot: {
160
+ permissionKey: definition.permissionKey,
161
+ featureKey: definition.featureKey,
162
+ allowedToolKeys: [],
163
+ toolNetworkMode: "forbidden",
164
+ },
165
+ context,
166
+ });
167
+ await authorizations.bind(grant, "run_1");
168
+ return {
169
+ actor,
170
+ invocation: {
171
+ executionGrant: grant.token,
172
+ runId: "run_1",
173
+ callId: "call_1",
174
+ language: "python" as const,
175
+ },
176
+ guard: (
177
+ ports: {
178
+ state?: ConstructorParameters<typeof IsolatedExecutionGuard>[5];
179
+ budget?: ConstructorParameters<typeof IsolatedExecutionGuard>[6];
180
+ available?: boolean;
181
+ } = {},
182
+ ) =>
183
+ new IsolatedExecutionGuard(
184
+ [module],
185
+ hosts,
186
+ runtime,
187
+ authorizations,
188
+ async () => actor,
189
+ ports.state,
190
+ ports.budget,
191
+ ports.available,
192
+ ),
193
+ };
194
+ }
@@ -0,0 +1,202 @@
1
+ import type {
2
+ AiIsolatedExecutionPolicyV1,
3
+ ModuleManifest,
4
+ } from "@southwind-ai/modules";
5
+ import { evaluateAccessGuards } from "../access-guards.js";
6
+ import type { RequestActor } from "../guards.js";
7
+ import { findFeature } from "../route-guards.js";
8
+ import type { createServerRuntime } from "../runtime.js";
9
+ import type { InitializedModuleHosts } from "../module-host.js";
10
+ import type { AgentRunAuthorizationStore } from "./run-authorization.js";
11
+ import { buildAgentRuntimeRelease } from "./run-release.js";
12
+
13
+ type ServerRuntime = ReturnType<typeof createServerRuntime>;
14
+
15
+ /** Must reserve atomically and durably by Run and call ID; absent storage denies admission. */
16
+ export interface IsolatedExecutionBudgetPort {
17
+ reserve(input: {
18
+ readonly runId: string;
19
+ readonly callId: string;
20
+ readonly maxToolCalls: number;
21
+ readonly maxToolRounds: number;
22
+ readonly maxConcurrentProcesses: number;
23
+ }): Promise<boolean>;
24
+ }
25
+
26
+ export interface IsolatedExecutionRunStatePort {
27
+ status(runId: string): Promise<"accepted" | "running" | "terminal">;
28
+ }
29
+
30
+ export class IsolatedExecutionAdmissionError extends Error {
31
+ constructor(
32
+ readonly code:
33
+ | "EXECUTION_DENIED"
34
+ | "BUDGET_EXCEEDED"
35
+ | "RUNTIME_UNAVAILABLE",
36
+ message: string,
37
+ ) {
38
+ super(message);
39
+ }
40
+ }
41
+
42
+ /** Revalidates the authoritative Run and current governance facts for every process call. */
43
+ export class IsolatedExecutionGuard {
44
+ readonly #operations: Map<
45
+ string,
46
+ {
47
+ definition: NonNullable<ModuleManifest["aiOperations"]>[number];
48
+ releaseId: string;
49
+ }
50
+ >;
51
+ readonly #features: ModuleManifest["features"];
52
+
53
+ constructor(
54
+ modules: readonly ModuleManifest[],
55
+ hosts: InitializedModuleHosts,
56
+ private readonly runtime: ServerRuntime,
57
+ private readonly authorizations: AgentRunAuthorizationStore,
58
+ private readonly resolveActor: (
59
+ actorId: string,
60
+ ) => Promise<RequestActor | undefined>,
61
+ private readonly state?: IsolatedExecutionRunStatePort,
62
+ private readonly budget?: IsolatedExecutionBudgetPort,
63
+ private readonly executionRuntimeAvailable = false,
64
+ ) {
65
+ this.#features = modules.flatMap((module) => module.features);
66
+ const handlers = new Map(
67
+ hosts.aiOperationHandlers.map(({ registration }) => [
68
+ registration.operationKey,
69
+ registration,
70
+ ]),
71
+ );
72
+ this.#operations = new Map(
73
+ modules.flatMap((module) =>
74
+ (module.aiOperations ?? [])
75
+ .filter((definition) => definition.kind === "agent")
76
+ .map(
77
+ (definition) =>
78
+ [
79
+ definition.key,
80
+ {
81
+ definition,
82
+ releaseId: buildAgentRuntimeRelease(
83
+ modules,
84
+ module,
85
+ definition,
86
+ handlers.get(definition.key),
87
+ ).releaseId,
88
+ },
89
+ ] as const,
90
+ ),
91
+ ),
92
+ );
93
+ }
94
+
95
+ async admit(input: {
96
+ readonly executionGrant: string;
97
+ readonly runId: string;
98
+ readonly callId: string;
99
+ readonly language: "python" | "node";
100
+ }): Promise<AiIsolatedExecutionPolicyV1> {
101
+ if (!input.callId || !input.runId) throw denied("执行调用坐标缺失");
102
+ let binding;
103
+ try {
104
+ binding = await this.authorizations.authorizeTool(
105
+ input.executionGrant,
106
+ input.runId,
107
+ );
108
+ } catch {
109
+ throw denied("Agent Run 执行授权无效");
110
+ }
111
+ const current = this.#operations.get(binding.operationKey);
112
+ const policy = current?.definition.isolatedExecution;
113
+ if (
114
+ !current ||
115
+ !policy ||
116
+ current.releaseId !== binding.releaseId ||
117
+ !policy.languages.includes(input.language) ||
118
+ binding.authorizationSnapshot.toolNetworkMode !== "forbidden" ||
119
+ binding.authorizationSnapshot.delegation !== undefined ||
120
+ binding.authorizationSnapshot.runtimeBudgetDigest !== undefined ||
121
+ current.definition.featureKey !==
122
+ binding.authorizationSnapshot.featureKey ||
123
+ current.definition.permissionKey !==
124
+ binding.authorizationSnapshot.permissionKey
125
+ ) {
126
+ throw denied("Operation 未授权隔离执行或版本已变化");
127
+ }
128
+ let actor: RequestActor | undefined;
129
+ try {
130
+ actor = await this.resolveActor(binding.actorId);
131
+ } catch {
132
+ throw denied("Agent Run Actor 当前不可用");
133
+ }
134
+ if (!actor || actor.id !== binding.actorId || actor.type === "anonymous") {
135
+ throw denied("Agent Run Actor 当前不可用");
136
+ }
137
+ let context: Awaited<
138
+ ReturnType<ServerRuntime["createGuardContextForActor"]>
139
+ >;
140
+ try {
141
+ context = await this.runtime.createGuardContextForActor(
142
+ new Request("http://agent-execution.internal"),
143
+ actor,
144
+ );
145
+ } catch {
146
+ throw denied("Agent Run 当前授权无法复核");
147
+ }
148
+ try {
149
+ this.authorizations.assertCurrentScope(binding, context);
150
+ } catch {
151
+ throw denied("Agent Run 当前数据范围已变化");
152
+ }
153
+ let access: Awaited<ReturnType<typeof evaluateAccessGuards>>;
154
+ try {
155
+ access = await evaluateAccessGuards(this.runtime, context, {
156
+ permission: { permissionKey: current.definition.permissionKey },
157
+ feature: findFeature(this.#features, current.definition.featureKey),
158
+ });
159
+ } catch {
160
+ throw denied("Agent Operation 当前授权无法复核");
161
+ }
162
+ if (!access.decision.allowed) throw denied("Agent Operation 当前无权执行");
163
+ if (!this.executionRuntimeAvailable || !this.state || !this.budget) {
164
+ throw new IsolatedExecutionAdmissionError(
165
+ "RUNTIME_UNAVAILABLE",
166
+ "隔离执行 Runtime 或预算存储未装配",
167
+ );
168
+ }
169
+ let status: "accepted" | "running" | "terminal";
170
+ try {
171
+ status = await this.state.status(input.runId);
172
+ } catch {
173
+ throw denied("Agent Run 状态无法复核");
174
+ }
175
+ if (status !== "running") throw denied("Agent Run 当前不可执行");
176
+ let reserved: boolean;
177
+ try {
178
+ reserved = await this.budget.reserve({
179
+ runId: input.runId,
180
+ callId: input.callId,
181
+ maxToolCalls: policy.limits.maxToolCalls,
182
+ maxToolRounds: policy.limits.maxToolRounds,
183
+ maxConcurrentProcesses: policy.limits.maxConcurrentProcesses,
184
+ });
185
+ } catch {
186
+ throw new IsolatedExecutionAdmissionError(
187
+ "RUNTIME_UNAVAILABLE",
188
+ "执行预算无法原子预留",
189
+ );
190
+ }
191
+ if (!reserved)
192
+ throw new IsolatedExecutionAdmissionError(
193
+ "BUDGET_EXCEEDED",
194
+ "隔离执行预算已耗尽",
195
+ );
196
+ return policy;
197
+ }
198
+ }
199
+
200
+ function denied(message: string): IsolatedExecutionAdmissionError {
201
+ return new IsolatedExecutionAdmissionError("EXECUTION_DENIED", message);
202
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ModuleManifest } from "@southwind-ai/modules";
2
2
  import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
3
- import type { AgentRuntimeError } from "./run-contracts.js";
3
+ import { AgentRuntimeError } from "./run-contracts.js";
4
4
  import { AgentRunFacadeError } from "./run-errors.js";
5
5
 
6
6
  export function toolDefinitions(
@@ -56,3 +56,13 @@ export function publicRunPath(
56
56
  ) {
57
57
  return `/api/agent/runs/${encodeURIComponent(runId)}/${resource}?operationKey=${encodeURIComponent(operationKey)}`;
58
58
  }
59
+
60
+ export function mapAgentRuntimeError(error: unknown) {
61
+ return error instanceof AgentRuntimeError
62
+ ? new AgentRunFacadeError(
63
+ error.code,
64
+ error.message,
65
+ runtimeErrorStatus(error.code),
66
+ )
67
+ : error;
68
+ }
@@ -17,7 +17,6 @@ import type {
17
17
  AgentRuntimePort,
18
18
  AgentRuntimeStartResponse,
19
19
  } from "./run-contracts.js";
20
- import { AgentRuntimeError } from "./run-contracts.js";
21
20
  import {
22
21
  rotateVisionArtifacts,
23
22
  validateVisionArtifacts,
@@ -33,7 +32,7 @@ import { attachAgentRun } from "./run-attach.js";
33
32
  import {
34
33
  digest,
35
34
  publicRunPath,
36
- runtimeErrorStatus,
35
+ mapAgentRuntimeError,
37
36
  } from "./run-definition-support.js";
38
37
  import { AgentRunFacadeError } from "./run-errors.js";
39
38
  import { scopedRuntimeIdempotencyKey } from "./idempotency-key.js";
@@ -63,7 +62,10 @@ import {
63
62
  resolveAgentRunExecutionPolicy,
64
63
  type AgentRunDelegationPolicy,
65
64
  } from "./agent-run-delegation-policy.js";
66
- import { publicAgentOperationPolicy } from "./run-operation-policy.js";
65
+ import {
66
+ assertAgentRuntimeAssembled,
67
+ publicAgentOperationPolicy,
68
+ } from "./run-operation-policy.js";
67
69
  export { AgentRunFacadeError } from "./run-errors.js";
68
70
  type ServerRuntime = ReturnType<typeof createServerRuntime>;
69
71
  export class AgentRunFacade {
@@ -124,6 +126,7 @@ export class AgentRunFacade {
124
126
  ? { sessionBinding: this.#sessionBindingAuthorization }
125
127
  : {}),
126
128
  });
129
+ assertAgentRuntimeAssembled(operation);
127
130
  const executionPolicy = resolveAgentRunExecutionPolicy(
128
131
  operation,
129
132
  context,
@@ -265,14 +268,7 @@ export class AgentRunFacade {
265
268
  sessionTurn,
266
269
  sessionLinked,
267
270
  });
268
- if (error instanceof AgentRuntimeError) {
269
- throw new AgentRunFacadeError(
270
- error.code,
271
- error.message,
272
- runtimeErrorStatus(error.code),
273
- );
274
- }
275
- throw error;
271
+ throw mapAgentRuntimeError(error);
276
272
  }
277
273
  }
278
274
  async authorizeRun(