@agent-vm/gateway-interface 0.0.82 → 0.0.85

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
@@ -25,6 +25,428 @@ function gatewayVmAllowedHosts(egressHosts) {
25
25
  return Array.from(new Set([controllerVmHost, ...egressHostsForAudience(egressHosts, "gateway")]));
26
26
  }
27
27
  //#endregion
28
+ //#region src/health/controller-request-policy.ts
29
+ const gatewayInternalControllerRequestOperations = [
30
+ "controller-health",
31
+ "health-event-publish",
32
+ "openclaw-runtime-status",
33
+ "zone-git-push",
34
+ "lease-create",
35
+ "lease-get",
36
+ "lease-peek",
37
+ "lease-list",
38
+ "lease-renew",
39
+ "lease-release",
40
+ "lease-use-start",
41
+ "lease-heartbeat",
42
+ "lease-use-end"
43
+ ];
44
+ const workerInternalControllerRequestOperations = ["worker-push-branches", "worker-pull-default"];
45
+ const dedicatedControllerRequestHealthEventOperationSet = new Set(["lease-heartbeat", "lease-renew"]);
46
+ function isGenericControllerRequestEventOperation(operation) {
47
+ return !dedicatedControllerRequestHealthEventOperationSet.has(operation);
48
+ }
49
+ const genericControllerRequestEventOperations = [...gatewayInternalControllerRequestOperations, ...workerInternalControllerRequestOperations].filter(isGenericControllerRequestEventOperation);
50
+ const externalControllerRoutes = [
51
+ "GET /controller-status",
52
+ "GET /zones/:zoneId/status",
53
+ "GET /zones/:zoneId/health",
54
+ "GET /zones/:zoneId/zone-git/status",
55
+ "GET /zones/:zoneId/logs",
56
+ "POST /zones/:zoneId/credentials/refresh",
57
+ "POST /zones/:zoneId/destroy",
58
+ "POST /zones/:zoneId/upgrade",
59
+ "GET /zones/:zoneId/tasks/:taskId",
60
+ "POST /zones/:zoneId/worker-tasks",
61
+ "POST /zones/:zoneId/tasks/:taskId/close",
62
+ "POST /zones/:zoneId/enable-ssh",
63
+ "POST /zones/:zoneId/execute-command",
64
+ "POST /stop-controller"
65
+ ];
66
+ var ControllerRequestPolicyTransportError = class extends Error {
67
+ code;
68
+ operation;
69
+ constructor(options) {
70
+ const causeMessage = options.cause instanceof Error ? options.cause.message : String(options.cause);
71
+ super(`${options.operation} ${options.code}: ${causeMessage}`, { cause: options.cause });
72
+ this.code = options.code;
73
+ this.operation = options.operation;
74
+ }
75
+ };
76
+ function sleep(ms, signal) {
77
+ return new Promise((resolve, reject) => {
78
+ if (signal?.aborted === true) {
79
+ reject(signal.reason);
80
+ return;
81
+ }
82
+ const timeout = setTimeout(() => {
83
+ signal?.removeEventListener("abort", onAbort);
84
+ resolve();
85
+ }, ms);
86
+ const onAbort = () => {
87
+ clearTimeout(timeout);
88
+ reject(signal?.reason);
89
+ };
90
+ signal?.addEventListener("abort", onAbort, { once: true });
91
+ });
92
+ }
93
+ function shouldRetryResponse(response, policy) {
94
+ return policy.retryEnabled && policy.retryStatuses.includes(response.status);
95
+ }
96
+ async function drainControllerResponseBody(response) {
97
+ if (response.bodyUsed) return;
98
+ await response.text().catch(() => void 0);
99
+ }
100
+ async function fetchWithTimeout(options) {
101
+ const abortController = new AbortController();
102
+ let callerAborted = options.init?.signal?.aborted ?? false;
103
+ let timedOut = false;
104
+ const abortFromCaller = () => {
105
+ callerAborted = true;
106
+ abortController.abort(options.init?.signal?.reason);
107
+ };
108
+ const timeout = setTimeout(() => {
109
+ timedOut = true;
110
+ abortController.abort(/* @__PURE__ */ new Error(`${options.operation} timed out after ${String(options.timeoutMs)}ms`));
111
+ }, options.timeoutMs);
112
+ if (callerAborted) abortController.abort(options.init?.signal?.reason);
113
+ else options.init?.signal?.addEventListener("abort", abortFromCaller, { once: true });
114
+ try {
115
+ return await options.fetchImpl(options.input, {
116
+ ...options.init,
117
+ signal: abortController.signal
118
+ });
119
+ } catch (error) {
120
+ if (callerAborted) throw error;
121
+ throw new ControllerRequestPolicyTransportError({
122
+ cause: error,
123
+ code: timedOut ? "controller-request-timeout" : "controller-request-failed",
124
+ operation: options.operation
125
+ });
126
+ } finally {
127
+ clearTimeout(timeout);
128
+ options.init?.signal?.removeEventListener("abort", abortFromCaller);
129
+ }
130
+ }
131
+ async function fetchControllerWithPolicy(options) {
132
+ const policy = options.policy ?? controllerRequestPolicies[options.operation];
133
+ const fetchImpl = options.fetchImpl ?? fetch;
134
+ let lastTransportError;
135
+ for (let attempt = 1; attempt <= policy.maxAttempts; attempt += 1) try {
136
+ const response = await fetchWithTimeout({
137
+ fetchImpl,
138
+ init: options.init,
139
+ input: options.input,
140
+ operation: options.operation,
141
+ timeoutMs: policy.timeoutMs
142
+ });
143
+ if (attempt < policy.maxAttempts && shouldRetryResponse(response, policy)) {
144
+ await drainControllerResponseBody(response);
145
+ if (policy.retryBaseDelayMs > 0) await sleep(policy.retryBaseDelayMs, options.init?.signal ?? void 0);
146
+ continue;
147
+ }
148
+ return response;
149
+ } catch (error) {
150
+ if (!(error instanceof ControllerRequestPolicyTransportError)) throw error;
151
+ lastTransportError = error;
152
+ if (!(policy.retryEnabled && attempt < policy.maxAttempts)) throw error;
153
+ if (policy.retryBaseDelayMs > 0) await sleep(policy.retryBaseDelayMs, options.init?.signal ?? void 0);
154
+ }
155
+ throw lastTransportError ?? /* @__PURE__ */ new Error(`${options.operation} failed without a response`);
156
+ }
157
+ const controllerRequestPolicies = {
158
+ "controller-health": {
159
+ idempotency: "read",
160
+ maxAttempts: 1,
161
+ retryBaseDelayMs: 0,
162
+ retryEnabled: false,
163
+ retryStatuses: [],
164
+ timeoutMs: 3e3
165
+ },
166
+ "health-event-publish": {
167
+ idempotency: "safe-mutation",
168
+ maxAttempts: 1,
169
+ retryBaseDelayMs: 0,
170
+ retryEnabled: false,
171
+ retryStatuses: [],
172
+ timeoutMs: 3e3
173
+ },
174
+ "openclaw-runtime-status": {
175
+ idempotency: "safe-mutation",
176
+ maxAttempts: 30,
177
+ retryBaseDelayMs: 1e3,
178
+ retryEnabled: true,
179
+ retryStatuses: [
180
+ 429,
181
+ 503,
182
+ 504
183
+ ],
184
+ timeoutMs: 3e3
185
+ },
186
+ "zone-git-push": {
187
+ idempotency: "unsafe-mutation",
188
+ maxAttempts: 1,
189
+ retryBaseDelayMs: 0,
190
+ retryEnabled: false,
191
+ retryStatuses: [],
192
+ timeoutMs: 12e4
193
+ },
194
+ "lease-create": {
195
+ idempotency: "unsafe-mutation",
196
+ maxAttempts: 1,
197
+ retryBaseDelayMs: 0,
198
+ retryEnabled: false,
199
+ retryStatuses: [],
200
+ timeoutMs: 18e4
201
+ },
202
+ "lease-get": {
203
+ idempotency: "read",
204
+ maxAttempts: 2,
205
+ retryBaseDelayMs: 250,
206
+ retryEnabled: true,
207
+ retryStatuses: [503, 504],
208
+ timeoutMs: 5e3
209
+ },
210
+ "lease-peek": {
211
+ idempotency: "read",
212
+ maxAttempts: 2,
213
+ retryBaseDelayMs: 250,
214
+ retryEnabled: true,
215
+ retryStatuses: [503, 504],
216
+ timeoutMs: 5e3
217
+ },
218
+ "lease-list": {
219
+ idempotency: "read",
220
+ maxAttempts: 2,
221
+ retryBaseDelayMs: 250,
222
+ retryEnabled: true,
223
+ retryStatuses: [503, 504],
224
+ timeoutMs: 5e3
225
+ },
226
+ "lease-renew": {
227
+ idempotency: "safe-mutation",
228
+ maxAttempts: 3,
229
+ retryBaseDelayMs: 250,
230
+ retryEnabled: true,
231
+ retryStatuses: [
232
+ 429,
233
+ 503,
234
+ 504
235
+ ],
236
+ timeoutMs: 1e4
237
+ },
238
+ "lease-release": {
239
+ idempotency: "safe-mutation",
240
+ maxAttempts: 2,
241
+ retryBaseDelayMs: 250,
242
+ retryEnabled: true,
243
+ retryStatuses: [503, 504],
244
+ timeoutMs: 5e3
245
+ },
246
+ "lease-use-start": {
247
+ idempotency: "safe-mutation",
248
+ maxAttempts: 2,
249
+ retryBaseDelayMs: 250,
250
+ retryEnabled: true,
251
+ retryStatuses: [
252
+ 429,
253
+ 503,
254
+ 504
255
+ ],
256
+ timeoutMs: 1e4
257
+ },
258
+ "lease-heartbeat": {
259
+ idempotency: "safe-mutation",
260
+ maxAttempts: 2,
261
+ retryBaseDelayMs: 250,
262
+ retryEnabled: true,
263
+ retryStatuses: [
264
+ 429,
265
+ 503,
266
+ 504
267
+ ],
268
+ timeoutMs: 5e3
269
+ },
270
+ "lease-use-end": {
271
+ idempotency: "safe-mutation",
272
+ maxAttempts: 2,
273
+ retryBaseDelayMs: 250,
274
+ retryEnabled: true,
275
+ retryStatuses: [503, 504],
276
+ timeoutMs: 5e3
277
+ },
278
+ "worker-push-branches": {
279
+ idempotency: "unsafe-mutation",
280
+ maxAttempts: 1,
281
+ retryBaseDelayMs: 0,
282
+ retryEnabled: false,
283
+ retryStatuses: [],
284
+ timeoutMs: 12e4
285
+ },
286
+ "worker-pull-default": {
287
+ idempotency: "unsafe-mutation",
288
+ maxAttempts: 1,
289
+ retryBaseDelayMs: 0,
290
+ retryEnabled: false,
291
+ retryStatuses: [],
292
+ timeoutMs: 12e4
293
+ }
294
+ };
295
+ //#endregion
296
+ //#region src/health/agent-vm-health.ts
297
+ const agentVmHealthEventKinds = [
298
+ "gateway-service-health",
299
+ "gateway-control-link",
300
+ "controller-request",
301
+ "lease-renew",
302
+ "lease-heartbeat",
303
+ "tool-vm-ssh",
304
+ "gateway-plugin-health"
305
+ ];
306
+ const agentVmHealthResultKinds = [
307
+ "ok",
308
+ "failed",
309
+ "timeout",
310
+ "stale"
311
+ ];
312
+ const gatewayControlLinkHealthPins = {
313
+ controllerHost: "controller.vm.host",
314
+ controllerPort: 18800,
315
+ operation: "controller-health",
316
+ path: "/health"
317
+ };
318
+ const zoneHealthStateKinds = [
319
+ "unknown",
320
+ "ok",
321
+ "stale",
322
+ "failed"
323
+ ];
324
+ const zoneHealthIssueKinds = [
325
+ "gateway-service-unhealthy",
326
+ "gateway-control-link-unhealthy",
327
+ "controller-request-failing",
328
+ "lease-heartbeat-failing",
329
+ "lease-renew-failing",
330
+ "tool-vm-ssh-failing",
331
+ "gateway-plugin-unhealthy",
332
+ "health-event-stale"
333
+ ];
334
+ function isRecord(value) {
335
+ return typeof value === "object" && value !== null && !Array.isArray(value);
336
+ }
337
+ function isOneOf(values, value) {
338
+ return typeof value === "string" && values.includes(value);
339
+ }
340
+ function isNonNegativeFiniteNumber(value) {
341
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
342
+ }
343
+ function hasBaseEventFields(record) {
344
+ return isNonNegativeFiniteNumber(record.observedAtMs) && isOneOf(agentVmHealthResultKinds, record.result) && typeof record.zoneId === "string" && record.zoneId.length > 0;
345
+ }
346
+ function optionalString(value) {
347
+ return value === void 0 || typeof value === "string";
348
+ }
349
+ function optionalStatusCode(value) {
350
+ return value === void 0 || Number.isInteger(value);
351
+ }
352
+ function isAgentVmHealthEvent(value) {
353
+ if (!isRecord(value) || !hasBaseEventFields(value)) return false;
354
+ switch (value.kind) {
355
+ case "gateway-service-health": return typeof value.path === "string" && value.path.length > 0 && Number.isInteger(value.port) && optionalStatusCode(value.statusCode);
356
+ case "gateway-control-link": return value.controllerHost === gatewayControlLinkHealthPins.controllerHost && value.controllerPort === gatewayControlLinkHealthPins.controllerPort && isNonNegativeFiniteNumber(value.elapsedMs) && value.operation === gatewayControlLinkHealthPins.operation && value.path === gatewayControlLinkHealthPins.path;
357
+ case "controller-request": return Number.isInteger(value.attempt) && isNonNegativeFiniteNumber(value.elapsedMs) && optionalString(value.errorCode) && Number.isInteger(value.maxAttempts) && isOneOf(genericControllerRequestEventOperations, value.operation) && optionalStatusCode(value.statusCode);
358
+ case "lease-renew": return typeof value.agentId === "string" && isNonNegativeFiniteNumber(value.elapsedMs) && optionalString(value.errorCode) && typeof value.leaseId === "string";
359
+ case "lease-heartbeat": return typeof value.agentId === "string" && isNonNegativeFiniteNumber(value.elapsedMs) && optionalString(value.errorCode) && typeof value.leaseId === "string" && typeof value.useId === "string";
360
+ case "tool-vm-ssh": return typeof value.agentId === "string" && isNonNegativeFiniteNumber(value.elapsedMs) && optionalString(value.errorCode) && typeof value.leaseId === "string" && isOneOf([
361
+ "command",
362
+ "file-bridge",
363
+ "finalize",
364
+ "probe"
365
+ ], value.operation);
366
+ case "gateway-plugin-health": return isOneOf(gatewayTypeValues, value.gatewayService) && isOneOf([
367
+ "starting",
368
+ "ready",
369
+ "stopping",
370
+ "failed"
371
+ ], value.state);
372
+ default: return false;
373
+ }
374
+ }
375
+ function healthEventBucketKey(event) {
376
+ switch (event.kind) {
377
+ case "gateway-control-link": return `${event.zoneId}:${event.kind}`;
378
+ case "gateway-service-health": return `${event.zoneId}:${event.kind}:${event.port}:${event.path}`;
379
+ case "controller-request": return `${event.zoneId}:${event.kind}:${event.operation}`;
380
+ case "lease-heartbeat": return `${event.zoneId}:${event.kind}:${event.leaseId}:${event.useId}`;
381
+ case "lease-renew": return `${event.zoneId}:${event.kind}:${event.leaseId}`;
382
+ case "tool-vm-ssh": return `${event.zoneId}:${event.kind}:${event.leaseId}:${event.operation}`;
383
+ case "gateway-plugin-health": return `${event.zoneId}:${event.kind}:${event.gatewayService}`;
384
+ }
385
+ return assertNeverHealthEvent(event);
386
+ }
387
+ function failedIssueKindForEvent(event) {
388
+ switch (event.kind) {
389
+ case "gateway-service-health": return "gateway-service-unhealthy";
390
+ case "gateway-control-link": return "gateway-control-link-unhealthy";
391
+ case "controller-request": return "controller-request-failing";
392
+ case "lease-heartbeat": return "lease-heartbeat-failing";
393
+ case "lease-renew": return "lease-renew-failing";
394
+ case "tool-vm-ssh": return "tool-vm-ssh-failing";
395
+ case "gateway-plugin-health": return "gateway-plugin-unhealthy";
396
+ }
397
+ return assertNeverHealthEvent(event);
398
+ }
399
+ function assertNeverHealthEvent(event) {
400
+ throw new Error(`Unhandled health event kind: ${JSON.stringify(event)}`);
401
+ }
402
+ function issueForEvent(event, options) {
403
+ if (options.nowMs - event.observedAtMs > options.staleAfterMs) return {
404
+ kind: "health-event-stale",
405
+ latestEvent: event,
406
+ message: `${event.kind} health event is stale`,
407
+ sinceMs: event.observedAtMs
408
+ };
409
+ if (event.result === "failed" || event.result === "timeout" || event.result === "stale") return {
410
+ kind: failedIssueKindForEvent(event),
411
+ latestEvent: event,
412
+ message: `${event.kind} health event reported ${event.result}`,
413
+ sinceMs: event.observedAtMs
414
+ };
415
+ }
416
+ function deriveZoneHealthSnapshot(events, options) {
417
+ const latestByKey = /* @__PURE__ */ new Map();
418
+ for (const event of events) {
419
+ if (event.zoneId !== options.zoneId) continue;
420
+ const key = healthEventBucketKey(event);
421
+ const previous = latestByKey.get(key);
422
+ if (!previous || previous.observedAtMs <= event.observedAtMs) latestByKey.set(key, event);
423
+ }
424
+ const latestEvents = [...latestByKey.values()].toSorted((first, second) => second.observedAtMs - first.observedAtMs);
425
+ if (latestEvents.length === 0) return {
426
+ kind: "unknown",
427
+ reason: "no-events",
428
+ zoneId: options.zoneId
429
+ };
430
+ const issues = latestEvents.map((event) => issueForEvent(event, options)).filter((issue) => issue !== void 0);
431
+ if (issues.length === 0) return {
432
+ kind: "ok",
433
+ latestEvents,
434
+ zoneId: options.zoneId
435
+ };
436
+ if (issues.some((issue) => issue.kind === "health-event-stale")) return {
437
+ issues,
438
+ kind: "stale",
439
+ latestEvents,
440
+ zoneId: options.zoneId
441
+ };
442
+ return {
443
+ issues,
444
+ kind: "failed",
445
+ latestEvents,
446
+ zoneId: options.zoneId
447
+ };
448
+ }
449
+ //#endregion
28
450
  //#region src/force-ipv4-egress.ts
