agim-cli 1.0.9 → 1.0.11
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/CHANGELOG.md +102 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/core/admin-allowlist.d.ts +20 -6
- package/dist/core/admin-allowlist.d.ts.map +1 -1
- package/dist/core/admin-allowlist.js +68 -15
- package/dist/core/admin-allowlist.js.map +1 -1
- package/dist/core/admin-bootstrap.d.ts +28 -0
- package/dist/core/admin-bootstrap.d.ts.map +1 -0
- package/dist/core/admin-bootstrap.js +132 -0
- package/dist/core/admin-bootstrap.js.map +1 -0
- package/dist/core/commands/builtin.d.ts.map +1 -1
- package/dist/core/commands/builtin.js +2 -0
- package/dist/core/commands/builtin.js.map +1 -1
- package/dist/core/commands/setup.d.ts +3 -0
- package/dist/core/commands/setup.d.ts.map +1 -0
- package/dist/core/commands/setup.js +64 -0
- package/dist/core/commands/setup.js.map +1 -0
- package/dist/core/restart-flow.d.ts.map +1 -1
- package/dist/core/restart-flow.js +48 -1
- package/dist/core/restart-flow.js.map +1 -1
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +7 -0
- package/dist/core/router.js.map +1 -1
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/web/public/settings.html +133 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +100 -0
- package/dist/web/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -139,6 +139,16 @@
|
|
|
139
139
|
safetyRestartHint: 'Click Save & Restart to apply.',
|
|
140
140
|
safetySaveRestart: 'Save & Restart',
|
|
141
141
|
safetySaved: '✓ Setting saved',
|
|
142
|
+
adminListTitle: 'Admin Allowlist',
|
|
143
|
+
adminListHint: 'Only these users can run /restart, /stop, and natural-language equivalents in IM. Edits persist to ~/.agim/env immediately — no restart needed.',
|
|
144
|
+
adminListLoading: 'Loading…',
|
|
145
|
+
adminListEmpty: '(no admins yet — IM service commands are disabled)',
|
|
146
|
+
adminUserIdPlaceholder: 'platform-specific user id (e.g. wxid_… / 123456 / ou_…)',
|
|
147
|
+
adminAdd: 'Add admin',
|
|
148
|
+
adminRemove: 'Remove',
|
|
149
|
+
adminConfirmRemove: 'Remove admin {p}:{u}?',
|
|
150
|
+
adminPublicBindWarn: '⚠️ Web is bound publicly — admin editor disabled to prevent random visitors from granting themselves access. Bind to 127.0.0.1 to enable.',
|
|
151
|
+
adminBootstrapHint: 'Bootstrap token exists at {path}. Run `cat` on the server, then send `/setup admin <token>` in IM to self-onboard.',
|
|
142
152
|
},
|
|
143
153
|
zh: {
|
|
144
154
|
title: 'Agim — 设置',
|
|
@@ -264,6 +274,16 @@
|
|
|
264
274
|
safetyRestartHint: '点「保存并重启」让改动生效。',
|
|
265
275
|
safetySaveRestart: '保存并重启',
|
|
266
276
|
safetySaved: '✓ 设置已保存',
|
|
277
|
+
adminListTitle: '管理员白名单',
|
|
278
|
+
adminListHint: '只有列表里的用户能在 IM 里跑 /restart / /stop 或自然语言"重启服务"。改动会立即写入 ~/.agim/env,无需重启。',
|
|
279
|
+
adminListLoading: '加载中…',
|
|
280
|
+
adminListEmpty: '(还没有管理员 — IM 服务管理命令处于禁用状态)',
|
|
281
|
+
adminUserIdPlaceholder: '该平台的 user id(如 wxid_… / 123456 / ou_…)',
|
|
282
|
+
adminAdd: '添加管理员',
|
|
283
|
+
adminRemove: '移除',
|
|
284
|
+
adminConfirmRemove: '移除管理员 {p}:{u}?',
|
|
285
|
+
adminPublicBindWarn: '⚠️ web 控制台目前监听公开地址 — 为防止陌生访问者擅自给自己加 admin,此编辑器已禁用。绑回 127.0.0.1 后启用。',
|
|
286
|
+
adminBootstrapHint: '检测到一次性 token 文件在 {path}。在服务器上 `cat` 该文件取 token,然后在 IM 里发 /setup admin <token> 即可把自己设为 admin。',
|
|
267
287
|
},
|
|
268
288
|
};
|
|
269
289
|
function t(key) { return T[window.__lang][key] || T.en[key] || key; }
|
|
@@ -717,6 +737,8 @@
|
|
|
717
737
|
void loadServiceStatus();
|
|
718
738
|
// Safety toggle reflects the current env value; load once on render.
|
|
719
739
|
void loadSafetyState();
|
|
740
|
+
// Admin allowlist list — loads via /api/admin-allowlist.
|
|
741
|
+
void loadAdminList();
|
|
720
742
|
}
|
|
721
743
|
|
|
722
744
|
// ==========================================
|
|
@@ -743,6 +765,26 @@
|
|
|
743
765
|
<div class="actions">
|
|
744
766
|
<button type="button" class="btn btn-primary" id="safety-save-restart">${t('safetySaveRestart')}</button>
|
|
745
767
|
</div>
|
|
768
|
+
<hr class="divider">
|
|
769
|
+
<div style="font-size:13px;font-weight:600;margin-bottom:6px">${t('adminListTitle')}</div>
|
|
770
|
+
<div class="hint" style="margin-bottom:10px">${t('adminListHint')}</div>
|
|
771
|
+
<div id="admin-list" style="margin-bottom:10px">${t('adminListLoading')}</div>
|
|
772
|
+
<div class="row">
|
|
773
|
+
<div>
|
|
774
|
+
<label>Platform</label>
|
|
775
|
+
<input type="text" id="admin-platform" placeholder="wechat-ilink / telegram / feishu …">
|
|
776
|
+
</div>
|
|
777
|
+
<div>
|
|
778
|
+
<label>User ID</label>
|
|
779
|
+
<input type="text" id="admin-userid" placeholder="${t('adminUserIdPlaceholder')}">
|
|
780
|
+
</div>
|
|
781
|
+
</div>
|
|
782
|
+
<div class="actions">
|
|
783
|
+
<button type="button" class="btn btn-primary" id="admin-add">${t('adminAdd')}</button>
|
|
784
|
+
</div>
|
|
785
|
+
<div class="hint" id="admin-public-warn" style="margin-top:8px;display:none;color:var(--yellow)">
|
|
786
|
+
${t('adminPublicBindWarn')}
|
|
787
|
+
</div>
|
|
746
788
|
</div>
|
|
747
789
|
`;
|
|
748
790
|
}
|
|
@@ -778,6 +820,69 @@
|
|
|
778
820
|
status.style.color = safetySkipPending ? 'var(--red)' : 'var(--text-dim)';
|
|
779
821
|
}
|
|
780
822
|
|
|
823
|
+
// ─── Admin allowlist editor ──────────────────────────────────
|
|
824
|
+
async function loadAdminList() {
|
|
825
|
+
const listEl = document.getElementById('admin-list');
|
|
826
|
+
const warnEl = document.getElementById('admin-public-warn');
|
|
827
|
+
if (!listEl) return;
|
|
828
|
+
try {
|
|
829
|
+
const data = await authFetch('/api/admin-allowlist').then(r => r.json());
|
|
830
|
+
const admins = data.admins || [];
|
|
831
|
+
if (admins.length === 0) {
|
|
832
|
+
let inner = `<div class="hint" style="font-style:italic">${esc(t('adminListEmpty'))}</div>`;
|
|
833
|
+
if (data.bootstrapAvailable) {
|
|
834
|
+
inner += `<div class="hint" style="margin-top:6px;color:var(--yellow)">${esc(t('adminBootstrapHint').replace('{path}', data.bootstrapTokenPath || '~/.agim/admin-bootstrap-token'))}</div>`;
|
|
835
|
+
}
|
|
836
|
+
listEl.innerHTML = inner;
|
|
837
|
+
} else {
|
|
838
|
+
listEl.innerHTML = admins.map(a => `
|
|
839
|
+
<div class="agent-row" style="padding:6px 0">
|
|
840
|
+
<div class="left">
|
|
841
|
+
<div>
|
|
842
|
+
<div style="font-family:monospace;font-size:13px">${esc(a.platform)}:${esc(a.userId)}</div>
|
|
843
|
+
</div>
|
|
844
|
+
</div>
|
|
845
|
+
<button type="button" class="btn btn-sm btn-danger" data-admin-remove='${esc(JSON.stringify({platform: a.platform, userId: a.userId}))}'>${esc(t('adminRemove'))}</button>
|
|
846
|
+
</div>
|
|
847
|
+
`).join('');
|
|
848
|
+
// Wire the remove buttons
|
|
849
|
+
listEl.querySelectorAll('[data-admin-remove]').forEach(btn => {
|
|
850
|
+
btn.addEventListener('click', async () => {
|
|
851
|
+
const meta = JSON.parse(btn.getAttribute('data-admin-remove') || '{}');
|
|
852
|
+
if (!confirm(t('adminConfirmRemove').replace('{p}', meta.platform).replace('{u}', meta.userId))) return;
|
|
853
|
+
try {
|
|
854
|
+
const res = await authFetch('/api/admin-allowlist', {
|
|
855
|
+
method: 'DELETE',
|
|
856
|
+
headers: { 'Content-Type': 'application/json' },
|
|
857
|
+
body: JSON.stringify(meta),
|
|
858
|
+
});
|
|
859
|
+
if (!res.ok) {
|
|
860
|
+
const j = await res.json().catch(() => ({}));
|
|
861
|
+
throw new Error(j.error || res.statusText);
|
|
862
|
+
}
|
|
863
|
+
await loadAdminList();
|
|
864
|
+
} catch (err) {
|
|
865
|
+
toast(t('error') + ': ' + (err && err.message ? err.message : err), 'error');
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
} catch (err) {
|
|
871
|
+
listEl.textContent = t('error') + ': ' + (err && err.message ? err.message : err);
|
|
872
|
+
}
|
|
873
|
+
// The public-bind warning shows when /api/service/status reports a
|
|
874
|
+
// non-loopback bind. Re-use that endpoint instead of adding another.
|
|
875
|
+
try {
|
|
876
|
+
const status = await authFetch('/api/service/status').then(r => r.json());
|
|
877
|
+
if (warnEl && status.web && status.web.bind && status.web.bind !== '127.0.0.1' && status.web.bind !== 'localhost' && status.web.bind !== '::1') {
|
|
878
|
+
warnEl.style.display = 'block';
|
|
879
|
+
// Also disable the Add button
|
|
880
|
+
const addBtn = document.getElementById('admin-add');
|
|
881
|
+
if (addBtn) addBtn.disabled = true;
|
|
882
|
+
}
|
|
883
|
+
} catch { /* non-fatal */ }
|
|
884
|
+
}
|
|
885
|
+
|
|
781
886
|
// ==========================================
|
|
782
887
|
// Service control card
|
|
783
888
|
// ==========================================
|
|
@@ -1336,6 +1441,34 @@
|
|
|
1336
1441
|
}
|
|
1337
1442
|
});
|
|
1338
1443
|
|
|
1444
|
+
// Add admin (Safety card → Admin Allowlist).
|
|
1445
|
+
document.getElementById('admin-add')?.addEventListener('click', async () => {
|
|
1446
|
+
const platform = (document.getElementById('admin-platform')?.value || '').trim();
|
|
1447
|
+
const userId = (document.getElementById('admin-userid')?.value || '').trim();
|
|
1448
|
+
if (!platform || !userId) {
|
|
1449
|
+
toast(t('error') + ': platform + userId required', 'error');
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
try {
|
|
1453
|
+
const res = await authFetch('/api/admin-allowlist', {
|
|
1454
|
+
method: 'POST',
|
|
1455
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1456
|
+
body: JSON.stringify({ platform, userId }),
|
|
1457
|
+
});
|
|
1458
|
+
if (!res.ok) {
|
|
1459
|
+
const j = await res.json().catch(() => ({}));
|
|
1460
|
+
throw new Error(j.error || res.statusText);
|
|
1461
|
+
}
|
|
1462
|
+
toast(t('savedMsg'), 'success');
|
|
1463
|
+
// Clear inputs + reload list
|
|
1464
|
+
document.getElementById('admin-platform').value = '';
|
|
1465
|
+
document.getElementById('admin-userid').value = '';
|
|
1466
|
+
await loadAdminList();
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
toast(`${t('error')}: ${err && err.message ? err.message : err}`, 'error');
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1339
1472
|
// Agent toggles — flip config.agents in memory + auto-manage
|
|
1340
1473
|
// defaultAgent (promote / demote as needed), then re-render so the
|
|
1341
1474
|
// default-agent dropdown reflects the new eligible set.
|
package/dist/web/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAqDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/web/server.ts"],"names":[],"mappings":"AAqDA,wBAAgB,iBAAiB,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,CAOrE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;CACrB,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAgoB/C"}
|
package/dist/web/server.js
CHANGED
|
@@ -431,6 +431,19 @@ export async function startWebServer(options) {
|
|
|
431
431
|
if (url.pathname === '/api/service/restart' && req.method === 'POST') {
|
|
432
432
|
return handleServiceRestart(res);
|
|
433
433
|
}
|
|
434
|
+
// Admin allowlist — list + add/remove via the Safety card. Locally
|
|
435
|
+
// gated: only allowed when bound to 127.0.0.1, since the page has
|
|
436
|
+
// no auth and we don't want random LAN visitors granting themselves
|
|
437
|
+
// admin. See `bindHost` upthread.
|
|
438
|
+
if (url.pathname === '/api/admin-allowlist' && req.method === 'GET') {
|
|
439
|
+
return handleAdminAllowlistGet(res);
|
|
440
|
+
}
|
|
441
|
+
if (url.pathname === '/api/admin-allowlist' && req.method === 'POST') {
|
|
442
|
+
return handleAdminAllowlistAdd(req, res, bindHost);
|
|
443
|
+
}
|
|
444
|
+
if (url.pathname === '/api/admin-allowlist' && req.method === 'DELETE') {
|
|
445
|
+
return handleAdminAllowlistRemove(req, res, bindHost);
|
|
446
|
+
}
|
|
434
447
|
res.writeHead(404);
|
|
435
448
|
res.end('Not found');
|
|
436
449
|
});
|
|
@@ -1072,6 +1085,93 @@ async function handleServiceRestart(res) {
|
|
|
1072
1085
|
}
|
|
1073
1086
|
sendJson(res, 200, { ok: true, mode: st.mode, restarting: true, message: result.message });
|
|
1074
1087
|
}
|
|
1088
|
+
// ============================================================
|
|
1089
|
+
// Admin allowlist endpoints (Safety card → Admin Users)
|
|
1090
|
+
// ============================================================
|
|
1091
|
+
//
|
|
1092
|
+
// All three are gated on the server's bindHost being a loopback address.
|
|
1093
|
+
// The web console has no auth, so we MUST not let a LAN visitor add
|
|
1094
|
+
// themselves as admin. When bindHost is 0.0.0.0 we hide the editor on
|
|
1095
|
+
// the frontend AND refuse here as a defense-in-depth.
|
|
1096
|
+
function isLocalBind(bindHost) {
|
|
1097
|
+
return bindHost === '127.0.0.1' || bindHost === '::1' || bindHost === 'localhost';
|
|
1098
|
+
}
|
|
1099
|
+
async function handleAdminAllowlistGet(res) {
|
|
1100
|
+
try {
|
|
1101
|
+
const { listAdmins, isAllowlistConfigured } = await import('../core/admin-allowlist.js');
|
|
1102
|
+
const { BOOTSTRAP_TOKEN_FILE } = await import('../core/admin-bootstrap.js');
|
|
1103
|
+
const hasBootstrap = existsSync(BOOTSTRAP_TOKEN_FILE);
|
|
1104
|
+
sendJson(res, 200, {
|
|
1105
|
+
admins: listAdmins(),
|
|
1106
|
+
configured: isAllowlistConfigured(),
|
|
1107
|
+
bootstrapAvailable: hasBootstrap,
|
|
1108
|
+
bootstrapTokenPath: BOOTSTRAP_TOKEN_FILE,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async function handleAdminAllowlistAdd(req, res, bindHost) {
|
|
1116
|
+
if (!isLocalBind(bindHost)) {
|
|
1117
|
+
sendJson(res, 403, {
|
|
1118
|
+
error: 'admin editor disabled when web is bound publicly (no auth on this endpoint).',
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
const body = await readBody(req, res);
|
|
1124
|
+
const parsed = JSON.parse(body || '{}');
|
|
1125
|
+
const platform = (parsed.platform || '').trim();
|
|
1126
|
+
const userId = (parsed.userId || '').trim();
|
|
1127
|
+
if (!platform || !userId) {
|
|
1128
|
+
sendJson(res, 400, { error: 'platform and userId required' });
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
const { promoteAdmin, listAdmins } = await import('../core/admin-allowlist.js');
|
|
1132
|
+
const added = await promoteAdmin(platform, userId);
|
|
1133
|
+
sendJson(res, 200, { ok: true, added, admins: listAdmins() });
|
|
1134
|
+
}
|
|
1135
|
+
catch (err) {
|
|
1136
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
async function handleAdminAllowlistRemove(req, res, bindHost) {
|
|
1140
|
+
if (!isLocalBind(bindHost)) {
|
|
1141
|
+
sendJson(res, 403, { error: 'admin editor disabled on public binds' });
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
const body = await readBody(req, res);
|
|
1146
|
+
const parsed = JSON.parse(body || '{}');
|
|
1147
|
+
const platform = (parsed.platform || '').trim().toLowerCase();
|
|
1148
|
+
const userId = (parsed.userId || '').trim();
|
|
1149
|
+
if (!platform || !userId) {
|
|
1150
|
+
sendJson(res, 400, { error: 'platform and userId required' });
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const { listAdmins } = await import('../core/admin-allowlist.js');
|
|
1154
|
+
const remaining = listAdmins().filter((a) => !(a.platform === platform && a.userId === userId));
|
|
1155
|
+
const newRaw = remaining.map((a) => `${a.platform}:${a.userId}`).join(',');
|
|
1156
|
+
// Persist to env file. Note this nukes both runtime + env-derived
|
|
1157
|
+
// entries — fine for the editor's use case (it round-trips through
|
|
1158
|
+
// the full list).
|
|
1159
|
+
const { updateEnvFile } = await import('../cli-ui/env-file.js');
|
|
1160
|
+
updateEnvFile({ IMHUB_ADMIN_USERS: newRaw || null });
|
|
1161
|
+
// Update process.env so getEntries() picks it up next call.
|
|
1162
|
+
if (newRaw)
|
|
1163
|
+
process.env.IMHUB_ADMIN_USERS = newRaw;
|
|
1164
|
+
else
|
|
1165
|
+
delete process.env.IMHUB_ADMIN_USERS;
|
|
1166
|
+
// Reset the parse cache so the change is visible immediately.
|
|
1167
|
+
const { _resetCache } = await import('../core/admin-allowlist.js');
|
|
1168
|
+
_resetCache();
|
|
1169
|
+
sendJson(res, 200, { ok: true, admins: remaining });
|
|
1170
|
+
}
|
|
1171
|
+
catch (err) {
|
|
1172
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1075
1175
|
/**
|
|
1076
1176
|
* POST /api/notify → push a message to an IM thread.
|
|
1077
1177
|
*
|