@xcanwin/manyoyo 5.7.2 → 5.7.3

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.
@@ -783,8 +783,8 @@ body.mobile-actions-open .header-actions {
783
783
  color: var(--muted);
784
784
  font-size: 12px;
785
785
  font-weight: 700;
786
- text-transform: uppercase;
787
- letter-spacing: 0.3px;
786
+ text-transform: none;
787
+ letter-spacing: 0.1px;
788
788
  line-height: 1.45;
789
789
  overflow-wrap: anywhere;
790
790
  }
@@ -126,7 +126,7 @@
126
126
  <div id="configModal" class="modal-backdrop" hidden>
127
127
  <section class="modal" role="dialog" aria-modal="true" aria-labelledby="configModalTitle">
128
128
  <header class="modal-header">
129
- <h2 id="configModalTitle">编辑配置 (~/.manyoyo/manyoyo.json)</h2>
129
+ <h2 id="configModalTitle">查看配置摘要 (~/.manyoyo/manyoyo.json)</h2>
130
130
  <button type="button" id="configCancelBtn" class="secondary">关闭</button>
131
131
  </header>
132
132
  <div class="modal-body">
@@ -136,7 +136,7 @@
136
136
  </div>
137
137
  <footer class="modal-footer">
138
138
  <button type="button" id="configReloadBtn" class="secondary">重新加载</button>
139
- <button type="button" id="configSaveBtn">保存</button>
139
+ <button type="button" id="configSaveBtn">只读</button>
140
140
  </footer>
141
141
  </section>
142
142
  </div>
@@ -104,6 +104,7 @@
104
104
  const openConfigBtn = document.getElementById('openConfigBtn');
105
105
  const openCreateBtn = document.getElementById('openCreateBtn');
106
106
  const configModal = document.getElementById('configModal');
107
+ const configModalTitle = document.getElementById('configModalTitle');
107
108
  const configPath = document.getElementById('configPath');
108
109
  const configEditor = document.getElementById('configEditor');
109
110
  const configError = document.getElementById('configError');
@@ -621,9 +622,10 @@
621
622
  createAgentPromptCommand.value = value.agentPromptCommand || '';
622
623
  state.createAgentPromptAuto = false;
623
624
  createYolo.value = value.yolo || '';
624
- createEnv.value = envMapToText(value.env);
625
- createEnvFile.value = Array.isArray(value.envFile) ? value.envFile.join('\n') : '';
626
- createVolumes.value = Array.isArray(value.volumes) ? value.volumes.join('\n') : '';
625
+ // 敏感 env 与继承数组由服务端在创建时合并,前端表单默认不回显,避免泄露或重复提交。
626
+ createEnv.value = '';
627
+ createEnvFile.value = '';
628
+ createVolumes.value = '';
627
629
  updateCreateAgentPromptCommandFromCommand();
628
630
  }
629
631
 
