@xcanwin/manyoyo 8.2.1 → 8.3.0

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/README.en.md ADDED
@@ -0,0 +1,88 @@
1
+ <p align="center">
2
+ <img src="./assets/manyoyo-logo-09-cyberpunk-terminal.svg" alt="MANYOYO logo" width="560" />
3
+ </p>
4
+
5
+ # <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO</a></p>
6
+ <p align="center"><b>MANYOYO – open-source sandbox for running AI coding agents (Claude Code / Codex / Gemini) safely in Docker/Podman</b></p>
7
+ <p align="center">Run Claude Code / Codex / Gemini / OpenCode in YOLO mode inside a container: let the AI go wild without hurting your computer.</p>
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/@xcanwin/manyoyo"><img alt="npm" src="https://img.shields.io/npm/v/@xcanwin/manyoyo?style=flat-square" /></a>
10
+ <a href="https://github.com/xcanwin/manyoyo/actions/workflows/npm-publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/xcanwin/manyoyo/npm-publish.yml?style=flat-square" /></a>
11
+ <a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="README.md">中文</a> |
16
+ <a href="README.en.md"><b>English</b></a>
17
+ </p>
18
+ <p align="center">
19
+ Docs: <a href="https://xcanwin.github.io/manyoyo/en/">https://xcanwin.github.io/manyoyo/en/</a>
20
+ </p>
21
+
22
+ ---
23
+
24
+ ## Install
25
+
26
+ ### macOS / Linux (recommended)
27
+
28
+ ```bash
29
+ curl -fsSL https://github.com/xcanwin/manyoyo/raw/main/scripts/install.sh | sh
30
+ ```
31
+
32
+ When it finishes, follow the on-screen prompts to finish setup. macOS needs nothing installed first. Linux: if the container runtime is missing, the installer explains why and, with your consent, installs it with sudo.
33
+
34
+ ### Already have Node.js and Docker/Podman
35
+
36
+ ```bash
37
+ npm install -g @xcanwin/manyoyo
38
+ manyoyo
39
+ ```
40
+
41
+ ## Daily use
42
+
43
+ ```bash
44
+ manyoyo # open the web UI
45
+ manyoyo update # upgrade
46
+ manyoyo uninstall # uninstall (config and data are kept by default)
47
+ ```
48
+
49
+ ## Command line (optional)
50
+
51
+ ```bash
52
+ manyoyo init all # import the agent configs already on this machine
53
+ manyoyo run -y c # start Claude Code in YOLO mode inside the sandbox
54
+ ```
55
+
56
+ The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.1.0-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
57
+
58
+ ## What it does
59
+
60
+ - **Multiple agents**: `claude`, `codex`, `gemini`, `opencode` behind one entry point
61
+ - **Container isolation**: Docker / Podman; the agent only sees the work directory you give it
62
+ - **Web UI**: chat, browse files and open a terminal in the browser, phone friendly
63
+ - **One config**: run profiles, environment variables, mounts and image arguments in `~/.manyoyo/manyoyo.json`
64
+ - **One command** to install, upgrade and uninstall
65
+
66
+ | | Running the agent CLI directly | MANYOYO |
67
+ | --- | --- | --- |
68
+ | Host exposure | High | Lower |
69
+ | Run boundary | Scattered | Centralised in the container and config |
70
+ | Reproducible environment | Weak | Strong (image + config) |
71
+ | High-risk mode warnings | Left to the tool | Explicit YOLO / SOLO / sock warnings |
72
+
73
+ ## Security notes
74
+
75
+ MANYOYO reduces risk but is not "absolutely safe": the main isolation is a container, not a virtual machine; `YOLO / SOLO` can still run dangerous commands; `sock` mode exposes the host Docker socket; set a strong password before listening on a public address. See [Security notes](https://xcanwin.github.io/manyoyo/en/guide/security).
76
+
77
+ ## Docs
78
+
79
+ - 中文: <https://xcanwin.github.io/manyoyo/>
80
+ - English: <https://xcanwin.github.io/manyoyo/en/> ([Install](https://xcanwin.github.io/manyoyo/en/guide/quick-start), [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options), [Configuration](https://xcanwin.github.io/manyoyo/en/configuration/), [FAQ](https://xcanwin.github.io/manyoyo/en/troubleshooting/))
81
+
82
+ ## License
83
+
84
+ MIT
85
+
86
+ ## Contributing
87
+
88
+ [Issues](https://github.com/xcanwin/manyoyo/issues) and pull requests are welcome.
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  </p>
4
4
 
5
5
  # <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO(慢悠悠)</a></p>
6
- <p align="center">让 Claude Code / Codex / Gemini / OpenCode 在容器里放心跑 YOLO 模式:AI 随便折腾,伤不到你的电脑。</p>
6
+ <p align="center"><b>MANYOYO – open-source sandbox for running AI coding agents (Claude Code / Codex / Gemini) safely in Docker/Podman</b></p>
7
+ <p align="center">开源 AI Agent 沙箱:让 Claude Code / Codex / Gemini / OpenCode 在容器里放心跑 YOLO 模式,AI 随便折腾,伤不到你的电脑。</p>
7
8
  <p align="center">
8
9
  <a href="https://www.npmjs.com/package/@xcanwin/manyoyo"><img alt="npm" src="https://img.shields.io/npm/v/@xcanwin/manyoyo?style=flat-square" /></a>
9
10
  <a href="https://github.com/xcanwin/manyoyo/actions/workflows/npm-publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/xcanwin/manyoyo/npm-publish.yml?style=flat-square" /></a>
@@ -12,7 +13,7 @@
12
13
 
13
14
  <p align="center">
14
15
  <a href="README.md"><b>中文</b></a> |
15
- <a href="https://xcanwin.github.io/manyoyo/en/">English</a>
16
+ <a href="README.en.md">English</a>
16
17
  </p>
17
18
  <p align="center">
18
19
  文档:<a href="https://xcanwin.github.io/manyoyo/">https://xcanwin.github.io/manyoyo/</a>
@@ -28,7 +29,7 @@
28
29
  curl -fsSL https://github.com/xcanwin/manyoyo/raw/main/scripts/install.sh | sh
29
30
  ```
30
31
 
31
- 装完浏览器自动打开,按向导选 Agent、填 Key 就能开始对话。macOS 不需要预装任何东西;Linux 需要已有 Docker 或 Podman。
32
+ 装完按屏幕提示完成配置。macOS 不需要预装任何东西。Linux:缺少容器环境时,安装器会说明原因,征得你同意后用 sudo 自动安装。
32
33
 
33
34
  ### 已有 Node.js 和 Docker/Podman
34
35
 
package/bin/manyoyo.js CHANGED
@@ -9,7 +9,7 @@ const net = require('net');
9
9
  const readline = require('readline');
10
10
  const { Command, Help } = require('commander');
11
11
  const { startWebServer } = require('../lib/web/server');
12
- const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
12
+ const { buildContainerRunArgs, buildContainerRunCommand, runWithEnvFile } = require('../lib/container-run');
13
13
  const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
14
14
  const { resolveUpdateImageVersion } = require('../lib/image-version-policy');
15
15
  const { selectContainerRuntime, mergeRuntimeEnv } = require('../lib/container-runtime');
@@ -47,6 +47,7 @@ const { applyAptMirror } = require('../lib/mirrors');
47
47
  const {
48
48
  sanitizeSensitiveData,
49
49
  sanitizeServeLogText,
50
+ redactCommandArgs,
50
51
  formatServeLogValue,
51
52
  getServeProcessSnapshot
52
53
  } = require('../lib/serve-log');
@@ -728,6 +729,18 @@ async function ensureRunImage(runtime) {
728
729
  });
729
730
  }
730
731
 
732
+ // 把 run 失败的 stderr 尾部(已打码)与可执行的修复提示拼进错误信息,而不是只剩一行 Command failed
733
+ function explainCreateFailure(err) {
734
+ const raw = getCommandFailureText(err);
735
+ const tail = sanitizeServeLogText(String(err && err.stderr || '')).trim().split('\n').slice(-8).join('\n');
736
+ const info = describeError(raw, { command: DOCKER_CMD });
737
+ const parts = [err.message];
738
+ if (tail) parts.push(tail);
739
+ if (info && info.code === 'ROOTLESS_NETWORK_MISSING') parts.push(`提示: ${info.reason}\n${info.action}`);
740
+ err.message = parts.join('\n');
741
+ return err;
742
+ }
743
+
731
744
  function showImagePullHint(err) {
732
745
  const info = describeError(getCommandFailureText(err), { imageRef: `${IMAGE_NAME}:${IMAGE_VERSION}` });
733
746
  if (!info || !['IMAGE_PULL_FAILED', 'IMAGE_NOT_FOUND'].includes(info.code)) {
@@ -753,7 +766,7 @@ function runCmd(cmd, args, options = {}) {
753
766
  if (options.ignoreError) {
754
767
  return result.stdout || '';
755
768
  }
756
- const err = new Error(`Command failed: ${cmd} ${args.join(' ')}`);
769
+ const err = new Error(`Command failed: ${cmd} ${redactCommandArgs(args).join(' ')}`);
757
770
  err.stdout = result.stdout;
758
771
  err.stderr = result.stderr;
759
772
  err.status = result.status;
@@ -1473,6 +1486,9 @@ https://github.com/xcanwin/manyoyo
1473
1486
  await runAppLauncher({ force: launcherArgs.force });
1474
1487
  }
1475
1488
 
1489
+ // 顶层 -V 与 -v 等价(commander 只允许一个短选项;子命令里的 -v 仍是 --volume,不受影响)
1490
+ if (process.argv.length === 3 && process.argv[2] === '-V') process.argv[2] = '-v';
1491
+
1476
1492
  // Pre-handle -x/--shell-full: treat all following args as a single command
1477
1493
  normalizeShellFullArgv(process.argv);
1478
1494
  normalizeWorktreeArgv(process.argv);
@@ -1581,8 +1597,10 @@ https://github.com/xcanwin/manyoyo
1581
1597
  };
1582
1598
  }
1583
1599
 
1584
- // Load run config if specified
1585
- const runConfig = options.run ? loadRunConfig(options.run, config) : {};
1600
+ // Load run config if specified;doctor 不带 -r 且 runs 里只有一个配置时,按它检查
1601
+ const runsMap = config.runs && typeof config.runs === 'object' && !Array.isArray(config.runs) ? Object.keys(config.runs) : [];
1602
+ const runNameToLoad = options.run || (selectedAction === 'doctor' && runsMap.length === 1 ? runsMap[0] : '');
1603
+ const runConfig = runNameToLoad ? loadRunConfig(runNameToLoad, config) : {};
1586
1604
  const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
1587
1605
  const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
1588
1606
 
@@ -2175,10 +2193,12 @@ async function createNewContainer(runtime) {
2175
2193
  // 使用数组参数执行命令(安全方式)
2176
2194
  try {
2177
2195
  const args = buildDockerRunArgs(runtime);
2178
- dockerExecArgs(args, { stdio: 'pipe' });
2196
+ runWithEnvFile(args, path.join(os.homedir(), '.manyoyo', 'tmp'), finalArgs => dockerExecArgs(finalArgs, { stdio: 'pipe' }));
2179
2197
  } catch (e) {
2180
2198
  showImagePullHint(e);
2181
- throw e;
2199
+ // 失败的 run 可能留下 Created 状态的容器;这个名字创建前不存在,所以只清理本次留下的这一个
2200
+ try { dockerExecArgs(['rm', '-f', runtime.containerName], { stdio: 'pipe' }); } catch (cleanupError) { /* 尽力清理 */ }
2201
+ throw explainCreateFailure(e);
2182
2202
  }
2183
2203
 
2184
2204
  // Wait for container to be ready
@@ -2223,7 +2243,7 @@ function buildDockerRunArgs(runtime) {
2223
2243
  * @returns {string} 命令字符串
2224
2244
  */
2225
2245
  function buildDockerRunCmd(runtime) {
2226
- const args = buildDockerRunArgs(runtime);
2246
+ const args = redactCommandArgs(buildDockerRunArgs(runtime));
2227
2247
  return buildContainerRunCommand(DOCKER_CMD, args);
2228
2248
  }
2229
2249
 
@@ -1,4 +1,9 @@
1
1
  {
2
+ "attribution": {
3
+ "commit": "",
4
+ "pr": "",
5
+ "sessionUrl": false
6
+ },
2
7
  "statusLine": {
3
8
  "type": "command",
4
9
  "command": "bash /root/.claude/statusline.sh"
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
3
6
  const { buildMirrorEnvArgs } = require('./mirrors');
4
7
 
5
8
  function sanitizeDefaultCommand(defaultCommand) {
@@ -26,6 +29,38 @@ function buildContainerRunArgs(options) {
26
29
  return args;
27
30
  }
28
31
 
32
+ /**
33
+ * 把命令行里的 `--env KEY=值` 挪进 0600 的临时 env 文件,改用 `--env-file` 传入,
34
+ * 避免密钥出现在 ps / /proc 与错误信息里;exec 返回(或抛错)后立即删除文件。
35
+ * 没有 KEY=值 形式的 env 时原样执行。
36
+ */
37
+ function runWithEnvFile(args, dir, exec) {
38
+ const rest = [];
39
+ const lines = [];
40
+ let envFileIndex = -1;
41
+ for (let i = 0; i < args.length; i++) {
42
+ if (args[i] === '--env' && /^[^=]+=/.test(String(args[i + 1] ?? ''))) {
43
+ if (envFileIndex < 0) envFileIndex = rest.length;
44
+ lines.push(args[i + 1]);
45
+ i += 1;
46
+ } else {
47
+ rest.push(args[i]);
48
+ }
49
+ }
50
+ if (!lines.length) return exec(args);
51
+
52
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
53
+ fs.chmodSync(dir, 0o700);
54
+ const file = path.join(dir, `env-${process.pid}-${crypto.randomBytes(4).toString('hex')}`);
55
+ fs.writeFileSync(file, `${lines.join('\n')}\n`, { mode: 0o600, flag: 'wx' });
56
+ rest.splice(envFileIndex, 0, '--env-file', file);
57
+ try {
58
+ return exec(rest);
59
+ } finally {
60
+ fs.rmSync(file, { force: true });
61
+ }
62
+ }
63
+
29
64
  function quoteShellArg(value) {
30
65
  const text = String(value);
31
66
  if (text.includes(' ') || text.includes('"') || text.includes('=')) {
@@ -40,5 +75,6 @@ function buildContainerRunCommand(dockerCmd, args) {
40
75
 
41
76
  module.exports = {
42
77
  buildContainerRunArgs,
78
+ runWithEnvFile,
43
79
  buildContainerRunCommand
44
80
  };
package/lib/doctor.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { resolveContainerMode } = require('./container-modes');
4
4
  const { resolveAgentProgram } = require('./agent-resume');
5
+ const { describeError } = require('./error-hints');
6
+ const { checkRootlessNetwork } = require('./rootless-network');
5
7
 
6
8
  function createCheck(code, status, summary, action = '', detail = '') {
7
9
  return { code, status, summary, action, detail };
@@ -9,6 +11,8 @@ function createCheck(code, status, summary, action = '', detail = '') {
9
11
 
10
12
  // 卡死的 daemon(如无响应的 Docker Desktop)不能让诊断无限挂起
11
13
  const COMMAND_TIMEOUT_MS = 10000;
14
+ // 真实启动一次容器(含网络配置),比 info 慢
15
+ const START_TEST_TIMEOUT_MS = 60000;
12
16
 
13
17
  // runCommand 可以是同步或异步执行器;serve 里传异步的,避免阻塞事件循环
14
18
  async function runDoctorChecks(options = {}) {
@@ -45,13 +49,37 @@ async function runDoctorChecks(options = {}) {
45
49
  } catch (error) {
46
50
  checks.push(createCheck('DAEMON_UNAVAILABLE', 'error', `${runtimeCommand} daemon 不可用`, `启动 ${runtimeCommand} daemon 后重试。`, error.message || ''));
47
51
  }
52
+ const network = await checkRootlessNetwork(runCommand, { command: runtimeCommand, env: runtimeEnv, timeout: COMMAND_TIMEOUT_MS, platform: options.platform });
53
+ if (network && network.missing) {
54
+ checks.push(createCheck('ROOTLESS_NETWORK_MISSING', 'error', network.reason, network.action));
55
+ } else if (network) {
56
+ checks.push(createCheck('ROOTLESS_NETWORK_OK', 'ok', `rootless 网络组件可用: ${network.component}`));
57
+ }
58
+ const image = `${options.imageName || ''}:${options.imageVersion || ''}`;
59
+ let imageAvailable = false;
48
60
  try {
49
- const image = `${options.imageName || ''}:${options.imageVersion || ''}`;
50
61
  const detail = String(await runCommand(runtimeCommand, ['image', 'inspect', image], { env: runtimeEnv, timeout: COMMAND_TIMEOUT_MS }) || '').trim();
51
62
  checks.push(createCheck('IMAGE_AVAILABLE', 'ok', `镜像可用: ${image}`, '', detail));
63
+ imageAvailable = true;
52
64
  } catch (error) {
53
65
  checks.push(createCheck('IMAGE_MISSING', 'warning', '目标镜像尚不可用', '执行 manyoyo build,或拉取匹配镜像。', error.message || ''));
54
66
  }
67
+ // 镜像在、网络组件也没查出问题时,真实启动一次:info 正常不代表容器能起来
68
+ if (imageAvailable && !(network && network.missing)) {
69
+ try {
70
+ await runCommand(runtimeCommand, ['run', '--rm', '--entrypoint', '', image, 'true'], { env: runtimeEnv, timeout: START_TEST_TIMEOUT_MS });
71
+ checks.push(createCheck('CONTAINER_START_OK', 'ok', '容器可以正常启动'));
72
+ } catch (error) {
73
+ const raw = `${error.message || ''}\n${error.stderr || ''}`;
74
+ const info = describeError(raw, { command: runtimeCommand, imageRef: image });
75
+ checks.push(createCheck(
76
+ 'CONTAINER_START_FAILED', 'error',
77
+ info ? info.reason : '容器无法启动',
78
+ info ? info.action : `手动执行 ${runtimeCommand} run --rm ${image} true 查看原因。`,
79
+ String(error.stderr || error.message || '').trim().split('\n').slice(-5).join('\n')
80
+ ));
81
+ }
82
+ }
55
83
  }
56
84
 
57
85
  checks.push(options.configExists === true
@@ -71,9 +99,13 @@ async function runDoctorChecks(options = {}) {
71
99
  }
72
100
 
73
101
  const pluginConfig = options.pluginConfig;
74
- checks.push(pluginConfig && typeof pluginConfig === 'object' && !Array.isArray(pluginConfig)
75
- ? createCheck('PLUGIN_CONFIG_VALID', 'ok', '插件配置有效')
76
- : createCheck('PLUGIN_CONFIG_INVALID', 'warning', '插件配置不存在或格式无效', '将 plugins 配置为对象(map)。'));
102
+ if (pluginConfig === undefined || pluginConfig === null) {
103
+ checks.push(createCheck('PLUGIN_CONFIG_VALID', 'ok', '未配置插件'));
104
+ } else {
105
+ checks.push(typeof pluginConfig === 'object' && !Array.isArray(pluginConfig)
106
+ ? createCheck('PLUGIN_CONFIG_VALID', 'ok', '插件配置有效')
107
+ : createCheck('PLUGIN_CONFIG_INVALID', 'warning', '插件配置格式无效', '将 plugins 配置为对象(map)。'));
108
+ }
77
109
 
78
110
  if (options.portStatus === 'available') {
79
111
  checks.push(createCheck('PORT_AVAILABLE', 'ok', '监听端口可用'));
@@ -1,7 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ const { describeMissing } = require('./rootless-network');
4
+
3
5
  // 原始运行时/系统错误 -> “原因 + 下一步”。新增映射只改这里。
4
6
  const RULES = [
7
+ {
8
+ code: 'ROOTLESS_NETWORK_MISSING',
9
+ pattern: /could not find (slirp4netns|pasta)/i,
10
+ describe: (ctx, match) => describeMissing(match[1].toLowerCase(), ctx)
11
+ },
5
12
  {
6
13
  code: 'PODMAN_MACHINE_UNAVAILABLE',
7
14
  pattern: /Cannot connect to Podman|unable to connect to Podman socket|podman machine start|podman system connection/i,
@@ -78,8 +85,9 @@ function describeError(text, context = {}) {
78
85
  const raw = String(text || '');
79
86
  const ctx = { command: 'docker', imageRef: '', port: '', ...context };
80
87
  for (const rule of RULES) {
81
- if (rule.pattern.test(raw)) {
82
- return { code: rule.code, ...rule.describe(ctx) };
88
+ const match = rule.pattern.exec(raw);
89
+ if (match) {
90
+ return { code: rule.code, ...rule.describe(ctx, match) };
83
91
  }
84
92
  }
85
93
  return null;
@@ -235,11 +235,26 @@ function formatPropertyKey(key) {
235
235
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
236
236
  }
237
237
 
238
+ // 某个位置所在行的缩进
239
+ function lineIndentAt(text, index) {
240
+ const lineStart = text.lastIndexOf('\n', index - 1) + 1;
241
+ return /^[ \t]*/.exec(text.slice(lineStart))[0];
242
+ }
243
+
244
+ // 多行值(JSON.stringify 的结果)的后续行补上插入位置的缩进,否则内层会顶格
245
+ function reindent(valueText, indent) {
246
+ return String(valueText).replace(/\n/g, `\n${indent}`);
247
+ }
248
+
238
249
  // 永远只在对象的 '{' 后面插入,不管这个对象原来是空的还是已经有属性——
239
250
  // 不用去判断"是不是最后一个属性、要不要补逗号",语法上总是安全的
240
251
  function insertObjectProperty(text, objectStartIndex, key, valueText) {
241
- const prefix = `\n ${formatPropertyKey(key)}: `;
242
- const insertion = `${prefix}${valueText},`;
252
+ const objectIndent = lineIndentAt(text, objectStartIndex);
253
+ const childIndent = `${objectIndent} `;
254
+ const prefix = `\n${childIndent}${formatPropertyKey(key)}: `;
255
+ // 空对象:闭合的 } 另起一行,不要贴在逗号后面
256
+ const closesRightAfter = text[skipTrivia(text, objectStartIndex + 1)] === '}';
257
+ const insertion = `${prefix}${reindent(valueText, childIndent)},${closesRightAfter ? `\n${objectIndent}` : ''}`;
243
258
  const insertPos = objectStartIndex + 1;
244
259
  const nextText = `${text.slice(0, insertPos)}${insertion}${text.slice(insertPos)}`;
245
260
  return { text: nextText, valueStart: insertPos + prefix.length };
@@ -269,7 +284,7 @@ function upsertValueByPath(text, pathParts, valueText) {
269
284
  if (existingRange) {
270
285
  if (isLast) {
271
286
  return applyTextReplacements(currentText, [
272
- { start: existingRange.start, end: existingRange.end, text: valueText }
287
+ { start: existingRange.start, end: existingRange.end, text: reindent(valueText, lineIndentAt(currentText, existingRange.start)) }
273
288
  ]);
274
289
  }
275
290
  const nextObjectStart = skipTrivia(currentText, existingRange.start);
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ // rootless podman 的默认网络组件:4.x 用 slirp4netns,5.x 起用 pasta
6
+ const PACKAGE_OF = { slirp4netns: 'slirp4netns', pasta: 'passt' };
7
+
8
+ function parsePodmanMajor(versionText) {
9
+ const match = /(\d+)\.\d+/.exec(String(versionText || ''));
10
+ return match ? Number(match[1]) : null;
11
+ }
12
+
13
+ function defaultNetworkComponent(major) {
14
+ return major !== null && major >= 5 ? 'pasta' : 'slirp4netns';
15
+ }
16
+
17
+ function readOsRelease() {
18
+ try {
19
+ return fs.readFileSync('/etc/os-release', 'utf8');
20
+ } catch (e) {
21
+ return '';
22
+ }
23
+ }
24
+
25
+ function hasCommand(name) {
26
+ return String(process.env.PATH || '').split(':').some(dir => dir && fs.existsSync(`${dir}/${name}`));
27
+ }
28
+
29
+ // 与 scripts/offline/install.sh 的 Linux 分支保持同一套规则;不认识的发行版返回 null(只给说明)
30
+ function installCommand(component, { osRelease = readOsRelease(), hasDnf = hasCommand('dnf') } = {}) {
31
+ const pkg = PACKAGE_OF[component] || component;
32
+ const ids = (String(osRelease).match(/^(?:ID|ID_LIKE)=(.*)$/gm) || [])
33
+ .map(line => line.split('=')[1].replace(/["']/g, '').toLowerCase())
34
+ .join(' ')
35
+ .split(/\s+/);
36
+ if (ids.some(id => id === 'debian' || id === 'ubuntu')) return `sudo apt-get install -y ${pkg}`;
37
+ if (ids.some(id => ['fedora', 'rhel', 'centos'].includes(id))) return `sudo ${hasDnf ? 'dnf' : 'yum'} install -y ${pkg}`;
38
+ return null;
39
+ }
40
+
41
+ function describeMissing(component, options) {
42
+ const pkg = PACKAGE_OF[component] || component;
43
+ const command = installCommand(component, options);
44
+ return {
45
+ reason: `rootless Podman 缺少默认网络组件 ${component},容器无法创建网络。`,
46
+ action: command ? `请执行: ${command}` : `请用系统包管理器安装 ${pkg}(提供 ${component})。`
47
+ };
48
+ }
49
+
50
+ /**
51
+ * 检查 rootless podman 的默认网络组件是否存在(只在 Linux 的 podman 上检查)。
52
+ * @returns {Promise<null|{component: string, reason: string, action: string}>} null 表示无需处理
53
+ */
54
+ async function checkRootlessNetwork(runCommand, { command, env, timeout, platform = process.platform } = {}) {
55
+ if (platform !== 'linux' || !/(^|\/)podman$/.test(String(command || ''))) return null;
56
+ const run = async args => String(await runCommand(command, args, { env, timeout }) || '').trim();
57
+ try {
58
+ if ((await run(['info', '--format', '{{.Host.Security.Rootless}}'])) !== 'true') return null;
59
+ const component = defaultNetworkComponent(parsePodmanMajor(await run(['--version'])));
60
+ const field = component === 'pasta' ? 'Pasta' : 'Slirp4NetNS';
61
+ if (await run(['info', '--format', `{{.Host.${field}.Executable}}`])) return { component, missing: false };
62
+ return { component, missing: true, ...describeMissing(component) };
63
+ } catch (e) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ module.exports = { parsePodmanMajor, defaultNetworkComponent, installCommand, describeMissing, checkRootlessNetwork };
package/lib/serve-log.js CHANGED
@@ -84,6 +84,16 @@ function sanitizeSensitiveData(obj) {
84
84
  return result;
85
85
  }
86
86
 
87
+ // 命令行参数里的 KEY=值(--env / 镜像构建参数等):敏感键的值整体打码,用于错误信息与 show-command
88
+ function redactCommandArgs(args) {
89
+ const sensitive = /KEY|TOKEN|SECRET|PASSWORD|PASS|AUTH|CREDENTIAL/i;
90
+ return (Array.isArray(args) ? args : []).map(arg => {
91
+ const text = String(arg);
92
+ const match = /^([A-Za-z_][A-Za-z0-9_.]*)=/.exec(text);
93
+ return match && sensitive.test(match[1]) ? `${match[1]}=****` : text;
94
+ });
95
+ }
96
+
87
97
  function formatServeLogValue(value) {
88
98
  if (value instanceof Error) {
89
99
  return sanitizeServeLogText(value.stack || value.message || String(value));
@@ -111,6 +121,7 @@ module.exports = {
111
121
  sanitizeProcessArgv,
112
122
  sanitizeServeLogText,
113
123
  sanitizeSensitiveData,
124
+ redactCommandArgs,
114
125
  formatServeLogValue,
115
126
  getServeProcessSnapshot
116
127
  };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "8.2.1",
3
+ "version": "8.3.0",
4
4
  "imageVersion": "2.1.0-common",
5
5
  "playwrightCliVersion": "0.1.21",
6
- "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
+ "description": "MANYOYO – open-source AI agent sandbox: run AI coding agents (Claude Code / Codex / Gemini) safely in Docker/Podman",
7
7
  "keywords": [
8
8
  "manyoyo",
9
9
  "ai-agent",