squad-openclaw 2026.2.2001 → 2026.2.2003
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.md +21 -3
- package/dist/index.js +152 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity regis
|
|
|
10
10
|
| `fs_read`, `fs_write`, `fs_list`, `fs_delete`, `fs_rename`, `fs_mkdir` | Remote filesystem access for browser clients (subject to security restrictions below) |
|
|
11
11
|
| `sql_query` | Restricted SQLite query tool — `sqlite3` only, scoped to `~/.openclaw/squad-ceo-data/` |
|
|
12
12
|
| `squad.version.check`, `squad.version.update` | Plugin version management and self-update |
|
|
13
|
-
| `tools.invoke` | RPC-based tool invocation for relay mode (
|
|
13
|
+
| `tools.invoke` | RPC-based tool invocation for relay mode — **only invokes this plugin's own tools**, each with its own security restrictions (see below) |
|
|
14
14
|
| Cloud relay client | **Disabled by default (opt-in).** Connects outbound to `relay.squad.ceo` for remote browser access |
|
|
15
15
|
|
|
16
16
|
## Security Model
|
|
@@ -112,13 +112,31 @@ Browser ──[connect request]──> relay.squad.ceo ──[relay.forward]─
|
|
|
112
112
|
|
|
113
113
|
The relay server only sees the outer `relay.forward` envelope. It **never** receives the modified request containing the token. The token injection happens entirely within the relay-client process, and the modified message is sent over a **local loopback** connection to the gateway. A compromised relay server cannot intercept the operator token because it never traverses the relay — it only exists on the `localhost:18789` path.
|
|
114
114
|
|
|
115
|
+
## Remote Tool Invocation (`tools.invoke`)
|
|
116
|
+
|
|
117
|
+
The `tools.invoke` gateway method allows the browser to call plugin tools over WebSocket (used in relay mode where there is no HTTP path). This is **not** a generic RPC gateway — it is scoped exclusively to the tools registered by **this plugin**:
|
|
118
|
+
|
|
119
|
+
| Tool | What it can access | Restrictions |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `fs_read`, `fs_write`, `fs_list`, `fs_delete`, `fs_rename`, `fs_mkdir` | `~/.openclaw/` only | All 4 security layers apply (blocked dirs, blocked files, redaction, allowed roots, write protection) |
|
|
122
|
+
| `sql_query` | `~/.openclaw/squad-ceo-data/*.db` only | sqlite3 only, no shell, no command injection (see below) |
|
|
123
|
+
| `entity_list`, `entity_search`, `entity_sync` | In-memory entity index | Read-only metadata (names, types, paths) |
|
|
124
|
+
| `squad.version.check`, `squad.version.update` | npm registry | Read-only check + controlled `npm install` |
|
|
125
|
+
|
|
126
|
+
It **cannot** invoke gateway core tools (`exec`, `bash`, `read`, `write`, `web_fetch`, etc.) — only the tools this plugin registers via `api.registerTool()`. Every invoked tool enforces its own security restrictions independently — `tools.invoke` is just a transport layer, not a privilege escalation.
|
|
127
|
+
|
|
128
|
+
**Authentication chain for relay access:** Browser JWT → relay claim token → operator-approved device pairing → operator auth token (localhost only). All four must be valid for a `tools.invoke` call to reach the gateway.
|
|
129
|
+
|
|
115
130
|
## SQL Query Tool
|
|
116
131
|
|
|
132
|
+
> **`sql_query` can only access the plugin's own application data** in `~/.openclaw/squad-ceo-data/`. It cannot read or modify any other files on the system — not system databases, not user documents, not gateway configuration.
|
|
133
|
+
|
|
117
134
|
The `sql_query` tool provides restricted SQLite access:
|
|
118
135
|
|
|
119
|
-
- **Path restriction:** Database files must be within `~/.openclaw/squad-ceo-data/`
|
|
136
|
+
- **Path restriction:** Database files must be within `~/.openclaw/squad-ceo-data/` — the plugin's own data directory containing entity registries and application state. Paths outside this directory are rejected before any query is executed.
|
|
120
137
|
- **No shell:** Uses `execFile` (not `exec`) — arguments are passed as an argv array, preventing command injection
|
|
121
|
-
- **No arbitrary commands:** Only `sqlite3` is executed
|
|
138
|
+
- **No arbitrary commands:** Only `sqlite3` is executed — no other binary can be invoked through this tool
|
|
139
|
+
- **Data scope:** The databases in `squad-ceo-data/` contain only Squad application data (entity metadata, search indexes, user preferences). No credentials, tokens, or gateway configuration is stored in these databases.
|
|
122
140
|
|
|
123
141
|
## Build Transparency
|
|
124
142
|
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,107 @@
|
|
|
1
|
+
// src/agents.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
function registerAgentMethods(api) {
|
|
4
|
+
api.registerGatewayMethod(
|
|
5
|
+
"squad.agents.add",
|
|
6
|
+
async ({ params, respond }) => {
|
|
7
|
+
const name = params?.name;
|
|
8
|
+
const model = params?.model;
|
|
9
|
+
if (!name || typeof name !== "string" || !name.trim()) {
|
|
10
|
+
respond(false, { error: "Missing or empty 'name' parameter" });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const safeName = name.trim();
|
|
14
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/.test(safeName)) {
|
|
15
|
+
respond(false, { error: "Agent name must start with a letter/number and contain only letters, numbers, spaces, hyphens, or underscores" });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
let cmd = `openclaw agents add ${JSON.stringify(safeName)} --non-interactive`;
|
|
20
|
+
if (model) {
|
|
21
|
+
cmd += ` --model ${JSON.stringify(model)}`;
|
|
22
|
+
}
|
|
23
|
+
const output = execSync(cmd, {
|
|
24
|
+
timeout: 3e4,
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
27
|
+
});
|
|
28
|
+
respond(true, { ok: true, output: output.slice(0, 1e3) });
|
|
29
|
+
} catch (err2) {
|
|
30
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
31
|
+
const stderr = err2?.stderr;
|
|
32
|
+
respond(false, {
|
|
33
|
+
error: `Failed to add agent: ${stderr || msg}`.slice(0, 500)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
api.registerGatewayMethod(
|
|
39
|
+
"squad.agents.delete",
|
|
40
|
+
async ({ params, respond }) => {
|
|
41
|
+
const agentId = params?.agentId;
|
|
42
|
+
if (!agentId || typeof agentId !== "string" || !agentId.trim()) {
|
|
43
|
+
respond(false, { error: "Missing or empty 'agentId' parameter" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (agentId === "main") {
|
|
47
|
+
respond(false, { error: "Cannot delete the main agent" });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(agentId)) {
|
|
51
|
+
respond(false, { error: "Invalid agent ID format" });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const output = execSync(
|
|
56
|
+
`openclaw agents delete ${JSON.stringify(agentId)} --non-interactive 2>&1`,
|
|
57
|
+
{ timeout: 3e4, encoding: "utf-8" }
|
|
58
|
+
);
|
|
59
|
+
respond(true, { ok: true, output: output.slice(0, 1e3) });
|
|
60
|
+
} catch (err2) {
|
|
61
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
62
|
+
const stderr = err2?.stderr;
|
|
63
|
+
respond(false, {
|
|
64
|
+
error: `Failed to delete agent: ${stderr || msg}`.slice(0, 500)
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
api.registerGatewayMethod(
|
|
70
|
+
"squad.agents.set-identity",
|
|
71
|
+
async ({ params, respond }) => {
|
|
72
|
+
const agentId = params?.agentId;
|
|
73
|
+
const name = params?.name;
|
|
74
|
+
const emoji = params?.emoji;
|
|
75
|
+
const theme = params?.theme;
|
|
76
|
+
if (!agentId || typeof agentId !== "string") {
|
|
77
|
+
respond(false, { error: "Missing 'agentId' parameter" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const args = [`--agent`, JSON.stringify(agentId)];
|
|
81
|
+
if (name) args.push(`--name`, JSON.stringify(name));
|
|
82
|
+
if (emoji) args.push(`--emoji`, JSON.stringify(emoji));
|
|
83
|
+
if (theme) args.push(`--theme`, JSON.stringify(theme));
|
|
84
|
+
if (args.length <= 2) {
|
|
85
|
+
respond(false, { error: "No identity fields provided (name, emoji, or theme)" });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const output = execSync(
|
|
90
|
+
`openclaw agents set-identity ${args.join(" ")} 2>&1`,
|
|
91
|
+
{ timeout: 15e3, encoding: "utf-8" }
|
|
92
|
+
);
|
|
93
|
+
respond(true, { ok: true, output: output.slice(0, 1e3) });
|
|
94
|
+
} catch (err2) {
|
|
95
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
96
|
+
const stderr = err2?.stderr;
|
|
97
|
+
respond(false, {
|
|
98
|
+
error: `Failed to set identity: ${stderr || msg}`.slice(0, 500)
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
1
105
|
// src/entities.ts
|
|
2
106
|
import { Type as T } from "@sinclair/typebox";
|
|
3
107
|
import path3 from "path";
|
|
@@ -1111,7 +1215,7 @@ function registerSqlTools(api) {
|
|
|
1111
1215
|
}
|
|
1112
1216
|
|
|
1113
1217
|
// src/version.ts
|
|
1114
|
-
import { execSync } from "child_process";
|
|
1218
|
+
import { execSync as execSync2 } from "child_process";
|
|
1115
1219
|
import fs5 from "fs";
|
|
1116
1220
|
import path5 from "path";
|
|
1117
1221
|
import { fileURLToPath } from "url";
|
|
@@ -1176,13 +1280,13 @@ function registerVersionMethods(api) {
|
|
|
1176
1280
|
const before = getCurrentVersion();
|
|
1177
1281
|
let updateOutput = "";
|
|
1178
1282
|
try {
|
|
1179
|
-
updateOutput =
|
|
1283
|
+
updateOutput = execSync2(
|
|
1180
1284
|
`openclaw plugins update ${PACKAGE_NAME} 2>&1`,
|
|
1181
1285
|
{ timeout: 12e4, encoding: "utf-8" }
|
|
1182
1286
|
);
|
|
1183
1287
|
} catch {
|
|
1184
1288
|
try {
|
|
1185
|
-
updateOutput =
|
|
1289
|
+
updateOutput = execSync2(
|
|
1186
1290
|
`npm install -g ${PACKAGE_NAME}@latest 2>&1`,
|
|
1187
1291
|
{ timeout: 12e4, encoding: "utf-8" }
|
|
1188
1292
|
);
|
|
@@ -1208,7 +1312,7 @@ function registerVersionMethods(api) {
|
|
|
1208
1312
|
);
|
|
1209
1313
|
setTimeout(() => {
|
|
1210
1314
|
try {
|
|
1211
|
-
|
|
1315
|
+
execSync2("openclaw gateway restart 2>&1", {
|
|
1212
1316
|
timeout: 3e4,
|
|
1213
1317
|
encoding: "utf-8"
|
|
1214
1318
|
});
|
|
@@ -1895,6 +1999,7 @@ function squadAppPlugin(api) {
|
|
|
1895
1999
|
registerFilesystemTools(api);
|
|
1896
2000
|
registerSqlTools(api);
|
|
1897
2001
|
registerVersionMethods(api);
|
|
2002
|
+
registerAgentMethods(api);
|
|
1898
2003
|
api.registerGatewayMethod(
|
|
1899
2004
|
"tools.invoke",
|
|
1900
2005
|
async ({ params, respond }) => {
|
|
@@ -1918,6 +2023,49 @@ function squadAppPlugin(api) {
|
|
|
1918
2023
|
}
|
|
1919
2024
|
}
|
|
1920
2025
|
);
|
|
2026
|
+
api.registerGatewayMethod(
|
|
2027
|
+
"tools.list",
|
|
2028
|
+
async ({ respond }) => {
|
|
2029
|
+
const coreTools = [
|
|
2030
|
+
"exec",
|
|
2031
|
+
"bash",
|
|
2032
|
+
"process",
|
|
2033
|
+
"read",
|
|
2034
|
+
"write",
|
|
2035
|
+
"edit",
|
|
2036
|
+
"apply_patch",
|
|
2037
|
+
"web_search",
|
|
2038
|
+
"web_fetch",
|
|
2039
|
+
"browser",
|
|
2040
|
+
"canvas",
|
|
2041
|
+
"nodes",
|
|
2042
|
+
"image",
|
|
2043
|
+
"message",
|
|
2044
|
+
"cron",
|
|
2045
|
+
"gateway",
|
|
2046
|
+
"sessions_list",
|
|
2047
|
+
"sessions_history",
|
|
2048
|
+
"sessions_send",
|
|
2049
|
+
"sessions_spawn",
|
|
2050
|
+
"session_status",
|
|
2051
|
+
"agents_list",
|
|
2052
|
+
"memory_search"
|
|
2053
|
+
];
|
|
2054
|
+
const groups = [
|
|
2055
|
+
"group:fs",
|
|
2056
|
+
"group:runtime",
|
|
2057
|
+
"group:sessions",
|
|
2058
|
+
"group:memory",
|
|
2059
|
+
"group:web",
|
|
2060
|
+
"group:ui",
|
|
2061
|
+
"group:automation",
|
|
2062
|
+
"group:messaging",
|
|
2063
|
+
"group:nodes"
|
|
2064
|
+
];
|
|
2065
|
+
const pluginTools = Array.from(toolExecutors.keys());
|
|
2066
|
+
respond(true, { tools: [...coreTools, ...groups, ...pluginTools] });
|
|
2067
|
+
}
|
|
2068
|
+
);
|
|
1921
2069
|
const relayEnabled = api.pluginConfig?.["relay.enabled"] ?? false;
|
|
1922
2070
|
if (relayEnabled) {
|
|
1923
2071
|
const relayUrl = api.pluginConfig?.["relay.url"] || "wss://relay.squad.ceo";
|
package/package.json
CHANGED