@actiondock/core 2.18.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.
Files changed (74) hide show
  1. package/dist/client/config.d.ts +17 -14
  2. package/dist/client/config.js +112 -14
  3. package/dist/client/index.d.ts +1 -1
  4. package/dist/client/index.js +1 -1
  5. package/dist/client/runs.d.ts +9 -0
  6. package/dist/client/runs.js +7 -0
  7. package/dist/errors.d.ts +3 -1
  8. package/dist/errors.js +2 -0
  9. package/dist/execution/service.js +8 -5
  10. package/dist/execution/types.d.ts +3 -3
  11. package/dist/host/host.d.ts +9 -1
  12. package/dist/host/host.js +266 -45
  13. package/dist/host/types.d.ts +3 -0
  14. package/dist/index.d.ts +4 -3
  15. package/dist/index.js +1 -1
  16. package/dist/input/config-override.d.ts +5 -0
  17. package/dist/input/config-override.js +23 -0
  18. package/dist/input/input-resolver.d.ts +24 -7
  19. package/dist/input/input-resolver.js +73 -5
  20. package/dist/invocation/bindings.d.ts +53 -0
  21. package/dist/invocation/bindings.js +248 -0
  22. package/dist/invocation/policy.d.ts +5 -1
  23. package/dist/invocation/policy.js +16 -9
  24. package/dist/invocation/remote.d.ts +40 -0
  25. package/dist/invocation/remote.js +489 -0
  26. package/dist/invocation/types.d.ts +2 -0
  27. package/dist/invocation/types.js +1 -0
  28. package/dist/package/index.d.ts +1 -1
  29. package/dist/package/runtime.js +105 -15
  30. package/dist/platform/types.d.ts +48 -1
  31. package/dist/project/config-declarations.d.ts +61 -0
  32. package/dist/project/config-declarations.js +297 -0
  33. package/dist/project/index.d.ts +5 -2
  34. package/dist/project/index.js +5 -2
  35. package/dist/project/init.js +2 -2
  36. package/dist/project/loader.js +4 -0
  37. package/dist/project/manifest.js +10 -0
  38. package/dist/project/types.d.ts +30 -0
  39. package/dist/runtime/context.d.ts +6 -3
  40. package/dist/runtime/context.js +60 -13
  41. package/dist/runtime/env.js +15 -1
  42. package/dist/runtime/standalone-commands.d.ts +2 -2
  43. package/dist/runtime/standalone-commands.js +113 -47
  44. package/dist/runtime/standalone-help.d.ts +1 -0
  45. package/dist/runtime/standalone-help.js +15 -9
  46. package/dist/runtime/standalone.js +2 -2
  47. package/dist/server/policy.d.ts +24 -107
  48. package/dist/server/policy.js +134 -291
  49. package/dist/server/routes/actions.js +10 -8
  50. package/dist/server/routes/common.d.ts +5 -20
  51. package/dist/server/routes/common.js +22 -58
  52. package/dist/server/routes/config.js +65 -7
  53. package/dist/server/routes/doctor.d.ts +1 -0
  54. package/dist/server/routes/doctor.js +4 -3
  55. package/dist/server/routes/info.js +100 -13
  56. package/dist/server/routes/playbooks.js +1 -3
  57. package/dist/server/routes/runs.js +112 -18
  58. package/dist/server/server.d.ts +2 -1
  59. package/dist/server/server.js +38 -55
  60. package/dist/server/types.d.ts +6 -6
  61. package/dist/service/factory.js +1 -0
  62. package/dist/service/remote.js +28 -29
  63. package/dist/service/types.d.ts +34 -5
  64. package/dist/service/types.js +8 -0
  65. package/dist/storage/mask.d.ts +33 -6
  66. package/dist/storage/mask.js +298 -16
  67. package/dist/storage/sqlite-runs.d.ts +5 -17
  68. package/dist/storage/sqlite-runs.js +127 -5
  69. package/dist/storage/sqlite.d.ts +5 -17
  70. package/dist/storage/sqlite.js +2 -2
  71. package/dist/storage/types.d.ts +31 -16
  72. package/dist/version.d.ts +1 -1
  73. package/dist/version.js +1 -1
  74. package/package.json +2 -2
@@ -3,7 +3,7 @@ import { filterByIntent } from "../../utils/intent.js";
3
3
  import { ACTION_EXECUTION_ERROR, ACTION_FORBIDDEN, ACTION_NOT_FOUND, ACTION_START_FAILED, ACTION_TIMEOUT, ACTIONS_LIST_ERROR, IDEMPOTENCY_CONFLICT, INPUT_VALIDATION_FAILED, INVALID_JSON, PACKAGE_FORBIDDEN, PACKAGE_NOT_FOUND, REQUEST_TOO_LARGE, } from "../../errors.js";
