@xcanwin/manyoyo 8.5.1 → 8.6.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.
- package/bin/manyoyo.js +4 -4
- package/lib/container-exec.js +20 -1
- package/lib/container-network.js +100 -48
- package/lib/container-run.js +1 -1
- package/lib/egress-proxy.js +146 -64
- package/lib/egress-sidecar-main.js +23 -5
- package/lib/egress-sidecar.js +22 -12
- package/lib/network-endpoints.js +18 -19
- package/lib/network-policy.js +296 -77
- package/lib/web/AGENTS.md +1 -1
- package/lib/web/container-manage.js +84 -30
- package/lib/web/index.html +81 -82
- package/lib/web/server.js +103 -25
- package/manyoyo.example.json +9 -6
- package/package.json +1 -1
package/lib/web/server.js
CHANGED
|
@@ -13,19 +13,19 @@ const { buildContainerRunArgs, hasNetworkArg } = require('../container-run');
|
|
|
13
13
|
const { buildContainerIntegration, mergeIntegration } = require('../plugin');
|
|
14
14
|
const { applyAptMirror } = require('../mirrors');
|
|
15
15
|
const containerState = require('../container-state');
|
|
16
|
-
const { buildExecArgs, resolveContainerId } = require('../container-exec');
|
|
16
|
+
const { buildExecArgs, resolveContainerId, readMergedEnv } = require('../container-exec');
|
|
17
17
|
const { createNetworkManager, NETWORK_NAME } = require('../container-network');
|
|
18
|
-
const { normalizePolicy } = require('../network-policy');
|
|
18
|
+
const { normalizePolicy, isUnrestricted: isUnrestrictedPolicy } = require('../network-policy');
|
|
19
19
|
const { withEnvEndpoints } = require('../network-endpoints');
|
|
20
20
|
const { egressPaths, isSidecarName } = require('../egress-sidecar');
|
|
21
21
|
const { read: readDenied } = require('../egress-denied');
|
|
22
22
|
const { isBrowserBackground } = require('../browser-background');
|
|
23
23
|
const { createDeniedNotifier } = require('./denied-notify');
|
|
24
|
-
const {
|
|
24
|
+
const { domainRuleAllows } = require('../network-policy');
|
|
25
25
|
const { createPortForwarder } = require('../port-forward');
|
|
26
26
|
const { createContainerManageRoutes, pendingRisks } = require('./container-manage');
|
|
27
27
|
const { resolveManageOptions } = require('../container-manage-options');
|
|
28
|
-
const { defaultPolicy } = require('../network-policy');
|
|
28
|
+
const { defaultPolicy, usesProxy } = require('../network-policy');
|
|
29
29
|
const { normalizeMirrors } = require('../runtime-normalizers');
|
|
30
30
|
const { estimateContainerCapacity } = require('../capacity');
|
|
31
31
|
const { extractAgentMessageFromCodexJsonl } = require('../codex-output');
|
|
@@ -829,7 +829,7 @@ function buildCodexAgentExecCommand(template, prompt, options = {}) {
|
|
|
829
829
|
const hasJson = /(?:^|\s)--json(?:\s|$)/.test(suffix);
|
|
830
830
|
const injectedFlags = hasJson ? '' : ' --json';
|
|
831
831
|
const hasModel = /(?:^|\s)(?:-m|--model)(?:\s|$)/.test(suffix);
|
|
832
|
-
const modelFlag = model && !hasModel ? ` --model ${model}` : '';
|
|
832
|
+
const modelFlag = model && !hasModel ? ` --model ${quoteBashSingleValue(model)}` : '';
|
|
833
833
|
if (sessionId) {
|
|
834
834
|
const promptIndex = suffix.indexOf('{prompt}');
|
|
835
835
|
const resumeSuffix = `${suffix.slice(0, promptIndex)}${quoteBashSingleValue(sessionId)} ${suffix.slice(promptIndex)}`;
|
|
@@ -877,7 +877,7 @@ function buildClaudeAgentExecCommand(template, prompt, options = {}) {
|
|
|
877
877
|
}
|
|
878
878
|
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
879
879
|
if (model) {
|
|
880
|
-
flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
|
|
880
|
+
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)--model(?:\s|$)/ });
|
|
881
881
|
}
|
|
882
882
|
const claudeTemplate = prependAgentFlags(
|
|
883
883
|
templateText,
|
|
@@ -896,7 +896,7 @@ function buildGeminiAgentExecCommand(template, prompt, options = {}) {
|
|
|
896
896
|
];
|
|
897
897
|
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
898
898
|
if (model) {
|
|
899
|
-
flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
899
|
+
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
900
900
|
}
|
|
901
901
|
const geminiTemplate = prependAgentFlags(
|
|
902
902
|
templateText,
|
|
@@ -922,7 +922,7 @@ function buildOpenCodeAgentExecCommand(template, prompt, options = {}) {
|
|
|
922
922
|
}
|
|
923
923
|
const model = options && typeof options.model === 'string' ? options.model.trim() : '';
|
|
924
924
|
if (model) {
|
|
925
|
-
flagSpecs.push({ flag: `--model ${model}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
925
|
+
flagSpecs.push({ flag: `--model ${quoteBashSingleValue(model)}`, pattern: /(?:^|\s)(?:-m|--model)(?:\s|$)/ });
|
|
926
926
|
}
|
|
927
927
|
const opencodeTemplate = prependAgentFlags(
|
|
928
928
|
templateText,
|
|
@@ -1319,6 +1319,61 @@ function extractOpenCodeModelsFromOutput(text) {
|
|
|
1319
1319
|
.map(value => ({ value, label: value, description: '' }));
|
|
1320
1320
|
}
|
|
1321
1321
|
|
|
1322
|
+
// 模型目录要在容器里起一个 Agent 进程才能问到,很慢:按“容器 + Agent 程序”缓存,
|
|
1323
|
+
// 并合并同一时刻的重复请求。缓存挂在 ctx 上(随 serve 实例走),不放模块级,避免测试之间串味
|
|
1324
|
+
const MODEL_CATALOG_TTL_MS = 10 * 60 * 1000;
|
|
1325
|
+
const modelCatalogCaches = new WeakMap();
|
|
1326
|
+
|
|
1327
|
+
async function fetchAgentModelCatalogCached(ctx, containerName, agentProgram) {
|
|
1328
|
+
let cache = modelCatalogCaches.get(ctx);
|
|
1329
|
+
if (!cache) {
|
|
1330
|
+
cache = new Map();
|
|
1331
|
+
modelCatalogCaches.set(ctx, cache);
|
|
1332
|
+
}
|
|
1333
|
+
const key = `${containerName}\u0000${agentProgram}`;
|
|
1334
|
+
const hit = cache.get(key);
|
|
1335
|
+
if (hit && (hit.pending || Date.now() - hit.at < MODEL_CATALOG_TTL_MS)) {
|
|
1336
|
+
return hit.pending || hit.models;
|
|
1337
|
+
}
|
|
1338
|
+
const pending = fetchAgentModelCatalog(ctx, containerName, agentProgram).then(models => {
|
|
1339
|
+
// 探测失败(超时 / 容器没起来)返回空数组,不缓存,下次还能重试
|
|
1340
|
+
if (models.length) cache.set(key, { at: Date.now(), models });
|
|
1341
|
+
else cache.delete(key);
|
|
1342
|
+
return models;
|
|
1343
|
+
}, error => {
|
|
1344
|
+
cache.delete(key);
|
|
1345
|
+
throw error;
|
|
1346
|
+
});
|
|
1347
|
+
cache.set(key, { pending });
|
|
1348
|
+
return pending;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// 以会话实际配置的 env 生成模型选项。defaultModel 是“跟随默认”实际用的模型;
|
|
1352
|
+
// Claude 另有按档位别名(opus / sonnet / haiku …)一行一个的选项:value 是别名(--model opus 由 CLI 按 env 解析),
|
|
1353
|
+
// label 是该档位对应的真实模型名。不同档位指向同一个模型也各占一行,不合并。
|
|
1354
|
+
// 只读 *_MODEL 这类非敏感键,容器可写的 env 只用来展示,不参与任何安全判断
|
|
1355
|
+
const CLAUDE_TIER_ORDER = ['opus', 'fable', 'sonnet', 'haiku'];
|
|
1356
|
+
|
|
1357
|
+
function buildEnvModelOptions(agentProgram, env) {
|
|
1358
|
+
const source = env && typeof env === 'object' ? env : {};
|
|
1359
|
+
const defaultKey = { claude: 'ANTHROPIC_MODEL', codex: 'OPENAI_MODEL', opencode: 'OPENAI_MODEL', gemini: 'GEMINI_MODEL' }[agentProgram];
|
|
1360
|
+
if (!defaultKey) return { defaultModel: '', options: [] };
|
|
1361
|
+
const defaultModel = normalizeStoredAgentModelValue(source[defaultKey]);
|
|
1362
|
+
if (agentProgram !== 'claude') return { defaultModel, options: [] };
|
|
1363
|
+
const tiers = [];
|
|
1364
|
+
Object.keys(source).forEach(key => {
|
|
1365
|
+
const matched = key.match(/^ANTHROPIC_DEFAULT_([A-Z0-9]+)_MODEL$/);
|
|
1366
|
+
const model = matched ? normalizeStoredAgentModelValue(source[key]) : '';
|
|
1367
|
+
if (model) tiers.push({ alias: matched[1].toLowerCase(), model });
|
|
1368
|
+
});
|
|
1369
|
+
const rank = alias => (CLAUDE_TIER_ORDER.includes(alias) ? CLAUDE_TIER_ORDER.indexOf(alias) : CLAUDE_TIER_ORDER.length);
|
|
1370
|
+
tiers.sort((x, y) => rank(x.alias) - rank(y.alias) || x.alias.localeCompare(y.alias));
|
|
1371
|
+
return {
|
|
1372
|
+
defaultModel,
|
|
1373
|
+
options: tiers.map(tier => ({ value: tier.alias, label: tier.model, description: tier.alias }))
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1322
1377
|
async function fetchAgentModelCatalog(ctx, containerName, agentProgram) {
|
|
1323
1378
|
if (agentProgram === 'claude') {
|
|
1324
1379
|
const requestLine = JSON.stringify({
|
|
@@ -2826,10 +2881,10 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2826
2881
|
parseServerListen: () => ({ host: '', port: 0 })
|
|
2827
2882
|
});
|
|
2828
2883
|
|
|
2829
|
-
// 自启动与网络:请求 > runs.<name> >
|
|
2884
|
+
// 自启动与网络:请求 > runs.<name> > 全局配置;请求里放开限制(自定义 / 很宽的允许规则 / 非本机绑定)要带 confirmRisk
|
|
2830
2885
|
const manage = resolveManageOptions({ requestOptions, runConfig, globalConfig: config });
|
|
2831
2886
|
if (manage.fromRequest && requestOptions.confirmRisk !== true && pendingRisks(defaultPolicy(), manage.network).length) {
|
|
2832
|
-
throw new Error('
|
|
2887
|
+
throw new Error('该网络设置会放开限制(自定义模式、很宽的允许规则或绑定非本机地址),需要确认(confirmRisk)');
|
|
2833
2888
|
}
|
|
2834
2889
|
|
|
2835
2890
|
const containerName = resolvedBase.containerName;
|
|
@@ -2952,7 +3007,7 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2952
3007
|
agentPromptCommand,
|
|
2953
3008
|
defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
|
|
2954
3009
|
// 仅供服务端内部复用(如"创建相同配置容器"/"复制容器"),绝不能透传给任何对外响应字段,
|
|
2955
|
-
// 因为 env/volumes/ports
|
|
3010
|
+
// 因为 env/volumes/ports 可能包含密钥原始值(对外响应不暴露它们)。
|
|
2956
3011
|
runtimeSnapshot: {
|
|
2957
3012
|
hostPath,
|
|
2958
3013
|
containerPath,
|
|
@@ -2987,11 +3042,8 @@ function buildCreateRuntime(ctx, state, payload) {
|
|
|
2987
3042
|
agentProgram: agentProgram || '',
|
|
2988
3043
|
resumeSupported,
|
|
2989
3044
|
yolo: yolo || '',
|
|
2990
|
-
envCount: Math.floor(containerEnvs.length / 2),
|
|
2991
3045
|
autostartEnabled: Boolean(manage.autostart.trim()),
|
|
2992
|
-
networkPreset: manage.network.preset
|
|
2993
|
-
volumeCount: Math.floor(containerVolumes.length / 2),
|
|
2994
|
-
portCount: Math.floor(containerPorts.length / 2)
|
|
3046
|
+
networkPreset: manage.network.preset
|
|
2995
3047
|
}
|
|
2996
3048
|
};
|
|
2997
3049
|
}
|
|
@@ -3336,10 +3388,10 @@ function createWebDeniedNotifier(ctx, sessionRef, emit) {
|
|
|
3336
3388
|
if (!id || !containerState.stateExists(homeDir, id)) return null;
|
|
3337
3389
|
const deniedDir = egressPaths(homeDir).denied;
|
|
3338
3390
|
const policyOf = () => normalizePolicy(containerState.readNetworkRaw(homeDir, id) || undefined);
|
|
3339
|
-
if (policyOf()
|
|
3391
|
+
if (!usesProxy(policyOf())) return null;
|
|
3340
3392
|
return createDeniedNotifier({
|
|
3341
3393
|
read: () => readDenied(deniedDir, id),
|
|
3342
|
-
isIgnored: host => isBrowserBackground(host) ||
|
|
3394
|
+
isIgnored: host => isBrowserBackground(host) || domainRuleAllows(policyOf(), host),
|
|
3343
3395
|
emit
|
|
3344
3396
|
});
|
|
3345
3397
|
} catch (e) {
|
|
@@ -3503,8 +3555,8 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
|
|
|
3503
3555
|
const userEnvLines = containerState.userEnvLines(runtime.containerEnvs);
|
|
3504
3556
|
const playwright = await buildWebPlaywrightIntegration(ctx, state, runtime);
|
|
3505
3557
|
const containerEnvs = containerState.stripEnvKeys(playwright.containerEnvs, new Set(userEnvLines.map(containerState.envLineKey)));
|
|
3506
|
-
const networkPolicy = await withEnvEndpoints(normalizePolicy(runtime.network), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
|
|
3507
3558
|
const networkManager = getWebNetworkManager(ctx, state);
|
|
3559
|
+
const networkPolicy = await withEnvEndpoints(await networkManager.resolveRefs(normalizePolicy(runtime.network)), [...userEnvLines, ...containerState.readEnvFilePaths(runtime.envFiles || []).flatMap(file => file.entries.map(entry => `${entry.key}=${entry.value}`))]);
|
|
3508
3560
|
if (!hasNetworkArg([...(runtime.contModeArgs || []), ...(playwright.containerExtraArgs || [])])) {
|
|
3509
3561
|
await networkManager.ensureBridgeNetwork();
|
|
3510
3562
|
}
|
|
@@ -3514,7 +3566,7 @@ async function ensureWebContainer(ctx, state, containerInput, messageSessionRef
|
|
|
3514
3566
|
envFiles: runtime.envFiles,
|
|
3515
3567
|
autostart: runtime.autostart,
|
|
3516
3568
|
network: networkPolicy,
|
|
3517
|
-
netRequired: networkPolicy
|
|
3569
|
+
netRequired: !isUnrestrictedPolicy(networkPolicy),
|
|
3518
3570
|
meta: { name: runtime.containerName }
|
|
3519
3571
|
});
|
|
3520
3572
|
const args = buildContainerRunArgs({
|
|
@@ -4619,9 +4671,6 @@ function buildSessionFallbackApplied(ctx, state, name, history, summary) {
|
|
|
4619
4671
|
agentProgram: effectiveAgentProgram,
|
|
4620
4672
|
resumeSupported: effectiveResumeSupported,
|
|
4621
4673
|
yolo: defaults.yolo || '',
|
|
4622
|
-
envCount: Object.keys(defaults.env || {}).length,
|
|
4623
|
-
volumeCount: Array.isArray(defaults.volumes) ? defaults.volumes.length : 0,
|
|
4624
|
-
portCount: Array.isArray(defaults.ports) ? defaults.ports.length : 0,
|
|
4625
4674
|
status: summary.status || 'history'
|
|
4626
4675
|
};
|
|
4627
4676
|
}
|
|
@@ -5956,7 +6005,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5956
6005
|
containerInfo.defaultCommand
|
|
5957
6006
|
);
|
|
5958
6007
|
const agentProgram = resolveAgentProgram(effectiveTemplate);
|
|
5959
|
-
const models = await
|
|
6008
|
+
const models = await fetchAgentModelCatalogCached(ctx, sessionRef.containerName, agentProgram);
|
|
5960
6009
|
sendJson(res, 200, {
|
|
5961
6010
|
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
5962
6011
|
agentProgram,
|
|
@@ -5964,6 +6013,34 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
5964
6013
|
});
|
|
5965
6014
|
}
|
|
5966
6015
|
},
|
|
6016
|
+
{
|
|
6017
|
+
// 毫秒级:只读宿主机上的 env 文件,不进容器;慢的 CLI 目录探测走上面的 /models
|
|
6018
|
+
method: 'GET',
|
|
6019
|
+
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model-options$/),
|
|
6020
|
+
handler: async match => {
|
|
6021
|
+
const sessionRef = getValidSessionRef(ctx, res, match[1]);
|
|
6022
|
+
if (!sessionRef) {
|
|
6023
|
+
return;
|
|
6024
|
+
}
|
|
6025
|
+
const containerMap = listWebManyoyoContainers(ctx);
|
|
6026
|
+
const containerInfo = containerMap[sessionRef.containerName] || {};
|
|
6027
|
+
const history = loadWebSessionHistory(state.webHistoryDir, sessionRef.containerName);
|
|
6028
|
+
const agentProgram = resolveAgentProgram(resolveEffectiveAgentPromptCommandForSession(
|
|
6029
|
+
history,
|
|
6030
|
+
sessionRef.agentId,
|
|
6031
|
+
containerInfo.defaultCommand
|
|
6032
|
+
));
|
|
6033
|
+
let env = {};
|
|
6034
|
+
try {
|
|
6035
|
+
env = readMergedEnv({ homeDir: ctx.homeDir || os.homedir(), dockerExecArgs: ctx.dockerExecArgs }, sessionRef.containerName);
|
|
6036
|
+
} catch (e) { /* 读不到 env 就只给空选项 */ }
|
|
6037
|
+
sendJson(res, 200, {
|
|
6038
|
+
name: buildWebSessionKey(sessionRef.containerName, sessionRef.agentId),
|
|
6039
|
+
agentProgram,
|
|
6040
|
+
...buildEnvModelOptions(agentProgram, env)
|
|
6041
|
+
});
|
|
6042
|
+
}
|
|
6043
|
+
},
|
|
5967
6044
|
{
|
|
5968
6045
|
method: 'POST',
|
|
5969
6046
|
match: currentPath => currentPath.match(/^\/api\/sessions\/([^/]+)\/model$/),
|
|
@@ -6333,7 +6410,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
|
|
|
6333
6410
|
});
|
|
6334
6411
|
|
|
6335
6412
|
};
|
|
6336
|
-
//
|
|
6413
|
+
// 有域名规则时被过滤代理拦下的新域名:对话进行中立即提示一次
|
|
6337
6414
|
const deniedNotifier = createWebDeniedNotifier(ctx, sessionRef, text => handleStreamEvent({ type: 'trace', stream: 'stdout', text }));
|
|
6338
6415
|
try {
|
|
6339
6416
|
if (deniedNotifier) deniedNotifier.start();
|
|
@@ -6878,5 +6955,6 @@ async function startWebServer(options) {
|
|
|
6878
6955
|
}
|
|
6879
6956
|
|
|
6880
6957
|
module.exports = {
|
|
6881
|
-
startWebServer
|
|
6958
|
+
startWebServer,
|
|
6959
|
+
buildEnvModelOptions
|
|
6882
6960
|
};
|
package/manyoyo.example.json
CHANGED
|
@@ -30,14 +30,17 @@
|
|
|
30
30
|
"ports": ["8888:8888"],
|
|
31
31
|
|
|
32
32
|
// 网络策略(新建容器时写入,Web「容器」页随时改,不需要重启容器)。不写 = 默认收紧:允许公网,
|
|
33
|
-
//
|
|
34
|
-
// preset:restricted(默认)/ allowlist
|
|
33
|
+
// 禁止宿主机、局域网 / 私有网段、云元数据与其他容器。
|
|
34
|
+
// preset:restricted(默认)/ allowlist(只放行你写的允许规则)/ custom(没匹配的就允许)
|
|
35
|
+
// outbound / inbound:规则从上往下,第一条命中的生效。action:allow / deny;target(入站为 source):
|
|
36
|
+
// IP / 网段 / 域名(仅出站)/ @containers @container:<名称> @host @private @public @metadata @any;ports 空 = 全部;proto:all / tcp / udp
|
|
35
37
|
"network": {
|
|
38
|
+
"version": 2,
|
|
36
39
|
"preset": "restricted",
|
|
37
|
-
//
|
|
38
|
-
"
|
|
39
|
-
//
|
|
40
|
-
"
|
|
40
|
+
// 例:允许容器访问宿主机的 Ollama:{ "action": "allow", "target": "@host", "ports": "11434", "proto": "tcp" }
|
|
41
|
+
"outbound": [],
|
|
42
|
+
// 例:允许容器 B 访问我的 7000 端口:{ "action": "allow", "source": "@container:B", "ports": "7000", "proto": "tcp" }
|
|
43
|
+
"inbound": [],
|
|
41
44
|
// 端口暴露(需要 serve 运行):监听地址默认 127.0.0.1;绑 0.0.0.0 等于局域网 / 公网可见
|
|
42
45
|
"expose": []
|
|
43
46
|
},
|