squad-openclaw 2026.2.2706 → 2026.2.2708
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 +4 -0
- package/dist/index.js +796 -108
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ OpenClaw gateway plugin for [Squad](https://squad.ceo) — provides entity regis
|
|
|
11
11
|
| `squad.agents.add` | Plugin wrapper for creating agent scaffolding (workspace seed files, sessions skeleton, and config list entry) |
|
|
12
12
|
| `squad.version.check` | Plugin version reporting |
|
|
13
13
|
| `squad.questions.validate-envelope` | HUMAN_INPUT_REQUIRED envelope validation |
|
|
14
|
+
| `squad.extensions.list`, `squad.extensions.update`, `squad.extensions.updateStatus` | Installed extension metadata, cached version checks, and update lifecycle status |
|
|
15
|
+
| `squad.gateway.restart` | Trigger gateway restart for extension-update activation |
|
|
14
16
|
| `tools.invoke`, `tools.list`, `squad.layout.get` | Core plugin RPC entrypoints for tool invocation/listing and gateway layout metadata |
|
|
15
17
|
| `squad.plugin.status`, `squad.plugin.recover`, `squad.plugin.disable` | Plugin safety-state RPC control (status, recovery, manual disable) |
|
|
16
18
|
| `GET /squad-internal/health` | Tailnet internal health + pairing capability metadata |
|
|
@@ -31,6 +33,8 @@ All `/squad-internal/*` routes enforce Tailnet context and origin checks. CORS p
|
|
|
31
33
|
| `/squad-internal/pairing/request` | `POST` | Creates pairing request via gateway-native pairing methods (`node/devices/device.pair.request`) |
|
|
32
34
|
| `/squad-internal/pairing/status` | `GET` | Resolves pairing status via gateway-native status methods (`node/devices/device.pair.status/get`) |
|
|
33
35
|
|
|
36
|
+
Extension-management routes intentionally do not expose install commands. Only update/status/restart are available via RPC methods.
|
|
37
|
+
|
|
34
38
|
## State Directory Resolution
|
|
35
39
|
|
|
36
40
|
All paths in this plugin (and throughout this README) that reference `~/.openclaw` resolve via environment override when set. This supports Docker and other containerized deployments where the OpenClaw data directory may not be at the default location.
|
package/dist/index.js
CHANGED
|
@@ -1412,12 +1412,662 @@ async function withTimeout(promise, timeoutMs, operation) {
|
|
|
1412
1412
|
}
|
|
1413
1413
|
}
|
|
1414
1414
|
|
|
1415
|
-
// src/
|
|
1416
|
-
import crypto from "crypto";
|
|
1415
|
+
// src/extensions-management.ts
|
|
1417
1416
|
import fs8 from "fs";
|
|
1418
1417
|
import path8 from "path";
|
|
1419
|
-
|
|
1420
|
-
|
|
1418
|
+
import { spawn } from "child_process";
|
|
1419
|
+
|
|
1420
|
+
// src/gateway-invoke.ts
|
|
1421
|
+
function asRecord2(value) {
|
|
1422
|
+
return value && typeof value === "object" ? value : null;
|
|
1423
|
+
}
|
|
1424
|
+
function isInvoker(fn) {
|
|
1425
|
+
return typeof fn === "function";
|
|
1426
|
+
}
|
|
1427
|
+
function isUnknownGatewayMethodError(message) {
|
|
1428
|
+
return /unknown method|method .* unavailable|not found|invalid[_ ]request|does not exist/i.test(
|
|
1429
|
+
message
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
async function callGatewayAny(ctx, api, method, params) {
|
|
1433
|
+
const ctxGateway = asRecord2(ctx.gateway);
|
|
1434
|
+
const apiGateway = asRecord2(api?.gateway);
|
|
1435
|
+
const candidates = [
|
|
1436
|
+
ctx.request,
|
|
1437
|
+
ctx.callGatewayMethod,
|
|
1438
|
+
ctx.gatewayRequest,
|
|
1439
|
+
ctx.invokeGatewayMethod,
|
|
1440
|
+
ctxGateway?.request,
|
|
1441
|
+
ctxGateway?.callGatewayMethod,
|
|
1442
|
+
api?.request,
|
|
1443
|
+
api?.callGatewayMethod,
|
|
1444
|
+
api?.gatewayRequest,
|
|
1445
|
+
api?.invokeGatewayMethod,
|
|
1446
|
+
apiGateway?.request,
|
|
1447
|
+
apiGateway?.callGatewayMethod
|
|
1448
|
+
];
|
|
1449
|
+
let lastErr = null;
|
|
1450
|
+
for (const candidate of candidates) {
|
|
1451
|
+
if (!isInvoker(candidate)) continue;
|
|
1452
|
+
try {
|
|
1453
|
+
return await candidate(method, params);
|
|
1454
|
+
} catch (err2) {
|
|
1455
|
+
lastErr = err2;
|
|
1456
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
1457
|
+
if (isUnknownGatewayMethodError(msg)) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
throw err2;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
if (lastErr) throw lastErr;
|
|
1464
|
+
throw new Error("Gateway method invocation API unavailable in plugin context");
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/extensions-management.ts
|
|
1468
|
+
var DEFAULT_TTL_MS = 60 * 60 * 1e3;
|
|
1469
|
+
var MIN_TTL_MS = 1e4;
|
|
1470
|
+
var MAX_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1471
|
+
var NPM_FETCH_TIMEOUT_MS = readTimeoutMs("SQUAD_EXTENSIONS_NPM_FETCH_TIMEOUT_MS", 5e3, 1e3, 3e4);
|
|
1472
|
+
var RESTART_METHOD_TIMEOUT_MS = readTimeoutMs("SQUAD_GATEWAY_RESTART_METHOD_TIMEOUT_MS", 2e3, 500, 1e4);
|
|
1473
|
+
var OPENCLAW_BIN = process.env.SQUAD_OPENCLAW_BIN?.trim() || "openclaw";
|
|
1474
|
+
var DEFAULT_REGISTRY_BASE_URL = "https://registry.npmjs.org";
|
|
1475
|
+
var NPM_REGISTRY_BASE_URL = (process.env.SQUAD_NPM_REGISTRY_BASE_URL?.trim() || DEFAULT_REGISTRY_BASE_URL).replace(/\/+$/g, "");
|
|
1476
|
+
var updateStatuses = /* @__PURE__ */ new Map();
|
|
1477
|
+
var activeUpdatePluginId = null;
|
|
1478
|
+
var ExtensionsManagementError = class extends Error {
|
|
1479
|
+
code;
|
|
1480
|
+
details;
|
|
1481
|
+
constructor(code, message, details = {}) {
|
|
1482
|
+
super(message);
|
|
1483
|
+
this.code = code;
|
|
1484
|
+
this.details = details;
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
function isExtensionsManagementError(error) {
|
|
1488
|
+
return error instanceof ExtensionsManagementError;
|
|
1489
|
+
}
|
|
1490
|
+
function toExtensionsErrorPayload(error) {
|
|
1491
|
+
return {
|
|
1492
|
+
code: error.code,
|
|
1493
|
+
error: error.message,
|
|
1494
|
+
errorCode: error.code,
|
|
1495
|
+
errorMessage: error.message,
|
|
1496
|
+
...error.details
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function nowIso() {
|
|
1500
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1501
|
+
}
|
|
1502
|
+
function parseMs(value) {
|
|
1503
|
+
if (!value) return null;
|
|
1504
|
+
const parsed = Date.parse(value);
|
|
1505
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1506
|
+
}
|
|
1507
|
+
function normalizeRefresh(value) {
|
|
1508
|
+
if (value === "force" || value === "cache-only") return value;
|
|
1509
|
+
return "if-stale";
|
|
1510
|
+
}
|
|
1511
|
+
function normalizeTtl(value) {
|
|
1512
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1513
|
+
return DEFAULT_TTL_MS;
|
|
1514
|
+
}
|
|
1515
|
+
const rounded = Math.floor(value);
|
|
1516
|
+
if (rounded < MIN_TTL_MS) return MIN_TTL_MS;
|
|
1517
|
+
if (rounded > MAX_TTL_MS) return MAX_TTL_MS;
|
|
1518
|
+
return rounded;
|
|
1519
|
+
}
|
|
1520
|
+
function asNonEmptyString(value) {
|
|
1521
|
+
if (typeof value !== "string") return null;
|
|
1522
|
+
const trimmed = value.trim();
|
|
1523
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
1524
|
+
}
|
|
1525
|
+
function readJsonFile(filePath) {
|
|
1526
|
+
try {
|
|
1527
|
+
return JSON.parse(fs8.readFileSync(filePath, "utf-8"));
|
|
1528
|
+
} catch {
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function ensureParentDir(filePath) {
|
|
1533
|
+
fs8.mkdirSync(path8.dirname(filePath), { recursive: true });
|
|
1534
|
+
}
|
|
1535
|
+
function writeJsonFileAtomic(filePath, payload) {
|
|
1536
|
+
ensureParentDir(filePath);
|
|
1537
|
+
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
1538
|
+
fs8.writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}
|
|
1539
|
+
`, "utf-8");
|
|
1540
|
+
fs8.renameSync(tempPath, filePath);
|
|
1541
|
+
}
|
|
1542
|
+
function normalizeSource(value) {
|
|
1543
|
+
if (value === "npm") return "npm";
|
|
1544
|
+
if (typeof value === "string" && value.trim()) return "other";
|
|
1545
|
+
return "unknown";
|
|
1546
|
+
}
|
|
1547
|
+
function normalizePluginStatus(value) {
|
|
1548
|
+
if (value === "up_to_date" || value === "update_available" || value === "unknown" || value === "error") {
|
|
1549
|
+
return value;
|
|
1550
|
+
}
|
|
1551
|
+
return "unknown";
|
|
1552
|
+
}
|
|
1553
|
+
function defaultUpdateStatus() {
|
|
1554
|
+
return {
|
|
1555
|
+
state: "idle",
|
|
1556
|
+
startedAt: null,
|
|
1557
|
+
finishedAt: null,
|
|
1558
|
+
needsRestart: false,
|
|
1559
|
+
error: null
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
function setUpdateStatus(pluginId, next) {
|
|
1563
|
+
updateStatuses.set(pluginId, next);
|
|
1564
|
+
}
|
|
1565
|
+
function appendOutputTail(current, chunk, maxChars = 6e3) {
|
|
1566
|
+
const combined = current + chunk;
|
|
1567
|
+
if (combined.length <= maxChars) return combined;
|
|
1568
|
+
return combined.slice(-maxChars);
|
|
1569
|
+
}
|
|
1570
|
+
function updateCachePath(stateDir) {
|
|
1571
|
+
return path8.join(stateDir, "squad-ceo-data", "extensions", "update-check-cache.json");
|
|
1572
|
+
}
|
|
1573
|
+
function readUpdateCheckCache(stateDir) {
|
|
1574
|
+
const filePath = updateCachePath(stateDir);
|
|
1575
|
+
const raw = readJsonFile(filePath);
|
|
1576
|
+
const entries = {};
|
|
1577
|
+
const rawEntries = raw?.entries;
|
|
1578
|
+
if (rawEntries && typeof rawEntries === "object" && !Array.isArray(rawEntries)) {
|
|
1579
|
+
for (const [pluginId, value] of Object.entries(rawEntries)) {
|
|
1580
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
1581
|
+
const record = value;
|
|
1582
|
+
const checkedAt = asNonEmptyString(record.checkedAt);
|
|
1583
|
+
if (!checkedAt) continue;
|
|
1584
|
+
entries[pluginId] = {
|
|
1585
|
+
pluginId,
|
|
1586
|
+
packageName: asNonEmptyString(record.packageName),
|
|
1587
|
+
checkedAt,
|
|
1588
|
+
latestVersion: asNonEmptyString(record.latestVersion),
|
|
1589
|
+
status: normalizePluginStatus(record.status),
|
|
1590
|
+
checkError: asNonEmptyString(record.checkError)
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
return {
|
|
1595
|
+
version: 1,
|
|
1596
|
+
updatedAt: asNonEmptyString(raw?.updatedAt) ?? nowIso(),
|
|
1597
|
+
entries
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function writeUpdateCheckCache(stateDir, cache) {
|
|
1601
|
+
const filePath = updateCachePath(stateDir);
|
|
1602
|
+
writeJsonFileAtomic(filePath, cache);
|
|
1603
|
+
}
|
|
1604
|
+
function invalidateUpdateCheckCacheEntry(stateDir, pluginId) {
|
|
1605
|
+
const cache = readUpdateCheckCache(stateDir);
|
|
1606
|
+
if (!(pluginId in cache.entries)) return;
|
|
1607
|
+
delete cache.entries[pluginId];
|
|
1608
|
+
cache.updatedAt = nowIso();
|
|
1609
|
+
writeUpdateCheckCache(stateDir, cache);
|
|
1610
|
+
}
|
|
1611
|
+
function loadInstallConfig(stateDir) {
|
|
1612
|
+
const configPath = path8.join(stateDir, "openclaw.json");
|
|
1613
|
+
const config = readJsonFile(configPath) ?? {};
|
|
1614
|
+
const plugins = config.plugins ?? {};
|
|
1615
|
+
const installs = plugins.installs ?? {};
|
|
1616
|
+
const result = {};
|
|
1617
|
+
for (const [pluginId, value] of Object.entries(installs)) {
|
|
1618
|
+
const record = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1619
|
+
result[pluginId] = {
|
|
1620
|
+
source: normalizeSource(record.source),
|
|
1621
|
+
spec: asNonEmptyString(record.spec),
|
|
1622
|
+
version: asNonEmptyString(record.version),
|
|
1623
|
+
installPath: asNonEmptyString(record.installPath)
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
return result;
|
|
1627
|
+
}
|
|
1628
|
+
function derivePackageNameFromSpec(spec) {
|
|
1629
|
+
if (!spec) return null;
|
|
1630
|
+
let normalized = spec.trim();
|
|
1631
|
+
if (!normalized) return null;
|
|
1632
|
+
if (normalized.startsWith("npm:")) {
|
|
1633
|
+
normalized = normalized.slice(4);
|
|
1634
|
+
}
|
|
1635
|
+
if (normalized.startsWith("@")) {
|
|
1636
|
+
const slashIndex = normalized.indexOf("/");
|
|
1637
|
+
if (slashIndex < 1) return null;
|
|
1638
|
+
const versionAtIndex2 = normalized.indexOf("@", slashIndex + 1);
|
|
1639
|
+
return versionAtIndex2 === -1 ? normalized : normalized.slice(0, versionAtIndex2);
|
|
1640
|
+
}
|
|
1641
|
+
const versionAtIndex = normalized.indexOf("@");
|
|
1642
|
+
return versionAtIndex === -1 ? normalized : normalized.slice(0, versionAtIndex);
|
|
1643
|
+
}
|
|
1644
|
+
function isValidNpmPackageName(value) {
|
|
1645
|
+
if (!value) return false;
|
|
1646
|
+
return /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(value);
|
|
1647
|
+
}
|
|
1648
|
+
function isValidSpecForCommand(value) {
|
|
1649
|
+
if (!value) return false;
|
|
1650
|
+
return /^[a-z0-9@._/-]+$/i.test(value);
|
|
1651
|
+
}
|
|
1652
|
+
function readInstalledPluginDescriptors(stateDir) {
|
|
1653
|
+
const extensionsDir = path8.join(stateDir, "extensions");
|
|
1654
|
+
let entries = [];
|
|
1655
|
+
try {
|
|
1656
|
+
entries = fs8.readdirSync(extensionsDir, { withFileTypes: true });
|
|
1657
|
+
} catch {
|
|
1658
|
+
return [];
|
|
1659
|
+
}
|
|
1660
|
+
const installs = loadInstallConfig(stateDir);
|
|
1661
|
+
const descriptors = [];
|
|
1662
|
+
for (const entry of entries) {
|
|
1663
|
+
if (!entry.isDirectory()) continue;
|
|
1664
|
+
const pluginDir = path8.join(extensionsDir, entry.name);
|
|
1665
|
+
const manifestPath = path8.join(pluginDir, "openclaw.plugin.json");
|
|
1666
|
+
const manifest = readJsonFile(manifestPath);
|
|
1667
|
+
if (!manifest) continue;
|
|
1668
|
+
const pluginId = asNonEmptyString(manifest.id) ?? entry.name;
|
|
1669
|
+
const packageJson = readJsonFile(path8.join(pluginDir, "package.json"));
|
|
1670
|
+
const install = installs[pluginId] ?? {
|
|
1671
|
+
source: "unknown",
|
|
1672
|
+
spec: null,
|
|
1673
|
+
version: null,
|
|
1674
|
+
installPath: null
|
|
1675
|
+
};
|
|
1676
|
+
const packageName = asNonEmptyString(packageJson?.name) ?? derivePackageNameFromSpec(install.spec);
|
|
1677
|
+
const source = install.source;
|
|
1678
|
+
descriptors.push({
|
|
1679
|
+
pluginId,
|
|
1680
|
+
name: asNonEmptyString(manifest.name) ?? asNonEmptyString(packageJson?.name) ?? pluginId,
|
|
1681
|
+
description: asNonEmptyString(manifest.description) ?? asNonEmptyString(packageJson?.description),
|
|
1682
|
+
pluginDir,
|
|
1683
|
+
packageName: isValidNpmPackageName(packageName) ? packageName : null,
|
|
1684
|
+
currentVersion: asNonEmptyString(packageJson?.version) ?? install.version,
|
|
1685
|
+
source,
|
|
1686
|
+
spec: install.spec
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
descriptors.sort((a, b) => a.name.localeCompare(b.name));
|
|
1690
|
+
return descriptors;
|
|
1691
|
+
}
|
|
1692
|
+
function comparePrerelease(a, b) {
|
|
1693
|
+
if (!a && !b) return 0;
|
|
1694
|
+
if (!a) return 1;
|
|
1695
|
+
if (!b) return -1;
|
|
1696
|
+
const aParts = a.split(".");
|
|
1697
|
+
const bParts = b.split(".");
|
|
1698
|
+
const maxLen = Math.max(aParts.length, bParts.length);
|
|
1699
|
+
for (let i = 0; i < maxLen; i++) {
|
|
1700
|
+
const aPart = aParts[i];
|
|
1701
|
+
const bPart = bParts[i];
|
|
1702
|
+
if (aPart == null) return -1;
|
|
1703
|
+
if (bPart == null) return 1;
|
|
1704
|
+
const aNum = Number(aPart);
|
|
1705
|
+
const bNum = Number(bPart);
|
|
1706
|
+
const bothNumeric = Number.isInteger(aNum) && Number.isInteger(bNum);
|
|
1707
|
+
if (bothNumeric) {
|
|
1708
|
+
if (aNum > bNum) return 1;
|
|
1709
|
+
if (aNum < bNum) return -1;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
const cmp = aPart.localeCompare(bPart);
|
|
1713
|
+
if (cmp !== 0) return cmp > 0 ? 1 : -1;
|
|
1714
|
+
}
|
|
1715
|
+
return 0;
|
|
1716
|
+
}
|
|
1717
|
+
function compareVersionValues(a, b) {
|
|
1718
|
+
const semverRe = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
1719
|
+
const aMatch = a.match(semverRe);
|
|
1720
|
+
const bMatch = b.match(semverRe);
|
|
1721
|
+
if (!aMatch || !bMatch) {
|
|
1722
|
+
const cmp = a.localeCompare(b, void 0, { numeric: true, sensitivity: "base" });
|
|
1723
|
+
if (cmp === 0) return 0;
|
|
1724
|
+
return cmp > 0 ? 1 : -1;
|
|
1725
|
+
}
|
|
1726
|
+
for (let i = 1; i <= 3; i++) {
|
|
1727
|
+
const aPart = Number(aMatch[i]);
|
|
1728
|
+
const bPart = Number(bMatch[i]);
|
|
1729
|
+
if (aPart > bPart) return 1;
|
|
1730
|
+
if (aPart < bPart) return -1;
|
|
1731
|
+
}
|
|
1732
|
+
return comparePrerelease(aMatch[4] ?? null, bMatch[4] ?? null);
|
|
1733
|
+
}
|
|
1734
|
+
function supportsNpmUpdate(plugin) {
|
|
1735
|
+
return plugin.source === "npm" && isValidNpmPackageName(plugin.packageName);
|
|
1736
|
+
}
|
|
1737
|
+
function resolveUpdateSpec(plugin) {
|
|
1738
|
+
if (isValidSpecForCommand(plugin.spec)) return plugin.spec;
|
|
1739
|
+
if (isValidSpecForCommand(plugin.packageName)) return plugin.packageName;
|
|
1740
|
+
return null;
|
|
1741
|
+
}
|
|
1742
|
+
async function fetchLatestVersion(packageName) {
|
|
1743
|
+
const encoded = encodeURIComponent(packageName);
|
|
1744
|
+
const url = `${NPM_REGISTRY_BASE_URL}/${encoded}/latest`;
|
|
1745
|
+
const controller = new AbortController();
|
|
1746
|
+
const timer = setTimeout(() => controller.abort(), NPM_FETCH_TIMEOUT_MS);
|
|
1747
|
+
try {
|
|
1748
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
1749
|
+
if (!response.ok) {
|
|
1750
|
+
return {
|
|
1751
|
+
latestVersion: null,
|
|
1752
|
+
checkError: `Registry request failed (${response.status})`
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
const json2 = await response.json();
|
|
1756
|
+
const latestVersion = asNonEmptyString(json2?.version);
|
|
1757
|
+
if (!latestVersion) {
|
|
1758
|
+
return {
|
|
1759
|
+
latestVersion: null,
|
|
1760
|
+
checkError: "Registry response did not include a valid version."
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
return { latestVersion, checkError: null };
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
const isAbortError = !!(error && typeof error === "object" && "name" in error && error.name === "AbortError");
|
|
1766
|
+
if (isAbortError) {
|
|
1767
|
+
return { latestVersion: null, checkError: `Registry request timed out after ${NPM_FETCH_TIMEOUT_MS}ms` };
|
|
1768
|
+
}
|
|
1769
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1770
|
+
return { latestVersion: null, checkError: message };
|
|
1771
|
+
} finally {
|
|
1772
|
+
clearTimeout(timer);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function shouldRefreshCacheEntry(mode, ttlMs, now, entry) {
|
|
1776
|
+
if (mode === "cache-only") return false;
|
|
1777
|
+
if (mode === "force") return true;
|
|
1778
|
+
if (!entry) return true;
|
|
1779
|
+
const checkedAtMs = parseMs(entry.checkedAt);
|
|
1780
|
+
if (checkedAtMs == null) return true;
|
|
1781
|
+
return now - checkedAtMs > ttlMs;
|
|
1782
|
+
}
|
|
1783
|
+
async function listExtensions(params = {}) {
|
|
1784
|
+
const stateDir = getOpenclawStateDir();
|
|
1785
|
+
const refreshMode = normalizeRefresh(params.refresh);
|
|
1786
|
+
const ttlMs = normalizeTtl(params.ttlMs);
|
|
1787
|
+
const nowMs = Date.now();
|
|
1788
|
+
const now = nowIso();
|
|
1789
|
+
const plugins = readInstalledPluginDescriptors(stateDir);
|
|
1790
|
+
const cache = readUpdateCheckCache(stateDir);
|
|
1791
|
+
let cacheDirty = false;
|
|
1792
|
+
let maxCheckedAtMs = null;
|
|
1793
|
+
const resultPlugins = [];
|
|
1794
|
+
for (const plugin of plugins) {
|
|
1795
|
+
const updateSupport = supportsNpmUpdate(plugin) ? "supported" : "unsupported";
|
|
1796
|
+
let latestVersion = null;
|
|
1797
|
+
let status = "unknown";
|
|
1798
|
+
let checkError = null;
|
|
1799
|
+
let checkedAt = null;
|
|
1800
|
+
const cacheEntry = cache.entries[plugin.pluginId] ?? null;
|
|
1801
|
+
if (updateSupport === "supported") {
|
|
1802
|
+
if (shouldRefreshCacheEntry(refreshMode, ttlMs, nowMs, cacheEntry)) {
|
|
1803
|
+
const fetchResult = await fetchLatestVersion(plugin.packageName);
|
|
1804
|
+
if (fetchResult.checkError) {
|
|
1805
|
+
latestVersion = cacheEntry?.latestVersion ?? null;
|
|
1806
|
+
status = "error";
|
|
1807
|
+
checkError = fetchResult.checkError;
|
|
1808
|
+
checkedAt = now;
|
|
1809
|
+
} else {
|
|
1810
|
+
latestVersion = fetchResult.latestVersion;
|
|
1811
|
+
status = "unknown";
|
|
1812
|
+
checkError = null;
|
|
1813
|
+
checkedAt = now;
|
|
1814
|
+
}
|
|
1815
|
+
cache.entries[plugin.pluginId] = {
|
|
1816
|
+
pluginId: plugin.pluginId,
|
|
1817
|
+
packageName: plugin.packageName,
|
|
1818
|
+
checkedAt,
|
|
1819
|
+
latestVersion,
|
|
1820
|
+
status,
|
|
1821
|
+
checkError
|
|
1822
|
+
};
|
|
1823
|
+
cacheDirty = true;
|
|
1824
|
+
} else if (cacheEntry) {
|
|
1825
|
+
latestVersion = cacheEntry.latestVersion;
|
|
1826
|
+
status = cacheEntry.status;
|
|
1827
|
+
checkError = cacheEntry.checkError;
|
|
1828
|
+
checkedAt = cacheEntry.checkedAt;
|
|
1829
|
+
}
|
|
1830
|
+
if (plugin.currentVersion && latestVersion) {
|
|
1831
|
+
const updateAvailable2 = compareVersionValues(latestVersion, plugin.currentVersion) > 0;
|
|
1832
|
+
if (status !== "error") {
|
|
1833
|
+
status = updateAvailable2 ? "update_available" : "up_to_date";
|
|
1834
|
+
}
|
|
1835
|
+
} else if (status !== "error") {
|
|
1836
|
+
status = "unknown";
|
|
1837
|
+
}
|
|
1838
|
+
} else {
|
|
1839
|
+
status = "unknown";
|
|
1840
|
+
}
|
|
1841
|
+
const checkedAtMs = parseMs(checkedAt);
|
|
1842
|
+
if (checkedAtMs != null && (maxCheckedAtMs == null || checkedAtMs > maxCheckedAtMs)) {
|
|
1843
|
+
maxCheckedAtMs = checkedAtMs;
|
|
1844
|
+
}
|
|
1845
|
+
const updateAvailable = updateSupport === "supported" && latestVersion && plugin.currentVersion ? compareVersionValues(latestVersion, plugin.currentVersion) > 0 : null;
|
|
1846
|
+
resultPlugins.push({
|
|
1847
|
+
pluginId: plugin.pluginId,
|
|
1848
|
+
name: plugin.name,
|
|
1849
|
+
description: plugin.description,
|
|
1850
|
+
currentVersion: plugin.currentVersion,
|
|
1851
|
+
latestVersion,
|
|
1852
|
+
updateAvailable,
|
|
1853
|
+
updateSupport,
|
|
1854
|
+
source: plugin.source,
|
|
1855
|
+
spec: plugin.spec,
|
|
1856
|
+
packageName: plugin.packageName,
|
|
1857
|
+
status,
|
|
1858
|
+
checkError,
|
|
1859
|
+
checkedAt
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
if (cacheDirty) {
|
|
1863
|
+
cache.updatedAt = now;
|
|
1864
|
+
writeUpdateCheckCache(stateDir, cache);
|
|
1865
|
+
}
|
|
1866
|
+
return {
|
|
1867
|
+
checkedAt: maxCheckedAtMs == null ? null : new Date(maxCheckedAtMs).toISOString(),
|
|
1868
|
+
ttlMs,
|
|
1869
|
+
plugins: resultPlugins
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function buildOpenClawCommandEnv(stateDir) {
|
|
1873
|
+
return {
|
|
1874
|
+
...process.env,
|
|
1875
|
+
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR ?? stateDir,
|
|
1876
|
+
OPENCLAW_CONFIG_PATH: process.env.OPENCLAW_CONFIG_PATH ?? path8.join(stateDir, "openclaw.json"),
|
|
1877
|
+
OPENCLAW_HOME: process.env.OPENCLAW_HOME ?? path8.join(stateDir, "home")
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
function listInstalledPluginIds(stateDir) {
|
|
1881
|
+
return readInstalledPluginDescriptors(stateDir).map((plugin) => plugin.pluginId);
|
|
1882
|
+
}
|
|
1883
|
+
function normalizePluginIds(value) {
|
|
1884
|
+
if (!Array.isArray(value)) return null;
|
|
1885
|
+
const ids = [];
|
|
1886
|
+
for (const entry of value) {
|
|
1887
|
+
const id = asNonEmptyString(entry);
|
|
1888
|
+
if (!id) continue;
|
|
1889
|
+
if (!ids.includes(id)) ids.push(id);
|
|
1890
|
+
}
|
|
1891
|
+
return ids;
|
|
1892
|
+
}
|
|
1893
|
+
function getExtensionsUpdateStatus(params = {}) {
|
|
1894
|
+
const stateDir = getOpenclawStateDir();
|
|
1895
|
+
const requestedIds = normalizePluginIds(params.pluginIds);
|
|
1896
|
+
const pluginIds = requestedIds ?? listInstalledPluginIds(stateDir);
|
|
1897
|
+
for (const id of updateStatuses.keys()) {
|
|
1898
|
+
if (!pluginIds.includes(id) && requestedIds == null) {
|
|
1899
|
+
pluginIds.push(id);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
const statuses = {};
|
|
1903
|
+
for (const pluginId of pluginIds) {
|
|
1904
|
+
statuses[pluginId] = updateStatuses.get(pluginId) ?? defaultUpdateStatus();
|
|
1905
|
+
}
|
|
1906
|
+
return { statuses };
|
|
1907
|
+
}
|
|
1908
|
+
function updateFinishMessage(command, code, signal, stdoutTail, stderrTail) {
|
|
1909
|
+
const details = [];
|
|
1910
|
+
details.push(`${command} exited with code=${String(code)} signal=${String(signal)}`);
|
|
1911
|
+
if (stderrTail.trim()) {
|
|
1912
|
+
details.push(`stderr: ${stderrTail.trim()}`);
|
|
1913
|
+
} else if (stdoutTail.trim()) {
|
|
1914
|
+
details.push(`stdout: ${stdoutTail.trim()}`);
|
|
1915
|
+
}
|
|
1916
|
+
return details.join(" | ");
|
|
1917
|
+
}
|
|
1918
|
+
function startExtensionUpdate(params) {
|
|
1919
|
+
const pluginId = asNonEmptyString(params.pluginId);
|
|
1920
|
+
if (!pluginId) {
|
|
1921
|
+
throw new ExtensionsManagementError("INVALID_REQUEST", "pluginId is required");
|
|
1922
|
+
}
|
|
1923
|
+
if (activeUpdatePluginId) {
|
|
1924
|
+
const activeStatus = updateStatuses.get(activeUpdatePluginId);
|
|
1925
|
+
if (activeStatus?.state === "running") {
|
|
1926
|
+
throw new ExtensionsManagementError(
|
|
1927
|
+
"UPDATE_IN_PROGRESS",
|
|
1928
|
+
`Another extension update is already in progress (${activeUpdatePluginId}).`,
|
|
1929
|
+
{ inProgressPluginId: activeUpdatePluginId }
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
const stateDir = getOpenclawStateDir();
|
|
1934
|
+
const plugins = readInstalledPluginDescriptors(stateDir);
|
|
1935
|
+
const plugin = plugins.find((entry) => entry.pluginId === pluginId);
|
|
1936
|
+
if (!plugin) {
|
|
1937
|
+
throw new ExtensionsManagementError("EXTENSION_NOT_FOUND", `Extension '${pluginId}' is not installed.`);
|
|
1938
|
+
}
|
|
1939
|
+
if (!supportsNpmUpdate(plugin)) {
|
|
1940
|
+
throw new ExtensionsManagementError(
|
|
1941
|
+
"UPDATE_NOT_SUPPORTED",
|
|
1942
|
+
`Extension '${pluginId}' does not support updates via npm.`
|
|
1943
|
+
);
|
|
1944
|
+
}
|
|
1945
|
+
const spec = resolveUpdateSpec(plugin);
|
|
1946
|
+
if (!spec) {
|
|
1947
|
+
throw new ExtensionsManagementError(
|
|
1948
|
+
"UPDATE_NOT_SUPPORTED",
|
|
1949
|
+
`Extension '${pluginId}' does not have a valid npm spec for updates.`
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
const startedAt = nowIso();
|
|
1953
|
+
setUpdateStatus(pluginId, {
|
|
1954
|
+
state: "running",
|
|
1955
|
+
startedAt,
|
|
1956
|
+
finishedAt: null,
|
|
1957
|
+
needsRestart: false,
|
|
1958
|
+
error: null
|
|
1959
|
+
});
|
|
1960
|
+
activeUpdatePluginId = pluginId;
|
|
1961
|
+
const commandArgs = ["plugins", "update", spec];
|
|
1962
|
+
const child = spawn(OPENCLAW_BIN, commandArgs, {
|
|
1963
|
+
env: buildOpenClawCommandEnv(stateDir),
|
|
1964
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1965
|
+
});
|
|
1966
|
+
let stdoutTail = "";
|
|
1967
|
+
let stderrTail = "";
|
|
1968
|
+
let finalized = false;
|
|
1969
|
+
const finalize = (status) => {
|
|
1970
|
+
if (finalized) return;
|
|
1971
|
+
finalized = true;
|
|
1972
|
+
setUpdateStatus(pluginId, status);
|
|
1973
|
+
if (activeUpdatePluginId === pluginId) {
|
|
1974
|
+
activeUpdatePluginId = null;
|
|
1975
|
+
}
|
|
1976
|
+
invalidateUpdateCheckCacheEntry(stateDir, pluginId);
|
|
1977
|
+
};
|
|
1978
|
+
child.stdout?.on("data", (chunk) => {
|
|
1979
|
+
stdoutTail = appendOutputTail(stdoutTail, chunk.toString("utf-8"));
|
|
1980
|
+
});
|
|
1981
|
+
child.stderr?.on("data", (chunk) => {
|
|
1982
|
+
stderrTail = appendOutputTail(stderrTail, chunk.toString("utf-8"));
|
|
1983
|
+
});
|
|
1984
|
+
child.on("error", (error) => {
|
|
1985
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1986
|
+
finalize({
|
|
1987
|
+
state: "failed",
|
|
1988
|
+
startedAt,
|
|
1989
|
+
finishedAt: nowIso(),
|
|
1990
|
+
needsRestart: false,
|
|
1991
|
+
error: `Failed to start update command: ${message}`
|
|
1992
|
+
});
|
|
1993
|
+
});
|
|
1994
|
+
child.on("exit", (code, signal) => {
|
|
1995
|
+
const finishedAt = nowIso();
|
|
1996
|
+
if (code === 0) {
|
|
1997
|
+
finalize({
|
|
1998
|
+
state: "success",
|
|
1999
|
+
startedAt,
|
|
2000
|
+
finishedAt,
|
|
2001
|
+
needsRestart: true,
|
|
2002
|
+
error: null
|
|
2003
|
+
});
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
finalize({
|
|
2007
|
+
state: "failed",
|
|
2008
|
+
startedAt,
|
|
2009
|
+
finishedAt,
|
|
2010
|
+
needsRestart: false,
|
|
2011
|
+
error: updateFinishMessage(`${OPENCLAW_BIN} ${commandArgs.join(" ")}`, code, signal, stdoutTail, stderrTail)
|
|
2012
|
+
});
|
|
2013
|
+
});
|
|
2014
|
+
return {
|
|
2015
|
+
accepted: true,
|
|
2016
|
+
pluginId,
|
|
2017
|
+
state: "running"
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
async function tryGatewayRestartMethod(payload, api) {
|
|
2021
|
+
const methods = [
|
|
2022
|
+
"gateway.restart",
|
|
2023
|
+
"openclaw.gateway.restart",
|
|
2024
|
+
"system.restart",
|
|
2025
|
+
"node.restart"
|
|
2026
|
+
];
|
|
2027
|
+
for (const method of methods) {
|
|
2028
|
+
try {
|
|
2029
|
+
await withTimeout(
|
|
2030
|
+
callGatewayAny(payload, api, method, {}),
|
|
2031
|
+
RESTART_METHOD_TIMEOUT_MS,
|
|
2032
|
+
`restart method ${method}`
|
|
2033
|
+
);
|
|
2034
|
+
return true;
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2037
|
+
if (isUnknownGatewayMethodError(message)) {
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
return false;
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
return false;
|
|
2044
|
+
}
|
|
2045
|
+
function spawnGatewayRestartCommand(stateDir) {
|
|
2046
|
+
const child = spawn(OPENCLAW_BIN, ["gateway", "restart"], {
|
|
2047
|
+
env: buildOpenClawCommandEnv(stateDir),
|
|
2048
|
+
detached: true,
|
|
2049
|
+
stdio: "ignore"
|
|
2050
|
+
});
|
|
2051
|
+
child.unref();
|
|
2052
|
+
}
|
|
2053
|
+
async function restartGateway(payload, api) {
|
|
2054
|
+
const stateDir = getOpenclawStateDir();
|
|
2055
|
+
const usedGatewayMethod = await tryGatewayRestartMethod(payload, api);
|
|
2056
|
+
if (!usedGatewayMethod) {
|
|
2057
|
+
spawnGatewayRestartCommand(stateDir);
|
|
2058
|
+
}
|
|
2059
|
+
return {
|
|
2060
|
+
accepted: true,
|
|
2061
|
+
issuedAt: nowIso()
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/plugin-safety-state.ts
|
|
2066
|
+
import crypto from "crypto";
|
|
2067
|
+
import fs9 from "fs";
|
|
2068
|
+
import path9 from "path";
|
|
2069
|
+
var SAFETY_DIR = path9.join(getOpenclawStateDir(), "squad-ceo-data", "safety");
|
|
2070
|
+
var PLUGIN_SAFETY_STATE_PATH = path9.join(SAFETY_DIR, "plugin-state.json");
|
|
1421
2071
|
var FAILURE_THRESHOLD = readIntegerEnv("SQUAD_PLUGIN_FAILURE_THRESHOLD", 3, 1, 50);
|
|
1422
2072
|
var FAILURE_WINDOW_MS = readTimeoutMs("SQUAD_PLUGIN_FAILURE_WINDOW_MS", 5 * 60 * 1e3, 1e3, 24 * 60 * 60 * 1e3);
|
|
1423
2073
|
var QUARANTINE_MS = readTimeoutMs("SQUAD_PLUGIN_QUARANTINE_MS", 10 * 60 * 1e3, 1e3, 24 * 60 * 60 * 1e3);
|
|
@@ -1431,7 +2081,7 @@ function readIntegerEnv(envName, fallback, min, max) {
|
|
|
1431
2081
|
if (rounded > max) return max;
|
|
1432
2082
|
return rounded;
|
|
1433
2083
|
}
|
|
1434
|
-
function
|
|
2084
|
+
function nowIso2() {
|
|
1435
2085
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1436
2086
|
}
|
|
1437
2087
|
function normalizeIso(value) {
|
|
@@ -1456,7 +2106,7 @@ function normalizeFailureCount(value) {
|
|
|
1456
2106
|
const rounded = Math.floor(value);
|
|
1457
2107
|
return rounded > 0 ? rounded : 0;
|
|
1458
2108
|
}
|
|
1459
|
-
function
|
|
2109
|
+
function parseMs2(value) {
|
|
1460
2110
|
if (!value) return null;
|
|
1461
2111
|
const parsed = Date.parse(value);
|
|
1462
2112
|
if (!Number.isFinite(parsed)) return null;
|
|
@@ -1477,7 +2127,7 @@ function defaultPersistedState() {
|
|
|
1477
2127
|
failureWindowStartedAt: null,
|
|
1478
2128
|
quarantineUntil: null,
|
|
1479
2129
|
lastErrorId: null,
|
|
1480
|
-
updatedAt:
|
|
2130
|
+
updatedAt: nowIso2()
|
|
1481
2131
|
};
|
|
1482
2132
|
}
|
|
1483
2133
|
function sanitizePersistedState(value) {
|
|
@@ -1498,12 +2148,12 @@ function sanitizePersistedState(value) {
|
|
|
1498
2148
|
failureWindowStartedAt: normalizeIso(record.failureWindowStartedAt),
|
|
1499
2149
|
quarantineUntil: normalizeIso(record.quarantineUntil),
|
|
1500
2150
|
lastErrorId: normalizeString(record.lastErrorId),
|
|
1501
|
-
updatedAt: normalizeIso(record.updatedAt) ??
|
|
2151
|
+
updatedAt: normalizeIso(record.updatedAt) ?? nowIso2()
|
|
1502
2152
|
};
|
|
1503
2153
|
}
|
|
1504
2154
|
function readPersistedState() {
|
|
1505
2155
|
try {
|
|
1506
|
-
const raw =
|
|
2156
|
+
const raw = fs9.readFileSync(PLUGIN_SAFETY_STATE_PATH, "utf-8");
|
|
1507
2157
|
return sanitizePersistedState(JSON.parse(raw));
|
|
1508
2158
|
} catch {
|
|
1509
2159
|
return defaultPersistedState();
|
|
@@ -1511,11 +2161,11 @@ function readPersistedState() {
|
|
|
1511
2161
|
}
|
|
1512
2162
|
function writePersistedState(state) {
|
|
1513
2163
|
try {
|
|
1514
|
-
|
|
2164
|
+
fs9.mkdirSync(SAFETY_DIR, { recursive: true });
|
|
1515
2165
|
const tempPath = `${PLUGIN_SAFETY_STATE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
|
1516
|
-
|
|
2166
|
+
fs9.writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}
|
|
1517
2167
|
`, "utf-8");
|
|
1518
|
-
|
|
2168
|
+
fs9.renameSync(tempPath, PLUGIN_SAFETY_STATE_PATH);
|
|
1519
2169
|
} catch (error) {
|
|
1520
2170
|
const message = error instanceof Error ? error.message : String(error);
|
|
1521
2171
|
console.warn(`[squad-openclaw] failed to persist plugin safety state: ${message}`);
|
|
@@ -1554,7 +2204,7 @@ function snapshotFromState(state, source, canRecover) {
|
|
|
1554
2204
|
}
|
|
1555
2205
|
function maybeReleaseExpiredQuarantine(state) {
|
|
1556
2206
|
if (state.state !== "QUARANTINED_AUTO") return state;
|
|
1557
|
-
const untilMs =
|
|
2207
|
+
const untilMs = parseMs2(state.quarantineUntil);
|
|
1558
2208
|
if (untilMs == null || Date.now() < untilMs) return state;
|
|
1559
2209
|
const released = {
|
|
1560
2210
|
...state,
|
|
@@ -1562,11 +2212,11 @@ function maybeReleaseExpiredQuarantine(state) {
|
|
|
1562
2212
|
reasonCode: null,
|
|
1563
2213
|
reasonMessage: null,
|
|
1564
2214
|
remediation: null,
|
|
1565
|
-
triggeredAt:
|
|
2215
|
+
triggeredAt: nowIso2(),
|
|
1566
2216
|
quarantineUntil: null,
|
|
1567
2217
|
failureCount: 0,
|
|
1568
2218
|
failureWindowStartedAt: null,
|
|
1569
|
-
updatedAt:
|
|
2219
|
+
updatedAt: nowIso2()
|
|
1570
2220
|
};
|
|
1571
2221
|
writePersistedState(released);
|
|
1572
2222
|
return released;
|
|
@@ -1587,8 +2237,8 @@ function getPluginSafetySnapshot() {
|
|
|
1587
2237
|
reasonCode: "ENV_KILL_SWITCH",
|
|
1588
2238
|
reasonMessage: envKillSwitchReason(),
|
|
1589
2239
|
remediation: "Unset SQUAD_PLUGIN_DISABLED and restart the gateway process.",
|
|
1590
|
-
triggeredAt: persisted.triggeredAt ??
|
|
1591
|
-
updatedAt:
|
|
2240
|
+
triggeredAt: persisted.triggeredAt ?? nowIso2(),
|
|
2241
|
+
updatedAt: nowIso2()
|
|
1592
2242
|
};
|
|
1593
2243
|
return snapshotFromState(envState, "env", false);
|
|
1594
2244
|
}
|
|
@@ -1602,7 +2252,7 @@ function recordPluginFailure(failureCode, failureMessage, remediation) {
|
|
|
1602
2252
|
const state = readPersistedState();
|
|
1603
2253
|
const nowMs = Date.now();
|
|
1604
2254
|
const now = new Date(nowMs).toISOString();
|
|
1605
|
-
const windowStartMs =
|
|
2255
|
+
const windowStartMs = parseMs2(state.failureWindowStartedAt);
|
|
1606
2256
|
if (windowStartMs == null || nowMs - windowStartMs > FAILURE_WINDOW_MS) {
|
|
1607
2257
|
state.failureWindowStartedAt = now;
|
|
1608
2258
|
state.failureCount = 1;
|
|
@@ -1630,7 +2280,7 @@ function recordPluginFailure(failureCode, failureMessage, remediation) {
|
|
|
1630
2280
|
function setPluginManualDisabled(reasonCode = "MANUAL_KILL_SWITCH", reasonMessage = "Plugin manually disabled", remediation = "Run squad.plugin.recover after resolving plugin issues.") {
|
|
1631
2281
|
if (isEnvKillSwitchActive()) return getPluginSafetySnapshot();
|
|
1632
2282
|
const state = readPersistedState();
|
|
1633
|
-
const now =
|
|
2283
|
+
const now = nowIso2();
|
|
1634
2284
|
state.state = "DISABLED_MANUAL";
|
|
1635
2285
|
state.reasonCode = reasonCode;
|
|
1636
2286
|
state.reasonMessage = reasonMessage;
|
|
@@ -1650,7 +2300,7 @@ function recoverPlugin(reasonMessage = "Plugin manually recovered") {
|
|
|
1650
2300
|
};
|
|
1651
2301
|
}
|
|
1652
2302
|
const state = readPersistedState();
|
|
1653
|
-
const now =
|
|
2303
|
+
const now = nowIso2();
|
|
1654
2304
|
state.state = "ACTIVE";
|
|
1655
2305
|
state.reasonCode = null;
|
|
1656
2306
|
state.reasonMessage = null;
|
|
@@ -1837,6 +2487,93 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1837
2487
|
}
|
|
1838
2488
|
}
|
|
1839
2489
|
);
|
|
2490
|
+
safeRegisterGatewayMethod(
|
|
2491
|
+
"squad.extensions.list",
|
|
2492
|
+
async ({ params, respond }) => {
|
|
2493
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
2494
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
2495
|
+
respond(false, pluginBlockedPayload(safetySnapshot));
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
try {
|
|
2499
|
+
const result = await listExtensions({
|
|
2500
|
+
refresh: params?.refresh,
|
|
2501
|
+
ttlMs: params?.ttlMs
|
|
2502
|
+
});
|
|
2503
|
+
respond(true, result);
|
|
2504
|
+
} catch (err2) {
|
|
2505
|
+
if (isExtensionsManagementError(err2)) {
|
|
2506
|
+
respond(false, toExtensionsErrorPayload(err2));
|
|
2507
|
+
return;
|
|
2508
|
+
}
|
|
2509
|
+
respond(false, { errorMessage: errorMessage(err2) });
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
);
|
|
2513
|
+
safeRegisterGatewayMethod(
|
|
2514
|
+
"squad.extensions.update",
|
|
2515
|
+
async ({ params, respond }) => {
|
|
2516
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
2517
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
2518
|
+
respond(false, pluginBlockedPayload(safetySnapshot));
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
try {
|
|
2522
|
+
const result = startExtensionUpdate({
|
|
2523
|
+
pluginId: params?.pluginId
|
|
2524
|
+
});
|
|
2525
|
+
respond(true, result);
|
|
2526
|
+
} catch (err2) {
|
|
2527
|
+
if (isExtensionsManagementError(err2)) {
|
|
2528
|
+
respond(false, toExtensionsErrorPayload(err2));
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
respond(false, { errorMessage: errorMessage(err2) });
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
);
|
|
2535
|
+
safeRegisterGatewayMethod(
|
|
2536
|
+
"squad.extensions.updateStatus",
|
|
2537
|
+
async ({ params, respond }) => {
|
|
2538
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
2539
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
2540
|
+
respond(false, pluginBlockedPayload(safetySnapshot));
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
try {
|
|
2544
|
+
const result = getExtensionsUpdateStatus({
|
|
2545
|
+
pluginIds: params?.pluginIds
|
|
2546
|
+
});
|
|
2547
|
+
respond(true, result);
|
|
2548
|
+
} catch (err2) {
|
|
2549
|
+
if (isExtensionsManagementError(err2)) {
|
|
2550
|
+
respond(false, toExtensionsErrorPayload(err2));
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2553
|
+
respond(false, { errorMessage: errorMessage(err2) });
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
);
|
|
2557
|
+
safeRegisterGatewayMethod(
|
|
2558
|
+
"squad.gateway.restart",
|
|
2559
|
+
async (payload) => {
|
|
2560
|
+
const safetySnapshot = getPluginSafetySnapshot();
|
|
2561
|
+
if (isPluginExecutionBlocked(safetySnapshot)) {
|
|
2562
|
+
payload.respond(false, pluginBlockedPayload(safetySnapshot));
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
try {
|
|
2566
|
+
const result = await restartGateway(payload, api);
|
|
2567
|
+
payload.respond(true, result);
|
|
2568
|
+
} catch (err2) {
|
|
2569
|
+
if (isExtensionsManagementError(err2)) {
|
|
2570
|
+
payload.respond(false, toExtensionsErrorPayload(err2));
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
payload.respond(false, { errorMessage: errorMessage(err2) });
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
);
|
|
1840
2577
|
};
|
|
1841
2578
|
return {
|
|
1842
2579
|
invokeTool,
|
|
@@ -1846,13 +2583,13 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
1846
2583
|
}
|
|
1847
2584
|
|
|
1848
2585
|
// src/migrations/runner.ts
|
|
1849
|
-
import
|
|
1850
|
-
import
|
|
2586
|
+
import fs12 from "fs";
|
|
2587
|
+
import path12 from "path";
|
|
1851
2588
|
|
|
1852
2589
|
// src/migrations/001-enable-main-subagent-access.ts
|
|
1853
|
-
import
|
|
1854
|
-
import
|
|
1855
|
-
function
|
|
2590
|
+
import fs10 from "fs";
|
|
2591
|
+
import path10 from "path";
|
|
2592
|
+
function asRecord3(value) {
|
|
1856
2593
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1857
2594
|
return value;
|
|
1858
2595
|
}
|
|
@@ -1867,24 +2604,24 @@ function mergeStringArrayWithWildcard(value) {
|
|
|
1867
2604
|
return Array.from(next);
|
|
1868
2605
|
}
|
|
1869
2606
|
function patchConfigOnDisk() {
|
|
1870
|
-
const configPath =
|
|
1871
|
-
const raw =
|
|
2607
|
+
const configPath = path10.join(getOpenclawStateDir(), "openclaw.json");
|
|
2608
|
+
const raw = fs10.readFileSync(configPath, "utf-8");
|
|
1872
2609
|
const parsed = JSON.parse(raw);
|
|
1873
|
-
const agents =
|
|
1874
|
-
const defaults =
|
|
1875
|
-
const subagentsDefaults =
|
|
2610
|
+
const agents = asRecord3(parsed.agents) ?? {};
|
|
2611
|
+
const defaults = asRecord3(agents.defaults) ?? {};
|
|
2612
|
+
const subagentsDefaults = asRecord3(defaults.subagents) ?? {};
|
|
1876
2613
|
defaults.maxConcurrent = 4;
|
|
1877
2614
|
defaults.subagents = {
|
|
1878
2615
|
...subagentsDefaults,
|
|
1879
2616
|
maxConcurrent: 8
|
|
1880
2617
|
};
|
|
1881
2618
|
const listRaw = Array.isArray(agents.list) ? agents.list : [];
|
|
1882
|
-
const list = listRaw.map((entry) =>
|
|
2619
|
+
const list = listRaw.map((entry) => asRecord3(entry)).filter((entry) => Boolean(entry));
|
|
1883
2620
|
const mainIndex = list.findIndex((entry) => entry.id === "main");
|
|
1884
2621
|
const existingMain = mainIndex >= 0 ? list[mainIndex] : {};
|
|
1885
|
-
const existingIdentity =
|
|
1886
|
-
const existingTools =
|
|
1887
|
-
const existingSubagents =
|
|
2622
|
+
const existingIdentity = asRecord3(existingMain.identity) ?? {};
|
|
2623
|
+
const existingTools = asRecord3(existingMain.tools) ?? {};
|
|
2624
|
+
const existingSubagents = asRecord3(existingMain.subagents) ?? {};
|
|
1888
2625
|
const nextMain = {
|
|
1889
2626
|
...existingMain,
|
|
1890
2627
|
id: "main",
|
|
@@ -1908,7 +2645,7 @@ function patchConfigOnDisk() {
|
|
|
1908
2645
|
defaults,
|
|
1909
2646
|
list
|
|
1910
2647
|
};
|
|
1911
|
-
|
|
2648
|
+
fs10.writeFileSync(configPath, `${JSON.stringify(parsed, null, 2)}
|
|
1912
2649
|
`, "utf-8");
|
|
1913
2650
|
}
|
|
1914
2651
|
var migration = {
|
|
@@ -1983,13 +2720,13 @@ var migration = {
|
|
|
1983
2720
|
var enable_main_subagent_access_default = migration;
|
|
1984
2721
|
|
|
1985
2722
|
// src/auth-profiles.ts
|
|
1986
|
-
import
|
|
1987
|
-
import
|
|
2723
|
+
import fs11 from "fs";
|
|
2724
|
+
import path11 from "path";
|
|
1988
2725
|
function getMainAuthProfilesPath(stateDir) {
|
|
1989
|
-
return
|
|
2726
|
+
return path11.join(stateDir, "agents", "main", "agent", "auth-profiles.json");
|
|
1990
2727
|
}
|
|
1991
2728
|
function getAgentAuthProfilesPath(stateDir, agentId) {
|
|
1992
|
-
return
|
|
2729
|
+
return path11.join(stateDir, "agents", agentId, "agent", "auth-profiles.json");
|
|
1993
2730
|
}
|
|
1994
2731
|
function ensureAgentAuthProfiles(agentId) {
|
|
1995
2732
|
const normalizedAgentId = agentId.trim();
|
|
@@ -1997,17 +2734,17 @@ function ensureAgentAuthProfiles(agentId) {
|
|
|
1997
2734
|
const stateDir = getOpenclawStateDir();
|
|
1998
2735
|
const sourcePath = getMainAuthProfilesPath(stateDir);
|
|
1999
2736
|
const targetPath = getAgentAuthProfilesPath(stateDir, normalizedAgentId);
|
|
2000
|
-
if (!
|
|
2001
|
-
|
|
2002
|
-
|
|
2737
|
+
if (!fs11.existsSync(sourcePath) || fs11.existsSync(targetPath)) return false;
|
|
2738
|
+
fs11.mkdirSync(path11.dirname(targetPath), { recursive: true });
|
|
2739
|
+
fs11.copyFileSync(sourcePath, targetPath);
|
|
2003
2740
|
return true;
|
|
2004
2741
|
}
|
|
2005
2742
|
function backfillAgentAuthProfiles() {
|
|
2006
2743
|
const stateDir = getOpenclawStateDir();
|
|
2007
|
-
const agentsDir =
|
|
2008
|
-
if (!
|
|
2744
|
+
const agentsDir = path11.join(stateDir, "agents");
|
|
2745
|
+
if (!fs11.existsSync(agentsDir)) return [];
|
|
2009
2746
|
const copied = [];
|
|
2010
|
-
for (const entry of
|
|
2747
|
+
for (const entry of fs11.readdirSync(agentsDir, { withFileTypes: true })) {
|
|
2011
2748
|
if (!entry.isDirectory()) continue;
|
|
2012
2749
|
const agentId = entry.name;
|
|
2013
2750
|
if (ensureAgentAuthProfiles(agentId)) {
|
|
@@ -2038,8 +2775,8 @@ var STARTUP_MIGRATIONS = [
|
|
|
2038
2775
|
];
|
|
2039
2776
|
|
|
2040
2777
|
// src/migrations/runner.ts
|
|
2041
|
-
var MIGRATIONS_DIR =
|
|
2042
|
-
var MIGRATIONS_PATH =
|
|
2778
|
+
var MIGRATIONS_DIR = path12.join(getOpenclawStateDir(), "squad-ceo-data");
|
|
2779
|
+
var MIGRATIONS_PATH = path12.join(MIGRATIONS_DIR, "migrations.json");
|
|
2043
2780
|
var STARTUP_MIGRATION_TIMEOUT_MS = readTimeoutMs("SQUAD_STARTUP_MIGRATION_TIMEOUT_MS", 2e4);
|
|
2044
2781
|
var STARTUP_GATEWAY_CALL_TIMEOUT_MS = readTimeoutMs("SQUAD_STARTUP_GATEWAY_CALL_TIMEOUT_MS", 5e3);
|
|
2045
2782
|
function defaultState() {
|
|
@@ -2050,7 +2787,7 @@ function defaultState() {
|
|
|
2050
2787
|
}
|
|
2051
2788
|
function readState() {
|
|
2052
2789
|
try {
|
|
2053
|
-
const raw =
|
|
2790
|
+
const raw = fs12.readFileSync(MIGRATIONS_PATH, "utf-8");
|
|
2054
2791
|
const parsed = JSON.parse(raw);
|
|
2055
2792
|
if (!Array.isArray(parsed.completed)) return defaultState();
|
|
2056
2793
|
return {
|
|
@@ -2062,8 +2799,8 @@ function readState() {
|
|
|
2062
2799
|
}
|
|
2063
2800
|
}
|
|
2064
2801
|
function writeState(state) {
|
|
2065
|
-
|
|
2066
|
-
|
|
2802
|
+
fs12.mkdirSync(MIGRATIONS_DIR, { recursive: true });
|
|
2803
|
+
fs12.writeFileSync(MIGRATIONS_PATH, JSON.stringify(state, null, 2), "utf-8");
|
|
2067
2804
|
}
|
|
2068
2805
|
function makeGatewayCaller(api) {
|
|
2069
2806
|
return async (method, params = {}) => {
|
|
@@ -2130,55 +2867,6 @@ async function runStartupMigrations(api) {
|
|
|
2130
2867
|
|
|
2131
2868
|
// src/http-routes.ts
|
|
2132
2869
|
import crypto2 from "crypto";
|
|
2133
|
-
|
|
2134
|
-
// src/gateway-invoke.ts
|
|
2135
|
-
function asRecord3(value) {
|
|
2136
|
-
return value && typeof value === "object" ? value : null;
|
|
2137
|
-
}
|
|
2138
|
-
function isInvoker(fn) {
|
|
2139
|
-
return typeof fn === "function";
|
|
2140
|
-
}
|
|
2141
|
-
function isUnknownGatewayMethodError(message) {
|
|
2142
|
-
return /unknown method|method .* unavailable|not found|invalid[_ ]request|does not exist/i.test(
|
|
2143
|
-
message
|
|
2144
|
-
);
|
|
2145
|
-
}
|
|
2146
|
-
async function callGatewayAny(ctx, api, method, params) {
|
|
2147
|
-
const ctxGateway = asRecord3(ctx.gateway);
|
|
2148
|
-
const apiGateway = asRecord3(api?.gateway);
|
|
2149
|
-
const candidates = [
|
|
2150
|
-
ctx.request,
|
|
2151
|
-
ctx.callGatewayMethod,
|
|
2152
|
-
ctx.gatewayRequest,
|
|
2153
|
-
ctx.invokeGatewayMethod,
|
|
2154
|
-
ctxGateway?.request,
|
|
2155
|
-
ctxGateway?.callGatewayMethod,
|
|
2156
|
-
api?.request,
|
|
2157
|
-
api?.callGatewayMethod,
|
|
2158
|
-
api?.gatewayRequest,
|
|
2159
|
-
api?.invokeGatewayMethod,
|
|
2160
|
-
apiGateway?.request,
|
|
2161
|
-
apiGateway?.callGatewayMethod
|
|
2162
|
-
];
|
|
2163
|
-
let lastErr = null;
|
|
2164
|
-
for (const candidate of candidates) {
|
|
2165
|
-
if (!isInvoker(candidate)) continue;
|
|
2166
|
-
try {
|
|
2167
|
-
return await candidate(method, params);
|
|
2168
|
-
} catch (err2) {
|
|
2169
|
-
lastErr = err2;
|
|
2170
|
-
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
2171
|
-
if (isUnknownGatewayMethodError(msg)) {
|
|
2172
|
-
continue;
|
|
2173
|
-
}
|
|
2174
|
-
throw err2;
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
if (lastErr) throw lastErr;
|
|
2178
|
-
throw new Error("Gateway method invocation API unavailable in plugin context");
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
// src/http-routes.ts
|
|
2182
2870
|
var DEFAULT_ALLOWED_ORIGINS = [
|
|
2183
2871
|
"https://squad.ceo",
|
|
2184
2872
|
"https://www.squad.ceo",
|
|
@@ -2677,10 +3365,10 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2677
3365
|
};
|
|
2678
3366
|
const handleRequest = async (request) => {
|
|
2679
3367
|
const url = getRequestUrl(request);
|
|
2680
|
-
const
|
|
3368
|
+
const path13 = url.pathname;
|
|
2681
3369
|
cleanupCaches();
|
|
2682
3370
|
let pluginState = getPluginSafetySnapshot();
|
|
2683
|
-
if (request.method === "OPTIONS" &&
|
|
3371
|
+
if (request.method === "OPTIONS" && path13.startsWith("/squad-internal/")) {
|
|
2684
3372
|
const origin = ensureOriginAllowed(request);
|
|
2685
3373
|
if (!origin) {
|
|
2686
3374
|
return new Response(null, { status: 403 });
|
|
@@ -2698,7 +3386,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2698
3386
|
})
|
|
2699
3387
|
);
|
|
2700
3388
|
}
|
|
2701
|
-
if (request.method === "GET" &&
|
|
3389
|
+
if (request.method === "GET" && path13 === "/squad-internal/health") {
|
|
2702
3390
|
if (!isTailnetContext(request)) {
|
|
2703
3391
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2704
3392
|
}
|
|
@@ -2715,7 +3403,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2715
3403
|
})
|
|
2716
3404
|
);
|
|
2717
3405
|
}
|
|
2718
|
-
if (request.method === "GET" &&
|
|
3406
|
+
if (request.method === "GET" && path13 === "/squad-internal/plugin/status") {
|
|
2719
3407
|
if (!isTailnetContext(request)) {
|
|
2720
3408
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2721
3409
|
}
|
|
@@ -2730,7 +3418,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2730
3418
|
})
|
|
2731
3419
|
);
|
|
2732
3420
|
}
|
|
2733
|
-
if (request.method === "POST" &&
|
|
3421
|
+
if (request.method === "POST" && path13 === "/squad-internal/plugin/recover") {
|
|
2734
3422
|
if (!isTailnetContext(request)) {
|
|
2735
3423
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2736
3424
|
}
|
|
@@ -2763,7 +3451,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2763
3451
|
pluginState = result.snapshot;
|
|
2764
3452
|
return withCors(request, json({ ok: true, plugin: pluginState, message: result.message }));
|
|
2765
3453
|
}
|
|
2766
|
-
if (request.method === "POST" &&
|
|
3454
|
+
if (request.method === "POST" && path13 === "/squad-internal/plugin/disable") {
|
|
2767
3455
|
if (!isTailnetContext(request)) {
|
|
2768
3456
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
2769
3457
|
}
|
|
@@ -2789,7 +3477,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2789
3477
|
pluginState = setPluginManualDisabled(reasonCode, reasonMessage, remediation);
|
|
2790
3478
|
return withCors(request, json({ ok: true, plugin: pluginState }));
|
|
2791
3479
|
}
|
|
2792
|
-
if (
|
|
3480
|
+
if (path13.startsWith("/squad-internal/") && isPluginExecutionBlocked(pluginState)) {
|
|
2793
3481
|
const code = pluginBlockedCode(pluginState);
|
|
2794
3482
|
return jsonError(
|
|
2795
3483
|
request,
|
|
@@ -2799,7 +3487,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2799
3487
|
{ plugin: pluginState }
|
|
2800
3488
|
);
|
|
2801
3489
|
}
|
|
2802
|
-
if (request.method === "POST" &&
|
|
3490
|
+
if (request.method === "POST" && path13 === "/squad-internal/pairing/request") {
|
|
2803
3491
|
const origin = ensureOriginAllowed(request);
|
|
2804
3492
|
if (!origin) {
|
|
2805
3493
|
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
@@ -2851,7 +3539,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
2851
3539
|
);
|
|
2852
3540
|
}
|
|
2853
3541
|
}
|
|
2854
|
-
if (request.method === "GET" &&
|
|
3542
|
+
if (request.method === "GET" && path13 === "/squad-internal/pairing/status") {
|
|
2855
3543
|
const origin = ensureOriginAllowed(request);
|
|
2856
3544
|
if (!origin) {
|
|
2857
3545
|
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
package/package.json
CHANGED