@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.js CHANGED
@@ -1,4 +1,5 @@
1
- import { OPENCLAW_STATE_SANDBOXES_VM_ROOT, TOOL_VM_SCRATCH_GUEST_ROOT, TOOL_VM_WORKSPACE_GUEST_ROOT, createToolVmActiveUseHandle, isToolVmLeasePeek, isToolVmSshLease, translateRuntimePath } from "@agent-vm/gateway-interface";
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 fetchImpl(`${baseUrl}/lease/${encodeURIComponent(leaseId)}/renew`, { method: "POST" }), "Controller lease renew API", isToolVmSshLease);
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 fetchImpl(`${baseUrl}/lease/${encodeURIComponent(leaseId)}/uses/${encodeURIComponent(useId)}`, {
95
- body: JSON.stringify(request),
96
- headers: { "content-type": "application/json" },
97
- method: "DELETE"
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 fetchImpl(`${baseUrl}/lease/${encodeURIComponent(leaseId)}/uses/${encodeURIComponent(useId)}/heartbeat`, {
111
- body: JSON.stringify(request),
112
- headers: { "content-type": "application/json" },
113
- method: "POST"
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 fetchImpl(`${baseUrl}/lease/${encodeURIComponent(leaseId)}/peek`), "Controller lease peek API", isToolVmLeasePeek);
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 fetchImpl(`${baseUrl}/zones/${encodeURIComponent(report.zoneId)}/openclaw-runtime-status`, {
122
- body: JSON.stringify(report),
123
- headers: { "content-type": "application/json" },
124
- method: "POST"
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 fetchImpl(releaseUrl.toString(), { method: "DELETE" });
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 fetchImpl(`${baseUrl}/lease`, {
152
- body: JSON.stringify({
153
- agentId: request.agentId,
154
- agentWorkspaceDir: request.agentWorkspaceDir,
155
- ...request.idleTtlMs !== void 0 ? { idleTtlMs: request.idleTtlMs } : {},
156
- profileId: request.profileId,
157
- sessionKey: request.sessionKey,
158
- workMountDir: request.workMountDir,
159
- zoneId: request.zoneId
160
- }),
161
- headers: { "content-type": "application/json" },
162
- method: "POST"
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 fetchImpl(`${baseUrl}/lease/${encodeURIComponent(leaseId)}/uses`, {
167
- body: JSON.stringify(request),
168
- headers: { "content-type": "application/json" },
169
- method: "POST"
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
- return isOpenClawAgentId(trimmed) ? trimmed : OPENCLAW_DEFAULT_AGENT_ID;
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]) return OPENCLAW_DEFAULT_AGENT_ID;
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.hostPath ?? OPENCLAW_STATE_SANDBOXES_VM_ROOT : `${OPENCLAW_STATE_SANDBOXES_VM_ROOT}/${sandboxChild}`;
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 === "host") return isRoot ? "host-workspace-root" : "host-workspace-subpath";
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 translation = translateRuntimePath({
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: createOpenClawToolVmPathMapping({ agentWorkspaceDir: options.agentWorkspaceDir }),
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: sandboxPathIntent !== void 0 ? sandboxPathIntent.effectiveGuestCwd : translation.value.guestPath ?? TOOL_VM_WORKSPACE_GUEST_ROOT,
350
- ...translation.value.hostPath !== void 0 ? { hostEquivalentPath: translation.value.hostPath } : {},
535
+ effectiveGuestCwd: translation.value.outputPath,
536
+ ...hostEquivalentTranslation.ok ? { hostEquivalentPath: hostEquivalentTranslation.value.outputPath } : {},
351
537
  kind: kindForTranslation(translation.value),
352
- leaseWorkMountDir: sandboxPathIntent !== void 0 ? sandboxPathIntent.leaseWorkMountDir : translation.value.hostRoot ?? options.agentWorkspaceDir
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: params.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: params.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: params.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 (options.fetchImpl ?? fetch)(buildControllerUrl(options.controllerUrl, options.zoneId), {
981
- body: JSON.stringify({ expectedHead }),
982
- headers: {
983
- "content-type": "application/json",
984
- ...options.zoneGitToken ? { [zoneGitCapabilityHeader]: options.zoneGitToken } : {}
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
- method: "POST"
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
- const runtimeStatusPublishMaxAttempts = 30;
1004
- const runtimeStatusPublishRetryDelayMs = 1e3;
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) publishRuntimeStatusWithRetry({
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