29
451
  /**
30
452
  * Canonical NODE_OPTIONS value for forcing IPv4-preference egress
@@ -62,6 +484,7 @@ function gatewayVmAllowedHosts(egressHosts) {
62
484
  * revert recommendation by the Node core team).
63
485
  */
64
486
  const FORCE_IPV4_EGRESS_NODE_OPTIONS = "--dns-result-order=ipv4first --no-network-family-autoselection";
487
+ const FORCE_IPV4_EGRESS_NODE_OPTION_FLAGS = FORCE_IPV4_EGRESS_NODE_OPTIONS.split(/\s+/u);
65
488
  /**
66
489
  * Compose the forced IPv4-preference flags with a user-provided
67
490
  * NODE_OPTIONS value (if any).
@@ -72,8 +495,9 @@ const FORCE_IPV4_EGRESS_NODE_OPTIONS = "--dns-result-order=ipv4first --no-networ
72
495
  * a zone secret happens to provide its own NODE_OPTIONS.
73
496
  *
74
497
  * Forced flags come FIRST so they are unambiguously applied.
75
- * User-provided flags are appended verbatim. Node treats NODE_OPTIONS
76
- * as a whitespace-separated list and all flags apply.
498
+ * User-provided flags are appended verbatim except for duplicate
499
+ * forced IPv4-preference flags. Node treats NODE_OPTIONS as a
500
+ * whitespace-separated list and all flags apply.
77
501
  *
78
502
  * Returns just the forced flags if the user value is undefined,
79
503
  * empty, or whitespace-only.
@@ -93,7 +517,9 @@ const FORCE_IPV4_EGRESS_NODE_OPTIONS = "--dns-result-order=ipv4first --no-networ
93
517
  function composeNodeOptions(userValue) {
94
518
  const trimmed = userValue?.trim() ?? "";
95
519
  if (trimmed === "") return FORCE_IPV4_EGRESS_NODE_OPTIONS;
96
- return `${FORCE_IPV4_EGRESS_NODE_OPTIONS} ${trimmed}`;
520
+ const userFlags = trimmed.split(/\s+/u).filter((flag) => !FORCE_IPV4_EGRESS_NODE_OPTION_FLAGS.includes(flag));
521
+ if (userFlags.length === 0) return FORCE_IPV4_EGRESS_NODE_OPTIONS;
522
+ return `${FORCE_IPV4_EGRESS_NODE_OPTIONS} ${userFlags.join(" ")}`;
97
523
  }
98
524
  //#endregion
99
525
  //#region src/split-resolved-gateway-secrets.ts
@@ -250,9 +676,11 @@ const TOOL_VM_WORKSPACE_GUEST_ROOT = "/workspace";
250
676
  const TOOL_VM_SCRATCH_GUEST_ROOT = "/work";
251
677
  const OPENCLAW_STATE_VM_ROOT = "/home/openclaw/.openclaw/state";
252
678
  const OPENCLAW_STATE_SANDBOXES_VM_ROOT = `${OPENCLAW_STATE_VM_ROOT}/sandboxes`;
253
- function isHostRealfsRootMapping(root) {
254
- return root.backing.kind === "host-realfs";
255
- }
679
+ const guidanceNamespaceOrder = [
680
+ "tool-vm-guest",
681
+ "openclaw-gateway",
682
+ "controller-host"
683
+ ];
256
684
  function pathContainsParentTraversal(inputPath) {
257
685
  return inputPath.split(/\/+/u).includes("..");
258
686
  }
@@ -272,11 +700,22 @@ function relativePathForRoot(candidatePath, rootPath) {
272
700
  function joinRootAndRelative(rootPath, relativePath) {
273
701
  return relativePath === "" ? rootPath : `${rootPath}/${relativePath}`;
274
702
  }
703
+ function runtimeRootIsInvalid(rootPath) {
704
+ return rootPath.trim() === "" || !rootPath.startsWith("/") || pathContainsParentTraversal(rootPath);
705
+ }
706
+ function namespaceShouldShowInGuidance(root, namespace) {
707
+ if (root.showInGuidance?.[namespace] !== void 0) return root.showInGuidance[namespace];
708
+ return namespace !== "controller-host";
709
+ }
275
710
  function allowedPathFormsForMapping(mapping, purpose) {
276
711
  return mapping.roots.flatMap((root) => {
277
712
  if (!root.capabilities[purpose]) return [];
278
713
  const suffix = root.rootPathAllowed ? "[/subpath]" : "/<child>";
279
- return [root.guestRoot, root.backing.kind === "host-realfs" && root.showHostRootInGuidance !== false ? root.hostRoot : void 0].filter((value) => value !== void 0).map((value) => `${normalizeRoot(value)}${suffix}`);
714
+ return guidanceNamespaceOrder.flatMap((namespace) => {
715
+ const rootPath = root.locations[namespace];
716
+ if (rootPath === void 0 || !namespaceShouldShowInGuidance(root, namespace)) return [];
717
+ return [`${normalizeRoot(rootPath)}${suffix}`];
718
+ });
280
719
  });
281
720
  }
282
721
  function retryGuidanceForMapping(mapping, purpose) {
@@ -297,23 +736,26 @@ function errorResult(params) {
297
736
  };
298
737
  }
299
738
  function findBestRootMatch(params) {
300
- return params.mapping.roots.flatMap((root) => {
301
- const guestRoot = root.guestRoot === void 0 ? void 0 : normalizeRoot(root.guestRoot);
302
- let hostRoot;
303
- if (isHostRealfsRootMapping(root)) hostRoot = normalizeRoot(root.hostRoot);
304
- const rootMatches = [];
305
- if (guestRoot !== void 0 && pathMatchesRoot(params.inputPath, guestRoot)) rootMatches.push({
306
- inputNamespace: "guest",
307
- matchedRoot: guestRoot,
308
- root
309
- });
310
- if (hostRoot !== void 0 && pathMatchesRoot(params.inputPath, hostRoot)) rootMatches.push({
311
- inputNamespace: "host",
312
- matchedRoot: hostRoot,
739
+ return params.mapping.roots.flatMap((root) => guidanceNamespaceOrder.flatMap((inputNamespace) => {
740
+ const rootPath = root.locations[inputNamespace];
741
+ if (rootPath === void 0) return [];
742
+ if (params.sourceNamespace !== void 0 && inputNamespace !== params.sourceNamespace) return [];
743
+ const normalizedRoot = normalizeRoot(rootPath);
744
+ return pathMatchesRoot(params.inputPath, normalizedRoot) ? [{
745
+ inputNamespace,
746
+ matchedRoot: normalizedRoot,
313
747
  root
314
- });
315
- return rootMatches;
316
- }).toSorted((left, right) => right.matchedRoot.length - left.matchedRoot.length)[0];
748
+ }] : [];
749
+ })).toSorted((left, right) => right.matchedRoot.length - left.matchedRoot.length)[0];
750
+ }
751
+ function findInvalidRoot(mapping) {
752
+ for (const root of mapping.roots) for (const namespace of guidanceNamespaceOrder) {
753
+ const rootPath = root.locations[namespace];
754
+ if (rootPath !== void 0 && runtimeRootIsInvalid(rootPath)) return {
755
+ rootId: root.id,
756
+ rootPath
757
+ };
758
+ }
317
759
  }
318
760
  function translateRuntimePath(input) {
319
761
  if (!input.inputPath.startsWith("/")) return errorResult({
@@ -330,10 +772,19 @@ function translateRuntimePath(input) {
330
772
  message: `Path '${input.inputPath}' must not contain parent traversal.`,
331
773
  purpose: input.purpose
332
774
  });
775
+ const invalidRoot = findInvalidRoot(input.mapping);
776
+ if (invalidRoot !== void 0) return errorResult({
777
+ code: "invalid-runtime-root",
778
+ inputPath: input.inputPath,
779
+ mapping: input.mapping,
780
+ message: `Runtime path root '${invalidRoot.rootId}' has invalid path '${invalidRoot.rootPath}'.`,
781
+ purpose: input.purpose
782
+ });
333
783
  const normalizedInputPath = normalizeAbsolutePath(input.inputPath);
334
784
  const match = findBestRootMatch({
335
785
  inputPath: normalizedInputPath,
336
- mapping: input.mapping
786
+ mapping: input.mapping,
787
+ ...input.sourceNamespace === void 0 ? {} : { sourceNamespace: input.sourceNamespace }
337
788
  });
338
789
  if (match === void 0) return errorResult({
339
790
  code: "unknown-runtime-path",
@@ -357,48 +808,25 @@ function translateRuntimePath(input) {
357
808
  message: `Path '${normalizedInputPath}' matched ${match.root.guidanceLabel} but cannot be used for ${input.purpose}.`,
358
809
  purpose: input.purpose
359
810
  });
360
- const guestRoot = match.root.guestRoot === void 0 ? void 0 : normalizeRoot(match.root.guestRoot);
361
- let hostRoot;
362
- if (isHostRealfsRootMapping(match.root)) hostRoot = normalizeRoot(match.root.hostRoot);
363
- if (hostRoot === void 0) {
364
- if (guestRoot === void 0) return errorResult({
365
- code: "invalid-runtime-root",
366
- inputPath: normalizedInputPath,
367
- mapping: input.mapping,
368
- message: `Runtime path root '${match.root.id}' has no guest path.`,
369
- purpose: input.purpose
370
- });
371
- return {
372
- ok: true,
373
- value: {
374
- backing: match.root.backing,
375
- capabilities: match.root.capabilities,
376
- guestPath: joinRootAndRelative(guestRoot, relativePath),
377
- guestRoot,
378
- hasHostBacking: false,
379
- inputNamespace: match.inputNamespace,
380
- inputPath: normalizedInputPath,
381
- kind: "guest-only",
382
- mappingId: input.mapping.id,
383
- relativePath,
384
- rootId: match.root.id
385
- }
386
- };
387
- }
811
+ const targetRoot = match.root.locations[input.targetNamespace];
812
+ if (targetRoot === void 0) return errorResult({
813
+ code: "target-namespace-not-available",
814
+ inputPath: normalizedInputPath,
815
+ mapping: input.mapping,
816
+ message: `Path '${normalizedInputPath}' matched ${match.root.guidanceLabel}, but '${input.targetNamespace}' is not available for that root.`,
817
+ purpose: input.purpose
818
+ });
819
+ const normalizedTargetRoot = normalizeRoot(targetRoot);
388
820
  return {
389
821
  ok: true,
390
822
  value: {
391
823
  backing: match.root.backing,
392
824
  capabilities: match.root.capabilities,
393
- ...guestRoot !== void 0 ? { guestPath: joinRootAndRelative(guestRoot, relativePath) } : {},
394
- ...guestRoot !== void 0 ? { guestRoot } : {},
395
- hasHostBacking: true,
396
- hostPath: joinRootAndRelative(hostRoot, relativePath),
397
- hostRoot,
398
825
  inputNamespace: match.inputNamespace,
399
826
  inputPath: normalizedInputPath,
400
- kind: "host-backed",
401
827
  mappingId: input.mapping.id,
828
+ outputNamespace: input.targetNamespace,
829
+ outputPath: joinRootAndRelative(normalizedTargetRoot, relativePath),
402
830
  relativePath,
403
831
  rootId: match.root.id
404
832
  }
@@ -458,6 +886,6 @@ function isToolVmLeasePeek(value) {
458
886
  return isVmCapabilityLease(record, "ssh-sandbox") && isToolVmLeaseId(Reflect.get(record, "leaseId")) && typeof Reflect.get(record, "agentId") === "string" && typeof Reflect.get(record, "createdAt") === "number" && typeof Reflect.get(record, "idleTtlMs") === "number" && typeof Reflect.get(record, "lastUsedAt") === "number" && typeof Reflect.get(record, "profileId") === "string" && isVmSshPublicEndpoint(Reflect.get(record, "ssh")) && typeof Reflect.get(record, "tcpSlot") === "number" && typeof Reflect.get(record, "workdir") === "string" && typeof Reflect.get(record, "zoneId") === "string" && !Reflect.has(record, deprecatedScopeKeyPropertyName);
459
887
  }
460
888
  //#endregion
461
- export { FORCE_IPV4_EGRESS_NODE_OPTIONS, OPENCLAW_STATE_SANDBOXES_VM_ROOT, OPENCLAW_STATE_VM_ROOT, TOOL_VM_SCRATCH_GUEST_ROOT, TOOL_VM_WORKSPACE_GUEST_ROOT, buildGatewaySessionLabel, buildToolSessionLabel, composeNodeOptions, controllerVmHost, createToolVmActiveUseHandle, createToolVmActiveUseId, createToolVmLeaseId, egressHostsForAudience, gatewayTypeValues, gatewayVmAllowedHosts, isToolVmActiveUseId, isToolVmLeaseId, isToolVmLeasePeek, isToolVmSshLease, isVmCapabilityLease, isVmSshEndpoint, isVmSshPublicEndpoint, mergeRuntimeGatewaySecrets, parseToolVmLeaseId, splitResolvedGatewaySecrets, splitResolvedSecretsByInjection, targetsAudience, translateRuntimePath, vmAudienceValues };
889
+ export { ControllerRequestPolicyTransportError, FORCE_IPV4_EGRESS_NODE_OPTIONS, OPENCLAW_STATE_SANDBOXES_VM_ROOT, OPENCLAW_STATE_VM_ROOT, TOOL_VM_SCRATCH_GUEST_ROOT, TOOL_VM_WORKSPACE_GUEST_ROOT, agentVmHealthEventKinds, agentVmHealthResultKinds, buildGatewaySessionLabel, buildToolSessionLabel, composeNodeOptions, controllerRequestPolicies, controllerVmHost, createToolVmActiveUseHandle, createToolVmActiveUseId, createToolVmLeaseId, deriveZoneHealthSnapshot, drainControllerResponseBody, egressHostsForAudience, externalControllerRoutes, fetchControllerWithPolicy, gatewayControlLinkHealthPins, gatewayInternalControllerRequestOperations, gatewayTypeValues, gatewayVmAllowedHosts, genericControllerRequestEventOperations, healthEventBucketKey, isAgentVmHealthEvent, isToolVmActiveUseId, isToolVmLeaseId, isToolVmLeasePeek, isToolVmSshLease, isVmCapabilityLease, isVmSshEndpoint, isVmSshPublicEndpoint, mergeRuntimeGatewaySecrets, parseToolVmLeaseId, splitResolvedGatewaySecrets, splitResolvedSecretsByInjection, targetsAudience, translateRuntimePath, vmAudienceValues, workerInternalControllerRequestOperations, zoneHealthIssueKinds, zoneHealthStateKinds };
462
890
 
463
891
  //# sourceMappingURL=index.js.map