@atbash/cli 0.6.0-dev.1 → 0.6.1-dev.2

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 (42) hide show
  1. package/README.md +57 -8
  2. package/dist/bin/atbash.js +6 -3
  3. package/dist/bin/atbash.js.map +1 -1
  4. package/dist/commands/config-cmd.js +15 -15
  5. package/dist/commands/config-cmd.js.map +1 -1
  6. package/dist/commands/connect.d.ts +84 -24
  7. package/dist/commands/connect.js +289 -73
  8. package/dist/commands/connect.js.map +1 -1
  9. package/dist/commands/held.js +2 -2
  10. package/dist/commands/held.js.map +1 -1
  11. package/dist/commands/history.js +1 -1
  12. package/dist/commands/history.js.map +1 -1
  13. package/dist/commands/judge.js +2 -2
  14. package/dist/commands/judge.js.map +1 -1
  15. package/dist/commands/mcp-cmd.d.ts +13 -0
  16. package/dist/commands/mcp-cmd.js +216 -0
  17. package/dist/commands/mcp-cmd.js.map +1 -0
  18. package/dist/commands/policy.js +1 -1
  19. package/dist/commands/policy.js.map +1 -1
  20. package/dist/commands/setup.d.ts +390 -8
  21. package/dist/commands/setup.js +1873 -164
  22. package/dist/commands/setup.js.map +1 -1
  23. package/dist/commands/stats.js +1 -1
  24. package/dist/commands/stats.js.map +1 -1
  25. package/dist/commands/status.js +1 -1
  26. package/dist/commands/status.js.map +1 -1
  27. package/dist/commands/tier.js +1 -1
  28. package/dist/commands/tier.js.map +1 -1
  29. package/dist/commands/tools.js +3 -3
  30. package/dist/commands/tools.js.map +1 -1
  31. package/dist/commands/whoami.js +1 -1
  32. package/dist/commands/whoami.js.map +1 -1
  33. package/dist/shared/atbash-targets.d.ts +49 -0
  34. package/dist/shared/atbash-targets.js +63 -0
  35. package/dist/shared/atbash-targets.js.map +1 -0
  36. package/dist/shared/openclaw-runtime.d.ts +221 -0
  37. package/dist/shared/openclaw-runtime.js +476 -0
  38. package/dist/shared/openclaw-runtime.js.map +1 -0
  39. package/dist/shared/win-exec.d.ts +63 -0
  40. package/dist/shared/win-exec.js +147 -0
  41. package/dist/shared/win-exec.js.map +1 -0
  42. package/package.json +3 -2
