@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.9

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.
Files changed (130) hide show
  1. package/out/analytics/dashboard/provider.js +13 -2
  2. package/out/analytics/dashboard/webview/panel.js +3 -2
  3. package/out/analytics/identity.js +0 -0
  4. package/out/analytics/service.js +23 -25
  5. package/out/analytics/sessionEvents.js +98 -23
  6. package/out/auth/authBaseUrl.js +59 -0
  7. package/out/auth/bootstrapApply.js +9 -10
  8. package/out/auth/brokerClient.js +48 -14
  9. package/out/auth/provider.js +8 -21
  10. package/out/auth/sessionStore.js +3 -40
  11. package/out/auth/signInFlow.js +2 -1
  12. package/out/auth-broker/config.js +50 -5
  13. package/out/auth-broker/server.js +122 -26
  14. package/out/auth-broker/store.js +25 -24
  15. package/out/authority/warrant.js +206 -0
  16. package/out/branding.js +27 -1
  17. package/out/cli/commands/conformance.js +249 -0
  18. package/out/cli/commands/data-plane.js +67 -0
  19. package/out/cli/commands/deploy.js +1 -0
  20. package/out/cli/commands/gateway.js +4 -0
  21. package/out/cli/commands/pack.js +182 -3
  22. package/out/cli/commands/sandbox.js +332 -0
  23. package/out/cli/commands/serve.js +875 -52
  24. package/out/cli/commands/store.js +12 -4
  25. package/out/cli/data-plane/gateway.js +28 -5
  26. package/out/cli/data-plane/index.js +17 -2
  27. package/out/cli/data-plane/registry.js +9 -0
  28. package/out/cli/data-plane/remote.js +106 -0
  29. package/out/cli/data-plane/sidecar.js +161 -0
  30. package/out/cli/deploy/layers/identity.js +85 -4
  31. package/out/cli/deploy-init.js +89 -24
  32. package/out/cli/deps.js +106 -2
  33. package/out/cli/idp-init.js +35 -0
  34. package/out/cli/index.js +155 -16
  35. package/out/cli/pack-install-ref.js +15 -0
  36. package/out/cli/pack-loader.js +8 -1
  37. package/out/cli/pack-runtime.js +61 -0
  38. package/out/cli/runtime.js +35 -1
  39. package/out/cli/serve-hardening.js +80 -3
  40. package/out/cli/utils.js +12 -1
  41. package/out/engines/base.js +32 -3
  42. package/out/engines/claude.js +33 -15
  43. package/out/engines/codex.js +110 -26
  44. package/out/engines/custom.js +8 -0
  45. package/out/evidence/sessionShip.js +610 -0
  46. package/out/extension.js +11 -2
  47. package/out/gateway/bridge.js +40 -2
  48. package/out/gateway/client.js +34 -1
  49. package/out/gateway/installSource.js +68 -0
  50. package/out/gateway/lifecycle.js +185 -51
  51. package/out/gateway/outboundPaths.js +4 -1
  52. package/out/gateway/secretsPreflight.js +90 -0
  53. package/out/gateway/service.js +7 -2
  54. package/out/headless/index.js +17 -1
  55. package/out/live-e2e/mcp-demo/server.mjs +32 -0
  56. package/out/mcp.js +30 -0
  57. package/out/pack/caps.js +271 -0
  58. package/out/pack/governor.js +309 -0
  59. package/out/pack/lease-schema.js +270 -0
  60. package/out/pack/lease.js +640 -0
  61. package/out/pack/reconcile.js +217 -0
  62. package/out/pack/signature.js +839 -0
  63. package/out/remote/handleRemoteChat.js +44 -3
  64. package/out/remote/remoteTrust.js +277 -0
  65. package/out/remote/serveClient.js +52 -5
  66. package/out/runner/host.js +131 -0
  67. package/out/runner/protocol.js +108 -0
  68. package/out/runner/remoteEngine.js +130 -0
  69. package/out/runtime/interfaces/edit-tracker.js +2 -0
  70. package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
  71. package/out/sandbox/containerDriver.js +570 -0
  72. package/out/sandbox/processDriver.js +315 -0
  73. package/out/sandbox/sandboxRunner.js +191 -0
  74. package/out/sandbox/support.js +301 -0
  75. package/out/sandbox/types.js +55 -0
  76. package/out/scripts/agentide-litellm.sh +67 -5
  77. package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
  78. package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
  79. package/out/scripts/analytics/setup-analytics.ps1 +37 -13
  80. package/out/scripts/analytics/setup-analytics.sh +93 -13
  81. package/out/scripts/analytics/start-analytics.sh +86 -14
  82. package/out/scripts/gateway/setup-gateway.sh +72 -32
  83. package/out/shared/models.js +11 -110
  84. package/out/spotlight/install.js +7 -1
  85. package/out/store/index.js +41 -10
  86. package/out/sync.js +3 -1
  87. package/out/test/analytics.test.js +162 -23
  88. package/out/test/auth-baseurl.test.js +132 -0
  89. package/out/test/auth-bootstrapApply.test.js +27 -6
  90. package/out/test/auth-broker-hardening.test.js +227 -0
  91. package/out/test/auth-broker.test.js +59 -9
  92. package/out/test/branding.test.js +13 -0
  93. package/out/test/caps-governor.test.js +455 -0
  94. package/out/test/codex-new-file-survival.test.js +119 -0
  95. package/out/test/configuration-scopes.test.js +131 -0
  96. package/out/test/data-plane.test.js +67 -0
  97. package/out/test/deploy-init.test.js +40 -0
  98. package/out/test/gateway-client.test.js +28 -0
  99. package/out/test/gateway-install-source.test.js +111 -0
  100. package/out/test/gateway-lifecycle.test.js +1 -1
  101. package/out/test/gateway-outbound.test.js +7 -0
  102. package/out/test/gateway-secrets-preflight.test.js +127 -0
  103. package/out/test/gateway-session-routing.test.js +18 -0
  104. package/out/test/governor-hardening.test.js +217 -0
  105. package/out/test/headless-sync.test.js +22 -0
  106. package/out/test/lease-signature.test.js +357 -0
  107. package/out/test/lease-trust-root.test.js +273 -0
  108. package/out/test/lease-validation.test.js +169 -0
  109. package/out/test/lease.test.js +114 -0
  110. package/out/test/mcp.test.js +27 -1
  111. package/out/test/pack-admission.test.js +216 -0
  112. package/out/test/pack-digest-parity.test.js +140 -0
  113. package/out/test/pack-reconcile.test.js +183 -0
  114. package/out/test/pack-signature-artifact.test.js +103 -0
  115. package/out/test/pack-signature.test.js +176 -0
  116. package/out/test/pack-trust-root.test.js +202 -0
  117. package/out/test/remote-trust.test.js +242 -0
  118. package/out/test/runner-protocol.test.js +249 -0
  119. package/out/test/sandbox-driver.test.js +553 -0
  120. package/out/test/sandbox-hardening.test.js +395 -0
  121. package/out/test/sandbox-runner.test.js +343 -0
  122. package/out/test/serve-hardening.test.js +45 -6
  123. package/out/test/serveClient.test.js +6 -5
  124. package/out/test/sessionShip.test.js +560 -0
  125. package/out/test/spotlightPublish.test.js +28 -0
  126. package/out/test/store-install-safe.test.js +69 -0
  127. package/out/test/tls-bypass.test.js +90 -0
  128. package/out/test/warrant.test.js +138 -0
  129. package/out/utils.js +46 -9
  130. package/package.json +1 -1
