@opentrust/guards 7.3.9 → 7.3.11
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/agent/command-executor.ts +145 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/platform-client/index.ts +19 -0
- package/platform-client/types.ts +9 -0
- package/plugin/lifecycle.ts +34 -0
- package/plugin/state.ts +4 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { RemoteCommand } from "../platform-client/index.js";
|
|
6
|
+
import type { Logger } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export interface CommandResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
output?: string;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function executeCommand(cmd: RemoteCommand, log: Logger): CommandResult {
|
|
15
|
+
switch (cmd.type) {
|
|
16
|
+
case "install_skill":
|
|
17
|
+
return executeSkillInstall(cmd.payload, log);
|
|
18
|
+
case "install_custom_skill":
|
|
19
|
+
return executeCustomSkillInstall(cmd.payload, log);
|
|
20
|
+
case "uninstall_skill":
|
|
21
|
+
return executeSkillUninstall(cmd.payload, log);
|
|
22
|
+
case "update_config":
|
|
23
|
+
return executeUpdateConfig(cmd.payload, log);
|
|
24
|
+
default:
|
|
25
|
+
return { success: false, error: `Unknown command type: ${cmd.type}` };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function executeSkillInstall(payload: Record<string, unknown> | null, log: Logger): CommandResult {
|
|
30
|
+
const skillName = payload?.skillName as string;
|
|
31
|
+
if (!skillName) return { success: false, error: "Missing skillName in payload" };
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
log.info(`Command: installing skill "${skillName}"...`);
|
|
35
|
+
const output = execSync(`openclaw skills install ${skillName}`, {
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
timeout: 120_000,
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39
|
+
});
|
|
40
|
+
log.info(`Command: skill "${skillName}" installed`);
|
|
41
|
+
return { success: true, output: output.trim() };
|
|
42
|
+
} catch (err: any) {
|
|
43
|
+
const msg = err.stderr?.toString() || err.message || String(err);
|
|
44
|
+
log.warn(`Command: skill install failed — ${msg}`);
|
|
45
|
+
return { success: false, error: msg.slice(0, 500) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function executeCustomSkillInstall(payload: Record<string, unknown> | null, log: Logger): CommandResult {
|
|
50
|
+
const skillName = payload?.skillName as string;
|
|
51
|
+
const content = payload?.content as string;
|
|
52
|
+
if (!skillName || !content) {
|
|
53
|
+
return { success: false, error: "Missing skillName or content in payload" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const skillsDir = path.join(
|
|
58
|
+
process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw"),
|
|
59
|
+
"skills",
|
|
60
|
+
);
|
|
61
|
+
const skillDir = path.join(skillsDir, skillName);
|
|
62
|
+
|
|
63
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
const ext = detectContentFormat(content);
|
|
66
|
+
const fileName = `skill.${ext}`;
|
|
67
|
+
fs.writeFileSync(path.join(skillDir, fileName), content, "utf-8");
|
|
68
|
+
|
|
69
|
+
log.info(`Command: custom skill "${skillName}" written to ${skillDir}/${fileName}`);
|
|
70
|
+
|
|
71
|
+
// Try `openclaw skills install` from local path; fall back to file-only approach
|
|
72
|
+
try {
|
|
73
|
+
const output = execSync(`openclaw skills install ${skillDir}`, {
|
|
74
|
+
encoding: "utf-8",
|
|
75
|
+
timeout: 120_000,
|
|
76
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
77
|
+
});
|
|
78
|
+
return { success: true, output: output.trim() };
|
|
79
|
+
} catch {
|
|
80
|
+
return { success: true, output: `Custom skill "${skillName}" saved to ${skillDir}/${fileName}` };
|
|
81
|
+
}
|
|
82
|
+
} catch (err: any) {
|
|
83
|
+
const msg = err.message || String(err);
|
|
84
|
+
log.warn(`Command: custom skill install failed — ${msg}`);
|
|
85
|
+
return { success: false, error: msg.slice(0, 500) };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function detectContentFormat(content: string): string {
|
|
90
|
+
const trimmed = content.trim();
|
|
91
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
|
|
92
|
+
if (trimmed.startsWith("---") || /^\w+:/m.test(trimmed)) return "yaml";
|
|
93
|
+
return "md";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function executeSkillUninstall(payload: Record<string, unknown> | null, log: Logger): CommandResult {
|
|
97
|
+
const skillName = payload?.skillName as string;
|
|
98
|
+
if (!skillName) return { success: false, error: "Missing skillName in payload" };
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
log.info(`Command: uninstalling skill "${skillName}"...`);
|
|
102
|
+
const output = execSync(`openclaw skills uninstall ${skillName}`, {
|
|
103
|
+
encoding: "utf-8",
|
|
104
|
+
timeout: 60_000,
|
|
105
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
106
|
+
});
|
|
107
|
+
log.info(`Command: skill "${skillName}" uninstalled`);
|
|
108
|
+
return { success: true, output: output.trim() };
|
|
109
|
+
} catch (err: any) {
|
|
110
|
+
const msg = err.stderr?.toString() || err.message || String(err);
|
|
111
|
+
log.warn(`Command: skill uninstall failed — ${msg}`);
|
|
112
|
+
return { success: false, error: msg.slice(0, 500) };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function executeUpdateConfig(payload: Record<string, unknown> | null, log: Logger): CommandResult {
|
|
117
|
+
if (!payload || Object.keys(payload).length === 0) {
|
|
118
|
+
return { success: false, error: "Empty config payload" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const configDir = process.env.OPENCLAW_HOME || path.join(os.homedir(), ".openclaw");
|
|
123
|
+
const configFile = path.join(configDir, "openclaw.json");
|
|
124
|
+
|
|
125
|
+
if (!fs.existsSync(configFile)) {
|
|
126
|
+
return { success: false, error: `Config file not found: ${configFile}` };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const json = JSON.parse(fs.readFileSync(configFile, "utf-8"));
|
|
130
|
+
const entry = json?.plugins?.entries?.["opentrust-guard"];
|
|
131
|
+
if (!entry) {
|
|
132
|
+
return { success: false, error: "opentrust-guard plugin entry not found in openclaw.json" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
entry.config = { ...entry.config, ...payload };
|
|
136
|
+
fs.writeFileSync(configFile, JSON.stringify(json, null, 2) + "\n", "utf-8");
|
|
137
|
+
|
|
138
|
+
log.info(`Command: config updated — ${JSON.stringify(payload)}`);
|
|
139
|
+
return { success: true, output: `Config updated: ${Object.keys(payload).join(", ")}` };
|
|
140
|
+
} catch (err: any) {
|
|
141
|
+
const msg = err.message || String(err);
|
|
142
|
+
log.warn(`Command: config update failed — ${msg}`);
|
|
143
|
+
return { success: false, error: msg.slice(0, 500) };
|
|
144
|
+
}
|
|
145
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "opentrust-guard",
|
|
3
3
|
"name": "OpenTrust Guard",
|
|
4
4
|
"description": "AI security guard for OpenClaw agents: prompt injection detection, credential scanning, and behavioral monitoring.",
|
|
5
|
-
"version": "7.3.
|
|
5
|
+
"version": "7.3.11",
|
|
6
6
|
"configSchema": {
|
|
7
7
|
"type": "object",
|
|
8
8
|
"additionalProperties": false,
|
package/package.json
CHANGED
package/platform-client/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
AgentRegisterRequest,
|
|
19
19
|
ToolCallObservationRequest,
|
|
20
20
|
AgentPermission,
|
|
21
|
+
RemoteCommand,
|
|
21
22
|
} from "./types.js";
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -214,6 +215,23 @@ export class DashboardClient {
|
|
|
214
215
|
return result.data ?? [];
|
|
215
216
|
}
|
|
216
217
|
|
|
218
|
+
// ── 远程命令 API ───────────────────────────────────
|
|
219
|
+
|
|
220
|
+
async fetchPendingCommands(): Promise<RemoteCommand[]> {
|
|
221
|
+
if (!this.config.agentId) return [];
|
|
222
|
+
const result = await this.request<{ success: boolean; data: RemoteCommand[] }>(
|
|
223
|
+
`/api/commands/pending?agentId=${this.config.agentId}`,
|
|
224
|
+
);
|
|
225
|
+
return result.data ?? [];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async ackCommand(commandId: string, status: "running" | "completed" | "failed", result?: Record<string, unknown>): Promise<void> {
|
|
229
|
+
await this.request(`/api/commands/${commandId}/ack`, {
|
|
230
|
+
method: "PUT",
|
|
231
|
+
body: JSON.stringify({ status, result }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
217
235
|
// ── 健康检查 API ───────────────────────────────────
|
|
218
236
|
|
|
219
237
|
/**
|
|
@@ -238,4 +256,5 @@ export {
|
|
|
238
256
|
type DashboardDetectResponse,
|
|
239
257
|
type ToolCallObservationRequest,
|
|
240
258
|
type AgentPermission,
|
|
259
|
+
type RemoteCommand,
|
|
241
260
|
} from "./types.js";
|
package/platform-client/types.ts
CHANGED
|
@@ -108,6 +108,15 @@ export type ToolCallObservationRequest = {
|
|
|
108
108
|
* Agent 权限记录
|
|
109
109
|
* 表示 Agent 对某个资源的访问权限
|
|
110
110
|
*/
|
|
111
|
+
export type RemoteCommand = {
|
|
112
|
+
id: string;
|
|
113
|
+
agentId: string;
|
|
114
|
+
type: "install_skill" | "install_custom_skill" | "uninstall_skill" | "update_config";
|
|
115
|
+
payload: Record<string, unknown> | null;
|
|
116
|
+
status: string;
|
|
117
|
+
createdAt: string;
|
|
118
|
+
};
|
|
119
|
+
|
|
111
120
|
export type AgentPermission = {
|
|
112
121
|
/** 记录 ID */
|
|
113
122
|
id: string;
|
package/plugin/lifecycle.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { DashboardClient } from "../platform-client/index.js";
|
|
|
31
31
|
import { registerHooks } from "./hooks.js";
|
|
32
32
|
import { registerCommands } from "./commands.js";
|
|
33
33
|
import { resetState } from "./state.js";
|
|
34
|
+
import { executeCommand } from "../agent/command-executor.js";
|
|
34
35
|
import fs from "node:fs";
|
|
35
36
|
import os from "node:os";
|
|
36
37
|
import path from "node:path";
|
|
@@ -103,6 +104,36 @@ function startProfileSync(log: Logger, state: PluginState): void {
|
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
/**
|
|
108
|
+
* 启动命令轮询
|
|
109
|
+
* 每 10 秒从 Dashboard 拉取待执行命令并执行
|
|
110
|
+
*/
|
|
111
|
+
function startCommandPolling(log: Logger, state: PluginState): void {
|
|
112
|
+
if (state.commandPollTimer) return;
|
|
113
|
+
|
|
114
|
+
const poll = async () => {
|
|
115
|
+
if (!state.dashboardClient?.agentId) return;
|
|
116
|
+
try {
|
|
117
|
+
const cmds = await state.dashboardClient.fetchPendingCommands();
|
|
118
|
+
for (const cmd of cmds) {
|
|
119
|
+
await state.dashboardClient.ackCommand(cmd.id, "running").catch(() => {});
|
|
120
|
+
const result = executeCommand(cmd, log);
|
|
121
|
+
await state.dashboardClient.ackCommand(
|
|
122
|
+
cmd.id,
|
|
123
|
+
result.success ? "completed" : "failed",
|
|
124
|
+
{ output: result.output, error: result.error },
|
|
125
|
+
).catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Dashboard may be unreachable; silently retry next cycle
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
state.commandPollTimer = setInterval(poll, 10_000);
|
|
133
|
+
// Run once immediately
|
|
134
|
+
poll();
|
|
135
|
+
}
|
|
136
|
+
|
|
106
137
|
/**
|
|
107
138
|
* 插件注册入口
|
|
108
139
|
* 完成所有初始化工作:配置解析、检测器创建、凭证加载、钩子注册等
|
|
@@ -205,6 +236,9 @@ export function register(
|
|
|
205
236
|
|
|
206
237
|
// 启动心跳定时器(每分钟)
|
|
207
238
|
state.heartbeatTimer = state.dashboardClient.startHeartbeat(60_000);
|
|
239
|
+
|
|
240
|
+
// 启动命令轮询(每 10 秒)
|
|
241
|
+
startCommandPolling(log, state);
|
|
208
242
|
}
|
|
209
243
|
|
|
210
244
|
// 如果已有凭证,立即初始化 Dashboard 客户端
|
package/plugin/state.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface PluginState {
|
|
|
33
33
|
dashboardClient: DashboardClient | null;
|
|
34
34
|
heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
35
35
|
emailPollTimer: ReturnType<typeof setInterval> | null;
|
|
36
|
+
commandPollTimer: ReturnType<typeof setInterval> | null;
|
|
36
37
|
profileWatchers: ReturnType<typeof fs.watch>[];
|
|
37
38
|
profileDebounceTimer: ReturnType<typeof setTimeout> | null;
|
|
38
39
|
lastRegisterResult: RegisterResult | null;
|
|
@@ -49,6 +50,7 @@ export function createState(): PluginState {
|
|
|
49
50
|
dashboardClient: null,
|
|
50
51
|
heartbeatTimer: null,
|
|
51
52
|
emailPollTimer: null,
|
|
53
|
+
commandPollTimer: null,
|
|
52
54
|
profileWatchers: [],
|
|
53
55
|
profileDebounceTimer: null,
|
|
54
56
|
lastRegisterResult: null,
|
|
@@ -67,6 +69,8 @@ export function resetState(s: PluginState): void {
|
|
|
67
69
|
if (s.emailPollTimer) clearInterval(s.emailPollTimer);
|
|
68
70
|
// 清理心跳定时器
|
|
69
71
|
if (s.heartbeatTimer) clearInterval(s.heartbeatTimer);
|
|
72
|
+
// 清理命令轮询定时器
|
|
73
|
+
if (s.commandPollTimer) clearInterval(s.commandPollTimer);
|
|
70
74
|
// 清理 Profile 同步防抖定时器
|
|
71
75
|
if (s.profileDebounceTimer) clearTimeout(s.profileDebounceTimer);
|
|
72
76
|
// 关闭所有文件监视器
|