@actiondock/core 2.19.0 → 2.20.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.
- package/dist/client/config.d.ts +17 -14
- package/dist/client/config.js +112 -14
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +2 -0
- package/dist/execution/service.js +8 -5
- package/dist/execution/types.d.ts +3 -3
- package/dist/host/host.d.ts +9 -1
- package/dist/host/host.js +183 -38
- package/dist/host/types.d.ts +3 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +1 -1
- package/dist/input/config-override.d.ts +5 -0
- package/dist/input/config-override.js +23 -0
- package/dist/input/input-resolver.d.ts +24 -7
- package/dist/input/input-resolver.js +73 -5
- package/dist/invocation/bindings.d.ts +53 -0
- package/dist/invocation/bindings.js +248 -0
- package/dist/invocation/policy.d.ts +5 -1
- package/dist/invocation/policy.js +16 -9
- package/dist/invocation/remote.d.ts +40 -0
- package/dist/invocation/remote.js +489 -0
- package/dist/invocation/types.d.ts +2 -0
- package/dist/invocation/types.js +1 -0
- package/dist/package/index.d.ts +1 -1
- package/dist/package/runtime.js +31 -12
- package/dist/platform/types.d.ts +48 -1
- package/dist/project/config-declarations.d.ts +61 -0
- package/dist/project/config-declarations.js +297 -0
- package/dist/project/index.d.ts +5 -2
- package/dist/project/index.js +5 -2
- package/dist/project/init.js +2 -2
- package/dist/project/loader.js +4 -0
- package/dist/project/manifest.js +10 -0
- package/dist/project/types.d.ts +30 -0
- package/dist/runtime/context.d.ts +6 -3
- package/dist/runtime/context.js +60 -13
- package/dist/runtime/env.js +15 -1
- package/dist/runtime/standalone-commands.d.ts +2 -2
- package/dist/runtime/standalone-commands.js +113 -47
- package/dist/runtime/standalone-help.d.ts +1 -0
- package/dist/runtime/standalone-help.js +15 -9
- package/dist/runtime/standalone.js +2 -2
- package/dist/server/policy.d.ts +24 -107
- package/dist/server/policy.js +134 -291
- package/dist/server/routes/actions.js +10 -8
- package/dist/server/routes/common.d.ts +5 -20
- package/dist/server/routes/common.js +22 -58
- package/dist/server/routes/config.js +65 -7
- package/dist/server/routes/doctor.d.ts +1 -0
- package/dist/server/routes/doctor.js +4 -3
- package/dist/server/routes/info.js +100 -13
- package/dist/server/routes/playbooks.js +1 -3
- package/dist/server/routes/runs.js +96 -18
- package/dist/server/server.js +25 -53
- package/dist/server/types.d.ts +6 -6
- package/dist/service/factory.js +1 -0
- package/dist/service/remote.js +14 -29
- package/dist/service/types.d.ts +20 -5
- package/dist/service/types.js +8 -0
- package/dist/storage/mask.d.ts +33 -6
- package/dist/storage/mask.js +298 -16
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,84 +1,48 @@
|
|
|
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, };
|
|
5
|
-
/**
|
|
6
|
-
* 构造带 CORS 头的标准 JSON HTTP 响应。
|
|
7
|
-
*/
|
|
2
|
+
import { ActionDockError, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND } from "../../errors.js";
|
|
3
|
+
import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isFullAccessGranted, isManagementAllowedByPolicy, isPackageAllowedByPolicy, isPackageVisibleByPolicy, PackageNotAllowedError, requiresMinimalPackageProjection, } from "../policy.js";
|
|
4
|
+
export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isFullAccessGranted, isPackageAllowedByPolicy, isPackageVisibleByPolicy, isManagementAllowedByPolicy, requiresMinimalPackageProjection, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
|
|
8
5
|
export function jsonResponse(data, status = 200, corsHeaders = {}) {
|
|
9
6
|
return new Response(JSON.stringify(data, null, 2), {
|
|
10
7
|
status,
|
|
11
|
-
headers: {
|
|
12
|
-
"Content-Type": "application/json",
|
|
13
|
-
...corsHeaders,
|
|
14
|
-
},
|
|
8
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
15
9
|
});
|
|
16
10
|
}
|
|
17
|
-
/**
|
|
18
|
-
* 剥离标准版本前缀 (/api/v2/),获取相对路由子路径。
|
|
19
|
-
*/
|
|
20
11
|
export function getSubPath(pathname) {
|
|
21
|
-
|
|
22
|
-
return pathname.slice(7);
|
|
23
|
-
}
|
|
24
|
-
return pathname;
|
|
12
|
+
return pathname.startsWith("/api/v2/") ? pathname.slice(7) : pathname;
|
|
25
13
|
}
|
|
26
|
-
/**
|
|
27
|
-
* 校验目标 action 是否属于允许动作白名单。
|
|
28
|
-
* 当 options 或 policy 中的 actionAllowlist 存在且目标 action 不在白名单中时抛出 403 ACTION_FORBIDDEN 异常。
|
|
29
|
-
*/
|
|
30
14
|
export function assertActionAllowed(action, options) {
|
|
31
|
-
|
|
32
|
-
? {
|
|
33
|
-
packageAllowlist: options.packageAllowlist,
|
|
34
|
-
actionAllowlist: options.actionAllowlist,
|
|
35
|
-
}
|
|
36
|
-
: undefined;
|
|
37
|
-
assertActionAllowedByPolicy(action, policy);
|
|
15
|
+
assertActionAllowedByPolicy(action, options);
|
|
38
16
|
}
|
|
39
|
-
/**
|
|
40
|
-
* 校验目标 packageId 是否属于允许包白名单。
|
|
41
|
-
* 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
|
|
42
|
-
*/
|
|
43
17
|
export function assertPackageAllowed(packageId, options) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
18
|
+
assertPackageAllowedByPolicy(packageId, options);
|
|
19
|
+
}
|
|
20
|
+
/** 包发现可见性与整包能力分开判断,不能因单动作授权开放包级管理。 */
|
|
21
|
+
export function assertPackageVisible(packageId, options) {
|
|
22
|
+
if (!isPackageVisibleByPolicy(packageId, options))
|
|
23
|
+
throw new PackageNotAllowedError(packageId);
|
|
50
24
|
}
|
|
51
|
-
/**
|
|
52
|
-
* 解析并确定生效的目标 packageId。
|
|
53
|
-
* 遵循严格的解析规则与白名单检查。
|
|
54
|
-
*/
|
|
25
|
+
/** 管理接口只能选取整包授权目标;全局管理必须显式拥有全部授权。 */
|
|
55
26
|
export function resolveTargetPackageId(packages, requestedPackageId, options) {
|
|
56
|
-
const allowlist = options?.packageAllowlist;
|
|
57
27
|
if (requestedPackageId) {
|
|
58
28
|
if (requestedPackageId === "global") {
|
|
29
|
+
if (options && !isFullAccessGranted(options)) {
|
|
30
|
+
throw new ActionDockError(PACKAGE_NOT_ALLOWED, "Global management requires explicit full package authorization");
|
|
31
|
+
}
|
|
59
32
|
return "global";
|
|
60
33
|
}
|
|
61
34
|
assertValidPackageId(requestedPackageId);
|
|
62
35
|
assertPackageAllowed(requestedPackageId, options);
|
|
63
|
-
const matched = packages.find((
|
|
36
|
+
const matched = packages.find((pkg) => pkg.id === requestedPackageId || pkg.packageRoot === requestedPackageId);
|
|
64
37
|
if (!matched) {
|
|
65
38
|
throw new ActionDockError(PACKAGE_NOT_FOUND, `Unknown or unregistered package: '${requestedPackageId}'`);
|
|
66
39
|
}
|
|
67
40
|
return matched.id;
|
|
68
41
|
}
|
|
69
|
-
|
|
70
|
-
if (allowlist &&
|
|
71
|
-
Array.isArray(allowlist) &&
|
|
72
|
-
allowlist.length > 0) {
|
|
73
|
-
const allowed = packages.find((p) => allowlist.includes(p.id));
|
|
74
|
-
targetId = allowed ? allowed.id : (packages[0]?.id || "");
|
|
75
|
-
}
|
|
76
|
-
else if (packages.length > 0) {
|
|
77
|
-
targetId = packages[0].id;
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
42
|
+
if (packages.length === 0)
|
|
80
43
|
throw new ActionDockError(PACKAGE_NOT_FOUND, "No registered package found in service");
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
44
|
+
const target = packages.find((pkg) => isPackageAllowedByPolicy(pkg.id, options));
|
|
45
|
+
if (!target)
|
|
46
|
+
throw new PackageNotAllowedError("No fully authorized package available for management");
|
|
47
|
+
return target.id;
|
|
84
48
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { CAPABILITY_UNAVAILABLE, CONFIG_DELETE_ERROR, CONFIG_ENV_ERROR, CONFIG_LIST_ERROR, CONFIG_SET_ERROR, INVALID_ARGUMENT, INVALID_PACKAGE_ID, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND, PATH_TRAVERSAL, } from "../../errors.js";
|
|
1
|
+
import { CAPABILITY_UNAVAILABLE, CONFIG_DELETE_ERROR, CONFIG_ENV_ERROR, CONFIG_LIST_ERROR, CONFIG_SET_ERROR, CONFIG_VALUE_INVALID, INVALID_ARGUMENT, INVALID_PACKAGE_ID, PACKAGE_NOT_ALLOWED, PACKAGE_NOT_FOUND, PATH_TRAVERSAL, } from "../../errors.js";
|
|
2
2
|
import { resolveEnvValue } from "../../runtime/index.js";
|
|
3
3
|
import { isSecretConfigKey, sanitizeConfigDefinitions } from "../../storage/index.js";
|
|
4
|
+
import { assertConcreteConfigKey, isConfigTemplate } from "../../project/config-declarations.js";
|
|
4
5
|
import { readJsonBody } from "../body.js";
|
|
5
6
|
import { getSubPath, isManagementAllowedByPolicy, jsonResponse, resolveTargetPackageId, } from "./common.js";
|
|
6
7
|
function isClientConfigError(err) {
|
|
@@ -8,6 +9,7 @@ function isClientConfigError(err) {
|
|
|
8
9
|
err?.code === INVALID_PACKAGE_ID ||
|
|
9
10
|
err?.code === PATH_TRAVERSAL ||
|
|
10
11
|
err?.code === INVALID_ARGUMENT ||
|
|
12
|
+
err?.code === CONFIG_VALUE_INVALID ||
|
|
11
13
|
err?.status === 400 ||
|
|
12
14
|
err?.status === 404);
|
|
13
15
|
}
|
|
@@ -44,6 +46,9 @@ export async function handleConfigRoutes(ctx) {
|
|
|
44
46
|
const declared = pkg?.config || {};
|
|
45
47
|
const envChecks = [];
|
|
46
48
|
for (const [k, def] of Object.entries(declared)) {
|
|
49
|
+
if (isConfigTemplate(k)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
47
52
|
const resolved = resolveEnvValue(k, def, targetPackageId);
|
|
48
53
|
const matchedEnv = resolved?.envKey || null;
|
|
49
54
|
envChecks.push({
|
|
@@ -80,17 +85,38 @@ export async function handleConfigRoutes(ctx) {
|
|
|
80
85
|
const pkg = pkgs.find((p) => p.id === targetPackageId);
|
|
81
86
|
const rawDeclared = pkg?.config || {};
|
|
82
87
|
const declared = sanitizeConfigDefinitions(rawDeclared) || {};
|
|
88
|
+
const queryKey = url.searchParams.get("key");
|
|
89
|
+
if (queryKey !== null) {
|
|
90
|
+
assertConcreteConfigKey(queryKey);
|
|
91
|
+
const item = await service.management.config.get(targetPackageId, queryKey);
|
|
92
|
+
return jsonResponse({ ok: true, configViewVersion: 1, packageId: targetPackageId, declared, item }, 200, corsHeaders);
|
|
93
|
+
}
|
|
83
94
|
const views = await service.management.config.list(targetPackageId);
|
|
84
|
-
const maskedValues =
|
|
95
|
+
const maskedValues = Object.create(null);
|
|
96
|
+
const maskedPaths = Object.create(null);
|
|
85
97
|
for (const v of views) {
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
maskedPaths[v.key] = v.redactedPaths || [];
|
|
99
|
+
if (v.secret && v.value === undefined) {
|
|
100
|
+
if (v.redactedPaths && v.redactedPaths.length > 0) {
|
|
101
|
+
maskedValues[v.key] = "********";
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
maskedValues[v.key] = undefined;
|
|
105
|
+
}
|
|
88
106
|
}
|
|
89
107
|
else {
|
|
90
108
|
maskedValues[v.key] = v.value;
|
|
91
109
|
}
|
|
92
110
|
}
|
|
93
|
-
return jsonResponse({
|
|
111
|
+
return jsonResponse({
|
|
112
|
+
ok: true,
|
|
113
|
+
configViewVersion: 1,
|
|
114
|
+
packageId: targetPackageId,
|
|
115
|
+
declared,
|
|
116
|
+
values: maskedValues,
|
|
117
|
+
items: views,
|
|
118
|
+
masked: maskedPaths,
|
|
119
|
+
}, 200, corsHeaders);
|
|
94
120
|
}
|
|
95
121
|
catch (err) {
|
|
96
122
|
if (err.code === PACKAGE_NOT_ALLOWED || err.status === 403) {
|
|
@@ -117,6 +143,7 @@ export async function handleConfigRoutes(ctx) {
|
|
|
117
143
|
if (!key) {
|
|
118
144
|
return jsonResponse({ ok: false, error: { code: INVALID_ARGUMENT, message: "Config 'key' is required" } }, 400, corsHeaders);
|
|
119
145
|
}
|
|
146
|
+
assertConcreteConfigKey(key);
|
|
120
147
|
await service.management.config.set(targetPackageId, key, body.value);
|
|
121
148
|
return jsonResponse({ ok: true, packageId: targetPackageId, key, message: "updated" }, 200, corsHeaders);
|
|
122
149
|
}
|
|
@@ -131,14 +158,45 @@ export async function handleConfigRoutes(ctx) {
|
|
|
131
158
|
}, 403, corsHeaders);
|
|
132
159
|
}
|
|
133
160
|
const isClient = isClientConfigError(err);
|
|
134
|
-
|
|
161
|
+
const errCode = err?.code === CONFIG_VALUE_INVALID
|
|
162
|
+
? CONFIG_VALUE_INVALID
|
|
163
|
+
: isClient
|
|
164
|
+
? INVALID_ARGUMENT
|
|
165
|
+
: CONFIG_SET_ERROR;
|
|
166
|
+
return jsonResponse({ ok: false, error: { code: errCode, message: err.message, details: err.details } }, isClient ? 400 : 500, corsHeaders);
|
|
135
167
|
}
|
|
136
168
|
}
|
|
137
|
-
// 4. Config
|
|
169
|
+
// 4. Config Item Route: GET /config/:key or DELETE /config/:key
|
|
138
170
|
const configKeyMatch = subpath.match(/^\/config\/([^/]+)$/);
|
|
171
|
+
if (configKeyMatch && req.method === "GET") {
|
|
172
|
+
try {
|
|
173
|
+
const key = decodeURIComponent(configKeyMatch[1]);
|
|
174
|
+
assertConcreteConfigKey(key);
|
|
175
|
+
const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
176
|
+
const pkgs = await service.discovery.listPackages();
|
|
177
|
+
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
|
|
178
|
+
const item = await service.management.config.get(targetPackageId, key);
|
|
179
|
+
const declared = sanitizeConfigDefinitions(pkgs.find((pkg) => pkg.id === targetPackageId)?.config) || {};
|
|
180
|
+
return jsonResponse({ ok: true, configViewVersion: 1, packageId: targetPackageId, declared, item }, 200, corsHeaders);
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err.code === PACKAGE_NOT_ALLOWED || err.status === 403) {
|
|
184
|
+
return jsonResponse({
|
|
185
|
+
ok: false,
|
|
186
|
+
error: {
|
|
187
|
+
code: PACKAGE_NOT_ALLOWED,
|
|
188
|
+
message: err.message || "Package is not in the allowed package list",
|
|
189
|
+
},
|
|
190
|
+
}, 403, corsHeaders);
|
|
191
|
+
}
|
|
192
|
+
const isClient = isClientConfigError(err);
|
|
193
|
+
return jsonResponse({ ok: false, error: { code: isClient ? INVALID_ARGUMENT : CONFIG_LIST_ERROR, message: err.message } }, isClient ? 400 : 500, corsHeaders);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
139
196
|
if (configKeyMatch && req.method === "DELETE") {
|
|
140
197
|
try {
|
|
141
198
|
const key = decodeURIComponent(configKeyMatch[1]);
|
|
199
|
+
assertConcreteConfigKey(key);
|
|
142
200
|
const pkgParam = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
143
201
|
const pkgs = await service.discovery.listPackages();
|
|
144
202
|
const targetPackageId = resolveTargetPackageId(pkgs, pkgParam, policy);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { DOCTOR_ERROR, PACKAGE_NOT_ALLOWED } from "../../errors.js";
|
|
2
2
|
import { runDoctorChecks } from "../../doctor/doctor.js";
|
|
3
|
-
import { assertPackageAllowed, getSubPath, jsonResponse } from "./common.js";
|
|
3
|
+
import { assertPackageAllowed, getSubPath, isFullAccessGranted, jsonResponse } from "./common.js";
|
|
4
4
|
/**
|
|
5
5
|
* 处理环境与依赖诊断接口(GET /api/v2/doctor 与 /doctor)。
|
|
6
|
+
* 只有整包授权开放该包诊断,全局诊断必须显式授予全部包。
|
|
6
7
|
*/
|
|
7
8
|
export async function handleDoctorRoute(ctx) {
|
|
8
9
|
const { req, url, pathname, corsHeaders, projectRoot, customHome, service, options, activePolicy: policy } = ctx;
|
|
@@ -12,7 +13,7 @@ export async function handleDoctorRoute(ctx) {
|
|
|
12
13
|
}
|
|
13
14
|
try {
|
|
14
15
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
15
|
-
if (policy
|
|
16
|
+
if (!isFullAccessGranted(policy)) {
|
|
16
17
|
if (!targetPkg) {
|
|
17
18
|
return jsonResponse({
|
|
18
19
|
ok: false,
|
|
@@ -36,7 +37,7 @@ export async function handleDoctorRoute(ctx) {
|
|
|
36
37
|
cwd: packageRoot || projectRoot || process.cwd(),
|
|
37
38
|
packageIdOrPath: packageRoot || targetPkg,
|
|
38
39
|
customHome,
|
|
39
|
-
packageAllowlist: policy.packageAllowlist,
|
|
40
|
+
packageAllowlist: isFullAccessGranted(policy) ? undefined : policy.packageAllowlist,
|
|
40
41
|
});
|
|
41
42
|
return jsonResponse({ ok: true, report }, 200, corsHeaders);
|
|
42
43
|
}
|
|
@@ -2,7 +2,8 @@ import { filterWithFallbackInfo } from "../../utils/intent.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 {
|
|
5
|
+
import { parseActionRef } from "../../catalog/resolve-action.js";
|
|
6
|
+
import { assertPackageVisible, filterPackagesByPolicy, getSubPath, jsonResponse, requiresMinimalPackageProjection, } from "./common.js";
|
|
6
7
|
function sanitizePackage(pkg, exposeDebugInfo) {
|
|
7
8
|
if (!pkg)
|
|
8
9
|
return pkg;
|
|
@@ -16,6 +17,73 @@ function sanitizePackage(pkg, exposeDebugInfo) {
|
|
|
16
17
|
}
|
|
17
18
|
return sanitized;
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* 仅在部分授权包缺少动作清单时补查目录;已有真实清单时不增加查询。
|
|
22
|
+
*/
|
|
23
|
+
function requiresCatalogForMinimalProjection(packages, policy) {
|
|
24
|
+
return packages.some((p) => requiresMinimalPackageProjection(p.id, policy) && !Array.isArray(p.actions));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 拉取真实动作目录并按包分组,查询失败交由路由返回错误,不能伪造发现结果。
|
|
28
|
+
*/
|
|
29
|
+
async function listCatalogActionIds(service) {
|
|
30
|
+
const actions = await service.discovery.listActions();
|
|
31
|
+
const byPackage = new Map();
|
|
32
|
+
for (const act of actions) {
|
|
33
|
+
const parsed = parseActionRef(act.id);
|
|
34
|
+
const pkgId = act.packageId || parsed.packageId;
|
|
35
|
+
if (!pkgId)
|
|
36
|
+
continue;
|
|
37
|
+
const bucket = byPackage.get(pkgId) || [];
|
|
38
|
+
if (!bucket.includes(parsed.actionId)) {
|
|
39
|
+
bucket.push(parsed.actionId);
|
|
40
|
+
}
|
|
41
|
+
byPackage.set(pkgId, bucket);
|
|
42
|
+
}
|
|
43
|
+
return byPackage;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 按当前生效策略统一投影包信息。
|
|
47
|
+
* 整包授权与全量授权沿用安全脱敏后的完整结构;
|
|
48
|
+
* 仅持有单动作授权的包收缩为最小身份元数据:
|
|
49
|
+
* 仅保留 id、name、version、description 与已授权动作列表信息,
|
|
50
|
+
* 不泄露 actionsDir/playbooksDir/config/actions 完整清单/playbooks/packageRoot 等结构。
|
|
51
|
+
* 授权动作引用统一经 core parseActionRef 单一事实源归一化
|
|
52
|
+
* (兼容 @scope/pkg/read 形态与前后空白),与鉴权判定同源;
|
|
53
|
+
* 返回的已授权动作与真实包清单取交集并去重,
|
|
54
|
+
* 不把不存在的声明动作当成已发现能力,真实清单查询失败时透明返回错误。
|
|
55
|
+
* 统一投影单点收敛,避免各响应分支复制裁剪逻辑。
|
|
56
|
+
*/
|
|
57
|
+
function projectPackageForPolicy(pkg, policy, exposeDebugInfo, realActionIds) {
|
|
58
|
+
const sanitized = sanitizePackage(pkg, exposeDebugInfo);
|
|
59
|
+
if (!requiresMinimalPackageProjection(pkg.id, policy)) {
|
|
60
|
+
return sanitized;
|
|
61
|
+
}
|
|
62
|
+
// 授权引用统一归一化,支持带作用域的包标识和引用两端的空白。
|
|
63
|
+
const grantedRefs = new Set();
|
|
64
|
+
for (const ref of policy.actionAllowlist || []) {
|
|
65
|
+
const parsed = parseActionRef(ref);
|
|
66
|
+
if (parsed.packageId === pkg.id) {
|
|
67
|
+
grantedRefs.add(parsed.actionId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 授权声明只用于过滤真实清单,不能替代已注册能力。
|
|
71
|
+
const declaredRealActions = Array.isArray(sanitized.actions)
|
|
72
|
+
? sanitized.actions
|
|
73
|
+
: realActionIds?.get(pkg.id) || [];
|
|
74
|
+
const grantedActionIds = Array.from(new Set(declaredRealActions
|
|
75
|
+
.map((id) => parseActionRef(id).actionId)
|
|
76
|
+
.filter((id) => grantedRefs.has(id))));
|
|
77
|
+
const minimal = {
|
|
78
|
+
id: sanitized.id,
|
|
79
|
+
...(sanitized.name !== undefined ? { name: sanitized.name } : {}),
|
|
80
|
+
...(sanitized.version !== undefined ? { version: sanitized.version } : {}),
|
|
81
|
+
...(sanitized.description !== undefined ? { description: sanitized.description } : {}),
|
|
82
|
+
actions: grantedActionIds,
|
|
83
|
+
actionsCount: grantedActionIds.length,
|
|
84
|
+
};
|
|
85
|
+
return minimal;
|
|
86
|
+
}
|
|
19
87
|
/**
|
|
20
88
|
* 处理系统自省与包信息接口:
|
|
21
89
|
* - GET /api/v2/info 与 GET /info -> 委托 service.info()
|
|
@@ -29,10 +97,14 @@ export async function handleInfoRoute(ctx) {
|
|
|
29
97
|
try {
|
|
30
98
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
31
99
|
if (targetPkg) {
|
|
32
|
-
|
|
100
|
+
assertPackageVisible(targetPkg, policy);
|
|
33
101
|
}
|
|
34
102
|
const rawPackages = await service.discovery.listPackages();
|
|
35
|
-
|
|
103
|
+
// 缺少包内清单时补查真实目录,查询失败不得伪造已发现能力。
|
|
104
|
+
const realActionIds = requiresCatalogForMinimalProjection(rawPackages, policy)
|
|
105
|
+
? await listCatalogActionIds(service)
|
|
106
|
+
: undefined;
|
|
107
|
+
const packages = filterPackagesByPolicy(rawPackages.map((p) => projectPackageForPolicy(p, policy, options.exposeDebugInfo, realActionIds)), policy);
|
|
36
108
|
return jsonResponse({
|
|
37
109
|
ok: true,
|
|
38
110
|
packages,
|
|
@@ -61,14 +133,18 @@ export async function handleInfoRoute(ctx) {
|
|
|
61
133
|
const intent = url.searchParams.get("intent") || undefined;
|
|
62
134
|
const targetPkg = url.searchParams.get("package") || url.searchParams.get("packageId") || undefined;
|
|
63
135
|
if (targetPkg) {
|
|
64
|
-
|
|
136
|
+
assertPackageVisible(targetPkg, policy);
|
|
65
137
|
}
|
|
66
138
|
const rawPackages = await service.info();
|
|
67
|
-
const
|
|
139
|
+
const realActionIds = requiresCatalogForMinimalProjection(rawPackages, policy)
|
|
140
|
+
? await listCatalogActionIds(service)
|
|
141
|
+
: undefined;
|
|
142
|
+
const packages = filterPackagesByPolicy(rawPackages.map((p) => projectPackageForPolicy(p, policy, options.exposeDebugInfo, realActionIds)), policy);
|
|
68
143
|
if (isTree) {
|
|
69
144
|
return jsonResponse({ ok: true, type: "tree", packages }, 200, corsHeaders);
|
|
70
145
|
}
|
|
71
|
-
// 显式指定 package
|
|
146
|
+
// 显式指定 package 详情下钻:最小投影包仅返回最小身份元数据,
|
|
147
|
+
// 不能借单动作授权展开完整包结构
|
|
72
148
|
if (targetPkg) {
|
|
73
149
|
const matched = packages.find((p) => p.id === targetPkg || p.packageRoot === targetPkg || p.path === targetPkg);
|
|
74
150
|
if (!matched) {
|
|
@@ -80,6 +156,14 @@ export async function handleInfoRoute(ctx) {
|
|
|
80
156
|
},
|
|
81
157
|
}, 404, corsHeaders);
|
|
82
158
|
}
|
|
159
|
+
const isMinimal = requiresMinimalPackageProjection(matched.id, policy);
|
|
160
|
+
if (isMinimal) {
|
|
161
|
+
return jsonResponse({
|
|
162
|
+
ok: true,
|
|
163
|
+
type: "package_detail",
|
|
164
|
+
...matched,
|
|
165
|
+
}, 200, corsHeaders);
|
|
166
|
+
}
|
|
83
167
|
return jsonResponse({
|
|
84
168
|
ok: true,
|
|
85
169
|
type: "package_detail",
|
|
@@ -96,12 +180,15 @@ export async function handleInfoRoute(ctx) {
|
|
|
96
180
|
], true);
|
|
97
181
|
if (filterRes.matchedCount === 1) {
|
|
98
182
|
const single = filterRes.items[0];
|
|
183
|
+
const isMinimal = requiresMinimalPackageProjection(single.id, policy);
|
|
99
184
|
return jsonResponse({
|
|
100
185
|
ok: true,
|
|
101
186
|
type: "package_detail",
|
|
102
187
|
isSingleMatch: true,
|
|
103
188
|
...single,
|
|
104
|
-
...(options.exposeDebugInfo
|
|
189
|
+
...(options.exposeDebugInfo && !isMinimal
|
|
190
|
+
? { projectRoot: single.packageRoot || projectRoot }
|
|
191
|
+
: {}),
|
|
105
192
|
}, 200, corsHeaders);
|
|
106
193
|
}
|
|
107
194
|
return jsonResponse({
|
|
@@ -111,15 +198,18 @@ export async function handleInfoRoute(ctx) {
|
|
|
111
198
|
packages: filterRes.items,
|
|
112
199
|
}, 200, corsHeaders);
|
|
113
200
|
}
|
|
114
|
-
//
|
|
201
|
+
// 单包模式下展开详情:最小投影包不展开 projectRoot 等调试信息
|
|
115
202
|
if (packages.length === 1) {
|
|
116
203
|
const p = packages[0];
|
|
204
|
+
const isMinimal = requiresMinimalPackageProjection(p.id, policy);
|
|
117
205
|
return jsonResponse({
|
|
118
206
|
ok: true,
|
|
119
207
|
type: "package_detail",
|
|
120
208
|
...p,
|
|
121
209
|
packages,
|
|
122
|
-
...(options.exposeDebugInfo
|
|
210
|
+
...(options.exposeDebugInfo && !isMinimal
|
|
211
|
+
? { projectRoot: p.packageRoot || projectRoot }
|
|
212
|
+
: {}),
|
|
123
213
|
}, 200, corsHeaders);
|
|
124
214
|
}
|
|
125
215
|
// 多包列表概览
|
|
@@ -141,10 +231,7 @@ export async function handleInfoRoute(ctx) {
|
|
|
141
231
|
},
|
|
142
232
|
}, 403, corsHeaders);
|
|
143
233
|
}
|
|
144
|
-
return jsonResponse({
|
|
145
|
-
ok: false,
|
|
146
|
-
error: { code: INFO_ERROR, message: err.message },
|
|
147
|
-
}, 500, corsHeaders);
|
|
234
|
+
return jsonResponse({ ok: false, error: { code: INFO_ERROR, message: err.message } }, 500, corsHeaders);
|
|
148
235
|
}
|
|
149
236
|
}
|
|
150
237
|
return null;
|
|
@@ -19,9 +19,7 @@ export async function handlePlaybooksRoutes(ctx) {
|
|
|
19
19
|
if (targetPkg) {
|
|
20
20
|
pbs = pbs.filter((p) => p.packageId === targetPkg);
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
pbs = pbs.filter((p) => isPackageAllowedByPolicy(p.packageId, policy));
|
|
24
|
-
}
|
|
22
|
+
pbs = pbs.filter((p) => isPackageAllowedByPolicy(p.packageId, policy));
|
|
25
23
|
if (intent) {
|
|
26
24
|
pbs = filterByIntent(pbs, intent, [(p) => p.id, (p) => p.description || "", (p) => p.packageId || "", (p) => (p.actions || []).join(" ")], false);
|
|
27
25
|
}
|