@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.
- package/README.md +21 -1
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +2 -0
- package/dist/host/host.d.ts +2 -0
- package/dist/host/host.js +10 -0
- package/dist/host/types.d.ts +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/ipc/host.js +1 -0
- package/dist/ipc/service.js +3 -0
- package/dist/package/runtime.d.ts +3 -0
- package/dist/package/runtime.js +8 -0
- package/dist/package/types.d.ts +4 -0
- package/dist/platform/node.js +1 -0
- package/dist/platform/types.d.ts +3 -1
- package/dist/profile/client-runs.d.ts +10 -1
- package/dist/profile/client-runs.js +14 -1
- package/dist/profile/client.d.ts +1 -1
- package/dist/profile/client.js +1 -1
- package/dist/project/init.js +2 -2
- package/dist/project/types.d.ts +5 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/policy.d.ts +138 -0
- package/dist/server/policy.js +368 -0
- package/dist/server/routes/actions.js +86 -42
- package/dist/server/routes/common.d.ts +14 -25
- package/dist/server/routes/common.js +25 -21
- package/dist/server/routes/config.d.ts +1 -1
- package/dist/server/routes/config.js +8 -8
- package/dist/server/routes/doctor.js +4 -4
- package/dist/server/routes/health.js +2 -2
- package/dist/server/routes/info.js +6 -12
- package/dist/server/routes/playbooks.js +8 -8
- package/dist/server/routes/runs.js +111 -30
- package/dist/server/routes/state.d.ts +1 -1
- package/dist/server/routes/state.js +7 -7
- package/dist/server/security.d.ts +11 -0
- package/dist/server/security.js +36 -19
- package/dist/server/server.js +215 -30
- package/dist/server/types.d.ts +60 -2
- package/dist/service/local.js +3 -0
- package/dist/service/remote.js +16 -1
- package/dist/service/types.d.ts +4 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +6 -1
- package/dist/storage/sqlite.d.ts +18 -1
- package/dist/storage/sqlite.js +159 -2
- package/dist/storage/types.d.ts +29 -0
- package/dist/storage/types.js +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- 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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
41
|
+
* 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
|
|
39
42
|
*/
|
|
40
43
|
export function assertPackageAllowed(packageId, options) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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 (
|
|
67
|
-
Array.isArray(
|
|
68
|
-
|
|
69
|
-
const allowed = packages.find((p) =>
|
|
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) {
|
|
@@ -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
|
-
*
|
|
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 (
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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 (
|
|
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,
|
|
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:
|
|
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,
|
|
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,
|
|
32
|
+
assertPackageAllowed(targetPkg, policy);
|
|
33
33
|
}
|
|
34
34
|
const rawPackages = await service.discovery.listPackages();
|
|
35
|
-
|
|
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,
|
|
64
|
+
assertPackageAllowed(targetPkg, policy);
|
|
68
65
|
}
|
|
69
66
|
const rawPackages = await service.info();
|
|
70
|
-
|
|
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,
|
|
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 (
|
|
23
|
-
pbs = pbs.filter((p) => 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,
|
|
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,
|
|
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,
|
|
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 (
|
|
43
|
-
allRuns = allRuns.filter((r) =>
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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({
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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,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
|
-
*
|
|
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 (
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
*
|