squad-openclaw 2026.2.2703 → 2026.2.2705
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 +39 -22
- package/dist/index.js +708 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
1
|
# squad-openclaw
|
|
2
2
|
|
|
3
|
-
OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity registry, filesystem tools,
|
|
3
|
+
OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity registry, locked-down filesystem tools, agent setup wrappers, plugin safety controls, and Tailnet internal routes for onboarding helpers.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
| Tool / Method | Description |
|
|
7
|
+
| Tool / Method / Endpoint | Description |
|
|
8
8
|
|---|---|
|
|
9
9
|
| `entity_list`, `entity_search`, `entity_sync` | In-memory entity registry with filesystem watching (agents, skills, plugins, tools, media) |
|
|
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
|
-
| `
|
|
12
|
-
| `squad.version.check
|
|
13
|
-
| `
|
|
11
|
+
| `squad.agents.add` | Plugin wrapper for creating agent scaffolding (workspace seed files, sessions skeleton, and config list entry) |
|
|
12
|
+
| `squad.version.check` | Plugin version reporting |
|
|
13
|
+
| `squad.questions.validate-envelope` | HUMAN_INPUT_REQUIRED envelope validation |
|
|
14
|
+
| `tools.invoke`, `tools.list`, `squad.layout.get` | Core plugin RPC entrypoints for tool invocation/listing and gateway layout metadata |
|
|
14
15
|
| `squad.plugin.status`, `squad.plugin.recover`, `squad.plugin.disable` | Plugin safety-state RPC control (status, recovery, manual disable) |
|
|
15
|
-
|
|
|
16
|
+
| `GET /squad-internal/health` | Tailnet internal health + pairing capability metadata |
|
|
17
|
+
| `GET /squad-internal/plugin/status` | Tailnet internal plugin safety-state status |
|
|
18
|
+
| `POST /squad-internal/plugin/recover`, `POST /squad-internal/plugin/disable` | Tailnet internal plugin safety-state controls |
|
|
19
|
+
| `POST /squad-internal/pairing/request`, `GET /squad-internal/pairing/status` | Tailnet internal pairing helper routes with origin/Tailnet/browser-proof checks |
|
|
20
|
+
|
|
21
|
+
## Internal Endpoint Reference
|
|
22
|
+
|
|
23
|
+
All `/squad-internal/*` routes enforce Tailnet context and origin checks. CORS preflight (`OPTIONS`) is handled for these routes.
|
|
24
|
+
|
|
25
|
+
| Route | Method | Purpose |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `/squad-internal/health` | `GET` | Returns plugin safety snapshot and pairing capability hints |
|
|
28
|
+
| `/squad-internal/plugin/status` | `GET` | Returns current plugin safety state |
|
|
29
|
+
| `/squad-internal/plugin/recover` | `POST` | Recovers plugin safety state back to active (unless env kill-switch is set) |
|
|
30
|
+
| `/squad-internal/plugin/disable` | `POST` | Manually disables plugin safety state |
|
|
31
|
+
| `/squad-internal/pairing/request` | `POST` | Creates pairing request via gateway-native pairing methods (`node/devices/device.pair.request`) |
|
|
32
|
+
| `/squad-internal/pairing/status` | `GET` | Resolves pairing status via gateway-native status methods (`node/devices/device.pair.status/get`) |
|
|
16
33
|
|
|
17
34
|
## State Directory Resolution
|
|
18
35
|
|
|
@@ -102,7 +119,7 @@ Filesystem operations are restricted to configured root directories. By default,
|
|
|
102
119
|
|
|
103
120
|
Operators can customize via the `fs.allowedRoots` config option.
|
|
104
121
|
|
|
105
|
-
### Layer 4: Filesystem Write Protection
|
|
122
|
+
### Layer 4: Filesystem Write Protection
|
|
106
123
|
|
|
107
124
|
These files/directories cannot be written via filesystem tools (`fs_write`, `fs_rename`, etc.), even if they fall within `allowedRoots`:
|
|
108
125
|
|
|
@@ -192,6 +209,19 @@ Pairing helper routes enforce:
|
|
|
192
209
|
|
|
193
210
|
`/squad-internal/pairing/request` and `/squad-internal/pairing/status` are still present for compatibility, but the current SPA pairing flow no longer depends on them as primary path.
|
|
194
211
|
|
|
212
|
+
`POST /squad-internal/pairing/request` expects browser proof fields:
|
|
213
|
+
|
|
214
|
+
- `deviceId`
|
|
215
|
+
- `publicKey` (Ed25519) **or** `publicKeyJwk` (P-256)
|
|
216
|
+
- `signature`
|
|
217
|
+
- `nonce`
|
|
218
|
+
- `signedAt`
|
|
219
|
+
|
|
220
|
+
`GET /squad-internal/pairing/status` expects:
|
|
221
|
+
|
|
222
|
+
- `requestId` query param
|
|
223
|
+
- `deviceId` query param
|
|
224
|
+
|
|
195
225
|
### Legacy Relay-State File Handling
|
|
196
226
|
|
|
197
227
|
`~/.openclaw/squad-ceo-data/relay/squad-relay.json` is still blocked by filesystem security rules to protect historical secrets from older relay transport versions.
|
|
@@ -203,23 +233,10 @@ The `tools.invoke` gateway method allows the browser to call plugin tools over W
|
|
|
203
233
|
| Tool | What it can access | Restrictions |
|
|
204
234
|
|---|---|---|
|
|
205
235
|
| `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) |
|
|
206
|
-
| `sql_query` | `~/.openclaw/squad-ceo-data/*.db` only | sqlite3 only, no shell, no command injection (see below) |
|
|
207
236
|
| `entity_list`, `entity_search`, `entity_sync` | In-memory entity index | Read-only metadata (names, types, paths) |
|
|
208
|
-
| `squad.version.check`, `squad.version.update` | npm registry | Read-only check + controlled `npm install` |
|
|
209
237
|
|
|
210
238
|
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.
|
|
211
239
|
|
|
212
|
-
## SQL Query Tool
|
|
213
|
-
|
|
214
|
-
> **`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.
|
|
215
|
-
|
|
216
|
-
The `sql_query` tool provides restricted SQLite access:
|
|
217
|
-
|
|
218
|
-
- **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.
|
|
219
|
-
- **No shell:** Uses `execFile` (not `exec`) — arguments are passed as an argv array, preventing command injection
|
|
220
|
-
- **No arbitrary commands:** Only `sqlite3` is executed — no other binary can be invoked through this tool
|
|
221
|
-
- **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.
|
|
222
|
-
|
|
223
240
|
## Build Transparency
|
|
224
241
|
|
|
225
242
|
The build configuration (`tsup.config.ts`) is optimized for security auditing:
|
|
@@ -244,10 +261,10 @@ Configure in your gateway's `openclaw.json` under the plugin section:
|
|
|
244
261
|
- **Plugin directory:** `extensions/squad-openclaw/`
|
|
245
262
|
- **Security-critical files:**
|
|
246
263
|
- `src/filesystem.ts` — path blocking, redaction, write protection
|
|
247
|
-
- `src/
|
|
264
|
+
- `src/agents.ts` — wrapper for agent setup (workspace/session skeleton + config entry update)
|
|
248
265
|
- `src/http-routes.ts` — Tailnet internal routes and browser-proof validation
|
|
249
266
|
- `src/shared-api.ts` — gateway method + tool registration boundaries
|
|
250
|
-
- `src/
|
|
267
|
+
- `src/plugin-safety-gateway.ts`, `src/plugin-safety-state.ts` — plugin safety-state control and persistence
|
|
251
268
|
|
|
252
269
|
## License
|
|
253
270
|
|
package/dist/index.js
CHANGED
|
@@ -1233,6 +1233,150 @@ function registerQuestionMethods(api) {
|
|
|
1233
1233
|
);
|
|
1234
1234
|
}
|
|
1235
1235
|
|
|
1236
|
+
// src/agents.ts
|
|
1237
|
+
import fs7 from "fs";
|
|
1238
|
+
import os2 from "os";
|
|
1239
|
+
import path7 from "path";
|
|
1240
|
+
var DEFAULT_WORKSPACE_FILES = {
|
|
1241
|
+
"SOUL.md": "# Soul\n",
|
|
1242
|
+
"USER.md": "# User\n",
|
|
1243
|
+
"AGENTS.md": "# Agents\n",
|
|
1244
|
+
"IDENTITY.md": "# Identity\n",
|
|
1245
|
+
"BOOTSTRAP.md": "# Bootstrap\n",
|
|
1246
|
+
"BOOT.md": "# Boot\n",
|
|
1247
|
+
"HEARTBEAT.md": "# Heartbeat\n",
|
|
1248
|
+
"TOOLS.md": "# Tools\n"
|
|
1249
|
+
};
|
|
1250
|
+
function asRecord(value) {
|
|
1251
|
+
return value && typeof value === "object" ? value : {};
|
|
1252
|
+
}
|
|
1253
|
+
function normalizeAgentId(params) {
|
|
1254
|
+
const candidate = typeof params.agentId === "string" && params.agentId.trim() || typeof params.name === "string" && params.name.trim() || "";
|
|
1255
|
+
if (!candidate) throw new Error("Missing required parameter: agentId");
|
|
1256
|
+
if (!/^[a-z0-9_-]+$/i.test(candidate)) {
|
|
1257
|
+
throw new Error("Invalid agentId: only letters, numbers, _ and - are allowed");
|
|
1258
|
+
}
|
|
1259
|
+
return candidate;
|
|
1260
|
+
}
|
|
1261
|
+
function resolveWorkspacePath(input, stateDir, agentId) {
|
|
1262
|
+
const fallback = agentId === "main" ? path7.join(stateDir, "workspace") : path7.join(stateDir, `workspace-${agentId}`);
|
|
1263
|
+
if (typeof input !== "string" || !input.trim()) return fallback;
|
|
1264
|
+
const raw = input.trim();
|
|
1265
|
+
if (raw === "~/.openclaw") return stateDir;
|
|
1266
|
+
if (raw.startsWith("~/.openclaw/")) {
|
|
1267
|
+
return path7.resolve(stateDir, raw.slice("~/.openclaw/".length));
|
|
1268
|
+
}
|
|
1269
|
+
if (raw === "~") return os2.homedir();
|
|
1270
|
+
if (raw.startsWith("~/")) return path7.resolve(os2.homedir(), raw.slice(2));
|
|
1271
|
+
if (!path7.isAbsolute(raw)) return path7.resolve(stateDir, raw);
|
|
1272
|
+
return path7.resolve(raw);
|
|
1273
|
+
}
|
|
1274
|
+
function assertWithinStateDir(targetPath, stateDir) {
|
|
1275
|
+
const resolvedStateDir = path7.resolve(stateDir);
|
|
1276
|
+
const resolvedTarget = path7.resolve(targetPath);
|
|
1277
|
+
if (resolvedTarget === resolvedStateDir) return;
|
|
1278
|
+
if (!resolvedTarget.startsWith(resolvedStateDir + path7.sep)) {
|
|
1279
|
+
throw new Error("Workspace path must be inside OPENCLAW_STATE_DIR");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function normalizeModel(input) {
|
|
1283
|
+
return typeof input === "string" && input.trim() ? input.trim() : void 0;
|
|
1284
|
+
}
|
|
1285
|
+
function normalizeIdentity(params) {
|
|
1286
|
+
const identity = asRecord(params.identity);
|
|
1287
|
+
const name = typeof identity.name === "string" && identity.name.trim() ? identity.name.trim() : typeof params.displayName === "string" && params.displayName.trim() ? params.displayName.trim() : void 0;
|
|
1288
|
+
const emoji = typeof identity.emoji === "string" && identity.emoji.trim() ? identity.emoji.trim() : typeof params.emoji === "string" && params.emoji.trim() ? params.emoji.trim() : void 0;
|
|
1289
|
+
const theme = typeof identity.theme === "string" && identity.theme.trim() ? identity.theme.trim() : typeof params.description === "string" && params.description.trim() ? params.description.trim() : typeof params.theme === "string" && params.theme.trim() ? params.theme.trim() : void 0;
|
|
1290
|
+
return {
|
|
1291
|
+
...name ? { name } : {},
|
|
1292
|
+
...emoji ? { emoji } : {},
|
|
1293
|
+
...theme ? { theme } : {}
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function ensureWorkspaceSkeleton(workspacePath) {
|
|
1297
|
+
fs7.mkdirSync(workspacePath, { recursive: true });
|
|
1298
|
+
for (const [fileName, defaultContent] of Object.entries(DEFAULT_WORKSPACE_FILES)) {
|
|
1299
|
+
const filePath = path7.join(workspacePath, fileName);
|
|
1300
|
+
if (!fs7.existsSync(filePath)) {
|
|
1301
|
+
fs7.writeFileSync(filePath, defaultContent, "utf-8");
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function ensureSessionsSkeleton(stateDir, agentId) {
|
|
1306
|
+
const sessionsDir = path7.join(stateDir, "agents", agentId, "sessions");
|
|
1307
|
+
fs7.mkdirSync(sessionsDir, { recursive: true });
|
|
1308
|
+
const sessionsJsonPath = path7.join(sessionsDir, "sessions.json");
|
|
1309
|
+
if (!fs7.existsSync(sessionsJsonPath)) {
|
|
1310
|
+
fs7.writeFileSync(sessionsJsonPath, "{}", "utf-8");
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function extractConfigList(snapshot) {
|
|
1314
|
+
const cfgRoot = asRecord(snapshot);
|
|
1315
|
+
const agentsRoot = asRecord(cfgRoot.agents);
|
|
1316
|
+
const list = agentsRoot.list;
|
|
1317
|
+
return Array.isArray(list) ? [...list] : [];
|
|
1318
|
+
}
|
|
1319
|
+
function upsertAgentListEntry(list, agentId, fields) {
|
|
1320
|
+
const nextList = [];
|
|
1321
|
+
let found = false;
|
|
1322
|
+
for (const entry of list) {
|
|
1323
|
+
const id = typeof entry?.id === "string" ? entry.id : void 0;
|
|
1324
|
+
if (id === agentId) {
|
|
1325
|
+
nextList.push({ ...entry, ...fields, id: agentId });
|
|
1326
|
+
found = true;
|
|
1327
|
+
} else {
|
|
1328
|
+
nextList.push(entry);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (!found) nextList.push({ id: agentId, ...fields });
|
|
1332
|
+
return nextList;
|
|
1333
|
+
}
|
|
1334
|
+
function registerAgentMethods(api) {
|
|
1335
|
+
api.registerGatewayMethod(
|
|
1336
|
+
"squad.agents.add",
|
|
1337
|
+
async (ctx) => {
|
|
1338
|
+
try {
|
|
1339
|
+
const params = ctx.params ?? {};
|
|
1340
|
+
const stateDir = getOpenclawStateDir();
|
|
1341
|
+
const configPath = process.env.OPENCLAW_CONFIG_PATH ? path7.resolve(process.env.OPENCLAW_CONFIG_PATH) : path7.join(stateDir, "openclaw.json");
|
|
1342
|
+
const agentId = normalizeAgentId(params);
|
|
1343
|
+
const workspacePath = resolveWorkspacePath(params.workspace, stateDir, agentId);
|
|
1344
|
+
assertWithinStateDir(workspacePath, stateDir);
|
|
1345
|
+
const model = normalizeModel(params.model);
|
|
1346
|
+
const identity = normalizeIdentity(params);
|
|
1347
|
+
ensureWorkspaceSkeleton(workspacePath);
|
|
1348
|
+
ensureSessionsSkeleton(stateDir, agentId);
|
|
1349
|
+
const rawConfig = fs7.readFileSync(configPath, "utf-8");
|
|
1350
|
+
const snapshot = JSON.parse(rawConfig);
|
|
1351
|
+
const cfgRoot = asRecord(snapshot);
|
|
1352
|
+
const agentsRoot = asRecord(cfgRoot.agents);
|
|
1353
|
+
const currentList = extractConfigList(snapshot);
|
|
1354
|
+
const nextList = upsertAgentListEntry(currentList, agentId, {
|
|
1355
|
+
workspace: workspacePath,
|
|
1356
|
+
...model ? { model } : {},
|
|
1357
|
+
...Object.keys(identity).length > 0 ? { identity } : {}
|
|
1358
|
+
});
|
|
1359
|
+
cfgRoot.agents = {
|
|
1360
|
+
...agentsRoot,
|
|
1361
|
+
list: nextList
|
|
1362
|
+
};
|
|
1363
|
+
fs7.writeFileSync(configPath, JSON.stringify(cfgRoot, null, 2), "utf-8");
|
|
1364
|
+
ctx.respond(true, {
|
|
1365
|
+
ok: true,
|
|
1366
|
+
agentId,
|
|
1367
|
+
workspace: workspacePath
|
|
1368
|
+
});
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1371
|
+
ctx.respond(false, {
|
|
1372
|
+
error: message,
|
|
1373
|
+
errorMessage: message
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1236
1380
|
// src/safety.ts
|
|
1237
1381
|
var TimeoutError = class extends Error {
|
|
1238
1382
|
operation;
|
|
@@ -1268,6 +1412,262 @@ async function withTimeout(promise, timeoutMs, operation) {
|
|
|
1268
1412
|
}
|
|
1269
1413
|
}
|
|
1270
1414
|
|
|
1415
|
+
// src/plugin-safety-state.ts
|
|
1416
|
+
import crypto from "crypto";
|
|
1417
|
+
import fs8 from "fs";
|
|
1418
|
+
import path8 from "path";
|
|
1419
|
+
var SAFETY_DIR = path8.join(getOpenclawStateDir(), "squad-ceo-data", "safety");
|
|
1420
|
+
var PLUGIN_SAFETY_STATE_PATH = path8.join(SAFETY_DIR, "plugin-state.json");
|
|
1421
|
+
var FAILURE_THRESHOLD = readIntegerEnv("SQUAD_PLUGIN_FAILURE_THRESHOLD", 3, 1, 50);
|
|
1422
|
+
var FAILURE_WINDOW_MS = readTimeoutMs("SQUAD_PLUGIN_FAILURE_WINDOW_MS", 5 * 60 * 1e3, 1e3, 24 * 60 * 60 * 1e3);
|
|
1423
|
+
var QUARANTINE_MS = readTimeoutMs("SQUAD_PLUGIN_QUARANTINE_MS", 10 * 60 * 1e3, 1e3, 24 * 60 * 60 * 1e3);
|
|
1424
|
+
function readIntegerEnv(envName, fallback, min, max) {
|
|
1425
|
+
const raw = process.env[envName];
|
|
1426
|
+
if (!raw) return fallback;
|
|
1427
|
+
const parsed = Number(raw);
|
|
1428
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
1429
|
+
const rounded = Math.floor(parsed);
|
|
1430
|
+
if (rounded < min) return min;
|
|
1431
|
+
if (rounded > max) return max;
|
|
1432
|
+
return rounded;
|
|
1433
|
+
}
|
|
1434
|
+
function nowIso() {
|
|
1435
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1436
|
+
}
|
|
1437
|
+
function normalizeIso(value) {
|
|
1438
|
+
if (typeof value !== "string") return null;
|
|
1439
|
+
const parsed = Date.parse(value);
|
|
1440
|
+
if (!Number.isFinite(parsed)) return null;
|
|
1441
|
+
return new Date(parsed).toISOString();
|
|
1442
|
+
}
|
|
1443
|
+
function normalizeString(value) {
|
|
1444
|
+
if (typeof value !== "string") return null;
|
|
1445
|
+
const trimmed = value.trim();
|
|
1446
|
+
return trimmed ? trimmed : null;
|
|
1447
|
+
}
|
|
1448
|
+
function normalizeState(value) {
|
|
1449
|
+
if (value === "ACTIVE" || value === "DISABLED_MANUAL" || value === "QUARANTINED_AUTO") {
|
|
1450
|
+
return value;
|
|
1451
|
+
}
|
|
1452
|
+
return "ACTIVE";
|
|
1453
|
+
}
|
|
1454
|
+
function normalizeFailureCount(value) {
|
|
1455
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
1456
|
+
const rounded = Math.floor(value);
|
|
1457
|
+
return rounded > 0 ? rounded : 0;
|
|
1458
|
+
}
|
|
1459
|
+
function parseMs(value) {
|
|
1460
|
+
if (!value) return null;
|
|
1461
|
+
const parsed = Date.parse(value);
|
|
1462
|
+
if (!Number.isFinite(parsed)) return null;
|
|
1463
|
+
return parsed;
|
|
1464
|
+
}
|
|
1465
|
+
function defaultPersistedState() {
|
|
1466
|
+
return {
|
|
1467
|
+
version: 1,
|
|
1468
|
+
state: "ACTIVE",
|
|
1469
|
+
reasonCode: null,
|
|
1470
|
+
reasonMessage: null,
|
|
1471
|
+
remediation: null,
|
|
1472
|
+
triggeredAt: null,
|
|
1473
|
+
lastFailureAt: null,
|
|
1474
|
+
lastFailureCode: null,
|
|
1475
|
+
lastFailureMessage: null,
|
|
1476
|
+
failureCount: 0,
|
|
1477
|
+
failureWindowStartedAt: null,
|
|
1478
|
+
quarantineUntil: null,
|
|
1479
|
+
lastErrorId: null,
|
|
1480
|
+
updatedAt: nowIso()
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
function sanitizePersistedState(value) {
|
|
1484
|
+
const state = defaultPersistedState();
|
|
1485
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return state;
|
|
1486
|
+
const record = value;
|
|
1487
|
+
return {
|
|
1488
|
+
version: 1,
|
|
1489
|
+
state: normalizeState(record.state),
|
|
1490
|
+
reasonCode: normalizeString(record.reasonCode),
|
|
1491
|
+
reasonMessage: normalizeString(record.reasonMessage),
|
|
1492
|
+
remediation: normalizeString(record.remediation),
|
|
1493
|
+
triggeredAt: normalizeIso(record.triggeredAt),
|
|
1494
|
+
lastFailureAt: normalizeIso(record.lastFailureAt),
|
|
1495
|
+
lastFailureCode: normalizeString(record.lastFailureCode),
|
|
1496
|
+
lastFailureMessage: normalizeString(record.lastFailureMessage),
|
|
1497
|
+
failureCount: normalizeFailureCount(record.failureCount),
|
|
1498
|
+
failureWindowStartedAt: normalizeIso(record.failureWindowStartedAt),
|
|
1499
|
+
quarantineUntil: normalizeIso(record.quarantineUntil),
|
|
1500
|
+
lastErrorId: normalizeString(record.lastErrorId),
|
|
1501
|
+
updatedAt: normalizeIso(record.updatedAt) ?? nowIso()
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
function readPersistedState() {
|
|
1505
|
+
try {
|
|
1506
|
+
const raw = fs8.readFileSync(PLUGIN_SAFETY_STATE_PATH, "utf-8");
|
|
1507
|
+
return sanitizePersistedState(JSON.parse(raw));
|
|
1508
|
+
} catch {
|
|
1509
|
+
return defaultPersistedState();
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function writePersistedState(state) {
|
|
1513
|
+
try {
|
|
1514
|
+
fs8.mkdirSync(SAFETY_DIR, { recursive: true });
|
|
1515
|
+
const tempPath = `${PLUGIN_SAFETY_STATE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
1516
|
+
fs8.writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}
|
|
1517
|
+
`, "utf-8");
|
|
1518
|
+
fs8.renameSync(tempPath, PLUGIN_SAFETY_STATE_PATH);
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1521
|
+
console.warn(`[squad-openclaw] failed to persist plugin safety state: ${message}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function isTruthy(value) {
|
|
1525
|
+
if (!value) return false;
|
|
1526
|
+
return /^(1|true|yes|on)$/i.test(value.trim());
|
|
1527
|
+
}
|
|
1528
|
+
function isEnvKillSwitchActive() {
|
|
1529
|
+
return isTruthy(process.env.SQUAD_PLUGIN_DISABLED);
|
|
1530
|
+
}
|
|
1531
|
+
function envKillSwitchReason() {
|
|
1532
|
+
const configured = normalizeString(process.env.SQUAD_PLUGIN_DISABLE_REASON);
|
|
1533
|
+
return configured ?? "Plugin disabled by SQUAD_PLUGIN_DISABLED";
|
|
1534
|
+
}
|
|
1535
|
+
function snapshotFromState(state, source, canRecover) {
|
|
1536
|
+
return {
|
|
1537
|
+
state: state.state,
|
|
1538
|
+
source,
|
|
1539
|
+
blocked: state.state !== "ACTIVE",
|
|
1540
|
+
canRecover,
|
|
1541
|
+
reasonCode: state.reasonCode,
|
|
1542
|
+
reasonMessage: state.reasonMessage,
|
|
1543
|
+
remediation: state.remediation,
|
|
1544
|
+
triggeredAt: state.triggeredAt,
|
|
1545
|
+
lastFailureAt: state.lastFailureAt,
|
|
1546
|
+
lastFailureCode: state.lastFailureCode,
|
|
1547
|
+
lastFailureMessage: state.lastFailureMessage,
|
|
1548
|
+
failureCount: state.failureCount,
|
|
1549
|
+
quarantineUntil: state.quarantineUntil,
|
|
1550
|
+
lastErrorId: state.lastErrorId,
|
|
1551
|
+
updatedAt: state.updatedAt,
|
|
1552
|
+
stateFilePath: PLUGIN_SAFETY_STATE_PATH
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
function maybeReleaseExpiredQuarantine(state) {
|
|
1556
|
+
if (state.state !== "QUARANTINED_AUTO") return state;
|
|
1557
|
+
const untilMs = parseMs(state.quarantineUntil);
|
|
1558
|
+
if (untilMs == null || Date.now() < untilMs) return state;
|
|
1559
|
+
const released = {
|
|
1560
|
+
...state,
|
|
1561
|
+
state: "ACTIVE",
|
|
1562
|
+
reasonCode: null,
|
|
1563
|
+
reasonMessage: null,
|
|
1564
|
+
remediation: null,
|
|
1565
|
+
triggeredAt: nowIso(),
|
|
1566
|
+
quarantineUntil: null,
|
|
1567
|
+
failureCount: 0,
|
|
1568
|
+
failureWindowStartedAt: null,
|
|
1569
|
+
updatedAt: nowIso()
|
|
1570
|
+
};
|
|
1571
|
+
writePersistedState(released);
|
|
1572
|
+
return released;
|
|
1573
|
+
}
|
|
1574
|
+
function generateErrorId() {
|
|
1575
|
+
try {
|
|
1576
|
+
return crypto.randomUUID();
|
|
1577
|
+
} catch {
|
|
1578
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
function getPluginSafetySnapshot() {
|
|
1582
|
+
const persisted = maybeReleaseExpiredQuarantine(readPersistedState());
|
|
1583
|
+
if (isEnvKillSwitchActive()) {
|
|
1584
|
+
const envState = {
|
|
1585
|
+
...persisted,
|
|
1586
|
+
state: "DISABLED_MANUAL",
|
|
1587
|
+
reasonCode: "ENV_KILL_SWITCH",
|
|
1588
|
+
reasonMessage: envKillSwitchReason(),
|
|
1589
|
+
remediation: "Unset SQUAD_PLUGIN_DISABLED and restart the gateway process.",
|
|
1590
|
+
triggeredAt: persisted.triggeredAt ?? nowIso(),
|
|
1591
|
+
updatedAt: nowIso()
|
|
1592
|
+
};
|
|
1593
|
+
return snapshotFromState(envState, "env", false);
|
|
1594
|
+
}
|
|
1595
|
+
return snapshotFromState(persisted, "file", true);
|
|
1596
|
+
}
|
|
1597
|
+
function isPluginExecutionBlocked(snapshot) {
|
|
1598
|
+
return snapshot.state !== "ACTIVE";
|
|
1599
|
+
}
|
|
1600
|
+
function recordPluginFailure(failureCode, failureMessage, remediation) {
|
|
1601
|
+
if (isEnvKillSwitchActive()) return getPluginSafetySnapshot();
|
|
1602
|
+
const state = readPersistedState();
|
|
1603
|
+
const nowMs = Date.now();
|
|
1604
|
+
const now = new Date(nowMs).toISOString();
|
|
1605
|
+
const windowStartMs = parseMs(state.failureWindowStartedAt);
|
|
1606
|
+
if (windowStartMs == null || nowMs - windowStartMs > FAILURE_WINDOW_MS) {
|
|
1607
|
+
state.failureWindowStartedAt = now;
|
|
1608
|
+
state.failureCount = 1;
|
|
1609
|
+
} else {
|
|
1610
|
+
state.failureCount += 1;
|
|
1611
|
+
}
|
|
1612
|
+
state.lastFailureAt = now;
|
|
1613
|
+
state.lastFailureCode = failureCode;
|
|
1614
|
+
state.lastFailureMessage = failureMessage;
|
|
1615
|
+
state.lastErrorId = generateErrorId();
|
|
1616
|
+
if (state.state !== "DISABLED_MANUAL" && state.failureCount >= FAILURE_THRESHOLD) {
|
|
1617
|
+
state.state = "QUARANTINED_AUTO";
|
|
1618
|
+
state.reasonCode = "AUTO_QUARANTINED";
|
|
1619
|
+
state.reasonMessage = `Plugin auto-quarantined after ${state.failureCount} failures in ${Math.round(
|
|
1620
|
+
FAILURE_WINDOW_MS / 1e3
|
|
1621
|
+
)} seconds`;
|
|
1622
|
+
state.remediation = remediation ?? "Resolve the underlying plugin issue, then run squad.plugin.recover or wait for quarantine expiry.";
|
|
1623
|
+
state.triggeredAt = now;
|
|
1624
|
+
state.quarantineUntil = new Date(nowMs + QUARANTINE_MS).toISOString();
|
|
1625
|
+
}
|
|
1626
|
+
state.updatedAt = now;
|
|
1627
|
+
writePersistedState(state);
|
|
1628
|
+
return getPluginSafetySnapshot();
|
|
1629
|
+
}
|
|
1630
|
+
function setPluginManualDisabled(reasonCode = "MANUAL_KILL_SWITCH", reasonMessage = "Plugin manually disabled", remediation = "Run squad.plugin.recover after resolving plugin issues.") {
|
|
1631
|
+
if (isEnvKillSwitchActive()) return getPluginSafetySnapshot();
|
|
1632
|
+
const state = readPersistedState();
|
|
1633
|
+
const now = nowIso();
|
|
1634
|
+
state.state = "DISABLED_MANUAL";
|
|
1635
|
+
state.reasonCode = reasonCode;
|
|
1636
|
+
state.reasonMessage = reasonMessage;
|
|
1637
|
+
state.remediation = remediation;
|
|
1638
|
+
state.triggeredAt = now;
|
|
1639
|
+
state.quarantineUntil = null;
|
|
1640
|
+
state.updatedAt = now;
|
|
1641
|
+
writePersistedState(state);
|
|
1642
|
+
return getPluginSafetySnapshot();
|
|
1643
|
+
}
|
|
1644
|
+
function recoverPlugin(reasonMessage = "Plugin manually recovered") {
|
|
1645
|
+
if (isEnvKillSwitchActive()) {
|
|
1646
|
+
return {
|
|
1647
|
+
ok: false,
|
|
1648
|
+
message: "Cannot recover while SQUAD_PLUGIN_DISABLED is enabled.",
|
|
1649
|
+
snapshot: getPluginSafetySnapshot()
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
const state = readPersistedState();
|
|
1653
|
+
const now = nowIso();
|
|
1654
|
+
state.state = "ACTIVE";
|
|
1655
|
+
state.reasonCode = null;
|
|
1656
|
+
state.reasonMessage = null;
|
|
1657
|
+
state.remediation = null;
|
|
1658
|
+
state.triggeredAt = now;
|
|
1659
|
+
state.quarantineUntil = null;
|
|
1660
|
+
state.failureCount = 0;
|
|
1661
|
+
state.failureWindowStartedAt = null;
|
|
1662
|
+
state.updatedAt = now;
|
|
1663
|
+
writePersistedState(state);
|
|
1664
|
+
return {
|
|
1665
|
+
ok: true,
|
|
1666
|
+
message: reasonMessage,
|
|
1667
|
+
snapshot: getPluginSafetySnapshot()
|
|
1668
|
+
};
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1271
1671
|
// src/shared-api.ts
|
|
1272
1672
|
var CORE_TOOLS = [
|
|
1273
1673
|
"exec",
|
|
@@ -1309,6 +1709,17 @@ var TOOLS_INVOKE_TIMEOUT_MS = readTimeoutMs("SQUAD_TOOLS_INVOKE_TIMEOUT_MS", 6e4
|
|
|
1309
1709
|
function errorMessage(error) {
|
|
1310
1710
|
return error instanceof Error ? error.message : String(error);
|
|
1311
1711
|
}
|
|
1712
|
+
function pluginBlockedPayload(snapshot) {
|
|
1713
|
+
const code = snapshot.state === "QUARANTINED_AUTO" ? "PLUGIN_QUARANTINED" : "PLUGIN_DISABLED";
|
|
1714
|
+
const message = snapshot.reasonMessage ?? (snapshot.state === "QUARANTINED_AUTO" ? "Plugin is temporarily auto-quarantined" : "Plugin is currently disabled");
|
|
1715
|
+
return {
|
|
1716
|
+
code,
|
|
1717
|
+
error: message,
|
|
1718
|
+
errorCode: code,
|
|
1719
|
+
errorMessage: message,
|
|
1720
|
+
plugin: snapshot
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1312
1723
|
function registerSquadSharedApi(api, onFsChange) {
|
|
1313
1724
|
const toolExecutors = /* @__PURE__ */ new Map();
|
|
1314
1725
|
const registerToolFn = typeof api?.registerTool === "function" ? api.registerTool.bind(api) : null;
|
|
@@ -1333,6 +1744,7 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1333
1744
|
registerStep("filesystem tools", () => registerFilesystemTools(api));
|
|
1334
1745
|
registerStep("version methods", () => registerVersionMethods(api));
|
|
1335
1746
|
registerStep("question methods", () => registerQuestionMethods(api));
|
|
1747
|
+
registerStep("agent methods", () => registerAgentMethods(api));
|
|
1336
1748
|
const invokeTool = async (tool, args) => {
|
|
1337
1749
|
const executeFn = toolExecutors.get(tool);
|
|
1338
1750
|
if (!executeFn) {
|
|
@@ -1357,6 +1769,11 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1357
1769
|
safeRegisterGatewayMethod(
|
|
1358
1770
|
"tools.invoke",
|
|
1359
1771
|
async ({ params, respond }) => {
|
|
1772
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
1773
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
1774
|
+
respond(false, pluginBlockedPayload(safetySnapshot));
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1360
1777
|
const tool = params?.tool;
|
|
1361
1778
|
const args = params?.args ?? {};
|
|
1362
1779
|
if (!tool) {
|
|
@@ -1372,7 +1789,18 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1372
1789
|
respond(true, result);
|
|
1373
1790
|
} catch (err2) {
|
|
1374
1791
|
if (err2 instanceof TimeoutError) {
|
|
1792
|
+
const snapshot = recordPluginFailure(
|
|
1793
|
+
"TOOLS_INVOKE_TIMEOUT",
|
|
1794
|
+
`Tool '${tool}' timed out after ${TOOLS_INVOKE_TIMEOUT_MS}ms`,
|
|
1795
|
+
"Investigate long-running plugin operations and run squad.plugin.recover."
|
|
1796
|
+
);
|
|
1797
|
+
if (isPluginExecutionBlocked(snapshot)) {
|
|
1798
|
+
respond(false, pluginBlockedPayload(snapshot));
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1375
1801
|
respond(false, {
|
|
1802
|
+
code: "PLUGIN_TIMEOUT",
|
|
1803
|
+
error: `Tool '${tool}' timed out after ${TOOLS_INVOKE_TIMEOUT_MS}ms`,
|
|
1376
1804
|
errorCode: "PLUGIN_TIMEOUT",
|
|
1377
1805
|
errorMessage: `Tool '${tool}' timed out after ${TOOLS_INVOKE_TIMEOUT_MS}ms`
|
|
1378
1806
|
});
|
|
@@ -1385,12 +1813,22 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1385
1813
|
safeRegisterGatewayMethod(
|
|
1386
1814
|
"tools.list",
|
|
1387
1815
|
async ({ respond }) => {
|
|
1816
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
1817
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
1818
|
+
respond(false, pluginBlockedPayload(safetySnapshot));
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1388
1821
|
respond(true, { tools: listTools() });
|
|
1389
1822
|
}
|
|
1390
1823
|
);
|
|
1391
1824
|
safeRegisterGatewayMethod(
|
|
1392
1825
|
"squad.layout.get",
|
|
1393
1826
|
async ({ respond }) => {
|
|
1827
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
1828
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
1829
|
+
respond(false, pluginBlockedPayload(safetySnapshot));
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1394
1832
|
try {
|
|
1395
1833
|
const layout = resolveGatewayLayout();
|
|
1396
1834
|
respond(true, layout);
|
|
@@ -1408,13 +1846,13 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1408
1846
|
}
|
|
1409
1847
|
|
|
1410
1848
|
// src/migrations/runner.ts
|
|
1411
|
-
import
|
|
1412
|
-
import
|
|
1849
|
+
import fs11 from "fs";
|
|
1850
|
+
import path11 from "path";
|
|
1413
1851
|
|
|
1414
1852
|
// src/migrations/001-enable-main-subagent-access.ts
|
|
1415
|
-
import
|
|
1416
|
-
import
|
|
1417
|
-
function
|
|
1853
|
+
import fs9 from "fs";
|
|
1854
|
+
import path9 from "path";
|
|
1855
|
+
function asRecord2(value) {
|
|
1418
1856
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1419
1857
|
return value;
|
|
1420
1858
|
}
|
|
@@ -1429,24 +1867,24 @@ function mergeStringArrayWithWildcard(value) {
|
|
|
1429
1867
|
return Array.from(next);
|
|
1430
1868
|
}
|
|
1431
1869
|
function patchConfigOnDisk() {
|
|
1432
|
-
const configPath =
|
|
1433
|
-
const raw =
|
|
1870
|
+
const configPath = path9.join(getOpenclawStateDir(), "openclaw.json");
|
|
1871
|
+
const raw = fs9.readFileSync(configPath, "utf-8");
|
|
1434
1872
|
const parsed = JSON.parse(raw);
|
|
1435
|
-
const agents =
|
|
1436
|
-
const defaults =
|
|
1437
|
-
const subagentsDefaults =
|
|
1873
|
+
const agents = asRecord2(parsed.agents) ?? {};
|
|
1874
|
+
const defaults = asRecord2(agents.defaults) ?? {};
|
|
1875
|
+
const subagentsDefaults = asRecord2(defaults.subagents) ?? {};
|
|
1438
1876
|
defaults.maxConcurrent = 4;
|
|
1439
1877
|
defaults.subagents = {
|
|
1440
1878
|
...subagentsDefaults,
|
|
1441
1879
|
maxConcurrent: 8
|
|
1442
1880
|
};
|
|
1443
1881
|
const listRaw = Array.isArray(agents.list) ? agents.list : [];
|
|
1444
|
-
const list = listRaw.map((entry) =>
|
|
1882
|
+
const list = listRaw.map((entry) => asRecord2(entry)).filter((entry) => Boolean(entry));
|
|
1445
1883
|
const mainIndex = list.findIndex((entry) => entry.id === "main");
|
|
1446
1884
|
const existingMain = mainIndex >= 0 ? list[mainIndex] : {};
|
|
1447
|
-
const existingIdentity =
|
|
1448
|
-
const existingTools =
|
|
1449
|
-
const existingSubagents =
|
|
1885
|
+
const existingIdentity = asRecord2(existingMain.identity) ?? {};
|
|
1886
|
+
const existingTools = asRecord2(existingMain.tools) ?? {};
|
|
1887
|
+
const existingSubagents = asRecord2(existingMain.subagents) ?? {};
|
|
1450
1888
|
const nextMain = {
|
|
1451
1889
|
...existingMain,
|
|
1452
1890
|
id: "main",
|
|
@@ -1470,7 +1908,7 @@ function patchConfigOnDisk() {
|
|
|
1470
1908
|
defaults,
|
|
1471
1909
|
list
|
|
1472
1910
|
};
|
|
1473
|
-
|
|
1911
|
+
fs9.writeFileSync(configPath, `${JSON.stringify(parsed, null, 2)}
|
|
1474
1912
|
`, "utf-8");
|
|
1475
1913
|
}
|
|
1476
1914
|
var migration = {
|
|
@@ -1545,13 +1983,13 @@ var migration = {
|
|
|
1545
1983
|
var enable_main_subagent_access_default = migration;
|
|
1546
1984
|
|
|
1547
1985
|
// src/auth-profiles.ts
|
|
1548
|
-
import
|
|
1549
|
-
import
|
|
1986
|
+
import fs10 from "fs";
|
|
1987
|
+
import path10 from "path";
|
|
1550
1988
|
function getMainAuthProfilesPath(stateDir) {
|
|
1551
|
-
return
|
|
1989
|
+
return path10.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
|
|
1552
1990
|
}
|
|
1553
1991
|
function getAgentAuthProfilesPath(stateDir, agentId) {
|
|
1554
|
-
return
|
|
1992
|
+
return path10.join(stateDir, "agents", agentId, "agent", "auth-profiles.json");
|
|
1555
1993
|
}
|
|
1556
1994
|
function ensureAgentAuthProfiles(agentId) {
|
|
1557
1995
|
const normalizedAgentId = agentId.trim();
|
|
@@ -1559,17 +1997,17 @@ function ensureAgentAuthProfiles(agentId) {
|
|
|
1559
1997
|
const stateDir = getOpenclawStateDir();
|
|
1560
1998
|
const sourcePath = getMainAuthProfilesPath(stateDir);
|
|
1561
1999
|
const targetPath = getAgentAuthProfilesPath(stateDir, normalizedAgentId);
|
|
1562
|
-
if (!
|
|
1563
|
-
|
|
1564
|
-
|
|
2000
|
+
if (!fs10.existsSync(sourcePath) || fs10.existsSync(targetPath)) return false;
|
|
2001
|
+
fs10.mkdirSync(path10.dirname(targetPath), { recursive: true });
|
|
2002
|
+
fs10.copyFileSync(sourcePath, targetPath);
|
|
1565
2003
|
return true;
|
|
1566
2004
|
}
|
|
1567
2005
|
function backfillAgentAuthProfiles() {
|
|
1568
2006
|
const stateDir = getOpenclawStateDir();
|
|
1569
|
-
const agentsDir =
|
|
1570
|
-
if (!
|
|
2007
|
+
const agentsDir = path10.join(stateDir, "agents");
|
|
2008
|
+
if (!fs10.existsSync(agentsDir)) return [];
|
|
1571
2009
|
const copied = [];
|
|
1572
|
-
for (const entry of
|
|
2010
|
+
for (const entry of fs10.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
1573
2011
|
if (!entry.isDirectory()) continue;
|
|
1574
2012
|
const agentId = entry.name;
|
|
1575
2013
|
if (ensureAgentAuthProfiles(agentId)) {
|
|
@@ -1600,8 +2038,8 @@ var STARTUP_MIGRATIONS = [
|
|
|
1600
2038
|
];
|
|
1601
2039
|
|
|
1602
2040
|
// src/migrations/runner.ts
|
|
1603
|
-
var MIGRATIONS_DIR =
|
|
1604
|
-
var MIGRATIONS_PATH =
|
|
2041
|
+
var MIGRATIONS_DIR = path11.join(getOpenclawStateDir(), "squad-ceo-data");
|
|
2042
|
+
var MIGRATIONS_PATH = path11.join(MIGRATIONS_DIR, "migrations.json");
|
|
1605
2043
|
var STARTUP_MIGRATION_TIMEOUT_MS = readTimeoutMs("SQUAD_STARTUP_MIGRATION_TIMEOUT_MS", 2e4);
|
|
1606
2044
|
var STARTUP_GATEWAY_CALL_TIMEOUT_MS = readTimeoutMs("SQUAD_STARTUP_GATEWAY_CALL_TIMEOUT_MS", 5e3);
|
|
1607
2045
|
function defaultState() {
|
|
@@ -1612,7 +2050,7 @@ function defaultState() {
|
|
|
1612
2050
|
}
|
|
1613
2051
|
function readState() {
|
|
1614
2052
|
try {
|
|
1615
|
-
const raw =
|
|
2053
|
+
const raw = fs11.readFileSync(MIGRATIONS_PATH, "utf-8");
|
|
1616
2054
|
const parsed = JSON.parse(raw);
|
|
1617
2055
|
if (!Array.isArray(parsed.completed)) return defaultState();
|
|
1618
2056
|
return {
|
|
@@ -1624,8 +2062,8 @@ function readState() {
|
|
|
1624
2062
|
}
|
|
1625
2063
|
}
|
|
1626
2064
|
function writeState(state) {
|
|
1627
|
-
|
|
1628
|
-
|
|
2065
|
+
fs11.mkdirSync(MIGRATIONS_DIR, { recursive: true });
|
|
2066
|
+
fs11.writeFileSync(MIGRATIONS_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
1629
2067
|
}
|
|
1630
2068
|
function makeGatewayCaller(api) {
|
|
1631
2069
|
return async (method, params = {}) => {
|
|
@@ -1691,10 +2129,10 @@ async function runStartupMigrations(api) {
|
|
|
1691
2129
|
}
|
|
1692
2130
|
|
|
1693
2131
|
// src/http-routes.ts
|
|
1694
|
-
import
|
|
2132
|
+
import crypto2 from "crypto";
|
|
1695
2133
|
|
|
1696
2134
|
// src/gateway-invoke.ts
|
|
1697
|
-
function
|
|
2135
|
+
function asRecord3(value) {
|
|
1698
2136
|
return value && typeof value === "object" ? value : null;
|
|
1699
2137
|
}
|
|
1700
2138
|
function isInvoker(fn) {
|
|
@@ -1706,8 +2144,8 @@ function isUnknownGatewayMethodError(message) {
|
|
|
1706
2144
|
);
|
|
1707
2145
|
}
|
|
1708
2146
|
async function callGatewayAny(ctx, api, method, params) {
|
|
1709
|
-
const ctxGateway =
|
|
1710
|
-
const apiGateway =
|
|
2147
|
+
const ctxGateway = asRecord3(ctx.gateway);
|
|
2148
|
+
const apiGateway = asRecord3(api?.gateway);
|
|
1711
2149
|
const candidates = [
|
|
1712
2150
|
ctx.request,
|
|
1713
2151
|
ctx.callGatewayMethod,
|
|
@@ -1768,7 +2206,7 @@ var PAIRING_GATEWAY_CALL_TIMEOUT_MS = readTimeoutMs("SQUAD_PAIRING_GATEWAY_CALL_
|
|
|
1768
2206
|
function errorMessage2(error) {
|
|
1769
2207
|
return error instanceof Error ? error.message : String(error);
|
|
1770
2208
|
}
|
|
1771
|
-
function
|
|
2209
|
+
function asRecord4(value) {
|
|
1772
2210
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1773
2211
|
return value;
|
|
1774
2212
|
}
|
|
@@ -1834,6 +2272,11 @@ function ensureOriginAllowed(request) {
|
|
|
1834
2272
|
if (!origin) return null;
|
|
1835
2273
|
return resolveAllowedOrigin(origin);
|
|
1836
2274
|
}
|
|
2275
|
+
function isOriginAllowedIfPresent(request) {
|
|
2276
|
+
const origin = request.headers.get("origin");
|
|
2277
|
+
if (!origin) return true;
|
|
2278
|
+
return resolveAllowedOrigin(origin) !== null;
|
|
2279
|
+
}
|
|
1837
2280
|
function firstHeaderToken(value) {
|
|
1838
2281
|
if (!value) return null;
|
|
1839
2282
|
const first = value.split(",")[0]?.trim();
|
|
@@ -1923,7 +2366,7 @@ function encodeUtf8(value) {
|
|
|
1923
2366
|
function normalizeDevicePublicKeyBase64Url(publicKey) {
|
|
1924
2367
|
try {
|
|
1925
2368
|
if (publicKey.includes("BEGIN")) {
|
|
1926
|
-
const spki =
|
|
2369
|
+
const spki = crypto2.createPublicKey(publicKey).export({
|
|
1927
2370
|
type: "spki",
|
|
1928
2371
|
format: "der"
|
|
1929
2372
|
});
|
|
@@ -1943,7 +2386,7 @@ function deriveDeviceIdFromPublicKey(publicKey) {
|
|
|
1943
2386
|
if (!normalized) return null;
|
|
1944
2387
|
const raw = decodeBase64Url(normalized);
|
|
1945
2388
|
if (raw.length !== 32) return null;
|
|
1946
|
-
return
|
|
2389
|
+
return crypto2.createHash("sha256").update(raw).digest("hex");
|
|
1947
2390
|
} catch {
|
|
1948
2391
|
return null;
|
|
1949
2392
|
}
|
|
@@ -1952,7 +2395,7 @@ function verifyEd25519Signature(publicKey, payload, signatureBase64Url) {
|
|
|
1952
2395
|
try {
|
|
1953
2396
|
const normalized = normalizeDevicePublicKeyBase64Url(publicKey);
|
|
1954
2397
|
if (!normalized) return false;
|
|
1955
|
-
const key =
|
|
2398
|
+
const key = crypto2.createPublicKey({
|
|
1956
2399
|
key: Buffer.concat([ED25519_SPKI_PREFIX, decodeBase64Url(normalized)]),
|
|
1957
2400
|
type: "spki",
|
|
1958
2401
|
format: "der"
|
|
@@ -1964,13 +2407,13 @@ function verifyEd25519Signature(publicKey, payload, signatureBase64Url) {
|
|
|
1964
2407
|
return Buffer.from(signatureBase64Url, "base64");
|
|
1965
2408
|
}
|
|
1966
2409
|
})();
|
|
1967
|
-
return
|
|
2410
|
+
return crypto2.verify(null, Buffer.from(payload, "utf8"), key, signature);
|
|
1968
2411
|
} catch {
|
|
1969
2412
|
return false;
|
|
1970
2413
|
}
|
|
1971
2414
|
}
|
|
1972
2415
|
function canonicalizeP256Jwk(value) {
|
|
1973
|
-
const record =
|
|
2416
|
+
const record = asRecord4(value);
|
|
1974
2417
|
const kty = pickString(record?.kty);
|
|
1975
2418
|
const crv = pickString(record?.crv);
|
|
1976
2419
|
const x = pickString(record?.x);
|
|
@@ -1982,7 +2425,7 @@ function canonicalizeP256Jwk(value) {
|
|
|
1982
2425
|
}
|
|
1983
2426
|
function computeDeviceIdFromJwk(jwk) {
|
|
1984
2427
|
const canonical = JSON.stringify(jwk);
|
|
1985
|
-
return
|
|
2428
|
+
return crypto2.createHash("sha256").update(canonical).digest("hex");
|
|
1986
2429
|
}
|
|
1987
2430
|
function buildProofPayload(action, deviceId, nonce, signedAt, origin) {
|
|
1988
2431
|
return `squad.${action}|${deviceId}|${nonce}|${signedAt}|${origin}`;
|
|
@@ -2063,14 +2506,14 @@ async function verifyBrowserProof(payload, origin, action, usedProofNonces) {
|
|
|
2063
2506
|
};
|
|
2064
2507
|
}
|
|
2065
2508
|
try {
|
|
2066
|
-
const key = await
|
|
2509
|
+
const key = await crypto2.webcrypto.subtle.importKey(
|
|
2067
2510
|
"jwk",
|
|
2068
2511
|
jwk,
|
|
2069
2512
|
{ name: "ECDSA", namedCurve: "P-256" },
|
|
2070
2513
|
false,
|
|
2071
2514
|
["verify"]
|
|
2072
2515
|
);
|
|
2073
|
-
verified = await
|
|
2516
|
+
verified = await crypto2.webcrypto.subtle.verify(
|
|
2074
2517
|
{ name: "ECDSA", hash: "SHA-256" },
|
|
2075
2518
|
key,
|
|
2076
2519
|
decodeBase64Url(signature),
|
|
@@ -2101,18 +2544,18 @@ function normalizePairingStatus(value) {
|
|
|
2101
2544
|
return "unknown";
|
|
2102
2545
|
}
|
|
2103
2546
|
function extractRequestId(result) {
|
|
2104
|
-
const obj =
|
|
2547
|
+
const obj = asRecord4(result);
|
|
2105
2548
|
if (!obj) return null;
|
|
2106
|
-
const nestedRequest =
|
|
2549
|
+
const nestedRequest = asRecord4(obj.request);
|
|
2107
2550
|
return pickString(obj.requestId) ?? pickString(obj.id) ?? pickString(nestedRequest?.requestId) ?? pickString(nestedRequest?.id);
|
|
2108
2551
|
}
|
|
2109
2552
|
function extractExpiresAt(result) {
|
|
2110
|
-
const obj =
|
|
2553
|
+
const obj = asRecord4(result);
|
|
2111
2554
|
const candidates = [
|
|
2112
2555
|
obj?.expiresAt,
|
|
2113
2556
|
obj?.expiresAtMs,
|
|
2114
|
-
|
|
2115
|
-
|
|
2557
|
+
asRecord4(obj?.request)?.expiresAt,
|
|
2558
|
+
asRecord4(obj?.request)?.expiresAtMs
|
|
2116
2559
|
];
|
|
2117
2560
|
for (const candidate of candidates) {
|
|
2118
2561
|
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > Date.now()) {
|
|
@@ -2132,12 +2575,12 @@ function extractExpiresAt(result) {
|
|
|
2132
2575
|
return Date.now() + DEFAULT_PAIRING_TTL_MS;
|
|
2133
2576
|
}
|
|
2134
2577
|
function extractPairingStatus(result) {
|
|
2135
|
-
const obj =
|
|
2578
|
+
const obj = asRecord4(result);
|
|
2136
2579
|
if (!obj) return "unknown";
|
|
2137
2580
|
const candidates = [
|
|
2138
2581
|
obj.status,
|
|
2139
|
-
|
|
2140
|
-
|
|
2582
|
+
asRecord4(obj.request)?.status,
|
|
2583
|
+
asRecord4(obj.pairing)?.status
|
|
2141
2584
|
];
|
|
2142
2585
|
for (const candidate of candidates) {
|
|
2143
2586
|
const parsed = normalizePairingStatus(candidate);
|
|
@@ -2158,6 +2601,12 @@ function isRateLimited(bucket, key, limit, windowMs) {
|
|
|
2158
2601
|
existing.count += 1;
|
|
2159
2602
|
return false;
|
|
2160
2603
|
}
|
|
2604
|
+
function pluginBlockedCode(snapshot) {
|
|
2605
|
+
return snapshot.state === "QUARANTINED_AUTO" ? "PLUGIN_QUARANTINED" : "PLUGIN_DISABLED";
|
|
2606
|
+
}
|
|
2607
|
+
function pluginBlockedMessage(snapshot) {
|
|
2608
|
+
return snapshot.reasonMessage ?? (snapshot.state === "QUARANTINED_AUTO" ? "Plugin is temporarily auto-quarantined" : "Plugin is currently disabled");
|
|
2609
|
+
}
|
|
2161
2610
|
function registerTailnetInternalRoutes(api) {
|
|
2162
2611
|
const pendingPairings = /* @__PURE__ */ new Map();
|
|
2163
2612
|
const proofNonces = /* @__PURE__ */ new Map();
|
|
@@ -2228,9 +2677,10 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2228
2677
|
};
|
|
2229
2678
|
const handleRequest = async (request) => {
|
|
2230
2679
|
const url = getRequestUrl(request);
|
|
2231
|
-
const
|
|
2680
|
+
const path12 = url.pathname;
|
|
2232
2681
|
cleanupCaches();
|
|
2233
|
-
|
|
2682
|
+
let pluginState = getPluginSafetySnapshot();
|
|
2683
|
+
if (request.method === "OPTIONS" && path12.startsWith("/squad-internal/")) {
|
|
2234
2684
|
const origin = ensureOriginAllowed(request);
|
|
2235
2685
|
if (!origin) {
|
|
2236
2686
|
return new Response(null, { status: 403 });
|
|
@@ -2248,7 +2698,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2248
2698
|
})
|
|
2249
2699
|
);
|
|
2250
2700
|
}
|
|
2251
|
-
if (request.method === "GET" &&
|
|
2701
|
+
if (request.method === "GET" && path12 === "/squad-internal/health") {
|
|
2252
2702
|
if (!isTailnetContext(request)) {
|
|
2253
2703
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2254
2704
|
}
|
|
@@ -2257,6 +2707,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2257
2707
|
json({
|
|
2258
2708
|
ok: true,
|
|
2259
2709
|
mode: "tailnet-direct",
|
|
2710
|
+
plugin: pluginState,
|
|
2260
2711
|
pairing: {
|
|
2261
2712
|
requestSupported: preferredPairingRequestMethod ?? PAIRING_REQUEST_METHODS[0],
|
|
2262
2713
|
statusSupported: preferredPairingStatusMethod ?? "auto"
|
|
@@ -2264,7 +2715,91 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2264
2715
|
})
|
|
2265
2716
|
);
|
|
2266
2717
|
}
|
|
2267
|
-
if (request.method === "
|
|
2718
|
+
if (request.method === "GET" && path12 === "/squad-internal/plugin/status") {
|
|
2719
|
+
if (!isTailnetContext(request)) {
|
|
2720
|
+
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2721
|
+
}
|
|
2722
|
+
if (!isOriginAllowedIfPresent(request)) {
|
|
2723
|
+
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
2724
|
+
}
|
|
2725
|
+
return withCors(
|
|
2726
|
+
request,
|
|
2727
|
+
json({
|
|
2728
|
+
ok: true,
|
|
2729
|
+
plugin: pluginState
|
|
2730
|
+
})
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
if (request.method === "POST" && path12 === "/squad-internal/plugin/recover") {
|
|
2734
|
+
if (!isTailnetContext(request)) {
|
|
2735
|
+
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2736
|
+
}
|
|
2737
|
+
if (!isOriginAllowedIfPresent(request)) {
|
|
2738
|
+
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
2739
|
+
}
|
|
2740
|
+
let note;
|
|
2741
|
+
try {
|
|
2742
|
+
const body = await request.json();
|
|
2743
|
+
if (typeof body?.note === "string" && body.note.trim()) {
|
|
2744
|
+
note = body.note.trim();
|
|
2745
|
+
}
|
|
2746
|
+
} catch {
|
|
2747
|
+
}
|
|
2748
|
+
const result = recoverPlugin(note ?? "Plugin recovered via internal route");
|
|
2749
|
+
if (!result.ok) {
|
|
2750
|
+
return withCors(
|
|
2751
|
+
request,
|
|
2752
|
+
json(
|
|
2753
|
+
{
|
|
2754
|
+
ok: false,
|
|
2755
|
+
code: "PLUGIN_KILL_SWITCH_ENV",
|
|
2756
|
+
error: result.message,
|
|
2757
|
+
plugin: result.snapshot
|
|
2758
|
+
},
|
|
2759
|
+
409
|
|
2760
|
+
)
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
pluginState = result.snapshot;
|
|
2764
|
+
return withCors(request, json({ ok: true, plugin: pluginState, message: result.message }));
|
|
2765
|
+
}
|
|
2766
|
+
if (request.method === "POST" && path12 === "/squad-internal/plugin/disable") {
|
|
2767
|
+
if (!isTailnetContext(request)) {
|
|
2768
|
+
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2769
|
+
}
|
|
2770
|
+
if (!isOriginAllowedIfPresent(request)) {
|
|
2771
|
+
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
2772
|
+
}
|
|
2773
|
+
let reasonCode = "MANUAL_KILL_SWITCH";
|
|
2774
|
+
let reasonMessage = "Plugin manually disabled via internal route";
|
|
2775
|
+
let remediation = "Run /squad-internal/plugin/recover after fixing plugin issues.";
|
|
2776
|
+
try {
|
|
2777
|
+
const body = await request.json();
|
|
2778
|
+
if (typeof body?.reasonCode === "string" && body.reasonCode.trim()) {
|
|
2779
|
+
reasonCode = body.reasonCode.trim();
|
|
2780
|
+
}
|
|
2781
|
+
if (typeof body?.reasonMessage === "string" && body.reasonMessage.trim()) {
|
|
2782
|
+
reasonMessage = body.reasonMessage.trim();
|
|
2783
|
+
}
|
|
2784
|
+
if (typeof body?.remediation === "string" && body.remediation.trim()) {
|
|
2785
|
+
remediation = body.remediation.trim();
|
|
2786
|
+
}
|
|
2787
|
+
} catch {
|
|
2788
|
+
}
|
|
2789
|
+
pluginState = setPluginManualDisabled(reasonCode, reasonMessage, remediation);
|
|
2790
|
+
return withCors(request, json({ ok: true, plugin: pluginState }));
|
|
2791
|
+
}
|
|
2792
|
+
if (path12.startsWith("/squad-internal/") && isPluginExecutionBlocked(pluginState)) {
|
|
2793
|
+
const code = pluginBlockedCode(pluginState);
|
|
2794
|
+
return jsonError(
|
|
2795
|
+
request,
|
|
2796
|
+
code,
|
|
2797
|
+
pluginBlockedMessage(pluginState),
|
|
2798
|
+
503,
|
|
2799
|
+
{ plugin: pluginState }
|
|
2800
|
+
);
|
|
2801
|
+
}
|
|
2802
|
+
if (request.method === "POST" && path12 === "/squad-internal/pairing/request") {
|
|
2268
2803
|
const origin = ensureOriginAllowed(request);
|
|
2269
2804
|
if (!origin) {
|
|
2270
2805
|
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
@@ -2316,7 +2851,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2316
2851
|
);
|
|
2317
2852
|
}
|
|
2318
2853
|
}
|
|
2319
|
-
if (request.method === "GET" &&
|
|
2854
|
+
if (request.method === "GET" && path12 === "/squad-internal/pairing/status") {
|
|
2320
2855
|
const origin = ensureOriginAllowed(request);
|
|
2321
2856
|
if (!origin) {
|
|
2322
2857
|
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
@@ -2365,20 +2900,32 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2365
2900
|
);
|
|
2366
2901
|
} catch (error) {
|
|
2367
2902
|
if (error instanceof TimeoutError) {
|
|
2903
|
+
const snapshot2 = recordPluginFailure(
|
|
2904
|
+
"INTERNAL_ROUTE_TIMEOUT",
|
|
2905
|
+
`${error.operation} timed out after ${error.timeoutMs}ms`,
|
|
2906
|
+
"Investigate internal route hangs and run squad.plugin.recover after remediation."
|
|
2907
|
+
);
|
|
2368
2908
|
console.warn(`[squad-openclaw] ${error.operation}`);
|
|
2369
2909
|
return jsonError(
|
|
2370
2910
|
request,
|
|
2371
|
-
"ROUTE_TIMEOUT",
|
|
2911
|
+
snapshot2.state === "QUARANTINED_AUTO" ? "PLUGIN_QUARANTINED" : "ROUTE_TIMEOUT",
|
|
2372
2912
|
`Internal route timed out after ${error.timeoutMs}ms`,
|
|
2373
|
-
504
|
|
2913
|
+
snapshot2.state === "QUARANTINED_AUTO" ? 503 : 504,
|
|
2914
|
+
snapshot2.state === "QUARANTINED_AUTO" ? { plugin: snapshot2 } : {}
|
|
2374
2915
|
);
|
|
2375
2916
|
}
|
|
2917
|
+
const snapshot = recordPluginFailure(
|
|
2918
|
+
"INTERNAL_ROUTE_ERROR",
|
|
2919
|
+
errorMessage2(error),
|
|
2920
|
+
"Inspect internal route failures and run squad.plugin.recover after remediation."
|
|
2921
|
+
);
|
|
2376
2922
|
console.warn(`[squad-openclaw] internal route failure: ${errorMessage2(error)}`);
|
|
2377
2923
|
return jsonError(
|
|
2378
2924
|
request,
|
|
2379
|
-
"INTERNAL_ROUTE_ERROR",
|
|
2925
|
+
snapshot.state === "QUARANTINED_AUTO" ? "PLUGIN_QUARANTINED" : "INTERNAL_ROUTE_ERROR",
|
|
2380
2926
|
"Internal route failed unexpectedly",
|
|
2381
|
-
500
|
|
2927
|
+
snapshot.state === "QUARANTINED_AUTO" ? 503 : 500,
|
|
2928
|
+
snapshot.state === "QUARANTINED_AUTO" ? { plugin: snapshot } : {}
|
|
2382
2929
|
);
|
|
2383
2930
|
}
|
|
2384
2931
|
};
|
|
@@ -2386,6 +2933,9 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2386
2933
|
api.registerHttpHandler(handle);
|
|
2387
2934
|
} else if (typeof api.registerHttpRoute === "function") {
|
|
2388
2935
|
api.registerHttpRoute("/squad-internal/health", handle);
|
|
2936
|
+
api.registerHttpRoute("/squad-internal/plugin/status", handle);
|
|
2937
|
+
api.registerHttpRoute("/squad-internal/plugin/recover", handle);
|
|
2938
|
+
api.registerHttpRoute("/squad-internal/plugin/disable", handle);
|
|
2389
2939
|
api.registerHttpRoute("/squad-internal/pairing/request", handle);
|
|
2390
2940
|
api.registerHttpRoute("/squad-internal/pairing/status", handle);
|
|
2391
2941
|
api.registerHttpRoute("/squad-internal/*", handle);
|
|
@@ -2396,36 +2946,122 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2396
2946
|
}
|
|
2397
2947
|
}
|
|
2398
2948
|
|
|
2949
|
+
// src/plugin-safety-gateway.ts
|
|
2950
|
+
function errorMessage3(error) {
|
|
2951
|
+
return error instanceof Error ? error.message : String(error);
|
|
2952
|
+
}
|
|
2953
|
+
function registerPluginSafetyGatewayMethods(api) {
|
|
2954
|
+
const registerGatewayMethod = typeof api?.registerGatewayMethod === "function" ? api.registerGatewayMethod.bind(api) : null;
|
|
2955
|
+
if (!registerGatewayMethod) {
|
|
2956
|
+
console.warn("[squad-openclaw] registerGatewayMethod unavailable; plugin safety methods disabled");
|
|
2957
|
+
return;
|
|
2958
|
+
}
|
|
2959
|
+
const safeRegister = (method, handler) => {
|
|
2960
|
+
try {
|
|
2961
|
+
registerGatewayMethod(method, handler);
|
|
2962
|
+
} catch (error) {
|
|
2963
|
+
console.warn(`[squad-openclaw] failed to register ${method}: ${errorMessage3(error)}`);
|
|
2964
|
+
}
|
|
2965
|
+
};
|
|
2966
|
+
safeRegister("squad.plugin.status", async ({ respond }) => {
|
|
2967
|
+
respond(true, { plugin: getPluginSafetySnapshot() });
|
|
2968
|
+
});
|
|
2969
|
+
safeRegister("squad.plugin.recover", async ({ params, respond }) => {
|
|
2970
|
+
const note = typeof params?.note === "string" ? params.note.trim() : "";
|
|
2971
|
+
const result = recoverPlugin(note || "Plugin recovered via gateway RPC");
|
|
2972
|
+
if (!result.ok) {
|
|
2973
|
+
respond(false, {
|
|
2974
|
+
code: "PLUGIN_KILL_SWITCH_ENV",
|
|
2975
|
+
error: result.message,
|
|
2976
|
+
plugin: result.snapshot
|
|
2977
|
+
});
|
|
2978
|
+
return;
|
|
2979
|
+
}
|
|
2980
|
+
respond(true, { plugin: result.snapshot, message: result.message });
|
|
2981
|
+
});
|
|
2982
|
+
safeRegister("squad.plugin.disable", async ({ params, respond }) => {
|
|
2983
|
+
const reasonCode = typeof params?.reasonCode === "string" && params.reasonCode.trim() ? params.reasonCode.trim() : "MANUAL_KILL_SWITCH";
|
|
2984
|
+
const reasonMessage = typeof params?.reasonMessage === "string" && params.reasonMessage.trim() ? params.reasonMessage.trim() : "Plugin manually disabled via gateway RPC";
|
|
2985
|
+
const remediation = typeof params?.remediation === "string" && params.remediation.trim() ? params.remediation.trim() : "Run squad.plugin.recover after resolving plugin issues.";
|
|
2986
|
+
const snapshot = setPluginManualDisabled(reasonCode, reasonMessage, remediation);
|
|
2987
|
+
respond(true, { plugin: snapshot });
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2399
2991
|
// src/index.ts
|
|
2400
2992
|
var STARTUP_MIGRATIONS_TIMEOUT_MS = readTimeoutMs("SQUAD_STARTUP_MIGRATIONS_TIMEOUT_MS", 3e4);
|
|
2401
|
-
function
|
|
2993
|
+
function errorMessage4(error) {
|
|
2402
2994
|
return error instanceof Error ? error.message : String(error);
|
|
2403
2995
|
}
|
|
2404
2996
|
function squadAppPlugin(api) {
|
|
2997
|
+
registerPluginSafetyGatewayMethods(api);
|
|
2998
|
+
let safetySnapshot = getPluginSafetySnapshot();
|
|
2999
|
+
if (safetySnapshot.blocked) {
|
|
3000
|
+
console.warn(
|
|
3001
|
+
`[squad-openclaw] plugin boot starts in ${safetySnapshot.state} mode (${safetySnapshot.reasonCode ?? "NO_REASON"})`
|
|
3002
|
+
);
|
|
3003
|
+
}
|
|
2405
3004
|
let sharedApi = null;
|
|
3005
|
+
try {
|
|
3006
|
+
registerTailnetInternalRoutes(api);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
safetySnapshot = recordPluginFailure(
|
|
3009
|
+
"STARTUP_INTERNAL_ROUTES_FAILED",
|
|
3010
|
+
errorMessage4(error),
|
|
3011
|
+
"Fix route registration errors and run squad.plugin.recover."
|
|
3012
|
+
);
|
|
3013
|
+
console.warn(`[squad-openclaw] internal route registration failed: ${errorMessage4(error)}`);
|
|
3014
|
+
}
|
|
3015
|
+
safetySnapshot = getPluginSafetySnapshot();
|
|
3016
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
3017
|
+
console.warn(
|
|
3018
|
+
`[squad-openclaw] plugin features disabled (${safetySnapshot.state}). ${safetySnapshot.reasonMessage ?? "No reason provided."}`
|
|
3019
|
+
);
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
2406
3022
|
try {
|
|
2407
3023
|
sharedApi = registerSquadSharedApi(api);
|
|
2408
3024
|
} catch (error) {
|
|
2409
|
-
|
|
3025
|
+
safetySnapshot = recordPluginFailure(
|
|
3026
|
+
"STARTUP_SHARED_API_FAILED",
|
|
3027
|
+
errorMessage4(error),
|
|
3028
|
+
"Fix shared API registration errors and run squad.plugin.recover."
|
|
3029
|
+
);
|
|
3030
|
+
console.warn(`[squad-openclaw] shared API registration failed: ${errorMessage4(error)}`);
|
|
2410
3031
|
}
|
|
2411
|
-
if (sharedApi) {
|
|
2412
|
-
|
|
2413
|
-
sharedApi.registerCoreGatewayMethods();
|
|
2414
|
-
} catch (error) {
|
|
2415
|
-
console.warn(`[squad-openclaw] core gateway method registration failed: ${errorMessage3(error)}`);
|
|
2416
|
-
}
|
|
3032
|
+
if (!sharedApi || isPluginExecutionBlocked(safetySnapshot)) {
|
|
3033
|
+
return;
|
|
2417
3034
|
}
|
|
2418
3035
|
try {
|
|
2419
|
-
|
|
3036
|
+
sharedApi.registerCoreGatewayMethods();
|
|
2420
3037
|
} catch (error) {
|
|
2421
|
-
|
|
3038
|
+
safetySnapshot = recordPluginFailure(
|
|
3039
|
+
"STARTUP_GATEWAY_METHODS_FAILED",
|
|
3040
|
+
errorMessage4(error),
|
|
3041
|
+
"Fix gateway method registration errors and run squad.plugin.recover."
|
|
3042
|
+
);
|
|
3043
|
+
console.warn(`[squad-openclaw] core gateway method registration failed: ${errorMessage4(error)}`);
|
|
3044
|
+
}
|
|
3045
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
3046
|
+
return;
|
|
2422
3047
|
}
|
|
2423
3048
|
void withTimeout(runStartupMigrations(api), STARTUP_MIGRATIONS_TIMEOUT_MS, "startup migrations").catch((error) => {
|
|
3049
|
+
const errorId = error instanceof TimeoutError ? "STARTUP_MIGRATIONS_TIMEOUT" : "STARTUP_MIGRATIONS_FAILED";
|
|
3050
|
+
const snapshot = recordPluginFailure(
|
|
3051
|
+
errorId,
|
|
3052
|
+
errorMessage4(error),
|
|
3053
|
+
"Inspect startup migration logs and run squad.plugin.recover after fixing migration failures."
|
|
3054
|
+
);
|
|
2424
3055
|
if (error instanceof TimeoutError) {
|
|
2425
3056
|
console.warn(`[squad-openclaw] startup migrations timed out after ${STARTUP_MIGRATIONS_TIMEOUT_MS}ms`);
|
|
2426
|
-
|
|
3057
|
+
} else {
|
|
3058
|
+
console.warn(`[squad-openclaw] startup migrations failed: ${errorMessage4(error)}`);
|
|
3059
|
+
}
|
|
3060
|
+
if (snapshot.blocked) {
|
|
3061
|
+
console.warn(
|
|
3062
|
+
`[squad-openclaw] plugin moved to ${snapshot.state} after startup migration failure (${snapshot.reasonCode ?? "NO_REASON"})`
|
|
3063
|
+
);
|
|
2427
3064
|
}
|
|
2428
|
-
console.warn(`[squad-openclaw] startup migrations failed: ${errorMessage3(error)}`);
|
|
2429
3065
|
});
|
|
2430
3066
|
}
|
|
2431
3067
|
export {
|
package/package.json
CHANGED