@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 +88 -0
- package/README.md +4 -3
- package/bin/manyoyo.js +27 -7
- package/docker/res/claude/settings.json +5 -0
- package/lib/container-run.js +36 -0
- package/lib/doctor.js +36 -4
- package/lib/error-hints.js +10 -2
- package/lib/json5-text-edit.js +18 -3
- package/lib/rootless-network.js +68 -0
- package/lib/serve-log.js +11 -0
- package/package.json +2 -2
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"
|
|
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="
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2196
|
+
runWithEnvFile(args, path.join(os.homedir(), '.manyoyo', 'tmp'), finalArgs => dockerExecArgs(finalArgs, { stdio: 'pipe' }));
|
|
2179
2197
|
} catch (e) {
|
|
2180
2198
|
showImagePullHint(e);
|
|
2181
|
-
|
|
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
|
|
package/lib/container-run.js
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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', '监听端口可用'));
|
package/lib/error-hints.js
CHANGED
|
@@ -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
|
-
|
|
82
|
-
|
|
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;
|
package/lib/json5-text-edit.js
CHANGED
|
@@ -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
|
|
242
|
-
const
|
|
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.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"imageVersion": "2.1.0-common",
|
|
5
5
|
"playwrightCliVersion": "0.1.21",
|
|
6
|
-
"description": "AI
|
|
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",
|