@agent-vm/openclaw-agent-vm-plugin 0.0.82 → 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 +35 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +506 -75
- 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 {
|
|
@@ -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,23 +121,35 @@ function createLeaseClient(options) {
|
|
|
105
121
|
status: response.status
|
|
106
122
|
});
|
|
107
123
|
}
|
|
124
|
+
await drainControllerResponseBody(response);
|
|
108
125
|
},
|
|
109
126
|
heartbeatActiveUse: async (leaseId, useId, request) => {
|
|
110
|
-
return await readJsonResponse(await
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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"
|
|
114
135
|
}), "Controller active-use heartbeat API", isHeartbeatActiveUseResponse);
|
|
115
136
|
},
|
|
116
137
|
renewLease,
|
|
117
138
|
peekLease: async (leaseId) => {
|
|
118
|
-
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);
|
|
119
143
|
},
|
|
120
144
|
publishOpenClawRuntimeStatus: async (report) => {
|
|
121
|
-
const response = await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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"
|
|
125
153
|
});
|
|
126
154
|
if (!response.ok) {
|
|
127
155
|
const errorBody = await readErrorBody(response, "Controller OpenClaw runtime status API");
|
|
@@ -132,11 +160,16 @@ function createLeaseClient(options) {
|
|
|
132
160
|
status: response.status
|
|
133
161
|
});
|
|
134
162
|
}
|
|
163
|
+
await drainControllerResponseBody(response);
|
|
135
164
|
},
|
|
136
165
|
releaseLease: async (leaseId, releaseOptions = {}) => {
|
|
137
166
|
const releaseUrl = new URL(`${baseUrl}/lease/${encodeURIComponent(leaseId)}`);
|
|
138
167
|
if (releaseOptions.force === true) releaseUrl.searchParams.set("force", "true");
|
|
139
|
-
const response = await
|
|
168
|
+
const response = await fetchController({
|
|
169
|
+
input: releaseUrl.toString(),
|
|
170
|
+
init: { method: "DELETE" },
|
|
171
|
+
operation: "lease-release"
|
|
172
|
+
});
|
|
140
173
|
if (!response.ok) {
|
|
141
174
|
const errorBody = await readErrorBody(response, "Controller lease release API");
|
|
142
175
|
throw new ControllerLeaseRequestError({
|
|
@@ -146,27 +179,36 @@ function createLeaseClient(options) {
|
|
|
146
179
|
status: response.status
|
|
147
180
|
});
|
|
148
181
|
}
|
|
182
|
+
await drainControllerResponseBody(response);
|
|
149
183
|
},
|
|
150
184
|
requestLease: async (request) => {
|
|
151
|
-
return await readJsonResponse(await
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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"
|
|
163
201
|
}), "Controller lease API", isToolVmSshLease);
|
|
164
202
|
},
|
|
165
203
|
startActiveUse: async (leaseId, request) => {
|
|
166
|
-
return await readJsonResponse(await
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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"
|
|
170
212
|
}), "Controller active-use start API", isStartActiveUseResponse);
|
|
171
213
|
}
|
|
172
214
|
};
|
|
@@ -194,6 +236,12 @@ const OPENCLAW_GONDOLIN_SANDBOX_REQUIREMENTS = [
|
|
|
194
236
|
}
|
|
195
237
|
];
|
|
196
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
|
+
};
|
|
197
245
|
function isOpenClawAgentId(value) {
|
|
198
246
|
return agentIdPattern.test(value.trim());
|
|
199
247
|
}
|
|
@@ -211,11 +259,13 @@ function formatOpenClawGondolinRequirementHint(options) {
|
|
|
211
259
|
}
|
|
212
260
|
function normalizeOpenClawAgentId(value) {
|
|
213
261
|
const trimmed = (value ?? "").trim().toLowerCase();
|
|
214
|
-
|
|
262
|
+
if (trimmed === "") return OPENCLAW_DEFAULT_AGENT_ID;
|
|
263
|
+
if (!isOpenClawAgentId(trimmed)) throw new OpenClawAgentIdError(`Invalid OpenClaw agentId '${value}'.`);
|
|
264
|
+
return trimmed;
|
|
215
265
|
}
|
|
216
266
|
function resolveOpenClawAgentIdFromSessionKey(sessionKey) {
|
|
217
267
|
const parts = sessionKey.trim().split(":");
|
|
218
|
-
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.`);
|
|
219
269
|
return normalizeOpenClawAgentId(parts[1]);
|
|
220
270
|
}
|
|
221
271
|
function isOpenClawAgentSessionKey(sessionKey) {
|
|
@@ -234,6 +284,97 @@ function findOpenClawGondolinSandboxMismatch(sandbox) {
|
|
|
234
284
|
return OPENCLAW_GONDOLIN_SANDBOX_REQUIREMENTS.find((requirement) => sandbox[requirement.key] !== requirement.expectedValue);
|
|
235
285
|
}
|
|
236
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
|
|
237
378
|
//#region src/sandbox-backend/openclaw-tool-vm-path-mapping.ts
|
|
238
379
|
var OpenClawToolVmPathIntentError = class extends Error {
|
|
239
380
|
details;
|
|
@@ -269,8 +410,6 @@ function createOpenClawToolVmPathMapping(options) {
|
|
|
269
410
|
roots: [
|
|
270
411
|
{
|
|
271
412
|
id: "agent-workspace",
|
|
272
|
-
guestRoot: TOOL_VM_WORKSPACE_GUEST_ROOT,
|
|
273
|
-
hostRoot: options.agentWorkspaceDir,
|
|
274
413
|
backing: {
|
|
275
414
|
kind: "host-realfs",
|
|
276
415
|
durability: "durable",
|
|
@@ -280,12 +419,15 @@ function createOpenClawToolVmPathMapping(options) {
|
|
|
280
419
|
executionCwd: true,
|
|
281
420
|
leaseMount: true
|
|
282
421
|
},
|
|
422
|
+
locations: {
|
|
423
|
+
"openclaw-gateway": options.agentWorkspaceDir,
|
|
424
|
+
"tool-vm-guest": TOOL_VM_WORKSPACE_GUEST_ROOT
|
|
425
|
+
},
|
|
283
426
|
rootPathAllowed: true,
|
|
284
427
|
guidanceLabel: "agent workspace"
|
|
285
428
|
},
|
|
286
429
|
{
|
|
287
430
|
id: "tool-vm-scratch",
|
|
288
|
-
guestRoot: TOOL_VM_SCRATCH_GUEST_ROOT,
|
|
289
431
|
backing: {
|
|
290
432
|
kind: "guest-rootfs-cow",
|
|
291
433
|
durability: "vm-lifetime"
|
|
@@ -294,12 +436,12 @@ function createOpenClawToolVmPathMapping(options) {
|
|
|
294
436
|
executionCwd: true,
|
|
295
437
|
leaseMount: false
|
|
296
438
|
},
|
|
439
|
+
locations: { "tool-vm-guest": TOOL_VM_SCRATCH_GUEST_ROOT },
|
|
297
440
|
rootPathAllowed: true,
|
|
298
441
|
guidanceLabel: "Tool VM scratch"
|
|
299
442
|
},
|
|
300
443
|
{
|
|
301
444
|
id: "openclaw-sandboxes",
|
|
302
|
-
hostRoot: OPENCLAW_STATE_SANDBOXES_VM_ROOT,
|
|
303
445
|
backing: {
|
|
304
446
|
kind: "host-realfs",
|
|
305
447
|
durability: "durable",
|
|
@@ -309,6 +451,7 @@ function createOpenClawToolVmPathMapping(options) {
|
|
|
309
451
|
executionCwd: true,
|
|
310
452
|
leaseMount: true
|
|
311
453
|
},
|
|
454
|
+
locations: { "openclaw-gateway": OPENCLAW_STATE_SANDBOXES_VM_ROOT },
|
|
312
455
|
rootPathAllowed: false,
|
|
313
456
|
guidanceLabel: "OpenClaw sandbox work directory"
|
|
314
457
|
}
|
|
@@ -317,7 +460,7 @@ function createOpenClawToolVmPathMapping(options) {
|
|
|
317
460
|
}
|
|
318
461
|
function resolveOpenClawSandboxPathIntent(translation) {
|
|
319
462
|
const [sandboxChild, ...guestCwdSegments] = translation.relativePath.split("/");
|
|
320
|
-
const leaseWorkMountDir = sandboxChild === void 0 || sandboxChild === "" ? translation.
|
|
463
|
+
const leaseWorkMountDir = sandboxChild === void 0 || sandboxChild === "" ? translation.outputPath : `${OPENCLAW_STATE_SANDBOXES_VM_ROOT}/${sandboxChild}`;
|
|
321
464
|
return {
|
|
322
465
|
effectiveGuestCwd: guestCwdSegments.length === 0 ? TOOL_VM_WORKSPACE_GUEST_ROOT : `${TOOL_VM_WORKSPACE_GUEST_ROOT}/${guestCwdSegments.join("/")}`,
|
|
323
466
|
leaseWorkMountDir
|
|
@@ -327,29 +470,72 @@ function kindForTranslation(translation) {
|
|
|
327
470
|
const isRoot = translation.relativePath === "";
|
|
328
471
|
if (translation.rootId === "tool-vm-scratch") return isRoot ? "scratch-root" : "scratch-subpath";
|
|
329
472
|
if (translation.rootId === "openclaw-sandboxes") return "openclaw-sandbox-path";
|
|
330
|
-
if (translation.inputNamespace === "
|
|
473
|
+
if (translation.inputNamespace === "openclaw-gateway") return isRoot ? "host-workspace-root" : "host-workspace-subpath";
|
|
331
474
|
return isRoot ? "workspace-root" : "workspace-subpath";
|
|
332
475
|
}
|
|
476
|
+
function leaseRootForTranslation(translation) {
|
|
477
|
+
return translation.relativePath === "" ? translation.outputPath : translation.outputPath.slice(0, -(translation.relativePath.length + 1));
|
|
478
|
+
}
|
|
333
479
|
function resolveOpenClawToolVmPathIntent(options) {
|
|
334
480
|
const agentWorkspaceDirError = validateAgentWorkspaceDir(options.agentWorkspaceDir);
|
|
335
481
|
if (agentWorkspaceDirError !== void 0) return {
|
|
336
482
|
error: agentWorkspaceDirError,
|
|
337
483
|
ok: false
|
|
338
484
|
};
|
|
339
|
-
const
|
|
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({
|
|
340
493
|
inputPath: options.inputPath,
|
|
341
|
-
mapping
|
|
342
|
-
purpose: "executionCwd"
|
|
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"
|
|
343
531
|
});
|
|
344
|
-
if (!translation.ok) return translation;
|
|
345
|
-
const sandboxPathIntent = translation.value.rootId === "openclaw-sandboxes" ? resolveOpenClawSandboxPathIntent(translation.value) : void 0;
|
|
346
532
|
return {
|
|
347
533
|
ok: true,
|
|
348
534
|
value: {
|
|
349
|
-
effectiveGuestCwd:
|
|
350
|
-
...
|
|
535
|
+
effectiveGuestCwd: translation.value.outputPath,
|
|
536
|
+
...hostEquivalentTranslation.ok ? { hostEquivalentPath: hostEquivalentTranslation.value.outputPath } : {},
|
|
351
537
|
kind: kindForTranslation(translation.value),
|
|
352
|
-
leaseWorkMountDir:
|
|
538
|
+
leaseWorkMountDir: hostEquivalentTranslation.ok && hostEquivalentTranslation.value.rootId !== "tool-vm-scratch" ? leaseRootForTranslation(hostEquivalentTranslation.value) : options.agentWorkspaceDir
|
|
353
539
|
}
|
|
354
540
|
};
|
|
355
541
|
}
|
|
@@ -378,11 +564,34 @@ var ToolVmSshOperationStaleError = class extends Error {
|
|
|
378
564
|
function formatUnknownError$1(error) {
|
|
379
565
|
return error instanceof Error ? error.message : String(error);
|
|
380
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
|
+
}
|
|
381
589
|
async function runToolVmSshOperationWithGuard(options) {
|
|
382
590
|
const now = options.now ?? Date.now;
|
|
383
591
|
const setTimeoutImpl = options.setTimeoutImpl ?? setTimeout;
|
|
384
592
|
const clearTimeoutImpl = options.clearTimeoutImpl ?? clearTimeout;
|
|
385
593
|
const abortController = new AbortController();
|
|
594
|
+
const startedAtMs = now();
|
|
386
595
|
let timeoutHandle;
|
|
387
596
|
options.report({
|
|
388
597
|
observedAtMs: now(),
|
|
@@ -405,6 +614,13 @@ async function runToolVmSshOperationWithGuard(options) {
|
|
|
405
614
|
phase: "completed",
|
|
406
615
|
ssh: { probeSucceeded: true }
|
|
407
616
|
});
|
|
617
|
+
const observedAtMs = now();
|
|
618
|
+
publishHealthEvent({
|
|
619
|
+
elapsedMs: observedAtMs - startedAtMs,
|
|
620
|
+
guardOptions: options,
|
|
621
|
+
observedAtMs,
|
|
622
|
+
result: "ok"
|
|
623
|
+
});
|
|
408
624
|
return result;
|
|
409
625
|
} catch (error) {
|
|
410
626
|
const staleError = error instanceof ToolVmSshOperationStaleError ? error : new ToolVmSshOperationStaleError({
|
|
@@ -420,6 +636,14 @@ async function runToolVmSshOperationWithGuard(options) {
|
|
|
420
636
|
message: staleError.message
|
|
421
637
|
} }
|
|
422
638
|
});
|
|
639
|
+
const observedAtMs = now();
|
|
640
|
+
publishHealthEvent({
|
|
641
|
+
elapsedMs: observedAtMs - startedAtMs,
|
|
642
|
+
errorCode: staleError.reason,
|
|
643
|
+
guardOptions: options,
|
|
644
|
+
observedAtMs,
|
|
645
|
+
result: "failed"
|
|
646
|
+
});
|
|
423
647
|
throw staleError;
|
|
424
648
|
} finally {
|
|
425
649
|
if (timeoutHandle !== void 0) clearTimeoutImpl(timeoutHandle);
|
|
@@ -469,6 +693,24 @@ function isActiveUseFinalizeToken(value) {
|
|
|
469
693
|
function activeUseOutcomeForFinalizeParams(finalizeParams) {
|
|
470
694
|
return finalizeParams.timedOut ? "timed-out" : finalizeParams.status === "completed" ? "completed" : "failed";
|
|
471
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
|
+
}
|
|
472
714
|
function mergedAbortSignal(firstSignal, secondSignal) {
|
|
473
715
|
if (firstSignal === void 0) return secondSignal;
|
|
474
716
|
return AbortSignal.any([firstSignal, secondSignal]);
|
|
@@ -482,6 +724,18 @@ function mergedAbortSignals(signals) {
|
|
|
482
724
|
function resolveLeaseRequestAgentId(sessionKey) {
|
|
483
725
|
return resolveOpenClawAgentIdFromSessionKey(sessionKey);
|
|
484
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
|
+
}
|
|
485
739
|
function assertPluginLeaseContract(params) {
|
|
486
740
|
const mismatch = findOpenClawGondolinSandboxMismatch(params.cfg);
|
|
487
741
|
if (mismatch) throw new Error(`OpenClaw Gondolin sandbox requires ${mismatch.key}=${mismatch.expectedValue}; received ${String(params.cfg[mismatch.key])}.`);
|
|
@@ -493,8 +747,18 @@ function createGondolinSandboxBackendFactory(options, dependencies) {
|
|
|
493
747
|
const profileId = options.profileId ?? "standard";
|
|
494
748
|
const agentId = resolveLeaseRequestAgentId(params.sessionKey);
|
|
495
749
|
assertPluginLeaseContract({ cfg: params.cfg });
|
|
750
|
+
const defaultWorkspaceDir = options.openClawDefaultWorkspaceDirProvider?.() ?? defaultOpenClawWorkspaceDir();
|
|
751
|
+
const equivalentAgentWorkspaceDirs = defaultWorkspaceDir === void 0 ? [] : [defaultWorkspaceDir];
|
|
752
|
+
const workspaceSource = resolveOpenClawAgentWorkspaceSource({
|
|
753
|
+
agentId,
|
|
754
|
+
defaultWorkspaceDir,
|
|
755
|
+
openClawConfig: options.openClawRuntimeConfigProvider?.(),
|
|
756
|
+
paramsAgentWorkspaceDir: params.agentWorkspaceDir,
|
|
757
|
+
stateDir: options.openClawStateDirProvider?.() ?? defaultOpenClawStateDir()
|
|
758
|
+
});
|
|
496
759
|
const pathIntent = assertOpenClawToolVmPathIntent({
|
|
497
|
-
agentWorkspaceDir:
|
|
760
|
+
agentWorkspaceDir: workspaceSource.sourceDir,
|
|
761
|
+
equivalentAgentWorkspaceDirs,
|
|
498
762
|
inputPath: params.workspaceDir
|
|
499
763
|
});
|
|
500
764
|
const cacheKey = agentLeaseCacheKey({
|
|
@@ -502,11 +766,27 @@ function createGondolinSandboxBackendFactory(options, dependencies) {
|
|
|
502
766
|
zoneId: options.zoneId
|
|
503
767
|
});
|
|
504
768
|
const requestedCacheEntry = {
|
|
505
|
-
agentWorkspaceDir:
|
|
769
|
+
agentWorkspaceDir: workspaceSource.sourceDir,
|
|
506
770
|
leaseWorkMountDir: pathIntent.leaseWorkMountDir,
|
|
507
771
|
profileId
|
|
508
772
|
};
|
|
509
773
|
const leaseClient = dependencies.createLeaseClient?.({ controllerUrl: options.controllerUrl }) ?? createLeaseClient({ controllerUrl: options.controllerUrl });
|
|
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
|
+
};
|
|
510
790
|
const markLeaseStale = async (lease, reason, error) => {
|
|
511
791
|
agentLeaseCache.delete(cacheKey);
|
|
512
792
|
writeSandboxBackendLog(`lease marked stale for zone '${options.zoneId}' agent '${agentId}' lease '${lease.leaseId}' reason '${reason}': ${formatUnknownError(error)}`);
|
|
@@ -526,6 +806,13 @@ function createGondolinSandboxBackendFactory(options, dependencies) {
|
|
|
526
806
|
try {
|
|
527
807
|
const renewedLease = await leaseClient.renewLease(cachedEntry.lease.leaseId);
|
|
528
808
|
await runToolVmSshOperationWithGuard({
|
|
809
|
+
healthEvent: {
|
|
810
|
+
agentId,
|
|
811
|
+
leaseId: renewedLease.leaseId,
|
|
812
|
+
operation: "probe",
|
|
813
|
+
publish: publishHealthEvent,
|
|
814
|
+
zoneId: options.zoneId
|
|
815
|
+
},
|
|
529
816
|
operation: async (signal) => await dependencies.runRemoteShellScript({
|
|
530
817
|
allowFailure: false,
|
|
531
818
|
script: "true",
|
|
@@ -565,7 +852,7 @@ function createGondolinSandboxBackendFactory(options, dependencies) {
|
|
|
565
852
|
if (runtimeStatus && leaseClient.publishOpenClawRuntimeStatus) await leaseClient.publishOpenClawRuntimeStatus(runtimeStatus);
|
|
566
853
|
const leaseResponse = await leaseClient.requestLease({
|
|
567
854
|
agentId,
|
|
568
|
-
agentWorkspaceDir:
|
|
855
|
+
agentWorkspaceDir: workspaceSource.sourceDir,
|
|
569
856
|
profileId,
|
|
570
857
|
sessionKey: params.sessionKey,
|
|
571
858
|
workMountDir: pathIntent.leaseWorkMountDir,
|
|
@@ -597,6 +884,7 @@ function createGondolinSandboxBackendFactory(options, dependencies) {
|
|
|
597
884
|
markCachedLeaseStale: async (reason, error) => {
|
|
598
885
|
await markLeaseStale(lease, reason, error);
|
|
599
886
|
},
|
|
887
|
+
publishHealthEvent,
|
|
600
888
|
runRemoteShellScript: dependencies.runRemoteShellScript,
|
|
601
889
|
buildExecSpec: dependencies.buildExecSpec,
|
|
602
890
|
sessionKey: params.sessionKey,
|
|
@@ -649,6 +937,13 @@ function createSandboxBackendHandle(options) {
|
|
|
649
937
|
sessionKey: options.sessionKey,
|
|
650
938
|
toolName: "fs-bridge"
|
|
651
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
|
+
},
|
|
652
947
|
operation: async (signal) => {
|
|
653
948
|
const operationSignal = mergedAbortSignals([
|
|
654
949
|
shellParams.signal,
|
|
@@ -740,6 +1035,13 @@ function createSandboxBackendHandle(options) {
|
|
|
740
1035
|
} }
|
|
741
1036
|
});
|
|
742
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
|
+
});
|
|
743
1045
|
if (finalizeParams.timedOut) await options.markCachedLeaseStale("ssh-command-timed-out", void 0);
|
|
744
1046
|
return;
|
|
745
1047
|
}
|
|
@@ -749,6 +1051,13 @@ function createSandboxBackendHandle(options) {
|
|
|
749
1051
|
sessionKey: options.sessionKey,
|
|
750
1052
|
toolName: "runShellCommand"
|
|
751
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
|
+
},
|
|
752
1061
|
operation: async (signal) => await options.runRemoteShellScript({
|
|
753
1062
|
script: commandParams.script,
|
|
754
1063
|
signal: mergedAbortSignal(activeUseHandle.signal, signal),
|
|
@@ -788,10 +1097,20 @@ function createGondolinSandboxBackendManager(options, dependencies) {
|
|
|
788
1097
|
}
|
|
789
1098
|
//#endregion
|
|
790
1099
|
//#region src/gondolin-plugin-config.ts
|
|
1100
|
+
function isObjectRecord$1(value) {
|
|
1101
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1102
|
+
}
|
|
791
1103
|
function resolveGondolinPluginConfig(config) {
|
|
792
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;
|
|
793
1111
|
return {
|
|
794
1112
|
controllerUrl: config.controllerUrl,
|
|
1113
|
+
...gatewayControlLinkMonitor ? { gatewayControlLinkMonitor } : {},
|
|
795
1114
|
...typeof config.profileId === "string" ? { profileId: config.profileId } : {},
|
|
796
1115
|
...typeof config.zoneGitToken === "string" ? { zoneGitToken: config.zoneGitToken } : {},
|
|
797
1116
|
...typeof config.zoneGitTokenEnv === "string" ? { zoneGitTokenEnv: config.zoneGitTokenEnv } : {},
|
|
@@ -799,6 +1118,119 @@ function resolveGondolinPluginConfig(config) {
|
|
|
799
1118
|
};
|
|
800
1119
|
}
|
|
801
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
|
|
802
1234
|
//#region src/openclaw-backend-dependencies.ts
|
|
803
1235
|
const OPENCLAW_SSH_SESSION_SCRATCH_ROOT = "/work";
|
|
804
1236
|
function createBackendDeps(ssh) {
|
|
@@ -977,13 +1409,18 @@ function registerZoneGitTool(options) {
|
|
|
977
1409
|
},
|
|
978
1410
|
execute: async (_toolCallId, input) => {
|
|
979
1411
|
const expectedHead = readExpectedHead(input);
|
|
980
|
-
const response = await (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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"
|
|
985
1422
|
},
|
|
986
|
-
|
|
1423
|
+
operation: "zone-git-push"
|
|
987
1424
|
});
|
|
988
1425
|
const responseText = await readResponseText(response);
|
|
989
1426
|
if (!response.ok) throw new Error(`zone_git_push failed: ${response.status} ${responseText.slice(0, 500)}`);
|
|
@@ -1000,22 +1437,8 @@ function registerZoneGitTool(options) {
|
|
|
1000
1437
|
}
|
|
1001
1438
|
//#endregion
|
|
1002
1439
|
//#region src/openclaw-plugin-registration.ts
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
function sleep(ms) {
|
|
1006
|
-
return new Promise((resolve) => {
|
|
1007
|
-
setTimeout(resolve, ms);
|
|
1008
|
-
});
|
|
1009
|
-
}
|
|
1010
|
-
async function publishRuntimeStatusWithRetry(options) {
|
|
1011
|
-
const leaseClient = createLeaseClient({ controllerUrl: options.controllerUrl });
|
|
1012
|
-
for (let attemptIndex = 0; attemptIndex < runtimeStatusPublishMaxAttempts; attemptIndex += 1) try {
|
|
1013
|
-
await leaseClient.publishOpenClawRuntimeStatus?.(options.report);
|
|
1014
|
-
return;
|
|
1015
|
-
} catch (error) {
|
|
1016
|
-
if (attemptIndex === runtimeStatusPublishMaxAttempts - 1) throw error;
|
|
1017
|
-
await sleep(runtimeStatusPublishRetryDelayMs);
|
|
1018
|
-
}
|
|
1440
|
+
async function publishRuntimeStatus(options) {
|
|
1441
|
+
await createLeaseClient({ controllerUrl: options.controllerUrl }).publishOpenClawRuntimeStatus?.(options.report);
|
|
1019
1442
|
}
|
|
1020
1443
|
const plugin = {
|
|
1021
1444
|
id: "gondolin",
|
|
@@ -1036,6 +1459,13 @@ const plugin = {
|
|
|
1036
1459
|
zoneId: pluginConfig.zoneId
|
|
1037
1460
|
});
|
|
1038
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();
|
|
1039
1469
|
const buildRuntimeStatus = () => {
|
|
1040
1470
|
const runtimeConfig = api.runtime?.config?.current?.() ?? api.config;
|
|
1041
1471
|
return runtimeConfig ? buildOpenClawRuntimeStatusReport({
|
|
@@ -1044,7 +1474,7 @@ const plugin = {
|
|
|
1044
1474
|
}) : void 0;
|
|
1045
1475
|
};
|
|
1046
1476
|
const initialRuntimeStatus = buildRuntimeStatus();
|
|
1047
|
-
if (initialRuntimeStatus)
|
|
1477
|
+
if (initialRuntimeStatus) publishRuntimeStatus({
|
|
1048
1478
|
controllerUrl: pluginConfig.controllerUrl,
|
|
1049
1479
|
report: initialRuntimeStatus
|
|
1050
1480
|
}).catch((error) => {
|
|
@@ -1066,6 +1496,7 @@ const plugin = {
|
|
|
1066
1496
|
sdkRaw.registerSandboxBackend("gondolin", {
|
|
1067
1497
|
factory: createGondolinSandboxBackendFactory({
|
|
1068
1498
|
...pluginConfig,
|
|
1499
|
+
openClawRuntimeConfigProvider: () => api.runtime?.config?.current?.() ?? api.config,
|
|
1069
1500
|
openClawRuntimeStatusProvider: buildRuntimeStatus
|
|
1070
1501
|
}, backendDependencies),
|
|
1071
1502
|
manager: createGondolinSandboxBackendManager(pluginConfig, backendDependencies)
|
|
@@ -1080,6 +1511,6 @@ const plugin = {
|
|
|
1080
1511
|
//#region src/index.ts
|
|
1081
1512
|
const OPENCLAW_GONDOLIN_PLUGIN_PACKAGE_NAME = "@agent-vm/openclaw-agent-vm-plugin";
|
|
1082
1513
|
//#endregion
|
|
1083
|
-
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 };
|
|
1084
1515
|
|
|
1085
1516
|
//# sourceMappingURL=index.js.map
|