@agent-vm/openclaw-agent-vm-plugin 0.0.81 → 0.0.84
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.d.ts +38 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +871 -133
- package/dist/index.js.map +1 -1
- package/dist/openclaw.plugin.json +21 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import path from "node:path/posix";
|
|
2
|
+
import { ControllerRequestPolicyTransportError, OPENCLAW_STATE_SANDBOXES_VM_ROOT, TOOL_VM_SCRATCH_GUEST_ROOT, TOOL_VM_WORKSPACE_GUEST_ROOT, createToolVmActiveUseHandle, drainControllerResponseBody, fetchControllerWithPolicy, gatewayControlLinkHealthPins, isToolVmLeasePeek, isToolVmSshLease, translateRuntimePath } from "@agent-vm/gateway-interface";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
//#region src/controller-lease-client.ts
|
|
4
5
|
var ControllerLeaseRequestError = class extends Error {
|
|
@@ -46,7 +47,7 @@ function isHeartbeatActiveUseResponse(value) {
|
|
|
46
47
|
const record = objectValue(value);
|
|
47
48
|
return record !== void 0 && typeof Reflect.get(record, "expiresAt") === "number" && typeof Reflect.get(record, "heartbeatAfterMs") === "number";
|
|
48
49
|
}
|
|
49
|
-
function formatUnknownError$
|
|
50
|
+
function formatUnknownError$2(error) {
|
|
50
51
|
return error instanceof Error ? error.message : String(error);
|
|
51
52
|
}
|
|
52
53
|
function writeLeaseClientLog(message) {
|
|
@@ -58,7 +59,7 @@ function parseJsonBody(bodyText, context) {
|
|
|
58
59
|
const parsedBody = jsonValueSchema.safeParse(parsedJson);
|
|
59
60
|
return parsedBody.success ? parsedBody.data : void 0;
|
|
60
61
|
} catch (error) {
|
|
61
|
-
writeLeaseClientLog(`${context} returned a non-JSON error body: ${formatUnknownError$
|
|
62
|
+
writeLeaseClientLog(`${context} returned a non-JSON error body: ${formatUnknownError$2(error)}`);
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -86,15 +87,30 @@ async function readJsonResponse(response, context, isExpectedResponse) {
|
|
|
86
87
|
function createLeaseClient(options) {
|
|
87
88
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
88
89
|
const baseUrl = options.controllerUrl.replace(/\/$/u, "");
|
|
90
|
+
const fetchController = async (optionsForRequest) => await fetchControllerWithPolicy({
|
|
91
|
+
fetchImpl,
|
|
92
|
+
input: optionsForRequest.input,
|
|
93
|
+
operation: optionsForRequest.operation,
|
|
94
|
+
...optionsForRequest.init === void 0 ? {} : { init: optionsForRequest.init },
|
|
95
|
+
...options.requestPolicy === void 0 ? {} : { policy: options.requestPolicy }
|
|
96
|
+
});
|
|
89
97
|
const renewLease = async (leaseId) => {
|
|
90
|
-
return await readJsonResponse(await
|
|
98
|
+
return await readJsonResponse(await fetchController({
|
|
99
|
+
input: `${baseUrl}/lease/${encodeURIComponent(leaseId)}/renew`,
|
|
100
|
+
init: { method: "POST" },
|
|
101
|
+
operation: "lease-renew"
|
|
102
|
+
}), "Controller lease renew API", isToolVmSshLease);
|
|
91
103
|
};
|
|
92
104
|
return {
|
|
93
105
|
endActiveUse: async (leaseId, useId, request) => {
|
|
94
|
-
const response = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
const response = await fetchController({
|
|
107
|
+
input: `${baseUrl}/lease/${encodeURIComponent(leaseId)}/uses/${encodeURIComponent(useId)}`,
|
|
108
|
+
init: {
|
|
109
|
+
body: JSON.stringify(request),
|
|
110
|
+
headers: { "content-type": "application/json" },
|
|
111
|
+
method: "DELETE"
|
|
112
|
+
},
|
|
113
|
+
operation: "lease-use-end"
|
|
98
114
|
});
|
|
99
115
|
if (!response.ok) {
|
|
100
116
|
const errorBody = await readErrorBody(response, "Controller active-use end API");
|
|
@@ -105,19 +121,35 @@ function createLeaseClient(options) {
|
|
|
105
121
|
status: response.status
|
|
106
122
|
});
|
|
107
123
|
}
|
|
124
|
+
await drainControllerResponseBody(response);
|
|
108
125
|
},
|
|
109
|
-
heartbeatActiveUse: async (leaseId, useId) => {
|
|
110
|
-
return await readJsonResponse(await
|
|
126
|
+
heartbeatActiveUse: async (leaseId, useId, request) => {
|
|
127
|
+
return await readJsonResponse(await fetchController({
|
|
128
|
+
input: `${baseUrl}/lease/${encodeURIComponent(leaseId)}/uses/${encodeURIComponent(useId)}/heartbeat`,
|
|
129
|
+
init: {
|
|
130
|
+
body: JSON.stringify(request),
|
|
131
|
+
headers: { "content-type": "application/json" },
|
|
132
|
+
method: "POST"
|
|
133
|
+
},
|
|
134
|
+
operation: "lease-heartbeat"
|
|
135
|
+
}), "Controller active-use heartbeat API", isHeartbeatActiveUseResponse);
|
|
111
136
|
},
|
|
112
137
|
renewLease,
|
|
113
138
|
peekLease: async (leaseId) => {
|
|
114
|
-
return await readJsonResponse(await
|
|
139
|
+
return await readJsonResponse(await fetchController({
|
|
140
|
+
input: `${baseUrl}/lease/${encodeURIComponent(leaseId)}/peek`,
|
|
141
|
+
operation: "lease-peek"
|
|
142
|
+
}), "Controller lease peek API", isToolVmLeasePeek);
|
|
115
143
|
},
|
|
116
144
|
publishOpenClawRuntimeStatus: async (report) => {
|
|
117
|
-
const response = await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
const response = await fetchController({
|
|
146
|
+
input: `${baseUrl}/zones/${encodeURIComponent(report.zoneId)}/openclaw-runtime-status`,
|
|
147
|
+
init: {
|
|
148
|
+
body: JSON.stringify(report),
|
|
149
|
+
headers: { "content-type": "application/json" },
|
|
150
|
+
method: "POST"
|
|
151
|
+
},
|
|
152
|
+
operation: "openclaw-runtime-status"
|
|
121
153
|
});
|
|
122
154
|
if (!response.ok) {
|
|
123
155
|
const errorBody = await readErrorBody(response, "Controller OpenClaw runtime status API");
|
|
@@ -128,11 +160,16 @@ function createLeaseClient(options) {
|
|
|
128
160
|
status: response.status
|
|
129
161
|
});
|
|
130
162
|
}
|
|
163
|
+
await drainControllerResponseBody(response);
|
|
131
164
|
},
|
|
132
165
|
releaseLease: async (leaseId, releaseOptions = {}) => {
|
|
133
166
|
const releaseUrl = new URL(`${baseUrl}/lease/${encodeURIComponent(leaseId)}`);
|
|
134
167
|
if (releaseOptions.force === true) releaseUrl.searchParams.set("force", "true");
|
|
135
|
-
const response = await
|
|
168
|
+
const response = await fetchController({
|
|
169
|
+
input: releaseUrl.toString(),
|
|
170
|
+
init: { method: "DELETE" },
|
|
171
|
+
operation: "lease-release"
|
|
172
|
+
});
|
|
136
173
|
if (!response.ok) {
|
|
137
174
|
const errorBody = await readErrorBody(response, "Controller lease release API");
|
|
138
175
|
throw new ControllerLeaseRequestError({
|
|
@@ -142,28 +179,36 @@ function createLeaseClient(options) {
|
|
|
142
179
|
status: response.status
|
|
143
180
|
});
|
|
144
181
|
}
|
|
182
|
+
await drainControllerResponseBody(response);
|
|
145
183
|
},
|
|
146
184
|
requestLease: async (request) => {
|
|
147
|
-
return await readJsonResponse(await
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
return await readJsonResponse(await fetchController({
|
|
186
|
+
input: `${baseUrl}/lease`,
|
|
187
|
+
init: {
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
agentId: request.agentId,
|
|
190
|
+
agentWorkspaceDir: request.agentWorkspaceDir,
|
|
191
|
+
...request.idleTtlMs !== void 0 ? { idleTtlMs: request.idleTtlMs } : {},
|
|
192
|
+
profileId: request.profileId,
|
|
193
|
+
sessionKey: request.sessionKey,
|
|
194
|
+
workMountDir: request.workMountDir,
|
|
195
|
+
zoneId: request.zoneId
|
|
196
|
+
}),
|
|
197
|
+
headers: { "content-type": "application/json" },
|
|
198
|
+
method: "POST"
|
|
199
|
+
},
|
|
200
|
+
operation: "lease-create"
|
|
160
201
|
}), "Controller lease API", isToolVmSshLease);
|
|
161
202
|
},
|
|
162
203
|
startActiveUse: async (leaseId, request) => {
|
|
163
|
-
return await readJsonResponse(await
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
return await readJsonResponse(await fetchController({
|
|
205
|
+
input: `${baseUrl}/lease/${encodeURIComponent(leaseId)}/uses`,
|
|
206
|
+
init: {
|
|
207
|
+
body: JSON.stringify(request),
|
|
208
|
+
headers: { "content-type": "application/json" },
|
|
209
|
+
method: "POST"
|
|
210
|
+
},
|
|
211
|
+
operation: "lease-use-start"
|
|
167
212
|
}), "Controller active-use start API", isStartActiveUseResponse);
|
|
168
213
|
}
|
|
169
214
|
};
|
|
@@ -190,7 +235,13 @@ const OPENCLAW_GONDOLIN_SANDBOX_REQUIREMENTS = [
|
|
|
190
235
|
key: "workspaceAccess"
|
|
191
236
|
}
|
|
192
237
|
];
|
|
193
|
-
const OPENCLAW_GONDOLIN_LEASE_SCOPE_GUIDANCE = "Managed OpenClaw/Gondolin
|
|
238
|
+
const OPENCLAW_GONDOLIN_LEASE_SCOPE_GUIDANCE = "Managed OpenClaw/Gondolin leases are agent-scoped. The plugin derives agentId from sessionKey and does not send OpenClaw scope keys to the controller.";
|
|
239
|
+
var OpenClawAgentIdError = class extends Error {
|
|
240
|
+
constructor(message) {
|
|
241
|
+
super(message);
|
|
242
|
+
this.name = "OpenClawAgentIdError";
|
|
243
|
+
}
|
|
244
|
+
};
|
|
194
245
|
function isOpenClawAgentId(value) {
|
|
195
246
|
return agentIdPattern.test(value.trim());
|
|
196
247
|
}
|
|
@@ -208,11 +259,13 @@ function formatOpenClawGondolinRequirementHint(options) {
|
|
|
208
259
|
}
|
|
209
260
|
function normalizeOpenClawAgentId(value) {
|
|
210
261
|
const trimmed = (value ?? "").trim().toLowerCase();
|
|
211
|
-
|
|
262
|
+
if (trimmed === "") return OPENCLAW_DEFAULT_AGENT_ID;
|
|
263
|
+
if (!isOpenClawAgentId(trimmed)) throw new OpenClawAgentIdError(`Invalid OpenClaw agentId '${value}'.`);
|
|
264
|
+
return trimmed;
|
|
212
265
|
}
|
|
213
266
|
function resolveOpenClawAgentIdFromSessionKey(sessionKey) {
|
|
214
267
|
const parts = sessionKey.trim().split(":");
|
|
215
|
-
if (parts[0] !== "agent" || !parts[1])
|
|
268
|
+
if (parts[0] !== "agent" || !parts[1] || !isOpenClawAgentId(parts[1])) throw new OpenClawAgentIdError(`OpenClaw sessionKey '${sessionKey}' must be agent-shaped and include a valid agentId.`);
|
|
216
269
|
return normalizeOpenClawAgentId(parts[1]);
|
|
217
270
|
}
|
|
218
271
|
function isOpenClawAgentSessionKey(sessionKey) {
|
|
@@ -231,21 +284,385 @@ function findOpenClawGondolinSandboxMismatch(sandbox) {
|
|
|
231
284
|
return OPENCLAW_GONDOLIN_SANDBOX_REQUIREMENTS.find((requirement) => sandbox[requirement.key] !== requirement.expectedValue);
|
|
232
285
|
}
|
|
233
286
|
//#endregion
|
|
287
|
+
//#region src/sandbox-backend/openclaw-agent-workspace-source.ts
|
|
288
|
+
var OpenClawAgentWorkspaceSourceError = class extends Error {
|
|
289
|
+
constructor(message) {
|
|
290
|
+
super(message);
|
|
291
|
+
this.name = "OpenClawAgentWorkspaceSourceError";
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function isRecord(value) {
|
|
295
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
296
|
+
}
|
|
297
|
+
function normalizeAbsolutePosixPath(inputPath) {
|
|
298
|
+
return `/${inputPath.split("/").filter((segment) => segment !== "" && segment !== ".").join("/")}`;
|
|
299
|
+
}
|
|
300
|
+
function containsParentTraversal(inputPath) {
|
|
301
|
+
return inputPath.split(/\/+/u).includes("..");
|
|
302
|
+
}
|
|
303
|
+
function pathIsInsideOrEqual(inputPath, rootPath) {
|
|
304
|
+
return inputPath === rootPath || inputPath.startsWith(`${rootPath}/`);
|
|
305
|
+
}
|
|
306
|
+
function isRuntimePathLeak(inputPath, defaultWorkspaceDir) {
|
|
307
|
+
const normalized = normalizeAbsolutePosixPath(inputPath);
|
|
308
|
+
const normalizedDefaultWorkspace = defaultWorkspaceDir === void 0 ? void 0 : normalizeAbsolutePosixPath(resolveUserPathLikeOpenClaw(defaultWorkspaceDir));
|
|
309
|
+
const implicitWorkspaceFamilyRoot = normalizedDefaultWorkspace === void 0 ? void 0 : normalizedDefaultWorkspace.replace(/(?:-[^/]+)?$/u, "");
|
|
310
|
+
return normalized === TOOL_VM_WORKSPACE_GUEST_ROOT || normalized.startsWith(`${TOOL_VM_WORKSPACE_GUEST_ROOT}/`) || normalized === TOOL_VM_SCRATCH_GUEST_ROOT || normalized.startsWith(`${TOOL_VM_SCRATCH_GUEST_ROOT}/`) || normalized === OPENCLAW_STATE_SANDBOXES_VM_ROOT || normalized.startsWith(`${OPENCLAW_STATE_SANDBOXES_VM_ROOT}/`) || normalizedDefaultWorkspace !== void 0 && (pathIsInsideOrEqual(normalized, normalizedDefaultWorkspace) || normalized.startsWith(`${normalizedDefaultWorkspace}-`)) || implicitWorkspaceFamilyRoot !== void 0 && (pathIsInsideOrEqual(normalized, implicitWorkspaceFamilyRoot) || normalized.startsWith(`${implicitWorkspaceFamilyRoot}-`));
|
|
311
|
+
}
|
|
312
|
+
function resolveUserPathLikeOpenClaw(inputPath) {
|
|
313
|
+
const trimmedPath = inputPath.trim();
|
|
314
|
+
const homeDirectory = process.env.HOME?.trim();
|
|
315
|
+
if (trimmedPath === "~" && homeDirectory) return homeDirectory;
|
|
316
|
+
if (trimmedPath.startsWith("~/") && homeDirectory) return path.resolve(path.join(homeDirectory, trimmedPath.slice(2)));
|
|
317
|
+
return path.resolve(trimmedPath);
|
|
318
|
+
}
|
|
319
|
+
function assertCanonicalSourcePath(inputPath, context) {
|
|
320
|
+
const trimmedPath = inputPath.trim();
|
|
321
|
+
if (trimmedPath === "" || containsParentTraversal(trimmedPath)) throw new OpenClawAgentWorkspaceSourceError(`${context} must be a non-empty path without parent traversal.`);
|
|
322
|
+
if (!trimmedPath.startsWith("/") && !trimmedPath.startsWith("~")) throw new OpenClawAgentWorkspaceSourceError(`${context} must be an absolute or home-relative path.`);
|
|
323
|
+
const normalized = normalizeAbsolutePosixPath(resolveUserPathLikeOpenClaw(trimmedPath));
|
|
324
|
+
if (normalized === "/" || normalized === TOOL_VM_WORKSPACE_GUEST_ROOT || normalized.startsWith(`${TOOL_VM_WORKSPACE_GUEST_ROOT}/`) || normalized === TOOL_VM_SCRATCH_GUEST_ROOT || normalized.startsWith(`${TOOL_VM_SCRATCH_GUEST_ROOT}/`)) throw new OpenClawAgentWorkspaceSourceError(`${context} must resolve to an OpenClaw/Gondolin source path, not Tool VM guest path '${normalized}'.`);
|
|
325
|
+
if (normalized === OPENCLAW_STATE_SANDBOXES_VM_ROOT || normalized.startsWith(`${OPENCLAW_STATE_SANDBOXES_VM_ROOT}/`)) throw new OpenClawAgentWorkspaceSourceError(`${context} must resolve to a stable agent workspace path, not transient OpenClaw sandbox path '${normalized}'.`);
|
|
326
|
+
return normalized;
|
|
327
|
+
}
|
|
328
|
+
function assertLeaseBackedSourcePath(inputPath, context, defaultWorkspaceDir) {
|
|
329
|
+
const normalized = assertCanonicalSourcePath(inputPath, context);
|
|
330
|
+
if (isRuntimePathLeak(normalized, defaultWorkspaceDir)) throw new OpenClawAgentWorkspaceSourceError(`${context} must resolve to a controller lease-backed OpenClaw/Gondolin source path, not OpenClaw runtime fallback path '${normalized}'.`);
|
|
331
|
+
return normalized;
|
|
332
|
+
}
|
|
333
|
+
function readWorkspace(value) {
|
|
334
|
+
return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
|
|
335
|
+
}
|
|
336
|
+
function readAgentId(value) {
|
|
337
|
+
return normalizeOpenClawAgentId(typeof value === "string" ? value : void 0);
|
|
338
|
+
}
|
|
339
|
+
function agentEntries(config) {
|
|
340
|
+
return config?.agents?.list?.filter(isRecord) ?? [];
|
|
341
|
+
}
|
|
342
|
+
function findAgentEntry(config, agentId) {
|
|
343
|
+
return agentEntries(config).find((entry) => readAgentId(entry.id) === agentId);
|
|
344
|
+
}
|
|
345
|
+
function resolveDefaultAgentId(config) {
|
|
346
|
+
const entries = agentEntries(config);
|
|
347
|
+
return readAgentId((entries.find((entry) => entry.default === true) ?? entries[0])?.id);
|
|
348
|
+
}
|
|
349
|
+
function resolveOpenClawAgentWorkspaceSource(options) {
|
|
350
|
+
const agentId = normalizeOpenClawAgentId(options.agentId);
|
|
351
|
+
const agentWorkspace = readWorkspace(findAgentEntry(options.openClawConfig, agentId)?.workspace);
|
|
352
|
+
if (agentWorkspace !== void 0) return {
|
|
353
|
+
kind: "configured-agent-workspace",
|
|
354
|
+
sourceDir: assertLeaseBackedSourcePath(agentWorkspace, `agents.list workspace for '${agentId}'`, options.defaultWorkspaceDir)
|
|
355
|
+
};
|
|
356
|
+
const defaultsWorkspace = readWorkspace(options.openClawConfig?.agents?.defaults?.workspace);
|
|
357
|
+
if (defaultsWorkspace !== void 0) {
|
|
358
|
+
const defaultsRoot = assertLeaseBackedSourcePath(defaultsWorkspace, "agents.defaults.workspace", options.defaultWorkspaceDir);
|
|
359
|
+
const defaultAgentId = resolveDefaultAgentId(options.openClawConfig);
|
|
360
|
+
return {
|
|
361
|
+
kind: agentId === defaultAgentId ? "default-agent-workspace" : "default-workspace-child",
|
|
362
|
+
sourceDir: agentId === defaultAgentId ? defaultsRoot : path.join(defaultsRoot, agentId)
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!isRuntimePathLeak(options.paramsAgentWorkspaceDir, options.defaultWorkspaceDir)) return {
|
|
366
|
+
kind: "sdk-agent-workspace",
|
|
367
|
+
sourceDir: assertCanonicalSourcePath(options.paramsAgentWorkspaceDir, "OpenClaw backend agentWorkspaceDir")
|
|
368
|
+
};
|
|
369
|
+
const stateRoot = options.stateDir === void 0 ? void 0 : assertCanonicalSourcePath(options.stateDir, "OpenClaw stateDir");
|
|
370
|
+
if (stateRoot === void 0) throw new OpenClawAgentWorkspaceSourceError(`OpenClaw provided agentWorkspaceDir '${options.paramsAgentWorkspaceDir}' for agent '${agentId}', which is a runtime path. Provide an OpenClaw stateDir provider or configure agents.list[].workspace.`);
|
|
371
|
+
if (agentId === resolveDefaultAgentId(options.openClawConfig)) throw new OpenClawAgentWorkspaceSourceError(`OpenClaw provided agentWorkspaceDir '${options.paramsAgentWorkspaceDir}' for default agent '${agentId}', but OpenClaw's implicit default workspace is not controller lease backed; configure agents.list[].workspace or agents.defaults.workspace for managed Gondolin agents.`);
|
|
372
|
+
return {
|
|
373
|
+
kind: "state-workspace-child",
|
|
374
|
+
sourceDir: path.join(stateRoot, `workspace-${agentId}`)
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
//#endregion
|
|
378
|
+
//#region src/sandbox-backend/openclaw-tool-vm-path-mapping.ts
|
|
379
|
+
var OpenClawToolVmPathIntentError = class extends Error {
|
|
380
|
+
details;
|
|
381
|
+
constructor(details) {
|
|
382
|
+
super(`${details.message} ${details.retryGuidance}`);
|
|
383
|
+
this.name = "OpenClawToolVmPathIntentError";
|
|
384
|
+
this.details = details;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
function pathContainsParentTraversal(inputPath) {
|
|
388
|
+
return inputPath.split(/\/+/u).includes("..");
|
|
389
|
+
}
|
|
390
|
+
function normalizedAbsolutePath(inputPath) {
|
|
391
|
+
return `/${inputPath.split("/").filter((segment) => segment !== "" && segment !== ".").join("/")}`;
|
|
392
|
+
}
|
|
393
|
+
function invalidAgentWorkspaceRootError(agentWorkspaceDir) {
|
|
394
|
+
return {
|
|
395
|
+
allowedPathForms: [],
|
|
396
|
+
code: "invalid-runtime-root",
|
|
397
|
+
inputPath: agentWorkspaceDir,
|
|
398
|
+
mappingId: "openclaw-tool-vm",
|
|
399
|
+
message: `OpenClaw agentWorkspaceDir '${agentWorkspaceDir}' must be an absolute non-root path without parent traversal.`,
|
|
400
|
+
purpose: "executionCwd",
|
|
401
|
+
retryGuidance: "Retry with OpenClaw agentWorkspaceDir set to the resolved host RealFS workspace for the requested agent."
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function validateAgentWorkspaceDir(agentWorkspaceDir) {
|
|
405
|
+
if (agentWorkspaceDir.trim() === "" || !agentWorkspaceDir.startsWith("/") || normalizedAbsolutePath(agentWorkspaceDir) === "/" || pathContainsParentTraversal(agentWorkspaceDir)) return invalidAgentWorkspaceRootError(agentWorkspaceDir);
|
|
406
|
+
}
|
|
407
|
+
function createOpenClawToolVmPathMapping(options) {
|
|
408
|
+
return {
|
|
409
|
+
id: "openclaw-tool-vm",
|
|
410
|
+
roots: [
|
|
411
|
+
{
|
|
412
|
+
id: "agent-workspace",
|
|
413
|
+
backing: {
|
|
414
|
+
kind: "host-realfs",
|
|
415
|
+
durability: "durable",
|
|
416
|
+
backup: "included"
|
|
417
|
+
},
|
|
418
|
+
capabilities: {
|
|
419
|
+
executionCwd: true,
|
|
420
|
+
leaseMount: true
|
|
421
|
+
},
|
|
422
|
+
locations: {
|
|
423
|
+
"openclaw-gateway": options.agentWorkspaceDir,
|
|
424
|
+
"tool-vm-guest": TOOL_VM_WORKSPACE_GUEST_ROOT
|
|
425
|
+
},
|
|
426
|
+
rootPathAllowed: true,
|
|
427
|
+
guidanceLabel: "agent workspace"
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: "tool-vm-scratch",
|
|
431
|
+
backing: {
|
|
432
|
+
kind: "guest-rootfs-cow",
|
|
433
|
+
durability: "vm-lifetime"
|
|
434
|
+
},
|
|
435
|
+
capabilities: {
|
|
436
|
+
executionCwd: true,
|
|
437
|
+
leaseMount: false
|
|
438
|
+
},
|
|
439
|
+
locations: { "tool-vm-guest": TOOL_VM_SCRATCH_GUEST_ROOT },
|
|
440
|
+
rootPathAllowed: true,
|
|
441
|
+
guidanceLabel: "Tool VM scratch"
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
id: "openclaw-sandboxes",
|
|
445
|
+
backing: {
|
|
446
|
+
kind: "host-realfs",
|
|
447
|
+
durability: "durable",
|
|
448
|
+
backup: "included"
|
|
449
|
+
},
|
|
450
|
+
capabilities: {
|
|
451
|
+
executionCwd: true,
|
|
452
|
+
leaseMount: true
|
|
453
|
+
},
|
|
454
|
+
locations: { "openclaw-gateway": OPENCLAW_STATE_SANDBOXES_VM_ROOT },
|
|
455
|
+
rootPathAllowed: false,
|
|
456
|
+
guidanceLabel: "OpenClaw sandbox work directory"
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
function resolveOpenClawSandboxPathIntent(translation) {
|
|
462
|
+
const [sandboxChild, ...guestCwdSegments] = translation.relativePath.split("/");
|
|
463
|
+
const leaseWorkMountDir = sandboxChild === void 0 || sandboxChild === "" ? translation.outputPath : `${OPENCLAW_STATE_SANDBOXES_VM_ROOT}/${sandboxChild}`;
|
|
464
|
+
return {
|
|
465
|
+
effectiveGuestCwd: guestCwdSegments.length === 0 ? TOOL_VM_WORKSPACE_GUEST_ROOT : `${TOOL_VM_WORKSPACE_GUEST_ROOT}/${guestCwdSegments.join("/")}`,
|
|
466
|
+
leaseWorkMountDir
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function kindForTranslation(translation) {
|
|
470
|
+
const isRoot = translation.relativePath === "";
|
|
471
|
+
if (translation.rootId === "tool-vm-scratch") return isRoot ? "scratch-root" : "scratch-subpath";
|
|
472
|
+
if (translation.rootId === "openclaw-sandboxes") return "openclaw-sandbox-path";
|
|
473
|
+
if (translation.inputNamespace === "openclaw-gateway") return isRoot ? "host-workspace-root" : "host-workspace-subpath";
|
|
474
|
+
return isRoot ? "workspace-root" : "workspace-subpath";
|
|
475
|
+
}
|
|
476
|
+
function leaseRootForTranslation(translation) {
|
|
477
|
+
return translation.relativePath === "" ? translation.outputPath : translation.outputPath.slice(0, -(translation.relativePath.length + 1));
|
|
478
|
+
}
|
|
479
|
+
function resolveOpenClawToolVmPathIntent(options) {
|
|
480
|
+
const agentWorkspaceDirError = validateAgentWorkspaceDir(options.agentWorkspaceDir);
|
|
481
|
+
if (agentWorkspaceDirError !== void 0) return {
|
|
482
|
+
error: agentWorkspaceDirError,
|
|
483
|
+
ok: false
|
|
484
|
+
};
|
|
485
|
+
const mappings = [createOpenClawToolVmPathMapping({ agentWorkspaceDir: options.agentWorkspaceDir }), ...(options.equivalentAgentWorkspaceDirs ?? []).map((equivalentAgentWorkspaceDir) => createOpenClawToolVmPathMapping({ agentWorkspaceDir: equivalentAgentWorkspaceDir }))];
|
|
486
|
+
const invalidEquivalentRoot = (options.equivalentAgentWorkspaceDirs ?? []).map((equivalentAgentWorkspaceDir) => validateAgentWorkspaceDir(equivalentAgentWorkspaceDir)).find((error) => error !== void 0);
|
|
487
|
+
if (invalidEquivalentRoot !== void 0) return {
|
|
488
|
+
error: invalidEquivalentRoot,
|
|
489
|
+
ok: false
|
|
490
|
+
};
|
|
491
|
+
const mapping = createOpenClawToolVmPathMapping({ agentWorkspaceDir: options.agentWorkspaceDir });
|
|
492
|
+
const sandboxTranslation = translateRuntimePath({
|
|
493
|
+
inputPath: options.inputPath,
|
|
494
|
+
mapping,
|
|
495
|
+
purpose: "executionCwd",
|
|
496
|
+
sourceNamespace: "openclaw-gateway",
|
|
497
|
+
targetNamespace: "openclaw-gateway"
|
|
498
|
+
});
|
|
499
|
+
if (sandboxTranslation.ok && sandboxTranslation.value.rootId === "openclaw-sandboxes") {
|
|
500
|
+
const sandboxPathIntent = resolveOpenClawSandboxPathIntent(sandboxTranslation.value);
|
|
501
|
+
return {
|
|
502
|
+
ok: true,
|
|
503
|
+
value: {
|
|
504
|
+
effectiveGuestCwd: sandboxPathIntent.effectiveGuestCwd,
|
|
505
|
+
hostEquivalentPath: sandboxTranslation.value.outputPath,
|
|
506
|
+
kind: kindForTranslation(sandboxTranslation.value),
|
|
507
|
+
leaseWorkMountDir: sandboxPathIntent.leaseWorkMountDir
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const translationResults = mappings.map((candidateMapping) => translateRuntimePath({
|
|
512
|
+
inputPath: options.inputPath,
|
|
513
|
+
mapping: candidateMapping,
|
|
514
|
+
purpose: "executionCwd",
|
|
515
|
+
targetNamespace: "tool-vm-guest"
|
|
516
|
+
}));
|
|
517
|
+
const translation = translationResults.find((candidateTranslation) => candidateTranslation.ok);
|
|
518
|
+
if (translation === void 0) {
|
|
519
|
+
const primaryTranslation = translationResults[0];
|
|
520
|
+
if (primaryTranslation === void 0 || primaryTranslation.ok) return {
|
|
521
|
+
error: invalidAgentWorkspaceRootError(options.agentWorkspaceDir),
|
|
522
|
+
ok: false
|
|
523
|
+
};
|
|
524
|
+
return primaryTranslation;
|
|
525
|
+
}
|
|
526
|
+
const hostEquivalentTranslation = translateRuntimePath({
|
|
527
|
+
inputPath: options.inputPath,
|
|
528
|
+
mapping,
|
|
529
|
+
purpose: "executionCwd",
|
|
530
|
+
targetNamespace: "openclaw-gateway"
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
ok: true,
|
|
534
|
+
value: {
|
|
535
|
+
effectiveGuestCwd: translation.value.outputPath,
|
|
536
|
+
...hostEquivalentTranslation.ok ? { hostEquivalentPath: hostEquivalentTranslation.value.outputPath } : {},
|
|
537
|
+
kind: kindForTranslation(translation.value),
|
|
538
|
+
leaseWorkMountDir: hostEquivalentTranslation.ok && hostEquivalentTranslation.value.rootId !== "tool-vm-scratch" ? leaseRootForTranslation(hostEquivalentTranslation.value) : options.agentWorkspaceDir
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function assertOpenClawToolVmPathIntent(options) {
|
|
543
|
+
const result = resolveOpenClawToolVmPathIntent(options);
|
|
544
|
+
if (!result.ok) throw new OpenClawToolVmPathIntentError(result.error);
|
|
545
|
+
return result.value;
|
|
546
|
+
}
|
|
547
|
+
//#endregion
|
|
234
548
|
//#region src/sandbox-backend/sandbox-shell-script.ts
|
|
235
549
|
function buildShellScriptWithArgs(script, args) {
|
|
236
550
|
if (!args || args.length === 0) return script;
|
|
237
551
|
return `set -- ${args.map((arg) => `'${arg.replace(/'/g, "'\\''")}'`).join(" ")}; ${script}`;
|
|
238
552
|
}
|
|
239
553
|
//#endregion
|
|
554
|
+
//#region src/sandbox-backend/tool-vm-ssh-operation-guard.ts
|
|
555
|
+
var ToolVmSshOperationStaleError = class extends Error {
|
|
556
|
+
cause;
|
|
557
|
+
reason;
|
|
558
|
+
constructor(options) {
|
|
559
|
+
super(options.message);
|
|
560
|
+
this.cause = options.cause;
|
|
561
|
+
this.reason = options.reason;
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
function formatUnknownError$1(error) {
|
|
565
|
+
return error instanceof Error ? error.message : String(error);
|
|
566
|
+
}
|
|
567
|
+
function defaultWriteLog$1(message) {
|
|
568
|
+
process.stderr.write(`[tool-vm-ssh-operation-guard] ${message}\n`);
|
|
569
|
+
}
|
|
570
|
+
async function publishHealthEvent(options) {
|
|
571
|
+
if (!options.guardOptions.healthEvent) return;
|
|
572
|
+
const event = {
|
|
573
|
+
agentId: options.guardOptions.healthEvent.agentId,
|
|
574
|
+
elapsedMs: options.elapsedMs,
|
|
575
|
+
...options.errorCode === void 0 ? {} : { errorCode: options.errorCode },
|
|
576
|
+
kind: "tool-vm-ssh",
|
|
577
|
+
leaseId: options.guardOptions.healthEvent.leaseId,
|
|
578
|
+
observedAtMs: options.observedAtMs,
|
|
579
|
+
operation: options.guardOptions.healthEvent.operation,
|
|
580
|
+
result: options.result,
|
|
581
|
+
zoneId: options.guardOptions.healthEvent.zoneId
|
|
582
|
+
};
|
|
583
|
+
try {
|
|
584
|
+
await options.guardOptions.healthEvent.publish(event);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
(options.guardOptions.writeLog ?? defaultWriteLog$1)(`tool-vm-ssh health publish failed operation=${options.guardOptions.healthEvent.operation} elapsedMs=${String(options.elapsedMs)} error=${formatUnknownError$1(error)}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function runToolVmSshOperationWithGuard(options) {
|
|
590
|
+
const now = options.now ?? Date.now;
|
|
591
|
+
const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
|
|
592
|
+
const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
|
|
593
|
+
const abortController = new AbortController();
|
|
594
|
+
const startedAtMs = now();
|
|
595
|
+
let timeoutHandle;
|
|
596
|
+
options.report({
|
|
597
|
+
observedAtMs: now(),
|
|
598
|
+
phase: "running"
|
|
599
|
+
});
|
|
600
|
+
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
601
|
+
timeoutHandle = setTimeoutImpl(() => {
|
|
602
|
+
abortController.abort();
|
|
603
|
+
reject(new ToolVmSshOperationStaleError({
|
|
604
|
+
cause: void 0,
|
|
605
|
+
message: `${options.operationName} exceeded ${String(options.timeoutMs)}ms.`,
|
|
606
|
+
reason: "ssh-command-timed-out"
|
|
607
|
+
}));
|
|
608
|
+
}, options.timeoutMs);
|
|
609
|
+
});
|
|
610
|
+
try {
|
|
611
|
+
const result = await Promise.race([options.operation(abortController.signal), timeoutPromise]);
|
|
612
|
+
options.report({
|
|
613
|
+
observedAtMs: now(),
|
|
614
|
+
phase: "completed",
|
|
615
|
+
ssh: { probeSucceeded: true }
|
|
616
|
+
});
|
|
617
|
+
const observedAtMs = now();
|
|
618
|
+
publishHealthEvent({
|
|
619
|
+
elapsedMs: observedAtMs - startedAtMs,
|
|
620
|
+
guardOptions: options,
|
|
621
|
+
observedAtMs,
|
|
622
|
+
result: "ok"
|
|
623
|
+
});
|
|
624
|
+
return result;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
const staleError = error instanceof ToolVmSshOperationStaleError ? error : new ToolVmSshOperationStaleError({
|
|
627
|
+
cause: error,
|
|
628
|
+
message: formatUnknownError$1(error),
|
|
629
|
+
reason: "ssh-command-failed"
|
|
630
|
+
});
|
|
631
|
+
options.report({
|
|
632
|
+
observedAtMs: now(),
|
|
633
|
+
phase: "failed",
|
|
634
|
+
ssh: { failure: {
|
|
635
|
+
kind: staleError.reason,
|
|
636
|
+
message: staleError.message
|
|
637
|
+
} }
|
|
638
|
+
});
|
|
639
|
+
const observedAtMs = now();
|
|
640
|
+
publishHealthEvent({
|
|
641
|
+
elapsedMs: observedAtMs - startedAtMs,
|
|
642
|
+
errorCode: staleError.reason,
|
|
643
|
+
guardOptions: options,
|
|
644
|
+
observedAtMs,
|
|
645
|
+
result: "failed"
|
|
646
|
+
});
|
|
647
|
+
throw staleError;
|
|
648
|
+
} finally {
|
|
649
|
+
if (timeoutHandle !== void 0) clearTimeoutImpl(timeoutHandle);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
//#endregion
|
|
240
653
|
//#region src/sandbox-backend/sandbox-backend-handle-factory.ts
|
|
241
654
|
function agentLeaseCacheKey(params) {
|
|
242
|
-
return [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
655
|
+
return [params.zoneId, params.agentId].join("\0");
|
|
656
|
+
}
|
|
657
|
+
function findCachedLeaseCompatibilityMismatch(params) {
|
|
658
|
+
if (params.cachedEntry.agentWorkspaceDir !== params.requestedEntry.agentWorkspaceDir) return "agentWorkspaceDir";
|
|
659
|
+
if (params.cachedEntry.leaseWorkMountDir !== params.requestedEntry.leaseWorkMountDir) return "leaseWorkMountDir";
|
|
660
|
+
if (params.cachedEntry.profileId !== params.requestedEntry.profileId) return "profileId";
|
|
661
|
+
}
|
|
662
|
+
function assertCachedLeaseCompatible(params) {
|
|
663
|
+
const mismatch = findCachedLeaseCompatibilityMismatch(params);
|
|
664
|
+
if (mismatch === void 0) return;
|
|
665
|
+
throw new Error(`Cannot reuse cached Tool VM lease for zone '${params.zoneId}' agent '${params.agentId}': ${mismatch} changed.`);
|
|
249
666
|
}
|
|
250
667
|
function formatControllerLeaseRequestError(error) {
|
|
251
668
|
const responseBody = error.responseBody === void 0 ? error.bodyText : JSON.stringify(error.responseBody);
|
|
@@ -259,7 +676,10 @@ function writeSandboxBackendLog(message) {
|
|
|
259
676
|
process.stderr.write(`[openclaw-agent-vm-plugin] ${message}\n`);
|
|
260
677
|
}
|
|
261
678
|
function shouldRefreshCachedLease(error) {
|
|
262
|
-
return error
|
|
679
|
+
return isRefreshableLeaseError(error);
|
|
680
|
+
}
|
|
681
|
+
function isRefreshableLeaseError(error) {
|
|
682
|
+
return error instanceof ControllerLeaseRequestError && (error.status === 404 || error.status === 410);
|
|
263
683
|
}
|
|
264
684
|
function isCleanupNotFound(error) {
|
|
265
685
|
return error instanceof ControllerLeaseRequestError && error.status === 404;
|
|
@@ -273,111 +693,276 @@ function isActiveUseFinalizeToken(value) {
|
|
|
273
693
|
function activeUseOutcomeForFinalizeParams(finalizeParams) {
|
|
274
694
|
return finalizeParams.timedOut ? "timed-out" : finalizeParams.status === "completed" ? "completed" : "failed";
|
|
275
695
|
}
|
|
696
|
+
async function publishFinalizeToolVmSshHealthEvent(options) {
|
|
697
|
+
const event = {
|
|
698
|
+
agentId: options.agentId,
|
|
699
|
+
elapsedMs: 0,
|
|
700
|
+
...options.timedOut ? { errorCode: "ssh-command-timed-out" } : {},
|
|
701
|
+
kind: "tool-vm-ssh",
|
|
702
|
+
leaseId: options.leaseId,
|
|
703
|
+
observedAtMs: Date.now(),
|
|
704
|
+
operation: "finalize",
|
|
705
|
+
result: options.timedOut ? "failed" : "ok",
|
|
706
|
+
zoneId: options.zoneId
|
|
707
|
+
};
|
|
708
|
+
try {
|
|
709
|
+
await options.publishHealthEvent(event);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
writeSandboxBackendLog(`tool-vm-ssh finalize health publish failed for zone '${options.zoneId}' lease '${options.leaseId}': ${formatUnknownError(error)}`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function mergedAbortSignal(firstSignal, secondSignal) {
|
|
715
|
+
if (firstSignal === void 0) return secondSignal;
|
|
716
|
+
return AbortSignal.any([firstSignal, secondSignal]);
|
|
717
|
+
}
|
|
718
|
+
function mergedAbortSignals(signals) {
|
|
719
|
+
const presentSignals = signals.filter((signal) => signal !== void 0);
|
|
720
|
+
if (presentSignals.length === 0) return;
|
|
721
|
+
if (presentSignals.length === 1) return presentSignals[0];
|
|
722
|
+
return AbortSignal.any(presentSignals);
|
|
723
|
+
}
|
|
276
724
|
function resolveLeaseRequestAgentId(sessionKey) {
|
|
277
725
|
return resolveOpenClawAgentIdFromSessionKey(sessionKey);
|
|
278
726
|
}
|
|
727
|
+
function defaultOpenClawStateDir() {
|
|
728
|
+
const explicitStateDir = process.env.OPENCLAW_STATE_DIR?.trim();
|
|
729
|
+
if (explicitStateDir) return path.resolve(explicitStateDir);
|
|
730
|
+
const homeDirectory = process.env.HOME?.trim();
|
|
731
|
+
return homeDirectory ? path.join(homeDirectory, ".openclaw", "state") : void 0;
|
|
732
|
+
}
|
|
733
|
+
function defaultOpenClawWorkspaceDir() {
|
|
734
|
+
const homeDirectory = process.env.HOME?.trim();
|
|
735
|
+
if (!homeDirectory) return;
|
|
736
|
+
const profile = process.env.OPENCLAW_PROFILE?.trim().toLowerCase();
|
|
737
|
+
return profile && profile !== "default" ? path.join(homeDirectory, ".openclaw", `workspace-${profile}`) : path.join(homeDirectory, ".openclaw", "workspace");
|
|
738
|
+
}
|
|
279
739
|
function assertPluginLeaseContract(params) {
|
|
280
740
|
const mismatch = findOpenClawGondolinSandboxMismatch(params.cfg);
|
|
281
741
|
if (mismatch) throw new Error(`OpenClaw Gondolin sandbox requires ${mismatch.key}=${mismatch.expectedValue}; received ${String(params.cfg[mismatch.key])}.`);
|
|
282
742
|
}
|
|
283
743
|
function createGondolinSandboxBackendFactory(options, dependencies) {
|
|
284
|
-
const
|
|
744
|
+
const agentLeaseCache = /* @__PURE__ */ new Map();
|
|
745
|
+
const inFlightLeaseRequests = /* @__PURE__ */ new Map();
|
|
285
746
|
return async (params) => {
|
|
286
747
|
const profileId = options.profileId ?? "standard";
|
|
287
748
|
const agentId = resolveLeaseRequestAgentId(params.sessionKey);
|
|
288
|
-
assertPluginLeaseContract({
|
|
749
|
+
assertPluginLeaseContract({ cfg: params.cfg });
|
|
750
|
+
const defaultWorkspaceDir = options.openClawDefaultWorkspaceDirProvider?.() ?? defaultOpenClawWorkspaceDir();
|
|
751
|
+
const equivalentAgentWorkspaceDirs = defaultWorkspaceDir === void 0 ? [] : [defaultWorkspaceDir];
|
|
752
|
+
const workspaceSource = resolveOpenClawAgentWorkspaceSource({
|
|
289
753
|
agentId,
|
|
290
|
-
|
|
291
|
-
|
|
754
|
+
defaultWorkspaceDir,
|
|
755
|
+
openClawConfig: options.openClawRuntimeConfigProvider?.(),
|
|
756
|
+
paramsAgentWorkspaceDir: params.agentWorkspaceDir,
|
|
757
|
+
stateDir: options.openClawStateDirProvider?.() ?? defaultOpenClawStateDir()
|
|
758
|
+
});
|
|
759
|
+
const pathIntent = assertOpenClawToolVmPathIntent({
|
|
760
|
+
agentWorkspaceDir: workspaceSource.sourceDir,
|
|
761
|
+
equivalentAgentWorkspaceDirs,
|
|
762
|
+
inputPath: params.workspaceDir
|
|
292
763
|
});
|
|
293
764
|
const cacheKey = agentLeaseCacheKey({
|
|
294
765
|
agentId,
|
|
295
|
-
agentWorkspaceDir: params.agentWorkspaceDir,
|
|
296
|
-
profileId,
|
|
297
|
-
workspaceDir: params.workspaceDir,
|
|
298
766
|
zoneId: options.zoneId
|
|
299
767
|
});
|
|
768
|
+
const requestedCacheEntry = {
|
|
769
|
+
agentWorkspaceDir: workspaceSource.sourceDir,
|
|
770
|
+
leaseWorkMountDir: pathIntent.leaseWorkMountDir,
|
|
771
|
+
profileId
|
|
772
|
+
};
|
|
300
773
|
const leaseClient = dependencies.createLeaseClient?.({ controllerUrl: options.controllerUrl }) ?? createLeaseClient({ controllerUrl: options.controllerUrl });
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
774
|
+
const publishHealthEvent = async (event) => {
|
|
775
|
+
const response = await fetchControllerWithPolicy({
|
|
776
|
+
input: `${options.controllerUrl.replace(/\/+$/u, "")}/zones/${encodeURIComponent(options.zoneId)}/health-events`,
|
|
777
|
+
init: {
|
|
778
|
+
body: JSON.stringify(event),
|
|
779
|
+
headers: { "content-type": "application/json" },
|
|
780
|
+
method: "POST"
|
|
781
|
+
},
|
|
782
|
+
operation: "health-event-publish"
|
|
783
|
+
});
|
|
784
|
+
if (!response.ok) {
|
|
785
|
+
await response.text().catch(() => void 0);
|
|
786
|
+
throw new Error(`health event publish returned HTTP ${String(response.status)}`);
|
|
787
|
+
}
|
|
788
|
+
await response.text().catch(() => void 0);
|
|
789
|
+
};
|
|
790
|
+
const markLeaseStale = async (lease, reason, error) => {
|
|
791
|
+
agentLeaseCache.delete(cacheKey);
|
|
792
|
+
writeSandboxBackendLog(`lease marked stale for zone '${options.zoneId}' agent '${agentId}' lease '${lease.leaseId}' reason '${reason}': ${formatUnknownError(error)}`);
|
|
793
|
+
await leaseClient.releaseLease(lease.leaseId, { force: true }).catch((releaseError) => {
|
|
794
|
+
writeSandboxBackendLog(`best-effort stale lease release failed for zone '${options.zoneId}' agent '${agentId}' lease '${lease.leaseId}': ${formatUnknownError(releaseError)}`);
|
|
795
|
+
});
|
|
796
|
+
};
|
|
797
|
+
const cachedEntry = agentLeaseCache.get(cacheKey);
|
|
798
|
+
let lease;
|
|
799
|
+
if (cachedEntry) {
|
|
800
|
+
assertCachedLeaseCompatible({
|
|
801
|
+
agentId,
|
|
802
|
+
cachedEntry,
|
|
803
|
+
requestedEntry: requestedCacheEntry,
|
|
804
|
+
zoneId: options.zoneId
|
|
805
|
+
});
|
|
806
|
+
try {
|
|
807
|
+
const renewedLease = await leaseClient.renewLease(cachedEntry.lease.leaseId);
|
|
808
|
+
await runToolVmSshOperationWithGuard({
|
|
809
|
+
healthEvent: {
|
|
810
|
+
agentId,
|
|
811
|
+
leaseId: renewedLease.leaseId,
|
|
812
|
+
operation: "probe",
|
|
813
|
+
publish: publishHealthEvent,
|
|
814
|
+
zoneId: options.zoneId
|
|
815
|
+
},
|
|
816
|
+
operation: async (signal) => await dependencies.runRemoteShellScript({
|
|
817
|
+
allowFailure: false,
|
|
818
|
+
script: "true",
|
|
819
|
+
signal,
|
|
820
|
+
ssh: renewedLease.ssh
|
|
821
|
+
}),
|
|
822
|
+
operationName: "cached-ssh-probe",
|
|
823
|
+
report: () => {},
|
|
824
|
+
timeoutMs: 3e4
|
|
825
|
+
});
|
|
826
|
+
lease = renewedLease;
|
|
827
|
+
agentLeaseCache.set(cacheKey, {
|
|
828
|
+
...requestedCacheEntry,
|
|
829
|
+
lease
|
|
830
|
+
});
|
|
831
|
+
} catch (error) {
|
|
832
|
+
writeSandboxBackendLog(`lease renew failed for zone '${options.zoneId}' agent '${agentId}' lease '${cachedEntry.lease.leaseId}': ${formatUnknownError(error)}`);
|
|
833
|
+
if (error instanceof ToolVmSshOperationStaleError) await markLeaseStale(cachedEntry.lease, error.reason, error);
|
|
834
|
+
else if (shouldRefreshCachedLease(error)) agentLeaseCache.delete(cacheKey);
|
|
835
|
+
else throw error;
|
|
836
|
+
}
|
|
309
837
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
838
|
+
if (lease === void 0) {
|
|
839
|
+
const inFlightLeaseRequest = inFlightLeaseRequests.get(cacheKey);
|
|
840
|
+
if (inFlightLeaseRequest !== void 0) {
|
|
841
|
+
const inFlightEntry = await inFlightLeaseRequest;
|
|
842
|
+
assertCachedLeaseCompatible({
|
|
843
|
+
agentId,
|
|
844
|
+
cachedEntry: inFlightEntry,
|
|
845
|
+
requestedEntry: requestedCacheEntry,
|
|
846
|
+
zoneId: options.zoneId
|
|
847
|
+
});
|
|
848
|
+
lease = inFlightEntry.lease;
|
|
849
|
+
} else {
|
|
850
|
+
const leaseRequestPromise = (async () => {
|
|
851
|
+
const runtimeStatus = options.openClawRuntimeStatusProvider?.();
|
|
852
|
+
if (runtimeStatus && leaseClient.publishOpenClawRuntimeStatus) await leaseClient.publishOpenClawRuntimeStatus(runtimeStatus);
|
|
853
|
+
const leaseResponse = await leaseClient.requestLease({
|
|
854
|
+
agentId,
|
|
855
|
+
agentWorkspaceDir: workspaceSource.sourceDir,
|
|
856
|
+
profileId,
|
|
857
|
+
sessionKey: params.sessionKey,
|
|
858
|
+
workMountDir: pathIntent.leaseWorkMountDir,
|
|
859
|
+
zoneId: options.zoneId
|
|
860
|
+
});
|
|
861
|
+
if (!isToolVmSshLease(leaseResponse)) throw new TypeError("Controller lease API returned an unexpected response.");
|
|
862
|
+
return {
|
|
863
|
+
...requestedCacheEntry,
|
|
864
|
+
lease: leaseResponse
|
|
865
|
+
};
|
|
866
|
+
})();
|
|
867
|
+
inFlightLeaseRequests.set(cacheKey, leaseRequestPromise);
|
|
868
|
+
try {
|
|
869
|
+
const leaseEntry = await leaseRequestPromise;
|
|
870
|
+
agentLeaseCache.set(cacheKey, leaseEntry);
|
|
871
|
+
lease = leaseEntry.lease;
|
|
872
|
+
} finally {
|
|
873
|
+
if (inFlightLeaseRequests.get(cacheKey) === leaseRequestPromise) inFlightLeaseRequests.delete(cacheKey);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return createSandboxBackendHandle({
|
|
325
878
|
cfg: params.cfg,
|
|
326
879
|
controllerUrl: options.controllerUrl,
|
|
327
880
|
createFsBridgeBuilder: dependencies.createFsBridgeBuilder,
|
|
881
|
+
effectiveGuestCwd: pathIntent.effectiveGuestCwd,
|
|
328
882
|
lease,
|
|
329
883
|
leaseClient,
|
|
884
|
+
markCachedLeaseStale: async (reason, error) => {
|
|
885
|
+
await markLeaseStale(lease, reason, error);
|
|
886
|
+
},
|
|
887
|
+
publishHealthEvent,
|
|
330
888
|
runRemoteShellScript: dependencies.runRemoteShellScript,
|
|
331
889
|
buildExecSpec: dependencies.buildExecSpec,
|
|
332
|
-
scopeKey: params.scopeKey,
|
|
333
890
|
sessionKey: params.sessionKey,
|
|
334
891
|
zoneId: options.zoneId
|
|
335
892
|
});
|
|
336
|
-
scopeCache.set(cacheKey, {
|
|
337
|
-
handle,
|
|
338
|
-
lease
|
|
339
|
-
});
|
|
340
|
-
return handle;
|
|
341
893
|
};
|
|
342
894
|
}
|
|
343
895
|
function createSandboxBackendHandle(options) {
|
|
344
|
-
const createActiveUseHandle = async (correlation) =>
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
896
|
+
const createActiveUseHandle = async (correlation) => {
|
|
897
|
+
try {
|
|
898
|
+
return await createToolVmActiveUseHandle({
|
|
899
|
+
correlation,
|
|
900
|
+
endActiveUse: async (useId, request) => {
|
|
901
|
+
await options.leaseClient.endActiveUse(options.lease.leaseId, useId, request);
|
|
902
|
+
},
|
|
903
|
+
heartbeatActiveUse: async (useId, request) => await options.leaseClient.heartbeatActiveUse(options.lease.leaseId, useId, request),
|
|
904
|
+
isEndErrorTolerable: isCleanupNotFound,
|
|
905
|
+
isHeartbeatErrorRefreshable: isRefreshableLeaseError,
|
|
906
|
+
logEndFailure: (error) => {
|
|
907
|
+
writeSandboxBackendLog(`active-use cleanup ignored for zone '${options.zoneId}' lease '${options.lease.leaseId}': ${formatUnknownError(error)}`);
|
|
908
|
+
},
|
|
909
|
+
logHeartbeatFailure: (error) => {
|
|
910
|
+
writeSandboxBackendLog(`active-use heartbeat failed for zone '${options.zoneId}' lease '${options.lease.leaseId}': ${formatUnknownError(error)}`);
|
|
911
|
+
},
|
|
912
|
+
onRefreshableHeartbeatFailure: async (error) => {
|
|
913
|
+
await options.markCachedLeaseStale("active-use-refreshable-failure", error);
|
|
914
|
+
},
|
|
915
|
+
startActiveUse: async (request) => await options.leaseClient.startActiveUse(options.lease.leaseId, request)
|
|
916
|
+
});
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (isRefreshableLeaseError(error)) await options.markCachedLeaseStale("active-use-refreshable-failure", error);
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
359
922
|
const runWithActiveUse = async (correlation, fn) => {
|
|
360
923
|
const activeUseHandle = await createActiveUseHandle(correlation);
|
|
361
924
|
try {
|
|
362
|
-
const result = await fn();
|
|
925
|
+
const result = await fn(activeUseHandle);
|
|
363
926
|
await activeUseHandle.dispose("completed");
|
|
364
927
|
return result;
|
|
365
928
|
} catch (error) {
|
|
366
|
-
await activeUseHandle.dispose("failed").catch((cleanupError) => {
|
|
929
|
+
await activeUseHandle.dispose(error instanceof ToolVmSshOperationStaleError && error.reason === "ssh-command-timed-out" ? "timed-out" : "failed").catch((cleanupError) => {
|
|
367
930
|
writeSandboxBackendLog(`failed to end active use after operation failure for zone '${options.zoneId}' lease '${options.lease.leaseId}': ${formatUnknownError(cleanupError)}`);
|
|
368
931
|
});
|
|
932
|
+
if (error instanceof ToolVmSshOperationStaleError) await options.markCachedLeaseStale(error.reason, error);
|
|
369
933
|
throw error;
|
|
370
934
|
}
|
|
371
935
|
};
|
|
372
936
|
const boundRunRemoteShellScript = async (shellParams) => await runWithActiveUse({
|
|
373
937
|
sessionKey: options.sessionKey,
|
|
374
938
|
toolName: "fs-bridge"
|
|
375
|
-
}, async () => await
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
939
|
+
}, async (activeUseHandle) => await runToolVmSshOperationWithGuard({
|
|
940
|
+
healthEvent: {
|
|
941
|
+
agentId: options.lease.agentId,
|
|
942
|
+
leaseId: options.lease.leaseId,
|
|
943
|
+
operation: "file-bridge",
|
|
944
|
+
publish: options.publishHealthEvent,
|
|
945
|
+
zoneId: options.zoneId
|
|
946
|
+
},
|
|
947
|
+
operation: async (signal) => {
|
|
948
|
+
const operationSignal = mergedAbortSignals([
|
|
949
|
+
shellParams.signal,
|
|
950
|
+
activeUseHandle.signal,
|
|
951
|
+
signal
|
|
952
|
+
]);
|
|
953
|
+
return await options.runRemoteShellScript({
|
|
954
|
+
...shellParams.allowFailure !== void 0 ? { allowFailure: shellParams.allowFailure } : {},
|
|
955
|
+
script: buildShellScriptWithArgs(shellParams.script, shellParams.args),
|
|
956
|
+
...operationSignal === void 0 ? {} : { signal: operationSignal },
|
|
957
|
+
ssh: options.lease.ssh,
|
|
958
|
+
...shellParams.stdin !== void 0 ? { stdin: shellParams.stdin } : {}
|
|
959
|
+
});
|
|
960
|
+
},
|
|
961
|
+
operationName: "fs-bridge",
|
|
962
|
+
report: (report) => {
|
|
963
|
+
activeUseHandle.report(report);
|
|
964
|
+
},
|
|
965
|
+
timeoutMs: 3e4
|
|
381
966
|
}));
|
|
382
967
|
const disposeInnerFinalizeToken = async (token) => {
|
|
383
968
|
if (isDisposableFinalizeToken(token)) await token.dispose();
|
|
@@ -400,7 +985,7 @@ function createSandboxBackendHandle(options) {
|
|
|
400
985
|
};
|
|
401
986
|
const createFsBridge = options.createFsBridgeBuilder?.({
|
|
402
987
|
remoteAgentWorkspaceDir: options.lease.workdir,
|
|
403
|
-
remoteWorkspaceDir: options.
|
|
988
|
+
remoteWorkspaceDir: options.effectiveGuestCwd,
|
|
404
989
|
runRemoteShellScript: boundRunRemoteShellScript
|
|
405
990
|
});
|
|
406
991
|
return {
|
|
@@ -411,7 +996,7 @@ function createSandboxBackendHandle(options) {
|
|
|
411
996
|
id: "gondolin",
|
|
412
997
|
runtimeId: options.lease.leaseId,
|
|
413
998
|
runtimeLabel: options.lease.leaseId,
|
|
414
|
-
workdir: options.
|
|
999
|
+
workdir: options.effectiveGuestCwd,
|
|
415
1000
|
buildExecSpec: async (execParams) => {
|
|
416
1001
|
const activeUseHandle = await createActiveUseHandle({
|
|
417
1002
|
sessionKey: options.sessionKey,
|
|
@@ -423,7 +1008,7 @@ function createSandboxBackendHandle(options) {
|
|
|
423
1008
|
env: execParams.env,
|
|
424
1009
|
ssh: options.lease.ssh,
|
|
425
1010
|
usePty: execParams.usePty,
|
|
426
|
-
workdir: execParams.workdir ?? options.
|
|
1011
|
+
workdir: execParams.workdir ?? options.effectiveGuestCwd
|
|
427
1012
|
});
|
|
428
1013
|
return {
|
|
429
1014
|
...execSpec,
|
|
@@ -441,7 +1026,23 @@ function createSandboxBackendHandle(options) {
|
|
|
441
1026
|
},
|
|
442
1027
|
finalizeExec: async (finalizeParams) => {
|
|
443
1028
|
if (isActiveUseFinalizeToken(finalizeParams.token)) {
|
|
1029
|
+
if (finalizeParams.timedOut) finalizeParams.token.activeUseHandle.report({
|
|
1030
|
+
observedAtMs: Date.now(),
|
|
1031
|
+
phase: "failed",
|
|
1032
|
+
ssh: { failure: {
|
|
1033
|
+
kind: "ssh-command-timed-out",
|
|
1034
|
+
message: "exec command timed out."
|
|
1035
|
+
} }
|
|
1036
|
+
});
|
|
444
1037
|
await endActiveUseFinalizeToken(finalizeParams.token, activeUseOutcomeForFinalizeParams(finalizeParams));
|
|
1038
|
+
publishFinalizeToolVmSshHealthEvent({
|
|
1039
|
+
agentId: options.lease.agentId,
|
|
1040
|
+
leaseId: options.lease.leaseId,
|
|
1041
|
+
publishHealthEvent: options.publishHealthEvent,
|
|
1042
|
+
timedOut: finalizeParams.timedOut,
|
|
1043
|
+
zoneId: options.zoneId
|
|
1044
|
+
});
|
|
1045
|
+
if (finalizeParams.timedOut) await options.markCachedLeaseStale("ssh-command-timed-out", void 0);
|
|
445
1046
|
return;
|
|
446
1047
|
}
|
|
447
1048
|
await disposeInnerFinalizeToken(finalizeParams.token);
|
|
@@ -449,9 +1050,24 @@ function createSandboxBackendHandle(options) {
|
|
|
449
1050
|
runShellCommand: async (commandParams) => await runWithActiveUse({
|
|
450
1051
|
sessionKey: options.sessionKey,
|
|
451
1052
|
toolName: "runShellCommand"
|
|
452
|
-
}, async () => await
|
|
453
|
-
|
|
454
|
-
|
|
1053
|
+
}, async (activeUseHandle) => await runToolVmSshOperationWithGuard({
|
|
1054
|
+
healthEvent: {
|
|
1055
|
+
agentId: options.lease.agentId,
|
|
1056
|
+
leaseId: options.lease.leaseId,
|
|
1057
|
+
operation: "command",
|
|
1058
|
+
publish: options.publishHealthEvent,
|
|
1059
|
+
zoneId: options.zoneId
|
|
1060
|
+
},
|
|
1061
|
+
operation: async (signal) => await options.runRemoteShellScript({
|
|
1062
|
+
script: commandParams.script,
|
|
1063
|
+
signal: mergedAbortSignal(activeUseHandle.signal, signal),
|
|
1064
|
+
ssh: options.lease.ssh
|
|
1065
|
+
}),
|
|
1066
|
+
operationName: "runShellCommand",
|
|
1067
|
+
report: (report) => {
|
|
1068
|
+
activeUseHandle.report(report);
|
|
1069
|
+
},
|
|
1070
|
+
timeoutMs: 3e4
|
|
455
1071
|
}))
|
|
456
1072
|
};
|
|
457
1073
|
}
|
|
@@ -481,10 +1097,20 @@ function createGondolinSandboxBackendManager(options, dependencies) {
|
|
|
481
1097
|
}
|
|
482
1098
|
//#endregion
|
|
483
1099
|
//#region src/gondolin-plugin-config.ts
|
|
1100
|
+
function isObjectRecord$1(value) {
|
|
1101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1102
|
+
}
|
|
484
1103
|
function resolveGondolinPluginConfig(config) {
|
|
485
1104
|
if (typeof config.controllerUrl !== "string" || typeof config.zoneId !== "string") throw new Error("Gondolin plugin config requires controllerUrl and zoneId.");
|
|
1105
|
+
const rawGatewayControlLinkMonitor = config.gatewayControlLinkMonitor;
|
|
1106
|
+
const gatewayControlLinkMonitor = isObjectRecord$1(rawGatewayControlLinkMonitor) ? {
|
|
1107
|
+
baseIntervalMs: typeof rawGatewayControlLinkMonitor.baseIntervalMs === "number" ? rawGatewayControlLinkMonitor.baseIntervalMs : 1e4,
|
|
1108
|
+
enabled: typeof rawGatewayControlLinkMonitor.enabled === "boolean" ? rawGatewayControlLinkMonitor.enabled : true,
|
|
1109
|
+
maxIntervalMs: typeof rawGatewayControlLinkMonitor.maxIntervalMs === "number" ? rawGatewayControlLinkMonitor.maxIntervalMs : 12e4
|
|
1110
|
+
} : void 0;
|
|
486
1111
|
return {
|
|
487
1112
|
controllerUrl: config.controllerUrl,
|
|
1113
|
+
...gatewayControlLinkMonitor ? { gatewayControlLinkMonitor } : {},
|
|
488
1114
|
...typeof config.profileId === "string" ? { profileId: config.profileId } : {},
|
|
489
1115
|
...typeof config.zoneGitToken === "string" ? { zoneGitToken: config.zoneGitToken } : {},
|
|
490
1116
|
...typeof config.zoneGitTokenEnv === "string" ? { zoneGitTokenEnv: config.zoneGitTokenEnv } : {},
|
|
@@ -492,6 +1118,119 @@ function resolveGondolinPluginConfig(config) {
|
|
|
492
1118
|
};
|
|
493
1119
|
}
|
|
494
1120
|
//#endregion
|
|
1121
|
+
//#region src/gateway-control-link-monitor.ts
|
|
1122
|
+
function defaultWriteLog(message) {
|
|
1123
|
+
process.stderr.write(`[gateway-control-link-monitor] ${message}\n`);
|
|
1124
|
+
}
|
|
1125
|
+
function joinUrl(baseUrl, path) {
|
|
1126
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
1127
|
+
}
|
|
1128
|
+
function nextIntervalMs(options) {
|
|
1129
|
+
const multiplier = 2 ** Math.min(options.consecutiveFailureCount, 8);
|
|
1130
|
+
return Math.min(options.maxIntervalMs, options.baseIntervalMs * multiplier);
|
|
1131
|
+
}
|
|
1132
|
+
function createGatewayControlLinkMonitor(options) {
|
|
1133
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1134
|
+
const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
|
|
1135
|
+
const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
|
|
1136
|
+
const writeLog = options.writeLog ?? defaultWriteLog;
|
|
1137
|
+
let consecutiveFailureCount = 0;
|
|
1138
|
+
let stopped = true;
|
|
1139
|
+
let timer;
|
|
1140
|
+
const publish = async (event) => {
|
|
1141
|
+
const response = await fetchControllerWithPolicy({
|
|
1142
|
+
fetchImpl,
|
|
1143
|
+
input: joinUrl(options.controllerUrl, `/zones/${encodeURIComponent(options.zoneId)}/health-events`),
|
|
1144
|
+
init: {
|
|
1145
|
+
body: JSON.stringify(event),
|
|
1146
|
+
headers: { "content-type": "application/json" },
|
|
1147
|
+
method: "POST"
|
|
1148
|
+
},
|
|
1149
|
+
operation: "health-event-publish"
|
|
1150
|
+
});
|
|
1151
|
+
if (!response.ok) {
|
|
1152
|
+
await response.text().catch(() => void 0);
|
|
1153
|
+
throw new Error(`health event publish returned HTTP ${String(response.status)}`);
|
|
1154
|
+
}
|
|
1155
|
+
await response.text().catch(() => void 0);
|
|
1156
|
+
};
|
|
1157
|
+
const scheduleNext = () => {
|
|
1158
|
+
if (stopped) return;
|
|
1159
|
+
if (timer) return;
|
|
1160
|
+
timer = setTimeoutImpl(() => {
|
|
1161
|
+
timer = void 0;
|
|
1162
|
+
tick().finally(scheduleNext);
|
|
1163
|
+
}, nextIntervalMs({
|
|
1164
|
+
baseIntervalMs: options.baseIntervalMs,
|
|
1165
|
+
consecutiveFailureCount,
|
|
1166
|
+
maxIntervalMs: options.maxIntervalMs
|
|
1167
|
+
}));
|
|
1168
|
+
timer.unref?.();
|
|
1169
|
+
};
|
|
1170
|
+
const tick = async () => {
|
|
1171
|
+
const startedAtMs = options.now();
|
|
1172
|
+
let event;
|
|
1173
|
+
try {
|
|
1174
|
+
const response = await fetchControllerWithPolicy({
|
|
1175
|
+
fetchImpl,
|
|
1176
|
+
input: joinUrl(options.controllerUrl, gatewayControlLinkHealthPins.path),
|
|
1177
|
+
init: { method: "GET" },
|
|
1178
|
+
operation: "controller-health"
|
|
1179
|
+
});
|
|
1180
|
+
const ok = response.ok;
|
|
1181
|
+
await response.text().catch(() => void 0);
|
|
1182
|
+
consecutiveFailureCount = ok ? 0 : consecutiveFailureCount + 1;
|
|
1183
|
+
event = {
|
|
1184
|
+
controllerHost: gatewayControlLinkHealthPins.controllerHost,
|
|
1185
|
+
controllerPort: gatewayControlLinkHealthPins.controllerPort,
|
|
1186
|
+
elapsedMs: options.now() - startedAtMs,
|
|
1187
|
+
kind: "gateway-control-link",
|
|
1188
|
+
observedAtMs: options.now(),
|
|
1189
|
+
operation: gatewayControlLinkHealthPins.operation,
|
|
1190
|
+
path: gatewayControlLinkHealthPins.path,
|
|
1191
|
+
result: ok ? "ok" : "failed",
|
|
1192
|
+
zoneId: options.zoneId
|
|
1193
|
+
};
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
consecutiveFailureCount += 1;
|
|
1196
|
+
event = {
|
|
1197
|
+
controllerHost: gatewayControlLinkHealthPins.controllerHost,
|
|
1198
|
+
controllerPort: gatewayControlLinkHealthPins.controllerPort,
|
|
1199
|
+
elapsedMs: options.now() - startedAtMs,
|
|
1200
|
+
kind: "gateway-control-link",
|
|
1201
|
+
observedAtMs: options.now(),
|
|
1202
|
+
operation: gatewayControlLinkHealthPins.operation,
|
|
1203
|
+
path: gatewayControlLinkHealthPins.path,
|
|
1204
|
+
result: error instanceof ControllerRequestPolicyTransportError && error.code === "controller-request-timeout" ? "timeout" : "failed",
|
|
1205
|
+
zoneId: options.zoneId
|
|
1206
|
+
};
|
|
1207
|
+
writeLog(`gateway-control-link fetch failed operation=controller-health elapsedMs=${String(event.elapsedMs)} errorCode=${error instanceof ControllerRequestPolicyTransportError ? error.code : "controller-request-failed"}`);
|
|
1208
|
+
}
|
|
1209
|
+
try {
|
|
1210
|
+
await publish(event);
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
writeLog(`gateway-control-link publish failed operation=health-event-publish elapsedMs=${String(event.elapsedMs)} errorCode=${error instanceof ControllerRequestPolicyTransportError ? error.code : "health-event-publish-failed"} message=${error instanceof Error ? error.message : String(error)}`);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
return {
|
|
1216
|
+
consecutiveFailureCount: () => consecutiveFailureCount,
|
|
1217
|
+
noteFailureForTest: () => {
|
|
1218
|
+
consecutiveFailureCount += 1;
|
|
1219
|
+
},
|
|
1220
|
+
start: () => {
|
|
1221
|
+
stopped = false;
|
|
1222
|
+
scheduleNext();
|
|
1223
|
+
},
|
|
1224
|
+
stop: () => {
|
|
1225
|
+
stopped = true;
|
|
1226
|
+
if (!timer) return;
|
|
1227
|
+
clearTimeoutImpl(timer);
|
|
1228
|
+
timer = void 0;
|
|
1229
|
+
},
|
|
1230
|
+
tick
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
//#endregion
|
|
495
1234
|
//#region src/openclaw-backend-dependencies.ts
|
|
496
1235
|
const OPENCLAW_SSH_SESSION_SCRATCH_ROOT = "/work";
|
|
497
1236
|
function createBackendDeps(ssh) {
|
|
@@ -670,13 +1409,18 @@ function registerZoneGitTool(options) {
|
|
|
670
1409
|
},
|
|
671
1410
|
execute: async (_toolCallId, input) => {
|
|
672
1411
|
const expectedHead = readExpectedHead(input);
|
|
673
|
-
const response = await (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1412
|
+
const response = await fetchControllerWithPolicy({
|
|
1413
|
+
fetchImpl: options.fetchImpl ?? fetch,
|
|
1414
|
+
input: buildControllerUrl(options.controllerUrl, options.zoneId),
|
|
1415
|
+
init: {
|
|
1416
|
+
body: JSON.stringify({ expectedHead }),
|
|
1417
|
+
headers: {
|
|
1418
|
+
"content-type": "application/json",
|
|
1419
|
+
...options.zoneGitToken ? { [zoneGitCapabilityHeader]: options.zoneGitToken } : {}
|
|
1420
|
+
},
|
|
1421
|
+
method: "POST"
|
|
678
1422
|
},
|
|
679
|
-
|
|
1423
|
+
operation: "zone-git-push"
|
|
680
1424
|
});
|
|
681
1425
|
const responseText = await readResponseText(response);
|
|
682
1426
|
if (!response.ok) throw new Error(`zone_git_push failed: ${response.status} ${responseText.slice(0, 500)}`);
|
|
@@ -693,22 +1437,8 @@ function registerZoneGitTool(options) {
|
|
|
693
1437
|
}
|
|
694
1438
|
//#endregion
|
|
695
1439
|
//#region src/openclaw-plugin-registration.ts
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
function sleep(ms) {
|
|
699
|
-
return new Promise((resolve) => {
|
|
700
|
-
setTimeout(resolve, ms);
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
async function publishRuntimeStatusWithRetry(options) {
|
|
704
|
-
const leaseClient = createLeaseClient({ controllerUrl: options.controllerUrl });
|
|
705
|
-
for (let attemptIndex = 0; attemptIndex < runtimeStatusPublishMaxAttempts; attemptIndex += 1) try {
|
|
706
|
-
await leaseClient.publishOpenClawRuntimeStatus?.(options.report);
|
|
707
|
-
return;
|
|
708
|
-
} catch (error) {
|
|
709
|
-
if (attemptIndex === runtimeStatusPublishMaxAttempts - 1) throw error;
|
|
710
|
-
await sleep(runtimeStatusPublishRetryDelayMs);
|
|
711
|
-
}
|
|
1440
|
+
async function publishRuntimeStatus(options) {
|
|
1441
|
+
await createLeaseClient({ controllerUrl: options.controllerUrl }).publishOpenClawRuntimeStatus?.(options.report);
|
|
712
1442
|
}
|
|
713
1443
|
const plugin = {
|
|
714
1444
|
id: "gondolin",
|
|
@@ -729,6 +1459,13 @@ const plugin = {
|
|
|
729
1459
|
zoneId: pluginConfig.zoneId
|
|
730
1460
|
});
|
|
731
1461
|
if (api.registrationMode !== "full") return;
|
|
1462
|
+
if (pluginConfig.gatewayControlLinkMonitor?.enabled) createGatewayControlLinkMonitor({
|
|
1463
|
+
baseIntervalMs: pluginConfig.gatewayControlLinkMonitor.baseIntervalMs,
|
|
1464
|
+
controllerUrl: pluginConfig.controllerUrl,
|
|
1465
|
+
maxIntervalMs: pluginConfig.gatewayControlLinkMonitor.maxIntervalMs,
|
|
1466
|
+
now: () => Date.now(),
|
|
1467
|
+
zoneId: pluginConfig.zoneId
|
|
1468
|
+
}).start();
|
|
732
1469
|
const buildRuntimeStatus = () => {
|
|
733
1470
|
const runtimeConfig = api.runtime?.config?.current?.() ?? api.config;
|
|
734
1471
|
return runtimeConfig ? buildOpenClawRuntimeStatusReport({
|
|
@@ -737,7 +1474,7 @@ const plugin = {
|
|
|
737
1474
|
}) : void 0;
|
|
738
1475
|
};
|
|
739
1476
|
const initialRuntimeStatus = buildRuntimeStatus();
|
|
740
|
-
if (initialRuntimeStatus)
|
|
1477
|
+
if (initialRuntimeStatus) publishRuntimeStatus({
|
|
741
1478
|
controllerUrl: pluginConfig.controllerUrl,
|
|
742
1479
|
report: initialRuntimeStatus
|
|
743
1480
|
}).catch((error) => {
|
|
@@ -759,6 +1496,7 @@ const plugin = {
|
|
|
759
1496
|
sdkRaw.registerSandboxBackend("gondolin", {
|
|
760
1497
|
factory: createGondolinSandboxBackendFactory({
|
|
761
1498
|
...pluginConfig,
|
|
1499
|
+
openClawRuntimeConfigProvider: () => api.runtime?.config?.current?.() ?? api.config,
|
|
762
1500
|
openClawRuntimeStatusProvider: buildRuntimeStatus
|
|
763
1501
|
}, backendDependencies),
|
|
764
1502
|
manager: createGondolinSandboxBackendManager(pluginConfig, backendDependencies)
|
|
@@ -773,6 +1511,6 @@ const plugin = {
|
|
|
773
1511
|
//#region src/index.ts
|
|
774
1512
|
const OPENCLAW_GONDOLIN_PLUGIN_PACKAGE_NAME = "@agent-vm/openclaw-agent-vm-plugin";
|
|
775
1513
|
//#endregion
|
|
776
|
-
export { ControllerLeaseRequestError, OPENCLAW_DEFAULT_AGENT_ID, OPENCLAW_GONDOLIN_LEASE_SCOPE_GUIDANCE, OPENCLAW_GONDOLIN_PLUGIN_PACKAGE_NAME, OPENCLAW_GONDOLIN_SANDBOX_REQUIREMENTS, OPENCLAW_SSH_SESSION_SCRATCH_ROOT, buildOpenClawRuntimeStatusReport, createBackendDeps, createGondolinSandboxBackendFactory, createGondolinSandboxBackendManager, createLeaseClient, plugin as default, effectiveOpenClawGondolinSandboxValue, findOpenClawGondolinSandboxMismatch, formatOpenClawGondolinRequirementFieldPath, formatOpenClawGondolinRequirementFindingId, formatOpenClawGondolinRequirementHint, isOpenClawAgentId, isOpenClawAgentSessionKey, normalizeOpenClawAgentId, resolveGondolinPluginConfig, resolveOpenClawAgentIdFromSessionKey, snapshotOpenClawGondolinSandboxConfig };
|
|
1514
|
+
export { ControllerLeaseRequestError, ControllerRequestPolicyTransportError, OPENCLAW_DEFAULT_AGENT_ID, OPENCLAW_GONDOLIN_LEASE_SCOPE_GUIDANCE, OPENCLAW_GONDOLIN_PLUGIN_PACKAGE_NAME, OPENCLAW_GONDOLIN_SANDBOX_REQUIREMENTS, OPENCLAW_SSH_SESSION_SCRATCH_ROOT, OpenClawAgentIdError, buildOpenClawRuntimeStatusReport, createBackendDeps, createGatewayControlLinkMonitor, createGondolinSandboxBackendFactory, createGondolinSandboxBackendManager, createLeaseClient, plugin as default, drainControllerResponseBody, effectiveOpenClawGondolinSandboxValue, fetchControllerWithPolicy, findOpenClawGondolinSandboxMismatch, formatOpenClawGondolinRequirementFieldPath, formatOpenClawGondolinRequirementFindingId, formatOpenClawGondolinRequirementHint, isOpenClawAgentId, isOpenClawAgentSessionKey, normalizeOpenClawAgentId, resolveGondolinPluginConfig, resolveOpenClawAgentIdFromSessionKey, snapshotOpenClawGondolinSandboxConfig };
|
|
777
1515
|
|
|
778
1516
|
//# sourceMappingURL=index.js.map
|