@moltbankhq/openclaw 0.1.11 → 0.1.13

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 (2) hide show
  1. package/index.ts +353 -28
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { spawn, spawnSync } from 'child_process';
2
2
  import { chmodSync, chownSync, existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, type Dirent } from 'fs';
3
- import { join, dirname } from 'path';
3
+ import { join, dirname, basename } from 'path';
4
4
  import { homedir, userInfo } from 'os';
5
5
  const IS_WIN = process.platform === 'win32';
6
6
 
@@ -16,6 +16,7 @@ interface CredentialsOrganization {
16
16
  name?: string;
17
17
  access_token?: string;
18
18
  x402_signer_private_key?: string;
19
+ x402_wallet?: Record<string, unknown>;
19
20
  }
20
21
 
21
22
  interface CredentialsFile {
@@ -365,10 +366,136 @@ function getExecErrorMessage(error: unknown): string {
365
366
  return String(error);
366
367
  }
367
368
 
369
+ function getPathEnvKey(env: NodeJS.ProcessEnv): string {
370
+ const existing = Object.keys(env).find((key) => key.toLowerCase() === 'path');
371
+ return existing ?? 'Path';
372
+ }
373
+
374
+ function splitPathEntries(value: string): string[] {
375
+ return value
376
+ .split(IS_WIN ? ';' : ':')
377
+ .map((entry) => entry.trim())
378
+ .filter(Boolean);
379
+ }
380
+
381
+ function prependPathEntries(env: NodeJS.ProcessEnv, entries: string[]): void {
382
+ if (entries.length === 0) return;
383
+
384
+ const pathKey = getPathEnvKey(env);
385
+ const existing = splitPathEntries(asString(env[pathKey]));
386
+ const seen = new Set<string>();
387
+ const ordered: string[] = [];
388
+
389
+ for (const entry of [...entries, ...existing]) {
390
+ const normalized = IS_WIN ? entry.toLowerCase() : entry;
391
+ if (seen.has(normalized)) continue;
392
+ seen.add(normalized);
393
+ ordered.push(entry);
394
+ }
395
+
396
+ env[pathKey] = ordered.join(IS_WIN ? ';' : ':');
397
+ }
398
+
399
+ function isExplicitCommandPath(command: string): boolean {
400
+ return command.includes('/') || command.includes('\\') || /^[A-Za-z]:/.test(command);
401
+ }
402
+
403
+ function getWindowsCommandExtensions(env: NodeJS.ProcessEnv): string[] {
404
+ const fromEnv = asString(env.PATHEXT || process.env.PATHEXT)
405
+ .split(';')
406
+ .map((value) => value.trim().toLowerCase())
407
+ .filter(Boolean);
408
+
409
+ const preferred = ['.exe', '.com', '.ps1', '.cmd', '.bat'];
410
+ const out: string[] = [];
411
+ for (const ext of [...preferred, ...fromEnv]) {
412
+ const normalized = ext.startsWith('.') ? ext : `.${ext}`;
413
+ if (!out.includes(normalized)) {
414
+ out.push(normalized);
415
+ }
416
+ }
417
+ return out;
418
+ }
419
+
420
+ function resolveWindowsCommandPath(command: string, env: NodeJS.ProcessEnv): string {
421
+ const extensions = getWindowsCommandExtensions(env);
422
+
423
+ const tryCandidates = (basePath: string): string => {
424
+ if (existsSync(basePath)) return basePath;
425
+
426
+ const lower = basePath.toLowerCase();
427
+ for (const ext of extensions) {
428
+ if (lower.endsWith(ext)) continue;
429
+ const candidate = `${basePath}${ext}`;
430
+ if (existsSync(candidate)) {
431
+ return candidate;
432
+ }
433
+ }
434
+
435
+ return '';
436
+ };
437
+
438
+ if (isExplicitCommandPath(command)) {
439
+ return tryCandidates(command);
440
+ }
441
+
442
+ const pathValue = asString(env[getPathEnvKey(env)]);
443
+ for (const entry of splitPathEntries(pathValue)) {
444
+ const resolved = tryCandidates(join(entry, command));
445
+ if (resolved) {
446
+ return resolved;
447
+ }
448
+ }
449
+
450
+ return '';
451
+ }
452
+
453
+ function quoteCmdArgument(value: string): string {
454
+ if (!value) return '""';
455
+
456
+ const escaped = value
457
+ .replace(/([()%!^"<>&|])/g, '^$1')
458
+ .replace(/\r/g, ' ')
459
+ .replace(/\n/g, ' ');
460
+
461
+ return /[\s()%!^"<>&|]/.test(value) ? `"${escaped}"` : escaped;
462
+ }
463
+
464
+ function prepareCommandForSpawn(command: string, args: string[], env: NodeJS.ProcessEnv): { command: string; args: string[] } {
465
+ if (!IS_WIN) {
466
+ return { command, args };
467
+ }
468
+
469
+ const resolved = resolveWindowsCommandPath(command, env);
470
+ if (!resolved) {
471
+ return { command, args };
472
+ }
473
+
474
+ const lower = resolved.toLowerCase();
475
+ if (lower.endsWith('.ps1')) {
476
+ return {
477
+ command: 'powershell.exe',
478
+ args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', resolved, ...args]
479
+ };
480
+ }
481
+
482
+ if (lower.endsWith('.cmd') || lower.endsWith('.bat')) {
483
+ const cmdExe = asString(env.ComSpec || env.COMSPEC, 'cmd.exe');
484
+ const commandLine = [resolved, ...args].map((value) => quoteCmdArgument(value)).join(' ');
485
+ return {
486
+ command: cmdExe,
487
+ args: ['/d', '/s', '/c', commandLine]
488
+ };
489
+ }
490
+
491
+ return { command: resolved, args };
492
+ }
493
+
368
494
  function runCommand(command: string, args: string[], opts: RunOptions = {}): RunResult {
369
495
  try {
370
496
  const mergedEnv: NodeJS.ProcessEnv = { ...process.env, ...(opts.env ?? {}) };
371
- const result = spawnSync(command, args, {
497
+ const prepared = prepareCommandForSpawn(command, args, mergedEnv);
498
+ const result = spawnSync(prepared.command, prepared.args, {
372
499
  cwd: opts.cwd,
373
500
  env: mergedEnv,
374
501
  encoding: 'utf8',
@@ -563,6 +690,122 @@ function applyRecursiveMode(rootDir: string, mode: number): void {
563
690
  });
564
691
  }
565
692
 
693
+ function applyRecursiveModes(rootDir: string, dirMode: number, fileMode: number): void {
694
+ try {
695
+ chmodSync(rootDir, dirMode);
696
+ } catch {
697
+ // ignore
698
+ }
699
+
700
+ visitFiles(rootDir, (fullPath, entry) => {
701
+ try {
702
+ chmodSync(fullPath, entry.isDirectory() ? dirMode : fileMode);
703
+ } catch {
704
+ // ignore
705
+ }
706
+ });
707
+ }
708
+
709
+ type CommandSpec = {
710
+ command: string;
711
+ args: string[];
712
+ env?: Record<string, string | undefined>;
713
+ };
714
+
715
+ function withOptionalSudo(command: string, args: string[], env?: Record<string, string | undefined>): CommandSpec {
716
+ const isRoot = typeof process.getuid === 'function' ? process.getuid() === 0 : false;
717
+ if (isRoot || !hasBin('sudo')) {
718
+ return { command, args, env };
719
+ }
720
+ return { command: 'sudo', args: [command, ...args], env };
721
+ }
722
+
723
+ function getUnixJqInstallPlan(): { name: string; steps: CommandSpec[] } | null {
724
+ if (hasBin('apt-get')) {
725
+ return {
726
+ name: 'apt-get',
727
+ steps: [
728
+ withOptionalSudo('apt-get', ['update', '-qq'], { DEBIAN_FRONTEND: 'noninteractive' }),
729
+ withOptionalSudo('apt-get', ['install', '-y', 'jq'], { DEBIAN_FRONTEND: 'noninteractive' })
730
+ ]
731
+ };
732
+ }
733
+
734
+ if (hasBin('dnf')) {
735
+ return {
736
+ name: 'dnf',
737
+ steps: [withOptionalSudo('dnf', ['install', '-y', 'jq'])]
738
+ };
739
+ }
740
+
741
+ if (hasBin('yum')) {
742
+ return {
743
+ name: 'yum',
744
+ steps: [withOptionalSudo('yum', ['install', '-y', 'jq'])]
745
+ };
746
+ }
747
+
748
+ if (hasBin('pacman')) {
749
+ return {
750
+ name: 'pacman',
751
+ steps: [withOptionalSudo('pacman', ['-Sy', '--noconfirm', 'jq'])]
752
+ };
753
+ }
754
+
755
+ if (hasBin('apk')) {
756
+ return {
757
+ name: 'apk',
758
+ steps: [withOptionalSudo('apk', ['add', 'jq'])]
759
+ };
760
+ }
761
+
762
+ if (hasBin('zypper')) {
763
+ return {
764
+ name: 'zypper',
765
+ steps: [withOptionalSudo('zypper', ['install', '-y', 'jq'])]
766
+ };
767
+ }
768
+
769
+ if (hasBin('brew')) {
770
+ return {
771
+ name: 'brew',
772
+ steps: [{ command: 'brew', args: ['install', 'jq'] }]
773
+ };
774
+ }
775
+
776
+ return null;
777
+ }
778
+
779
+ function ensureUnixJq(api: LoggerApi): boolean {
780
+ const plan = getUnixJqInstallPlan();
781
+ if (!plan) {
782
+ return false;
783
+ }
784
+
785
+ api.logger.info(`[moltbank] jq not found — attempting install via ${plan.name}...`);
786
+
787
+ for (const step of plan.steps) {
788
+ const result = runCommand(step.command, step.args, {
789
+ env: step.env,
790
+ timeoutMs: 180000
791
+ });
792
+
793
+ if (!result.ok) {
794
+ api.logger.warn(`[moltbank] jq install via ${plan.name} failed: ${result.stderr}`);
795
+ return false;
796
+ }
797
+ }
798
+
799
+ if (!hasBin('jq')) {
800
+ api.logger.warn(`[moltbank] jq install via ${plan.name} finished, but jq is still not visible in PATH`);
801
+ return false;
802
+ }
803
+
804
+ const version = runCommand('jq', ['--version'], { silent: true });
805
+ api.logger.info(`[moltbank] ✓ jq installed via ${plan.name} (${version.stdout || 'unknown version'})`);
806
+ return true;
807
+ }
808
+
566
809
  function findWindowsJqBinaryPath(): string {
567
810
  if (!IS_WIN) return '';
568
811
 
@@ -652,6 +895,7 @@ function ensureWindowsJqShim(api: LoggerApi): boolean {
652
895
  const shimBody = `@echo off\r\n"${escapedSource}" %*\r\n`;
653
896
  writeFileSync(shimPath, shimBody, 'utf8');
654
897
  }
898
+ prependPathEntries(process.env, [npmBinDir]);
655
899
  } catch (e) {
656
900
  api.logger.warn('[moltbank] could not create jq shim in npm global bin: ' + String(e));
657
901
  return false;
@@ -709,6 +953,77 @@ function getCredentialsPath(): string {
709
953
  return process.env.MOLTBANK_CREDENTIALS_PATH || join(homedir(), '.MoltBank', 'credentials.json');
710
954
  }
711
955
 
956
+ function getSandboxCredentialsHostPath(skillDir: string): string {
957
+ return join(skillDir, '.sandbox', 'credentials.json');
958
+ }
959
+
960
+ function getSandboxCredentialsContainerPath(skillName: string): string {
961
+ return `/workspace/skills/${skillName}/.sandbox/credentials.json`;
962
+ }
963
+
964
+ function writeSandboxCredentialsFile(
965
+ skillDir: string,
966
+ activeOrg: string,
967
+ org: CredentialsOrganization,
968
+ privateKey: string,
969
+ api: LoggerApi
970
+ ): boolean {
971
+ const sandboxDir = dirname(getSandboxCredentialsHostPath(skillDir));
972
+ const sandboxCredentialsPath = getSandboxCredentialsHostPath(skillDir);
973
+ const sandboxOrg: Record<string, unknown> = {
974
+ name: activeOrg,
975
+ access_token: org.access_token ?? ''
976
+ };
977
+
978
+ if (privateKey) {
979
+ sandboxOrg.x402_signer_private_key = privateKey;
980
+ }
981
+ if (isRecord(org.x402_wallet)) {
982
+ sandboxOrg.x402_wallet = org.x402_wallet;
983
+ }
984
+
985
+ const sandboxCredentials = {
986
+ active_organization: activeOrg,
987
+ organizations: [sandboxOrg]
988
+ };
989
+
990
+ try {
991
+ mkdirSync(sandboxDir, { recursive: true });
992
+ writeFileSync(sandboxCredentialsPath, JSON.stringify(sandboxCredentials, null, 2) + '\n', 'utf8');
993
+
994
+ if (!IS_WIN) {
995
+ try {
996
+ chmodSync(sandboxDir, 0o700);
997
+ } catch {
998
+ // ignore
999
+ }
1000
+ try {
1001
+ chmodSync(sandboxCredentialsPath, 0o600);
1002
+ } catch {
1003
+ // ignore
1004
+ }
1005
+ }
1006
+
1007
+ api.logger.info('[moltbank] ✓ sandbox credentials file updated');
1008
+ return true;
1009
+ } catch (e) {
1010
+ api.logger.warn('[moltbank] ✗ could not write sandbox credentials file: ' + String(e));
1011
+ return false;
1012
+ }
1013
+ }
1014
+
1015
+ function removeSandboxSecretEnvKeys(envObj: Record<string, string>, api: LoggerApi): boolean {
1016
+ let changed = false;
1017
+ for (const key of ['MOLTBANK', 'SIGNER']) {
1018
+ if (key in envObj) {
1019
+ delete envObj[key];
1020
+ changed = true;
1021
+ api.logger.info(`[moltbank] ✓ removed legacy sandbox secret env key ${key}`);
1022
+ }
1023
+ }
1024
+ return changed;
1025
+ }
1026
+
712
1027
  function readOpenclawConfig(): OpenclawConfig {
713
1028
  const configPath = join(homedir(), '.openclaw', 'openclaw.json');
714
1029
  try {
@@ -816,6 +1131,10 @@ function ensureJq(api: LoggerApi) {
816
1131
  }
817
1132
 
818
1133
  if (!IS_WIN) {
1134
+ if (ensureUnixJq(api)) {
1135
+ return;
1136
+ }
1137
+
819
1138
  api.logger.warn('[moltbank] ✗ jq not found in PATH (required by MoltBank wrapper on host mode)');
820
1139
  api.logger.warn(`[moltbank] install jq (${getJqInstallHint()}) and rerun setup`);
821
1140
  return;
@@ -1111,20 +1430,20 @@ function ensureSkillPermissions(skillDir: string, api: LoggerApi) {
1111
1430
  }
1112
1431
 
1113
1432
  if (existsSync(assetsDir)) {
1114
- applyRecursiveMode(assetsDir, 0o777);
1115
- api.logger.info('[moltbank] ✓ assets/ permissions → 777');
1433
+ applyRecursiveModes(assetsDir, 0o750, 0o640);
1434
+ api.logger.info('[moltbank] ✓ assets/ permissions → 750 dirs, 640 files');
1116
1435
  }
1117
1436
  if (existsSync(configFile)) {
1118
1437
  try {
1119
- chmodSync(configFile, 0o666);
1438
+ chmodSync(configFile, 0o600);
1120
1439
  } catch {
1121
1440
  // ignore
1122
1441
  }
1123
- api.logger.info('[moltbank] ✓ mcporter.json permissions → 666');
1442
+ api.logger.info('[moltbank] ✓ mcporter.json permissions → 600');
1124
1443
  }
1125
1444
  if (existsSync(scriptsDir)) {
1126
- applyRecursiveMode(scriptsDir, 0o755);
1127
- api.logger.info('[moltbank] ✓ scripts/ permissions → 755');
1445
+ applyRecursiveModes(scriptsDir, 0o750, 0o750);
1446
+ api.logger.info('[moltbank] ✓ scripts/ permissions → 750');
1128
1447
  normalizeFilesMatching(scriptsDir, () => true);
1129
1448
  api.logger.info('[moltbank] ✓ scripts/ line endings normalized (CRLF → LF)');
1130
1449
  }
@@ -1659,6 +1978,7 @@ export function ensureMcporterConfig(skillDir: string, appBaseUrl: string, api:
1659
1978
 
1660
1979
  export function injectSandboxEnv(skillDir: string, api: LoggerApi): boolean {
1661
1980
  const credsPath = getCredentialsPath();
1981
+ const skillName = basename(skillDir);
1662
1982
 
1663
1983
  if (!existsSync(credsPath)) {
1664
1984
  api.logger.info('[moltbank] no credentials.json found — skipping sandbox env injection');
@@ -1686,14 +2006,7 @@ export function injectSandboxEnv(skillDir: string, api: LoggerApi): boolean {
1686
2006
  const envPath = ['agents', 'defaults', 'sandbox', 'docker', 'env'];
1687
2007
  setNestedValue(config, envPath, getNestedValue(config, envPath) ?? {});
1688
2008
  const envObj = getNestedValue(config, envPath) as Record<string, string>;
1689
-
1690
- if (envObj.MOLTBANK !== org.access_token) {
1691
- envObj.MOLTBANK = org.access_token;
1692
- api.logger.info(`[moltbank] ✓ MOLTBANK injected (org: ${activeOrg})`);
1693
- changed = true;
1694
- } else {
1695
- api.logger.info(`[moltbank] ✓ MOLTBANK already injected (org: ${activeOrg})`);
1696
- }
2009
+ changed = removeSandboxSecretEnvKeys(envObj, api) || changed;
1697
2010
 
1698
2011
  if (envObj.ACTIVE_ORG_OVERRIDE !== activeOrg) {
1699
2012
  envObj.ACTIVE_ORG_OVERRIDE = activeOrg;
@@ -1729,21 +2042,28 @@ export function injectSandboxEnv(skillDir: string, api: LoggerApi): boolean {
1729
2042
  }
1730
2043
  }
1731
2044
 
2045
+ if (!writeSandboxCredentialsFile(skillDir, activeOrg, org, privateKey, api)) {
2046
+ return false;
2047
+ }
2048
+
2049
+ const sandboxCredentialsContainerPath = getSandboxCredentialsContainerPath(skillName);
2050
+ if (envObj.MOLTBANK_CREDENTIALS_PATH !== sandboxCredentialsContainerPath) {
2051
+ envObj.MOLTBANK_CREDENTIALS_PATH = sandboxCredentialsContainerPath;
2052
+ api.logger.info(`[moltbank] ✓ MOLTBANK_CREDENTIALS_PATH injected (${sandboxCredentialsContainerPath})`);
2053
+ changed = true;
2054
+ } else {
2055
+ api.logger.info(`[moltbank] ✓ MOLTBANK_CREDENTIALS_PATH already set (${sandboxCredentialsContainerPath})`);
2056
+ }
2057
+
1732
2058
  if (privateKey) {
1733
- if (envObj.SIGNER !== privateKey) {
1734
- envObj.SIGNER = privateKey;
1735
- api.logger.info('[moltbank] ✓ SIGNER injected');
1736
- changed = true;
1737
- } else {
1738
- api.logger.info('[moltbank] ✓ SIGNER already injected');
1739
- }
2059
+ api.logger.info('[moltbank] sandbox x402 signer available via sandbox credentials file');
1740
2060
  } else {
1741
- api.logger.info('[moltbank] ℹ SIGNER not available — skipped (agent will handle on first use)');
2061
+ api.logger.info('[moltbank] ℹ sandbox x402 signer not available yet helper scripts will use sandbox credentials path if generated later');
1742
2062
  }
1743
2063
 
1744
2064
  if (changed) {
1745
2065
  writeOpenclawConfig(config);
1746
- api.logger.info('[moltbank] ✓ openclaw.json updated with sandbox env vars');
2066
+ api.logger.info('[moltbank] ✓ openclaw.json updated with sandbox auth path only (no token/private key persisted)');
1747
2067
  }
1748
2068
 
1749
2069
  return changed;
@@ -1965,7 +2285,7 @@ export async function runSetup(
1965
2285
  api.logger.info('[moltbank] [sandbox 8/10] configuring sandbox docker settings...');
1966
2286
  const sandboxChanged = configureSandbox(api);
1967
2287
 
1968
- api.logger.info('[moltbank] [sandbox 9/10] injecting sandbox env vars (MOLTBANK, ACTIVE_ORG_OVERRIDE, SIGNER)...');
2288
+ api.logger.info('[moltbank] [sandbox 9/10] injecting sandbox auth env (MOLTBANK_CREDENTIALS_PATH, ACTIVE_ORG_OVERRIDE)...');
1969
2289
  const envChanged = injectSandboxEnv(skillDir, api);
1970
2290
 
1971
2291
  api.logger.info('[moltbank] [sandbox 10/10] apply sandbox changes (recreate + gateway stop)...');
@@ -2132,13 +2452,18 @@ export async function runSetup(
2132
2452
  const finalEnv = asStringRecord(getNestedValue(finalConfig, ['agents', 'defaults', 'sandbox', 'docker', 'env']));
2133
2453
  const finalNetwork = asString(getNestedValue(finalConfig, ['agents', 'defaults', 'sandbox', 'docker', 'network']));
2134
2454
  const finalCmd = asString(getNestedValue(finalConfig, ['agents', 'defaults', 'sandbox', 'docker', 'setupCommand']));
2455
+ const sandboxCredentialsPath = getSandboxCredentialsHostPath(skillDir);
2135
2456
 
2136
2457
  api.logger.info(`[moltbank] openclaw.json sandbox env:`);
2137
- api.logger.info(`[moltbank] MOLTBANK: ${finalEnv.MOLTBANK ? '✓ set' : '✗ missing'}`);
2458
+ api.logger.info(
2459
+ `[moltbank] MOLTBANK_CREDENTIALS_PATH: ${finalEnv.MOLTBANK_CREDENTIALS_PATH ? `✓ ${finalEnv.MOLTBANK_CREDENTIALS_PATH}` : '✗ missing'}`
2460
+ );
2138
2461
  api.logger.info(
2139
2462
  `[moltbank] ACTIVE_ORG_OVERRIDE: ${finalEnv.ACTIVE_ORG_OVERRIDE ? `✓ "${finalEnv.ACTIVE_ORG_OVERRIDE}"` : '✗ missing'}`
2140
2463
  );
2141
- api.logger.info(`[moltbank] SIGNER: ${finalEnv.SIGNER ? '✓ set' : '✗ missing (agent may generate on first use)'}`);
2464
+ api.logger.info(
2465
+ `[moltbank] sandbox credentials: ${existsSync(sandboxCredentialsPath) ? '✓ present (skill-local, 600)' : '✗ missing'}`
2466
+ );
2142
2467
  api.logger.info(`[moltbank] sandbox docker:`);
2143
2468
  api.logger.info(
2144
2469
  `[moltbank] network: ${finalNetwork === 'bridge' ? '✓ bridge' : `✗ "${finalNetwork}" (expected bridge)`}`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moltbankhq/openclaw",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "MoltBank stablecoin treasury CLI and OpenClaw plugin",
5
5
  "main": "index.ts",
6
6
  "bin": {