@actiondock/core 2.19.0 → 2.21.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 (108) 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/errors.d.ts +13 -1
  6. package/dist/errors.js +25 -0
  7. package/dist/execution/service.js +44 -10
  8. package/dist/execution/types.d.ts +3 -3
  9. package/dist/host/host.d.ts +20 -1
  10. package/dist/host/host.js +430 -66
  11. package/dist/host/types.d.ts +13 -1
  12. package/dist/index.d.ts +11 -3
  13. package/dist/index.js +5 -1
  14. package/dist/input/config-override.d.ts +5 -0
  15. package/dist/input/config-override.js +23 -0
  16. package/dist/input/input-resolver.d.ts +24 -7
  17. package/dist/input/input-resolver.js +73 -5
  18. package/dist/invocation/bindings.d.ts +53 -0
  19. package/dist/invocation/bindings.js +248 -0
  20. package/dist/invocation/policy.d.ts +5 -1
  21. package/dist/invocation/policy.js +16 -9
  22. package/dist/invocation/remote.d.ts +40 -0
  23. package/dist/invocation/remote.js +489 -0
  24. package/dist/invocation/types.d.ts +33 -0
  25. package/dist/invocation/types.js +11 -1
  26. package/dist/package/index.d.ts +2 -2
  27. package/dist/package/runtime.js +48 -15
  28. package/dist/platform/node.d.ts +6 -0
  29. package/dist/platform/node.js +59 -16
  30. package/dist/platform/types.d.ts +59 -1
  31. package/dist/process/command-executor.d.ts +35 -0
  32. package/dist/process/command-executor.js +326 -0
  33. package/dist/process/index.d.ts +1 -0
  34. package/dist/process/index.js +1 -0
  35. package/dist/process/process-driver.d.ts +19 -6
  36. package/dist/process/process-driver.js +223 -54
  37. package/dist/process/run-executor.js +101 -47
  38. package/dist/project/config-declarations.d.ts +61 -0
  39. package/dist/project/config-declarations.js +297 -0
  40. package/dist/project/dependencies.d.ts +92 -0
  41. package/dist/project/dependencies.js +543 -0
  42. package/dist/project/index.d.ts +8 -2
  43. package/dist/project/index.js +8 -2
  44. package/dist/project/init.js +2 -2
  45. package/dist/project/loader.js +19 -30
  46. package/dist/project/manifest.js +10 -0
  47. package/dist/project/package-manager.d.ts +40 -0
  48. package/dist/project/package-manager.js +224 -0
  49. package/dist/project/preparation.d.ts +8 -0
  50. package/dist/project/preparation.js +130 -0
  51. package/dist/project/transactions.d.ts +5 -1
  52. package/dist/project/transactions.js +35 -75
  53. package/dist/project/types.d.ts +30 -0
  54. package/dist/runtime/context.d.ts +27 -5
  55. package/dist/runtime/context.js +98 -26
  56. package/dist/runtime/env.js +15 -1
  57. package/dist/runtime/run-persistence.d.ts +1 -1
  58. package/dist/runtime/run-persistence.js +4 -3
  59. package/dist/runtime/standalone-commands.d.ts +2 -2
  60. package/dist/runtime/standalone-commands.js +113 -47
  61. package/dist/runtime/standalone-help.d.ts +1 -0
  62. package/dist/runtime/standalone-help.js +15 -9
  63. package/dist/runtime/standalone.js +2 -2
  64. package/dist/security/config-cipher.d.ts +54 -0
  65. package/dist/security/config-cipher.js +190 -0
  66. package/dist/security/config-key.d.ts +35 -0
  67. package/dist/security/config-key.js +469 -0
  68. package/dist/server/policy.d.ts +24 -107
  69. package/dist/server/policy.js +134 -291
  70. package/dist/server/routes/actions.js +10 -8
  71. package/dist/server/routes/common.d.ts +5 -20
  72. package/dist/server/routes/common.js +22 -58
  73. package/dist/server/routes/config.js +84 -7
  74. package/dist/server/routes/doctor.d.ts +1 -0
  75. package/dist/server/routes/doctor.js +4 -3
  76. package/dist/server/routes/info.js +100 -13
  77. package/dist/server/routes/playbooks.js +1 -3
  78. package/dist/server/routes/runs.js +96 -18
  79. package/dist/server/server.js +25 -53
  80. package/dist/server/types.d.ts +6 -6
  81. package/dist/service/factory.js +4 -0
  82. package/dist/service/remote.js +14 -29
  83. package/dist/service/types.d.ts +28 -5
  84. package/dist/service/types.js +8 -0
  85. package/dist/storage/config-value.d.ts +33 -0
  86. package/dist/storage/config-value.js +102 -0
  87. package/dist/storage/file-lock.js +35 -21
  88. package/dist/storage/index.d.ts +24 -2
  89. package/dist/storage/index.js +65 -2
  90. package/dist/storage/mask.d.ts +33 -6
  91. package/dist/storage/mask.js +298 -16
  92. package/dist/storage/migrations/index.d.ts +40 -0
  93. package/dist/storage/migrations/index.js +96 -0
  94. package/dist/storage/migrations/v2-to-v3.d.ts +16 -0
  95. package/dist/storage/migrations/v2-to-v3.js +116 -0
  96. package/dist/storage/sqlite-config.d.ts +62 -3
  97. package/dist/storage/sqlite-config.js +163 -9
  98. package/dist/storage/sqlite-driver.d.ts +5 -2
  99. package/dist/storage/sqlite-driver.js +24 -3
  100. package/dist/storage/sqlite-schema.d.ts +20 -2
  101. package/dist/storage/sqlite-schema.js +129 -132
  102. package/dist/storage/sqlite.d.ts +7 -2
  103. package/dist/storage/sqlite.js +64 -13
  104. package/dist/storage/types.d.ts +72 -4
  105. package/dist/storage/types.js +7 -2
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/package.json +2 -2
@@ -1,11 +1,48 @@
1
1
  import { snapshotRunOrigin } from "../runtime/run-origin.js";
