@actiondock/core 2.8.0 → 2.9.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 (53) hide show
  1. package/README.md +21 -1
  2. package/dist/errors.d.ts +5 -1
  3. package/dist/errors.js +2 -0
  4. package/dist/host/host.d.ts +2 -0
  5. package/dist/host/host.js +10 -0
  6. package/dist/host/types.d.ts +3 -0
  7. package/dist/index.d.ts +3 -1
  8. package/dist/index.js +3 -1
  9. package/dist/ipc/host.js +1 -0
  10. package/dist/ipc/service.js +3 -0
  11. package/dist/package/runtime.d.ts +3 -0
  12. package/dist/package/runtime.js +8 -0
  13. package/dist/package/types.d.ts +4 -0
  14. package/dist/platform/node.js +1 -0
  15. package/dist/platform/types.d.ts +3 -1
  16. package/dist/profile/client-runs.d.ts +10 -1
  17. package/dist/profile/client-runs.js +14 -1
  18. package/dist/profile/client.d.ts +1 -1
  19. package/dist/profile/client.js +1 -1
  20. package/dist/project/init.js +2 -2
  21. package/dist/project/types.d.ts +5 -0
  22. package/dist/server/index.d.ts +1 -0
  23. package/dist/server/index.js +1 -0
  24. package/dist/server/policy.d.ts +138 -0
  25. package/dist/server/policy.js +368 -0
  26. package/dist/server/routes/actions.js +86 -42
  27. package/dist/server/routes/common.d.ts +14 -25
  28. package/dist/server/routes/common.js +25 -21
  29. package/dist/server/routes/config.d.ts +1 -1
  30. package/dist/server/routes/config.js +8 -8
  31. package/dist/server/routes/doctor.js +4 -4
  32. package/dist/server/routes/health.js +2 -2
  33. package/dist/server/routes/info.js +6 -12
  34. package/dist/server/routes/playbooks.js +8 -8
  35. package/dist/server/routes/runs.js +111 -30
  36. package/dist/server/routes/state.d.ts +1 -1
  37. package/dist/server/routes/state.js +7 -7
  38. package/dist/server/security.d.ts +11 -0
  39. package/dist/server/security.js +36 -19
  40. package/dist/server/server.js +215 -30
  41. package/dist/server/types.d.ts +60 -2
  42. package/dist/service/local.js +3 -0
  43. package/dist/service/remote.js +16 -1
  44. package/dist/service/types.d.ts +4 -0
  45. package/dist/storage/index.d.ts +1 -0
  46. package/dist/storage/index.js +6 -1
  47. package/dist/storage/sqlite.d.ts +18 -1
  48. package/dist/storage/sqlite.js +159 -2
  49. package/dist/storage/types.d.ts +29 -0
  50. package/dist/storage/types.js +12 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/package.json +2 -2
@@ -1,4 +1,7 @@
1
1
  import { assertValidPackageId } from "../../utils/index.js";
2
+ import { ActionDockError, PACKAGE_NOT_FOUND } from "../../errors.js";
3
+ import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isManagementAllowedByPolicy, isPackageAllowedByPolicy, PackageNotAllowedError, } from "../policy.js";
4
+ export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isPackageAllowedByPolicy, isManagementAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
2
5
  /**
3
6
  * 构造带 CORS 头的标准 JSON HTTP 响应。
4
7
  */
@@ -20,36 +23,37 @@ export function getSubPath(pathname) {
20
23
  }
21
24
  return pathname;
22
25
  }
23
- import { ActionDockError, PACKAGE_NOT_FOUND, PACKAGE_NOT_ALLOWED } from "../../errors.js";
24
26
  /**
25
- * 包未列入白名单拒绝访问异常。
27
+ * 校验目标 action 是否属于允许动作白名单。
28
+ * 当 options 或 policy 中的 actionAllowlist 存在且目标 action 不在白名单中时抛出 403 ACTION_FORBIDDEN 异常。
26
29
  */
