@actiondock/core 2.8.0 → 2.9.0

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.
@@ -1,12 +1,12 @@
1
- import { CAPABILITY_UNAVAILABLE, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED, EXECUTION_FAILED, PACKAGE_FORBIDDEN, RUN_ALREADY_FINISHED, RUN_INTERRUPTED, RUN_NOT_FOUND, RUNS_CLEAR_ERROR, RUNS_LIST_ERROR, } from "../../errors.js";
1
+ import { ACTION_FORBIDDEN, CAPABILITY_UNAVAILABLE, EVENT_BACKPRESSURE_LIMIT, EVENT_CURSOR_EXPIRED, EXECUTION_FAILED, PACKAGE_FORBIDDEN, RUN_ALREADY_FINISHED, RUN_INTERRUPTED, RUN_NOT_FOUND, RUNS_CLEAR_ERROR, RUNS_LIST_ERROR, } from "../../errors.js";
2
2
  import { isTerminalRunStatus } from "../../storage/types.js";
3
3
  import { readJsonBody } from "../body.js";
4
- import { getSubPath, jsonResponse } from "./common.js";
4
+ import { getSubPath, isActionAllowedByPolicy, isPackageAllowedByPolicy, jsonResponse } from "./common.js";
5
5
  /**
6
6
  * 处理历史运行记录查询、清理、详情及 SSE 流式日志接口。
7
7
  */
