squad-openclaw 2026.3.1301 → 2026.3.1401
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/dist/index.js +864 -96
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -140,21 +140,26 @@ function updatePlugin(pluginDirName, configDir) {
|
|
|
140
140
|
registryDelete(`plugin:${pluginDirName}`);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
-
function startWatcher(configDir, onFsChange) {
|
|
144
|
-
const
|
|
143
|
+
function startWatcher(configDir, onFsChange, options = {}) {
|
|
144
|
+
const watcherOptions = {
|
|
145
145
|
persistent: true,
|
|
146
146
|
usePolling: false,
|
|
147
147
|
ignoreInitial: true,
|
|
148
148
|
awaitWriteFinish: { stabilityThreshold: 300 },
|
|
149
149
|
depth: 4,
|
|
150
|
-
ignored: [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"**/data/**"
|
|
156
|
-
]
|
|
150
|
+
ignored: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/data/**"]
|
|
151
|
+
};
|
|
152
|
+
const generalWatcher = chokidar.watch(configDir, {
|
|
153
|
+
...watcherOptions,
|
|
154
|
+
ignored: [...watcherOptions.ignored, "**/extensions/**"]
|
|
157
155
|
});
|
|
156
|
+
const pluginManifestWatcher = chokidar.watch(
|
|
157
|
+
path.join(configDir, "extensions", "*", "openclaw.plugin.json"),
|
|
158
|
+
watcherOptions
|
|
159
|
+
);
|
|
160
|
+
const watchers = [generalWatcher, pluginManifestWatcher];
|
|
161
|
+
let stopped = false;
|
|
162
|
+
let fatalErrorReported = false;
|
|
158
163
|
const emitFsChange = (action, filePath) => {
|
|
159
164
|
if (!onFsChange) return;
|
|
160
165
|
const rel = path.relative(configDir, filePath);
|
|
@@ -237,12 +242,31 @@ function startWatcher(configDir, onFsChange) {
|
|
|
237
242
|
return;
|
|
238
243
|
}
|
|
239
244
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
const onWatcherError = (error) => {
|
|
246
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
247
|
+
console.error(`[squad-openclaw] watcher error: ${normalized.message}`);
|
|
248
|
+
stop();
|
|
249
|
+
try {
|
|
250
|
+
if (!fatalErrorReported) {
|
|
251
|
+
fatalErrorReported = true;
|
|
252
|
+
options.onFatalError?.(normalized);
|
|
253
|
+
}
|
|
254
|
+
} catch (callbackError) {
|
|
255
|
+
const callbackMessage = callbackError instanceof Error ? callbackError.message : String(callbackError);
|
|
256
|
+
console.warn(`[squad-openclaw] watcher fatal-error callback failed: ${callbackMessage}`);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
for (const watcher of watchers) {
|
|
260
|
+
watcher.on("add", (fp) => handleChange(fp, "add"));
|
|
261
|
+
watcher.on("change", (fp) => handleChange(fp, "change"));
|
|
262
|
+
watcher.on("unlink", (fp) => handleChange(fp, "unlink"));
|
|
263
|
+
watcher.on("addDir", handleAddDir);
|
|
264
|
+
watcher.on("unlinkDir", handleUnlinkDir);
|
|
265
|
+
watcher.on("error", onWatcherError);
|
|
266
|
+
}
|
|
267
|
+
const stop = () => {
|
|
268
|
+
if (stopped) return;
|
|
269
|
+
stopped = true;
|
|
246
270
|
for (const timer of debounceTimers.values()) {
|
|
247
271
|
clearTimeout(timer);
|
|
248
272
|
}
|
|
@@ -251,8 +275,11 @@ function startWatcher(configDir, onFsChange) {
|
|
|
251
275
|
clearTimeout(timer);
|
|
252
276
|
}
|
|
253
277
|
fsDebounceTimers.clear();
|
|
254
|
-
watcher
|
|
278
|
+
for (const watcher of watchers) {
|
|
279
|
+
void watcher.close();
|
|
280
|
+
}
|
|
255
281
|
};
|
|
282
|
+
return stop;
|
|
256
283
|
}
|
|
257
284
|
|
|
258
285
|
// src/filesystem.ts
|
|
@@ -779,6 +806,110 @@ function registryList(type) {
|
|
|
779
806
|
if (!type) return all;
|
|
780
807
|
return all.filter((e) => e.type === type);
|
|
781
808
|
}
|
|
809
|
+
var DEFAULT_MEDIA_SCAN_MAX_ENTRIES = 5e3;
|
|
810
|
+
var DEFAULT_MEDIA_SCAN_MAX_DEPTH = 6;
|
|
811
|
+
var DEFAULT_MEDIA_SCAN_MAX_DURATION_MS = 2500;
|
|
812
|
+
var ENTITY_RUNTIME_STATE_KEY = "__squadOpenclawEntityRuntimeState_v1";
|
|
813
|
+
function readPositiveIntEnv(envName, fallback, min, max) {
|
|
814
|
+
const raw = process.env[envName];
|
|
815
|
+
if (!raw) return fallback;
|
|
816
|
+
const parsed = Number(raw);
|
|
817
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
818
|
+
const rounded = Math.floor(parsed);
|
|
819
|
+
if (rounded < min) return min;
|
|
820
|
+
if (rounded > max) return max;
|
|
821
|
+
return rounded;
|
|
822
|
+
}
|
|
823
|
+
function readMediaScanLimits() {
|
|
824
|
+
return {
|
|
825
|
+
maxEntries: readPositiveIntEnv(
|
|
826
|
+
"SQUAD_MEDIA_SCAN_MAX_ENTRIES",
|
|
827
|
+
DEFAULT_MEDIA_SCAN_MAX_ENTRIES,
|
|
828
|
+
1,
|
|
829
|
+
1e6
|
|
830
|
+
),
|
|
831
|
+
maxDepth: readPositiveIntEnv(
|
|
832
|
+
"SQUAD_MEDIA_SCAN_MAX_DEPTH",
|
|
833
|
+
DEFAULT_MEDIA_SCAN_MAX_DEPTH,
|
|
834
|
+
1,
|
|
835
|
+
64
|
|
836
|
+
),
|
|
837
|
+
maxDurationMs: readPositiveIntEnv(
|
|
838
|
+
"SQUAD_MEDIA_SCAN_MAX_DURATION_MS",
|
|
839
|
+
DEFAULT_MEDIA_SCAN_MAX_DURATION_MS,
|
|
840
|
+
200,
|
|
841
|
+
6e5
|
|
842
|
+
)
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
function getEntityRuntimeState() {
|
|
846
|
+
const globalState = globalThis;
|
|
847
|
+
if (!globalState[ENTITY_RUNTIME_STATE_KEY]) {
|
|
848
|
+
globalState[ENTITY_RUNTIME_STATE_KEY] = {
|
|
849
|
+
stopWatcher: null,
|
|
850
|
+
watcherConfigDir: null,
|
|
851
|
+
processSignalCleanup: null,
|
|
852
|
+
shutdownHookApis: /* @__PURE__ */ new WeakSet()
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
return globalState[ENTITY_RUNTIME_STATE_KEY];
|
|
856
|
+
}
|
|
857
|
+
function stopEntityWatcher() {
|
|
858
|
+
const runtime = getEntityRuntimeState();
|
|
859
|
+
if (!runtime.stopWatcher) return;
|
|
860
|
+
try {
|
|
861
|
+
runtime.stopWatcher();
|
|
862
|
+
} catch (error) {
|
|
863
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
864
|
+
console.warn(`[squad-openclaw] watcher stop failed: ${message}`);
|
|
865
|
+
} finally {
|
|
866
|
+
runtime.stopWatcher = null;
|
|
867
|
+
runtime.watcherConfigDir = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
function ensureProcessSignalHandlers() {
|
|
871
|
+
const runtime = getEntityRuntimeState();
|
|
872
|
+
runtime.processSignalCleanup?.();
|
|
873
|
+
const onProcessSignal = () => {
|
|
874
|
+
stopEntityWatcher();
|
|
875
|
+
};
|
|
876
|
+
process.on("SIGTERM", onProcessSignal);
|
|
877
|
+
process.on("SIGINT", onProcessSignal);
|
|
878
|
+
runtime.processSignalCleanup = () => {
|
|
879
|
+
process.off("SIGTERM", onProcessSignal);
|
|
880
|
+
process.off("SIGINT", onProcessSignal);
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
function registerShutdownHook(api) {
|
|
884
|
+
if (!api || typeof api !== "object") return;
|
|
885
|
+
const runtime = getEntityRuntimeState();
|
|
886
|
+
if (runtime.shutdownHookApis.has(api)) return;
|
|
887
|
+
const maybeApi = api;
|
|
888
|
+
if (typeof maybeApi.onShutdown !== "function") return;
|
|
889
|
+
maybeApi.onShutdown(() => {
|
|
890
|
+
stopEntityWatcher();
|
|
891
|
+
});
|
|
892
|
+
runtime.shutdownHookApis.add(api);
|
|
893
|
+
}
|
|
894
|
+
function startOrRestartWatcher(configDir, onFsChange, onWatcherError) {
|
|
895
|
+
const runtime = getEntityRuntimeState();
|
|
896
|
+
stopEntityWatcher();
|
|
897
|
+
runtime.stopWatcher = startWatcher(configDir, onFsChange, {
|
|
898
|
+
onFatalError: (error) => {
|
|
899
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
900
|
+
console.error(`[squad-openclaw] file watcher stopped after fatal error: ${message}`);
|
|
901
|
+
runtime.stopWatcher = null;
|
|
902
|
+
runtime.watcherConfigDir = null;
|
|
903
|
+
try {
|
|
904
|
+
onWatcherError?.(error);
|
|
905
|
+
} catch (callbackError) {
|
|
906
|
+
const callbackMessage = callbackError instanceof Error ? callbackError.message : String(callbackError);
|
|
907
|
+
console.warn(`[squad-openclaw] watcher error callback failed: ${callbackMessage}`);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
runtime.watcherConfigDir = configDir;
|
|
912
|
+
}
|
|
782
913
|
var IDENTITY_NAME_RE = /\*\*Name:\*\*\s*\n?\s*(.+?)(?=\n|$)/;
|
|
783
914
|
function parseIdentityName(content) {
|
|
784
915
|
const match = content.match(IDENTITY_NAME_RE);
|
|
@@ -990,9 +1121,48 @@ function getMimeType(filename) {
|
|
|
990
1121
|
function scanMedia(configDir) {
|
|
991
1122
|
const now = Date.now();
|
|
992
1123
|
const mediaDir = path5.join(configDir, "media");
|
|
993
|
-
|
|
1124
|
+
const limits = readMediaScanLimits();
|
|
1125
|
+
const budget = {
|
|
1126
|
+
visitedEntries: 0,
|
|
1127
|
+
maxEntries: limits.maxEntries,
|
|
1128
|
+
maxDepth: limits.maxDepth,
|
|
1129
|
+
deadlineMs: Date.now() + limits.maxDurationMs,
|
|
1130
|
+
truncatedByEntries: false,
|
|
1131
|
+
truncatedByDepth: false,
|
|
1132
|
+
truncatedByTimeout: false
|
|
1133
|
+
};
|
|
1134
|
+
scanMediaDir(mediaDir, now, 0, budget);
|
|
1135
|
+
if (budget.truncatedByEntries || budget.truncatedByDepth || budget.truncatedByTimeout) {
|
|
1136
|
+
const reasons = [];
|
|
1137
|
+
if (budget.truncatedByEntries) reasons.push(`max entries (${budget.maxEntries})`);
|
|
1138
|
+
if (budget.truncatedByDepth) reasons.push(`max depth (${budget.maxDepth})`);
|
|
1139
|
+
if (budget.truncatedByTimeout) reasons.push(`time budget (${limits.maxDurationMs}ms)`);
|
|
1140
|
+
console.warn(
|
|
1141
|
+
`[squad-openclaw] media scan truncated after ${budget.visitedEntries} entries: ${reasons.join(", ")}. Tune SQUAD_MEDIA_SCAN_MAX_ENTRIES/SQUAD_MEDIA_SCAN_MAX_DEPTH/SQUAD_MEDIA_SCAN_MAX_DURATION_MS if needed.`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function shouldStopMediaScan(budget) {
|
|
1146
|
+
if (Date.now() > budget.deadlineMs) {
|
|
1147
|
+
budget.truncatedByTimeout = true;
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
return false;
|
|
1151
|
+
}
|
|
1152
|
+
function claimMediaEntry(budget) {
|
|
1153
|
+
if (budget.visitedEntries >= budget.maxEntries) {
|
|
1154
|
+
budget.truncatedByEntries = true;
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
budget.visitedEntries += 1;
|
|
1158
|
+
return true;
|
|
994
1159
|
}
|
|
995
|
-
function scanMediaDir(dirPath, now) {
|
|
1160
|
+
function scanMediaDir(dirPath, now, depth, budget) {
|
|
1161
|
+
if (shouldStopMediaScan(budget)) return;
|
|
1162
|
+
if (depth > budget.maxDepth) {
|
|
1163
|
+
budget.truncatedByDepth = true;
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
996
1166
|
let entries;
|
|
997
1167
|
try {
|
|
998
1168
|
entries = fs5.readdirSync(dirPath, { withFileTypes: true });
|
|
@@ -1000,10 +1170,12 @@ function scanMediaDir(dirPath, now) {
|
|
|
1000
1170
|
return;
|
|
1001
1171
|
}
|
|
1002
1172
|
for (const entry of entries) {
|
|
1173
|
+
if (shouldStopMediaScan(budget)) return;
|
|
1003
1174
|
if (entry.name.startsWith(".")) continue;
|
|
1004
1175
|
const entryPath = path5.join(dirPath, entry.name);
|
|
1005
1176
|
if (isSensitivePath(entryPath)) continue;
|
|
1006
1177
|
if (entry.isDirectory()) {
|
|
1178
|
+
if (!claimMediaEntry(budget)) return;
|
|
1007
1179
|
registrySet({
|
|
1008
1180
|
id: entryPath,
|
|
1009
1181
|
type: "directory",
|
|
@@ -1016,8 +1188,13 @@ function scanMediaDir(dirPath, now) {
|
|
|
1016
1188
|
created_at: now,
|
|
1017
1189
|
updated_at: now
|
|
1018
1190
|
});
|
|
1019
|
-
|
|
1191
|
+
if (depth >= budget.maxDepth) {
|
|
1192
|
+
budget.truncatedByDepth = true;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
scanMediaDir(entryPath, now, depth + 1, budget);
|
|
1020
1196
|
} else if (entry.isFile()) {
|
|
1197
|
+
if (!claimMediaEntry(budget)) return;
|
|
1021
1198
|
const mimeType = getMimeType(entry.name);
|
|
1022
1199
|
let size;
|
|
1023
1200
|
let mtime = now;
|
|
@@ -1050,7 +1227,7 @@ function fullScan(configDir) {
|
|
|
1050
1227
|
scanTools(configDir);
|
|
1051
1228
|
scanMedia(configDir);
|
|
1052
1229
|
}
|
|
1053
|
-
function registerEntityTools(api, onFsChange) {
|
|
1230
|
+
function registerEntityTools(api, onFsChange, options = {}) {
|
|
1054
1231
|
const configDir = getOpenclawStateDir();
|
|
1055
1232
|
api.registerTool({
|
|
1056
1233
|
name: "entity_list",
|
|
@@ -1119,22 +1296,19 @@ function registerEntityTools(api, onFsChange) {
|
|
|
1119
1296
|
};
|
|
1120
1297
|
}
|
|
1121
1298
|
});
|
|
1299
|
+
stopEntityWatcher();
|
|
1122
1300
|
try {
|
|
1123
1301
|
fullScan(configDir);
|
|
1124
1302
|
} catch (err2) {
|
|
1125
1303
|
console.error("[squad-openclaw] Initial scan failed:", err2);
|
|
1126
1304
|
}
|
|
1127
|
-
let stopWatcher = null;
|
|
1128
1305
|
try {
|
|
1129
|
-
|
|
1306
|
+
startOrRestartWatcher(configDir, onFsChange, options.onWatcherError);
|
|
1130
1307
|
} catch (err2) {
|
|
1131
1308
|
console.error("[squad-openclaw] Watcher failed to start:", err2);
|
|
1132
1309
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
};
|
|
1136
|
-
process.on("SIGTERM", cleanup);
|
|
1137
|
-
process.on("SIGINT", cleanup);
|
|
1310
|
+
ensureProcessSignalHandlers();
|
|
1311
|
+
registerShutdownHook(api);
|
|
1138
1312
|
}
|
|
1139
1313
|
|
|
1140
1314
|
// src/version.ts
|
|
@@ -2390,7 +2564,23 @@ function registerSquadSharedApi(api, onFsChange) {
|
|
|
2390
2564
|
console.warn(`[squad-openclaw] ${label} registration failed: ${errorMessage(err2)}`);
|
|
2391
2565
|
}
|
|
2392
2566
|
};
|
|
2393
|
-
|
|
2567
|
+
const handleEntityWatcherError = (error) => {
|
|
2568
|
+
const snapshot = recordPluginFailure(
|
|
2569
|
+
"WATCHER_FATAL_ERROR",
|
|
2570
|
+
errorMessage(error),
|
|
2571
|
+
"Check inotify/file descriptor limits and run squad.plugin.recover after the host is stable."
|
|
2572
|
+
);
|
|
2573
|
+
console.error(`[squad-openclaw] entity watcher fatal error: ${errorMessage(error)}`);
|
|
2574
|
+
if (isPluginExecutionBlocked(snapshot)) {
|
|
2575
|
+
console.warn(
|
|
2576
|
+
`[squad-openclaw] plugin moved to ${snapshot.state} after watcher failure (${snapshot.reasonCode ?? "NO_REASON"})`
|
|
2577
|
+
);
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
registerStep(
|
|
2581
|
+
"entity tools",
|
|
2582
|
+
() => registerEntityTools(api, onFsChange, { onWatcherError: handleEntityWatcherError })
|
|
2583
|
+
);
|
|
2394
2584
|
registerStep("filesystem tools", () => registerFilesystemTools(api));
|
|
2395
2585
|
registerStep("version methods", () => registerVersionMethods(api));
|
|
2396
2586
|
registerStep("question methods", () => registerQuestionMethods(api));
|
|
@@ -2866,7 +3056,79 @@ async function runStartupMigrations(api) {
|
|
|
2866
3056
|
}
|
|
2867
3057
|
|
|
2868
3058
|
// src/http-routes.ts
|
|
3059
|
+
import crypto5 from "crypto";
|
|
3060
|
+
import fs15 from "fs";
|
|
3061
|
+
import path15 from "path";
|
|
3062
|
+
import { WebSocket as NodeWebSocket2 } from "ws";
|
|
3063
|
+
|
|
3064
|
+
// src/relay-client.ts
|
|
3065
|
+
import { WebSocket as NodeWebSocket } from "ws";
|
|
3066
|
+
import crypto4 from "crypto";
|
|
3067
|
+
import fs14 from "fs";
|
|
3068
|
+
import path14 from "path";
|
|
3069
|
+
|
|
3070
|
+
// src/e2e-crypto.ts
|
|
2869
3071
|
import crypto2 from "crypto";
|
|
3072
|
+
|
|
3073
|
+
// src/device-keys.ts
|
|
3074
|
+
import crypto3 from "crypto";
|
|
3075
|
+
import fs13 from "fs";
|
|
3076
|
+
import path13 from "path";
|
|
3077
|
+
var RELAY_DATA_DIR = path13.join(getOpenclawStateDir(), "squad-ceo-data", "relay");
|
|
3078
|
+
var RELAY_STATE_PATH = path13.join(RELAY_DATA_DIR, "squad-relay.json");
|
|
3079
|
+
var PENDING_APPROVAL_PATH = path13.join(RELAY_DATA_DIR, "pending-approval.json");
|
|
3080
|
+
function toBase64Url(buf) {
|
|
3081
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// src/relay-client.ts
|
|
3085
|
+
function readOperatorToken() {
|
|
3086
|
+
const stateDir = getOpenclawStateDir();
|
|
3087
|
+
const configPath = path14.join(stateDir, "openclaw.json");
|
|
3088
|
+
try {
|
|
3089
|
+
const raw = fs14.readFileSync(configPath, "utf-8");
|
|
3090
|
+
const config = JSON.parse(raw);
|
|
3091
|
+
return config?.gateway?.auth?.token ?? config?.gateway?.remote?.token ?? config?.gateway?.token ?? null;
|
|
3092
|
+
} catch {
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
function readGatewayLocalWsConfig() {
|
|
3097
|
+
const defaults = {
|
|
3098
|
+
port: 18789,
|
|
3099
|
+
// Try IPv4, hostname, then IPv6 loopback.
|
|
3100
|
+
hosts: ["127.0.0.1", "localhost", "[::1]"]
|
|
3101
|
+
};
|
|
3102
|
+
const stateDir = getOpenclawStateDir();
|
|
3103
|
+
const configPath = path14.join(stateDir, "openclaw.json");
|
|
3104
|
+
try {
|
|
3105
|
+
const raw = fs14.readFileSync(configPath, "utf-8");
|
|
3106
|
+
const config = JSON.parse(raw);
|
|
3107
|
+
const parsedPort = Number(config?.gateway?.port);
|
|
3108
|
+
if (Number.isFinite(parsedPort) && parsedPort > 0) {
|
|
3109
|
+
defaults.port = parsedPort;
|
|
3110
|
+
}
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
return defaults;
|
|
3114
|
+
}
|
|
3115
|
+
function signDeviceIdentity(keys, clientId, clientMode, role, scopes, token, challengeNonce) {
|
|
3116
|
+
const signedAtMs = Date.now();
|
|
3117
|
+
const nonce = challengeNonce || crypto4.randomBytes(16).toString("hex");
|
|
3118
|
+
const scopeStr = scopes.join(",");
|
|
3119
|
+
const payload = `v2|${keys.deviceId}|${clientId}|${clientMode}|${role}|${scopeStr}|${signedAtMs}|${token ?? ""}|${nonce}`;
|
|
3120
|
+
const privateKey = crypto4.createPrivateKey(keys.privateKeyPem);
|
|
3121
|
+
const signature = crypto4.sign(null, Buffer.from(payload), privateKey);
|
|
3122
|
+
return {
|
|
3123
|
+
id: keys.deviceId,
|
|
3124
|
+
publicKey: keys.publicKey,
|
|
3125
|
+
signature: toBase64Url(signature),
|
|
3126
|
+
signedAt: signedAtMs,
|
|
3127
|
+
nonce
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
// src/http-routes.ts
|
|
2870
3132
|
var DEFAULT_ALLOWED_ORIGINS = [
|
|
2871
3133
|
"https://squad.ceo",
|
|
2872
3134
|
"https://www.squad.ceo",
|
|
@@ -2888,9 +3150,13 @@ var PAIRING_STATUS_METHODS = [
|
|
|
2888
3150
|
var PROOF_MAX_SKEW_MS = 5 * 60 * 1e3;
|
|
2889
3151
|
var DEFAULT_PAIRING_TTL_MS = 15 * 60 * 1e3;
|
|
2890
3152
|
var NONCE_TTL_MS = 10 * 60 * 1e3;
|
|
3153
|
+
var BROWSER_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
3154
|
+
var BROWSER_SESSION_COOKIE_NAME = "squad_browser_session";
|
|
2891
3155
|
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
2892
3156
|
var INTERNAL_ROUTE_TIMEOUT_MS = readTimeoutMs("SQUAD_INTERNAL_ROUTE_TIMEOUT_MS", 1e4);
|
|
2893
3157
|
var PAIRING_GATEWAY_CALL_TIMEOUT_MS = readTimeoutMs("SQUAD_PAIRING_GATEWAY_CALL_TIMEOUT_MS", 4e3);
|
|
3158
|
+
var BROWSER_SESSION_DIR = path15.join(getOpenclawStateDir(), "squad-ceo-data", "browser-sessions");
|
|
3159
|
+
var BROWSER_SESSION_STORE_PATH = path15.join(BROWSER_SESSION_DIR, "sessions.json");
|
|
2894
3160
|
function errorMessage2(error) {
|
|
2895
3161
|
return error instanceof Error ? error.message : String(error);
|
|
2896
3162
|
}
|
|
@@ -2922,6 +3188,7 @@ function withCors(request, response, allowMethods = "GET, POST, OPTIONS") {
|
|
|
2922
3188
|
const allowedOrigin = resolveAllowedOrigin(origin);
|
|
2923
3189
|
if (allowedOrigin) {
|
|
2924
3190
|
headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
3191
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
2925
3192
|
headers.set("Access-Control-Allow-Methods", allowMethods);
|
|
2926
3193
|
headers.set("Access-Control-Allow-Headers", "Content-Type");
|
|
2927
3194
|
headers.set("Vary", "Origin");
|
|
@@ -2937,8 +3204,16 @@ function jsonError(request, code, message, status = 500, extra = {}) {
|
|
|
2937
3204
|
}
|
|
2938
3205
|
function resolveAllowedOrigin(origin) {
|
|
2939
3206
|
if (!origin) return null;
|
|
3207
|
+
try {
|
|
3208
|
+
const url = new URL(origin);
|
|
3209
|
+
const hostname = url.hostname.toLowerCase();
|
|
3210
|
+
if (url.protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]")) {
|
|
3211
|
+
return origin;
|
|
3212
|
+
}
|
|
3213
|
+
} catch {
|
|
3214
|
+
}
|
|
2940
3215
|
const configured = process.env.SQUAD_ALLOWED_ORIGINS ? process.env.SQUAD_ALLOWED_ORIGINS.split(",").map((item) => item.trim()).filter(Boolean) : [];
|
|
2941
|
-
const allowed =
|
|
3216
|
+
const allowed = [.../* @__PURE__ */ new Set([...DEFAULT_ALLOWED_ORIGINS, ...configured])];
|
|
2942
3217
|
return allowed.includes(origin) ? origin : null;
|
|
2943
3218
|
}
|
|
2944
3219
|
function isTailnetContext(request) {
|
|
@@ -2970,6 +3245,71 @@ function firstHeaderToken(value) {
|
|
|
2970
3245
|
const first = value.split(",")[0]?.trim();
|
|
2971
3246
|
return first ? first : null;
|
|
2972
3247
|
}
|
|
3248
|
+
function parseCookieHeader(cookieHeader) {
|
|
3249
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
3250
|
+
if (!cookieHeader) return cookies;
|
|
3251
|
+
for (const segment of cookieHeader.split(";")) {
|
|
3252
|
+
const trimmed = segment.trim();
|
|
3253
|
+
if (!trimmed) continue;
|
|
3254
|
+
const separator = trimmed.indexOf("=");
|
|
3255
|
+
if (separator <= 0) continue;
|
|
3256
|
+
const name = trimmed.slice(0, separator).trim();
|
|
3257
|
+
const value = trimmed.slice(separator + 1).trim();
|
|
3258
|
+
if (!name) continue;
|
|
3259
|
+
try {
|
|
3260
|
+
cookies.set(name, decodeURIComponent(value));
|
|
3261
|
+
} catch {
|
|
3262
|
+
cookies.set(name, value);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
return cookies;
|
|
3266
|
+
}
|
|
3267
|
+
function isSecureRequest(request) {
|
|
3268
|
+
const protocol = firstHeaderToken(request.headers.get("x-forwarded-proto"))?.toLowerCase();
|
|
3269
|
+
if (protocol === "https" || protocol === "wss") return true;
|
|
3270
|
+
try {
|
|
3271
|
+
return new URL(request.url).protocol === "https:";
|
|
3272
|
+
} catch {
|
|
3273
|
+
return false;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
function buildBrowserSessionCookie(request, sessionId) {
|
|
3277
|
+
const secure = isSecureRequest(request);
|
|
3278
|
+
const sameSite = secure ? "SameSite=None" : "SameSite=Lax";
|
|
3279
|
+
const parts = [
|
|
3280
|
+
`${BROWSER_SESSION_COOKIE_NAME}=${encodeURIComponent(sessionId)}`,
|
|
3281
|
+
"Path=/",
|
|
3282
|
+
"HttpOnly",
|
|
3283
|
+
sameSite,
|
|
3284
|
+
`Max-Age=${Math.floor(BROWSER_SESSION_TTL_MS / 1e3)}`
|
|
3285
|
+
];
|
|
3286
|
+
if (secure) {
|
|
3287
|
+
parts.push("Secure");
|
|
3288
|
+
}
|
|
3289
|
+
return parts.join("; ");
|
|
3290
|
+
}
|
|
3291
|
+
function browserSessionStoreToObject(store) {
|
|
3292
|
+
return Object.fromEntries(store.entries());
|
|
3293
|
+
}
|
|
3294
|
+
function loadBrowserSessions() {
|
|
3295
|
+
try {
|
|
3296
|
+
const raw = fs15.readFileSync(BROWSER_SESSION_STORE_PATH, "utf8");
|
|
3297
|
+
const parsed = JSON.parse(raw);
|
|
3298
|
+
const now = Date.now();
|
|
3299
|
+
const entries = Object.entries(parsed).filter(([, record]) => typeof record?.sessionId === "string" && typeof record?.deviceId === "string" && typeof record?.publicKey === "string" && typeof record?.privateKeyPem === "string" && typeof record?.createdAt === "number" && typeof record?.lastSeenAt === "number" && typeof record?.expiresAt === "number" && record.expiresAt > now);
|
|
3300
|
+
return new Map(entries);
|
|
3301
|
+
} catch {
|
|
3302
|
+
return /* @__PURE__ */ new Map();
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
function persistBrowserSessions(store) {
|
|
3306
|
+
fs15.mkdirSync(BROWSER_SESSION_DIR, { recursive: true });
|
|
3307
|
+
fs15.writeFileSync(
|
|
3308
|
+
BROWSER_SESSION_STORE_PATH,
|
|
3309
|
+
JSON.stringify(browserSessionStoreToObject(store), null, 2),
|
|
3310
|
+
{ mode: 384 }
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
2973
3313
|
function readObjectHeader(request, name) {
|
|
2974
3314
|
const headers = request.headers;
|
|
2975
3315
|
if (!headers || typeof headers !== "object") return null;
|
|
@@ -2980,6 +3320,29 @@ function readObjectHeader(request, name) {
|
|
|
2980
3320
|
if (typeof value === "string") return value;
|
|
2981
3321
|
return value == null ? null : String(value);
|
|
2982
3322
|
}
|
|
3323
|
+
async function readRequestBodyRecord(request) {
|
|
3324
|
+
const contentType = request.headers.get("content-type")?.toLowerCase() ?? "";
|
|
3325
|
+
if (contentType.includes("application/json")) {
|
|
3326
|
+
return await request.json();
|
|
3327
|
+
}
|
|
3328
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
3329
|
+
const form = await request.formData();
|
|
3330
|
+
const record = {};
|
|
3331
|
+
for (const [key, value] of form.entries()) {
|
|
3332
|
+
const normalized = typeof value === "string" ? value : value.name;
|
|
3333
|
+
const existing = record[key];
|
|
3334
|
+
if (existing === void 0) {
|
|
3335
|
+
record[key] = normalized;
|
|
3336
|
+
} else if (Array.isArray(existing)) {
|
|
3337
|
+
existing.push(normalized);
|
|
3338
|
+
} else {
|
|
3339
|
+
record[key] = [existing, normalized];
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
return record;
|
|
3343
|
+
}
|
|
3344
|
+
return {};
|
|
3345
|
+
}
|
|
2983
3346
|
async function toFetchRequest(incoming) {
|
|
2984
3347
|
if (incoming && typeof incoming.method === "string" && typeof incoming.url === "string" && typeof incoming.headers?.get === "function") {
|
|
2985
3348
|
return incoming;
|
|
@@ -3022,6 +3385,66 @@ async function toFetchRequest(incoming) {
|
|
|
3022
3385
|
}
|
|
3023
3386
|
return new Request(absoluteUrl, { method, headers, body });
|
|
3024
3387
|
}
|
|
3388
|
+
async function readIncomingBody(incoming) {
|
|
3389
|
+
const method = typeof incoming.method === "string" ? incoming.method.toUpperCase() : "GET";
|
|
3390
|
+
if (method === "GET" || method === "HEAD") return void 0;
|
|
3391
|
+
const bodyValue = incoming.body;
|
|
3392
|
+
if (typeof bodyValue === "string") return bodyValue;
|
|
3393
|
+
if (bodyValue instanceof Uint8Array) return Buffer.from(bodyValue).toString("utf8");
|
|
3394
|
+
if (bodyValue instanceof ArrayBuffer) return Buffer.from(bodyValue).toString("utf8");
|
|
3395
|
+
const on = incoming.on;
|
|
3396
|
+
if (typeof on !== "function") return void 0;
|
|
3397
|
+
return await new Promise((resolve, reject) => {
|
|
3398
|
+
const chunks = [];
|
|
3399
|
+
const addChunk = (chunk) => {
|
|
3400
|
+
if (typeof chunk === "string") {
|
|
3401
|
+
chunks.push(Buffer.from(chunk));
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
if (chunk instanceof Uint8Array) {
|
|
3405
|
+
chunks.push(Buffer.from(chunk));
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
if (chunk instanceof ArrayBuffer) {
|
|
3409
|
+
chunks.push(Buffer.from(chunk));
|
|
3410
|
+
}
|
|
3411
|
+
};
|
|
3412
|
+
on.call(incoming, "data", addChunk);
|
|
3413
|
+
on.call(incoming, "end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
3414
|
+
on.call(incoming, "error", (error) => reject(error));
|
|
3415
|
+
});
|
|
3416
|
+
}
|
|
3417
|
+
async function toRouteRequest(incoming) {
|
|
3418
|
+
if (incoming && typeof incoming.method === "string" && typeof incoming.url === "string" && typeof incoming.headers?.get === "function") {
|
|
3419
|
+
return incoming;
|
|
3420
|
+
}
|
|
3421
|
+
const requestLike = incoming;
|
|
3422
|
+
const body = await readIncomingBody(requestLike);
|
|
3423
|
+
if (body === void 0) {
|
|
3424
|
+
return toFetchRequest(requestLike);
|
|
3425
|
+
}
|
|
3426
|
+
return toFetchRequest({
|
|
3427
|
+
method: typeof requestLike.method === "string" ? requestLike.method : "POST",
|
|
3428
|
+
url: typeof requestLike.url === "string" ? requestLike.url : typeof requestLike.originalUrl === "string" ? requestLike.originalUrl : "/",
|
|
3429
|
+
originalUrl: typeof requestLike.originalUrl === "string" ? requestLike.originalUrl : void 0,
|
|
3430
|
+
headers: requestLike.headers,
|
|
3431
|
+
body
|
|
3432
|
+
});
|
|
3433
|
+
}
|
|
3434
|
+
async function writeNodeResponse(outgoing, response) {
|
|
3435
|
+
outgoing.statusCode = response.status;
|
|
3436
|
+
const setHeader = typeof outgoing.setHeader === "function" ? outgoing.setHeader.bind(outgoing) : null;
|
|
3437
|
+
if (setHeader) {
|
|
3438
|
+
for (const [key, value] of response.headers.entries()) {
|
|
3439
|
+
setHeader(key, value);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
const body = response.body ? Buffer.from(await response.arrayBuffer()) : Buffer.alloc(0);
|
|
3443
|
+
const end = typeof outgoing.end === "function" ? outgoing.end.bind(outgoing) : null;
|
|
3444
|
+
if (end) {
|
|
3445
|
+
end(body.length > 0 ? body : void 0);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3025
3448
|
function getRequestUrl(request) {
|
|
3026
3449
|
try {
|
|
3027
3450
|
return new URL(request.url);
|
|
@@ -3051,10 +3474,25 @@ function base64UrlEncode(bytes) {
|
|
|
3051
3474
|
function encodeUtf8(value) {
|
|
3052
3475
|
return new TextEncoder().encode(value);
|
|
3053
3476
|
}
|
|
3477
|
+
function generateBrowserSessionDevice() {
|
|
3478
|
+
const { publicKey, privateKey } = crypto5.generateKeyPairSync("ed25519");
|
|
3479
|
+
const pubDer = publicKey.export({ type: "spki", format: "der" });
|
|
3480
|
+
const rawPub = pubDer.subarray(pubDer.length - 32);
|
|
3481
|
+
return {
|
|
3482
|
+
deviceId: crypto5.createHash("sha256").update(rawPub).digest("hex"),
|
|
3483
|
+
publicKey: base64UrlEncode(rawPub),
|
|
3484
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" })
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
function signBrowserSessionPayload(privateKeyPem, payload) {
|
|
3488
|
+
const privateKey = crypto5.createPrivateKey(privateKeyPem);
|
|
3489
|
+
const signature = crypto5.sign(null, Buffer.from(payload, "utf8"), privateKey);
|
|
3490
|
+
return base64UrlEncode(signature);
|
|
3491
|
+
}
|
|
3054
3492
|
function normalizeDevicePublicKeyBase64Url(publicKey) {
|
|
3055
3493
|
try {
|
|
3056
3494
|
if (publicKey.includes("BEGIN")) {
|
|
3057
|
-
const spki =
|
|
3495
|
+
const spki = crypto5.createPublicKey(publicKey).export({
|
|
3058
3496
|
type: "spki",
|
|
3059
3497
|
format: "der"
|
|
3060
3498
|
});
|
|
@@ -3074,7 +3512,7 @@ function deriveDeviceIdFromPublicKey(publicKey) {
|
|
|
3074
3512
|
if (!normalized) return null;
|
|
3075
3513
|
const raw = decodeBase64Url(normalized);
|
|
3076
3514
|
if (raw.length !== 32) return null;
|
|
3077
|
-
return
|
|
3515
|
+
return crypto5.createHash("sha256").update(raw).digest("hex");
|
|
3078
3516
|
} catch {
|
|
3079
3517
|
return null;
|
|
3080
3518
|
}
|
|
@@ -3083,7 +3521,7 @@ function verifyEd25519Signature(publicKey, payload, signatureBase64Url) {
|
|
|
3083
3521
|
try {
|
|
3084
3522
|
const normalized = normalizeDevicePublicKeyBase64Url(publicKey);
|
|
3085
3523
|
if (!normalized) return false;
|
|
3086
|
-
const key =
|
|
3524
|
+
const key = crypto5.createPublicKey({
|
|
3087
3525
|
key: Buffer.concat([ED25519_SPKI_PREFIX, decodeBase64Url(normalized)]),
|
|
3088
3526
|
type: "spki",
|
|
3089
3527
|
format: "der"
|
|
@@ -3095,7 +3533,7 @@ function verifyEd25519Signature(publicKey, payload, signatureBase64Url) {
|
|
|
3095
3533
|
return Buffer.from(signatureBase64Url, "base64");
|
|
3096
3534
|
}
|
|
3097
3535
|
})();
|
|
3098
|
-
return
|
|
3536
|
+
return crypto5.verify(null, Buffer.from(payload, "utf8"), key, signature);
|
|
3099
3537
|
} catch {
|
|
3100
3538
|
return false;
|
|
3101
3539
|
}
|
|
@@ -3113,11 +3551,24 @@ function canonicalizeP256Jwk(value) {
|
|
|
3113
3551
|
}
|
|
3114
3552
|
function computeDeviceIdFromJwk(jwk) {
|
|
3115
3553
|
const canonical = JSON.stringify(jwk);
|
|
3116
|
-
return
|
|
3554
|
+
return crypto5.createHash("sha256").update(canonical).digest("hex");
|
|
3117
3555
|
}
|
|
3118
3556
|
function buildProofPayload(action, deviceId, nonce, signedAt, origin) {
|
|
3119
3557
|
return `squad.${action}|${deviceId}|${nonce}|${signedAt}|${origin}`;
|
|
3120
3558
|
}
|
|
3559
|
+
function buildGatewayConnectPayload(params) {
|
|
3560
|
+
return [
|
|
3561
|
+
"v2",
|
|
3562
|
+
params.deviceId,
|
|
3563
|
+
params.clientId,
|
|
3564
|
+
params.clientMode,
|
|
3565
|
+
params.role,
|
|
3566
|
+
params.scopes.join(","),
|
|
3567
|
+
String(params.signedAtMs),
|
|
3568
|
+
"",
|
|
3569
|
+
params.nonce
|
|
3570
|
+
].join("|");
|
|
3571
|
+
}
|
|
3121
3572
|
async function verifyBrowserProof(payload, origin, action, usedProofNonces) {
|
|
3122
3573
|
const deviceId = pickString(payload.deviceId);
|
|
3123
3574
|
const signature = pickString(payload.signature);
|
|
@@ -3194,14 +3645,14 @@ async function verifyBrowserProof(payload, origin, action, usedProofNonces) {
|
|
|
3194
3645
|
};
|
|
3195
3646
|
}
|
|
3196
3647
|
try {
|
|
3197
|
-
const key = await
|
|
3648
|
+
const key = await crypto5.webcrypto.subtle.importKey(
|
|
3198
3649
|
"jwk",
|
|
3199
3650
|
jwk,
|
|
3200
3651
|
{ name: "ECDSA", namedCurve: "P-256" },
|
|
3201
3652
|
false,
|
|
3202
3653
|
["verify"]
|
|
3203
3654
|
);
|
|
3204
|
-
verified = await
|
|
3655
|
+
verified = await crypto5.webcrypto.subtle.verify(
|
|
3205
3656
|
{ name: "ECDSA", hash: "SHA-256" },
|
|
3206
3657
|
key,
|
|
3207
3658
|
decodeBase64Url(signature),
|
|
@@ -3299,8 +3750,50 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3299
3750
|
const pendingPairings = /* @__PURE__ */ new Map();
|
|
3300
3751
|
const proofNonces = /* @__PURE__ */ new Map();
|
|
3301
3752
|
const rateLimitBucket = /* @__PURE__ */ new Map();
|
|
3753
|
+
const browserSessions = loadBrowserSessions();
|
|
3302
3754
|
let preferredPairingRequestMethod = null;
|
|
3303
3755
|
let preferredPairingStatusMethod = null;
|
|
3756
|
+
const persistBrowserSessionsSafe = () => {
|
|
3757
|
+
try {
|
|
3758
|
+
persistBrowserSessions(browserSessions);
|
|
3759
|
+
} catch (error) {
|
|
3760
|
+
console.warn(`[squad-openclaw] failed to persist browser sessions: ${errorMessage2(error)}`);
|
|
3761
|
+
}
|
|
3762
|
+
};
|
|
3763
|
+
const getOrCreateBrowserSession = (request) => {
|
|
3764
|
+
const now = Date.now();
|
|
3765
|
+
const cookies = parseCookieHeader(request.headers.get("cookie"));
|
|
3766
|
+
const sessionId = cookies.get(BROWSER_SESSION_COOKIE_NAME) ?? null;
|
|
3767
|
+
const existing = sessionId ? browserSessions.get(sessionId) ?? null : null;
|
|
3768
|
+
if (existing && existing.expiresAt > now) {
|
|
3769
|
+
const next = {
|
|
3770
|
+
...existing,
|
|
3771
|
+
lastSeenAt: now,
|
|
3772
|
+
expiresAt: now + BROWSER_SESSION_TTL_MS
|
|
3773
|
+
};
|
|
3774
|
+
browserSessions.set(existing.sessionId, next);
|
|
3775
|
+
persistBrowserSessionsSafe();
|
|
3776
|
+
return {
|
|
3777
|
+
record: next,
|
|
3778
|
+
setCookie: buildBrowserSessionCookie(request, next.sessionId)
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
const createdSessionId = crypto5.randomBytes(32).toString("hex");
|
|
3782
|
+
const device = generateBrowserSessionDevice();
|
|
3783
|
+
const created = {
|
|
3784
|
+
sessionId: createdSessionId,
|
|
3785
|
+
...device,
|
|
3786
|
+
createdAt: now,
|
|
3787
|
+
lastSeenAt: now,
|
|
3788
|
+
expiresAt: now + BROWSER_SESSION_TTL_MS
|
|
3789
|
+
};
|
|
3790
|
+
browserSessions.set(createdSessionId, created);
|
|
3791
|
+
persistBrowserSessionsSafe();
|
|
3792
|
+
return {
|
|
3793
|
+
record: created,
|
|
3794
|
+
setCookie: buildBrowserSessionCookie(request, createdSessionId)
|
|
3795
|
+
};
|
|
3796
|
+
};
|
|
3304
3797
|
const cleanupCaches = () => {
|
|
3305
3798
|
const now = Date.now();
|
|
3306
3799
|
for (const [key, value] of pendingPairings) {
|
|
@@ -3312,13 +3805,171 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3312
3805
|
for (const [key, bucket] of rateLimitBucket) {
|
|
3313
3806
|
if (bucket.resetAt <= now) rateLimitBucket.delete(key);
|
|
3314
3807
|
}
|
|
3808
|
+
let sessionsDirty = false;
|
|
3809
|
+
for (const [key, record] of browserSessions) {
|
|
3810
|
+
if (record.expiresAt <= now) {
|
|
3811
|
+
browserSessions.delete(key);
|
|
3812
|
+
sessionsDirty = true;
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
if (sessionsDirty) {
|
|
3816
|
+
persistBrowserSessionsSafe();
|
|
3817
|
+
}
|
|
3818
|
+
};
|
|
3819
|
+
const callGatewayMethodViaLocalWs = async (method, params) => {
|
|
3820
|
+
const { port, hosts } = readGatewayLocalWsConfig();
|
|
3821
|
+
const operatorToken = readOperatorToken();
|
|
3822
|
+
if (!operatorToken) {
|
|
3823
|
+
throw new Error("Gateway auth token missing");
|
|
3824
|
+
}
|
|
3825
|
+
const relayStatePath = path15.join(getOpenclawStateDir(), "squad-ceo-data", "relay", "squad-relay.json");
|
|
3826
|
+
let deviceKeys = loadBrowserSessions().get("__relay_fallback__");
|
|
3827
|
+
if (!deviceKeys) {
|
|
3828
|
+
const relayDevice = (() => {
|
|
3829
|
+
try {
|
|
3830
|
+
const raw = fs15.readFileSync(relayStatePath, "utf8");
|
|
3831
|
+
const parsed = JSON.parse(raw);
|
|
3832
|
+
const keys = parsed.deviceKeys;
|
|
3833
|
+
if (typeof keys?.deviceId === "string" && typeof keys?.publicKey === "string" && typeof keys?.privateKeyPem === "string") {
|
|
3834
|
+
return keys;
|
|
3835
|
+
}
|
|
3836
|
+
} catch {
|
|
3837
|
+
}
|
|
3838
|
+
return generateBrowserSessionDevice();
|
|
3839
|
+
})();
|
|
3840
|
+
deviceKeys = {
|
|
3841
|
+
sessionId: "__relay_fallback__",
|
|
3842
|
+
deviceId: relayDevice.deviceId,
|
|
3843
|
+
publicKey: relayDevice.publicKey,
|
|
3844
|
+
privateKeyPem: relayDevice.privateKeyPem,
|
|
3845
|
+
createdAt: Date.now(),
|
|
3846
|
+
lastSeenAt: Date.now(),
|
|
3847
|
+
expiresAt: Date.now() + BROWSER_SESSION_TTL_MS
|
|
3848
|
+
};
|
|
3849
|
+
browserSessions.set(deviceKeys.sessionId, deviceKeys);
|
|
3850
|
+
}
|
|
3851
|
+
let lastError = null;
|
|
3852
|
+
for (const host of hosts) {
|
|
3853
|
+
try {
|
|
3854
|
+
const result = await new Promise((resolve, reject) => {
|
|
3855
|
+
const ws = new NodeWebSocket2(`ws://${host}:${port}`);
|
|
3856
|
+
const connectId = `connect-${crypto5.randomUUID()}`;
|
|
3857
|
+
const requestId = `request-${crypto5.randomUUID()}`;
|
|
3858
|
+
const scopes = ["operator.admin", "operator.read", "operator.write"];
|
|
3859
|
+
let settled = false;
|
|
3860
|
+
const finish = (fn) => {
|
|
3861
|
+
if (settled) return;
|
|
3862
|
+
settled = true;
|
|
3863
|
+
clearTimeout(timer);
|
|
3864
|
+
fn();
|
|
3865
|
+
};
|
|
3866
|
+
const timer = setTimeout(() => {
|
|
3867
|
+
finish(() => {
|
|
3868
|
+
try {
|
|
3869
|
+
ws.close();
|
|
3870
|
+
} catch {
|
|
3871
|
+
}
|
|
3872
|
+
reject(new Error(`gateway local call timed out for ${method}`));
|
|
3873
|
+
});
|
|
3874
|
+
}, PAIRING_GATEWAY_CALL_TIMEOUT_MS);
|
|
3875
|
+
ws.on("message", (data) => {
|
|
3876
|
+
let msg;
|
|
3877
|
+
try {
|
|
3878
|
+
msg = JSON.parse(data.toString());
|
|
3879
|
+
} catch {
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
if (msg.type === "event" && msg.event === "connect.challenge") {
|
|
3883
|
+
const payload = asRecord4(msg.payload);
|
|
3884
|
+
const nonce = pickString(payload?.nonce);
|
|
3885
|
+
const connectMsg = {
|
|
3886
|
+
type: "req",
|
|
3887
|
+
id: connectId,
|
|
3888
|
+
method: "connect",
|
|
3889
|
+
params: {
|
|
3890
|
+
minProtocol: 3,
|
|
3891
|
+
maxProtocol: 3,
|
|
3892
|
+
client: {
|
|
3893
|
+
id: "cli",
|
|
3894
|
+
version: "0.1.0",
|
|
3895
|
+
platform: "plugin",
|
|
3896
|
+
mode: "gateway"
|
|
3897
|
+
},
|
|
3898
|
+
role: "operator",
|
|
3899
|
+
scopes,
|
|
3900
|
+
auth: { token: operatorToken },
|
|
3901
|
+
device: signDeviceIdentity(
|
|
3902
|
+
{
|
|
3903
|
+
deviceId: deviceKeys.deviceId,
|
|
3904
|
+
publicKey: deviceKeys.publicKey,
|
|
3905
|
+
privateKeyPem: deviceKeys.privateKeyPem
|
|
3906
|
+
},
|
|
3907
|
+
"cli",
|
|
3908
|
+
"gateway",
|
|
3909
|
+
"operator",
|
|
3910
|
+
scopes,
|
|
3911
|
+
operatorToken,
|
|
3912
|
+
nonce
|
|
3913
|
+
)
|
|
3914
|
+
}
|
|
3915
|
+
};
|
|
3916
|
+
ws.send(JSON.stringify(connectMsg));
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
if (msg.type === "res" && msg.id === connectId) {
|
|
3920
|
+
if (msg.ok) {
|
|
3921
|
+
ws.send(JSON.stringify({ type: "req", id: requestId, method, params }));
|
|
3922
|
+
} else {
|
|
3923
|
+
const errorPayload = asRecord4(msg.error);
|
|
3924
|
+
const message = pickString(errorPayload?.message) ?? pickString(errorPayload?.error) ?? pickString(msg.error) ?? "Gateway connect rejected";
|
|
3925
|
+
finish(() => reject(new Error(message)));
|
|
3926
|
+
}
|
|
3927
|
+
return;
|
|
3928
|
+
}
|
|
3929
|
+
if (msg.type === "res" && msg.id === requestId) {
|
|
3930
|
+
if (msg.ok) {
|
|
3931
|
+
finish(() => {
|
|
3932
|
+
try {
|
|
3933
|
+
ws.close();
|
|
3934
|
+
} catch {
|
|
3935
|
+
}
|
|
3936
|
+
resolve(msg.payload);
|
|
3937
|
+
});
|
|
3938
|
+
} else {
|
|
3939
|
+
const errorPayload = asRecord4(msg.error);
|
|
3940
|
+
const message = pickString(errorPayload?.message) ?? pickString(errorPayload?.error) ?? pickString(msg.error) ?? "Gateway request failed";
|
|
3941
|
+
finish(() => reject(new Error(message)));
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
});
|
|
3945
|
+
ws.on("error", (error) => {
|
|
3946
|
+
finish(() => reject(error));
|
|
3947
|
+
});
|
|
3948
|
+
ws.on("close", (code, reason) => {
|
|
3949
|
+
if (settled) return;
|
|
3950
|
+
finish(() => reject(new Error(`gateway closed (${code}): ${reason.toString()}`)));
|
|
3951
|
+
});
|
|
3952
|
+
});
|
|
3953
|
+
return result;
|
|
3954
|
+
} catch (error) {
|
|
3955
|
+
lastError = error;
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
throw lastError instanceof Error ? lastError : new Error(errorMessage2(lastError));
|
|
3315
3959
|
};
|
|
3316
3960
|
const callGatewayMethod = async (method, params) => {
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3961
|
+
try {
|
|
3962
|
+
return await withTimeout(
|
|
3963
|
+
callGatewayAny({}, api, method, params),
|
|
3964
|
+
PAIRING_GATEWAY_CALL_TIMEOUT_MS,
|
|
3965
|
+
`pairing gateway call ${method}`
|
|
3966
|
+
);
|
|
3967
|
+
} catch (error) {
|
|
3968
|
+
if (error instanceof Error && error.message === "Gateway method invocation API unavailable in plugin context") {
|
|
3969
|
+
return callGatewayMethodViaLocalWs(method, params);
|
|
3970
|
+
}
|
|
3971
|
+
throw error;
|
|
3972
|
+
}
|
|
3322
3973
|
};
|
|
3323
3974
|
const requestPairingFromGateway = async () => {
|
|
3324
3975
|
const methods = preferredPairingRequestMethod ? [preferredPairingRequestMethod, ...PAIRING_REQUEST_METHODS.filter((m) => m !== preferredPairingRequestMethod)] : [...PAIRING_REQUEST_METHODS];
|
|
@@ -3348,6 +3999,45 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3348
3999
|
details ? `PAIRING_REQUEST_UNAVAILABLE: ${details}` : "PAIRING_REQUEST_UNAVAILABLE: No pairing request method supported"
|
|
3349
4000
|
);
|
|
3350
4001
|
};
|
|
4002
|
+
const issuePairingRequestResponse = async (request, deviceId, setCookie = null) => {
|
|
4003
|
+
const forwardedFor = request.headers.get("x-forwarded-for") ?? "unknown";
|
|
4004
|
+
const ipKey = `ip:${forwardedFor}`;
|
|
4005
|
+
const deviceKey = `device:${deviceId}`;
|
|
4006
|
+
if (isRateLimited(rateLimitBucket, ipKey, 20, 6e4) || isRateLimited(rateLimitBucket, deviceKey, 8, 6e4)) {
|
|
4007
|
+
return jsonError(request, "RATE_LIMITED", "Too many pairing requests", 429);
|
|
4008
|
+
}
|
|
4009
|
+
try {
|
|
4010
|
+
const pairing = await requestPairingFromGateway();
|
|
4011
|
+
pendingPairings.set(pairing.requestId, {
|
|
4012
|
+
requestId: pairing.requestId,
|
|
4013
|
+
deviceId,
|
|
4014
|
+
createdAt: Date.now(),
|
|
4015
|
+
expiresAt: pairing.expiresAt
|
|
4016
|
+
});
|
|
4017
|
+
const response = withCors(
|
|
4018
|
+
request,
|
|
4019
|
+
json({
|
|
4020
|
+
ok: true,
|
|
4021
|
+
requestId: pairing.requestId,
|
|
4022
|
+
approveCommand: `openclaw devices approve ${pairing.requestId}`,
|
|
4023
|
+
expiresAt: pairing.expiresAt
|
|
4024
|
+
})
|
|
4025
|
+
);
|
|
4026
|
+
if (setCookie) {
|
|
4027
|
+
response.headers.append("Set-Cookie", setCookie);
|
|
4028
|
+
}
|
|
4029
|
+
return response;
|
|
4030
|
+
} catch (error) {
|
|
4031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4032
|
+
return jsonError(
|
|
4033
|
+
request,
|
|
4034
|
+
"PAIRING_REQUEST_UNAVAILABLE",
|
|
4035
|
+
message.replace(/^PAIRING_REQUEST_UNAVAILABLE:\s*/i, ""),
|
|
4036
|
+
501,
|
|
4037
|
+
{ nextStep: "openclaw devices list --json" }
|
|
4038
|
+
);
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
3351
4041
|
const readPairingStatusFromGateway = async (requestId) => {
|
|
3352
4042
|
const methods = preferredPairingStatusMethod ? [preferredPairingStatusMethod, ...PAIRING_STATUS_METHODS.filter((m) => m !== preferredPairingStatusMethod)] : [...PAIRING_STATUS_METHODS];
|
|
3353
4043
|
for (const method of methods) {
|
|
@@ -3365,10 +4055,10 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3365
4055
|
};
|
|
3366
4056
|
const handleRequest = async (request) => {
|
|
3367
4057
|
const url = getRequestUrl(request);
|
|
3368
|
-
const
|
|
4058
|
+
const path16 = url.pathname;
|
|
3369
4059
|
cleanupCaches();
|
|
3370
4060
|
let pluginState = getPluginSafetySnapshot();
|
|
3371
|
-
if (request.method === "OPTIONS" &&
|
|
4061
|
+
if (request.method === "OPTIONS" && path16.startsWith("/squad-internal/")) {
|
|
3372
4062
|
const origin = ensureOriginAllowed(request);
|
|
3373
4063
|
if (!origin) {
|
|
3374
4064
|
return new Response(null, { status: 403 });
|
|
@@ -3386,7 +4076,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3386
4076
|
})
|
|
3387
4077
|
);
|
|
3388
4078
|
}
|
|
3389
|
-
if (request.method === "GET" &&
|
|
4079
|
+
if (request.method === "GET" && path16 === "/squad-internal/health") {
|
|
3390
4080
|
if (!isTailnetContext(request)) {
|
|
3391
4081
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
3392
4082
|
}
|
|
@@ -3403,7 +4093,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3403
4093
|
})
|
|
3404
4094
|
);
|
|
3405
4095
|
}
|
|
3406
|
-
if (request.method === "GET" &&
|
|
4096
|
+
if (request.method === "GET" && path16 === "/squad-internal/plugin/status") {
|
|
3407
4097
|
if (!isTailnetContext(request)) {
|
|
3408
4098
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
3409
4099
|
}
|
|
@@ -3418,7 +4108,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3418
4108
|
})
|
|
3419
4109
|
);
|
|
3420
4110
|
}
|
|
3421
|
-
if (request.method === "POST" &&
|
|
4111
|
+
if (request.method === "POST" && path16 === "/squad-internal/plugin/recover") {
|
|
3422
4112
|
if (!isTailnetContext(request)) {
|
|
3423
4113
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
3424
4114
|
}
|
|
@@ -3451,7 +4141,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3451
4141
|
pluginState = result.snapshot;
|
|
3452
4142
|
return withCors(request, json({ ok: true, plugin: pluginState, message: result.message }));
|
|
3453
4143
|
}
|
|
3454
|
-
if (request.method === "POST" &&
|
|
4144
|
+
if (request.method === "POST" && path16 === "/squad-internal/plugin/disable") {
|
|
3455
4145
|
if (!isTailnetContext(request)) {
|
|
3456
4146
|
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
3457
4147
|
}
|
|
@@ -3477,7 +4167,7 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3477
4167
|
pluginState = setPluginManualDisabled(reasonCode, reasonMessage, remediation);
|
|
3478
4168
|
return withCors(request, json({ ok: true, plugin: pluginState }));
|
|
3479
4169
|
}
|
|
3480
|
-
if (
|
|
4170
|
+
if (path16.startsWith("/squad-internal/") && isPluginExecutionBlocked(pluginState)) {
|
|
3481
4171
|
const code = pluginBlockedCode(pluginState);
|
|
3482
4172
|
return jsonError(
|
|
3483
4173
|
request,
|
|
@@ -3487,7 +4177,75 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3487
4177
|
{ plugin: pluginState }
|
|
3488
4178
|
);
|
|
3489
4179
|
}
|
|
3490
|
-
if (request.method === "POST" &&
|
|
4180
|
+
if (request.method === "POST" && path16 === "/squad-internal/browser-session/connect-proof") {
|
|
4181
|
+
const origin = ensureOriginAllowed(request);
|
|
4182
|
+
if (!origin) {
|
|
4183
|
+
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
4184
|
+
}
|
|
4185
|
+
if (!isTailnetContext(request)) {
|
|
4186
|
+
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
4187
|
+
}
|
|
4188
|
+
const forwardedFor = request.headers.get("x-forwarded-for") ?? "unknown";
|
|
4189
|
+
if (isRateLimited(rateLimitBucket, `connect-proof:${forwardedFor}`, 120, 6e4)) {
|
|
4190
|
+
return jsonError(request, "RATE_LIMITED", "Too many connect proof requests", 429);
|
|
4191
|
+
}
|
|
4192
|
+
let body;
|
|
4193
|
+
try {
|
|
4194
|
+
body = await readRequestBodyRecord(request);
|
|
4195
|
+
} catch {
|
|
4196
|
+
return jsonError(request, "INVALID_REQUEST", "Invalid request body", 400);
|
|
4197
|
+
}
|
|
4198
|
+
const nonce = pickString(body.nonce);
|
|
4199
|
+
const clientId = pickString(body.clientId) ?? "cli";
|
|
4200
|
+
const clientMode = pickString(body.clientMode) ?? "ui";
|
|
4201
|
+
const role = pickString(body.role) ?? "operator";
|
|
4202
|
+
const scopesRaw = Array.isArray(body.scopes) ? body.scopes : typeof body.scopes === "string" ? [body.scopes] : [];
|
|
4203
|
+
const scopes = scopesRaw.filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
4204
|
+
if (!nonce) {
|
|
4205
|
+
return jsonError(request, "INVALID_REQUEST", "nonce is required", 400);
|
|
4206
|
+
}
|
|
4207
|
+
const session = getOrCreateBrowserSession(request);
|
|
4208
|
+
const signedAt = Date.now();
|
|
4209
|
+
const payload = buildGatewayConnectPayload({
|
|
4210
|
+
deviceId: session.record.deviceId,
|
|
4211
|
+
clientId,
|
|
4212
|
+
clientMode,
|
|
4213
|
+
role,
|
|
4214
|
+
scopes,
|
|
4215
|
+
signedAtMs: signedAt,
|
|
4216
|
+
nonce
|
|
4217
|
+
});
|
|
4218
|
+
const signature = signBrowserSessionPayload(session.record.privateKeyPem, payload);
|
|
4219
|
+
const response = withCors(
|
|
4220
|
+
request,
|
|
4221
|
+
json({
|
|
4222
|
+
ok: true,
|
|
4223
|
+
device: {
|
|
4224
|
+
id: session.record.deviceId,
|
|
4225
|
+
publicKey: session.record.publicKey,
|
|
4226
|
+
signature,
|
|
4227
|
+
nonce,
|
|
4228
|
+
signedAt
|
|
4229
|
+
}
|
|
4230
|
+
})
|
|
4231
|
+
);
|
|
4232
|
+
if (session.setCookie) {
|
|
4233
|
+
response.headers.append("Set-Cookie", session.setCookie);
|
|
4234
|
+
}
|
|
4235
|
+
return response;
|
|
4236
|
+
}
|
|
4237
|
+
if (request.method === "POST" && path16 === "/squad-internal/browser-session/pairing/request") {
|
|
4238
|
+
const origin = ensureOriginAllowed(request);
|
|
4239
|
+
if (!origin) {
|
|
4240
|
+
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
4241
|
+
}
|
|
4242
|
+
if (!isTailnetContext(request)) {
|
|
4243
|
+
return jsonError(request, "TAILNET_REQUIRED", "Tailnet context required", 403);
|
|
4244
|
+
}
|
|
4245
|
+
const session = getOrCreateBrowserSession(request);
|
|
4246
|
+
return issuePairingRequestResponse(request, session.record.deviceId, session.setCookie);
|
|
4247
|
+
}
|
|
4248
|
+
if (request.method === "POST" && path16 === "/squad-internal/pairing/request") {
|
|
3491
4249
|
const origin = ensureOriginAllowed(request);
|
|
3492
4250
|
if (!origin) {
|
|
3493
4251
|
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
@@ -3505,41 +4263,9 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3505
4263
|
if (!proofCheck.ok) {
|
|
3506
4264
|
return jsonError(request, proofCheck.code, proofCheck.message, proofCheck.status);
|
|
3507
4265
|
}
|
|
3508
|
-
|
|
3509
|
-
const ipKey = `ip:${forwardedFor}`;
|
|
3510
|
-
const deviceKey = `device:${proofCheck.deviceId}`;
|
|
3511
|
-
if (isRateLimited(rateLimitBucket, ipKey, 20, 6e4) || isRateLimited(rateLimitBucket, deviceKey, 8, 6e4)) {
|
|
3512
|
-
return jsonError(request, "RATE_LIMITED", "Too many pairing requests", 429);
|
|
3513
|
-
}
|
|
3514
|
-
try {
|
|
3515
|
-
const pairing = await requestPairingFromGateway();
|
|
3516
|
-
pendingPairings.set(pairing.requestId, {
|
|
3517
|
-
requestId: pairing.requestId,
|
|
3518
|
-
deviceId: proofCheck.deviceId,
|
|
3519
|
-
createdAt: Date.now(),
|
|
3520
|
-
expiresAt: pairing.expiresAt
|
|
3521
|
-
});
|
|
3522
|
-
return withCors(
|
|
3523
|
-
request,
|
|
3524
|
-
json({
|
|
3525
|
-
ok: true,
|
|
3526
|
-
requestId: pairing.requestId,
|
|
3527
|
-
approveCommand: `openclaw devices approve ${pairing.requestId}`,
|
|
3528
|
-
expiresAt: pairing.expiresAt
|
|
3529
|
-
})
|
|
3530
|
-
);
|
|
3531
|
-
} catch (error) {
|
|
3532
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3533
|
-
return jsonError(
|
|
3534
|
-
request,
|
|
3535
|
-
"PAIRING_REQUEST_UNAVAILABLE",
|
|
3536
|
-
message.replace(/^PAIRING_REQUEST_UNAVAILABLE:\s*/i, ""),
|
|
3537
|
-
501,
|
|
3538
|
-
{ nextStep: "openclaw devices list --json" }
|
|
3539
|
-
);
|
|
3540
|
-
}
|
|
4266
|
+
return issuePairingRequestResponse(request, proofCheck.deviceId);
|
|
3541
4267
|
}
|
|
3542
|
-
if (request.method === "GET" &&
|
|
4268
|
+
if (request.method === "GET" && path16 === "/squad-internal/pairing/status") {
|
|
3543
4269
|
const origin = ensureOriginAllowed(request);
|
|
3544
4270
|
if (!origin) {
|
|
3545
4271
|
return jsonError(request, "ORIGIN_NOT_ALLOWED", "Origin is not allowed", 403);
|
|
@@ -3572,20 +4298,30 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3572
4298
|
}
|
|
3573
4299
|
return new Response("Not Found", { status: 404 });
|
|
3574
4300
|
};
|
|
3575
|
-
const handle = async (incoming) => {
|
|
4301
|
+
const handle = async (incoming, outgoing) => {
|
|
3576
4302
|
let request;
|
|
3577
4303
|
try {
|
|
3578
|
-
request = await
|
|
4304
|
+
request = await toRouteRequest(incoming);
|
|
3579
4305
|
} catch (error) {
|
|
3580
4306
|
console.warn(`[squad-openclaw] failed to normalize internal request: ${errorMessage2(error)}`);
|
|
3581
|
-
|
|
4307
|
+
const response = new Response("Bad Request", { status: 400 });
|
|
4308
|
+
if (outgoing) {
|
|
4309
|
+
await writeNodeResponse(outgoing, response);
|
|
4310
|
+
return true;
|
|
4311
|
+
}
|
|
4312
|
+
return response;
|
|
3582
4313
|
}
|
|
3583
4314
|
try {
|
|
3584
|
-
|
|
4315
|
+
const response = await withTimeout(
|
|
3585
4316
|
handleRequest(request),
|
|
3586
4317
|
INTERNAL_ROUTE_TIMEOUT_MS,
|
|
3587
4318
|
`internal route ${request.method} ${getRequestUrl(request).pathname}`
|
|
3588
4319
|
);
|
|
4320
|
+
if (outgoing) {
|
|
4321
|
+
await writeNodeResponse(outgoing, response);
|
|
4322
|
+
return true;
|
|
4323
|
+
}
|
|
4324
|
+
return response;
|
|
3589
4325
|
} catch (error) {
|
|
3590
4326
|
if (error instanceof TimeoutError) {
|
|
3591
4327
|
const snapshot2 = recordPluginFailure(
|
|
@@ -3594,13 +4330,18 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3594
4330
|
"Investigate internal route hangs and run squad.plugin.recover after remediation."
|
|
3595
4331
|
);
|
|
3596
4332
|
console.warn(`[squad-openclaw] ${error.operation}`);
|
|
3597
|
-
|
|
4333
|
+
const response2 = jsonError(
|
|
3598
4334
|
request,
|
|
3599
4335
|
snapshot2.state === "QUARANTINED_AUTO" ? "PLUGIN_QUARANTINED" : "ROUTE_TIMEOUT",
|
|
3600
4336
|
`Internal route timed out after ${error.timeoutMs}ms`,
|
|
3601
4337
|
snapshot2.state === "QUARANTINED_AUTO" ? 503 : 504,
|
|
3602
4338
|
snapshot2.state === "QUARANTINED_AUTO" ? { plugin: snapshot2 } : {}
|
|
3603
4339
|
);
|
|
4340
|
+
if (outgoing) {
|
|
4341
|
+
await writeNodeResponse(outgoing, response2);
|
|
4342
|
+
return true;
|
|
4343
|
+
}
|
|
4344
|
+
return response2;
|
|
3604
4345
|
}
|
|
3605
4346
|
const snapshot = recordPluginFailure(
|
|
3606
4347
|
"INTERNAL_ROUTE_ERROR",
|
|
@@ -3608,25 +4349,52 @@ function registerTailnetInternalRoutes(api) {
|
|
|
3608
4349
|
"Inspect internal route failures and run squad.plugin.recover after remediation."
|
|
3609
4350
|
);
|
|
3610
4351
|
console.warn(`[squad-openclaw] internal route failure: ${errorMessage2(error)}`);
|
|
3611
|
-
|
|
4352
|
+
const response = jsonError(
|
|
3612
4353
|
request,
|
|
3613
4354
|
snapshot.state === "QUARANTINED_AUTO" ? "PLUGIN_QUARANTINED" : "INTERNAL_ROUTE_ERROR",
|
|
3614
4355
|
"Internal route failed unexpectedly",
|
|
3615
4356
|
snapshot.state === "QUARANTINED_AUTO" ? 503 : 500,
|
|
3616
4357
|
snapshot.state === "QUARANTINED_AUTO" ? { plugin: snapshot } : {}
|
|
3617
4358
|
);
|
|
4359
|
+
if (outgoing) {
|
|
4360
|
+
await writeNodeResponse(outgoing, response);
|
|
4361
|
+
return true;
|
|
4362
|
+
}
|
|
4363
|
+
return response;
|
|
3618
4364
|
}
|
|
3619
4365
|
};
|
|
3620
4366
|
if (typeof api.registerHttpHandler === "function") {
|
|
3621
4367
|
api.registerHttpHandler(handle);
|
|
3622
4368
|
} else if (typeof api.registerHttpRoute === "function") {
|
|
3623
|
-
api.registerHttpRoute
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
4369
|
+
if (api.registerHttpRoute.length <= 1) {
|
|
4370
|
+
const registerRoute = (path16, match = "exact") => {
|
|
4371
|
+
api.registerHttpRoute({
|
|
4372
|
+
path: path16,
|
|
4373
|
+
handler: handle,
|
|
4374
|
+
auth: "plugin",
|
|
4375
|
+
match
|
|
4376
|
+
});
|
|
4377
|
+
};
|
|
4378
|
+
registerRoute("/squad-internal/health");
|
|
4379
|
+
registerRoute("/squad-internal/plugin/status");
|
|
4380
|
+
registerRoute("/squad-internal/plugin/recover");
|
|
4381
|
+
registerRoute("/squad-internal/plugin/disable");
|
|
4382
|
+
registerRoute("/squad-internal/browser-session/connect-proof");
|
|
4383
|
+
registerRoute("/squad-internal/browser-session/pairing/request");
|
|
4384
|
+
registerRoute("/squad-internal/pairing/request");
|
|
4385
|
+
registerRoute("/squad-internal/pairing/status");
|
|
4386
|
+
registerRoute("/squad-internal/*", "prefix");
|
|
4387
|
+
} else {
|
|
4388
|
+
api.registerHttpRoute("/squad-internal/health", handle);
|
|
4389
|
+
api.registerHttpRoute("/squad-internal/plugin/status", handle);
|
|
4390
|
+
api.registerHttpRoute("/squad-internal/plugin/recover", handle);
|
|
4391
|
+
api.registerHttpRoute("/squad-internal/plugin/disable", handle);
|
|
4392
|
+
api.registerHttpRoute("/squad-internal/browser-session/connect-proof", handle);
|
|
4393
|
+
api.registerHttpRoute("/squad-internal/browser-session/pairing/request", handle);
|
|
4394
|
+
api.registerHttpRoute("/squad-internal/pairing/request", handle);
|
|
4395
|
+
api.registerHttpRoute("/squad-internal/pairing/status", handle);
|
|
4396
|
+
api.registerHttpRoute("/squad-internal/*", handle);
|
|
4397
|
+
}
|
|
3630
4398
|
} else if (typeof api.registerHttpMiddleware === "function") {
|
|
3631
4399
|
api.registerHttpMiddleware(handle);
|
|
3632
4400
|
} else {
|
package/package.json
CHANGED