@@ -1236,9 +1238,9 @@
1236
1238
  function renderSessionDetailPanels() {
1237
1239
  const detail = state.sessionDetail;
1238
1240
  if (!state.active) {
1239
- renderEmptyInspector(detailSummary, '详情视图', '选择左侧会话后,这里会显示会话概览、Agent 状态与运行参数。');
1240
- renderEmptyInspector(configSummary, '配置视图', '选择会话后可查看当前容器会话的运行参数摘要。');
1241
- renderEmptyInspector(checkSummary, '检查视图', '选择会话后可查看当前会话的基础健康检查。');
1241
+ renderEmptyInspector(detailSummary, '详情视图', '选择左侧会话后,这里会显示会话概览、Agent 运行状态与最近活动。');
1242
+ renderEmptyInspector(configSummary, '配置视图', '选择会话后可查看当前容器会话的生效配置摘要。');
1243
+ renderEmptyInspector(checkSummary, '检查视图', '选择会话后可查看当前会话的诊断结论与最近问题。');
1242
1244
  return;
1243
1245
  }
1244
1246
  if (state.loadingSessionDetail) {
@@ -1258,6 +1260,51 @@
1258
1260
  const applied = detail.applied || {};
1259
1261
  const status = sessionStatusInfo(detail.status);
1260
1262
  const updatedText = formatDateTime(detail.updatedAt) || '暂无更新';
1263
+ const lastResumeText = detail.lastResumeAt ? formatDateTime(detail.lastResumeAt) : '暂无';
1264
+ const latestTimestampText = detail.latestTimestamp ? formatDateTime(detail.latestTimestamp) : '暂无';
1265
+ const latestRoleMap = {
1266
+ user: '我',
1267
+ assistant: 'Agent',
1268
+ system: '系统'
1269
+ };
1270
+ const latestRoleLabel = latestRoleMap[String(detail.latestRole || '').toLowerCase()] || (detail.latestRole || '暂无');
1271
+ const imageVersionValid = /^\d+\.\d+\.\d+-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(String(applied.imageVersion || ''));
1272
+ let resumeStatusValue = '未执行';
1273
+ let resumeStatusTone = 'warn';
1274
+ let resumeStatusDetail = detail.resumeSupported
1275
+ ? '支持 resume,但当前会话还没有最近一次执行记录。'
1276
+ : '当前 Agent 程序或模板不支持 resume。';
1277
+ if (detail.lastResumeOk === true) {
1278
+ resumeStatusValue = '最近成功';
1279
+ resumeStatusTone = 'ok';
1280
+ resumeStatusDetail = `最近一次 resume 成功,时间:${lastResumeText}。`;
1281
+ } else if (detail.lastResumeOk === false) {
1282
+ resumeStatusValue = '最近失败';
1283
+ resumeStatusTone = 'danger';
1284
+ resumeStatusDetail = detail.lastResumeError
1285
+ ? `最近一次 resume 失败:${detail.lastResumeError}`
1286
+ : `最近一次 resume 失败,时间:${lastResumeText}。`;
1287
+ } else if (!detail.resumeSupported) {
1288
+ resumeStatusValue = '不支持';
1289
+ }
1290
+
1291
+ const commandEntries = [];
1292
+ if (applied.shellPrefix) {
1293
+ commandEntries.push({ label: 'shellPrefix', value: applied.shellPrefix });
1294
+ }
1295
+ if (applied.shell) {
1296
+ commandEntries.push({ label: 'shell', value: applied.shell });
1297
+ }
1298
+ if (applied.shellSuffix) {
1299
+ commandEntries.push({ label: 'shellSuffix', value: applied.shellSuffix });
1300
+ }
1301
+ if (applied.defaultCommand && applied.defaultCommand !== applied.shell) {
1302
+ commandEntries.push({ label: '启动命令', value: applied.defaultCommand });
1303
+ } else if (!applied.shell) {
1304
+ commandEntries.push({ label: '启动命令', value: applied.defaultCommand || '—' });
1305
+ }
1306
+ commandEntries.push({ label: 'Agent 模板', value: detail.agentPromptCommand || '—' });
1307
+ commandEntries.push({ label: 'yolo', value: applied.yolo || '—' });
1261
1308
 
1262
1309
  if (detailSummary) {
1263
1310
  detailSummary.innerHTML = '';
@@ -1268,45 +1315,41 @@
1268
1315
  { label: '最近更新', value: updatedText },
1269
1316
  { label: '消息数', value: String(safeMessageCount(detail.messageCount)) }
1270
1317
  ]);
1271
- renderKeyValueCard(detailSummary, 'Agent 上下文', [
1318
+ renderKeyValueCard(detailSummary, 'Agent 运行', [
1272
1319
  { label: '已启用', value: detail.agentEnabled ? '是' : '否', tone: detail.agentEnabled ? 'ok' : 'warn' },
1273
1320
  { label: '程序', value: detail.agentProgram || '—' },
1274
1321
  { label: '支持 resume', value: detail.resumeSupported ? '是' : '否', tone: detail.resumeSupported ? 'ok' : 'warn' },
1275
- { label: '最近 resume', value: detail.lastResumeAt ? formatDateTime(detail.lastResumeAt) : '暂无' },
1322
+ { label: '最近 resume', value: lastResumeText },
1276
1323
  { label: '最近结果', value: detail.lastResumeOk == null ? '暂无' : (detail.lastResumeOk ? '成功' : '失败'), tone: detail.lastResumeOk == null ? 'info' : (detail.lastResumeOk ? 'ok' : 'danger') }
1277
1324
  ]);
1278
- renderKeyValueCard(detailSummary, '运行参数', [
1279
- { label: 'hostPath', value: applied.hostPath || '—' },
1280
- { label: 'containerPath', value: applied.containerPath || '—' },
1281
- { label: 'imageVersion', value: applied.imageVersion || '—' },
1282
- { label: 'containerMode', value: applied.containerMode || 'default' },
1283
- { label: 'env/vol/ports', value: `${applied.envCount || 0} / ${applied.volumeCount || 0} / ${applied.portCount || 0}` }
1325
+ renderKeyValueCard(detailSummary, '最近活动', [
1326
+ { label: '最近角色', value: latestRoleLabel },
1327
+ { label: '最近时间', value: latestTimestampText },
1328
+ { label: 'resume 状态', value: resumeStatusValue, tone: resumeStatusTone }
1284
1329
  ]);
1285
1330
  }
1286
1331
 
1287
1332
  if (configSummary) {
1288
1333
  configSummary.innerHTML = '';
1289
- renderKeyValueCard(configSummary, '配置摘要', [
1334
+ renderKeyValueCard(configSummary, '基础配置', [
1290
1335
  { label: 'containerName', value: applied.containerName || detail.name || state.active },
1291
- { label: 'hostPath', value: applied.hostPath || '—' },
1292
- { label: 'containerPath', value: applied.containerPath || '—' },
1293
1336
  { label: 'imageName', value: applied.imageName || detail.image || '—' },
1294
1337
  { label: 'imageVersion', value: applied.imageVersion || '—' },
1295
1338
  { label: 'containerMode', value: applied.containerMode || 'default' }
1296
- ], { actionLabel: '打开配置', actionId: 'configSummaryOpenBtn' });
1297
- renderKeyValueCard(configSummary, '命令与 Agent', [
1298
- { label: 'shellPrefix', value: applied.shellPrefix || '—' },
1299
- { label: 'shell', value: applied.shell || '—' },
1300
- { label: 'shellSuffix', value: applied.shellSuffix || '—' },
1301
- { label: '默认命令', value: applied.defaultCommand || '—' },
1302
- { label: 'Agent 模板', value: detail.agentPromptCommand || '—' },
1303
- { label: 'yolo', value: applied.yolo || '—' }
1304
1339
  ]);
1340
+ renderKeyValueCard(configSummary, '路径与资源', [
1341
+ { label: 'hostPath', value: applied.hostPath || '—' },
1342
+ { label: 'containerPath', value: applied.containerPath || '—' },
1343
+ { label: 'env 数量', value: String(applied.envCount || 0) },
1344
+ { label: 'volume 数量', value: String(applied.volumeCount || 0) },
1345
+ { label: 'port 数量', value: String(applied.portCount || 0) }
1346
+ ]);
1347
+ renderKeyValueCard(configSummary, '命令与 Agent', commandEntries);
1305
1348
  }
1306
1349
 
1307
1350
  if (checkSummary) {
1308
1351
  checkSummary.innerHTML = '';
1309
- renderCheckCard(checkSummary, '基础检查', [
1352
+ renderCheckCard(checkSummary, '运行检查', [
1310
1353
  {
1311
1354
  label: '容器状态',
1312
1355
  value: status.label,
@@ -1314,28 +1357,32 @@
1314
1357
  detail: status.tone === 'running' ? '容器处于可交互状态。' : '当前不是活跃运行态,部分功能可能受限。'
1315
1358
  },
1316
1359
  {
1317
- label: 'Agent 模板',
1360
+ label: 'Agent 输入',
1318
1361
  value: detail.agentEnabled ? '已配置' : '未配置',
1319
1362
  tone: detail.agentEnabled ? 'ok' : 'warn',
1320
1363
  detail: detail.agentEnabled ? '活动页可直接发送 Agent 提示词。' : '当前会话不支持 Agent 模式。'
1321
1364
  },
1322
1365
  {
1323
- label: 'Resume 能力',
1324
- value: detail.resumeSupported ? '支持' : '不支持',
1325
- tone: detail.resumeSupported ? 'ok' : 'warn',
1326
- detail: detail.resumeSupported ? '可以尝试基于历史继续 Agent 会话。' : '当前 Agent 程序或模板不支持 resume。'
1366
+ label: 'Resume 健康',
1367
+ value: resumeStatusValue,
1368
+ tone: resumeStatusTone,
1369
+ detail: resumeStatusDetail
1327
1370
  },
1328
1371
  {
1329
- label: '镜像版本格式',
1330
- value: applied.imageVersion || '缺失',
1331
- tone: /^\d+\.\d+\.\d+-[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(String(applied.imageVersion || '')) ? 'ok' : 'danger',
1332
- detail: '建议保持 x.y.z-后缀 格式,便于 manyoyo 的版本校验。'
1372
+ label: '镜像版本',
1373
+ value: imageVersionValid ? '格式正常' : '格式异常',
1374
+ tone: imageVersionValid ? 'ok' : 'danger',
1375
+ detail: applied.imageVersion
1376
+ ? `当前值:${applied.imageVersion}。建议保持 x.y.z-后缀 格式,便于 manyoyo 的版本校验。`
1377
+ : '缺少 imageVersion,manyoyo 的版本校验会失效。'
1333
1378
  },
1334
1379
  {
1335
- label: '工作目录',
1336
- value: applied.hostPath && applied.containerPath ? '完整' : '缺失',
1380
+ label: '工作目录映射',
1381
+ value: applied.hostPath && applied.containerPath ? '已配置' : '缺失',
1337
1382
  tone: applied.hostPath && applied.containerPath ? 'ok' : 'danger',
1338
- detail: 'hostPath / containerPath 是容器会话最关键的上下文。'
1383
+ detail: applied.hostPath && applied.containerPath
1384
+ ? '宿主目录与容器目录都已配置。'
1385
+ : 'hostPath / containerPath 是容器会话最关键的上下文。'
1339
1386
  }
1340
1387
  ]);
1341
1388
  if (detail.lastResumeError) {
@@ -1349,13 +1396,6 @@
1349
1396
  ]);
1350
1397
  }
1351
1398
  }
1352
-
1353
- const configSummaryOpenBtn = document.getElementById('configSummaryOpenBtn');
1354
- if (configSummaryOpenBtn) {
1355
- configSummaryOpenBtn.addEventListener('click', function () {
1356
- openConfigModal();
1357
- });
1358
- }
1359
1399
  }
1360
1400
 
1361
1401
  function syncUi() {
@@ -1444,7 +1484,7 @@
1444
1484
  openConfigBtn.disabled = state.configLoading || state.configSaving;
1445
1485
  }
1446
1486
  if (configSaveBtn) {
1447
- configSaveBtn.disabled = state.configLoading || state.configSaving;
1487
+ configSaveBtn.disabled = true;
1448
1488
  }
1449
1489
  if (configReloadBtn) {
1450
1490
  configReloadBtn.disabled = state.configLoading || state.configSaving;
@@ -1612,11 +1652,22 @@
1612
1652
  syncUi();
1613
1653
  try {
1614
1654
  const config = await fetchConfigSnapshot();
1655
+ if (configModalTitle) {
1656
+ configModalTitle.textContent = '查看配置摘要 (~/.manyoyo/manyoyo.json)';
1657
+ }
1615
1658
  if (configPath) {
1616
- configPath.textContent = config.path || '';
1659
+ const lines = [config.path || ''];
1660
+ if (config.notice) {
1661
+ lines.push(config.notice);
1662
+ }
1663
+ configPath.textContent = lines.filter(Boolean).join('\n');
1617
1664
  }
1618
1665
  if (configEditor) {
1619
- configEditor.value = typeof config.raw === 'string' ? config.raw : '';
1666
+ configEditor.readOnly = true;
1667
+ configEditor.value = stringifyPrettyJson({
1668
+ defaults: config.defaults || {},
1669
+ runs: config.parsed && config.parsed.runs ? config.parsed.runs : {}
1670
+ });
1620
1671
  }
1621
1672
  if (config.parseError) {
1622
1673
  showConfigError('当前文件存在解析错误:' + config.parseError);
@@ -1637,23 +1688,7 @@
1637
1688
  }
1638
1689
 
1639
1690
  async function saveConfig() {
1640
- state.configSaving = true;
1641
- showConfigError('');
1642
- syncUi();
1643
- try {
1644
- await api('/api/config', {
1645
- method: 'PUT',
1646
- body: JSON.stringify({ raw: configEditor ? configEditor.value : '' })
1647
- });
1648
- await fetchConfigSnapshot();
1649
- showConfigError('');
1650
- alert('配置已保存。后续新建会读取最新配置。');
1651
- } catch (e) {
1652
- showConfigError(e.message);
1653
- } finally {
1654
- state.configSaving = false;
1655
- syncUi();
1656
- }
1691
+ alert('Web 端已禁用明文配置编辑,请在本地 ~/.manyoyo/manyoyo.json 中维护敏感配置。');
1657
1692
  }
1658
1693
 
1659
1694
  async function openCreateModal() {
package/lib/web/server.js CHANGED
@@ -32,6 +32,8 @@ const WEB_AUTH_TTL_SECONDS = 12 * 60 * 60;
32
32
  const FRONTEND_DIR = path.join(__dirname, 'frontend');
33
33
  const SAFE_CONTAINER_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
34
34
  const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)$/;
35
+ const SENSITIVE_CONFIG_KEY_PATTERN = /(pass(word)?|passwd|secret|token|api(?:_|-)?key|auth(?:_|-)?token|oauth(?:_|-)?token)$/i;
36
+ const REDACTED_CONFIG_VALUE = '***';
35
37
 
36
38
  const YOLO_COMMAND_MAP = {
37
39
  claude: 'IS_SANDBOX=1 claude --dangerously-skip-permissions',
@@ -1300,6 +1302,59 @@ function toPlainObject(value) {
1300
1302
  return value;
1301
1303
  }
1302
1304
 
1305
+ function isSensitiveConfigKey(key) {
1306
+ const normalized = String(key || '').trim();
1307
+ return Boolean(normalized) && SENSITIVE_CONFIG_KEY_PATTERN.test(normalized);
1308
+ }
1309
+
1310
+ function redactConfigValue(value) {
1311
+ if (Array.isArray(value)) {
1312
+ return value.map(item => redactConfigValue(item));
1313
+ }
1314
+ if (!value || typeof value !== 'object') {
1315
+ return REDACTED_CONFIG_VALUE;
1316
+ }
1317
+ return redactConfigObject(value);
1318
+ }
1319
+
1320
+ function redactConfigObject(value) {
1321
+ if (Array.isArray(value)) {
1322
+ return value.map(item => redactConfigValue(item));
1323
+ }
1324
+ if (!value || typeof value !== 'object') {
1325
+ return value;
1326
+ }
1327
+ const result = {};
1328
+ Object.entries(toPlainObject(value)).forEach(([key, item]) => {
1329
+ if (isSensitiveConfigKey(key)) {
1330
+ result[key] = redactConfigValue(item);
1331
+ return;
1332
+ }
1333
+ if (Array.isArray(item)) {
1334
+ result[key] = item.map(entry => redactConfigValue(entry));
1335
+ return;
1336
+ }
1337
+ if (item && typeof item === 'object') {
1338
+ result[key] = redactConfigObject(item);
1339
+ return;
1340
+ }
1341
+ result[key] = item;
1342
+ });
1343
+ return result;
1344
+ }
1345
+
1346
+ function buildSafeWebConfigSnapshot(snapshot, ctx) {
1347
+ const parsed = snapshot && snapshot.parseError ? {} : toPlainObject(snapshot && snapshot.parsed);
1348
+ return {
1349
+ path: snapshot && snapshot.path ? snapshot.path : path.resolve(getDefaultWebConfigPath()),
1350
+ parsed: redactConfigObject(parsed),
1351
+ defaults: redactConfigObject(buildConfigDefaults(ctx, parsed)),
1352
+ parseError: snapshot && snapshot.parseError ? snapshot.parseError : null,
1353
+ editable: false,
1354
+ notice: 'Web 端仅显示脱敏后的配置摘要;敏感值请在本地 manyoyo.json 中维护。'
1355
+ };
1356
+ }
1357
+
1303
1358
  function pickFirstString() {
1304
1359
  for (let i = 0; i < arguments.length; i += 1) {
1305
1360
  const value = arguments[i];
@@ -1705,23 +1760,29 @@ function buildCreateRuntime(ctx, state, payload) {
1705
1760
  const hasConfigPorts = hasOwn(config, 'ports');
1706
1761
 
1707
1762
  const requestName = pickFirstString(requestOptions.containerName, body.name);
1708
- let containerName = pickFirstString(requestName, config.containerName);
1763
+ let containerName = pickFirstString(requestName, runConfig.containerName, config.containerName);
1709
1764
  if (!containerName) {
1710
1765
  containerName = `my-${ctx.formatDate()}`;
1711
1766
  }
1712
1767
  containerName = resolveNowTemplate(containerName, ctx.formatDate);
1713
1768
  validateContainerNameStrict(containerName);
1714
1769
 
1715
- const hostPath = pickFirstString(requestOptions.hostPath, config.hostPath, ctx.hostPath);
1770
+ const hostPath = pickFirstString(requestOptions.hostPath, runConfig.hostPath, config.hostPath, ctx.hostPath);
1716
1771
  if (typeof ctx.validateHostPath === 'function') {
1717
1772
  ctx.validateHostPath(hostPath);
1718
1773
  } else {
1719
1774
  validateWebHostPath(hostPath);
1720
1775
  }
1721
1776
 
1722
- const containerPath = pickFirstString(requestOptions.containerPath, config.containerPath, ctx.containerPath, hostPath) || hostPath;
1723
- const imageName = pickFirstString(requestOptions.imageName, config.imageName, ctx.imageName);
1724
- const imageVersion = pickFirstString(requestOptions.imageVersion, config.imageVersion, ctx.imageVersion);
1777
+ const containerPath = pickFirstString(
1778
+ requestOptions.containerPath,
1779
+ runConfig.containerPath,
1780
+ config.containerPath,
1781
+ ctx.containerPath,
1782
+ hostPath
1783
+ ) || hostPath;
1784
+ const imageName = pickFirstString(requestOptions.imageName, runConfig.imageName, config.imageName, ctx.imageName);
1785
+ const imageVersion = pickFirstString(requestOptions.imageVersion, runConfig.imageVersion, config.imageVersion, ctx.imageVersion);
1725
1786
 
1726
1787
  if (!/^[A-Za-z0-9][A-Za-z0-9._/:-]*$/.test(imageName)) {
1727
1788
  throw new Error(`imageName 非法: ${imageName}`);
@@ -1730,7 +1791,7 @@ function buildCreateRuntime(ctx, state, payload) {
1730
1791
 
1731
1792
  let contModeArgs = Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [];
1732
1793
  let containerMode = '';
1733
- const modeValue = pickFirstString(requestOptions.containerMode, config.containerMode);
1794
+ const modeValue = pickFirstString(requestOptions.containerMode, runConfig.containerMode, config.containerMode);
1734
1795
  if (modeValue) {
1735
1796
  const mode = resolveContainerModeArgs(modeValue);
1736
1797
  containerMode = mode.mode;
@@ -1739,16 +1800,24 @@ function buildCreateRuntime(ctx, state, payload) {
1739
1800
 
1740
1801
  const shellPrefix = hasOwn(requestOptions, 'shellPrefix')
1741
1802
  ? String(requestOptions.shellPrefix || '')
1742
- : (hasOwn(config, 'shellPrefix') ? String(config.shellPrefix || '') : String(ctx.execCommandPrefix || ''));
1803
+ : (hasOwn(runConfig, 'shellPrefix')
1804
+ ? String(runConfig.shellPrefix || '')
1805
+ : (hasOwn(config, 'shellPrefix') ? String(config.shellPrefix || '') : String(ctx.execCommandPrefix || '')));
1743
1806
  let shell = hasOwn(requestOptions, 'shell')
1744
1807
  ? String(requestOptions.shell || '')
1745
- : (hasOwn(config, 'shell') ? String(config.shell || '') : String(ctx.execCommand || ''));
1808
+ : (hasOwn(runConfig, 'shell')
1809
+ ? String(runConfig.shell || '')
1810
+ : (hasOwn(config, 'shell') ? String(config.shell || '') : String(ctx.execCommand || '')));
1746
1811
  const shellSuffix = hasOwn(requestOptions, 'shellSuffix')
1747
1812
  ? String(requestOptions.shellSuffix || '')
1748
- : (hasOwn(config, 'shellSuffix') ? String(config.shellSuffix || '') : String(ctx.execCommandSuffix || ''));
1813
+ : (hasOwn(runConfig, 'shellSuffix')
1814
+ ? String(runConfig.shellSuffix || '')
1815
+ : (hasOwn(config, 'shellSuffix') ? String(config.shellSuffix || '') : String(ctx.execCommandSuffix || '')));
1749
1816
  const yolo = hasOwn(requestOptions, 'yolo')
1750
1817
  ? String(requestOptions.yolo || '')
1751
- : (hasOwn(config, 'yolo') ? String(config.yolo || '') : '');
1818
+ : (hasOwn(runConfig, 'yolo')
1819
+ ? String(runConfig.yolo || '')
1820
+ : (hasOwn(config, 'yolo') ? String(config.yolo || '') : ''));
1752
1821
  const yoloCommand = resolveYoloCommand(yolo);
1753
1822
  if (yoloCommand) {
1754
1823
  shell = yoloCommand;
@@ -1769,19 +1838,23 @@ function buildCreateRuntime(ctx, state, payload) {
1769
1838
  const resumeSupported = Boolean(buildAgentResumeCommand(agentProgram));
1770
1839
 
1771
1840
  let containerEnvs = Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [];
1772
- if (hasRequestEnv || hasRequestEnvFile || hasConfigEnv || hasConfigEnvFile) {
1841
+ const hasRunEnv = hasOwn(runConfig, 'env');
1842
+ const hasRunEnvFile = hasOwn(runConfig, 'envFile');
1843
+ if (hasRequestEnv || hasRequestEnvFile || hasRunEnv || hasRunEnvFile || hasConfigEnv || hasConfigEnvFile) {
1773
1844
  const configEnv = normalizeEnvMap(config.env, 'config.env');
1845
+ const runEnv = normalizeEnvMap(runConfig.env, runName ? `runs.${runName}.env` : 'run.env');
1774
1846
  const requestEnv = hasRequestEnv ? normalizeEnvMap(requestOptions.env, 'createOptions.env') : {};
1775
- const mergedEnv = { ...configEnv, ...requestEnv };
1847
+ const mergedEnv = { ...configEnv, ...runEnv, ...requestEnv };
1776
1848
  const envArgs = [];
1777
1849
  Object.entries(mergedEnv).forEach(([key, value]) => {
1778
1850
  const parsed = parseEnvEntry(`${key}=${value}`);
1779
1851
  envArgs.push('--env', `${parsed.key}=${parsed.value}`);
1780
1852
  });
1781
1853
 
1782
- const envFileList = hasRequestEnvFile
1783
- ? normalizeStringArray(requestOptions.envFile, 'createOptions.envFile')
1784
- : normalizeStringArray(config.envFile, 'config.envFile');
1854
+ const envFileList = []
1855
+ .concat(normalizeStringArray(config.envFile, 'config.envFile'))
1856
+ .concat(normalizeStringArray(runConfig.envFile, runName ? `runs.${runName}.envFile` : 'run.envFile'))
1857
+ .concat(hasRequestEnvFile ? normalizeStringArray(requestOptions.envFile, 'createOptions.envFile') : []);
1785
1858
  const envFileArgs = [];
1786
1859
  envFileList.forEach(filePath => {
1787
1860
  envFileArgs.push(...parseEnvFileToArgs(filePath));
@@ -1791,10 +1864,12 @@ function buildCreateRuntime(ctx, state, payload) {
1791
1864
  }
1792
1865
 
1793
1866
  let containerVolumes = Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [];
1794
- if (hasRequestVolumes || hasConfigVolumes) {
1795
- const volumeList = hasRequestVolumes
1796
- ? normalizeStringArray(requestOptions.volumes, 'createOptions.volumes')
1797
- : normalizeStringArray(config.volumes, 'config.volumes');
1867
+ const hasRunVolumes = hasOwn(runConfig, 'volumes');
1868
+ if (hasRequestVolumes || hasRunVolumes || hasConfigVolumes) {
1869
+ const volumeList = []
1870
+ .concat(normalizeStringArray(config.volumes, 'config.volumes'))
1871
+ .concat(normalizeStringArray(runConfig.volumes, runName ? `runs.${runName}.volumes` : 'run.volumes'))
1872
+ .concat(hasRequestVolumes ? normalizeStringArray(requestOptions.volumes, 'createOptions.volumes') : []);
1798
1873
  containerVolumes = [];
1799
1874
  volumeList.forEach(volume => {
1800
1875
  containerVolumes.push('--volume', normalizeVolume(volume));
@@ -1802,10 +1877,12 @@ function buildCreateRuntime(ctx, state, payload) {
1802
1877
  }
1803
1878
 
1804
1879
  let containerPorts = Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [];
1805
- if (hasRequestPorts || hasConfigPorts) {
1806
- const portList = hasRequestPorts
1807
- ? normalizeStringArray(requestOptions.ports, 'createOptions.ports')
1808
- : normalizeStringArray(config.ports, 'config.ports');
1880
+ const hasRunPorts = hasOwn(runConfig, 'ports');
1881
+ if (hasRequestPorts || hasRunPorts || hasConfigPorts) {
1882
+ const portList = []
1883
+ .concat(normalizeStringArray(config.ports, 'config.ports'))
1884
+ .concat(normalizeStringArray(runConfig.ports, runName ? `runs.${runName}.ports` : 'run.ports'))
1885
+ .concat(hasRequestPorts ? normalizeStringArray(requestOptions.ports, 'createOptions.ports') : []);
1809
1886
  containerPorts = [];
1810
1887
  portList.forEach(port => {
1811
1888
  containerPorts.push('--publish', port);
@@ -2216,6 +2293,15 @@ function sendHtml(res, statusCode, html, extraHeaders = {}) {
2216
2293
  res.end(html);
2217
2294
  }
2218
2295
 
2296
+ function sendRedirect(res, statusCode, location, extraHeaders = {}) {
2297
+ res.writeHead(statusCode, {
2298
+ Location: location,
2299
+ 'Cache-Control': 'no-store',
2300
+ ...extraHeaders
2301
+ });
2302
+ res.end('');
2303
+ }
2304
+
2219
2305
  function decodeSessionName(encoded) {
2220
2306
  try {
2221
2307
  return decodeURIComponent(encoded);
@@ -2563,6 +2649,12 @@ function bindTerminalWebSocket(ctx, state, ws, containerName, cols, rows) {
2563
2649
  }
2564
2650
 
2565
2651
  async function handleWebAuthRoutes(req, res, pathname, ctx, state) {
2652
+ if (req.method === 'GET' && pathname === '/favicon.ico') {
2653
+ res.writeHead(204, { 'Cache-Control': 'no-store' });
2654
+ res.end();
2655
+ return true;
2656
+ }
2657
+
2566
2658
  if (req.method === 'GET' && pathname === '/auth/login') {
2567
2659
  sendHtml(res, 200, loadTemplate('login.html'));
2568
2660
  return true;
@@ -2625,6 +2717,10 @@ function sendWebUnauthorized(res, pathname) {
2625
2717
  sendJson(res, 401, { error: 'UNAUTHORIZED' });
2626
2718
  return;
2627
2719
  }
2720
+ if (pathname === '/' || pathname === '') {
2721
+ sendRedirect(res, 302, '/auth/login', { 'Set-Cookie': getWebAuthClearCookie() });
2722
+ return;
2723
+ }
2628
2724
  sendHtml(
2629
2725
  res,
2630
2726
  401,
@@ -2648,14 +2744,7 @@ async function handleWebApi(req, res, pathname, ctx, state) {
2648
2744
  match: currentPath => currentPath === '/api/config' ? [] : null,
2649
2745
  handler: async () => {
2650
2746
  const snapshot = readWebConfigSnapshot(state.webConfigPath);
2651
- const defaults = buildConfigDefaults(ctx, snapshot.parseError ? {} : snapshot.parsed);
2652
- sendJson(res, 200, {
2653
- path: snapshot.path,
2654
- raw: snapshot.raw,
2655
- parsed: snapshot.parseError ? null : snapshot.parsed,
2656
- parseError: snapshot.parseError,
2657
- defaults
2658
- });
2747
+ sendJson(res, 200, buildSafeWebConfigSnapshot(snapshot, ctx));
2659
2748
  }
2660
2749
  },
2661
2750
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.7.2",
3
+ "version": "5.7.3",
4
4
  "imageVersion": "1.9.0-common",
5
5
  "playwrightCliVersion": "0.1.1",
6
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",