@@ -0,0 +1,476 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectChannel = detectChannel;
7
+ exports.updateCommand = updateCommand;
8
+ exports.detectOpenclaw = detectOpenclaw;
9
+ exports.writeStrategy = writeStrategy;
10
+ exports.supportStatus = supportStatus;
11
+ exports.verifyOpenclawPlugin = verifyOpenclawPlugin;
12
+ exports.pluginEntryAcceptsHooks = pluginEntryAcceptsHooks;
13
+ exports.validateConfig = validateConfig;
14
+ /**
15
+ * What the OpenClaw on THIS machine actually is, and what it can actually do.
16
+ *
17
+ * WHY THIS MODULE EXISTS — it is the fix for a whole class of bug, not one bug.
18
+ *
19
+ * `atbash setup` used to assert OpenClaw's shape from a single machine we happened
20
+ * to have installed. It hardcoded `~/.openclaw/extensions/<id>` as the plugin
21
+ * store and wrote that path into `plugins.load.paths`. On the box it was written
22
+ * against — 2026.2.1, brew formula — that was correct. On a fresh 2026.6.6 it is
23
+ * a path OpenClaw never creates, and a `load.paths` entry naming a missing
24
+ * directory fails the ENTIRE config, which then takes down the
25
+ * `openclaw plugins install` in the same run. The command broke its own install
26
+ * step and left behind a dangling entry that `openclaw doctor --fix` offers to
27
+ * delete — silently un-governing the agent.
28
+ *
29
+ * The lesson is not "update the hardcoded path". It is that we cannot know what
30
+ * OpenClaw a user has:
31
+ *
32
+ * - It ships on at least three channels, each on its own version number. When
33
+ * this was written: brew formula `openclaw-cli` 2026.7.1, brew cask
34
+ * `openclaw` 2026.8.1, npm-global 2026.8.2, plus an npm `extended-stable`
35
+ * tag on a fourth number. "The latest OpenClaw" does not identify a build.
36
+ * - It moved its plugin store twice in six months, and the migration is
37
+ * destructive (`plugins/installs.json` → `installs.json.migrated`).
38
+ * - Keys change meaning: `plugins.allow` was repurposed to gate bundled
39
+ * provider discovery; `plugins.entries.<id>.hooks` went from a FATAL unknown
40
+ * key on 2026.2.1 to a valid one on 2026.6.6.
41
+ *
42
+ * So: ASK, never assume. Everything here is probed from the binary on the box,
43
+ * and version numbers are used for REPORTING, not for deciding. Capability
44
+ * detection is what makes a range of versions supportable instead of one —
45
+ * comparing version strings is how you end up shipping a table that goes stale
46
+ * the next time OpenClaw releases.
47
+ */
48
+ const child_process_1 = require("child_process");
49
+ const fs_1 = __importDefault(require("fs"));
50
+ const path_1 = __importDefault(require("path"));
51
+ const win_exec_1 = require("./win-exec");
52
+ const NO_CAPS = { patch: false, validate: false, schema: false, configFile: false };
53
+ /** Bounded probe of a command. Never throws; a missing binary is a normal answer. */
54
+ function probe(command, args) {
55
+ const r = (0, win_exec_1.spawnPortable)(command, args, { encoding: "utf8", timeout: 20000 });
56
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
57
+ return { ok: !r.error && r.status === 0, out };
58
+ }
59
+ /**
60
+ * Where the `openclaw` on PATH came from.
61
+ *
62
+ * Path-based rather than asked of a package manager, because shelling out to
63
+ * brew and npm to answer a question we can read off a path is slow and can hang
64
+ * on a bad network — and this runs before we have written anything, where a
65
+ * hang looks like the command being broken.
66
+ *
67
+ * ⚠️ The npm case is not hypothetical or unusual: the machine that reported the
68
+ * original failure ran `/Users/m4/.nvm/versions/node/v24.13.0/bin/openclaw`,
69
+ * so `brew info openclaw-cli` there said "Not installed" while OpenClaw was
70
+ * plainly running. Telling that user `brew upgrade openclaw-cli` would have been
71
+ * useless, which is the whole reason this returns a channel at all.
72
+ */
73
+ function detectChannel(binPath) {
74
+ if (!binPath)
75
+ return "unknown";
76
+ const p = binPath.replace(/\\/g, "/");
77
+ // Follows the formula's Cellar layout, and the /opt/homebrew symlink to it.
78
+ if (/\/Cellar\/openclaw-cli\//.test(p) || /\/(opt\/homebrew|usr\/local)\/(bin|Cellar)\//.test(p))
79
+ return "brew-formula";
80
+ if (/\/Applications\/OpenClaw\.app\//.test(p))
81
+ return "brew-cask";
82
+ // nvm, fnm, volta, asdf and a plain global prefix all land under a node dir or
83
+ // an npm/lib/node_modules path.
84
+ if (/\/(\.nvm|\.fnm|\.volta|\.asdf)\//.test(p) || /\/lib\/node_modules\//.test(p) || /\/node\/v?\d/.test(p))
85
+ return "npm";
86
+ return "unknown";
87
+ }
88
+ /**
89
+ * The command that updates THIS install, or undefined when we cannot tell.
90
+ *
91
+ * Returning undefined is deliberate and better than a plausible guess: a user
92
+ * told to run `brew upgrade openclaw-cli` on an npm install gets "No available
93
+ * formula", concludes our instructions are wrong, and is no closer to an
94
+ * upgrade. `openclaw update` is the honest fallback where the channel is
95
+ * unknown — it is OpenClaw's own self-updater, and 2026.6.6's `doctor` already
96
+ * recommends exactly that ("Run `openclaw update` to update via your package
97
+ * manager (npm/pnpm)").
98
+ */
99
+ function updateCommand(channel) {
100
+ switch (channel) {
101
+ case "brew-formula":
102
+ return "brew upgrade openclaw-cli";
103
+ case "brew-cask":
104
+ return "brew upgrade --cask openclaw";
105
+ case "npm":
106
+ return "openclaw update";
107
+ default:
108
+ return "openclaw update";
109
+ }
110
+ }
111
+ /**
112
+ * Parse the subcommand list out of `openclaw config --help`.
113
+ *
114
+ * Reads the two-space-indented ` <name> <description>` rows commander prints
115
+ * under `Commands:`. A capability we cannot see is reported as absent, which is
116
+ * the safe direction: we fall back to the path that works everywhere rather than
117
+ * invoking a subcommand that exits "unknown command" mid-run.
118
+ */
119
+ function parseConfigCaps(help) {
120
+ const lines = help.split(/\r?\n/);
121
+ // Only the `Commands:` section. `Options:` lists flags, and the trailing
122
+ // `Docs:` line is prose — neither describes a subcommand.
123
+ const start = lines.findIndex((l) => /^\s*Commands:\s*$/.test(l));
124
+ const section = start === -1 ? lines : lines.slice(start + 1);
125
+ const names = new Set(section
126
+ // EXACTLY two spaces of indent. Commander puts subcommand names at 2 and
127
+ // wraps long descriptions much deeper, and 2026.7.1's `config --help` is
128
+ // full of those continuations — `patch` and `set` each carry several lines
129
+ // of examples like "openclaw config patch --stdin". A looser `^ {2,}`
130
+ // could read a wrapped line as a subcommand and invent a capability that
131
+ // does not exist, which then gets invoked mid-run and exits "unknown
132
+ // command" after earlier steps have already written.
133
+ .map((line) => /^ {2}(?! )([a-z][a-z-]*)\s{2,}\S/.exec(line)?.[1])
134
+ .filter((n) => Boolean(n)));
135
+ return {
136
+ patch: names.has("patch"),
137
+ validate: names.has("validate"),
138
+ schema: names.has("schema"),
139
+ configFile: names.has("file"),
140
+ };
141
+ }
142
+ /**
143
+ * The version out of `openclaw --version`, in either format OpenClaw has used.
144
+ *
145
+ * ⚠️ THE OUTPUT FORMAT CHANGED TOO, and this function's first version only
146
+ * handled the newer one — the exact mistake this module exists to prevent,
147
+ * committed inside the module itself. Caught only by running a real old binary:
148
+ *
149
+ * 2026.2.1 → "2026.2.1\n" (bare, verified with od -c)
150
+ * 2026.7.1 → "OpenClaw 2026.7.1 (2d2ddc4)"
151
+ *
152
+ * A regex anchored on the word "OpenClaw" returned undefined for every 2026.2.x
153
+ * machine. Behavior did not break — capabilities drive every decision here, which
154
+ * is the point of the design — but the operator lost the version from the message
155
+ * that tells them what was detected, and any future version-dependent reporting
156
+ * would have silently had nothing to work with.
157
+ *
158
+ * Neither format can be assumed, so try the labelled form first and fall back to
159
+ * a line that is nothing but a version. Both are matched by PATTERN rather than
160
+ * by line position: a loaded Atbash plugin prints `[atbash] plugin loaded` and
161
+ * `guard manager ready …` ahead of anything else on stdout.
162
+ */
163
+ function parseVersion(out) {
164
+ const labelled = /OpenClaw\s+v?(\d[\w.-]*)(?:\s+\(([0-9a-f]{4,})\))?/i.exec(out);
165
+ if (labelled)
166
+ return { version: labelled[1], ...(labelled[2] ? { build: labelled[2] } : {}) };
167
+ // A line holding only a version, e.g. 2026.2.1 — with an optional build hash,
168
+ // since a future build could print that form too.
169
+ const bare = /^[ \t]*v?(\d{4}\.\d+[\w.-]*)(?:[ \t]+\(([0-9a-f]{4,})\))?[ \t]*$/m.exec(out);
170
+ if (bare)
171
+ return { version: bare[1], ...(bare[2] ? { build: bare[2] } : {}) };
172
+ return {};
173
+ }
174
+ /**
175
+ * The openclaw to inspect: an explicit path as given, a bare name off PATH.
176
+ *
177
+ * ⚠️ An explicit path must NOT go through `where`/`which`. `--runtime` and the
178
+ * tests both pass a full path, and `where C:\dir\openclaw.cmd` does not answer
179
+ * "yes, that file" — `where` takes a NAME to search for, so a path came back as
180
+ * no match and a caller who had named the binary outright was told none existed.
181
+ */
182
+ function resolveBinary(binary) {
183
+ if (path_1.default.isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
184
+ return fs_1.default.existsSync(binary) ? binary : undefined;
185
+ }
186
+ const which = (0, child_process_1.spawnSync)(process.platform === "win32" ? "where" : "which", [binary], { encoding: "utf8" });
187
+ if (which.status !== 0)
188
+ return undefined;
189
+ // `where` reports EVERY match, and on Windows npm writes two: the extensionless
190
+ // Git Bash script first, then the `.cmd` Windows can actually run. Taking line
191
+ // one picked the unrunnable one — see pickExecutable.
192
+ return (0, win_exec_1.pickExecutable)((which.stdout ?? "").split(/\r?\n/));
193
+ }
194
+ /**
195
+ * Inspect the OpenClaw on this machine. Read-only: probes `--version` and
196
+ * `config --help`, writes nothing, and never throws.
197
+ */
198
+ function detectOpenclaw(binary = "openclaw") {
199
+ const binPath = resolveBinary(binary);
200
+ if (!binPath)
201
+ return { present: false, channel: "unknown", caps: { ...NO_CAPS } };
202
+ const channel = detectChannel(binPath);
203
+ // Probe the RESOLVED path, not the name we were handed: on Windows those are
204
+ // different files, and only one of them runs.
205
+ const ver = probe(binPath, ["--version"]);
206
+ const parsedVersion = parseVersion(ver.out);
207
+ const help = probe(binPath, ["config", "--help"]);
208
+ return {
209
+ present: true,
210
+ binPath,
211
+ channel,
212
+ ...parsedVersion,
213
+ caps: help.out ? parseConfigCaps(help.out) : { ...NO_CAPS },
214
+ };
215
+ }
216
+ function writeStrategy(rt) {
217
+ return rt.caps.patch ? "openclaw-patch" : "hand-merge";
218
+ }
219
+ /**
220
+ * Whether this OpenClaw is one we can wire at all, and what to say if not.
221
+ *
222
+ * DELIBERATELY PERMISSIVE. The floor is "there is an OpenClaw here", not "it has
223
+ * the newest config commands", because 2026.2.x installs are real — the box every
224
+ * CLI attestation was verified on is one — and the minimal entry-only config we
225
+ * now write was confirmed to load there with the plugin reporting
226
+ * `[atbash] plugin loaded` and `guard manager ready`. Narrowing support to the
227
+ * versions we happen to run is the mistake that produced this whole module.
228
+ *
229
+ * So an older build is a DEGRADED path with an offer, never a refusal: it gets
230
+ * the hand-merge, and the reason to upgrade is stated once, with the command for
231
+ * its own channel. Refusal is reserved for "no OpenClaw at all", where there is
232
+ * genuinely nothing to configure.
233
+ */
234
+ function supportStatus(rt) {
235
+ if (!rt.present) {
236
+ return {
237
+ supported: false,
238
+ degraded: false,
239
+ message: "No OpenClaw found on this machine. Nothing was changed. If this agent runs on a different computer, run this command there instead — the plugin has to be installed where the agent actually runs.",
240
+ };
241
+ }
242
+ const where = rt.version ? `OpenClaw ${rt.version}` : "This OpenClaw";
243
+ if (writeStrategy(rt) === "openclaw-patch") {
244
+ return { supported: true, degraded: false, message: `${where} — supported, and OpenClaw will validate the config change itself.` };
245
+ }
246
+ return {
247
+ supported: true,
248
+ degraded: true,
249
+ message: `${where} is supported, but it predates OpenClaw's validated config commands (\`config patch\` / \`config validate\`), so Atbash has to edit the config file directly and cannot ask OpenClaw to check it first. Upgrading is optional and makes this step safer.`,
250
+ upgrade: updateCommand(rt.channel),
251
+ };
252
+ }
253
+ /**
254
+ * Parse JSON out of a stream that also carries log lines.
255
+ *
256
+ * `openclaw plugins list --json` is clean on stdout, but a loaded plugin prints
257
+ * its own startup lines — this project's plugin logs `[atbash] plugin loaded` and
258
+ * `guard manager ready` — and those can interleave. So find the JSON rather than
259
+ * assuming the output begins with it.
260
+ */
261
+ function parseLooseJson(text) {
262
+ for (let start = text.indexOf("{"); start !== -1; start = text.indexOf("{", start + 1)) {
263
+ // Walk to the matching brace instead of parsing "the rest of the output".
264
+ //
265
+ // ⚠️ Slicing to the end and parsing that is what the first version did, and
266
+ // it failed on real 2026.2.1: its plugin log lines land on the SAME stream
267
+ // AFTER the JSON object, so `JSON.parse` saw `{…}` followed by
268
+ // `[plugins] [atbash] plugin loaded` and threw on the trailing text. The
269
+ // whole verification then reported "unknown" for a plugin that was
270
+ // demonstrably loaded — a false negative on the one check that says whether
271
+ // an agent is governed.
272
+ //
273
+ // String-aware, because a description field legitimately contains braces:
274
+ // this project's own plugin describes itself as mapping OpenClaw's hook, and
275
+ // other plugins embed JSON snippets in their descriptions.
276
+ let depth = 0;
277
+ let inString = false;
278
+ let escaped = false;
279
+ for (let i = start; i < text.length; i++) {
280
+ const ch = text[i];
281
+ if (inString) {
282
+ if (escaped)
283
+ escaped = false;
284
+ else if (ch === "\\")
285
+ escaped = true;
286
+ else if (ch === '"')
287
+ inString = false;
288
+ continue;
289
+ }
290
+ if (ch === '"')
291
+ inString = true;
292
+ else if (ch === "{")
293
+ depth++;
294
+ else if (ch === "}") {
295
+ depth--;
296
+ if (depth === 0) {
297
+ try {
298
+ return JSON.parse(text.slice(start, i + 1));
299
+ }
300
+ catch {
301
+ break; // this `{` began something that is not JSON; try the next
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+ return undefined;
308
+ }
309
+ /**
310
+ * Ask OpenClaw whether the plugin actually loaded — the check setup never had.
311
+ *
312
+ * ⚠️ WHY THIS IS NOT OPTIONAL. Setup used to declare success on the strength of
313
+ * having written what it meant to write. That is not the same claim: on the
314
+ * machine that reported the original failure, the config entry existed and the
315
+ * plugin did not, and OpenClaw said so plainly —
316
+ *
317
+ * plugins.entries.atbash-openclaw: plugin not found: atbash-openclaw
318
+ * (stale config entry ignored; remove it from plugins config)
319
+ *
320
+ * — while setup reported the run as done. An owner reading that summary believes
321
+ * their agent is governed. Nothing is more expensive than a false green on a
322
+ * control boundary, so the last word belongs to OpenClaw, not to us.
323
+ *
324
+ * ⚠️ DO NOT check `hookCount` or `hookNames` here. They are 0 and empty for a
325
+ * perfectly healthy plugin: hooks register when the GATEWAY starts, not when
326
+ * `plugins list` runs. Verified against a plugin that was demonstrably loaded and
327
+ * enforcing — it reports `hookCount: 0`. Treating that as a fault would fail
328
+ * every correct install.
329
+ */
330
+ function verifyOpenclawPlugin(rt, opts) {
331
+ const binary = opts.binary ?? "openclaw";
332
+ if (!rt.present)
333
+ return { state: "unknown", detail: "OpenClaw is not on this machine." };
334
+ const r = (0, win_exec_1.spawnPortable)(binary, ["plugins", "list", "--json"], {
335
+ encoding: "utf8",
336
+ timeout: 120000,
337
+ env: {
338
+ ...process.env,
339
+ ...(opts.configPath ? { OPENCLAW_CONFIG_PATH: path_1.default.resolve(opts.configPath) } : {}),
340
+ ...(opts.home ? { HOME: opts.home } : {}),
341
+ },
342
+ });
343
+ if (r.error)
344
+ return { state: "unknown", detail: `\`${binary} plugins list\` could not run: ${r.error.message}` };
345
+ const parsed = parseLooseJson(`${r.stdout ?? ""}${r.stderr ?? ""}`);
346
+ const plugins = parsed?.plugins;
347
+ // No parseable answer is UNKNOWN, never "missing" — reporting a governed agent
348
+ // as ungoverned on a parse failure is its own kind of wrong.
349
+ if (!Array.isArray(plugins)) {
350
+ return { state: "unknown", detail: `Could not read \`${binary} plugins list --json\` output.` };
351
+ }
352
+ const rows = plugins;
353
+ // Preference order, so a stood-down legacy entry cannot shadow the live one —
354
+ // the same rule the dashboard scan needs for the same reason.
355
+ const found = opts.entryIds
356
+ .map((id) => rows.find((p) => typeof p.id === "string" && p.id === id))
357
+ .filter((p) => Boolean(p));
358
+ const row = found.find((p) => p.enabled !== false) ?? found[0];
359
+ if (!row) {
360
+ return {
361
+ state: "missing",
362
+ detail: `OpenClaw does not list the Atbash plugin (looked for: ${opts.entryIds.join(", ")}). The config may name a plugin that is not installed — OpenClaw calls that a stale entry and \`openclaw doctor --fix\` will delete it.`,
363
+ };
364
+ }
365
+ const entry = typeof row.id === "string" ? row.id : undefined;
366
+ const version = typeof row.version === "string" ? row.version : undefined;
367
+ const missing = Array.isArray(row.dependencyStatus?.missing)
368
+ ? row.dependencyStatus.missing.filter((m) => typeof m === "string")
369
+ : [];
370
+ const base = { entry, ...(version ? { version } : {}), ...(missing.length ? { missingDeps: missing } : {}) };
371
+ if (row.enabled === false) {
372
+ return { ...base, state: "disabled", detail: "The plugin is installed but switched off in the config, so it governs nothing." };
373
+ }
374
+ if (typeof row.status === "string" && row.status !== "loaded") {
375
+ return { ...base, state: "load-error", detail: `OpenClaw reports its status as "${row.status}" rather than "loaded".` };
376
+ }
377
+ // A plugin whose required dependencies are absent loads and then fails at the
378
+ // moment it matters — the SDK it signs with is one of them.
379
+ if (row.dependencyStatus?.requiredInstalled === false || missing.length) {
380
+ return {
381
+ ...base,
382
+ state: "load-error",
383
+ detail: `OpenClaw reports required dependencies missing${missing.length ? `: ${missing.join(", ")}` : ""}. The plugin cannot reach the judge without them.`,
384
+ };
385
+ }
386
+ // Right plugin, wrong build. Loads cleanly, hooks fire, and every judge call
387
+ // fails because the agent does not exist on the chain that build targets.
388
+ if (opts.expectedSpec && version) {
389
+ const wantsDev = /@dev$/.test(opts.expectedSpec);
390
+ const isDev = /-dev\./.test(version);
391
+ if (wantsDev !== isDev) {
392
+ return {
393
+ ...base,
394
+ state: "load-error",
395
+ detail: `The installed build (${version}) does not match the deployment this agent was onboarded on (${opts.expectedSpec}). It will load and fire its hook, then fail every judge call because the agent does not exist on the chain that build targets.`,
396
+ };
397
+ }
398
+ }
399
+ return { ...base, state: "loaded" };
400
+ }
401
+ /**
402
+ * Ask OpenClaw whether a config is valid, when it can answer.
403
+ *
404
+ * This is the PREFLIGHT the command never had. Setup merged into
405
+ * `~/.openclaw/openclaw.json` without ever checking whether that file currently
406
+ * loads — so on a machine whose config was already broken it added its own entry
407
+ * to an unloadable file, and then its `openclaw plugins install` step died on the
408
+ * pre-existing breakage. Reported as an Atbash failure; it was not one.
409
+ *
410
+ * `undefined` means "cannot tell" (no `validate` on this build) and must be
411
+ * treated as such — not as a pass and not as a failure.
412
+ */
413
+ /**
414
+ * Does THIS build accept `hooks` on a plugin entry?
415
+ *
416
+ * Asked of `openclaw config schema`, never inferred from a version number or
417
+ * correlated with other capabilities — the whole point of this module.
418
+ *
419
+ * ⚠️ WHY THIS QUESTION EXISTS. `hooks` was ADDED, not removed: it is unknown on
420
+ * 2026.2.1 and valid, optional and undeprecated on 2026.6.6 and 2026.7.1, where
421
+ * an entry accepts `enabled | hooks | subagent | llm | config`. Because the entry
422
+ * schema is `additionalProperties: false`, the same key is FATAL on the old build
423
+ * and legitimate on the new one.
424
+ *
425
+ * Setup never writes `hooks` — nothing under it is required, and it gates hook
426
+ * families this plugin does not use (it registers only `before_tool_call`). But
427
+ * it also used to DELETE an operator's `hooks` block unconditionally, and on a
428
+ * build that accepts the key that is destroying their configuration:
429
+ * `hooks.timeoutMs` bounds this plugin's hook latency "without changing plugin
430
+ * code", which is a deliberate operator choice, not damage to repair.
431
+ *
432
+ * `undefined` means "cannot tell" (no `config schema` on this build). The caller
433
+ * must treat that as NOT supported and strip: keeping an unrecognized key makes
434
+ * OpenClaw refuse the entire config, and losing a timeout knob is a far smaller
435
+ * harm than a machine whose config will not load at all.
436
+ */
437
+ function pluginEntryAcceptsHooks(rt, binary = "openclaw") {
438
+ if (!rt.present || !rt.caps.schema)
439
+ return undefined;
440
+ const r = (0, win_exec_1.spawnPortable)(binary, ["config", "schema"], {
441
+ encoding: "utf8",
442
+ timeout: 120000,
443
+ maxBuffer: 64 * 1024 * 1024, // the schema is a few MB
444
+ });
445
+ if (r.error || r.status !== 0)
446
+ return undefined;
447
+ const parsed = parseLooseJson(`${r.stdout ?? ""}`);
448
+ const entry = parsed?.properties?.plugins?.properties?.entries?.additionalProperties?.properties;
449
+ if (!entry || typeof entry !== "object")
450
+ return undefined;
451
+ return Object.prototype.hasOwnProperty.call(entry, "hooks");
452
+ }
453
+ function validateConfig(rt, configPath, binary = "openclaw",
454
+ /** The home being configured, when not the process's own — see verifyOpenclawPlugin. */
455
+ home) {
456
+ if (!rt.caps.validate)
457
+ return undefined;
458
+ const r = (0, win_exec_1.spawnPortable)(binary, ["config", "validate"], {
459
+ encoding: "utf8",
460
+ timeout: 60000,
461
+ env: { ...process.env, OPENCLAW_CONFIG_PATH: path_1.default.resolve(configPath), ...(home ? { HOME: home } : {}) },
462
+ });
463
+ if (r.error)
464
+ return undefined;
465
+ const out = `${r.stdout ?? ""}${r.stderr ?? ""}`;
466
+ // 2026.6.6/2026.7.1 print "Config valid: <path>" on success, and
467
+ // "OpenClaw config is invalid: <path>" followed by `× <key>: <reason>` lines.
468
+ const valid = r.status === 0 && /config valid/i.test(out);
469
+ const problems = out
470
+ .split(/\r?\n/)
471
+ .filter((l) => /^\s*[×x✘]\s/.test(l))
472
+ .map((l) => l.replace(/^\s*[×x✘]\s*/, "").trim())
473
+ .join("\n");
474
+ return { valid, problems: problems || (valid ? "" : out.trim()) };
475
+ }
476
+ //# sourceMappingURL=openclaw-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openclaw-runtime.js","sourceRoot":"","sources":["../../src/shared/openclaw-runtime.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Launching an npm-installed CLI on Windows, without opening a shell seam.
3
+ *
4
+ * `openclaw`, `hermes`, `npm` and every other Node CLI arrive on Windows as a
5
+ * `.cmd` shim, and a `.cmd` is not an executable — CreateProcess cannot run one.
6
+ * Node used to paper over that; since 18.20.2 / 20.12.2 (the CVE-2024-27980 fix)
7
+ * it refuses outright, and `spawnSync("openclaw.cmd", …)` fails with EINVAL
8
+ * unless the call goes through a shell. That is why every `openclaw` probe
9
+ * answered "could not run" on Windows while working everywhere else: detection
10
+ * did not misread the machine, it never reached the binary at all.
11
+ *
12
+ * ⚠️ THE OBVIOUS REPAIR, `shell: true`, IS THE WRONG ONE HERE. It concatenates
13
+ * the command and its arguments into ONE string for `cmd.exe` to re-parse, so a
14
+ * `&`, `|` or `%VAR%` anywhere in an argument — or in the install path, which is
15
+ * read off `where` and was never written by us — stops being data and becomes
16
+ * another command. This tool governs whether an agent's actions are allowed to
17
+ * happen; it does not get to introduce shell injection into its own probe path.
18
+ *
19
+ * So a shim is routed through `cmd.exe /d /s /c` explicitly, with the command
20
+ * line built here: each token is quoted for the child's own CRT parser and then
21
+ * caret-escaped for the `cmd.exe` pass that happens first. `shell: true` skips
22
+ * that second step, which is exactly the part that matters.
23
+ *
24
+ * Everything off Windows, and every real `.exe`, is spawned directly as before.
25
+ */
26
+ import { type SpawnSyncOptions, type SpawnSyncReturns } from "child_process";
27
+ /** Does this command need `cmd.exe` to run at all? */
28
+ export declare function isWindowsShim(command: string): boolean;
29
+ /**
30
+ * Quote one token the way the child's C runtime will parse it back.
31
+ *
32
+ * The backslash rule is the unobvious half: a backslash is literal EXCEPT in the
33
+ * run immediately before a quote, where each one must be doubled. `C:\dir\` as
34
+ * the last token would otherwise end `"C:\dir\"` — an escaped quote, which
35
+ * swallows the rest of the line.
36
+ */
37
+ export declare function quoteArg(arg: string): string;
38
+ /**
39
+ * Caret-escape a token for the `cmd.exe` parse.
40
+ *
41
+ * The quotes added by {@link quoteArg} are escaped too, deliberately. An escaped
42
+ * quote is literal text to `cmd.exe`, so it does not open a quoted section
43
+ * there, and arrives at the child as the plain `"` its CRT is expecting.
44
+ */
45
+ export declare function escapeForCmd(token: string): string;
46
+ /** Build the full `cmd.exe` command line for a shim and its arguments. */
47
+ export declare function cmdLineFor(command: string, args: readonly string[]): string;
48
+ /**
49
+ * `spawnSync`, but a Windows `.cmd`/`.bat` shim actually runs.
50
+ *
51
+ * Identical to `spawnSync` in every other case — same arguments, same return —
52
+ * so it is a drop-in at the call sites that probe an external CLI.
53
+ */
54
+ export declare function spawnPortable(command: string, args: readonly string[], options?: SpawnSyncOptions): SpawnSyncReturns<string>;
55
+ /**
56
+ * Pick the runnable match out of `where` output.
57
+ *
58
+ * `where openclaw` lists BOTH the extensionless POSIX-style script npm writes for
59
+ * Git Bash and the `openclaw.cmd` Windows actually uses, in that order. Taking
60
+ * the first line — which is what this did — hands back the one file on the list
61
+ * that Windows cannot execute.
62
+ */
63
+ export declare function pickExecutable(lines: string[]): string | undefined;