2
2
  import { parseActionRef } from "../catalog/resolve-action.js";
3
- import { ActionDockError, ACTION_FORBIDDEN, PACKAGE_NOT_ALLOWED } from "../errors.js";
3
+ import { ActionDockError, ACTION_FORBIDDEN, INVALID_ARGUMENT, PACKAGE_NOT_ALLOWED } from "../errors.js";
4
+ import { assertValidPackageId } from "../utils/index.js";
4
5
  import { safeEqual } from "./security.js";
5
6
  export { extractBearerToken } from "./security.js";
6
- /**
7
- * 动作未列入白名单拒绝访问异常。
8
- */
7
+ /** 校验两类白名单授权;省略或空数组不增加授权,包列表中的独立 * 显式授予全部权限。 */
8
+ export function assertServerPolicyValid(item, scope = "server options") {
9
+ for (const field of ["packageAllowlist", "actionAllowlist"]) {
10
+ const list = item[field];
11
+ if (list === undefined)
12
+ continue;
13
+ if (!Array.isArray(list)) {
14
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid ${scope}.${field}: expected an array`);
15
+ }
16
+ for (const entry of list) {
17
+ if (typeof entry !== "string" || !entry.trim()) {
18
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid ${scope}.${field}: entries must be non-empty strings`);
19
+ }
20
+ try {
21
+ if (field === "packageAllowlist") {
22
+ if (entry !== "*")
23
+ assertValidPackageId(entry);
24
+ }
25
+ else {
26
+ if (entry.includes("*") || entry.includes("?")) {
27
+ throw new Error("wildcards are not supported in action references");
28
+ }
29
+ const ref = parseActionRef(entry);
30
+ if (!ref.packageId) {
31
+ throw new Error("action references must be fully qualified packageId/actionId references");
32
+ }
33
+ assertValidPackageId(ref.packageId);
34
+ }
35
+ }
36
+ catch (error) {
37
+ throw new ActionDockError(INVALID_ARGUMENT, `Invalid ${scope}.${field}: ${error instanceof Error ? error.message : String(error)}`);
38
+ }
39
+ }
40
+ }
41
+ }
42
+ /** 是否显式授予全部包与动作;不替代独立的管理能力门禁。 */
43
+ export function isFullAccessGranted(policy) {
44
+ return policy?.packageAllowlist?.includes("*") === true;
45
+ }
9
46
  export class ActionForbiddenError extends ActionDockError {
10
47
  status = 403;
11
48
  statusCode = 403;
@@ -15,9 +52,6 @@ export class ActionForbiddenError extends ActionDockError {
15
52
  Object.setPrototypeOf(this, ActionForbiddenError.prototype);
16
53
  }
17
54
  }