27
- export class PackageNotAllowedError extends ActionDockError {
28
- status = 403;
29
- statusCode = 403;
30
- constructor(packageId) {
31
- super(PACKAGE_NOT_ALLOWED, `Package '${packageId}' is not in the allowed package list`);
32
- this.name = "PackageNotAllowedError";
33
- Object.setPrototypeOf(this, PackageNotAllowedError.prototype);
34
- }
30
+ export function assertActionAllowed(action, options) {
31
+ const policy = options
32
+ ? {
33
+ packageAllowlist: options.packageAllowlist,
34
+ actionAllowlist: options.actionAllowlist,
35
+ }
36
+ : undefined;
37
+ assertActionAllowedByPolicy(action, policy);
35
38
  }
36
39
  /**
37
40
  * 校验目标 packageId 是否属于允许包白名单。
38
- * 当 options.packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
41
+ * 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
39
42
  */
40
43
  export function assertPackageAllowed(packageId, options) {
41
- if (options?.packageAllowlist &&
42
- Array.isArray(options.packageAllowlist) &&
43
- options.packageAllowlist.length > 0 &&
44
- !options.packageAllowlist.includes(packageId)) {
45
- throw new PackageNotAllowedError(packageId);
46
- }
44
+ const policy = options
45
+ ? {
46
+ packageAllowlist: options.packageAllowlist,
47
+ }
48
+ : undefined;
49
+ assertPackageAllowedByPolicy(packageId, policy);
47
50
  }
48
51
  /**
49
52
  * 解析并确定生效的目标 packageId。
50
53
  * 遵循严格的解析规则与白名单检查。
51
54
  */
52
55
  export function resolveTargetPackageId(packages, requestedPackageId, options) {
56
+ const allowlist = options?.packageAllowlist;
53
57
  if (requestedPackageId) {
54
58
  if (requestedPackageId === "global") {
55
59
  return "global";
@@ -63,10 +67,10 @@ export function resolveTargetPackageId(packages, requestedPackageId, options) {
63
67
  return matched.id;
64
68
  }
65
69
  let targetId;
66
- if (options?.packageAllowlist &&
67
- Array.isArray(options.packageAllowlist) &&
68
- options.packageAllowlist.length > 0) {
69
- const allowed = packages.find((p) => options.packageAllowlist.includes(p.id));
70
+ if (allowlist &&
71
+ Array.isArray(allowlist) &&
72
+ allowlist.length > 0) {
73
+ const allowed = packages.find((p) => allowlist.includes(p.id));
70
74
  targetId = allowed ? allowed.id : (packages[0]?.id || "");
71
75
  }
72
76
  else if (packages.length > 0) {
@@ -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 handleConfigRoutes(ctx: RouteContext): Promise<Response | null>;
@@ -2,7 +2,7 @@ import { CAPABILITY_UNAVAILABLE, CONFIG_DELETE_ERROR, CONFIG_ENV_ERROR, CONFIG_L
2
2
  import { resolveEnvValue } from "../../runtime/index.js";
3
3
  import { isSecretConfigKey, sanitizeConfigDefinitions } from "../../storage/index.js";
4
4
  import { readJsonBody } from "../body.js";
5
- import { getSubPath, jsonResponse, resolveTargetPackageId } from "./common.js";
5
+ import { getSubPath, isManagementAllowedByPolicy, jsonResponse, resolveTargetPackageId, } from "./common.js";
6
6
  function isClientConfigError(err) {
7
7
  return (err?.code === PACKAGE_NOT_FOUND ||
8
8
  err?.code === INVALID_PACKAGE_ID ||
@@ -13,10 +13,10 @@ function isClientConfigError(err) {
13
13
  }
14
14
  /**
15
15
  * 处理配置元数据与当前值读取、更新及删除接口。
16
- * 在未显式开启管理功能 (options.enableManagement !== true) 时返回 403 拒绝。
16
+ * 在未显式开启管理功能时返回 403 拒绝。
17
17
  */
18
18
  export async function handleConfigRoutes(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 isConfigRoute = subpath === "/config" ||
22
22
  subpath.startsWith("/config/") ||
@@ -25,7 +25,7 @@ export async function handleConfigRoutes(ctx) {
25
25
  return null;
26
26
  }
27
27
  // 校验管理能力开关
28
- if (options.enableManagement !== true || !service.management?.config) {
28
+ if (!isManagementAllowedByPolicy(policy) || !service.management?.config) {
29
29
  return jsonResponse({
30
30
  ok: false,
31
31
  error: {
@@ -39,7 +39,7 @@ export async function handleConfigRoutes(ctx) {
39
39
  try {
40
40
  const pkgs = await service.discovery.listPackages();
41
41
  const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
42
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
42
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
43
43
  const pkg = pkgs.find((p) => p.id === targetPackageId);
44
44
  const declared = pkg?.config || {};
45
45
  const envChecks = [];
@@ -76,7 +76,7 @@ export async function handleConfigRoutes(ctx) {
76
76
  try {
77
77
  const pkgs = await service.discovery.listPackages();
78
78
  const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
79
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
79
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
80
80
  const pkg = pkgs.find((p) => p.id === targetPackageId);
81
81
  const rawDeclared = pkg?.config || {};
82
82
  const declared = sanitizeConfigDefinitions(rawDeclared) || {};
@@ -112,7 +112,7 @@ export async function handleConfigRoutes(ctx) {
112
112
  const body = await readJsonBody(req, { maxBytes: options.maxBodyBytes });
113
113
  const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || body.package || undefined;
114
114
  const pkgs = await service.discovery.listPackages();
115
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
115
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
116
116
  const key = body.key;
117
117
  if (!key) {
118
118
  return jsonResponse({ ok: false, error: { code: INVALID_ARGUMENT, message: "Config 'key' is required" } }, 400, corsHeaders);
@@ -141,7 +141,7 @@ export async function handleConfigRoutes(ctx) {
141
141
  const key = decodeURIComponent(configKeyMatch[1]);
142
142
  const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
143
143
  const pkgs = await service.discovery.listPackages();
144
- const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, options);
144
+ const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
145
145
  const deleted = await service.management.config.delete(targetPackageId, key);
146
146
  return jsonResponse({ ok: true, packageId: targetPackageId, key, deleted }, 200, corsHeaders);
147
147
  }
@@ -5,14 +5,14 @@ import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
5
5
  * 处理环境与依赖诊断接口(GET /api/v2/doctor 与 /doctor)。
6
6
  */
7
7
  export async function handleDoctorRoute(ctx) {
8
- const { req, url, pathname, corsHeaders, projectRoot, customHome, service, options } = ctx;
8
+ const { req, url, pathname, corsHeaders, projectRoot, customHome, service, options, activePolicy: policy } = ctx;
9
9
  const subpath = getSubPath(pathname);
10
10
  if (subpath !== "/doctor" || req.method !== "GET") {
11
11
  return null;
12
12
  }
13
13
  try {
14
14
  const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
15
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
15
+ if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
16
16
  if (!targetPkg) {
17
17
  return jsonResponse({
18
18
  ok: false,
@@ -24,7 +24,7 @@ export async function handleDoctorRoute(ctx) {
24
24
  }
25
25
  }
26
26
  if (targetPkg) {
27
- assertPackageAllowed(targetPkg, options);
27
+ assertPackageAllowed(targetPkg, policy);
28
28
  }
29
29
  let packageRoot;
30
30
  if (targetPkg) {
@@ -36,7 +36,7 @@ export async function handleDoctorRoute(ctx) {
36
36
  cwd: packageRoot || projectRoot || process.cwd(),
37
37
  packageIdOrPath: packageRoot || targetPkg,
38
38
  customHome,
39
- packageAllowlist: options.packageAllowlist,
39
+ packageAllowlist: policy.packageAllowlist,
40
40
  });
41
41
  return jsonResponse({ ok: true, report }, 200, corsHeaders);
42
42
  }
@@ -6,12 +6,12 @@ import { jsonResponse } from "./common.js";
6
6
  * 处理健康检查与就绪状态接口(支持 /api/v2/health 与 /health)。
7
7
  */
8
8
  export async function handleHealthRoute(ctx) {
9
- const { req, pathname, corsHeaders, options, projectRoot } = ctx;
9
+ const { req, pathname, corsHeaders, options, projectRoot, activePolicy: policy } = ctx;
10
10
  if (pathname !== "/api/v2/health" &&
11
11
  pathname !== "/health") {
12
12
  return null;
13
13
  }
14
- if (!verifyBearerToken(req, options.token, options)) {
14
+ if (!verifyBearerToken(req, policy.token, options)) {
15
15
  return jsonResponse({
16
16
  ok: false,
17
17
  error: {
@@ -2,7 +2,7 @@ import { filterWithFallbackInfo } from "../../filter/index.js";
2
2
  import { INFO_ERROR, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND, PACKAGES_INFO_ERROR } from "../../errors.js";
3
3
  import { ACTIONDOCK_VERSION } from "../../version.js";
4
4
  import { sanitizeConfigDefinitions } from "../../storage/index.js";
5
- import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
5
+ import { assertPackageAllowed, filterPackagesByPolicy, getSubPath, jsonResponse } from "./common.js";
6
6
  function sanitizePackage(pkg, exposeDebugInfo) {
7
7
  if (!pkg)
8
8
  return pkg;
@@ -22,20 +22,17 @@ function sanitizePackage(pkg, exposeDebugInfo) {
22
22
  * - GET /api/v2/packages 与 GET /packages -> 委托 service.discovery.listPackages()
23
23
  */
24
24
  export async function handleInfoRoute(ctx) {
25
- const { req, url, pathname, corsHeaders, options, service, projectRoot } = ctx;
25
+ const { req, url, pathname, corsHeaders, options, service, projectRoot, activePolicy: policy } = ctx;
26
26
  const subpath = getSubPath(pathname);
27
27
  // 1. Packages List: GET /api/v2/packages, /packages
28
28
  if ((subpath === "/packages" || pathname === "/api/v2/packages" || pathname === "/packages") && req.method === "GET") {
29
29
  try {
30
30
  const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
31
31
  if (targetPkg) {
32
- assertPackageAllowed(targetPkg, options);
32
+ assertPackageAllowed(targetPkg, policy);
33
33
  }
34
34
  const rawPackages = await service.discovery.listPackages();
35
- let packages = rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo));
36
- if (options.packageAllowlist && Array.isArray(options.packageAllowlist) && options.packageAllowlist.length > 0) {
37
- packages = packages.filter((p) => p.id && options.packageAllowlist.includes(p.id));
38
- }
35
+ const packages = filterPackagesByPolicy(rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo)), policy);
39
36
  return jsonResponse({
40
37
  ok: true,
41
38
  packages,
@@ -64,13 +61,10 @@ export async function handleInfoRoute(ctx) {
64
61
  const intent = url.searchParams.get("intent") || undefined;
65
62
  const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
66
63
  if (targetPkg) {
67
- assertPackageAllowed(targetPkg, options);
64
+ assertPackageAllowed(targetPkg, policy);
68
65
  }
69
66
  const rawPackages = await service.info();
70
- let packages = rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo));
71
- if (options.packageAllowlist && Array.isArray(options.packageAllowlist) && options.packageAllowlist.length > 0) {
72
- packages = packages.filter((p) => p.id && options.packageAllowlist.includes(p.id));
73
- }
67
+ const packages = filterPackagesByPolicy(rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo)), policy);
74
68
  if (isTree) {
75
69
  return jsonResponse({ ok: true, type: "tree", packages }, 200, corsHeaders);
76
70
  }
@@ -1,11 +1,11 @@
1
1
  import { filterByIntent } from "../../filter/index.js";
2
2
  import { PACKAGE_NOT_ALLOWED, PLAYBOOK_NOT_FOUND, PLAYBOOKS_LIST_ERROR } from "../../errors.js";
3
- import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
3
+ import { assertPackageAllowed, getSubPath, isPackageAllowedByPolicy, jsonResponse } from "./common.js";
4
4
  /**
5
5
  * 处理 Playbook 规程相关的 HTTP 路由(列表与 SOP 详情查询)。
6
6
  */
7
7
  export async function handlePlaybooksRoutes(ctx) {
8
- const { req, url, pathname, corsHeaders, service, options } = ctx;
8
+ const { req, url, pathname, corsHeaders, service, options, activePolicy: policy } = ctx;
9
9
  const subpath = getSubPath(pathname);
10
10
  // 1. Playbooks List: GET /api/v2/playbooks, GET /playbooks
11
11
  if (subpath === "/playbooks" && req.method === "GET") {
@@ -13,14 +13,14 @@ export async function handlePlaybooksRoutes(ctx) {
13
13
  const intent = url.searchParams.get("intent") || undefined;
14
14
  const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
15
15
  if (targetPkg) {
16
- assertPackageAllowed(targetPkg, options);
16
+ assertPackageAllowed(targetPkg, policy);
17
17
  }
18
18
  let pbs = await service.discovery.listPlaybooks({ intent, package: targetPkg });
19
19
  if (targetPkg) {
20
20
  pbs = pbs.filter((p) => p.packageId === targetPkg);
21
21
  }
22
- if (options.packageAllowlist && Array.isArray(options.packageAllowlist) && options.packageAllowlist.length > 0) {
23
- pbs = pbs.filter((p) => p.packageId && options.packageAllowlist.includes(p.packageId));
22
+ if (policy.packageAllowlist && Array.isArray(policy.packageAllowlist) && policy.packageAllowlist.length > 0) {
23
+ pbs = pbs.filter((p) => isPackageAllowedByPolicy(p.packageId, policy));
24
24
  }
25
25
  if (intent) {
26
26
  pbs = filterByIntent(pbs, intent, [(p) => p.id, (p) => p.description || "", (p) => p.packageId || "", (p) => (p.actions || []).join(" ")], false);
@@ -46,7 +46,7 @@ export async function handlePlaybooksRoutes(ctx) {
46
46
  const packageId = decodeURIComponent(pkgPlaybookMatch[1]);
47
47
  const playbookId = decodeURIComponent(pkgPlaybookMatch[2]);
48
48
  try {
49
- assertPackageAllowed(packageId, options);
49
+ assertPackageAllowed(packageId, policy);
50
50
  }
51
51
  catch (err) {
52
52
  return jsonResponse({
@@ -79,7 +79,7 @@ export async function handlePlaybooksRoutes(ctx) {
79
79
  if (playbookId.includes("/")) {
80
80
  const pkgFromRef = playbookId.slice(0, playbookId.lastIndexOf("/"));
81
81
  try {
82
- assertPackageAllowed(pkgFromRef, options);
82
+ assertPackageAllowed(pkgFromRef, policy);
83
83
  }
84
84
  catch (err) {
85
85
  return jsonResponse({
@@ -101,7 +101,7 @@ export async function handlePlaybooksRoutes(ctx) {
101
101
  pkgId = matched.packageId;
102
102
  }
103
103
  }
104
- assertPackageAllowed(pkgId, options);
104
+ assertPackageAllowed(pkgId, policy);
105
105
  return jsonResponse(pb, 200, corsHeaders);
106
106
  }
107
107
  catch (err) {
@@ -1,12 +1,13 @@
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
+ import { parseDuration } from "../../utils/index.js";
3
4
  import { readJsonBody } from "../body.js";
4
- import { getSubPath, jsonResponse } from "./common.js";
5
+ import { getSubPath, isActionAllowedByPolicy, isPackageAllowedByPolicy, jsonResponse } from "./common.js";
5
6
  /**
6
7
  * 处理历史运行记录查询、清理、详情及 SSE 流式日志接口。
7
8
  */
8
9
  export async function handleRunsRoutes(ctx) {
9
- const { req, url, pathname, corsHeaders, options, service } = ctx;
10
+ const { req, url, pathname, corsHeaders, options, service, activePolicy: policy } = ctx;
10
11
  const subpath = getSubPath(pathname);
11
12
  // 1. Runs List: GET /api/v2/runs, GET /runs
12
13
  if (subpath === "/runs" && req.method === "GET") {
@@ -20,10 +21,7 @@ export async function handleRunsRoutes(ctx) {
20
21
  const limit = Number.isFinite(parsedLimit) && parsedLimit > 0
21
22
  ? Math.min(parsedLimit, 500)
22
23
  : 50;
23
- if (packageId &&
24
- options.packageAllowlist &&
25
- options.packageAllowlist.length > 0 &&
26
- !options.packageAllowlist.includes(packageId)) {
24
+ if (packageId && !isPackageAllowedByPolicy(packageId, policy)) {
27
25
  return jsonResponse({
28
26
  ok: false,
29
27
  error: {
@@ -32,6 +30,15 @@ export async function handleRunsRoutes(ctx) {
32
30
  },
33
31
  }, 403, corsHeaders);
34
32
  }
33
+ if (actionId && !isActionAllowedByPolicy({ packageId, actionId }, policy)) {
34
+ return jsonResponse({
35
+ ok: false,
36
+ error: {
37
+ code: ACTION_FORBIDDEN,
38
+ message: `Action '${packageId ? `${packageId}/${actionId}` : actionId}' is not in the allowed action list`,
39
+ },
40
+ }, 403, corsHeaders);
41
+ }
35
42
  let allRuns = await service.runs.list({
36
43
  actionId,
37
44
  status,
@@ -39,8 +46,11 @@ export async function handleRunsRoutes(ctx) {
39
46
  intent,
40
47
  limit,
41
48
  });
42
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
43
- allRuns = allRuns.filter((r) => !r.packageId || options.packageAllowlist.includes(r.packageId));
49
+ if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
50
+ allRuns = allRuns.filter((r) => isPackageAllowedByPolicy(r.packageId, policy));
51
+ }
52
+ if (policy.actionAllowlist && policy.actionAllowlist.length > 0) {
53
+ allRuns = allRuns.filter((r) => isActionAllowedByPolicy({ packageId: r.packageId, actionId: r.actionId }, policy));
44
54
  }
45
55
  const sliced = allRuns.slice(0, limit);
46
56
  return jsonResponse({ ok: true, total: allRuns.length, items: sliced }, 200, corsHeaders);
@@ -60,10 +70,32 @@ export async function handleRunsRoutes(ctx) {
60
70
  const packageId = url.searchParams.get("packageId") || body.packageId || undefined;
61
71
  const actionId = url.searchParams.get("actionId") || body.actionId || undefined;
62
72
  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)) {
73
+ const rawOlderThan = url.searchParams.get("olderThan") ||
74
+ url.searchParams.get("olderThanMs") ||
75
+ body.olderThan ||
76
+ body.olderThanMs;
77
+ let olderThanMs;
78
+ if (typeof rawOlderThan === "number" && rawOlderThan > 0) {
79
+ olderThanMs = rawOlderThan;
80
+ }
81
+ else if (typeof rawOlderThan === "string" && rawOlderThan.trim()) {
82
+ try {
83
+ olderThanMs = parseDuration(rawOlderThan);
84
+ }
85
+ catch { }
86
+ }
87
+ const rawKeep = url.searchParams.get("keep") || body.keep;
88
+ let keep;
89
+ if (typeof rawKeep === "number" && rawKeep >= 0) {
90
+ keep = rawKeep;
91
+ }
92
+ else if (typeof rawKeep === "string" && rawKeep.trim()) {
93
+ const parsed = parseInt(rawKeep.trim(), 10);
94
+ if (!isNaN(parsed) && parsed >= 0) {
95
+ keep = parsed;
96
+ }
97
+ }
98
+ if (packageId && !isPackageAllowedByPolicy(packageId, policy)) {
67
99
  return jsonResponse({
68
100
  ok: false,
69
101
  error: {
@@ -74,7 +106,13 @@ export async function handleRunsRoutes(ctx) {
74
106
  }
75
107
  let clearedCount = 0;
76
108
  if (service.runs.clear) {
77
- clearedCount = await service.runs.clear({ packageId, actionId, status });
109
+ clearedCount = await service.runs.clear({
110
+ packageId,
111
+ actionId,
112
+ status,
113
+ olderThanMs,
114
+ keep,
115
+ });
78
116
  }
79
117
  return jsonResponse({ ok: true, clearedCount }, 200, corsHeaders);
80
118
  }
@@ -82,6 +120,23 @@ export async function handleRunsRoutes(ctx) {
82
120
  return jsonResponse({ ok: false, error: { code: RUNS_CLEAR_ERROR, message: err.message } }, 500, corsHeaders);
83
121
  }
84
122
  }
123
+ // 2.1 Runs Clean Expired: POST /api/v2/runs/clean-expired, POST /runs/clean-expired
124
+ if (subpath === "/runs/clean-expired" && req.method === "POST") {
125
+ try {
126
+ let body = {};
127
+ if (req.headers.get("content-type")?.includes("json")) {
128
+ body = await readJsonBody(req, { maxBytes: options.maxBodyBytes }).catch(() => ({}));
129
+ }
130
+ let cleanedCount = 0;
131
+ if (service.runs.cleanExpired) {
132
+ cleanedCount = await service.runs.cleanExpired(body);
133
+ }
134
+ return jsonResponse({ ok: true, cleanedCount }, 200, corsHeaders);
135
+ }
136
+ catch (err) {
137
+ return jsonResponse({ ok: false, error: { code: RUNS_CLEAR_ERROR, message: err.message } }, 500, corsHeaders);
138
+ }
139
+ }
85
140
  // 3. Run Events SSE: GET /api/v2/runs/:runId/events (and stream alias)
86
141
  const runEventsMatch = subpath.match(/^\/runs\/([^/]+)\/(events|stream)$/);
87
142
  if (runEventsMatch && req.method === "GET") {
@@ -90,14 +145,21 @@ export async function handleRunsRoutes(ctx) {
90
145
  if (!run) {
91
146
  return jsonResponse({ ok: false, error: { code: RUN_NOT_FOUND, message: `Run '${runId}' not found` } }, 404, corsHeaders);
92
147
  }
93
- if (options.packageAllowlist &&
94
- options.packageAllowlist.length > 0 &&
95
- (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
148
+ if (!isPackageAllowedByPolicy(run.packageId, policy)) {
96
149
  return jsonResponse({
97
150
  ok: false,
98
151
  error: {
99
152
  code: PACKAGE_FORBIDDEN,
100
- message: `Package '${run.packageId}' is not in the allowed package list`,
153
+ message: `Package '${run.packageId || "unknown"}' is not in the allowed package list`,
154
+ },
155
+ }, 403, corsHeaders);
156
+ }
157
+ if (!isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy)) {
158
+ return jsonResponse({
159
+ ok: false,
160
+ error: {
161
+ code: ACTION_FORBIDDEN,
162
+ message: `Action '${run.packageId ? `${run.packageId}/${run.actionId}` : run.actionId}' is not in the allowed action list`,
101
163
  },
102
164
  }, 403, corsHeaders);
103
165
  }
@@ -258,9 +320,7 @@ export async function handleRunsRoutes(ctx) {
258
320
  },
259
321
  }, 404, corsHeaders);
260
322
  }
261
- if (options.packageAllowlist &&
262
- options.packageAllowlist.length > 0 &&
263
- (!run.packageId || !options.packageAllowlist.includes(run.packageId))) {
323
+ if (!isPackageAllowedByPolicy(run.packageId, policy)) {
264
324
  return jsonResponse({
265
325
  ok: false,
266
326
  error: {
@@ -269,22 +329,43 @@ export async function handleRunsRoutes(ctx) {
269
329
  },
270
330
  }, 403, corsHeaders);
271
331
  }
332
+ if (!isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy)) {
333
+ return jsonResponse({
334
+ ok: false,
335
+ error: {
336
+ code: ACTION_FORBIDDEN,
337
+ message: `Action '${run.packageId ? `${run.packageId}/${run.actionId}` : run.actionId}' is not in the allowed action list`,
338
+ },
339
+ }, 403, corsHeaders);
340
+ }
272
341
  return jsonResponse(run, 200, corsHeaders);
273
342
  }
274
343
  // 5. Run Cancel: POST /api/v2/runs/:runId/cancel, POST /runs/:runId/cancel
275
344
  const runCancelMatch = subpath.match(/^\/runs\/([^/]+)\/cancel$/);
276
345
  if (runCancelMatch && req.method === "POST") {
277
346
  const runId = decodeURIComponent(runCancelMatch[1]);
278
- if (options.packageAllowlist && options.packageAllowlist.length > 0) {
347
+ if ((policy.packageAllowlist && policy.packageAllowlist.length > 0) ||
348
+ (policy.actionAllowlist && policy.actionAllowlist.length > 0)) {
279
349
  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);
350
+ if (run) {
351
+ if (!isPackageAllowedByPolicy(run.packageId, policy)) {
352
+ return jsonResponse({
353
+ ok: false,
354
+ error: {
355
+ code: PACKAGE_FORBIDDEN,
356
+ message: `Package '${run.packageId || "unknown"}' is not in the allowed package list`,
357
+ },
358
+ }, 403, corsHeaders);
359
+ }
360
+ if (!isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy)) {
361
+ return jsonResponse({
362
+ ok: false,
363
+ error: {
364
+ code: ACTION_FORBIDDEN,
365
+ message: `Action '${run.packageId ? `${run.packageId}/${run.actionId}` : run.actionId}' is not in the allowed action list`,
366
+ },
367
+ }, 403, corsHeaders);
368
+ }
288
369
  }
289
370
  }
290
371
  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
  *