8
8
  export async function handleRunsRoutes(ctx) {
9
- const { req, url, pathname, corsHeaders, options, service } = ctx;
9
+ const { req, url, pathname, corsHeaders, options, service, activePolicy: policy } = ctx;
10
10
  const subpath = getSubPath(pathname);
11
11
  // 1. Runs List: GET /api/v2/runs, GET /runs
12
12
  if (subpath === "/runs" && req.method === "GET") {
@@ -20,10 +20,7 @@ export async function handleRunsRoutes(ctx) {
20
20
  const limit = Number.isFinite(parsedLimit) && parsedLimit > 0
21
21
  ? Math.min(parsedLimit, 500)
22
22
  : 50;
23
- if (packageId &&
24
- options.packageAllowlist &&
25
- options.packageAllowlist.length > 0 &&
26
- !options.packageAllowlist.includes(packageId)) {
23
+ if (packageId && !isPackageAllowedByPolicy(packageId, policy)) {
27
24
  return jsonResponse({
28
25
  ok: false,
29
26
  error: {
@@ -32,6 +29,15 @@ export async function handleRunsRoutes(ctx) {
32
29
  },
33
30
  }, 403, corsHeaders);
34
31
  }
32
+ if (actionId && !isActionAllowedByPolicy({ packageId, actionId }, policy)) {
33
+ return jsonResponse({
34
+ ok: false,
35
+ error: {
36
+ code: ACTION_FORBIDDEN,
37
+ message: `Action '${packageId ? `${packageId}/${actionId}` : actionId}' is not in the allowed action list`,
38
+ },
39
+ }, 403, corsHeaders);
40
+ }
35
41
  let allRuns = await service.runs.list({
36
42
  actionId,
37
43
  status,
@@ -39,8 +45,11 @@ export async function handleRunsRoutes(ctx) {
39
45
  intent,
40
46
  limit,
41
47
  });
42
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
43
- allRuns = allRuns.filter((r) => !r.packageId || options.packageAllowlist.includes(r.packageId));
48
+ if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
49
+ allRuns = allRuns.filter((r) => isPackageAllowedByPolicy(r.packageId, policy));
50
+ }
51
+ if (policy.actionAllowlist && policy.actionAllowlist.length > 0) {
52
+ allRuns = allRuns.filter((r) => isActionAllowedByPolicy({ packageId: r.packageId, actionId: r.actionId }, policy));
44
53
  }
45
54
  const sliced = allRuns.slice(0, limit);
46
55
  return jsonResponse({ ok: true, total: allRuns.length, items: sliced }, 200, corsHeaders);
@@ -60,10 +69,7 @@ export async function handleRunsRoutes(ctx) {
60
69
  const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
61
70
  const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
62
71
  const status = url.searchParams.get("status") || body.status || undefined;
63
- if (packageId &&
64
- options.packageAllowlist &&
65
- options.packageAllowlist.length > 0 &&
66
- !options.packageAllowlist.includes(packageId)) {
72
+ if (packageId && !isPackageAllowedByPolicy(packageId, policy)) {
67
73
  return jsonResponse({
68
74
  ok: false,
69
75
  error: {
@@ -90,14 +96,21 @@ export async function handleRunsRoutes(ctx) {
90
96
  if (!run) {
91
97
  return jsonResponse({ ok: false, error: { code: RUN_NOT_FOUND, message: `Run '${runId}' not found` } }, 404, corsHeaders);
92
98
  }
93
- if (options.packageAllowlist &&
94
- options.packageAllowlist.length > 0 &&
95
- (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
99
+ if (!isPackageAllowedByPolicy(run.packageId, policy)) {
96
100
  return jsonResponse({
97
101
  ok: false,
98
102
  error: {
99
103
  code: PACKAGE_FORBIDDEN,
100
- message: `Package '${run.packageId}' is not in the allowed package list`,
104
+ message: `Package '${run.packageId || "unknown"}' is not in the allowed package list`,
105
+ },
106
+ }, 403, corsHeaders);
107
+ }
108
+ if (!isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy)) {
109
+ return jsonResponse({
110
+ ok: false,
111
+ error: {
112
+ code: ACTION_FORBIDDEN,
113
+ message: `Action '${run.packageId ? `${run.packageId}/${run.actionId}` : run.actionId}' is not in the allowed action list`,
101
114
  },
102
115
  }, 403, corsHeaders);
103
116
  }
@@ -258,9 +271,7 @@ export async function handleRunsRoutes(ctx) {
258
271
  },
259
272
  }, 404, corsHeaders);
260
273
  }
261
- if (options.packageAllowlist &&
262
- options.packageAllowlist.length > 0 &&
263
- (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
274
+ if (!isPackageAllowedByPolicy(run.packageId, policy)) {
264
275
  return jsonResponse({
265
276
  ok: false,
266
277
  error: {
@@ -269,22 +280,43 @@ export async function handleRunsRoutes(ctx) {
269
280
  },
270
281
  }, 403, corsHeaders);
271
282
  }
283
+ if (!isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy)) {
284
+ return jsonResponse({
285
+ ok: false,
286
+ error: {
287
+ code: ACTION_FORBIDDEN,
288
+ message: `Action '${run.packageId ? `${run.packageId}/${run.actionId}` : run.actionId}' is not in the allowed action list`,
289
+ },
290
+ }, 403, corsHeaders);
291
+ }
272
292
  return jsonResponse(run, 200, corsHeaders);
273
293
  }
274
294
  // 5. Run Cancel: POST /api/v2/runs/:runId/cancel, POST /runs/:runId/cancel
275
295
  const runCancelMatch = subpath.match(/^\/runs\/([^/]+)\/cancel$/);
276
296
  if (runCancelMatch && req.method === "POST") {
277
297
  const runId = decodeURIComponent(runCancelMatch[1]);
278
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
298
+ if ((policy.packageAllowlist && policy.packageAllowlist.length > 0) ||
299
+ (policy.actionAllowlist && policy.actionAllowlist.length > 0)) {
279
300
  const run = await service.runs.get(runId);
280
- if (run && (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
281
- return jsonResponse({
282
- ok: false,
283
- error: {
284
- code: PACKAGE_FORBIDDEN,
285
- message: `Package '${run.packageId}' is not in the allowed package list`,
286
- },
287
- }, 403, corsHeaders);
301
+ if (run) {
302
+ if (!isPackageAllowedByPolicy(run.packageId, policy)) {
303
+ return jsonResponse({
304
+ ok: false,
305
+ error: {
306
+ code: PACKAGE_FORBIDDEN,
307
+ message: `Package '${run.packageId || "unknown"}' is not in the allowed package list`,
308
+ },
309
+ }, 403, corsHeaders);
310
+ }
311
+ if (!isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy)) {
312
+ return jsonResponse({
313
+ ok: false,
314
+ error: {
315
+ code: ACTION_FORBIDDEN,
316
+ message: `Action '${run.packageId ? `${run.packageId}/${run.actionId}` : run.actionId}' is not in the allowed action list`,
317
+ },
318
+ }, 403, corsHeaders);
319
+ }
288
320
  }
289
321
  }
290
322
  let body = {};
@@ -1,6 +1,6 @@
1
1
  import { type RouteContext } from "./common.js";
2
2
  /**
3
3
  * 处理状态键名列表、读取、写入、删除及清空接口。
4
- * 在未显式开启管理功能 (options.enableManagement !== true) 时返回 403 拒绝。
4
+ * 在未显式开启管理功能时返回 403 拒绝。
5
5
  */
6
6
  export declare function handleStateRoutes(ctx: RouteContext): Promise<Response | null>;
@@ -1,7 +1,7 @@
1
1
  import { decodeStateKey } from "../../storage/index.js";
2
2
  import { AMBIGUOUS_STATE_KEY, CAPABILITY_UNAVAILABLE, INVALID_ARGUMENT, INVALID_PACKAGE_ID, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND, PATH_TRAVERSAL, STATE_CLEAR_ERROR, STATE_KEY_ERROR, STATE_KEY_NOT_FOUND, STATE_LIST_ERROR, } from "../../errors.js";
3
3
  import { readJsonBody } from "../body.js";
4
- import { getSubPath, jsonResponse, resolveTargetPackageId } from "./common.js";
4
+ import { getSubPath, isManagementAllowedByPolicy, jsonResponse, resolveTargetPackageId, } from "./common.js";
5
5
  function isClientStateError(err) {
6
6
  return (err?.code === PACKAGE_NOT_FOUND ||
7
7
  err?.code === INVALID_PACKAGE_ID ||
@@ -13,10 +13,10 @@ function isClientStateError(err) {
13
13
  }
14
14
  /**
15
15
  * 处理状态键名列表、读取、写入、删除及清空接口。
16
- * 在未显式开启管理功能 (options.enableManagement !== true) 时返回 403 拒绝。
16
+ * 在未显式开启管理功能时返回 403 拒绝。
17
17
  */
18
18
  export async function handleStateRoutes(ctx) {
19
- const { req, url, pathname, corsHeaders, options, service } = ctx;
19
+ const { req, url, pathname, corsHeaders, options, service, activePolicy: policy } = ctx;
20
20
  const subpath = getSubPath(pathname);
21
21
  const isStateRoute = subpath === "/state" ||
22
22
  subpath.startsWith("/state/") ||
@@ -25,7 +25,7 @@ export async function handleStateRoutes(ctx) {
25
25
  return null;
26
26
  }
27
27
  // 校验管理能力开关
28
- if (options.enableManagement !== true || !service.management?.state) {
28
+ if (!isManagementAllowedByPolicy(policy) || !service.management?.state) {
29
29
  return jsonResponse({
30
30
  ok: false,
31
31
  error: {
@@ -42,7 +42,7 @@ export async function handleStateRoutes(ctx) {
42
42
  const nsParam = url.searchParams.get("namespace") ?? undefined;
43
43
  const prefix = url.searchParams.get("prefix") || "";
44
44
  const pkgs = await service.discovery.listPackages();
45
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
45
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
46
46
  const keys = await service.management.state.list(targetPackageId, actionParam, {
47
47
  namespace: nsParam,
48
48
  prefix,
@@ -71,7 +71,7 @@ export async function handleStateRoutes(ctx) {
71
71
  const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || body.action || body.actionId || "";
72
72
  const baseNs = body.namespace ?? (url.searchParams.get("namespace") || undefined);
73
73
  const pkgs = await service.discovery.listPackages();
74
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
74
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
75
75
  const clearedCount = await service.management.state.clear(targetPackageId, actionParam, {
76
76
  namespace: baseNs,
77
77
  all: Boolean(body.all ?? url.searchParams.get("all") === "true"),
@@ -102,7 +102,7 @@ export async function handleStateRoutes(ctx) {
102
102
  const actionParam = url.searchParams.get("action") || url.searchParams.get("actionId") || "";
103
103
  const nsParam = url.searchParams.get("namespace") || undefined;
104
104
  const pkgs = await service.discovery.listPackages();
105
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
105
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
106
106
  if (req.method === "GET") {
107
107
  const entry = await service.management.state.get(targetPackageId, actionParam, key, {
108
108
  namespace: nsParam,
@@ -15,6 +15,17 @@ export interface VerifyBearerTokenOptions {
15
15
  /** 是否允许通过 URL 查询参数携带 Token(默认 false) */
16
16
  allowQueryToken?: boolean;
17
17
  }
18
+ /**
19
+ * 从 HTTP 请求中提取 Bearer Token。
20
+ *
21
+ * 默认仅接受 HTTP 请求头: `Authorization: Bearer <token>`。
22
+ * 仅当 allowQueryToken 为 true 时才允许解析 URL 查询参数 `?token=<token>`。
23
+ *
24
+ * @param req 传入的 HTTP Request 对象
25
+ * @param allowQueryToken 是否允许从 URL 查询参数解析 Token
26
+ * @returns 提取出的 Token 字符串,若未携带则返回 null
27
+ */
28
+ export declare function extractBearerToken(req: Request, allowQueryToken?: boolean): string | null;
18
29
  /**
19
30
  * 校验 HTTP 请求中的鉴权令牌是否有效。
20
31
  *
@@ -13,49 +13,66 @@ export function safeEqual(a, b) {
13
13
  const aa = Buffer.from(a);
14
14
  const bb = Buffer.from(b);
15
15
  if (aa.length !== bb.length) {
16
+ // 长度不等时对自身做一次恒定时间比较,消除因长度不等提前退出带来的时序差
17
+ timingSafeEqual(aa, aa);
16
18
  return false;
17
19
  }
18
20
  return timingSafeEqual(aa, bb);
19
21
  }
20
22
  /**
21
- * 校验 HTTP 请求中的鉴权令牌是否有效。
23
+ * 从 HTTP 请求中提取 Bearer Token。
22
24
  *
23
25
  * 默认仅接受 HTTP 请求头: `Authorization: Bearer <token>`。
24
- * 仅当 options.allowQueryToken 为 true 时才允许解析 URL 查询参数 `?token=<token>`。
26
+ * 仅当 allowQueryToken 为 true 时才允许解析 URL 查询参数 `?token=<token>`。
25
27
  *
26
28
  * @param req 传入的 HTTP Request 对象
27
- * @param expectedToken 服务端预期的正确 Token(若未配置 Token 则默认放行)
28
- * @param options 校验选项或布尔值 allowQueryToken 开关
29
- * @returns 是否鉴权成功
29
+ * @param allowQueryToken 是否允许从 URL 查询参数解析 Token
30
+ * @returns 提取出的 Token 字符串,若未携带则返回 null
30
31
  */
31
- export function verifyBearerToken(req, expectedToken, options) {
32
- if (!expectedToken || !expectedToken.trim()) {
33
- return true;
34
- }
35
- const trimmedExpected = expectedToken.trim();
32
+ export function extractBearerToken(req, allowQueryToken) {
36
33
  // 1. 请求头 Authorization: Bearer <token>
37
34
  const authHeader = req.headers.get("authorization");
38
35
  if (authHeader && authHeader.startsWith("Bearer ")) {
39
36
  const token = authHeader.slice(7).trim();
40
- if (safeEqual(token, trimmedExpected)) {
41
- return true;
42
- }
37
+ if (token)
38
+ return token;
43
39
  }
44
40
  // 2. URL 查询参数: ?token=<token>(仅当显式开启 allowQueryToken 时允许)
45
- const allowQuery = typeof options === "boolean" ? options : options?.allowQueryToken === true;
46
- if (allowQuery) {
41
+ if (allowQueryToken) {
47
42
  try {
48
43
  const url = new URL(req.url);
49
44
  const tokenParam = url.searchParams.get("token");
50
- if (tokenParam && safeEqual(tokenParam.trim(), trimmedExpected)) {
51
- return true;
45
+ if (tokenParam && tokenParam.trim()) {
46
+ return tokenParam.trim();
52
47
  }
53
48
  }
54
49
  catch {
55
- // 畸形 URL 视作鉴权失败
50
+ // 畸形 URL 忽略
56
51
  }
57
52
  }
58
- return false;
53
+ return null;
54
+ }
55
+ /**
56
+ * 校验 HTTP 请求中的鉴权令牌是否有效。
57
+ *
58
+ * 默认仅接受 HTTP 请求头: `Authorization: Bearer <token>`。
59
+ * 仅当 options.allowQueryToken 为 true 时才允许解析 URL 查询参数 `?token=<token>`。
60
+ *
61
+ * @param req 传入的 HTTP Request 对象
62
+ * @param expectedToken 服务端预期的正确 Token(若未配置 Token 则默认放行)
63
+ * @param options 校验选项或布尔值 allowQueryToken 开关
64
+ * @returns 是否鉴权成功
65
+ */
66
+ export function verifyBearerToken(req, expectedToken, options) {
67
+ if (!expectedToken || !expectedToken.trim()) {
68
+ return true;
69
+ }
70
+ const allowQuery = typeof options === "boolean" ? options : options?.allowQueryToken === true;
71
+ const token = extractBearerToken(req, allowQuery);
72
+ if (!token) {
73
+ return false;
74
+ }
75
+ return safeEqual(token, expectedToken.trim());
59
76
  }
60
77
  /**
61
78
  * 根据配置的 CORS 白名单解析并返回跨域响应头。
@@ -1,13 +1,15 @@
1
- import { resolve } from "node:path";
1
+ import { posix, resolve } from "node:path";
2
2
  import { NodeHttpServer } from "./http-server.js";
3
3
  import { createActionDock } from "../service/factory.js";
4
4
  import { NOT_FOUND, UNAUTHORIZED } from "../errors.js";
5
+ import { parseActionRef } from "../catalog/resolve-action.js";
5
6
  import { ensureDependencyClosure } from "../project/closure.js";
6
7
  import { findProjectRoot } from "../project/loader.js";
7
8
  import { listLinkedPackages, resolvePackageRoot } from "../registry/registry.js";
8
9
  import { handleActionsRoutes, handleConfigRoutes, handleDoctorRoute, handleHealthRoute, handleInfoRoute, handlePlaybooksRoutes, handleRunsRoutes, handleStateRoutes, jsonResponse, } from "./routes/index.js";
9
10
  import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security.js";
10
11
  import { createMcpEndpointHandler } from "./mcp-endpoint.js";
12
+ import { extractBearerToken, matchViewByToken, normalizeServerViews, } from "./policy.js";
11
13
  /**
12
14
  * 规范化主机地址用于拼接 URL。
13
15
  * 若 host 包含冒号 : 且不以 [ 开头,则添加中括号包裹(IPv6 标准格式)。
@@ -48,8 +50,13 @@ export async function startActionDockServer(options = {}) {
48
50
  const projectRoot = options.projectRoot
49
51
  ? resolve(options.projectRoot)
50
52
  : findProjectRoot(process.cwd());
53
+ // 1. 归一化解析视图配置集合(含默认视图与自定义视图)
54
+ const { defaultView, views } = normalizeServerViews(options);
51
55
  // 非回环地址强制要求配置 Token 鉴权(防裸奔)
52
- if (!isLoopbackHost(host) && !token && !options.allowInsecureNoAuth) {
56
+ const hasTokenConfigured = Boolean(token ||
57
+ defaultView.policy.token ||
58
+ Array.from(views.values()).some((v) => Boolean(v.policy.token)));
59
+ if (!isLoopbackHost(host) && !hasTokenConfigured && !options.allowInsecureNoAuth) {
53
60
  throw new Error("Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
54
61
  }
55
62
  let serviceInstance = options.service;
@@ -69,12 +76,27 @@ export async function startActionDockServer(options = {}) {
69
76
  if (!serviceInstance) {
70
77
  throw new Error("Failed to initialize ActionDockService for server");
71
78
  }
79
+ // 2. 聚合所有 views 声明的依赖包与动作,确保依赖闭包满足
72
80
  const roots = [];
73
81
  if (projectRoot) {
74
82
  roots.push(projectRoot);
75
83
  }
76
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
77
- for (const pkgId of options.packageAllowlist) {
84
+ const aggregatedPackageAllowlist = new Set();
85
+ const aggregatedActionAllowlist = new Set();
86
+ for (const v of views.values()) {
87
+ if (v.policy.packageAllowlist) {
88
+ for (const pkg of v.policy.packageAllowlist) {
89
+ aggregatedPackageAllowlist.add(pkg);
90
+ }
91
+ }
92
+ if (v.policy.actionAllowlist) {
93
+ for (const act of v.policy.actionAllowlist) {
94
+ aggregatedActionAllowlist.add(act);
95
+ }
96
+ }
97
+ }
98
+ if (aggregatedPackageAllowlist.size > 0) {
99
+ for (const pkgId of aggregatedPackageAllowlist) {
78
100
  const r = resolvePackageRoot(pkgId, projectRoot || undefined, customHome);
79
101
  if (r && !roots.includes(r)) {
80
102
  roots.push(r);
@@ -88,18 +110,85 @@ export async function startActionDockServer(options = {}) {
88
110
  }
89
111
  }
90
112
  }
113
+ if (aggregatedActionAllowlist.size > 0) {
114
+ for (const actRef of aggregatedActionAllowlist) {
115
+ try {
116
+ const parsed = parseActionRef(actRef);
117
+ if (parsed.packageId) {
118
+ const r = resolvePackageRoot(parsed.packageId, projectRoot || undefined, customHome);
119
+ if (r && !roots.includes(r)) {
120
+ roots.push(r);
121
+ }
122
+ }
123
+ }
124
+ catch { }
125
+ }
126
+ }
91
127
  if (roots.length > 0) {
92
128
  await ensureDependencyClosure(roots, { customHome });
93
129
  }
94
- // MCP 统一网关端点处理器:鉴权、请求体限流与委托序列收敛为共享单一事实源
95
- const mcpEndpointHandler = options.mcpHandler
96
- ? createMcpEndpointHandler(options.mcpHandler, {
97
- token,
98
- allowQueryToken: options.allowQueryToken,
99
- corsOrigins: options.corsOrigins,
100
- maxBodyBytes: options.maxBodyBytes,
101
- })
102
- : undefined;
130
+ // 3. 为每个启用 MCP 的视图独立装配 MCP 处理器
131
+ if (options.enableMcp !== false) {
132
+ for (const v of views.values()) {
133
+ if (v.enableMcp !== false) {
134
+ let delegate;
135
+ if (v.rawOptions.mcpHandler) {
136
+ if (v.rawOptions.mcpHandler.length >= 2) {
137
+ delegate = (req) => v.rawOptions.mcpHandler(req, v.policy);
138
+ }
139
+ else {
140
+ let factoryResult;
141
+ try {
142
+ factoryResult = v.rawOptions.mcpHandler(v.policy);
143
+ }
144
+ catch {
145
+ factoryResult = null;
146
+ }
147
+ if (typeof factoryResult === "function") {
148
+ delegate = factoryResult;
149
+ }
150
+ else {
151
+ delegate = (req) => v.rawOptions.mcpHandler(req, v.policy);
152
+ }
153
+ }
154
+ }
155
+ else if (options.mcpHandlerFactory) {
156
+ delegate = options.mcpHandlerFactory(v.policy);
157
+ }
158
+ else if (options.mcpHandler) {
159
+ if (options.mcpHandler.length >= 2) {
160
+ delegate = (req) => options.mcpHandler(req, v.policy);
161
+ }
162
+ else {
163
+ let factoryResult;
164
+ try {
165
+ factoryResult = options.mcpHandler(v.policy);
166
+ }
167
+ catch {
168
+ factoryResult = null;
169
+ }
170
+ if (typeof factoryResult === "function") {
171
+ delegate = factoryResult;
172
+ }
173
+ else {
174
+ if (factoryResult && typeof factoryResult.catch === "function") {
175
+ factoryResult.catch(() => { });
176
+ }
177
+ delegate = (req) => options.mcpHandler(req, v.policy);
178
+ }
179
+ }
180
+ }
181
+ if (delegate) {
182
+ v.mcpEndpointHandler = createMcpEndpointHandler(delegate, {
183
+ token: v.policy.token,
184
+ allowQueryToken: options.allowQueryToken,
185
+ corsOrigins: options.corsOrigins,
186
+ maxBodyBytes: options.maxBodyBytes,
187
+ });
188
+ }
189
+ }
190
+ }
191
+ }
103
192
  const fetchHandler = async (req) => {
104
193
  const origin = req.headers.get("origin");
105
194
  const corsHeaders = resolveCorsHeaders(origin, options.corsOrigins);
@@ -110,32 +199,113 @@ export async function startActionDockServer(options = {}) {
110
199
  });
111
200
  }
112
201
  const url = new URL(req.url);
113
- const pathname = url.pathname;
202
+ const rawPathname = url.pathname;
203
+ let activeView;
204
+ let effectivePathname;
205
+ let effectiveReq = req;
206
+ // 1. 检查是否为 /views/:viewName/... 命名空间路由
207
+ const viewMatch = rawPathname.match(/^\/views\/([^/]+)(\/.*)?$/);
208
+ if (viewMatch) {
209
+ let viewName;
210
+ try {
211
+ viewName = decodeURIComponent(viewMatch[1]);
212
+ }
213
+ catch {
214
+ return jsonResponse({
215
+ ok: false,
216
+ error: {
217
+ code: NOT_FOUND,
218
+ message: `Invalid view name encoding: '${viewMatch[1]}'`,
219
+ },
220
+ }, 400, corsHeaders);
221
+ }
222
+ const matched = views.get(viewName);
223
+ if (!matched) {
224
+ return jsonResponse({
225
+ ok: false,
226
+ error: {
227
+ code: NOT_FOUND,
228
+ message: `View '${viewName}' not found`,
229
+ },
230
+ }, 404, corsHeaders);
231
+ }
232
+ activeView = matched;
233
+ // 规范化子路径,防范路径穿透与多斜杠混淆
234
+ let rawEffective = viewMatch[2] || "/";
235
+ if (!rawEffective.startsWith("/")) {
236
+ rawEffective = "/" + rawEffective;
237
+ }
238
+ const normalizedPath = posix.normalize(rawEffective);
239
+ effectivePathname = normalizedPath.startsWith("/") ? normalizedPath : "/" + normalizedPath;
240
+ // 剥离 /views/:viewName 前缀后重构 URL 与 Request
241
+ const rewrittenUrl = new URL(req.url);
242
+ rewrittenUrl.pathname = effectivePathname;
243
+ try {
244
+ const init = {
245
+ method: req.method,
246
+ headers: req.headers,
247
+ signal: req.signal,
248
+ };
249
+ if (req.body && req.method !== "GET" && req.method !== "HEAD") {
250
+ init.body = req.body;
251
+ init.duplex = "half";
252
+ }
253
+ effectiveReq = new Request(rewrittenUrl.toString(), init);
254
+ }
255
+ catch {
256
+ effectiveReq = req;
257
+ }
258
+ }
259
+ else {
260
+ // 根路径请求:智能根据 Bearer Token 匹配视图或回退默认视图(遍历全量视图以防范时序差异)
261
+ effectivePathname = rawPathname;
262
+ const clientToken = extractBearerToken(req, options.allowQueryToken);
263
+ if (clientToken) {
264
+ const matchedByToken = matchViewByToken(clientToken, views.values());
265
+ activeView = matchedByToken ?? defaultView;
266
+ }
267
+ else {
268
+ activeView = defaultView;
269
+ }
270
+ }
271
+ const effectivePolicy = activeView.policy;
272
+ const effectiveUrl = new URL(effectiveReq.url);
273
+ effectiveUrl.pathname = effectivePathname;
114
274
  const ctx = {
115
- req,
116
- url,
117
- pathname,
275
+ req: effectiveReq,
276
+ url: effectiveUrl,
277
+ pathname: effectivePathname,
118
278
  corsHeaders,
119
279
  projectRoot,
120
280
  customHome,
121
281
  service: serviceInstance,
122
282
  options,
283
+ activePolicy: effectivePolicy,
123
284
  };
124
- // 1. 健康检查路由(内部处理独立鉴权逻辑)
285
+ // 2. 健康检查路由(内部处理独立鉴权逻辑,使用 activePolicy.token)
125
286
  const healthResponse = await handleHealthRoute(ctx);
126
287
  if (healthResponse) {
127
288
  return healthResponse;
128
289
  }
129
- // 2. MCP 统一网关端点(委托共享端点处理器)
130
- if (options.enableMcp !== false &&
131
- mcpEndpointHandler &&
132
- (pathname === "/mcp" || pathname.startsWith("/mcp/"))) {
133
- const mcpRes = await mcpEndpointHandler(req);
134
- if (mcpRes)
135
- return mcpRes;
290
+ // 3. MCP 统一网关端点(委托当前视图的端点处理器)
291
+ if (effectivePathname === "/mcp" || effectivePathname.startsWith("/mcp/")) {
292
+ if (activeView.enableMcp === false || options.enableMcp === false) {
293
+ return jsonResponse({
294
+ ok: false,
295
+ error: {
296
+ code: NOT_FOUND,
297
+ message: `MCP endpoint is disabled for view '${activeView.name}'`,
298
+ },
299
+ }, 404, corsHeaders);
300
+ }
301
+ if (activeView.mcpEndpointHandler) {
302
+ const mcpRes = await activeView.mcpEndpointHandler(effectiveReq);
303
+ if (mcpRes)
304
+ return mcpRes;
305
+ }
136
306
  }
137
- // 3. 全局 API 认证鉴权拦截
138
- if (!verifyBearerToken(req, token, options)) {
307
+ // 4. API 认证鉴权拦截(基于当前生效策略中的 Token)
308
+ if (!verifyBearerToken(effectiveReq, effectivePolicy.token, options)) {
139
309
  return jsonResponse({
140
310
  ok: false,
141
311
  error: {
@@ -144,7 +314,7 @@ export async function startActionDockServer(options = {}) {
144
314
  },
145
315
  }, 401, corsHeaders);
146
316
  }
147
- // 4. 业务领域路由分发(完全委托 Service)
317
+ // 5. 业务领域路由分发(完全委托 Service 与 RouteContext 中的 activePolicy)
148
318
  const routeResponse = (await handleInfoRoute(ctx)) ||
149
319
  (await handleDoctorRoute(ctx)) ||
150
320
  (await handleActionsRoutes(ctx)) ||
@@ -155,12 +325,12 @@ export async function startActionDockServer(options = {}) {
155
325
  if (routeResponse) {
156
326
  return routeResponse;
157
327
  }
158
- // 5. 404 路由兜底
328
+ // 6. 404 路由兜底
159
329
  return jsonResponse({
160
330
  ok: false,
161
331
  error: {
162
332
  code: NOT_FOUND,
163
- message: `Route not found: ${req.method} ${pathname}`,
333
+ message: `Route not found: ${req.method} ${rawPathname}`,
164
334
  },
165
335
  }, 404, corsHeaders);
166
336
  };