4
4
  import { InvalidJsonError, readJsonBody, RequestTooLargeError } from "../body.js";
5
5
  import { createServerRunOrigin } from "../policy.js";
6
- import { filterActionsByPolicy, getSubPath, isActionAllowedByPolicy, isPackageAllowedByPolicy, jsonResponse, } from "./common.js";
6
+ import { filterActionsByPolicy, getSubPath, isActionAllowedByPolicy, isPackageVisibleByPolicy, jsonResponse, } from "./common.js";
7
7
  /**
8
8
  * 处理 Action 相关的 HTTP 路由(列表、规范查询、同步执行与异步启动)。
9
9
  */
@@ -44,7 +44,9 @@ export async function handleActionsRoutes(ctx) {
44
44
  if (pkgActionShowMatch && req.method === "GET") {
45
45
  const packageId = decodeURIComponent(pkgActionShowMatch[1]);
46
46
  const actionId = decodeURIComponent(pkgActionShowMatch[2]);
47
- if (!isPackageAllowedByPolicy(packageId, policy)) {
47
+ // 先按包可见性拦截完全无关包,再按实际完整引用判定动作授权,
48
+ // 保证仅持有单动作授权(如 B/read)时 B/read 可见可用
49
+ if (!isPackageVisibleByPolicy(packageId, policy)) {
48
50
  return jsonResponse({
49
51
  ok: false,
50
52
  error: {
@@ -84,7 +86,7 @@ export async function handleActionsRoutes(ctx) {
84
86
  try {
85
87
  const parsed = parseActionRef(actionId);
86
88
  if (parsed.packageId) {
87
- if (!isPackageAllowedByPolicy(parsed.packageId, policy)) {
89
+ if (!isPackageVisibleByPolicy(parsed.packageId, policy)) {
88
90
  return jsonResponse({
89
91
  ok: false,
90
92
  error: {
@@ -107,7 +109,7 @@ export async function handleActionsRoutes(ctx) {
107
109
  catch { }
108
110
  try {
109
111
  const spec = await service.discovery.describeAction(actionId);
110
- if (!isPackageAllowedByPolicy(spec.packageId, policy)) {
112
+ if (!isPackageVisibleByPolicy(spec.packageId, policy)) {
111
113
  return jsonResponse({
112
114
  ok: false,
113
115
  error: {
@@ -166,8 +168,8 @@ export async function handleActionsRoutes(ctx) {
166
168
  }
167
169
  catch { }
168
170
  }
169
- // 解析并补全包与动作标识(仅在必要时单次查询 spec)
170
- if ((!pkgId || !actId) && (policy.packageAllowlist?.length || policy.actionAllowlist?.length)) {
171
+ // 查询实际包与动作身份,短名请求不能绕过全限定授权判断。
172
+ if (!pkgId || !actId) {
171
173
  try {
172
174
  const spec = await service.discovery.describeAction(actionRef);
173
175
  pkgId = pkgId || spec?.packageId;
@@ -185,8 +187,8 @@ export async function handleActionsRoutes(ctx) {
185
187
  actId = actionRef;
186
188
  }
187
189
  }
188
- // 校验 Package 允许白名单
189
- if (!isPackageAllowedByPolicy(pkgId, policy)) {
190
+ // 单动作授权只要求其所属包可见,不能用整包能力门禁将其拦截。
191
+ if (!isPackageVisibleByPolicy(pkgId, policy)) {
190
192
  return jsonResponse({
191
193
  ok: false,
192
194
  runId: "",
@@ -1,32 +1,17 @@
1
1
  import type { PackageInfo } from "../../package/types.js";
2
2
  import type { EffectiveServerPolicy, RouteContext, ServerOptions } from "../types.js";
3
- import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isManagementAllowedByPolicy, isPackageAllowedByPolicy, PackageNotAllowedError } from "../policy.js";
3
+ import { ActionForbiddenError, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, isActionAllowed, isActionAllowedByPolicy, isFullAccessGranted, isManagementAllowedByPolicy, isPackageAllowedByPolicy, isPackageVisibleByPolicy, PackageNotAllowedError, requiresMinimalPackageProjection } from "../policy.js";
4
4
  export type { RouteContext };
5
- export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isPackageAllowedByPolicy, isManagementAllowedByPolicy, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
6
- /**
7
- * 构造带 CORS 头的标准 JSON HTTP 响应。
8
- */
5
+ export { ActionForbiddenError, PackageNotAllowedError, isActionAllowed, isActionAllowedByPolicy, isFullAccessGranted, isPackageAllowedByPolicy, isPackageVisibleByPolicy, isManagementAllowedByPolicy, requiresMinimalPackageProjection, filterActionsByPolicy, filterPackagesByPolicy, assertActionAllowedByPolicy, assertPackageAllowedByPolicy, };
9
6
  export declare function jsonResponse(data: unknown, status?: number, corsHeaders?: Record<string, string>): Response;
10
- /**
11
- * 剥离标准版本前缀 (/api/v2/),获取相对路由子路径。
12
- */
13
7
  export declare function getSubPath(pathname: string): string;
14
- /**
15
- * 校验目标 action 是否属于允许动作白名单。
16
- * 当 options 或 policy 中的 actionAllowlist 存在且目标 action 不在白名单中时抛出 403 ACTION_FORBIDDEN 异常。
17
- */
18
8
  export declare function assertActionAllowed(action: {
19
9
  actionId?: string;
20
10
  id?: string;
21
11
  packageId?: string;
22
12
  } | string, options?: ServerOptions | EffectiveServerPolicy): void;
23
- /**
24
- * 校验目标 packageId 是否属于允许包白名单。
25
- * 当 options 或 policy 中的 packageAllowlist 存在且目标 packageId 不在白名单中时抛出 403 PACKAGE_NOT_ALLOWED 异常。
26
- */
27
13
  export declare function assertPackageAllowed(packageId: string, options?: ServerOptions | EffectiveServerPolicy): void;
28
- /**
29
- * 解析并确定生效的目标 packageId。
30
- * 遵循严格的解析规则与白名单检查。
31
- */
14
+ /** 包发现可见性与整包能力分开判断,不能因单动作授权开放包级管理。 */
15
+ export declare function assertPackageVisible(packageId: string, options?: ServerOptions | EffectiveServerPolicy): void;
16
+ /** 管理接口只能选取整包授权目标;全局管理必须显式拥有全部授权。 */
32
17
  export declare function resolveTargetPackageId(packages: PackageInfo[], requestedPackageId?: string, options?: ServerOptions | EffectiveServerPolicy): string;
@@ -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
- if (pathname.startsWith("/api/v2/")) {
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
- const policy = options
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
- const policy = options
45
- ? {
46
- packageAllowlist: options.packageAllowlist,
47
- }
48
- : undefined;
49
- assertPackageAllowedByPolicy(packageId, policy);
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((p) => p.id === requestedPackageId || p.packageRoot === requestedPackageId);
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
- let targetId;
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
- assertPackageAllowed(targetId, options);
83
- return targetId;
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
- if (v.secret) {
87
- maskedValues[v.key] = "********";
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({ ok: true, packageId: targetPackageId, declared, values: maskedValues }, 200, corsHeaders);
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
- return jsonResponse({ ok: false, error: { code: isClient ? INVALID_ARGUMENT : CONFIG_SET_ERROR, message: err.message } }, isClient ? 400 : 500, corsHeaders);
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 Delete: DELETE /config/:key
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,5 +1,6 @@
1
1
  import { type RouteContext } from "./common.js";
2
2
  /**
3
3
  * 处理环境与依赖诊断接口(GET /api/v2/doctor 与 /doctor)。
4
+ * 只有整包授权开放该包诊断,全局诊断必须显式授予全部包。
4
5
  */
5
6
  export declare function handleDoctorRoute(ctx: RouteContext): Promise<Response | null>;
@@ -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.packageAllowlist && policy.packageAllowlist.length > 0) {
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 { assertPackageAllowed, filterPackagesByPolicy, getSubPath, jsonResponse } from "./common.js";
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
- assertPackageAllowed(targetPkg, policy);
100
+ assertPackageVisible(targetPkg, policy);
33
101
  }
34
102
  const rawPackages = await service.discovery.listPackages();
35
- const packages = filterPackagesByPolicy(rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo)), policy);
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
- assertPackageAllowed(targetPkg, policy);
136
+ assertPackageVisible(targetPkg, policy);
65
137
  }
66
138
  const rawPackages = await service.info();
67
- const packages = filterPackagesByPolicy(rawPackages.map((p) => sanitizePackage(p, options.exposeDebugInfo)), policy);
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 ? { projectRoot: single.packageRoot || projectRoot } : {}),
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 ? { projectRoot: p.packageRoot || projectRoot } : {}),
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
- if (policy.packageAllowlist && Array.isArray(policy.packageAllowlist) && policy.packageAllowlist.length > 0) {
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
  }