18
- /**
19
- * 包未列入白名单拒绝访问异常。
20
- */
21
55
  export class PackageNotAllowedError extends ActionDockError {
22
56
  status = 403;
23
57
  statusCode = 403;
@@ -27,41 +61,43 @@ export class PackageNotAllowedError extends ActionDockError {
27
61
  Object.setPrototypeOf(this, PackageNotAllowedError.prototype);
28
62
  }
29
63
  }
30
- /**
31
- * 根据客户端携带的 Token 智能匹配已注册的视图。
32
- * 使用 safeEqual 进行恒定时间比较以防范时序攻击。
33
- *
34
- * @param token 客户端提供的 Bearer Token
35
- * @param views 可遍历的视图集合
36
- * @returns 匹配成功的视图对象,未命中返回 undefined
37
- */
64
+ /** 遍历全部视图并以常数时间比较令牌,避免提前退出泄露匹配位置。 */
38
65
  export function matchViewByToken(token, views) {
39
66
  const trimmed = token.trim();
40
- if (!trimmed) {
67
+ if (!trimmed)
41
68
  return undefined;
42
- }
43
- let matched = undefined;
69
+ let matched;
44
70
  for (const view of views) {
45
71
  const candidateToken = view.policy.token?.trim();
46
72
  if (candidateToken) {
47
73
  const isMatch = safeEqual(trimmed, candidateToken);
48
- if (isMatch && !matched) {
74
+ if (isMatch && !matched)
49
75
  matched = view;
50
- }
51
76
  }
52
77
  }
53
78
  return matched;
54
79
  }