@@ -6,9 +6,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
6
6
  return (mod && mod.__esModule) ? mod : { "default": mod };
7
7
  };
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.GatewayBridge = void 0;
9
+ exports.GatewayBridge = exports.MAX_GATEWAY_ID_CHARS = void 0;
10
10
  exports.isIdeThreadCompositeRouteKey = isIdeThreadCompositeRouteKey;
11
11
  exports.parseIdeThreadIdFromCompositeRouteKey = parseIdeThreadIdFromCompositeRouteKey;
12
+ exports.findOversizedRunField = findOversizedRunField;
12
13
  exports.buildIdeSessionRouteKey = buildIdeSessionRouteKey;
13
14
  exports.extractRouteLabelFromGatewaySessionKey = extractRouteLabelFromGatewaySessionKey;
14
15
  exports.looksLikeGatewayChannelRouteKey = looksLikeGatewayChannelRouteKey;
@@ -52,6 +53,29 @@ function parseIdeThreadIdFromCompositeRouteKey(routeKey) {
52
53
  const id = routeKey.slice(idx + IDE_THREAD_ROUTE_MARKER.length).trim();
53
54
  return id.length > 0 ? id : undefined;
54
55
  }
56
+ /** Max length for gateway-supplied identifiers that become filenames or route-index keys. */
57
+ exports.MAX_GATEWAY_ID_CHARS = 512;
58
+ const BOUNDED_RUN_FIELDS = [
59
+ "agentId", "sessionKey", "sessionId", "channel", "accountId",
60
+ "target", "threadId", "groupId", "senderUsername", "senderName", "senderId",
61
+ ];
62
+ /**
63
+ * Name the first `agent.run` identifier that is present but not a sane bounded string.
64
+ * Returns undefined when every field is absent or valid.
65
+ */
66
+ function findOversizedRunField(params) {
67
+ const record = params;
68
+ for (const field of BOUNDED_RUN_FIELDS) {
69
+ const value = record[field];
70
+ if (value === undefined || value === null) {
71
+ continue;
72
+ }
73
+ if (typeof value !== "string" || value.length > exports.MAX_GATEWAY_ID_CHARS) {
74
+ return field;
75
+ }
76
+ }
77
+ return undefined;
78
+ }
55
79
  function buildIdeSessionRouteKey(sessionKey, sessionId) {
56
80
  const sk = sessionKey?.trim();
57
81
  const sid = sessionId?.trim();
@@ -409,10 +433,24 @@ class GatewayBridge extends events_1.EventEmitter {
409
433
  */
410
434
  async handleAgentRun(frame) {
411
435
  const params = frame.params;
412
- if (!params?.message) {
436
+ if (!params?.message || typeof params.message !== "string") {
413
437
  this.client.respond(frame.id, undefined, false, { code: "INVALID_PARAMS", message: "message is required" });
414
438
  return;
415
439
  }
440
+ if (params.message.length > client_js_1.MAX_GATEWAY_MESSAGE_CHARS) {
441
+ this.client.respond(frame.id, undefined, false, {
442
+ code: "MESSAGE_TOO_LARGE",
443
+ message: `message exceeds ${client_js_1.MAX_GATEWAY_MESSAGE_CHARS} characters`,
444
+ });
445
+ return;
446
+ }
447
+ // These land in filenames, route-index keys and JSON stores. An over-long or non-string
448
+ // value from the gateway is rejected here rather than truncated downstream.
449
+ const oversizedField = findOversizedRunField(params);
450
+ if (oversizedField) {
451
+ this.client.respond(frame.id, undefined, false, { code: "INVALID_PARAMS", message: `${oversizedField} is not a valid string` });
452
+ return;
453
+ }
416
454
  const agentId = params.agentId || this.getDefaultAgentId();
417
455
  if (!agentId) {
418
456
  this.client.respond(frame.id, undefined, false, { code: "NO_AGENT", message: "No agent available" });
@@ -3,7 +3,7 @@
3
3
  * Copyright (c) AgentHippo.ai. All rights reserved.
4
4
  *--------------------------------------------------------------------------------------------*/
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.GatewayClient = void 0;
6
+ exports.GatewayClient = exports.MAX_GATEWAY_MESSAGE_CHARS = exports.MAX_GATEWAY_FRAME_BYTES = void 0;
7
7
  exports.createAgentIDEGatewayClient = createAgentIDEGatewayClient;
8
8
  exports.createEphemeralGatewayClient = createEphemeralGatewayClient;
9
9
  /* eslint-disable local/code-no-unexternalized-strings */
@@ -15,6 +15,27 @@ exports.createEphemeralGatewayClient = createEphemeralGatewayClient;
15
15
  */
16
16
  const events_1 = require("events");
17
17
  const protocol_js_1 = require("./protocol.js");
18
+ /**
19
+ * Largest inbound frame we will parse. Without a bound here a hostile or wedged gateway can OOM
20
+ * the extension host by streaming one unbounded frame into `JSON.parse`.
21
+ *
22
+ * This does NOT limit attachment size. Images, PDFs and other media travel as REFERENCES —
23
+ * `MediaAttachment.url` / `.path` pointing at gateway-side storage (e.g. `~/.openclaw/media/`) —
24
+ * so a 50 MB PDF adds a few hundred bytes to the frame, not 50 MB. The cap therefore only bounds
25
+ * the JSON control frame, and is set well above any legitimate one so that a real message is
26
+ * never dropped (a channel like Slack caps messages at ~40k characters).
27
+ *
28
+ * Overridable for operators who front the gateway with something unusual; values below 1 MiB are
29
+ * ignored so a typo cannot silently break normal traffic.
30
+ */
31
+ exports.MAX_GATEWAY_FRAME_BYTES = (() => {
32
+ const configured = Number(process.env.AGENTHIPPO_GATEWAY_MAX_FRAME_BYTES);
33
+ return Number.isFinite(configured) && configured >= 1024 * 1024
34
+ ? configured
35
+ : 16 * 1024 * 1024;
36
+ })();
37
+ /** Largest inbound `agent.run` message we hand to an engine, in characters. */
38
+ exports.MAX_GATEWAY_MESSAGE_CHARS = 256 * 1024;
18
39
  function formatWebSocketCloseDetails(ev) {
19
40
  if (!ev) {
20
41
  return "WebSocket closed";
@@ -157,6 +178,18 @@ class GatewayClient extends events_1.EventEmitter {
157
178
  this.ws.send(JSON.stringify(frame));
158
179
  }
159
180
  handleMessage(data) {
181
+ // Bound before parsing — an oversized frame is dropped, never buffered into an object.
182
+ const byteLength = typeof data === "string"
183
+ ? Buffer.byteLength(data, "utf8")
184
+ : 0;
185
+ if (byteLength > exports.MAX_GATEWAY_FRAME_BYTES) {
186
+ // Loud, not silent: to a channel user a dropped frame looks like the agent ignoring
187
+ // them, so name the limit and the override rather than failing quietly.
188
+ this.emit("error", new Error(`Gateway frame of ${byteLength} bytes exceeds the ${exports.MAX_GATEWAY_FRAME_BYTES}-byte limit and was dropped. ` +
189
+ `Attachments normally travel as references, so this suggests inline payloads; ` +
190
+ `raise AGENTHIPPO_GATEWAY_MAX_FRAME_BYTES if this traffic is expected.`));
191
+ return;
192
+ }
160
193
  let frame;
161
194
  try {
162
195
  frame = JSON.parse(data);
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) AgentHippo.ai. All rights reserved.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ANYWHERE_INSTALL_SOURCE_FORMS = exports.ANYWHERE_CONFIG_TEMPLATE_ENV = exports.ANYWHERE_SETUP_SCRIPT_ENV = exports.ANYWHERE_INSTALL_SOURCE_ENV = void 0;
7
+ exports.validateAnywhereInstallSource = validateAnywhereInstallSource;
8
+ /**
9
+ * Validation for `agentide.anywhere.installSource`, kept in its own module (no `vscode` import) so
10
+ * the rule can be tested directly rather than through the extension host.
11
+ */
12
+ /**
13
+ * Environment variable carrying the install source into the setup terminal.
14
+ *
15
+ * The value is never interpolated into the command STRING. `"$AH_INSTALL_SOURCE"` (POSIX) and
16
+ * `$env:AH_INSTALL_SOURCE` (PowerShell) expand to one argument after the shell has finished
17
+ * parsing, so no character in the value can become syntax — which is the property the old
18
+ * blocklist (`[;`|$&\n\r]`, missing the double quote) failed to deliver.
19
+ */
20
+ exports.ANYWHERE_INSTALL_SOURCE_ENV = 'AH_INSTALL_SOURCE';
21
+ /** Same treatment for the two bundled paths we pass alongside it. */
22
+ exports.ANYWHERE_SETUP_SCRIPT_ENV = 'AH_SETUP_SCRIPT';
23
+ exports.ANYWHERE_CONFIG_TEMPLATE_ENV = 'AH_CONFIG_TEMPLATE';
24
+ /**
25
+ * Strict allowlist for `agentide.anywhere.installSource`.
26
+ *
27
+ * This value names a package that is installed globally and whose `postinstall` script runs with
28
+ * the user's privileges, so it is a code-execution setting, not a string. The setting is declared
29
+ * `"scope": "application"` so a workspace cannot set it at all; this is the second gate, and it is
30
+ * an ALLOWLIST rather than a character blocklist — a blocklist has to be right about every shell
31
+ * metacharacter forever, and the previous one already missed `"`, which was enough to escape
32
+ * `--source "${installSource}"` and run arbitrary commands in the user's setup terminal.
33
+ *
34
+ * Accepted shapes, and nothing else:
35
+ * - npm specifier: `pkg`, `pkg@1.2.3`, `pkg@latest`, `pkg@^1.2.3`, `@scope/pkg@2026.7.28`
36
+ * - `github:owner/repo` optionally `#ref`
37
+ * - an https:// URL ending in a tarball extension (a pinned artifact)
38
+ * - an absolute or explicitly-relative local path (a developer's clone)
39
+ */
40
+ const NPM_SPECIFIER = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(?:@[a-zA-Z0-9^~*><=][a-zA-Z0-9.^~*+><=|_-]*)?$/;
41
+ const GITHUB_SPECIFIER = /^github:[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*(?:#[A-Za-z0-9._/-]+)?$/;
42
+ const TARBALL_URL = /^https:\/\/[A-Za-z0-9._-]+(?::\d+)?\/[A-Za-z0-9._~!$'()*+,;=:@%/-]*\.(?:tgz|tar\.gz)$/;
43
+ const POSIX_PATH = /^(?:\.{1,2})?\/[^\0<>"'`;|&$\n\r]*$/;
44
+ const WINDOWS_PATH = /^[A-Za-z]:[\\/][^\0<>"'`;|&$\n\r]*$/;
45
+ /** Named in every refusal so the message tells the user what they CAN write. */
46
+ exports.ANYWHERE_INSTALL_SOURCE_FORMS = 'accepted forms are: an npm specifier (e.g. @agenthippo-ai/hippoclaw@2026.7.28), ' +
47
+ 'github:owner/repo[#ref], an https:// .tgz/.tar.gz URL, or a local path';
48
+ function validateAnywhereInstallSource(source) {
49
+ const refuse = (why) => ({
50
+ ok: false,
51
+ reason: `${why} — ${exports.ANYWHERE_INSTALL_SOURCE_FORMS}`,
52
+ });
53
+ const value = source.trim();
54
+ if (!value) {
55
+ return refuse('it is empty');
56
+ }
57
+ if (value !== source) {
58
+ return refuse('it has leading or trailing whitespace');
59
+ }
60
+ if (NPM_SPECIFIER.test(value) ||
61
+ GITHUB_SPECIFIER.test(value) ||
62
+ TARBALL_URL.test(value) ||
63
+ POSIX_PATH.test(value) ||
64
+ WINDOWS_PATH.test(value)) {
65
+ return { ok: true };
66
+ }
67
+ return refuse('it is not one of the accepted forms');
68
+ }
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  };
37
37
  })();
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.GatewayLifecycleManager = exports.AGENT_ANYWHERE_LABEL = void 0;
39
+ exports.GatewayLifecycleManager = exports.validateAnywhereInstallSource = exports.ANYWHERE_SETUP_SCRIPT_ENV = exports.ANYWHERE_INSTALL_SOURCE_ENV = exports.ANYWHERE_CONFIG_TEMPLATE_ENV = exports.AGENT_ANYWHERE_LABEL = void 0;
40
40
  exports.detectGateway = detectGateway;
41
41
  /* eslint-disable local/code-no-unexternalized-strings */
42
42
  /**
@@ -57,9 +57,11 @@ const path = __importStar(require("path"));
57
57
  const os = __importStar(require("os"));
58
58
  const index_1 = require("../utils/index");
59
59
  const agentEnvFile_js_1 = require("../utils/agentEnvFile.js");
60
+ const secretsPreflight_js_1 = require("./secretsPreflight.js");
60
61
  const windowsCommandLine_js_1 = require("../utils/windowsCommandLine.js");
61
62
  const gateway_scope_errors_js_1 = require("./gateway-scope-errors.js");
62
63
  const openclaw_channel_routing_js_1 = require("./openclaw-channel-routing.js");
64
+ const branding_js_1 = require("../branding.js");
63
65
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
64
66
  const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
65
67
  const IS_WINDOWS = process.platform === "win32";
@@ -228,6 +230,7 @@ const STALE_GATEWAY_LOCK_MIN_AGE_MS = 30_000;
228
230
  /** PID file for fast status checks (~1-2ms vs 100ms+ for shell commands) */
229
231
  const GATEWAY_PID_FILE = index_1.PID_FILES.gateway();
230
232
  const FORK_CLI_NAME = "hippoclaw";
233
+ const GATEWAY_NPM_PACKAGE_DIRS = ["@agenthippo-ai/hippoclaw", "openclaw"];
231
234
  const MANAGED_GATEWAY_STATE_DIR = path.join(os.homedir(), ".agenthippo", "gateway");
232
235
  const MANAGED_GATEWAY_MANIFEST_PATH = path.join(MANAGED_GATEWAY_STATE_DIR, "install.json");
233
236
  const GATEWAY_INSTALL_STATUS_FILE = path.join(MANAGED_GATEWAY_STATE_DIR, "install-status.json");
@@ -239,6 +242,11 @@ const MANAGED_OPENCLAW_WRAPPER_PATH = path.join(MANAGED_GATEWAY_WRAPPER_DIR, IS_
239
242
  const MANAGED_GATEWAY_WRAPPER_MARKER = IS_WINDOWS
240
243
  ? "REM Managed by Agent Anywhere"
241
244
  : "# Managed by Agent Anywhere";
245
+ const installSource_js_1 = require("./installSource.js");
246
+ Object.defineProperty(exports, "ANYWHERE_CONFIG_TEMPLATE_ENV", { enumerable: true, get: function () { return installSource_js_1.ANYWHERE_CONFIG_TEMPLATE_ENV; } });
247
+ Object.defineProperty(exports, "ANYWHERE_INSTALL_SOURCE_ENV", { enumerable: true, get: function () { return installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV; } });
248
+ Object.defineProperty(exports, "ANYWHERE_SETUP_SCRIPT_ENV", { enumerable: true, get: function () { return installSource_js_1.ANYWHERE_SETUP_SCRIPT_ENV; } });
249
+ Object.defineProperty(exports, "validateAnywhereInstallSource", { enumerable: true, get: function () { return installSource_js_1.validateAnywhereInstallSource; } });
242
250
  function installSourceNeedsBuild(source) {
243
251
  if (!source) {
244
252
  return false;
@@ -495,6 +503,11 @@ function addManagedOpenclawRoot(roots, candidate) {
495
503
  roots.add(candidate);
496
504
  }
497
505
  }
506
+ function addManagedGatewayPackageRoots(roots, nodeModulesDir) {
507
+ for (const pkgDir of GATEWAY_NPM_PACKAGE_DIRS) {
508
+ addManagedOpenclawRoot(roots, path.join(nodeModulesDir, pkgDir));
509
+ }
510
+ }
498
511
  /** Scan nvm version dirs when `npm root -g` is unavailable in the extension host PATH. */
499
512
  function collectNvmOpenclawPackageRoots() {
500
513
  const home = os.homedir();
@@ -509,7 +522,7 @@ function collectNvmOpenclawPackageRoots() {
509
522
  if (!entry.isDirectory()) {
510
523
  continue;
511
524
  }
512
- candidates.push(path.join(versionsDir, entry.name, "lib", "node_modules", "openclaw"));
525
+ candidates.push(...GATEWAY_NPM_PACKAGE_DIRS.map((pkgDir) => path.join(versionsDir, entry.name, "lib", "node_modules", pkgDir)));
513
526
  }
514
527
  }
515
528
  catch {
@@ -533,15 +546,17 @@ async function getManagedOpenclawRoots() {
533
546
  continue;
534
547
  }
535
548
  // Windows npm uses node_modules/ directly (no lib/ prefix); Unix uses lib/node_modules/
536
- const candidate = path.join(bootstrapNodeRoot, entry.name, ...(IS_WINDOWS ? ["node_modules"] : ["lib", "node_modules"]), "openclaw");
537
- if (!fs.existsSync(candidate)) {
538
- continue;
539
- }
540
- try {
541
- roots.add(fs.realpathSync(candidate));
542
- }
543
- catch {
544
- roots.add(candidate);
549
+ for (const pkgDir of GATEWAY_NPM_PACKAGE_DIRS) {
550
+ const candidate = path.join(bootstrapNodeRoot, entry.name, ...(IS_WINDOWS ? ["node_modules"] : ["lib", "node_modules"]), pkgDir);
551
+ if (!fs.existsSync(candidate)) {
552
+ continue;
553
+ }
554
+ try {
555
+ roots.add(fs.realpathSync(candidate));
556
+ }
557
+ catch {
558
+ roots.add(candidate);
559
+ }
545
560
  }
546
561
  }
547
562
  }
@@ -550,8 +565,7 @@ async function getManagedOpenclawRoots() {
550
565
  }
551
566
  }
552
567
  // Windows npm uses node_modules/ directly (no lib/ prefix); Unix uses lib/node_modules/
553
- const legacyManaged = path.join(os.homedir(), ".npm-global", ...(IS_WINDOWS ? ["node_modules"] : ["lib", "node_modules"]), "openclaw");
554
- addManagedOpenclawRoot(roots, legacyManaged);
568
+ addManagedGatewayPackageRoots(roots, path.join(os.homedir(), ".npm-global", ...(IS_WINDOWS ? ["node_modules"] : ["lib", "node_modules"])));
555
569
  for (const nvmManaged of collectNvmOpenclawPackageRoots()) {
556
570
  addManagedOpenclawRoot(roots, nvmManaged);
557
571
  }
@@ -559,7 +573,7 @@ async function getManagedOpenclawRoots() {
559
573
  const { stdout } = await execAsync("npm root -g", { timeout: 3_000 });
560
574
  const npmRoot = stdout.trim();
561
575
  if (npmRoot) {
562
- addManagedOpenclawRoot(roots, path.join(npmRoot, "openclaw"));
576
+ addManagedGatewayPackageRoots(roots, npmRoot);
563
577
  }
564
578
  }
565
579
  catch {
@@ -1141,7 +1155,7 @@ class GatewayLifecycleManager {
1141
1155
  // Auto-start if configured and installed
1142
1156
  const autoStart = this.getConfig("autoStart", false);
1143
1157
  if (autoStart && this._lastDetection?.installed && !this._lastDetection?.running) {
1144
- this.log("Auto-starting gateway (agentide.anywhere.autoStart = true)");
1158
+ this.log(`Auto-starting gateway (${(0, branding_js_1.toSettingsDisplayKey)("agentide.anywhere.autoStart")} = true)`);
1145
1159
  await this.start();
1146
1160
  }
1147
1161
  }
@@ -1763,15 +1777,18 @@ class GatewayLifecycleManager {
1763
1777
  const installSource = this.getConfig("installSource", "");
1764
1778
  const port = this.getGatewayPort();
1765
1779
  if (!installSource) {
1766
- vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Install source not configured. Set "agentide.anywhere.installSource" in settings.`);
1780
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Install source not configured. Set "${(0, branding_js_1.toSettingsDisplayKey)("agentide.anywhere.installSource")}" in settings.`);
1767
1781
  return false;
1768
1782
  }
1769
- // Security: validate installSource to prevent command injection.
1770
- // Allowed: npm package names, scoped packages, git repos, local paths, version specifiers.
1771
- // Reject: semicolons, backticks, pipes, $(), &&, ||, newlines, etc.
1772
- if (/[;`|$&\n\r]/.test(installSource) || /\$\(/.test(installSource)) {
1773
- vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Invalid install source - contains disallowed characters. Check "agentide.anywhere.installSource" in settings.`);
1774
- this.log(`Rejected installSource due to suspicious characters: ${installSource}`);
1783
+ // Security: this value names a package that is installed globally and whose postinstall
1784
+ // script runs as the user. It is validated against an ALLOWLIST of specifier shapes (see
1785
+ // validateAnywhereInstallSource) and then passed to the setup terminal through an
1786
+ // environment variable rather than being interpolated into a command string.
1787
+ const sourceCheck = (0, installSource_js_1.validateAnywhereInstallSource)(installSource);
1788
+ if (!sourceCheck.ok) {
1789
+ vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Refusing to install from "${installSource}" — ${sourceCheck.reason}. ` +
1790
+ `Fix "${(0, branding_js_1.toSettingsDisplayKey)("agentide.anywhere.installSource")}" in your user settings.`);
1791
+ this.log(`Rejected installSource (${sourceCheck.reason}): ${installSource}`);
1775
1792
  return false;
1776
1793
  }
1777
1794
  if (this._gatewayInstallInProgress) {
@@ -1816,46 +1833,77 @@ class GatewayLifecycleManager {
1816
1833
  ];
1817
1834
  const templatePath = templateCandidates.find((p) => fs.existsSync(p));
1818
1835
  let terminalOptions;
1836
+ // NOTE ON WHY THIS IS NOT execFile/spawn WITH AN ARGV ARRAY.
1837
+ // The setup runs in a VISIBLE terminal (the user watches npm/git progress and answers
1838
+ // prompts), and it must run under a LOGIN shell: node/npm/pnpm on macOS commonly live on
1839
+ // a PATH built by ~/.zprofile, and `getAnywhereSetupShellArgs` uses `-lic`/`-lc` for
1840
+ // exactly that reason. A bare argv exec would break every nvm/volta install.
1841
+ // So the shell stays — but no untrusted value is ever spliced into the command text.
1842
+ // `installSource` (and the two bundled paths) travel in the terminal's ENVIRONMENT and
1843
+ // are referenced as `"$VAR"` / `$env:VAR`, which the shell expands AFTER parsing: the
1844
+ // value becomes exactly one argument and none of its characters can become syntax.
1845
+ const setupEnv = {
1846
+ ...anywhereSetupTerminalEnv(),
1847
+ [installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV]: installSource,
1848
+ ...(scriptPath ? { [installSource_js_1.ANYWHERE_SETUP_SCRIPT_ENV]: scriptPath } : {}),
1849
+ ...(templatePath ? { [installSource_js_1.ANYWHERE_CONFIG_TEMPLATE_ENV]: templatePath } : {}),
1850
+ };
1819
1851
  if (scriptPath && IS_WINDOWS) {
1820
1852
  // Windows: run PowerShell script
1821
- const args = [`-Source "${installSource}"`];
1853
+ const args = [`-Source $env:${installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV}`];
1822
1854
  if (templatePath) {
1823
- args.push(`-ConfigTemplate "${templatePath}"`);
1855
+ args.push(`-ConfigTemplate $env:${installSource_js_1.ANYWHERE_CONFIG_TEMPLATE_ENV}`);
1824
1856
  }
1825
- const psCmd = `& "${scriptPath}" ${args.join(" ")}; Write-Host ""; Write-Host "You can close this window."`;
1857
+ const psCmd = `& $env:${installSource_js_1.ANYWHERE_SETUP_SCRIPT_ENV} ${args.join(" ")}; Write-Host ""; Write-Host "You can close this window."`;
1826
1858
  terminalOptions = {
1827
1859
  name: AGENT_ANYWHERE_SETUP_TERMINAL,
1828
1860
  shellPath: "powershell.exe",
1829
1861
  shellArgs: ["-NoProfile", "-NoExit", "-ExecutionPolicy", "Bypass", "-Command", psCmd],
1862
+ env: {
1863
+ [installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV]: installSource,
1864
+ [installSource_js_1.ANYWHERE_SETUP_SCRIPT_ENV]: scriptPath,
1865
+ ...(templatePath ? { [installSource_js_1.ANYWHERE_CONFIG_TEMPLATE_ENV]: templatePath } : {}),
1866
+ },
1830
1867
  };
1831
1868
  }
1832
1869
  else if (scriptPath) {
1833
1870
  // macOS/Linux: run bash script
1834
- const args = [`--source "${installSource}"`];
1871
+ const args = [`--source "$${installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV}"`];
1835
1872
  if (templatePath) {
1836
- args.push(`--config-template "${templatePath}"`);
1873
+ args.push(`--config-template "$${installSource_js_1.ANYWHERE_CONFIG_TEMPLATE_ENV}"`);
1837
1874
  }
1838
- const setupCmd = `bash "${scriptPath}" ${args.join(" ")}`;
1875
+ const setupCmd = `bash "$${installSource_js_1.ANYWHERE_SETUP_SCRIPT_ENV}" ${args.join(" ")}`;
1839
1876
  const holdOpenCmd = `${setupCmd}; echo ""; echo "Press Enter to close..."; read`;
1840
1877
  terminalOptions = {
1841
1878
  name: AGENT_ANYWHERE_SETUP_TERMINAL,
1842
1879
  shellPath: getAnywhereSetupShellPath(),
1843
1880
  shellArgs: getAnywhereSetupShellArgs(holdOpenCmd),
1844
- env: anywhereSetupTerminalEnv(),
1881
+ env: setupEnv,
1845
1882
  };
1846
1883
  }
1847
1884
  else {
1848
1885
  // Fallback: raw npm install (script not bundled)
1849
1886
  this.log("Setup script not found, falling back to npm install");
1850
- const fallbackCmd = `npm install -g ${installSource}`;
1887
+ const fallbackCmd = `npm install -g "$${installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV}"`;
1851
1888
  const holdOpenCmd = `${fallbackCmd}; echo ""; echo "Press Enter to close..."; read`;
1852
1889
  terminalOptions = IS_WINDOWS
1853
- ? { name: AGENT_ANYWHERE_SETUP_TERMINAL, shellPath: "cmd.exe", shellArgs: ["/c", `${fallbackCmd} && echo. && echo Setup complete. && pause`] }
1890
+ // cmd.exe is the one shell here that expands `%VAR%` DURING parsing rather than
1891
+ // after it, so the env indirection alone is not a boundary on Windows — the
1892
+ // allowlist above is what makes this safe (it admits no `&`, `|`, `^`, quote or
1893
+ // whitespace outside a path shape). Kept as cmd.exe because this fallback only
1894
+ // fires when the bundled setup script is missing, and PowerShell is not
1895
+ // guaranteed present on every Windows host we support.
1896
+ ? {
1897
+ name: AGENT_ANYWHERE_SETUP_TERMINAL,
1898
+ shellPath: "cmd.exe",
1899
+ shellArgs: ["/c", `npm install -g "%${installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV}%" && echo. && echo Setup complete. && pause`],
1900
+ env: { [installSource_js_1.ANYWHERE_INSTALL_SOURCE_ENV]: installSource },
1901
+ }
1854
1902
  : {
1855
1903
  name: AGENT_ANYWHERE_SETUP_TERMINAL,
1856
1904
  shellPath: getAnywhereSetupShellPath(),
1857
1905
  shellArgs: getAnywhereSetupShellArgs(holdOpenCmd),
1858
- env: anywhereSetupTerminalEnv(),
1906
+ env: setupEnv,
1859
1907
  };
1860
1908
  }
1861
1909
  // Run in a visible terminal so user can see progress
@@ -2119,6 +2167,93 @@ class GatewayLifecycleManager {
2119
2167
  const doc = await vscode.workspace.openTextDocument(this.configPath);
2120
2168
  await vscode.window.showTextDocument(doc);
2121
2169
  }
2170
+ /**
2171
+ * Check that every enabled channel's env-sourced secrets resolve before spawning the
2172
+ * gateway. On missing secrets, offer to disable the affected channels (config is backed
2173
+ * up first) or to open ~/.agent-hippo/.env with the missing keys stubbed in.
2174
+ *
2175
+ * Configs that are not strict JSON (OpenClaw accepts JSON5 with comments) are left to
2176
+ * the gateway itself: rewriting them would destroy comments, and the fail-fast exit
2177
+ * detection in doStart still surfaces the error promptly.
2178
+ */
2179
+ async preflightChannelSecrets(gatewayEnv) {
2180
+ let content;
2181
+ try {
2182
+ content = fs.readFileSync(this.configPath, "utf-8");
2183
+ }
2184
+ catch {
2185
+ return "continue"; // No config yet — nothing to resolve.
2186
+ }
2187
+ const config = (0, secretsPreflight_js_1.parseStrictJsonConfig)(content);
2188
+ if (config === undefined) {
2189
+ return "continue";
2190
+ }
2191
+ const missing = (0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(config, gatewayEnv);
2192
+ if (missing.length === 0) {
2193
+ return "continue";
2194
+ }
2195
+ const envFilePath = (0, agentEnvFile_js_1.getAgentHomeEnvFilePath)();
2196
+ const channelNames = missing.map((m) => m.channel).join(", ");
2197
+ const varNames = [...new Set(missing.flatMap((m) => m.envVars))];
2198
+ this.log(`Channel secrets preflight: ${channelNames} missing ${varNames.join(", ")}`);
2199
+ const disableAndStart = `Disable ${channelNames} & Start`;
2200
+ const addSecrets = "Add Secrets";
2201
+ const choice = await vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: The ${channelNames} channel needs ${varNames.join(", ")}, which ${varNames.length === 1 ? "is" : "are"} not set in ${envFilePath} or the IDE environment. Add the secret${varNames.length === 1 ? "" : "s"} or disable the channel to start the gateway.`, disableAndStart, addSecrets);
2202
+ if (choice === disableAndStart) {
2203
+ const updated = (0, secretsPreflight_js_1.disableChannelsInConfigText)(content, missing.map((m) => m.channel));
2204
+ if (!updated) {
2205
+ void vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not update ${this.configPath}. Set "enabled": false on the ${channelNames} channel manually, then start again.`);
2206
+ return "abort";
2207
+ }
2208
+ const backupPath = `${this.configPath}.bak-missing-secrets`;
2209
+ try {
2210
+ fs.copyFileSync(this.configPath, backupPath);
2211
+ fs.writeFileSync(this.configPath, updated, { encoding: "utf-8", mode: IS_WINDOWS ? undefined : 0o600 });
2212
+ }
2213
+ catch (err) {
2214
+ const detail = err instanceof Error ? err.message : String(err);
2215
+ void vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not update ${this.configPath} (${detail}).`);
2216
+ return "abort";
2217
+ }
2218
+ this.log(`Disabled channels missing secrets: ${channelNames} (backup: ${backupPath})`);
2219
+ void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Disabled the ${channelNames} channel (backup: ${path.basename(backupPath)}). To re-enable: add ${varNames.join(", ")} to ${envFilePath}, set "enabled": true in the config, then restart the gateway.`);
2220
+ return "continue";
2221
+ }
2222
+ if (choice === addSecrets) {
2223
+ await this.openEnvFileWithSecretStubs(envFilePath, varNames);
2224
+ }
2225
+ return "abort";
2226
+ }
2227
+ /** Append stub lines for missing secret keys to ~/.agent-hippo/.env and open it. */
2228
+ async openEnvFileWithSecretStubs(envFilePath, keys) {
2229
+ try {
2230
+ let content = "";
2231
+ try {
2232
+ content = fs.readFileSync(envFilePath, "utf-8");
2233
+ }
2234
+ catch {
2235
+ // New file.
2236
+ }
2237
+ const toAdd = keys.filter((key) => !new RegExp(`^\\s*#?\\s*${key}=`, "m").test(content));
2238
+ if (toAdd.length) {
2239
+ const separator = content.length && !content.endsWith("\n") ? "\n" : "";
2240
+ const block = [
2241
+ `# Added by ${exports.AGENT_ANYWHERE_LABEL} — fill in, then start the gateway again.`,
2242
+ ...toAdd.map((key) => `${key}=`),
2243
+ ].join("\n");
2244
+ fs.writeFileSync(envFilePath, `${content}${separator}${block}\n`, {
2245
+ encoding: "utf-8",
2246
+ mode: IS_WINDOWS ? undefined : 0o600,
2247
+ });
2248
+ }
2249
+ const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(envFilePath));
2250
+ await vscode.window.showTextDocument(doc, { preview: false });
2251
+ }
2252
+ catch (err) {
2253
+ const detail = err instanceof Error ? err.message : String(err);
2254
+ void vscode.window.showErrorMessage(`${exports.AGENT_ANYWHERE_LABEL}: Could not open ${envFilePath} (${detail}).`);
2255
+ }
2256
+ }
2122
2257
  async startGatewayViaHiddenWindowsProcess(spawnExe, spawnArgs, logPath, errorLogPath, gatewayEnv) {
2123
2258
  const argumentLine = (0, windowsCommandLine_js_1.buildWindowsArgumentLine)(spawnArgs);
2124
2259
  const argumentClause = argumentLine.length > 0
@@ -2301,6 +2436,13 @@ class GatewayLifecycleManager {
2301
2436
  this.log(`Gateway env: ${key} present for child process`);
2302
2437
  }
2303
2438
  }
2439
+ // Channel secrets preflight: an enabled channel referencing an unset env var makes the
2440
+ // gateway crash at startup (SecretRefResolutionError) with the reason buried in the log.
2441
+ // Catch it here and offer a fix instead of spawning a process that cannot survive.
2442
+ if ((await this.preflightChannelSecrets(gatewayEnv)) === "abort") {
2443
+ this.setStatus("installed");
2444
+ return;
2445
+ }
2304
2446
  let startedPid;
2305
2447
  if (isWindowsLaunch) {
2306
2448
  startedPid = await this.startGatewayViaHiddenWindowsProcess(spawnExe, spawnArgs, logPath, errorLogPath, gatewayEnv);
@@ -2378,7 +2520,12 @@ class GatewayLifecycleManager {
2378
2520
  processAlive &&
2379
2521
  sawLaunchedProcessAlive &&
2380
2522
  elapsed < startupTimeoutMs + launchGraceMs;
2381
- if (trackingPid && sawLaunchedProcessAlive && !processAlive && !isWindowsLaunch) {
2523
+ // A gateway that crashes at boot (e.g. unresolvable secrets) can exit before
2524
+ // the first aliveness sample, so don't require sawLaunchedProcessAlive: on
2525
+ // Unix the exit handler nulling this.process is just as definitive. Without
2526
+ // this, an instant crash silently spins until the startup timeout.
2527
+ const launchedProcessGone = !processAlive && (sawLaunchedProcessAlive || this.process === null);
2528
+ if (trackingPid && launchedProcessGone && !isWindowsLaunch) {
2382
2529
  this.setStatus("error");
2383
2530
  this.log(`Gateway process exited before opening port ${port}`);
2384
2531
  await showGatewayStartFailedToast(logPath);
@@ -2609,16 +2756,8 @@ class GatewayLifecycleManager {
2609
2756
  encoding: "utf-8",
2610
2757
  mode: IS_WINDOWS ? undefined : 0o600,
2611
2758
  });
2759
+ // Application-scoped setting: user scope is the only writable target.
2612
2760
  const gwConfig = vscode.workspace.getConfiguration("agentide.gateway");
2613
- if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
2614
- try {
2615
- await gwConfig.update("token", newToken, vscode.ConfigurationTarget.Workspace);
2616
- }
2617
- catch (err) {
2618
- this.log(`Workspace token update skipped: ${err instanceof Error ? err.message : String(err)}`);
2619
- }
2620
- }
2621
- // Always persist user scope so empty-window mode (no workspace) still works.
2622
2761
  await gwConfig.update("token", newToken, vscode.ConfigurationTarget.Global);
2623
2762
  await this.restartGateway();
2624
2763
  void vscode.window.showInformationMessage(`${exports.AGENT_ANYWHERE_LABEL}: Auth reset; gateway restarted. If you use the Web UI, paste the new token in Control UI settings.`);
@@ -2702,19 +2841,14 @@ class GatewayLifecycleManager {
2702
2841
  await gwConfig.update("url", wsUrl, vscode.ConfigurationTarget.Global);
2703
2842
  }
2704
2843
  // For local gateways, apply gateway.auth.token from ~/.openclaw/openclaw.json.
2705
- // Prefer workspace scope when a workspace exists; otherwise use user scope in empty-window mode.
2844
+ // The setting is application-scoped (a credential must never land in a committable
2845
+ // .vscode/settings.json), and VS Code rejects any non-user write for that scope.
2706
2846
  // Remote URLs: do not override — token must come from settings / env for that host.
2707
2847
  const localGateway = /127\.0\.0\.1|localhost|\[::1\]/i.test(wsUrl);
2708
2848
  const fileToken = this.readGatewayToken();
2709
2849
  if (fileToken && localGateway) {
2710
- if (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0) {
2711
- await gwConfig.update("token", fileToken, vscode.ConfigurationTarget.Workspace);
2712
- this.log("Gateway token applied from openclaw.json (workspace scope)");
2713
- }
2714
- else {
2715
- await gwConfig.update("token", fileToken, vscode.ConfigurationTarget.Global);
2716
- this.log("Gateway token applied from openclaw.json (global scope; no workspace open)");
2717
- }
2850
+ await gwConfig.update("token", fileToken, vscode.ConfigurationTarget.Global);
2851
+ this.log("Gateway token applied from openclaw.json (user scope)");
2718
2852
  }
2719
2853
  await this._bridgeRef.connect();
2720
2854
  this.setBridgeConnected(this._bridgeRef.isConnected);
@@ -47,7 +47,10 @@ exports.GATEWAY_OUTBOUND_FILENAME = 'gateway-outbound.json';
47
47
  exports.AGENTHIPPO_GATEWAY_OUTBOUND_FILE_ENV = 'AGENTHIPPO_GATEWAY_OUTBOUND_FILE';
48
48
  /** Sanitize gateway session keys for use as `.agent-hippo/sessions/<id>/` directory names. */
49
49
  function safeGatewaySessionDirId(value) {
50
- return value.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').slice(0, 160) || 'session';
50
+ const sanitized = value.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_').slice(0, 160);
51
+ // Dot-only keys ("." / "..") would resolve outside the per-session directory — the key is
52
+ // gateway-supplied, so that is a path traversal. Map each dot to "_" to stay collision-free.
53
+ return sanitized.replace(/^\.+$/, m => '_'.repeat(m.length)) || 'session';
51
54
  }
52
55
  function getGatewayOutboundFilePath(workspaceRoot, sessionDirId) {
53
56
  return path.join(workspaceRoot, branding_js_1.AGENTIDE_DIR_NAME, 'sessions', sessionDirId, exports.GATEWAY_OUTBOUND_FILENAME);