@carfiedli/runtime-guardrail 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fe.md +256 -0
- package/README.hooks-security.md +1017 -0
- package/README.md +1316 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/persistence/file-store.d.ts +18 -0
- package/dist/adapters/persistence/index.d.ts +4 -0
- package/dist/adapters/persistence/json-event-log.d.ts +31 -0
- package/dist/adapters/persistence/queue-store.d.ts +19 -0
- package/dist/adapters/persistence/snapshot-store.d.ts +14 -0
- package/dist/approval/approval-service.d.ts +27 -0
- package/dist/approval/approval-state-machine.d.ts +5 -0
- package/dist/approval/hitl/hitl-connector.d.ts +9 -0
- package/dist/approval/index.d.ts +4 -0
- package/dist/approval/run-hold-service.d.ts +16 -0
- package/dist/audit/audit-event-store.d.ts +12 -0
- package/dist/audit/audit-read-model-builder.d.ts +17 -0
- package/dist/audit/audit-service.d.ts +18 -0
- package/dist/audit/incident-query-service.d.ts +7 -0
- package/dist/audit/index.d.ts +5 -0
- package/dist/audit/metrics-projection.d.ts +10 -0
- package/dist/bootstrap/create-runtime-guardrail-plugin.d.ts +3 -0
- package/dist/bootstrap/dependency-container.d.ts +2 -0
- package/dist/bootstrap/index.d.ts +3 -0
- package/dist/bootstrap/runtime-facade.d.ts +31 -0
- package/dist/compat/index.d.ts +1 -0
- package/dist/compat/legacy-types.d.ts +29 -0
- package/dist/contracts/core.d.ts +277 -0
- package/dist/contracts/events.d.ts +35 -0
- package/dist/contracts/host.d.ts +239 -0
- package/dist/contracts/index.d.ts +6 -0
- package/dist/contracts/operator.d.ts +110 -0
- package/dist/execution/egress-mediator.d.ts +7 -0
- package/dist/execution/execution-broker.d.ts +13 -0
- package/dist/execution/execution-plan-builder.d.ts +12 -0
- package/dist/execution/index.d.ts +4 -0
- package/dist/execution/model-governance-service.d.ts +7 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +23 -0
- package/dist/openclaw/hooks/egress-adapter.d.ts +9 -0
- package/dist/openclaw/hooks/hook-registry.d.ts +21 -0
- package/dist/openclaw/hooks/hook-result-mapper.d.ts +43 -0
- package/dist/openclaw/hooks/hook-types.d.ts +31 -0
- package/dist/openclaw/hooks/index.d.ts +8 -0
- package/dist/openclaw/hooks/ingress-adapter.d.ts +14 -0
- package/dist/openclaw/hooks/llm-request-adapter.d.ts +9 -0
- package/dist/openclaw/hooks/persist-adapter.d.ts +30 -0
- package/dist/openclaw/hooks/tool-call-adapter.d.ts +7 -0
- package/dist/openclaw/index.d.ts +4 -0
- package/dist/openclaw/plugin-runtime.d.ts +103 -0
- package/dist/openclaw/rpc-handlers.d.ts +20 -0
- package/dist/openclaw/skills-availability.d.ts +10 -0
- package/dist/openclaw/skills-upload.d.ts +17 -0
- package/dist/openclaw/testing/index.d.ts +1 -0
- package/dist/openclaw/testing/mock-openclaw-api.d.ts +74 -0
- package/dist/operator/cli/register-cli.d.ts +4 -0
- package/dist/operator/command-service.d.ts +15 -0
- package/dist/operator/index.d.ts +5 -0
- package/dist/operator/query-service.d.ts +21 -0
- package/dist/operator/reporting/report-service.d.ts +9 -0
- package/dist/operator/rpc/register-rpc.d.ts +5 -0
- package/dist/policy/detectors/detector-port.d.ts +23 -0
- package/dist/policy/finding-normalizer.d.ts +3 -0
- package/dist/policy/index.d.ts +4 -0
- package/dist/policy/policy-engine.d.ts +8 -0
- package/dist/policy/stage-resolver.d.ts +7 -0
- package/dist/runtime-core/device-id.d.ts +15 -0
- package/dist/runtime-core/evaluate-service.d.ts +91 -0
- package/dist/runtime-core/index.d.ts +10 -0
- package/dist/runtime-core/memory-audit-logger.d.ts +55 -0
- package/dist/runtime-core/memory-store.d.ts +141 -0
- package/dist/runtime-core/remote-guard-request-builder.d.ts +15 -0
- package/dist/runtime-core/remote-guard-transport.d.ts +79 -0
- package/dist/runtime-core/remote-guard-types.d.ts +183 -0
- package/dist/runtime-core/remote-policy-evaluator.d.ts +51 -0
- package/dist/runtime-core/skill-name-resolver.d.ts +31 -0
- package/dist/runtime-core/sync-remote-evaluate.d.ts +29 -0
- package/dist/runtime-core/sync-remote-worker.d.ts +14 -0
- package/dist/runtime-core/sync-remote-worker.js +2 -0
- package/dist/runtime-core/telemetry-service.d.ts +94 -0
- package/dist/runtime-core/telemetry-types.d.ts +181 -0
- package/dist/types.d.ts +224 -0
- package/dist/version.d.ts +1 -0
- package/openclaw.plugin.json +76 -0
- package/package.json +71 -0
- package/remote-guard-config.json +30 -0
- package/scripts/runtime-guardrailctl.mjs +864 -0
|
@@ -0,0 +1,864 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import JSON5 from "json5";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_PLUGIN_ID = "runtime-guardrail";
|
|
11
|
+
const DEFAULT_PLUGIN_SPEC = "@tencent/runtime-guardrail";
|
|
12
|
+
const DEFAULT_AUTH_FILE_NAME = "remote-guard-auth.json";
|
|
13
|
+
const API_KEY_ENV_NAME = "RUNTIME_GUARDRAIL_API_KEY";
|
|
14
|
+
const DEFAULT_OPENCLAW_STATE_DIR = path.join(os.homedir(), ".openclaw");
|
|
15
|
+
|
|
16
|
+
function printHelp() {
|
|
17
|
+
console.log(`runtime-guardrailctl
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
runtime-guardrailctl install [options]
|
|
21
|
+
runtime-guardrailctl update [options]
|
|
22
|
+
runtime-guardrailctl set-api-key [options]
|
|
23
|
+
runtime-guardrailctl reload [options]
|
|
24
|
+
runtime-guardrailctl sync [options] # reload 的兼容别名
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--plugin-spec <spec> 插件 npm spec,默认 ${DEFAULT_PLUGIN_SPEC}
|
|
28
|
+
--plugin-id <id> 插件 id,默认 ${DEFAULT_PLUGIN_ID}
|
|
29
|
+
--config-path <path> 自定义 remote-guard-config.json 路径(高级覆盖项)
|
|
30
|
+
--api-key <key> 直接设置 AI Guardrails 平台 API Key(注意避免 shell 历史泄露)
|
|
31
|
+
--openclaw-bin <bin> OpenClaw 可执行文件名/路径,默认 openclaw
|
|
32
|
+
--global 忽略 OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH,强制写入 ~/.openclaw
|
|
33
|
+
--state-dir <dir> 显式指定 OpenClaw state dir(优先级高于环境变量)
|
|
34
|
+
--no-restart 完成后不自动执行 gateway restart
|
|
35
|
+
-h, --help 查看帮助
|
|
36
|
+
`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fail(message, exitCode = 1) {
|
|
40
|
+
console.error(`[runtime-guardrailctl] ${message}`);
|
|
41
|
+
process.exit(exitCode);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveUserPath(input) {
|
|
45
|
+
const trimmed = input.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return trimmed;
|
|
48
|
+
}
|
|
49
|
+
if (trimmed === "~") {
|
|
50
|
+
return os.homedir();
|
|
51
|
+
}
|
|
52
|
+
if (trimmed.startsWith("~/")) {
|
|
53
|
+
return path.join(os.homedir(), trimmed.slice(2));
|
|
54
|
+
}
|
|
55
|
+
return path.resolve(trimmed);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveOpenClawStateDir(options = {}) {
|
|
59
|
+
if (options.global === true) {
|
|
60
|
+
return DEFAULT_OPENCLAW_STATE_DIR;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (options.stateDir?.trim()) {
|
|
64
|
+
return resolveUserPath(options.stateDir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const override = process.env.OPENCLAW_STATE_DIR?.trim();
|
|
68
|
+
return resolveUserPath(override || DEFAULT_OPENCLAW_STATE_DIR);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function resolveOpenClawConfigPath(options = {}, resolvedStateDir = resolveOpenClawStateDir(options)) {
|
|
72
|
+
if (options.global === true || options.stateDir?.trim()) {
|
|
73
|
+
return path.join(resolvedStateDir, "openclaw.json");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const override = process.env.OPENCLAW_CONFIG_PATH?.trim();
|
|
77
|
+
if (override) {
|
|
78
|
+
return resolveUserPath(override);
|
|
79
|
+
}
|
|
80
|
+
return path.join(resolvedStateDir, "openclaw.json");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveManagedConfigPath(explicitPath) {
|
|
84
|
+
if (!explicitPath?.trim()) {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
return resolveUserPath(explicitPath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveAuthConfigPath(pluginId, resolvedStateDir) {
|
|
91
|
+
return path.join(resolvedStateDir, pluginId, DEFAULT_AUTH_FILE_NAME);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildOpenClawCommandEnv(options, resolvedStateDir, openclawConfigPath) {
|
|
95
|
+
const nextEnv = { ...process.env };
|
|
96
|
+
|
|
97
|
+
if (options.global === true || options.stateDir?.trim()) {
|
|
98
|
+
nextEnv.OPENCLAW_STATE_DIR = resolvedStateDir;
|
|
99
|
+
nextEnv.OPENCLAW_CONFIG_PATH = openclawConfigPath;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return nextEnv;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function readConfigFile(configPath) {
|
|
106
|
+
if (!fs.existsSync(configPath)) {
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
110
|
+
if (!raw.trim()) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
return JSON5.parse(raw);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeJsonFile(targetPath, payload, options = {}) {
|
|
117
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
118
|
+
fs.writeFileSync(targetPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
119
|
+
if (typeof options.mode === "number") {
|
|
120
|
+
try {
|
|
121
|
+
fs.chmodSync(targetPath, options.mode);
|
|
122
|
+
} catch {
|
|
123
|
+
// best effort on cross-platform environments
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseArgs(argv) {
|
|
129
|
+
const args = [...argv];
|
|
130
|
+
const command = args.shift();
|
|
131
|
+
if (!command || command === "-h" || command === "--help" || command === "help") {
|
|
132
|
+
return { command: "help", options: {} };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const options = {
|
|
136
|
+
pluginSpec: DEFAULT_PLUGIN_SPEC,
|
|
137
|
+
pluginSpecExplicit: false,
|
|
138
|
+
pluginId: DEFAULT_PLUGIN_ID,
|
|
139
|
+
openclawBin: "openclaw",
|
|
140
|
+
restart: true,
|
|
141
|
+
global: false,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
while (args.length > 0) {
|
|
145
|
+
const token = args.shift();
|
|
146
|
+
switch (token) {
|
|
147
|
+
case "--plugin-spec":
|
|
148
|
+
options.pluginSpec = args.shift() || fail("--plugin-spec 需要值");
|
|
149
|
+
options.pluginSpecExplicit = true;
|
|
150
|
+
break;
|
|
151
|
+
case "--plugin-id":
|
|
152
|
+
options.pluginId = args.shift() || fail("--plugin-id 需要值");
|
|
153
|
+
break;
|
|
154
|
+
case "--config-path":
|
|
155
|
+
options.configPath = args.shift() || fail("--config-path 需要值");
|
|
156
|
+
break;
|
|
157
|
+
case "--api-key":
|
|
158
|
+
options.apiKey = args.shift() || fail("--api-key 需要值");
|
|
159
|
+
break;
|
|
160
|
+
case "--openclaw-bin":
|
|
161
|
+
options.openclawBin = args.shift() || fail("--openclaw-bin 需要值");
|
|
162
|
+
break;
|
|
163
|
+
case "--global":
|
|
164
|
+
options.global = true;
|
|
165
|
+
break;
|
|
166
|
+
case "--state-dir":
|
|
167
|
+
options.stateDir = args.shift() || fail("--state-dir 需要值");
|
|
168
|
+
break;
|
|
169
|
+
case "--no-restart":
|
|
170
|
+
options.restart = false;
|
|
171
|
+
break;
|
|
172
|
+
case "-h":
|
|
173
|
+
case "--help":
|
|
174
|
+
return { command: "help", options };
|
|
175
|
+
default:
|
|
176
|
+
fail(`未知参数: ${token}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.global && options.stateDir?.trim()) {
|
|
181
|
+
fail("--global 和 --state-dir 不能同时使用");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { command, options };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function ensurePluginAllowed(config, pluginId) {
|
|
188
|
+
const next = typeof config === "object" && config !== null ? { ...config } : {};
|
|
189
|
+
next.plugins = typeof next.plugins === "object" && next.plugins !== null ? { ...next.plugins } : {};
|
|
190
|
+
next.plugins.allow = Array.isArray(next.plugins.allow) ? [...next.plugins.allow] : [];
|
|
191
|
+
if (!next.plugins.allow.includes(pluginId)) {
|
|
192
|
+
next.plugins.allow.push(pluginId);
|
|
193
|
+
}
|
|
194
|
+
return next;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function ensurePluginEntry(config, params) {
|
|
198
|
+
const next = ensurePluginAllowed(config, params.pluginId);
|
|
199
|
+
next.plugins.entries =
|
|
200
|
+
typeof next.plugins.entries === "object" && next.plugins.entries !== null
|
|
201
|
+
? { ...next.plugins.entries }
|
|
202
|
+
: {};
|
|
203
|
+
|
|
204
|
+
const entry =
|
|
205
|
+
typeof next.plugins.entries[params.pluginId] === "object" && next.plugins.entries[params.pluginId] !== null
|
|
206
|
+
? { ...next.plugins.entries[params.pluginId] }
|
|
207
|
+
: {};
|
|
208
|
+
|
|
209
|
+
entry.enabled = true;
|
|
210
|
+
const currentConfig = typeof entry.config === "object" && entry.config !== null ? { ...entry.config } : {};
|
|
211
|
+
if (params.configPath) {
|
|
212
|
+
currentConfig.remoteGuard = {
|
|
213
|
+
...(typeof currentConfig.remoteGuard === "object" && currentConfig.remoteGuard !== null
|
|
214
|
+
? currentConfig.remoteGuard
|
|
215
|
+
: {}),
|
|
216
|
+
configPath: params.configPath,
|
|
217
|
+
};
|
|
218
|
+
} else {
|
|
219
|
+
delete currentConfig.remoteGuard;
|
|
220
|
+
}
|
|
221
|
+
delete currentConfig.configSync;
|
|
222
|
+
entry.config = currentConfig;
|
|
223
|
+
|
|
224
|
+
if (typeof entry.options === "object" && entry.options !== null) {
|
|
225
|
+
const nextOptions = { ...entry.options };
|
|
226
|
+
delete nextOptions.remoteGuard;
|
|
227
|
+
delete nextOptions.configSync;
|
|
228
|
+
entry.options = nextOptions;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
next.plugins.entries[params.pluginId] = entry;
|
|
232
|
+
return next;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveManagedPluginInstallPath(pluginId, resolvedStateDir) {
|
|
236
|
+
return path.join(resolvedStateDir, "extensions", pluginId);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function readJsonFileIfExists(filePath) {
|
|
240
|
+
if (!fs.existsSync(filePath)) {
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
246
|
+
if (!raw.trim()) {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
return JSON.parse(raw);
|
|
250
|
+
} catch {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function readInstalledPluginVersion(pluginId, resolvedStateDir) {
|
|
256
|
+
const installPath = resolveManagedPluginInstallPath(pluginId, resolvedStateDir);
|
|
257
|
+
const packageJson = readJsonFileIfExists(path.join(installPath, "package.json"));
|
|
258
|
+
if (typeof packageJson?.version === "string" && packageJson.version.trim()) {
|
|
259
|
+
return packageJson.version.trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const manifest = readJsonFileIfExists(path.join(installPath, "openclaw.plugin.json"));
|
|
263
|
+
if (typeof manifest?.version === "string" && manifest.version.trim()) {
|
|
264
|
+
return manifest.version.trim();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function ensurePluginTrackedInstallRecord(config, params) {
|
|
271
|
+
const next = ensurePluginEntry(config, params);
|
|
272
|
+
next.plugins.installs =
|
|
273
|
+
typeof next.plugins.installs === "object" && next.plugins.installs !== null
|
|
274
|
+
? { ...next.plugins.installs }
|
|
275
|
+
: {};
|
|
276
|
+
|
|
277
|
+
const installedVersion = readInstalledPluginVersion(params.pluginId, params.resolvedStateDir);
|
|
278
|
+
|
|
279
|
+
next.plugins.installs[params.pluginId] = {
|
|
280
|
+
...(typeof installedVersion === "string" && installedVersion ? { version: installedVersion } : {}),
|
|
281
|
+
installedAt: new Date().toISOString(),
|
|
282
|
+
source: "npm",
|
|
283
|
+
spec: params.pluginSpec,
|
|
284
|
+
installPath: resolveManagedPluginInstallPath(params.pluginId, params.resolvedStateDir),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return next;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function removeManagedPluginInstallDir(pluginId, resolvedStateDir) {
|
|
291
|
+
const installPath = resolveManagedPluginInstallPath(pluginId, resolvedStateDir);
|
|
292
|
+
if (!fs.existsSync(installPath)) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fs.rmSync(installPath, { recursive: true, force: true });
|
|
297
|
+
console.log(`[runtime-guardrailctl] 已移除插件目录 ${installPath}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function uninstallPluginForReinstall(params) {
|
|
301
|
+
const uninstallResult = runCommand(
|
|
302
|
+
params.openclawBin,
|
|
303
|
+
["plugins", "uninstall", params.pluginId, "--force"],
|
|
304
|
+
{ captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
|
|
305
|
+
);
|
|
306
|
+
if (uninstallResult.error) {
|
|
307
|
+
fail(`执行 ${params.openclawBin} plugins uninstall ${params.pluginId} 失败:${uninstallResult.error.message}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if ((uninstallResult.status ?? 1) === 0) {
|
|
311
|
+
echoCapturedOutput(uninstallResult);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.warn(
|
|
316
|
+
`[runtime-guardrailctl] 卸载旧版本返回非 0,将继续按目录重装方式更新 ${params.pluginId}。`,
|
|
317
|
+
);
|
|
318
|
+
echoCapturedOutput(uninstallResult);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function readPluginEntry(config, pluginId) {
|
|
322
|
+
return config?.plugins?.entries?.[pluginId] ?? {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function readStoredApiKey(authConfigPath) {
|
|
326
|
+
if (!fs.existsSync(authConfigPath)) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const raw = fs.readFileSync(authConfigPath, "utf-8");
|
|
331
|
+
const parsed = JSON.parse(raw);
|
|
332
|
+
if (typeof parsed?.auth?.key === "string" && parsed.auth.key.trim()) {
|
|
333
|
+
return parsed.auth.key.trim();
|
|
334
|
+
}
|
|
335
|
+
if (typeof parsed?.key === "string" && parsed.key.trim()) {
|
|
336
|
+
return parsed.key.trim();
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function maskApiKey(apiKey) {
|
|
345
|
+
if (!apiKey) {
|
|
346
|
+
return "<empty>";
|
|
347
|
+
}
|
|
348
|
+
if (apiKey.length <= 8) {
|
|
349
|
+
return `${apiKey.slice(0, 2)}***${apiKey.slice(-1)}`;
|
|
350
|
+
}
|
|
351
|
+
return `${apiKey.slice(0, 4)}***${apiKey.slice(-4)}`;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function promptForSecret(prompt) {
|
|
355
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
356
|
+
fail(`当前终端不可交互,请通过 --api-key 或环境变量 ${API_KEY_ENV_NAME} 提供 API Key。`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const rl = readline.createInterface({
|
|
360
|
+
input: process.stdin,
|
|
361
|
+
output: process.stdout,
|
|
362
|
+
terminal: true,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const answer = await new Promise((resolve) => {
|
|
367
|
+
rl.question(prompt, resolve);
|
|
368
|
+
});
|
|
369
|
+
return typeof answer === "string" ? answer.trim() : "";
|
|
370
|
+
} finally {
|
|
371
|
+
rl.close();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function resolveApiKey(params) {
|
|
376
|
+
const fromArg = params.apiKey?.trim();
|
|
377
|
+
if (fromArg) {
|
|
378
|
+
return fromArg;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const fromEnv = process.env[API_KEY_ENV_NAME]?.trim();
|
|
382
|
+
if (fromEnv) {
|
|
383
|
+
return fromEnv;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (params.allowStoredValue !== false) {
|
|
387
|
+
const stored = readStoredApiKey(params.authConfigPath);
|
|
388
|
+
if (stored) {
|
|
389
|
+
return stored;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const prompted = await promptForSecret("请输入 AI Guardrails 平台 API Key: ");
|
|
394
|
+
if (!prompted) {
|
|
395
|
+
fail("API Key 不能为空。");
|
|
396
|
+
}
|
|
397
|
+
return prompted;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function writeApiKey(authConfigPath, apiKey) {
|
|
401
|
+
writeJsonFile(
|
|
402
|
+
authConfigPath,
|
|
403
|
+
{
|
|
404
|
+
auth: {
|
|
405
|
+
key: apiKey,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
{ mode: 0o600 },
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function warnIfEnvApiKeyDiffers(storedApiKey) {
|
|
413
|
+
const envApiKey = process.env[API_KEY_ENV_NAME]?.trim();
|
|
414
|
+
if (!envApiKey || !storedApiKey || envApiKey === storedApiKey) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
console.warn(
|
|
419
|
+
`[runtime-guardrailctl] 检测到环境变量 ${API_KEY_ENV_NAME} (${maskApiKey(envApiKey)}) 与本地已保存的 API Key (${maskApiKey(storedApiKey)}) 不一致;本次 install/set-api-key 会优先取环境变量并写回本地文件,但插件运行时只会读取当前 OpenClaw state dir 下的 remote-guard-auth.json。若环境变量是旧值,请先清理该环境变量后再重新执行命令。`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const OPENCLAW_NPM_EXTRACT_FAILURE_RE =
|
|
424
|
+
/failed to extract archive:\s*SafeOpenError:\s*path is not a regular file under root/i;
|
|
425
|
+
const OPENCLAW_PLUGIN_ALREADY_EXISTS_RE = /plugin already exists\b/i;
|
|
426
|
+
|
|
427
|
+
function echoCapturedOutput(result) {
|
|
428
|
+
if (typeof result.stdout === "string" && result.stdout.length > 0) {
|
|
429
|
+
process.stdout.write(result.stdout);
|
|
430
|
+
}
|
|
431
|
+
if (typeof result.stderr === "string" && result.stderr.length > 0) {
|
|
432
|
+
process.stderr.write(result.stderr);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function readCombinedOutput(result) {
|
|
437
|
+
return `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function normalizePluginInstallResult(result, params, options = {}) {
|
|
441
|
+
if (result.error) {
|
|
442
|
+
fail(`执行 ${params.openclawBin} plugins install ${params.installTarget} 失败:${result.error.message}`);
|
|
443
|
+
}
|
|
444
|
+
if ((result.status ?? 1) === 0) {
|
|
445
|
+
echoCapturedOutput(result);
|
|
446
|
+
return { alreadyInstalled: false, needsArchiveFallback: false };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const combinedOutput = readCombinedOutput(result);
|
|
450
|
+
if (options.allowArchiveFallback && OPENCLAW_NPM_EXTRACT_FAILURE_RE.test(combinedOutput)) {
|
|
451
|
+
return { alreadyInstalled: false, needsArchiveFallback: true };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (OPENCLAW_PLUGIN_ALREADY_EXISTS_RE.test(combinedOutput)) {
|
|
455
|
+
console.log(
|
|
456
|
+
`[runtime-guardrailctl] 检测到插件 ${params.pluginId} 已存在,将跳过重复安装并继续执行 API Key 配置。若需升级插件,请改用 update 命令。`,
|
|
457
|
+
);
|
|
458
|
+
return { alreadyInstalled: true, needsArchiveFallback: false };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
echoCapturedOutput(result);
|
|
462
|
+
process.exit(result.status ?? 1);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function runCommand(bin, args, options = {}) {
|
|
466
|
+
const captureOutput = options.captureOutput === true;
|
|
467
|
+
const echoOutput = captureOutput && options.echoOutput !== false;
|
|
468
|
+
const baseOptions = {
|
|
469
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
470
|
+
...(options.env ? { env: options.env } : {}),
|
|
471
|
+
};
|
|
472
|
+
const result = spawnSync(bin, args, captureOutput
|
|
473
|
+
? { ...baseOptions, stdio: "pipe", encoding: "utf-8" }
|
|
474
|
+
: { ...baseOptions, stdio: "inherit" });
|
|
475
|
+
if (echoOutput) {
|
|
476
|
+
echoCapturedOutput(result);
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function runOrFail(bin, args, options = {}) {
|
|
482
|
+
const result = runCommand(bin, args, options);
|
|
483
|
+
if (result.error) {
|
|
484
|
+
fail(`执行 ${bin} ${args.join(" ")} 失败:${result.error.message}`);
|
|
485
|
+
}
|
|
486
|
+
if ((result.status ?? 1) !== 0) {
|
|
487
|
+
process.exit(result.status ?? 1);
|
|
488
|
+
}
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function tryRun(bin, args, options = {}) {
|
|
493
|
+
const result = runCommand(bin, args, options);
|
|
494
|
+
if (result.error) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
return (result.status ?? 1) === 0;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function parsePackedArchiveFilename(raw) {
|
|
501
|
+
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
502
|
+
if (!trimmed) {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
const candidates = [trimmed];
|
|
506
|
+
const arrayStart = trimmed.indexOf("[");
|
|
507
|
+
if (arrayStart > 0) {
|
|
508
|
+
candidates.push(trimmed.slice(arrayStart));
|
|
509
|
+
}
|
|
510
|
+
for (const candidate of candidates) {
|
|
511
|
+
try {
|
|
512
|
+
const parsed = JSON.parse(candidate);
|
|
513
|
+
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
|
514
|
+
for (let i = entries.length - 1; i >= 0; i -= 1) {
|
|
515
|
+
const entry = entries[i];
|
|
516
|
+
const filename = entry && typeof entry === "object" ? entry.filename : undefined;
|
|
517
|
+
if (typeof filename === "string" && filename.trim()) {
|
|
518
|
+
return filename.trim();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch {
|
|
522
|
+
// ignore parse failure and fallback to directory scan
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function findPackedArchiveInDir(dirPath) {
|
|
529
|
+
try {
|
|
530
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
531
|
+
const tgz = entries
|
|
532
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
|
|
533
|
+
.map((entry) => entry.name)
|
|
534
|
+
.sort();
|
|
535
|
+
return tgz[0];
|
|
536
|
+
} catch {
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function resolveLocalPluginSpecPath(pluginSpec) {
|
|
542
|
+
const trimmed = typeof pluginSpec === "string" ? pluginSpec.trim() : "";
|
|
543
|
+
if (!trimmed) {
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const normalized = trimmed.startsWith("file:") ? trimmed.slice("file:".length) : trimmed;
|
|
548
|
+
const looksLikePath =
|
|
549
|
+
normalized === "." ||
|
|
550
|
+
normalized === ".." ||
|
|
551
|
+
normalized.startsWith("./") ||
|
|
552
|
+
normalized.startsWith("../") ||
|
|
553
|
+
normalized.startsWith("~/") ||
|
|
554
|
+
normalized.startsWith("/") ||
|
|
555
|
+
normalized.endsWith(".tgz") ||
|
|
556
|
+
normalized.endsWith(".tar.gz");
|
|
557
|
+
if (!looksLikePath) {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const resolved = resolveUserPath(normalized);
|
|
562
|
+
if (!fs.existsSync(resolved)) {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
return resolved;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function extractArchiveToPackageDir(archivePath, extractDir) {
|
|
569
|
+
fs.mkdirSync(extractDir, { recursive: true });
|
|
570
|
+
runOrFail("tar", ["-xzf", archivePath, "-C", extractDir], { captureOutput: true });
|
|
571
|
+
|
|
572
|
+
const packageDir = path.join(extractDir, "package");
|
|
573
|
+
if (!fs.existsSync(path.join(packageDir, "package.json"))) {
|
|
574
|
+
fail(`安装失败:解压后未找到 package/package.json(${packageDir})。`);
|
|
575
|
+
}
|
|
576
|
+
return packageDir;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function installPluginFromDirectory(params, packageDir) {
|
|
580
|
+
const installResult = runCommand(
|
|
581
|
+
params.openclawBin,
|
|
582
|
+
["plugins", "install", packageDir],
|
|
583
|
+
{ captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
|
|
584
|
+
);
|
|
585
|
+
return normalizePluginInstallResult(installResult, {
|
|
586
|
+
...params,
|
|
587
|
+
installTarget: packageDir,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function installPluginFromPreparedSource(params) {
|
|
592
|
+
if (process.platform === "win32") {
|
|
593
|
+
fail(
|
|
594
|
+
"当前 OpenClaw 的 tgz 安装兼容模式未在 Windows 下启用。请先手动执行 npm pack 解压,再使用 openclaw plugins install <packageDir> 安装。",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const localSpecPath = resolveLocalPluginSpecPath(params.pluginSpec);
|
|
599
|
+
if (localSpecPath) {
|
|
600
|
+
const specStat = fs.statSync(localSpecPath);
|
|
601
|
+
if (specStat.isDirectory()) {
|
|
602
|
+
return installPluginFromDirectory(params, localSpecPath);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "runtime-guardrailctl-pack-"));
|
|
607
|
+
try {
|
|
608
|
+
let archivePath;
|
|
609
|
+
if (localSpecPath) {
|
|
610
|
+
const specStat = fs.statSync(localSpecPath);
|
|
611
|
+
if (!specStat.isFile()) {
|
|
612
|
+
fail(`安装失败:本地插件来源不是可用文件或目录(${localSpecPath})。`);
|
|
613
|
+
}
|
|
614
|
+
archivePath = localSpecPath;
|
|
615
|
+
console.log("[runtime-guardrailctl] 检测到本地 tgz 包,将先解压后按目录方式安装到 OpenClaw。");
|
|
616
|
+
} else {
|
|
617
|
+
console.log("[runtime-guardrailctl] 将使用预打包目录安装方式(npm pack + 本地目录)安装插件。");
|
|
618
|
+
const packResult = runOrFail(
|
|
619
|
+
"npm",
|
|
620
|
+
["pack", params.pluginSpec, "--ignore-scripts", "--json"],
|
|
621
|
+
{ captureOutput: true, echoOutput: false, cwd: tempRoot },
|
|
622
|
+
);
|
|
623
|
+
const packedName =
|
|
624
|
+
parsePackedArchiveFilename(packResult.stdout) ?? findPackedArchiveInDir(tempRoot);
|
|
625
|
+
if (!packedName) {
|
|
626
|
+
fail("安装失败:npm pack 成功但未找到生成的 .tgz 文件。");
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// npm pack --json 报告的 filename 可能带 scope 子目录(如 @tencent/runtime-guardrail-0.1.16.tgz),
|
|
630
|
+
// 但实际文件在 tempRoot 根目录下以展平命名存放(如 tencent-runtime-guardrail-0.1.16.tgz)。
|
|
631
|
+
// 因此先尝试 JSON 报告的路径,若不存在则回退到目录扫描。
|
|
632
|
+
const sourceArchivePath = path.resolve(
|
|
633
|
+
path.isAbsolute(packedName)
|
|
634
|
+
? packedName
|
|
635
|
+
: path.join(tempRoot, packedName),
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
if (fs.existsSync(sourceArchivePath)) {
|
|
639
|
+
archivePath = sourceArchivePath;
|
|
640
|
+
if (path.dirname(sourceArchivePath) !== tempRoot) {
|
|
641
|
+
archivePath = path.join(tempRoot, path.basename(sourceArchivePath));
|
|
642
|
+
fs.copyFileSync(sourceArchivePath, archivePath);
|
|
643
|
+
}
|
|
644
|
+
} else {
|
|
645
|
+
// JSON 报告的路径不存在,回退到扫描 tempRoot 下的实际 .tgz 文件
|
|
646
|
+
const fallbackName = findPackedArchiveInDir(tempRoot);
|
|
647
|
+
if (!fallbackName) {
|
|
648
|
+
fail(
|
|
649
|
+
`安装失败:npm pack 输出的文件路径不存在(${sourceArchivePath}),且在临时目录中未找到 .tgz 文件。`,
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
archivePath = path.join(tempRoot, fallbackName);
|
|
653
|
+
console.log(`[runtime-guardrailctl] npm pack 报告的路径与实际文件不一致,已回退到 ${fallbackName}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const extractDir = path.join(tempRoot, "extract");
|
|
658
|
+
const packageDir = extractArchiveToPackageDir(archivePath, extractDir);
|
|
659
|
+
return installPluginFromDirectory(params, packageDir);
|
|
660
|
+
} finally {
|
|
661
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function installPluginWithFallback(params) {
|
|
666
|
+
const localSpecPath = resolveLocalPluginSpecPath(params.pluginSpec);
|
|
667
|
+
if (process.platform !== "win32") {
|
|
668
|
+
return installPluginFromPreparedSource(params);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (localSpecPath && fs.statSync(localSpecPath).isDirectory()) {
|
|
672
|
+
return installPluginFromDirectory(params, localSpecPath);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const result = runCommand(
|
|
676
|
+
params.openclawBin,
|
|
677
|
+
["plugins", "install", params.pluginSpec],
|
|
678
|
+
{ captureOutput: true, echoOutput: false, env: params.openclawCommandEnv },
|
|
679
|
+
);
|
|
680
|
+
const normalizedResult = normalizePluginInstallResult(result, {
|
|
681
|
+
...params,
|
|
682
|
+
installTarget: params.pluginSpec,
|
|
683
|
+
}, { allowArchiveFallback: true });
|
|
684
|
+
if (normalizedResult.needsArchiveFallback) {
|
|
685
|
+
return installPluginFromPreparedSource(params);
|
|
686
|
+
}
|
|
687
|
+
return normalizedResult;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const OPENCLAW_INSTALL_STAGE_DIR_PREFIX = ".openclaw-install-stage-";
|
|
691
|
+
|
|
692
|
+
function cleanupOpenClawInstallStageDirs(resolvedStateDir) {
|
|
693
|
+
const extensionsDir = path.join(resolvedStateDir, "extensions");
|
|
694
|
+
if (!fs.existsSync(extensionsDir)) {
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const entries = fs.readdirSync(extensionsDir, { withFileTypes: true });
|
|
700
|
+
for (const entry of entries) {
|
|
701
|
+
if (!entry.isDirectory() || !entry.name.startsWith(OPENCLAW_INSTALL_STAGE_DIR_PREFIX)) {
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const targetPath = path.join(extensionsDir, entry.name);
|
|
706
|
+
fs.rmSync(targetPath, { recursive: true, force: true });
|
|
707
|
+
console.log(`[runtime-guardrailctl] 已清理 OpenClaw 安装阶段残留目录 ${targetPath}`);
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
711
|
+
console.warn(`[runtime-guardrailctl] 清理 OpenClaw 安装阶段残留目录失败:${message}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function refreshOrRestart(params) {
|
|
716
|
+
const refreshed = tryRun(params.openclawBin, [
|
|
717
|
+
"gateway",
|
|
718
|
+
"call",
|
|
719
|
+
"shield.config.refresh",
|
|
720
|
+
"--json",
|
|
721
|
+
], { env: params.openclawCommandEnv });
|
|
722
|
+
if (refreshed) {
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (params.restart) {
|
|
726
|
+
console.log("[runtime-guardrailctl] gateway call shield.config.refresh 未成功,将继续按 restart 策略处理。");
|
|
727
|
+
runOrFail(params.openclawBin, ["gateway", "restart"], { env: params.openclawCommandEnv });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
console.log("[runtime-guardrailctl] gateway call shield.config.refresh 未成功,且已按 --no-restart 跳过自动重启。");
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
async function main() {
|
|
734
|
+
const { command, options } = parseArgs(process.argv.slice(2));
|
|
735
|
+
if (command === "help") {
|
|
736
|
+
printHelp();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const normalizedCommand = command === "sync" ? "reload" : command;
|
|
741
|
+
if (!["install", "update", "set-api-key", "reload"].includes(normalizedCommand)) {
|
|
742
|
+
fail(`不支持的命令:${command}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const resolvedStateDir = resolveOpenClawStateDir(options);
|
|
746
|
+
const openclawConfigPath = resolveOpenClawConfigPath(options, resolvedStateDir);
|
|
747
|
+
const existingConfig = readConfigFile(openclawConfigPath);
|
|
748
|
+
const existingEntry = readPluginEntry(existingConfig, options.pluginId);
|
|
749
|
+
const mergedOptions = {
|
|
750
|
+
...options,
|
|
751
|
+
resolvedStateDir,
|
|
752
|
+
openclawConfigPath,
|
|
753
|
+
openclawCommandEnv: buildOpenClawCommandEnv(options, resolvedStateDir, openclawConfigPath),
|
|
754
|
+
configPath: resolveManagedConfigPath(
|
|
755
|
+
options.configPath || existingEntry?.config?.remoteGuard?.configPath,
|
|
756
|
+
),
|
|
757
|
+
authConfigPath: resolveAuthConfigPath(options.pluginId, resolvedStateDir),
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
console.log(`[runtime-guardrailctl] OpenClaw state dir: ${mergedOptions.resolvedStateDir}`);
|
|
761
|
+
console.log(`[runtime-guardrailctl] OpenClaw config path: ${mergedOptions.openclawConfigPath}`);
|
|
762
|
+
console.log(
|
|
763
|
+
`[runtime-guardrailctl] OpenClaw extensions dir: ${path.join(mergedOptions.resolvedStateDir, "extensions")}`,
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
if (normalizedCommand === "install") {
|
|
767
|
+
const apiKey = await resolveApiKey({
|
|
768
|
+
apiKey: mergedOptions.apiKey,
|
|
769
|
+
authConfigPath: mergedOptions.authConfigPath,
|
|
770
|
+
allowStoredValue: true,
|
|
771
|
+
});
|
|
772
|
+
writeApiKey(mergedOptions.authConfigPath, apiKey);
|
|
773
|
+
console.log(
|
|
774
|
+
`[runtime-guardrailctl] API Key 已写入 ${mergedOptions.authConfigPath} (${maskApiKey(apiKey)})`,
|
|
775
|
+
);
|
|
776
|
+
warnIfEnvApiKeyDiffers(apiKey);
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
installPluginWithFallback({
|
|
780
|
+
openclawBin: mergedOptions.openclawBin,
|
|
781
|
+
pluginId: mergedOptions.pluginId,
|
|
782
|
+
pluginSpec: mergedOptions.pluginSpec,
|
|
783
|
+
openclawCommandEnv: mergedOptions.openclawCommandEnv,
|
|
784
|
+
});
|
|
785
|
+
} finally {
|
|
786
|
+
cleanupOpenClawInstallStageDirs(mergedOptions.resolvedStateDir);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const installedConfig = readConfigFile(openclawConfigPath);
|
|
790
|
+
const nextConfig = ensurePluginTrackedInstallRecord(installedConfig, mergedOptions);
|
|
791
|
+
writeJsonFile(openclawConfigPath, nextConfig);
|
|
792
|
+
console.log(`[runtime-guardrailctl] OpenClaw 配置已写入 ${openclawConfigPath}`);
|
|
793
|
+
|
|
794
|
+
if (mergedOptions.restart) {
|
|
795
|
+
runOrFail(mergedOptions.openclawBin, ["gateway", "restart"], { env: mergedOptions.openclawCommandEnv });
|
|
796
|
+
}
|
|
797
|
+
} else if (normalizedCommand === "update") {
|
|
798
|
+
console.log(
|
|
799
|
+
`[runtime-guardrailctl] 将按“卸载旧版本 + 重装 ${mergedOptions.pluginSpec}”方式更新 ${mergedOptions.pluginId}。`,
|
|
800
|
+
);
|
|
801
|
+
uninstallPluginForReinstall(mergedOptions);
|
|
802
|
+
removeManagedPluginInstallDir(mergedOptions.pluginId, mergedOptions.resolvedStateDir);
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
installPluginWithFallback({
|
|
806
|
+
openclawBin: mergedOptions.openclawBin,
|
|
807
|
+
pluginId: mergedOptions.pluginId,
|
|
808
|
+
pluginSpec: mergedOptions.pluginSpec,
|
|
809
|
+
openclawCommandEnv: mergedOptions.openclawCommandEnv,
|
|
810
|
+
});
|
|
811
|
+
} finally {
|
|
812
|
+
cleanupOpenClawInstallStageDirs(mergedOptions.resolvedStateDir);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const currentConfig = readConfigFile(openclawConfigPath);
|
|
816
|
+
const nextConfig = ensurePluginTrackedInstallRecord(currentConfig, mergedOptions);
|
|
817
|
+
writeJsonFile(openclawConfigPath, nextConfig);
|
|
818
|
+
console.log(
|
|
819
|
+
`[runtime-guardrailctl] 已为 ${mergedOptions.pluginId} 写入可追踪的安装记录(${mergedOptions.pluginSpec})。`,
|
|
820
|
+
);
|
|
821
|
+
console.log(`[runtime-guardrailctl] OpenClaw 配置已写入 ${openclawConfigPath}`);
|
|
822
|
+
|
|
823
|
+
const installedVersion = readInstalledPluginVersion(mergedOptions.pluginId, mergedOptions.resolvedStateDir);
|
|
824
|
+
if (installedVersion) {
|
|
825
|
+
console.log(`[runtime-guardrailctl] 当前已安装版本:${installedVersion}`);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const existingApiKey = readStoredApiKey(mergedOptions.authConfigPath);
|
|
829
|
+
if (!existingApiKey) {
|
|
830
|
+
console.log(
|
|
831
|
+
`[runtime-guardrailctl] 尚未检测到 API Key,请执行 runtime-guardrailctl set-api-key 完成本地配置。`,
|
|
832
|
+
);
|
|
833
|
+
} else {
|
|
834
|
+
warnIfEnvApiKeyDiffers(existingApiKey);
|
|
835
|
+
}
|
|
836
|
+
if (mergedOptions.restart) {
|
|
837
|
+
runOrFail(mergedOptions.openclawBin, ["gateway", "restart"], { env: mergedOptions.openclawCommandEnv });
|
|
838
|
+
}
|
|
839
|
+
} else if (normalizedCommand === "set-api-key") {
|
|
840
|
+
const apiKey = await resolveApiKey({
|
|
841
|
+
apiKey: mergedOptions.apiKey,
|
|
842
|
+
authConfigPath: mergedOptions.authConfigPath,
|
|
843
|
+
allowStoredValue: false,
|
|
844
|
+
});
|
|
845
|
+
writeApiKey(mergedOptions.authConfigPath, apiKey);
|
|
846
|
+
console.log(
|
|
847
|
+
`[runtime-guardrailctl] API Key 已写入 ${mergedOptions.authConfigPath} (${maskApiKey(apiKey)})`,
|
|
848
|
+
);
|
|
849
|
+
warnIfEnvApiKeyDiffers(apiKey);
|
|
850
|
+
refreshOrRestart(mergedOptions);
|
|
851
|
+
} else if (normalizedCommand === "reload") {
|
|
852
|
+
refreshOrRestart(mergedOptions);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
console.log(`[runtime-guardrailctl] ${normalizedCommand} 完成。`);
|
|
856
|
+
if (mergedOptions.configPath) {
|
|
857
|
+
console.log(`[runtime-guardrailctl] 自定义配置路径:${mergedOptions.configPath}`);
|
|
858
|
+
} else {
|
|
859
|
+
console.log("[runtime-guardrailctl] 当前使用插件随包发布的静态基础配置。");
|
|
860
|
+
}
|
|
861
|
+
console.log(`[runtime-guardrailctl] API Key 配置路径:${mergedOptions.authConfigPath}`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
await main();
|