@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
|
@@ -3,7 +3,66 @@ import { isTerminalRunStatus } from "../../storage/types.js";
|
|
|
3
3
|
import { createAbortError } from "../../ipc/service.js";
|
|
4
4
|
import { parseDuration } from "../../utils/index.js";
|
|
5
5
|
import { readJsonBody } from "../body.js";
|
|
6
|
-
import {
|
|
6
|
+
import { parseActionRef } from "../../catalog/resolve-action.js";
|
|
7
|
+
import { getSubPath, isActionAllowedByPolicy, isFullAccessGranted, isPackageAllowedByPolicy, isPackageVisibleByPolicy, jsonResponse } from "./common.js";
|
|
8
|
+
/** 清理范围必须落在整包或精确动作授权内,全局清理必须显式拥有全部授权。 */
|
|
9
|
+
function resolveRunCleanupScope(packageId, actionId, policy) {
|
|
10
|
+
if (isFullAccessGranted(policy)) {
|
|
11
|
+
if (packageId && !isPackageVisibleByPolicy(packageId, policy)) {
|
|
12
|
+
return {
|
|
13
|
+
ok: false,
|
|
14
|
+
reason: `Package '${packageId}' is not in the allowed package list`,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return { ok: true, filter: { packageId, actionId } };
|
|
18
|
+
}
|
|
19
|
+
// 单动作清理必须解析出完整身份,避免鉴权与实际清理范围不一致。
|
|
20
|
+
if (actionId) {
|
|
21
|
+
let parsed;
|
|
22
|
+
try {
|
|
23
|
+
parsed = parseActionRef(actionId);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { ok: false, reason: `Invalid action reference '${actionId}'` };
|
|
27
|
+
}
|
|
28
|
+
// 全限定引用与显式 packageId 归一化后不一致必须拒绝,防止鉴权与过滤范围错位
|
|
29
|
+
if (parsed.packageId && packageId && parsed.packageId !== packageId) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: `Action reference '${parsed.packageId}/${parsed.actionId}' does not match the requested package scope '${packageId}'`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const targetPkgId = parsed.packageId || packageId;
|
|
36
|
+
if (!targetPkgId) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: "Restricted access policy requires a fully qualified 'packageId/actionId' action reference for run cleanup",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!isActionAllowedByPolicy({ packageId: targetPkgId, actionId: parsed.actionId }, policy)) {
|
|
43
|
+
return {
|
|
44
|
+
ok: false,
|
|
45
|
+
reason: `Action '${targetPkgId}/${parsed.actionId}' is not in the allowed action list`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
// 以归一化完整引用传递,确保底层过滤范围与鉴权引用一致且不扩大
|
|
49
|
+
return { ok: true, filter: { packageId: targetPkgId, actionId: parsed.actionId } };
|
|
50
|
+
}
|
|
51
|
+
// 无 actionId:必须有明确包范围,且该范围必须是整包授权
|
|
52
|
+
if (!packageId) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
reason: "Restricted access policy requires an explicit 'packageId' or fully qualified 'actionId' scope for run cleanup",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!isPackageAllowedByPolicy(packageId, policy)) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
reason: `Package '${packageId}' is not fully granted; run cleanup requires a fully granted package or an explicitly granted 'packageId/actionId' reference`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, filter: { packageId } };
|
|
65
|
+
}
|
|
7
66
|
/**
|
|
8
67
|
* 处理历史运行记录查询、清理、详情及 SSE 流式日志接口。
|
|
9
68
|
*/
|
|
@@ -54,7 +113,7 @@ export async function handleRunsRoutes(ctx) {
|
|
|
54
113
|
limit = Math.min(Math.max(parsedLimit, 1), 500);
|
|
55
114
|
}
|
|
56
115
|
}
|
|
57
|
-
if (packageId && !
|
|
116
|
+
if (packageId && !isPackageVisibleByPolicy(packageId, policy)) {
|
|
58
117
|
return jsonResponse({
|
|
59
118
|
ok: false,
|
|
60
119
|
error: {
|
|
@@ -63,7 +122,7 @@ export async function handleRunsRoutes(ctx) {
|
|
|
63
122
|
},
|
|
64
123
|
}, 403, corsHeaders);
|
|
65
124
|
}
|
|
66
|
-
if (actionId && !isActionAllowedByPolicy({ packageId, actionId }, policy)) {
|
|
125
|
+
if (actionId && (packageId || actionId.includes("/")) && !isActionAllowedByPolicy({ packageId, actionId }, policy)) {
|
|
67
126
|
return jsonResponse({
|
|
68
127
|
ok: false,
|
|
69
128
|
error: {
|
|
@@ -72,6 +131,15 @@ export async function handleRunsRoutes(ctx) {
|
|
|
72
131
|
},
|
|
73
132
|
}, 403, corsHeaders);
|
|
74
133
|
}
|
|
134
|
+
// 下推的是包可见性并集,仍须逐条校验动作,不能丢弃单动作授权包的记录。
|
|
135
|
+
const visiblePackageIds = new Set([
|
|
136
|
+
...(policy.packageAllowlist || []).filter((id) => id !== "*"),
|
|
137
|
+
...(policy.actionAllowlist || []).map((ref) => parseActionRef(ref).packageId),
|
|
138
|
+
]);
|
|
139
|
+
const backendPackageFilter = isFullAccessGranted(policy) ? undefined : [...visiblePackageIds];
|
|
140
|
+
if (backendPackageFilter?.length === 0) {
|
|
141
|
+
return jsonResponse({ ok: true, items: [], total: 0, limit, offset }, 200, corsHeaders);
|
|
142
|
+
}
|
|
75
143
|
const queryOptions = {
|
|
76
144
|
actionId,
|
|
77
145
|
status,
|
|
@@ -86,12 +154,9 @@ export async function handleRunsRoutes(ctx) {
|
|
|
86
154
|
intent,
|
|
87
155
|
requestIds: requestIds.length > 0 ? requestIds : undefined,
|
|
88
156
|
signal: req.signal,
|
|
89
|
-
packageAllowlist:
|
|
90
|
-
? policy.packageAllowlist
|
|
91
|
-
: undefined,
|
|
157
|
+
packageAllowlist: backendPackageFilter,
|
|
92
158
|
};
|
|
93
|
-
const hasPolicyFilter =
|
|
94
|
-
(policy.actionAllowlist && policy.actionAllowlist.length > 0));
|
|
159
|
+
const hasPolicyFilter = !isFullAccessGranted(policy);
|
|
95
160
|
let runs;
|
|
96
161
|
let total;
|
|
97
162
|
if (hasPolicyFilter) {
|
|
@@ -116,7 +181,9 @@ export async function handleRunsRoutes(ctx) {
|
|
|
116
181
|
break;
|
|
117
182
|
}
|
|
118
183
|
for (const run of batch) {
|
|
119
|
-
|
|
184
|
+
// 逐条以实际 packageId/actionId 完整引用判定授权:
|
|
185
|
+
// 包可见性拦无关包,动作授权保留单动作授权目标(如 B/read)
|
|
186
|
+
const packageAllowed = isPackageVisibleByPolicy(run.packageId, policy);
|
|
120
187
|
const actionAllowed = isActionAllowedByPolicy({ packageId: run.packageId, actionId: run.actionId }, policy);
|
|
121
188
|
if (packageAllowed && actionAllowed) {
|
|
122
189
|
if (matchedCount >= offset && pagedRuns.length < limit) {
|
|
@@ -218,20 +285,22 @@ export async function handleRunsRoutes(ctx) {
|
|
|
218
285
|
keep = parsed;
|
|
219
286
|
}
|
|
220
287
|
}
|
|
221
|
-
|
|
288
|
+
const scopeCheck = resolveRunCleanupScope(packageId, actionId, policy);
|
|
289
|
+
if (!scopeCheck.ok) {
|
|
222
290
|
return jsonResponse({
|
|
223
291
|
ok: false,
|
|
224
292
|
error: {
|
|
225
293
|
code: PACKAGE_FORBIDDEN,
|
|
226
|
-
message:
|
|
294
|
+
message: scopeCheck.reason,
|
|
227
295
|
},
|
|
228
296
|
}, 403, corsHeaders);
|
|
229
297
|
}
|
|
298
|
+
const clearedScope = scopeCheck.filter;
|
|
230
299
|
let clearedCount = 0;
|
|
231
300
|
if (service.runs.clear) {
|
|
232
301
|
clearedCount = await service.runs.clear({
|
|
233
|
-
packageId,
|
|
234
|
-
actionId,
|
|
302
|
+
packageId: clearedScope.packageId,
|
|
303
|
+
actionId: clearedScope.actionId,
|
|
235
304
|
status,
|
|
236
305
|
olderThanMs,
|
|
237
306
|
keep,
|
|
@@ -244,7 +313,17 @@ export async function handleRunsRoutes(ctx) {
|
|
|
244
313
|
}
|
|
245
314
|
}
|
|
246
315
|
// 2.1 Runs Clean Expired: POST /api/v2/runs/clean-expired, POST /runs/clean-expired
|
|
316
|
+
// 此操作无法按包或动作收窄范围,只允许显式的全部授权。
|
|
247
317
|
if (subpath === "/runs/clean-expired" && req.method === "POST") {
|
|
318
|
+
if (!isFullAccessGranted(policy)) {
|
|
319
|
+
return jsonResponse({
|
|
320
|
+
ok: false,
|
|
321
|
+
error: {
|
|
322
|
+
code: PACKAGE_FORBIDDEN,
|
|
323
|
+
message: "Expired run cleanup cannot be scoped under a restricted access policy",
|
|
324
|
+
},
|
|
325
|
+
}, 403, corsHeaders);
|
|
326
|
+
}
|
|
248
327
|
try {
|
|
249
328
|
let body = {};
|
|
250
329
|
if (req.headers.get("content-type")?.includes("json")) {
|
|
@@ -268,7 +347,7 @@ export async function handleRunsRoutes(ctx) {
|
|
|
268
347
|
if (!run) {
|
|
269
348
|
return jsonResponse({ ok: false, error: { code: RUN_NOT_FOUND, message: `Run '${runId}' not found` } }, 404, corsHeaders);
|
|
270
349
|
}
|
|
271
|
-
if (!
|
|
350
|
+
if (!isPackageVisibleByPolicy(run.packageId, policy)) {
|
|
272
351
|
return jsonResponse({
|
|
273
352
|
ok: false,
|
|
274
353
|
error: {
|
|
@@ -443,7 +522,7 @@ export async function handleRunsRoutes(ctx) {
|
|
|
443
522
|
},
|
|
444
523
|
}, 404, corsHeaders);
|
|
445
524
|
}
|
|
446
|
-
if (!
|
|
525
|
+
if (!isPackageVisibleByPolicy(run.packageId, policy)) {
|
|
447
526
|
return jsonResponse({
|
|
448
527
|
ok: false,
|
|
449
528
|
error: {
|
|
@@ -467,11 +546,10 @@ export async function handleRunsRoutes(ctx) {
|
|
|
467
546
|
const runCancelMatch = subpath.match(/^\/runs\/([^/]+)\/cancel$/);
|
|
468
547
|
if (runCancelMatch && req.method === "POST") {
|
|
469
548
|
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
470
|
-
if ((policy
|
|
471
|
-
(policy.actionAllowlist && policy.actionAllowlist.length > 0)) {
|
|
549
|
+
if (!isFullAccessGranted(policy)) {
|
|
472
550
|
const run = await service.runs.get(runId);
|
|
473
551
|
if (run) {
|
|
474
|
-
if (!
|
|
552
|
+
if (!isPackageVisibleByPolicy(run.packageId, policy)) {
|
|
475
553
|
return jsonResponse({
|
|
476
554
|
ok: false,
|
|
477
555
|
error: {
|
package/dist/server/server.js
CHANGED
|
@@ -3,8 +3,8 @@ import { NodeHttpServer } from "./http-server.js";
|
|
|
3
3
|
import { createActionDock } from "../service/factory.js";
|
|
4
4
|
import { ActionDockError, ACCESS_DENIED, NOT_FOUND, SERVICE_ERROR, UNAUTHORIZED } from "../errors.js";
|
|
5
5
|
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
6
|
-
import { findProjectRoot } from "../project/loader.js";
|
|
7
|
-
import {
|
|
6
|
+
import { findProjectRoot, loadProjectConfig } from "../project/loader.js";
|
|
7
|
+
import { resolvePackageRoot } from "../registry/registry.js";
|
|
8
8
|
import { handleActionsRoutes, handleConfigRoutes, handleDoctorRoute, handleHealthRoute, handleInfoRoute, handlePlaybooksRoutes, handleRunsRoutes, handleStateRoutes, jsonResponse, } from "./routes/index.js";
|
|
9
9
|
import { isLoopbackHost, resolveCorsHeaders, verifyBearerToken } from "./security.js";
|
|
10
10
|
import { createMcpEndpointHandler } from "./mcp-endpoint.js";
|
|
@@ -70,70 +70,42 @@ export async function startActionDockServer(arg1, arg2) {
|
|
|
70
70
|
throw new ActionDockError(ACCESS_DENIED, "Authentication token is required when binding to a non-loopback address. Use --allow-insecure-no-auth to override.");
|
|
71
71
|
}
|
|
72
72
|
let serviceInstance = options.service;
|
|
73
|
-
//
|
|
73
|
+
// 包授权与单动作引用都参与自建服务的装载;外部注入服务自行负责能力装配。
|
|
74
74
|
if (!serviceInstance) {
|
|
75
|
-
const
|
|
75
|
+
const declaredId = projectRoot ? loadProjectConfig(projectRoot).id : undefined;
|
|
76
|
+
const referencedPackages = new Set();
|
|
77
|
+
for (const view of views.values()) {
|
|
78
|
+
for (const pkgId of view.policy.packageAllowlist || []) {
|
|
79
|
+
if (pkgId !== "*")
|
|
80
|
+
referencedPackages.add(pkgId);
|
|
81
|
+
}
|
|
82
|
+
for (const action of view.policy.actionAllowlist || []) {
|
|
83
|
+
referencedPackages.add(parseActionRef(action).packageId);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const extraPackages = [];
|
|
87
|
+
for (const pkgId of referencedPackages) {
|
|
88
|
+
if (pkgId === declaredId)
|
|
89
|
+
continue;
|
|
90
|
+
const root = resolvePackageRoot(pkgId, projectRoot || undefined, customHome);
|
|
91
|
+
if (root && !extraPackages.some((pkg) => pkg.packageRoot === root)) {
|
|
92
|
+
extraPackages.push({ packageRoot: root });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
76
95
|
serviceInstance = await createActionDock({
|
|
77
96
|
projectRoot: projectRoot || undefined,
|
|
97
|
+
packages: extraPackages.length > 0 ? extraPackages : undefined,
|
|
78
98
|
customHome,
|
|
79
99
|
platform: options.platform,
|
|
80
100
|
dataDir: options.dataDir,
|
|
81
101
|
inMemory: options.inMemory,
|
|
82
|
-
scanLinkedPackages,
|
|
102
|
+
scanLinkedPackages: options.scanLinkedPackages ?? !projectRoot,
|
|
83
103
|
enableManagement: options.enableManagement,
|
|
84
104
|
});
|
|
85
105
|
}
|
|
86
106
|
if (!serviceInstance) {
|
|
87
107
|
throw new ActionDockError(SERVICE_ERROR, "Failed to initialize ActionDockService for server");
|
|
88
108
|
}
|
|
89
|
-
// 2. 聚合所有 views 声明的依赖包与动作,确保依赖闭包满足
|
|
90
|
-
const roots = [];
|
|
91
|
-
if (projectRoot) {
|
|
92
|
-
roots.push(projectRoot);
|
|
93
|
-
}
|
|
94
|
-
const aggregatedPackageAllowlist = new Set();
|
|
95
|
-
const aggregatedActionAllowlist = new Set();
|
|
96
|
-
for (const v of views.values()) {
|
|
97
|
-
if (v.policy.packageAllowlist) {
|
|
98
|
-
for (const pkg of v.policy.packageAllowlist) {
|
|
99
|
-
aggregatedPackageAllowlist.add(pkg);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (v.policy.actionAllowlist) {
|
|
103
|
-
for (const act of v.policy.actionAllowlist) {
|
|
104
|
-
aggregatedActionAllowlist.add(act);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (aggregatedPackageAllowlist.size > 0) {
|
|
109
|
-
for (const pkgId of aggregatedPackageAllowlist) {
|
|
110
|
-
const r = resolvePackageRoot(pkgId, projectRoot || undefined, customHome);
|
|
111
|
-
if (r && !roots.includes(r)) {
|
|
112
|
-
roots.push(r);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
else if (!projectRoot) {
|
|
117
|
-
for (const pkg of listLinkedPackages(customHome)) {
|
|
118
|
-
if (!roots.includes(pkg.path)) {
|
|
119
|
-
roots.push(pkg.path);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
if (aggregatedActionAllowlist.size > 0) {
|
|
124
|
-
for (const actRef of aggregatedActionAllowlist) {
|
|
125
|
-
try {
|
|
126
|
-
const parsed = parseActionRef(actRef);
|
|
127
|
-
if (parsed.packageId) {
|
|
128
|
-
const r = resolvePackageRoot(parsed.packageId, projectRoot || undefined, customHome);
|
|
129
|
-
if (r && !roots.includes(r)) {
|
|
130
|
-
roots.push(r);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch { }
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
109
|
// 3. 为每个启用 MCP 的视图独立装配 MCP 处理器
|
|
138
110
|
if (options.enableMcp !== false) {
|
|
139
111
|
for (const v of views.values()) {
|
package/dist/server/types.d.ts
CHANGED
|
@@ -26,9 +26,9 @@ export interface ServerViewOptions {
|
|
|
26
26
|
name?: string;
|
|
27
27
|
/** 该视图独立的 Bearer Token 鉴权令牌 */
|
|
28
28
|
token?: string;
|
|
29
|
-
/**
|
|
29
|
+
/** 整包授权列表,与动作授权取并集;空列表不授予权限,独立 * 显式授权全部 */
|
|
30
30
|
packageAllowlist?: string[];
|
|
31
|
-
/**
|
|
31
|
+
/** 全限定 packageId/actionId 精确动作授权列表;空列表不授予权限 */
|
|
32
32
|
actionAllowlist?: string[];
|
|
33
33
|
/** 该视图是否开启配置与状态管理路由能力(默认关闭) */
|
|
34
34
|
enableManagement?: boolean;
|
|
@@ -45,9 +45,9 @@ export interface EffectiveServerPolicy {
|
|
|
45
45
|
viewName?: string;
|
|
46
46
|
/** 当前生效的 Bearer Token 鉴权令牌 */
|
|
47
47
|
token?: string;
|
|
48
|
-
/**
|
|
48
|
+
/** 当前生效的整包授权列表,独立 * 代表全部 */
|
|
49
49
|
packageAllowlist?: string[];
|
|
50
|
-
/**
|
|
50
|
+
/** 当前生效的全限定精确动作授权列表,与整包授权取并集 */
|
|
51
51
|
actionAllowlist?: string[];
|
|
52
52
|
/** 当前生效的管理能力开关 */
|
|
53
53
|
enableManagement?: boolean;
|
|
@@ -80,9 +80,9 @@ export interface ServerOptions {
|
|
|
80
80
|
allowInsecureNoAuth?: boolean;
|
|
81
81
|
/** 允许跨域请求的 CORS Origin 白名单列表 */
|
|
82
82
|
corsOrigins?: string[];
|
|
83
|
-
/**
|
|
83
|
+
/** 整包授权列表,与动作授权取并集;空列表不授予权限,独立 * 显式授权全部 */
|
|
84
84
|
packageAllowlist?: string[];
|
|
85
|
-
/**
|
|
85
|
+
/** 全限定 packageId/actionId 精确动作授权列表;空列表不授予权限 */
|
|
86
86
|
actionAllowlist?: string[];
|
|
87
87
|
/** 最大允许的请求体字节限制(默认 1MB,防 DoS) */
|
|
88
88
|
maxBodyBytes?: number;
|
package/dist/service/factory.js
CHANGED
|
@@ -10,6 +10,7 @@ export async function createActionDock(options = {}) {
|
|
|
10
10
|
const inMemory = options.inMemory ?? options.runtimeOptions.inMemory;
|
|
11
11
|
const host = await createActionDockHost({
|
|
12
12
|
packages: [options.runtimeOptions],
|
|
13
|
+
actionBindings: options.actionBindings ?? options.hostOptions?.actionBindings,
|
|
13
14
|
autoLoadCurrentProject: false,
|
|
14
15
|
scanLinkedPackages: false,
|
|
15
16
|
inMemory,
|
package/dist/service/remote.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { parseActionRef } from "../catalog/resolve-action.js";
|
|
2
|
+
import { assertConcreteConfigKey } from "../project/config-declarations.js";
|
|
2
3
|
import { ActionDockError, ACTION_CANCELLED, CAPABILITY_UNAVAILABLE, INVALID_ARGUMENT, TIMEOUT, } from "../errors.js";
|
|
3
4
|
import { createAbortError } from "../ipc/service.js";
|
|
4
|
-
import { cancelRemoteRun, clearRemoteRuns, cleanExpiredRemoteRuns, clearRemoteState, deleteRemoteConfig, deleteRemoteStateKey, executeRemoteAction, fetchRemoteActionShow, fetchRemoteActions, fetchRemoteConfig, fetchRemoteInfo, fetchRemotePlaybookShow, fetchRemotePlaybooks, fetchRemoteRun, fetchRemoteRuns, fetchRemoteStateList, getRemoteStateKey, setRemoteConfig, setRemoteStateKey, } from "../client/index.js";
|
|
5
|
+
import { cancelRemoteRun, clearRemoteRuns, cleanExpiredRemoteRuns, clearRemoteState, deleteRemoteConfig, deleteRemoteStateKey, executeRemoteAction, fetchRemoteActionShow, fetchRemoteActions, fetchRemoteConfig, fetchRemoteConfigItem, fetchRemoteInfo, fetchRemotePlaybookShow, fetchRemotePlaybooks, fetchRemoteRun, fetchRemoteRuns, fetchRemoteStateList, getRemoteStateKey, setRemoteConfig, setRemoteStateKey, } from "../client/index.js";
|
|
5
6
|
import { isRemoteStateKeyNotFound, wrapRemoteError } from "./remote-errors.js";
|
|
6
7
|
import { formatTerminalRunResult, pollRunCompletion } from "./remote-polling.js";
|
|
7
8
|
import { streamRemoteEvents } from "./sse-stream.js";
|
|
@@ -449,19 +450,15 @@ export class RemoteActionDockService {
|
|
|
449
450
|
config: {
|
|
450
451
|
async get(packageId, key) {
|
|
451
452
|
self.assertNotClosed();
|
|
453
|
+
assertConcreteConfigKey(key);
|
|
452
454
|
try {
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
configured: false,
|
|
461
|
-
secret: false,
|
|
462
|
-
source: "default",
|
|
463
|
-
value: undefined,
|
|
464
|
-
};
|
|
455
|
+
const response = await fetchRemoteConfigItem(self.serverUrl, key, self.token, packageId || undefined, {
|
|
456
|
+
allowInsecureHttp: self.allowInsecureHttp,
|
|
457
|
+
insecure: self.insecure,
|
|
458
|
+
dispatcher: self.dispatcher,
|
|
459
|
+
});
|
|
460
|
+
// 严格单项读取已保证恰一项且键匹配,直接消费,不伪造未命中视图掩盖契约错误。
|
|
461
|
+
return response.items[0];
|
|
465
462
|
}
|
|
466
463
|
catch (err) {
|
|
467
464
|
wrapRemoteError(err);
|
|
@@ -469,6 +466,7 @@ export class RemoteActionDockService {
|
|
|
469
466
|
},
|
|
470
467
|
async set(packageId, key, value) {
|
|
471
468
|
self.assertNotClosed();
|
|
469
|
+
assertConcreteConfigKey(key);
|
|
472
470
|
try {
|
|
473
471
|
await setRemoteConfig(self.serverUrl, key, value, self.token, packageId || undefined, {
|
|
474
472
|
allowInsecureHttp: self.allowInsecureHttp,
|
|
@@ -482,6 +480,7 @@ export class RemoteActionDockService {
|
|
|
482
480
|
},
|
|
483
481
|
async delete(packageId, key) {
|
|
484
482
|
self.assertNotClosed();
|
|
483
|
+
assertConcreteConfigKey(key);
|
|
485
484
|
try {
|
|
486
485
|
const res = await deleteRemoteConfig(self.serverUrl, key, self.token, packageId || undefined, {
|
|
487
486
|
allowInsecureHttp: self.allowInsecureHttp,
|
|
@@ -502,22 +501,8 @@ export class RemoteActionDockService {
|
|
|
502
501
|
insecure: self.insecure,
|
|
503
502
|
dispatcher: self.dispatcher,
|
|
504
503
|
});
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (Array.isArray(res?.items))
|
|
508
|
-
return res.items;
|
|
509
|
-
if (Array.isArray(res?.config))
|
|
510
|
-
return res.config;
|
|
511
|
-
const values = res?.values || {};
|
|
512
|
-
const declared = res?.declared || {};
|
|
513
|
-
const defaultSource = packageId === "global" ? "global" : "package";
|
|
514
|
-
return Object.entries(values).map(([k, v]) => ({
|
|
515
|
-
key: k,
|
|
516
|
-
configured: v !== undefined,
|
|
517
|
-
secret: Boolean(declared[k]?.secret),
|
|
518
|
-
source: defaultSource,
|
|
519
|
-
value: v,
|
|
520
|
-
}));
|
|
504
|
+
// 归一化边界已强校验完整视图,直接消费,不再叠加兜底。
|
|
505
|
+
return res.items;
|
|
521
506
|
}
|
|
522
507
|
catch (err) {
|
|
523
508
|
wrapRemoteError(err);
|
package/dist/service/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { CancelResult, ExecutionTicket } from "../execution/types.js";
|
|
|
4
4
|
import type { ActionDockHostOptions } from "../host/types.js";
|
|
5
5
|
import type { RuntimePlatform } from "../platform/types.js";
|
|
6
6
|
import type { StateEntry } from "../storage/types.js";
|
|
7
|
+
import type { ActionBindings } from "../invocation/bindings.js";
|
|
7
8
|
import { PROTOCOL_UNSUPPORTED, SERVICE_RESULT_UNKNOWN, SERVICE_CLOSED } from "../errors.js";
|
|
8
9
|
/**
|
|
9
10
|
* 公开运行控制选项(RunOptions)。
|
|
@@ -29,6 +30,16 @@ export declare const ACTIONDOCK_PROTOCOL_VERSION = "2.0";
|
|
|
29
30
|
* 服务端/通信协议错误码常量(单一事实源引用自 errors.ts)。
|
|
30
31
|
*/
|
|
31
32
|
export { PROTOCOL_UNSUPPORTED, SERVICE_RESULT_UNKNOWN, SERVICE_CLOSED };
|
|
33
|
+
/**
|
|
34
|
+
* 配置来源契约(单一事实源)。
|
|
35
|
+
* 仅表达当前生效配置值的实际来源,不混入传输方式或作用域显示别名:
|
|
36
|
+
* override 为运行时固定覆盖或单次调用覆盖,package 为包级持久化,global 为全局持久化,
|
|
37
|
+
* env 为环境变量命中,default 为清单声明且真实生效的默认值。
|
|
38
|
+
* 无任何生效值时为 null,不得用 default 伪装。
|
|
39
|
+
*/
|
|
40
|
+
export declare const CONFIG_SOURCES: readonly ["override", "package", "global", "env", "default"];
|
|
41
|
+
/** 配置来源类型;由来源元组派生,枚举值单一事实源。 */
|
|
42
|
+
export type ConfigSource = (typeof CONFIG_SOURCES)[number];
|
|
32
43
|
/**
|
|
33
44
|
* 配置项安全视图契约。
|
|
34
45
|
* 屏蔽敏感配置原文,仅暴露是否已配置、是否为秘密及实际解析来源。
|
|
@@ -36,14 +47,16 @@ export { PROTOCOL_UNSUPPORTED, SERVICE_RESULT_UNKNOWN, SERVICE_CLOSED };
|
|
|
36
47
|
export interface ConfigValueView {
|
|
37
48
|
/** 配置项键名 */
|
|
38
49
|
key: string;
|
|
39
|
-
/**
|
|
50
|
+
/** 是否来自显式来源(override/package/global/env);default 与未配置(source null)均为 false */
|
|
40
51
|
configured: boolean;
|
|
41
|
-
/**
|
|
52
|
+
/** 声明含敏感字段或实际发生保守隐藏;不代表整个值不可见 */
|
|
42
53
|
secret: boolean;
|
|
43
|
-
/**
|
|
44
|
-
source:
|
|
45
|
-
/**
|
|
54
|
+
/** 配置值实际解析来源;无任何生效值时为 null */
|
|
55
|
+
source: ConfigSource | null;
|
|
56
|
+
/** 管理侧安全值;顶层敏感时不提供原值 */
|
|
46
57
|
value?: JsonValue;
|
|
58
|
+
/** 标准 JSON Pointer 隐藏路径;根为 "",未发生隐藏时为空 */
|
|
59
|
+
redactedPaths: string[];
|
|
47
60
|
}
|
|
48
61
|
/**
|
|
49
62
|
* 状态作用域与筛选控制选项。
|
|
@@ -254,6 +267,8 @@ export interface ActionDockService {
|
|
|
254
267
|
* 创建 ActionDock 本地服务选项。
|
|
255
268
|
*/
|
|
256
269
|
export interface CreateActionDockOptions {
|
|
270
|
+
/** 按调用方包覆盖运行时命名绑定,不改变动作引用与 uses 声明。 */
|
|
271
|
+
actionBindings?: Readonly<Record<string, ActionBindings>>;
|
|
257
272
|
/** 模式类型(可选) */
|
|
258
273
|
type?: "local";
|
|
259
274
|
/** 单包 PackageRuntime 初始化配置 */
|
package/dist/service/types.js
CHANGED
|
@@ -7,3 +7,11 @@ export const ACTIONDOCK_PROTOCOL_VERSION = "2.0";
|
|
|
7
7
|
* 服务端/通信协议错误码常量(单一事实源引用自 errors.ts)。
|
|
8
8
|
*/
|
|
9
9
|
export { PROTOCOL_UNSUPPORTED, SERVICE_RESULT_UNKNOWN, SERVICE_CLOSED };
|
|
10
|
+
/**
|
|
11
|
+
* 配置来源契约(单一事实源)。
|
|
12
|
+
* 仅表达当前生效配置值的实际来源,不混入传输方式或作用域显示别名:
|
|
13
|
+
* override 为运行时固定覆盖或单次调用覆盖,package 为包级持久化,global 为全局持久化,
|
|
14
|
+
* env 为环境变量命中,default 为清单声明且真实生效的默认值。
|
|
15
|
+
* 无任何生效值时为 null,不得用 default 伪装。
|
|
16
|
+
*/
|
|
17
|
+
export const CONFIG_SOURCES = ["override", "package", "global", "env", "default"];
|
package/dist/storage/mask.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
+
import type { JsonValue } from "@actiondock/sdk";
|
|
1
2
|
import type { ConfigItemDefinition } from "../project/types.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* 判定逻辑:
|
|
6
|
-
* 严格依据配置项元数据属性判定:若声明了 `secret: true`,直接判定为敏感数据;否则不视为敏感数据。
|
|
7
|
-
* 不进行基于正则或键名的自动探测,由开发者显式配置驱动。
|
|
4
|
+
* 检查指定配置项是否为敏感数据(顶层 secret: true 或模式中嵌套 secret: true)。
|
|
5
|
+
* 不进行基于正则或键名的自动探测,调用方若涉及模板应先解析具体声明项。
|
|
8
6
|
*
|
|
9
7
|
* @param _key 配置键名(保留入参以兼容契约)
|
|
10
8
|
* @param declaredItem 项目中声明的配置元数据(可选)
|
|
@@ -17,9 +15,38 @@ export declare function isSecretConfigKey(_key?: string, declaredItem?: ConfigIt
|
|
|
17
15
|
* @returns 掩码脱敏后的安全展示字符串
|
|
18
16
|
*/
|
|
19
17
|
export declare function maskSecretValue(value: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* 配置值脱敏安全视图产物。
|
|
20
|
+
*/
|
|
21
|
+
export interface RedactResult {
|
|
22
|
+
/** 脱敏后的数据值(顶层敏感时为 undefined) */
|
|
23
|
+
value: JsonValue | undefined;
|
|
24
|
+
/** 是否包含敏感信息或发生了保守脱敏 */
|
|
25
|
+
secret: boolean;
|
|
26
|
+
/** 发生脱敏替换的字段路径集合(标准 JSON Pointer 格式) */
|
|
27
|
+
redactedPaths: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 依据单项配置声明对配置值执行纯函数脱敏处理。
|
|
31
|
+
*
|
|
32
|
+
* @param value 待脱敏的配置原始数据值
|
|
33
|
+
* @param item 对应的单项配置定义元数据
|
|
34
|
+
*/
|
|
35
|
+
export declare function redactConfigValue(value: unknown, item?: ConfigItemDefinition): RedactResult;
|
|
36
|
+
/** 根据可信隐藏路径应用幂等掩码,绝不把远端提供的路径本身当成已脱敏的证据。 */
|
|
37
|
+
export declare function redactConfigValueAtPaths(value: unknown, paths: string[]): RedactResult;
|
|
38
|
+
/**
|
|
39
|
+
* 依据跨包多重声明对配置值执行合并脱敏处理(取脱敏路径并集,祖先路径覆盖子路径)。
|
|
40
|
+
*
|
|
41
|
+
* @param value 待脱敏的配置原始数据值
|
|
42
|
+
* @param items 各相关包分别解析得到的配置声明集合
|
|
43
|
+
*/
|
|
44
|
+
export declare function redactConfigValueForDeclarations(value: unknown, items: ConfigItemDefinition[]): RedactResult;
|
|
20
45
|
/**
|
|
21
46
|
* 对配置定义字典进行脱敏处理:
|
|
22
|
-
*
|
|
47
|
+
* - 针对外层 secret: true 或嵌套包含敏感字段的项,剥离其 default 默认值,避免敏感默认值泄露或误用于执行;
|
|
48
|
+
* - 针对模式中敏感节点的 enum 约束列表进行清洗,杜绝约束文本泄露秘密;
|
|
49
|
+
* - 仅返回安全副本,严禁修改清单原始数据。
|
|
23
50
|
*
|
|
24
51
|
* @param config 声明的配置定义字典
|
|
25
52
|
*/
|