55
- /**
56
- * 归一化解析服务端配置的所有视图集合。
57
- * 保证默认视图与显式自定义视图具有统一的数据结构。
58
- *
59
- * @param options 服务端配置选项
60
- * @returns 默认视图与全部视图 Map 映射
61
- */
80
+ function normalizeView(item, name) {
81
+ assertServerPolicyValid(item, `view '${name}'`);
82
+ return {
83
+ name,
84
+ policy: {
85
+ viewName: name,
86
+ token: item.token,
87
+ packageAllowlist: item.packageAllowlist?.slice(),
88
+ actionAllowlist: item.actionAllowlist?.map((entry) => {
89
+ const ref = parseActionRef(entry);
90
+ return `${ref.packageId}/${ref.actionId}`;
91
+ }),
92
+ enableManagement: item.enableManagement,
93
+ },
94
+ enableMcp: item.enableMcp !== false,
95
+ rawOptions: item,
96
+ };
97
+ }
98
+ /** 全局选项仅配置默认视图;命名视图独立授权,缺少授权的视图默认拒绝全部。 */
62
99
  export function normalizeServerViews(options) {
63
- const views = new Map();
64
- // 1. 初始化基于全局配置的默认视图
100
+ assertServerPolicyValid(options);
65
101
  const defaultRawOptions = {
66
102
  name: "default",
67
103
  token: options.token,
@@ -72,307 +108,114 @@ export function normalizeServerViews(options) {
72
108
  mcpHandler: options.mcpHandler,
73
109
  };
74
110
  const customViews = new Map();
75
- // 2. 解析 options.views 自定义视图
76
- if (options.views) {
77
- if (Array.isArray(options.views)) {
78
- for (let i = 0; i < options.views.length; i++) {
79
- const item = options.views[i];
80
- if (!item || typeof item !== "object")
81
- continue;
82
- const name = item.name?.trim() || `view_${i}`;
83
- if (!name || name === "." || name === "..") {
84
- continue;
85
- }
86
- if (name === "default") {
87
- Object.assign(defaultRawOptions, item);
88
- }
89
- else {
90
- customViews.set(name, {
91
- name,
92
- policy: {
93
- viewName: name,
94
- token: item.token,
95
- packageAllowlist: item.packageAllowlist,
96
- actionAllowlist: item.actionAllowlist,
97
- enableManagement: item.enableManagement,
98
- },
99
- enableMcp: item.enableMcp !== false,
100
- rawOptions: item,
101
- });
102
- }
103
- }
111
+ const entries = Array.isArray(options.views)
112
+ ? options.views.map((item, index) => [`view_${index}`, item])
113
+ : Object.entries(options.views || {});
114
+ for (const [key, item] of entries) {
115
+ if (!item || typeof item !== "object")
116
+ continue;
117
+ const name = item.name?.trim() || key.trim();
118
+ if (!name || name === "." || name === "..")
119
+ continue;
120
+ if (name === "default") {
121
+ Object.assign(defaultRawOptions, item);
104
122
  }
105
- else if (typeof options.views === "object") {
106
- for (const [key, item] of Object.entries(options.views)) {
107
- if (!item || typeof item !== "object")
108
- continue;
109
- const name = item.name?.trim() || key.trim();
110
- if (!name || name === "." || name === "..") {
111
- continue;
112
- }
113
- if (name === "default") {
114
- Object.assign(defaultRawOptions, item);
115
- }
116
- else {
117
- customViews.set(name, {
118
- name,
119
- policy: {
120
- viewName: name,
121
- token: item.token,
122
- packageAllowlist: item.packageAllowlist,
123
- actionAllowlist: item.actionAllowlist,
124
- enableManagement: item.enableManagement,
125
- },
126
- enableMcp: item.enableMcp !== false,
127
- rawOptions: item,
128
- });
129
- }
130
- }
123
+ else {
124
+ customViews.set(name, normalizeView(item, name));
131
125
  }
132
126
  }
133
- const defaultView = {
134
- name: "default",
135
- policy: {
136
- viewName: "default",
137
- token: defaultRawOptions.token,
138
- packageAllowlist: defaultRawOptions.packageAllowlist,
139
- actionAllowlist: defaultRawOptions.actionAllowlist,
140
- enableManagement: defaultRawOptions.enableManagement,
141
- },
142
- enableMcp: defaultRawOptions.enableMcp !== false,
143
- rawOptions: defaultRawOptions,
144
- };
145
- // 默认视图置于首位,随后装载自定义视图
146
- views.set("default", defaultView);
147
- for (const [k, v] of customViews) {
148
- views.set(k, v);
149
- }
127
+ const defaultView = normalizeView(defaultRawOptions, "default");
128
+ const views = new Map([["default", defaultView], ...customViews]);
150
129
  return { defaultView, views };
151
130
  }
152
- /**
153
- * 校验目标动作是否在 actionAllowlist 允许白名单中。
154
- * 支持短名 actionId 与全限定名 packageId/actionId 匹配:
155
- * - 白名单项若含 '/'(如 pkg/act),则必须 packageId 与 actionId 同时精确匹配。
156
- * - 白名单项若不含 '/'(如 act),则只需 actionId 匹配(不限包)。
157
- *
158
- * @param action 目标动作对象或引用字符串
159
- * @param allowlist 允许动作白名单列表
160
- * @returns 是否允许访问
161
- */
162
- export function isActionAllowed(action, allowlist) {
163
- if (!allowlist || !Array.isArray(allowlist) || allowlist.length === 0) {
164
- return true;
165
- }
166
- if (!action) {
167
- return false;
168
- }
169
- let target;
131
+ /** 目标引用格式错误或包身份不一致时返回未命中,不能猜测其所属包。 */
132
+ function parsePolicyAction(action) {
133
+ if (!action)
134
+ return undefined;
170
135
  try {
171
- if (typeof action === "string") {
172
- target = parseActionRef(action);
173
- }
174
- else {
175
- const rawId = action.actionId || action.id;
176
- if (!rawId)
177
- return false;
178
- if (action.packageId) {
179
- if (rawId.includes("/")) {
180
- target = parseActionRef(rawId);
181
- }
182
- else {
183
- target = parseActionRef({
184
- packageId: action.packageId,
185
- actionId: rawId,
186
- });
187
- }
188
- }
189
- else {
190
- target = parseActionRef(rawId);
191
- }
192
- }
136
+ if (typeof action === "string")
137
+ return parseActionRef(action);
138
+ const rawId = action.actionId || action.id;
139
+ if (!rawId)
140
+ return undefined;
141
+ const ref = parseActionRef(rawId);
142
+ if (action.packageId && ref.packageId && action.packageId !== ref.packageId)
143
+ return undefined;
144
+ return { packageId: action.packageId || ref.packageId, actionId: ref.actionId };
193
145
  }
194
146
  catch {
195
- return false;
196
- }
197
- for (const item of allowlist) {
198
- if (typeof item !== "string")
199
- continue;
200
- const trimmed = item.trim();
201
- if (!trimmed)
202
- continue;
203
- try {
204
- const rule = parseActionRef(trimmed);
205
- if (rule.packageId) {
206
- if (rule.packageId === target.packageId && rule.actionId === target.actionId) {
207
- return true;
208
- }
209
- }
210
- else {
211
- if (rule.actionId === target.actionId) {
212
- return true;
213
- }
214
- }
215
- }
216
- catch {
217
- continue;
218
- }
147
+ return undefined;
219
148
  }
220
- return false;
221
149
  }
222
- /**
223
- * 根据生效策略统一判定目标动作是否允许访问。
224
- * 同时综合校验策略中的 packageAllowlist 与 actionAllowlist 约束。
225
- *
226
- * @param action 目标动作对象或引用字符串
227
- * @param policy 当前生效的服务端策略
228
- * @returns 是否允许访问
229
- */
150
+ /** 动作列表仅按全限定引用匹配;省略或空列表不会授予任何动作。 */
151
+ export function isActionAllowed(action, allowlist) {
152
+ const target = parsePolicyAction(action);
153
+ if (!target?.packageId)
154
+ return false;
155
+ return (allowlist || []).some((entry) => {
156
+ const rule = parsePolicyAction(entry);
157
+ return Boolean(rule?.packageId && rule.packageId === target.packageId && rule.actionId === target.actionId);
158
+ });
159
+ }
160
+ /** 整包授权与全限定单动作授权取并集;仅缺少策略对象的内部调用不执行过滤。 */
230
161
  export function isActionAllowedByPolicy(action, policy) {
231
- if (!policy) {
162
+ if (!policy)
232
163
  return true;
233
- }
234
- if (!action) {
164
+ const target = parsePolicyAction(action);
165
+ if (!target)
235
166
  return false;
236
- }
237
- let pkgId;
238
- let actId;
239
- try {
240
- if (typeof action === "string") {
241
- const parsed = parseActionRef(action);
242
- pkgId = parsed.packageId;
243
- actId = parsed.actionId;
244
- }
245
- else {
246
- pkgId = action.packageId;
247
- const rawId = action.actionId || action.id;
248
- if (!rawId)
249
- return false;
250
- if (rawId.includes("/")) {
251
- const parsed = parseActionRef(rawId);
252
- pkgId = parsed.packageId || pkgId;
253
- actId = parsed.actionId;
254
- }
255
- else {
256
- actId = rawId;
257
- }
258
- }
259
- }
260
- catch {
167
+ if (isFullAccessGranted(policy))
168
+ return true;
169
+ if (!target.packageId)
261
170
  return false;
262
- }
263
- // 1. 若配置了 packageAllowlist,校验所属包白名单
264
- if (policy.packageAllowlist && policy.packageAllowlist.length > 0) {
265
- if (pkgId) {
266
- if (!policy.packageAllowlist.includes(pkgId)) {
267
- return false;
268
- }
269
- }
270
- else {
271
- // 未指明所属包时,仅当其动作标识明确列入 actionAllowlist 时才允许放行
272
- if (!policy.actionAllowlist ||
273
- policy.actionAllowlist.length === 0 ||
274
- !isActionAllowed({ actionId: actId }, policy.actionAllowlist)) {
275
- return false;
276
- }
277
- }
278
- }
279
- // 2. 若配置了 actionAllowlist,校验动作白名单
280
- if (policy.actionAllowlist && policy.actionAllowlist.length > 0) {
281
- if (!isActionAllowed({ packageId: pkgId, actionId: actId }, policy.actionAllowlist)) {
282
- return false;
283
- }
284
- }
285
- return true;
171
+ return Boolean(policy.packageAllowlist?.includes(target.packageId) ||
172
+ isActionAllowed(target, policy.actionAllowlist));
286
173
  }
287
- /**
288
- * 根据生效策略统一判定目标包标识是否允许访问。
289
- *
290
- * @param packageId 目标包 ID
291
- * @param policy 当前生效的服务端策略
292
- * @returns 是否允许访问
293
- */
174
+ /** 整包能力授权,不从某个动作的授权推导配置、状态、规程或诊断权限。 */
294
175
  export function isPackageAllowedByPolicy(packageId, policy) {
295
- if (!policy?.packageAllowlist || policy.packageAllowlist.length === 0) {
176
+ if (!policy || isFullAccessGranted(policy))
296
177
  return true;
297
- }
298
- if (!packageId) {
178
+ return Boolean(packageId && policy.packageAllowlist?.includes(packageId));
179
+ }
180
+ /** 单动作授权的包可发现必要身份信息,但不因此获得整包能力。 */
181
+ export function isPackageVisibleByPolicy(packageId, policy) {
182
+ if (isPackageAllowedByPolicy(packageId, policy))
183
+ return true;
184
+ if (!packageId)
299
185
  return false;
300
- }
301
- return policy.packageAllowlist.includes(packageId);
186
+ return (policy?.actionAllowlist || []).some((entry) => parsePolicyAction(entry)?.packageId === packageId);
187
+ }
188
+ export function requiresMinimalPackageProjection(packageId, policy) {
189
+ return isPackageVisibleByPolicy(packageId, policy) && !isPackageAllowedByPolicy(packageId, policy);
302
190
  }
303
- /**
304
- * 根据生效策略统一判定配置与状态管理路由是否开启。
305
- *
306
- * @param policy 当前生效的服务端策略
307
- * @returns 是否允许管理操作
308
- */
309
191
  export function isManagementAllowedByPolicy(policy) {
310
192
  return policy?.enableManagement === true;
311
193
  }
312
- /**
313
- * 根据生效策略对动作列表进行统一过滤。
314
- *
315
- * @param actions 待过滤的动作列表
316
- * @param policy 当前生效的服务端策略
317
- * @returns 过滤后允许访问的动作列表
318
- */
319
194
  export function filterActionsByPolicy(actions, policy) {
320
- if (!policy) {
321
- return actions;
322
- }
323
- const hasPkg = policy.packageAllowlist && policy.packageAllowlist.length > 0;
324
- const hasAct = policy.actionAllowlist && policy.actionAllowlist.length > 0;
325
- if (!hasPkg && !hasAct) {
195
+ if (!policy)
326
196
  return actions;
327
- }
328
- return actions.filter((a) => isActionAllowedByPolicy({ packageId: a.packageId, actionId: a.actionId || a.id }, policy));
197
+ return actions.filter((action) => isActionAllowedByPolicy(action, policy));
329
198
  }
330
- /**
331
- * 根据生效策略对包列表进行统一过滤。
332
- *
333
- * @param packages 待过滤的包列表
334
- * @param policy 当前生效的服务端策略
335
- * @returns 过滤后允许访问的包列表
336
- */
337
199
  export function filterPackagesByPolicy(packages, policy) {
338
- if (!policy?.packageAllowlist || policy.packageAllowlist.length === 0) {
200
+ if (!policy)
339
201
  return packages;
340
- }
341
- return packages.filter((p) => p.id && policy.packageAllowlist.includes(p.id));
202
+ return packages.filter((pkg) => isPackageVisibleByPolicy(pkg.id, policy));
342
203
  }
343
- /**
344
- * 校验目标动作是否属于允许白名单,不符合则抛出 403 异常。
345
- *
346
- * @param action 目标动作对象或引用
347
- * @param policy 当前生效的服务端策略
348
- */
349
204
  export function assertActionAllowedByPolicy(action, policy) {
350
205
  if (!isActionAllowedByPolicy(action, policy)) {
351
- const actRef = typeof action === "string"
206
+ const ref = typeof action === "string"
352
207
  ? action
353
208
  : action.packageId
354
209
  ? `${action.packageId}/${action.actionId || action.id}`
355
210
  : action.actionId || action.id || "unknown";
356
- throw new ActionForbiddenError(actRef);
211
+ throw new ActionForbiddenError(ref);
357
212
  }
358
213
  }
359
- /**
360
- * 校验目标包是否属于允许白名单,不符合则抛出 403 异常。
361
- *
362
- * @param packageId 目标包 ID
363
- * @param policy 当前生效的服务端策略
364
- */
365
214
  export function assertPackageAllowedByPolicy(packageId, policy) {
366
- if (!isPackageAllowedByPolicy(packageId, policy)) {
215
+ if (!isPackageAllowedByPolicy(packageId, policy))
367
216
  throw new PackageNotAllowedError(packageId);
368
- }
369
217
  }
370
- /**
371
- * 构造受信任的服务端执行来源快照。
372
- *
373
- * 仅挑选 channel、viewName(默认 "default")与 authenticated(视图配置非空有效令牌)。
374
- * 严禁暴露令牌原文、片段或摘要。
375
- */
218
+ /** 只记录受信任的执行来源,不能包含令牌原文、片段或摘要。 */
376
219
  export function createServerRunOrigin(policy, channel) {
377
220
  return snapshotRunOrigin({
378
221
  channel,
@@ -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;