@yottameta/yotta-memory 0.13.1 → 0.16.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.
@@ -22,8 +22,12 @@ const os = require('os');
22
22
  const crypto = require('crypto');
23
23
  const http = require('http');
24
24
  const child_process = require('child_process');
25
+ const { AsyncLocalStorage } = require('async_hooks');
25
26
 
26
- const VERSION = '0.13.1';
27
+ const VERSION = '0.16.0';
28
+ // @generated view-html:start
29
+ const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\r\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\r\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\r\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\r\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\r\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\r\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\r\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\r\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\r\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\r\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\r\n</style></head><body>\r\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\r\n<div id=\"lock\" class=\"card\">\r\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></p>\r\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\r\n <button onclick=\"unlock()\">解锁</button>\r\n <div class=\"err\" id=\"lockerr\"></div>\r\n</div>\r\n<div id=\"app\">\r\n <div class=\"card\">\r\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\r\n <div class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/&lt;agent_id&gt;.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\r\n <div id=\"owners\" style=\"margin-top:8px\"></div>\r\n </div>\r\n <div class=\"card\">\r\n <b>记忆</b>\r\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\r\n <button onclick=\"off=0;load()\">搜索</button>\r\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\r\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\r\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\r\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\r\n <div id=\"entries\" style=\"margin-top:6px\"></div>\r\n <div id=\"pager\" style=\"margin-top:10px\">\r\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\r\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\r\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\r\n </div>\r\n </div>\r\n <div class=\"card\">\r\n <b>重设口令</b><br>\r\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\r\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\r\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\r\n <button onclick=\"resetPw()\">重设</button>\r\n <span id=\"pwout\"></span>\r\n </div>\r\n</div>\r\n<script>\r\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}\r\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\r\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\r\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\r\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\r\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\r\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\r\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){var owner=b.getAttribute('data-a');if(!confirm('确认由你为用户授权 '+owner+' 读取其私密记忆?授权后将生成只显示一次的 agent_key,请立即保存;同时写入待领取文件供该 AI 新会话领取。AI 不应代为执行该授权操作。'))return;b.disabled=true;api('/api/authorize',{owner:owner}).then(function(d){b.disabled=false;if(!d||d.error){alert((d&&d.error)||'授权失败');loadOwners();return;}if(d.agentKey){showKey(d.agentKey);}loadOwners();});};});\r\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){if(!confirm('确认吊销 '+b.getAttribute('data-r')+' 的 agent_key?吊销后该智能体立即失去私密读写能力。'))return;api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\r\nfunction showKey(k){var ov=document.createElement('div');ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index:99';var box=document.createElement('div');box.className='card';box.style.cssText='max-width:640px;word-break:break-all';var t=document.createElement('div');t.innerHTML='<b>agent_key(只显示一次)</b>';var hint=document.createElement('div');hint.className='meta';hint.textContent='请用户立即单独保存。AI 新会话先执行 yotta-memory key status <agent_id>,有 pending 再执行 key claim <agent_id>;默认写入 AI_HOME/.yotta-memory-agent-key,需要时用 --to 或 --agent-key-file 指定。若 key 丢失,可吊销后重新授权;旧 key 会立即校验失败。';var ta=document.createElement('textarea');ta.readOnly=true;ta.value=k;ta.style.cssText='width:100%;height:72px;margin-top:8px;font-family:monospace;font-size:12px';var close=document.createElement('button');close.textContent='我已保存,关闭';close.onclick=function(){ov.remove();};box.appendChild(t);box.appendChild(hint);box.appendChild(ta);box.appendChild(close);ov.appendChild(box);document.body.appendChild(ov);ta.focus();ta.select();}\r\n}\r\nlet off=0,PS=50;\r\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\r\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\r\nfunction nextPage(){off+=PS;load();}\r\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\r\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\r\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\r\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\r\nboot();\r\n</script></body></html>\r\n";
30
+ // @generated view-html:end
27
31
  // MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
28
32
  const MCP_PROTOCOL_MODERN = '2026-07-28';
29
33
  const MCP_PROTOCOL_LEGACY = '2025-11-25';
@@ -86,13 +90,555 @@ function memoryRoots() {
86
90
  }
87
91
  return out;
88
92
  }
93
+ // ---- v0.16.0 M2: runtime stable entry ----
94
+ const RUNTIME_SCHEMA = 1;
95
+ function runtimeRoot() {
96
+ if (process.env.YOTTA_MEMORY_RUNTIME_HOME) return path.resolve(process.env.YOTTA_MEMORY_RUNTIME_HOME);
97
+ return path.join(path.dirname(configPath()), 'runtime');
98
+ }
99
+ function runtimeManifestPath() { return path.join(runtimeRoot(), 'runtime.json'); }
100
+ function runtimeVersionsDir() { return path.join(runtimeRoot(), 'versions'); }
101
+ function runtimeVersionDir(version) { return path.join(runtimeVersionsDir(), String(version)); }
102
+ function runtimeCurrentDir() { return path.join(runtimeRoot(), 'current'); }
103
+ function runtimeCurrentBin() { return path.join(runtimeCurrentDir(), 'bin', 'yotta-memory.js'); }
104
+ function isSafeRuntimeVersion(version) {
105
+ const value = String(version || '').trim();
106
+ if (!value || value === '.' || value === '..') return false;
107
+ if (!/^[0-9A-Za-z][0-9A-Za-z.+-]*$/.test(value)) return false;
108
+ return !/[\\/]/.test(value);
109
+ }
110
+ function runtimePathWithin(parent, child) {
111
+ const p = path.resolve(parent);
112
+ const c = path.resolve(child);
113
+ return c === p || c.indexOf(p + path.sep) === 0;
114
+ }
115
+ function readRuntimeManifest() {
116
+ try {
117
+ const parsed = JSON.parse(fs.readFileSync(runtimeManifestPath(), 'utf8'));
118
+ if (!parsed || typeof parsed !== 'object') return null;
119
+ return parsed;
120
+ } catch (e) {
121
+ return null;
122
+ }
123
+ }
124
+ function writeRuntimeManifest(manifest) {
125
+ fs.mkdirSync(runtimeRoot(), { recursive: true });
126
+ const target = runtimeManifestPath();
127
+ const temp = target + '.tmp-' + process.pid + '-' + Date.now();
128
+ fs.writeFileSync(temp, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
129
+ fs.renameSync(temp, target);
130
+ }
131
+ function runtimeTreeHash(dir) {
132
+ const hash = crypto.createHash('sha256');
133
+ function walk(rel) {
134
+ const full = path.join(dir, rel);
135
+ const entries = fs.readdirSync(full, { withFileTypes: true })
136
+ .sort(function (a, b) { return a.name < b.name ? -1 : (a.name > b.name ? 1 : 0); });
137
+ for (const entry of entries) {
138
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
139
+ const childRel = rel ? path.join(rel, entry.name) : entry.name;
140
+ const childFull = path.join(dir, childRel);
141
+ if (entry.isDirectory()) {
142
+ walk(childRel);
143
+ } else if (entry.isFile()) {
144
+ hash.update(childRel.replace(/\\/g, '/') + '\0');
145
+ hash.update(fs.readFileSync(childFull));
146
+ hash.update('\0');
147
+ }
148
+ }
149
+ }
150
+ walk('');
151
+ return hash.digest('hex');
152
+ }
153
+ function runtimePackageVersion(dir) {
154
+ const pkgPath = path.join(dir, 'package.json');
155
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
156
+ const version = String(pkg.version || '').trim();
157
+ if (!isSafeRuntimeVersion(version)) throw new Error('runtime 包版本非法: ' + version);
158
+ return version;
159
+ }
160
+ function runtimeVerifyVersionDir(dir, expected, manifest) {
161
+ if (!fs.existsSync(dir)) return { error: true, text: 'runtime 版本目录不存在: ' + dir };
162
+ let version = '';
163
+ try { version = runtimePackageVersion(dir); } catch (e) { return { error: true, text: e.message }; }
164
+ if (version !== expected) return { error: true, text: 'runtime 版本不一致: 目录为 ' + version + ',请求 ' + expected };
165
+ if (!fs.existsSync(path.join(dir, 'bin', 'yotta-memory.js'))) {
166
+ return { error: true, text: 'runtime 版本缺少 bin/yotta-memory.js: ' + dir };
167
+ }
168
+ const treeHash = runtimeTreeHash(dir);
169
+ const record = manifest && manifest.versions && manifest.versions[expected];
170
+ if (record && record.treeHash && record.treeHash !== treeHash) {
171
+ return { error: true, text: 'runtime 版本内容哈希与 runtime.json 不一致;请重新安装 ' + expected, treeHash: treeHash };
172
+ }
173
+ return { error: false, treeHash: treeHash };
174
+ }
175
+ function compareRuntimeVersions(a, b) {
176
+ const pa = String(a).split('.').map(function (x) { return parseInt(x, 10) || 0; });
177
+ const pb = String(b).split('.').map(function (x) { return parseInt(x, 10) || 0; });
178
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
179
+ const d = (pa[i] || 0) - (pb[i] || 0);
180
+ if (d !== 0) return d;
181
+ }
182
+ return String(a).localeCompare(String(b));
183
+ }
184
+ function runtimeListVersions() {
185
+ const dir = runtimeVersionsDir();
186
+ if (!fs.existsSync(dir)) return [];
187
+ return fs.readdirSync(dir).filter(function (name) {
188
+ if (!isSafeRuntimeVersion(name)) return false;
189
+ return fs.existsSync(path.join(dir, name, 'package.json'));
190
+ }).sort(function (a, b) { return compareRuntimeVersions(b, a); });
191
+ }
192
+ function runtimeManagedScript() {
193
+ const current = runtimeCurrentBin();
194
+ return fs.existsSync(current) ? current : __filename;
195
+ }
196
+ function runtimePatternToRegExp(pattern) {
197
+ const escaped = String(pattern).split('*').map(function (part) {
198
+ return part.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
199
+ }).join('.*');
200
+ return new RegExp('^' + escaped + '$');
201
+ }
202
+ function runtimeMatchExclude(rel, pattern) {
203
+ const r = String(rel).replace(/\\/g, '/');
204
+ const p = String(pattern).replace(/\\/g, '/').replace(/^\.\//, '');
205
+ if (p.indexOf('*') === -1) return r === p || r.indexOf(p.replace(/\/$/, '') + '/') === 0;
206
+ return runtimePatternToRegExp(p).test(r);
207
+ }
208
+ function runtimeCopyTreeFiltered(src, dst, excludes, relBase) {
209
+ const base = relBase ? relBase + '/' : '';
210
+ const entries = fs.readdirSync(src, { withFileTypes: true });
211
+ fs.mkdirSync(dst, { recursive: true });
212
+ for (const entry of entries) {
213
+ if (entry.name === '.git' || entry.name === 'node_modules') continue;
214
+ const rel = base + entry.name;
215
+ if (excludes.some(function (pattern) { return runtimeMatchExclude(rel, pattern); })) continue;
216
+ const source = path.join(src, entry.name);
217
+ const target = path.join(dst, entry.name);
218
+ if (entry.isDirectory()) runtimeCopyTreeFiltered(source, target, excludes, rel);
219
+ else if (entry.isFile()) fs.copyFileSync(source, target);
220
+ }
221
+ }
222
+ function runtimeCopyTree(src, dst) {
223
+ const st = fs.statSync(src);
224
+ if (st.isDirectory()) {
225
+ runtimeCopyTreeFiltered(src, dst, [], '');
226
+ } else if (st.isFile()) {
227
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
228
+ fs.copyFileSync(src, dst);
229
+ }
230
+ }
231
+ function runtimeCopyPackageFromRoot(srcRoot, dstRoot) {
232
+ const pkg = JSON.parse(fs.readFileSync(path.join(srcRoot, 'package.json'), 'utf8'));
233
+ const files = Array.isArray(pkg.files) ? pkg.files : [];
234
+ const includes = ['package.json'].concat(files.filter(function (entry) { return entry && entry[0] !== '!'; }));
235
+ const excludes = files.filter(function (entry) { return entry && entry[0] === '!'; }).map(function (entry) { return entry.slice(1); });
236
+ for (const entry of includes) {
237
+ const rel = String(entry).replace(/^\.\//, '');
238
+ const source = path.join(srcRoot, rel);
239
+ if (!fs.existsSync(source)) continue;
240
+ const target = path.join(dstRoot, rel);
241
+ const st = fs.statSync(source);
242
+ if (st.isDirectory()) {
243
+ runtimeCopyTreeFiltered(source, target, excludes, rel);
244
+ } else if (st.isFile() && !excludes.some(function (pattern) { return runtimeMatchExclude(rel, pattern); })) {
245
+ fs.mkdirSync(path.dirname(target), { recursive: true });
246
+ fs.copyFileSync(source, target);
247
+ }
248
+ }
249
+ }
250
+ function runtimeRemoveVersionDir(version) {
251
+ const target = runtimeVersionDir(version);
252
+ if (!runtimePathWithin(runtimeVersionsDir(), target)) throw new Error('拒绝删除 runtime 目录外的路径: ' + target);
253
+ if (fs.existsSync(target)) fs.rmSync(target, { recursive: true, force: true });
254
+ }
255
+ function runtimeSwitchCurrent(version) {
256
+ const dir = runtimeVersionDir(version);
257
+ if (!fs.existsSync(path.join(dir, 'package.json'))) throw new Error('runtime 版本未安装: ' + version);
258
+ fs.mkdirSync(runtimeRoot(), { recursive: true });
259
+ const temp = path.join(runtimeRoot(), '.current-' + process.pid + '-' + Date.now());
260
+ const current = runtimeCurrentDir();
261
+ try {
262
+ fs.symlinkSync(dir, temp, process.platform === 'win32' ? 'junction' : 'dir');
263
+ if (fs.existsSync(current)) {
264
+ const st = fs.lstatSync(current);
265
+ if (st.isSymbolicLink() || st.isFile()) fs.unlinkSync(current);
266
+ else throw new Error('current 已存在且不是可替换的指针: ' + current);
267
+ }
268
+ fs.renameSync(temp, current);
269
+ } catch (e) {
270
+ try { if (fs.existsSync(temp)) fs.unlinkSync(temp); } catch (ignore) {}
271
+ throw e;
272
+ }
273
+ }
274
+ function runtimeInstallPrepared(pkgRoot, version, opts) {
275
+ opts = opts || {};
276
+ if (!isSafeRuntimeVersion(version)) return { error: true, text: '非法 runtime 版本: ' + version };
277
+ fs.mkdirSync(runtimeVersionsDir(), { recursive: true });
278
+ const finalDir = runtimeVersionDir(version);
279
+ const staging = path.join(runtimeVersionsDir(), '.' + version + '.tmp-' + process.pid + '-' + Date.now());
280
+ try {
281
+ runtimeCopyTree(pkgRoot, staging);
282
+ if (runtimePackageVersion(staging) !== version) throw new Error('安装包版本与目标版本不一致');
283
+ if (!fs.existsSync(path.join(staging, 'bin', 'yotta-memory.js'))) throw new Error('安装包缺少 bin/yotta-memory.js');
284
+ const treeHash = runtimeTreeHash(staging);
285
+ if (fs.existsSync(finalDir)) {
286
+ const existingHash = runtimeTreeHash(finalDir);
287
+ if (existingHash === treeHash) {
288
+ fs.rmSync(staging, { recursive: true, force: true });
289
+ return { error: false, installed: false, version: version, treeHash: treeHash, path: finalDir, text: 'runtime ' + version + ' 已安装且内容一致。' };
290
+ }
291
+ if (!opts.force) throw new Error('runtime ' + version + ' 已安装且内容不同;如需覆盖请加 --force。');
292
+ runtimeRemoveVersionDir(version);
293
+ }
294
+ fs.renameSync(staging, finalDir);
295
+ const previousManifest = readRuntimeManifest();
296
+ const manifest = previousManifest || { schema: RUNTIME_SCHEMA, current: '', previous: '', versions: {}, updatedAt: '' };
297
+ manifest.schema = RUNTIME_SCHEMA;
298
+ manifest.versions = manifest.versions || {};
299
+ manifest.versions[version] = Object.assign({}, manifest.versions[version], {
300
+ version: version,
301
+ path: finalDir,
302
+ treeHash: treeHash,
303
+ installedAt: new Date().toISOString(),
304
+ source: opts.source || 'unknown',
305
+ });
306
+ if (!manifest.current) manifest.current = version;
307
+ manifest.updatedAt = new Date().toISOString();
308
+ writeRuntimeManifest(manifest);
309
+ if (manifest.current === version) {
310
+ try {
311
+ runtimeSwitchCurrent(version);
312
+ } catch (e) {
313
+ if (previousManifest) writeRuntimeManifest(previousManifest);
314
+ return { error: true, text: '切换 current 失败: ' + e.message };
315
+ }
316
+ }
317
+ return { error: false, installed: true, version: version, treeHash: treeHash, path: finalDir, text: '已安装 runtime ' + version + ' 到 ' + finalDir };
318
+ } catch (e) {
319
+ try { if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true }); } catch (ignore) {}
320
+ return { error: true, text: e.message };
321
+ }
322
+ }
323
+ function runtimeValidateTarballEntries(entries) {
324
+ const normalized = (entries || []).map(function (entry) {
325
+ return String(entry || '').replace(/\\/g, '/').replace(/\/+$/, '');
326
+ }).filter(Boolean);
327
+ if (!normalized.length) throw new Error('runtime tarball 为空');
328
+ for (const entry of normalized) {
329
+ if (entry[0] === '/' || /^[A-Za-z]:/.test(entry)) throw new Error('runtime tarball 含绝对路径成员: ' + entry);
330
+ const parts = entry.split('/');
331
+ if (parts.indexOf('..') !== -1) throw new Error('runtime tarball 含路径穿越成员: ' + entry);
332
+ if (parts[0] !== 'package') throw new Error('runtime tarball 含 package/ 之外的成员: ' + entry);
333
+ }
334
+ if (normalized.indexOf('package/package.json') === -1) throw new Error('runtime tarball 缺少 package/package.json');
335
+ }
336
+ function runtimeListTarballEntries(tarball) {
337
+ const tarBin = process.env.YOTTA_RUNTIME_TAR || 'tar';
338
+ const listed = child_process.spawnSync(tarBin, ['-tzf', tarball], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
339
+ if (listed.error) throw new Error('无法执行 tar: ' + listed.error.message);
340
+ if (listed.status !== 0) throw new Error('读取 runtime tarball 失败: ' + String(listed.stderr || listed.stdout || '').trim());
341
+ return String(listed.stdout || '').split(/\r?\n/).filter(Boolean);
342
+ }
343
+ function runtimeExtractTarball(tarball, tempDir) {
344
+ runtimeValidateTarballEntries(runtimeListTarballEntries(tarball));
345
+ const tarBin = process.env.YOTTA_RUNTIME_TAR || 'tar';
346
+ const result = child_process.spawnSync(tarBin, ['-xzf', tarball, '-C', tempDir], { encoding: 'utf8' });
347
+ if (result.error) throw new Error('无法执行 tar: ' + result.error.message);
348
+ if (result.status !== 0) throw new Error('解压 runtime tarball 失败: ' + String(result.stderr || result.stdout || '').trim());
349
+ const packaged = path.join(tempDir, 'package');
350
+ if (fs.existsSync(path.join(packaged, 'package.json'))) return packaged;
351
+ if (fs.existsSync(path.join(tempDir, 'package.json'))) return tempDir;
352
+ throw new Error('runtime tarball 中未找到 package/package.json');
353
+ }
354
+ function runtimeInstallFromCurrent(opts) {
355
+ opts = opts || {};
356
+ const srcRoot = path.join(__dirname, '..');
357
+ const pkgPath = path.join(srcRoot, 'package.json');
358
+ if (!fs.existsSync(pkgPath)) return { error: true, text: '当前运行时不是完整 npm 包(缺少 package.json),无法安装稳定入口。' };
359
+ let version = '';
360
+ try { version = runtimePackageVersion(srcRoot); } catch (e) { return { error: true, text: e.message }; }
361
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-current-'));
362
+ try {
363
+ const staged = path.join(temp, 'package');
364
+ runtimeCopyPackageFromRoot(srcRoot, staged);
365
+ return runtimeInstallPrepared(staged, version, { source: 'current', force: !!opts.force });
366
+ } catch (e) {
367
+ return { error: true, text: e.message };
368
+ } finally {
369
+ try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
370
+ }
371
+ }
372
+ function runtimeInstallTarball(tarball, requestedVersion, opts) {
373
+ opts = opts || {};
374
+ const abs = path.resolve(String(tarball || ''));
375
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) return { error: true, text: 'runtime tarball 不存在: ' + abs };
376
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-tarball-'));
377
+ try {
378
+ const pkgRoot = runtimeExtractTarball(abs, temp);
379
+ const version = runtimePackageVersion(pkgRoot);
380
+ if (requestedVersion && String(requestedVersion) !== version) {
381
+ throw new Error('runtime 包版本 ' + version + ' 与请求版本 ' + requestedVersion + ' 不一致');
382
+ }
383
+ return runtimeInstallPrepared(pkgRoot, version, { source: abs, force: !!opts.force });
384
+ } catch (e) {
385
+ return { error: true, text: e.message };
386
+ } finally {
387
+ try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
388
+ }
389
+ }
390
+ function runtimeNpmSpec() {
391
+ const candidates = [];
392
+ if (process.env.npm_execpath) candidates.push(process.env.npm_execpath);
393
+ candidates.push(path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
394
+ candidates.push(path.resolve(path.dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'));
395
+ for (const candidate of candidates) {
396
+ if (candidate && fs.existsSync(candidate)) return { command: process.execPath, args: [path.resolve(candidate)] };
397
+ }
398
+ return null;
399
+ }
400
+ function runtimeRunNpm(args, opts) {
401
+ const spec = runtimeNpmSpec();
402
+ if (!spec) throw new Error('无法定位 npm CLI;请改用 runtime install <本地 tarball>。');
403
+ const result = child_process.spawnSync(spec.command, spec.args.concat(args), {
404
+ cwd: (opts && opts.cwd) || process.cwd(),
405
+ encoding: 'utf8',
406
+ timeout: 120000,
407
+ });
408
+ if (result.error) throw new Error('npm 执行失败: ' + result.error.message);
409
+ if (result.status !== 0) throw new Error('npm 执行失败: ' + String(result.stderr || result.stdout || '').trim());
410
+ return result;
411
+ }
412
+ function runtimeInstallVersion(version, opts) {
413
+ const target = String(version || '').trim();
414
+ if (!isSafeRuntimeVersion(target)) return { error: true, text: '非法 runtime 版本: ' + target };
415
+ if (target === VERSION) return runtimeInstallFromCurrent(opts);
416
+ const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'ytm-runtime-pack-'));
417
+ try {
418
+ fs.mkdirSync(runtimeRoot(), { recursive: true });
419
+ runtimeRunNpm(['pack', '@yottameta/yotta-memory@' + target, '--pack-destination', temp], { cwd: runtimeRoot() });
420
+ const tarball = fs.readdirSync(temp).filter(function (name) { return /\.tgz$/i.test(name); })[0];
421
+ if (!tarball) throw new Error('npm pack 未生成 tarball');
422
+ return runtimeInstallTarball(path.join(temp, tarball), target, opts);
423
+ } catch (e) {
424
+ return { error: true, text: e.message };
425
+ } finally {
426
+ try { fs.rmSync(temp, { recursive: true, force: true }); } catch (ignore) {}
427
+ }
428
+ }
429
+ function runtimeUseCore(version, opts) {
430
+ opts = opts || {};
431
+ const target = String(version || '').trim();
432
+ if (!isSafeRuntimeVersion(target)) return { error: true, text: '非法 runtime 版本: ' + target };
433
+ const manifest = readRuntimeManifest();
434
+ if (!manifest) return { error: true, text: 'runtime 未初始化,请先运行 yotta-memory runtime install --from-current。' };
435
+ const dir = runtimeVersionDir(target);
436
+ if (!fs.existsSync(dir)) return { error: true, text: 'runtime 版本未安装: ' + target };
437
+ const check = runtimeVerifyVersionDir(dir, target, manifest);
438
+ if (check.error) return check;
439
+ const previous = manifest.current && manifest.current !== target ? manifest.current : (manifest.previous && manifest.previous !== target ? manifest.previous : '');
440
+ const previousManifest = JSON.parse(JSON.stringify(manifest));
441
+ try {
442
+ runtimeSwitchCurrent(target);
443
+ } catch (e) {
444
+ return { error: true, text: '切换 current 失败: ' + e.message };
445
+ }
446
+ manifest.schema = RUNTIME_SCHEMA;
447
+ manifest.current = target;
448
+ manifest.previous = previous || '';
449
+ manifest.updatedAt = new Date().toISOString();
450
+ manifest.versions = manifest.versions || {};
451
+ manifest.versions[target] = Object.assign({}, manifest.versions[target], { treeHash: check.treeHash, lastUsedAt: new Date().toISOString() });
452
+ try {
453
+ writeRuntimeManifest(manifest);
454
+ } catch (e) {
455
+ try { if (previous) runtimeSwitchCurrent(previous); } catch (ignore) {}
456
+ return { error: true, text: '写入 runtime.json 失败: ' + e.message };
457
+ }
458
+ let tail = ';如需重启受管 server,请加 --restart';
459
+ if (opts.restart) {
460
+ const restart = opts.restartFn ? opts.restartFn() : runtimeRestartManagedServer();
461
+ if (restart && restart.error) {
462
+ let rollbackText = '';
463
+ if (previous) {
464
+ try {
465
+ runtimeSwitchCurrent(previous);
466
+ const rollback = JSON.parse(JSON.stringify(previousManifest));
467
+ rollback.current = previous;
468
+ rollback.previous = target;
469
+ rollback.updatedAt = new Date().toISOString();
470
+ writeRuntimeManifest(rollback);
471
+ (opts.restartFn ? opts.restartFn() : runtimeRestartManagedServer());
472
+ rollbackText = ';已回滚 current 到 ' + previous;
473
+ } catch (e) {
474
+ rollbackText = ';回滚失败: ' + e.message;
475
+ }
476
+ }
477
+ return { error: true, text: '受管 server 重启失败: ' + (restart.text || '') + rollbackText };
478
+ }
479
+ tail = ';受管 server 已重启';
480
+ }
481
+ return { error: false, text: '已切换 current 到 ' + target + tail + '。', version: target, treeHash: check.treeHash };
482
+ }
483
+ function runtimeRollbackCore(opts) {
484
+ const manifest = readRuntimeManifest();
485
+ if (!manifest || !manifest.current) return { error: true, text: 'runtime 未初始化或没有当前版本。' };
486
+ if (!manifest.previous) return { error: true, text: '没有可回滚的 runtime 版本。' };
487
+ const target = manifest.previous;
488
+ const result = runtimeUseCore(target, opts);
489
+ if (result.error) return result;
490
+ return { error: false, text: '已回滚 current 到 ' + target + '。' + result.text, version: target };
491
+ }
492
+ function runtimeStatusCore() {
493
+ const root = runtimeRoot();
494
+ const manifest = readRuntimeManifest();
495
+ const lines = ['runtime root: ' + root];
496
+ const drift = [];
497
+ if (!manifest) {
498
+ lines.push('runtime: 未初始化');
499
+ lines.push('提示: yotta-memory runtime install --from-current');
500
+ return { error: false, text: lines.join('\n'), runtimeRoot: root, drift: ['runtime manifest missing'] };
501
+ }
502
+ lines.push('current: ' + (manifest.current || '(none)'));
503
+ lines.push('current path: ' + runtimeCurrentDir());
504
+ if (manifest.previous) lines.push('previous: ' + manifest.previous);
505
+ if (!manifest.current) drift.push('current version missing in runtime.json');
506
+ else {
507
+ const dir = runtimeVersionDir(manifest.current);
508
+ if (!fs.existsSync(dir)) drift.push('current version dir missing');
509
+ if (!fs.existsSync(runtimeCurrentBin())) drift.push('current launcher missing');
510
+ else {
511
+ try {
512
+ const real = fs.realpathSync(runtimeCurrentDir());
513
+ if (path.resolve(real) !== path.resolve(dir)) drift.push('current pointer mismatch');
514
+ } catch (e) {
515
+ drift.push('current pointer unreadable');
516
+ }
517
+ }
518
+ }
519
+ const versions = runtimeListVersions();
520
+ lines.push('versions: ' + (versions.length ? versions.join(', ') : '(none)'));
521
+ lines.push('drift: ' + (drift.length ? drift.join('; ') : 'none'));
522
+ return { error: false, text: lines.join('\n'), runtimeRoot: root, manifest: manifest, drift: drift };
523
+ }
524
+ function runtimeListCore() {
525
+ const manifest = readRuntimeManifest() || { current: '', versions: {} };
526
+ const versions = runtimeListVersions();
527
+ const lines = ['runtime versions:'];
528
+ if (!versions.length) lines.push(' (none)');
529
+ for (const version of versions) {
530
+ const record = (manifest.versions && manifest.versions[version]) || {};
531
+ lines.push(' ' + version + (manifest.current === version ? ' [current]' : '') + (record.treeHash ? ' ' + record.treeHash.slice(0, 12) + '...' : ''));
532
+ }
533
+ return { error: false, text: lines.join('\n'), versions: versions, current: manifest.current || '' };
534
+ }
535
+ function runtimeExecutionPath() {
536
+ const invoked = process.argv && process.argv[1] ? path.resolve(process.argv[1]) : '';
537
+ if (invoked && path.basename(invoked).toLowerCase() === 'yotta-memory.js') return invoked;
538
+ return path.resolve(__filename);
539
+ }
540
+ function runtimeEnsureForManagedTask() {
541
+ const manifest = readRuntimeManifest();
542
+ if (manifest && manifest.current) {
543
+ const check = runtimeVerifyVersionDir(runtimeVersionDir(manifest.current), manifest.current, manifest);
544
+ if (!check.error && fs.existsSync(runtimeCurrentBin())) {
545
+ return { error: false, text: 'runtime current 已就绪: ' + manifest.current, version: manifest.current };
546
+ }
547
+ }
548
+ return runtimeInstallFromCurrent({ force: false });
549
+ }
550
+ function runtimeRestartManagedServer() {
551
+ const platform = lanPlatform();
552
+ if (platform === 'win32') {
553
+ const query = child_process.spawnSync('schtasks', ['/query', '/tn', LAN_TASK_NAME, '/fo', 'LIST'], { encoding: 'utf8' });
554
+ if (query.status !== 0) return { error: true, text: '未检测到受管计划任务 ' + LAN_TASK_NAME + '。' };
555
+ const end = child_process.spawnSync('schtasks', ['/end', '/tn', LAN_TASK_NAME], { encoding: 'utf8' });
556
+ const run = child_process.spawnSync('schtasks', ['/run', '/tn', LAN_TASK_NAME], { encoding: 'utf8' });
557
+ if (run.status !== 0) return { error: true, text: '计划任务重启失败: ' + String(run.stderr || run.stdout || '').trim() };
558
+ return { error: false, text: '已重启计划任务 ' + LAN_TASK_NAME + (end.status === 0 ? '' : '(原任务未在运行)') };
559
+ }
560
+ if (platform === 'linux') {
561
+ const unit = lanLinuxUnitPath();
562
+ if (fs.existsSync(unit) && lanFileHasMarker(unit)) {
563
+ const result = lanLinuxSystemctl(['restart', LAN_UNIT_NAME]);
564
+ if (result.status !== 0) return { error: true, text: 'systemd 重启失败: ' + String(result.err || '').trim() };
565
+ return { error: false, text: '已重启 systemd 用户单元 ' + LAN_UNIT_NAME };
566
+ }
567
+ if (lanCrontabHasOurLine(lanCrontabRead())) {
568
+ return { error: true, text: '受管 server 由 crontab @reboot 启动,无法在线重启;请手动重启。' };
569
+ }
570
+ }
571
+ return { error: true, text: '未检测到可管理的 server 自启配置,未执行重启。' };
572
+ }
89
573
  function today() {
90
574
  const d = new Date();
91
575
  const p = (n) => String(n).padStart(2, '0');
92
576
  return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
93
577
  }
94
- function currentAgent() {
95
- return process.env.YOTTA_AGENT_ID || process.env.AGENT_ID || '';
578
+ let RUNTIME_AGENT = { id: '', agentKey: '' };
579
+ const IDENTITY_CONTEXT = new AsyncLocalStorage();
580
+ const RUNTIME_OWNER_KEYS = new Map();
581
+ function setRuntimeAgent(id, agentKey) {
582
+ RUNTIME_AGENT = { id: String(id || '').trim(), agentKey: String(agentKey || '').trim() };
583
+ RUNTIME_OWNER_KEYS.clear();
584
+ }
585
+ function currentRuntimeIdentity() {
586
+ return IDENTITY_CONTEXT.getStore() || RUNTIME_AGENT;
587
+ }
588
+ function runWithIdentity(identity, fn) {
589
+ const store = {
590
+ id: String((identity && identity.id) || '').trim(),
591
+ agentKey: String((identity && identity.agentKey) || '').trim(),
592
+ };
593
+ return IDENTITY_CONTEXT.run(store, fn);
594
+ }
595
+ function legacyIdentityEnvNames() {
596
+ return ['YOTTA_AGENT_ID', 'AGENT_ID', 'YOTTA_MEMORY_AGENT_KEY', 'YOTTA_MEMORY_TRUST_ENV_AGENT']
597
+ .filter(function (name) { return String(process.env[name] || '').trim() !== ''; });
598
+ }
599
+ function legacyIdentityEnvError() {
600
+ const names = legacyIdentityEnvNames();
601
+ if (!names.length) return '';
602
+ return [
603
+ '[YTM_IDENTITY_ENV_REMOVED] 检测到旧身份环境变量: ' + names.join(', '),
604
+ '0.16.0 已删除身份 env:HTTP / 远程 MCP 只用请求头 Authorization + X-Agent-Id + X-Agent-Key;stdio MCP 只用显式参数 --agent-id <id> + --agent-key-file <path>。',
605
+ 'CLI 仍使用 --agent <id> + --agent-key/--agent-key-file。请从宿主 MCP 配置中移除身份 env 后重试。',
606
+ ].join('\n');
607
+ }
608
+ function resolveIdentity(opts) {
609
+ opts = opts || {};
610
+ const explicitAgent = String(opts.agent || opts.selfAgent || '').trim();
611
+ const explicitAgentId = String(opts.agentId || '').trim();
612
+ if (explicitAgent && explicitAgentId && explicitAgent !== explicitAgentId) {
613
+ return { id: '', agentKey: '', source: 'conflict', error: '身份冲突:--agent ' + explicitAgent + ' 与 --agent-id ' + explicitAgentId + ' 不一致,请只保留一个身份参数。' };
614
+ }
615
+ const explicit = explicitAgent || explicitAgentId;
616
+ let explicitKey = String(opts.agentKey || '').trim();
617
+ const keyFile = String(opts.agentKeyFile || '').trim();
618
+ if (keyFile) {
619
+ let fileKey = '';
620
+ try { fileKey = fs.readFileSync(path.resolve(keyFile), 'utf8').trim(); } catch (e) {
621
+ return { id: '', agentKey: '', source: 'invalid-key-file', error: '无法读取 agent-key 文件: ' + keyFile };
622
+ }
623
+ if (!fileKey) return { id: '', agentKey: '', source: 'invalid-key-file', error: 'agent-key 文件为空: ' + keyFile };
624
+ if (explicitKey && explicitKey !== fileKey) return { id: '', agentKey: '', source: 'key-conflict', error: '--agent-key 与 --agent-key-file 内容不一致。' };
625
+ explicitKey = fileKey;
626
+ }
627
+ if (explicit && !isSafeAgentId(explicit)) {
628
+ return { id: '', agentKey: '', source: 'invalid', error: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
629
+ }
630
+ if (explicit) {
631
+ return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
632
+ }
633
+ return { id: '', agentKey: '', source: 'none', error: '' };
634
+ }
635
+ function isSafeAgentId(id) {
636
+ const value = String(id || '');
637
+ if (!value || value.length > 128) return false;
638
+ if (value === '.' || value === '..') return false;
639
+ if (/[\\/]/.test(value)) return false;
640
+ if (/[\u0000-\u001f]/.test(value)) return false;
641
+ return true;
96
642
  }
97
643
  function typeDir(type) {
98
644
  const t = String(type).toUpperCase();
@@ -528,8 +1074,12 @@ function nextSeq(dir) {
528
1074
  const ENC_MAGIC = 'YTMENC1';
529
1075
  const IDX_MAGIC = 'YTMIDX1';
530
1076
  const KEY_MAGIC = 'YTMKEY1';
1077
+ const AGENT_MAGIC = 'YTMAGENT1';
531
1078
  const KEYS_DIR = 'keys';
532
1079
  const KEY_CACHE_DIR = 'cache';
1080
+ const AGENT_BINDINGS_DIR = 'bindings';
1081
+ const AGENT_PENDING_DIR = 'pending';
1082
+ const AGENT_KEY_FILENAME = '.yotta-memory-agent-key';
533
1083
  const ENC_INDEX_FILE = 'index.enc';
534
1084
  const ENC_SUFFIX = '.enc';
535
1085
  const PBKDF2_ITER = 600000;
@@ -544,6 +1094,53 @@ function encOwnerRecPath(root, id) { return path.join(encKeysDir(root), id + '.k
544
1094
  function encRecoveryEncPath(root) { return path.join(encKeysDir(root), 'recovery.key.enc'); }
545
1095
  function encCacheDir(root) { return path.join(encKeysDir(root), KEY_CACHE_DIR); }
546
1096
  function encCachePath(root, id) { return path.join(encCacheDir(root), id + '.key'); }
1097
+ function encBindingsDir(root) { return path.join(encKeysDir(root), AGENT_BINDINGS_DIR); }
1098
+ function encAgentBindingPath(root, id) { return path.join(encBindingsDir(root), id + '.key.agent'); }
1099
+ function encPendingDir(root) { return path.join(encKeysDir(root), AGENT_PENDING_DIR); }
1100
+ function encPendingPath(root, id) { return path.join(encPendingDir(root), id + '.key'); }
1101
+ function agentKeyFilePath(agentHome) { return path.join(path.resolve(agentHome), AGENT_KEY_FILENAME); }
1102
+ function detectAgentHost(owner) {
1103
+ const hint = String(process.env.YOTTA_MEMORY_AGENT_HOST || '').trim().toLowerCase();
1104
+ if (hint === 'codex' || hint === 'opencode') return hint;
1105
+ const id = String(owner || '').toLowerCase();
1106
+ if (/(^|[-_.])opencode([-_.]|$)/.test(id)) return 'opencode';
1107
+ if (id === 'codex' || id.indexOf('codex-') === 0) return 'codex';
1108
+ if (process.env.OPENCODE_API_KEY || process.env.OPENCODE_CONFIG) return 'opencode';
1109
+ if (process.env.CODEX_HOME) return 'codex';
1110
+ return 'generic';
1111
+ }
1112
+ function resolveAgentKeyLocation(owner, opts) {
1113
+ opts = opts || {};
1114
+ const explicitHome = String(opts.to || '').trim();
1115
+ const explicitFile = String(opts.agentKeyFile || '').trim();
1116
+ if (explicitHome && explicitFile) {
1117
+ return { ok: false, error: '--to 与 --agent-key-file 不能同时使用。' };
1118
+ }
1119
+ if (explicitFile) {
1120
+ return { ok: true, file: path.resolve(explicitFile), source: 'option:--agent-key-file' };
1121
+ }
1122
+ if (explicitHome) {
1123
+ return { ok: true, file: agentKeyFilePath(explicitHome), source: 'option:--to' };
1124
+ }
1125
+ const envFile = String(process.env.YOTTA_MEMORY_AGENT_KEY_FILE || '').trim();
1126
+ if (envFile) {
1127
+ return { ok: true, file: path.resolve(envFile), source: 'env:YOTTA_MEMORY_AGENT_KEY_FILE' };
1128
+ }
1129
+ const envHome = String(process.env.YOTTA_MEMORY_AGENT_HOME || '').trim();
1130
+ if (envHome) {
1131
+ return { ok: true, file: agentKeyFilePath(envHome), source: 'env:YOTTA_MEMORY_AGENT_HOME' };
1132
+ }
1133
+ const host = detectAgentHost(owner);
1134
+ if (host === 'codex') {
1135
+ const home = process.env.CODEX_HOME ? path.resolve(process.env.CODEX_HOME) : path.join(os.homedir(), '.codex');
1136
+ return { ok: true, file: agentKeyFilePath(home), source: 'host:codex' };
1137
+ }
1138
+ if (host === 'opencode') {
1139
+ const base = process.env.XDG_CONFIG_HOME ? path.resolve(process.env.XDG_CONFIG_HOME) : path.join(os.homedir(), '.config');
1140
+ return { ok: true, file: agentKeyFilePath(path.join(base, 'opencode')), source: 'host:opencode' };
1141
+ }
1142
+ return { ok: true, file: agentKeyFilePath(path.join(os.homedir(), '.' + owner)), source: 'host:generic' };
1143
+ }
547
1144
  function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
548
1145
  function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
549
1146
  function ownerFromPrivatePath(root, fp) {
@@ -637,26 +1234,189 @@ function unwrapOwnerKeyRecovery(root, owner, rk) {
637
1234
  if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复密钥文件格式错误: ' + p);
638
1235
  return aesDecryptBytes(rk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
639
1236
  }
640
- function writeOwnerKeyCache(root, owner, ownerKey) {
641
- ensureKeysDir(root);
642
- const p = encCachePath(root, owner);
643
- fs.writeFileSync(p, ownerKey);
644
- try { fs.chmodSync(p, 0o600); } catch (e) {}
1237
+ function recoverOwnerKeyFromRecovery(root, owner, umk, rk) {
1238
+ if (!isSafeAgentId(owner) || !Buffer.isBuffer(rk)) return null;
1239
+ try {
1240
+ const ownerKey = unwrapOwnerKeyRecovery(root, owner, rk);
1241
+ if (!ownerKey) return null;
1242
+ fs.writeFileSync(encOwnerKeyPath(root, owner), Buffer.concat([
1243
+ Buffer.from(KEY_MAGIC, 'utf8'),
1244
+ aesEncryptBytes(umk, ownerKey, Buffer.from('owner:' + owner, 'utf8')),
1245
+ ]));
1246
+ return ownerKey;
1247
+ } catch (e) {
1248
+ return null;
1249
+ }
645
1250
  }
646
- function loadOwnerKeyCache(root, owner) {
647
- const p = encCachePath(root, owner);
648
- if (!fs.existsSync(p)) return null;
649
- return fs.readFileSync(p);
1251
+ function privateOwnerHasData(root, owner) {
1252
+ if (!isSafeAgentId(owner)) return false;
1253
+ const dir = path.join(root, PRIVATE_DIR, owner);
1254
+ if (!fs.existsSync(dir)) return false;
1255
+ const stack = [dir];
1256
+ while (stack.length) {
1257
+ const current = stack.pop();
1258
+ let entries = [];
1259
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch (e) { continue; }
1260
+ for (const entry of entries) {
1261
+ if (entry.isDirectory()) {
1262
+ stack.push(path.join(current, entry.name));
1263
+ } else if (entry.isFile() && entry.name !== ENC_INDEX_FILE) {
1264
+ return true;
1265
+ }
1266
+ }
1267
+ }
1268
+ return false;
650
1269
  }
651
1270
  function revokeOwnerKeyCache(root, owner) {
652
1271
  const p = encCachePath(root, owner);
653
1272
  if (fs.existsSync(p)) { fs.unlinkSync(p); return true; }
654
1273
  return false;
655
1274
  }
656
- function getOwnerKeyFor(root, owner) {
1275
+ function writeAgentBinding(root, owner, ownerKey, agentKey) {
1276
+ if (!isSafeAgentId(owner)) throw new Error('非法 agent ID');
1277
+ if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) throw new Error('agent_key 必须是 32 字节');
1278
+ fs.mkdirSync(encBindingsDir(root), { recursive: true });
1279
+ const aad = Buffer.from('agent:' + owner, 'utf8');
1280
+ const env = aesEncryptBytes(agentKey, ownerKey, aad);
1281
+ fs.writeFileSync(encAgentBindingPath(root, owner), Buffer.concat([Buffer.from(AGENT_MAGIC, 'utf8'), env]));
1282
+ try { fs.chmodSync(encAgentBindingPath(root, owner), 0o600); } catch (e) {}
1283
+ }
1284
+ function writePendingAgentKey(root, owner, agentKey) {
1285
+ if (!isSafeAgentId(owner)) throw new Error('非法 agent ID');
1286
+ if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) throw new Error('agent_key 必须是 32 字节');
1287
+ fs.mkdirSync(encPendingDir(root), { recursive: true });
1288
+ const fp = encPendingPath(root, owner);
1289
+ fs.writeFileSync(fp, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
1290
+ try { fs.chmodSync(fp, 0o600); } catch (e) {}
1291
+ return fp;
1292
+ }
1293
+ function writeAgentBindingWithPending(root, owner, ownerKey, agentKey) {
1294
+ writeAgentBinding(root, owner, ownerKey, agentKey);
1295
+ try {
1296
+ return writePendingAgentKey(root, owner, agentKey);
1297
+ } catch (e) {
1298
+ try { fs.unlinkSync(encAgentBindingPath(root, owner)); } catch (ignore) {}
1299
+ try { fs.unlinkSync(encPendingPath(root, owner)); } catch (ignore) {}
1300
+ throw e;
1301
+ }
1302
+ }
1303
+ function removePendingAgentKey(root, owner) {
1304
+ if (!isSafeAgentId(owner)) return false;
1305
+ const fp = encPendingPath(root, owner);
1306
+ if (!fs.existsSync(fp)) return false;
1307
+ fs.unlinkSync(fp);
1308
+ return true;
1309
+ }
1310
+ function readPendingAgentKey(root, owner) {
1311
+ if (!isSafeAgentId(owner)) return null;
1312
+ const fp = encPendingPath(root, owner);
1313
+ if (!fs.existsSync(fp)) return null;
1314
+ const raw = fs.readFileSync(fp, 'utf8').trim();
1315
+ if (!raw) throw new Error('pending agent_key 文件为空');
1316
+ const key = Buffer.from(raw, 'base64');
1317
+ if (key.length !== 32) throw new Error('pending agent_key 格式错误');
1318
+ return key;
1319
+ }
1320
+ function readAgentKeyFile(fp) {
1321
+ const raw = fs.readFileSync(path.resolve(String(fp)), 'utf8').trim();
1322
+ if (!raw) throw new Error('agent-key 文件为空');
1323
+ const key = Buffer.from(raw, 'base64');
1324
+ if (key.length !== 32) throw new Error('agent-key 文件格式错误');
1325
+ return key;
1326
+ }
1327
+ function unwrapAgentBinding(root, owner, agentKey) {
1328
+ if (!isSafeAgentId(owner)) return null;
1329
+ if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) return null;
1330
+ const p = encAgentBindingPath(root, owner);
1331
+ if (!fs.existsSync(p)) return null;
1332
+ const buf = fs.readFileSync(p);
1333
+ if (buf.slice(0, AGENT_MAGIC.length).toString('utf8') !== AGENT_MAGIC) throw new Error('agent binding 文件格式错误: ' + p);
1334
+ return aesDecryptBytes(agentKey, buf.slice(AGENT_MAGIC.length), Buffer.from('agent:' + owner, 'utf8'));
1335
+ }
1336
+ function getOwnerKeyFor(root, owner, identity) {
657
1337
  if (!owner) return null;
658
1338
  if (!isEncrypted(root)) return null;
659
- return loadOwnerKeyCache(root, owner);
1339
+ const current = identity || currentRuntimeIdentity();
1340
+ if (!current.id || current.id !== owner || !current.agentKey) return null;
1341
+ const agentKey = Buffer.from(current.agentKey, 'base64');
1342
+ return unwrapAgentBinding(root, owner, agentKey);
1343
+ }
1344
+ function validatePrivateIdentity(root, ident, owner) {
1345
+ if (!ident || !ident.id) return '私密操作必须先声明显式身份。';
1346
+ if (!isSafeAgentId(owner || ident.id)) return '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。';
1347
+ if (!isEncrypted(root)) return '';
1348
+ const current = currentRuntimeIdentity();
1349
+ const agentKey = ident.agentKey || (current.id === ident.id ? current.agentKey : '');
1350
+ if (!agentKey) {
1351
+ return '记忆库已加密,但缺少 agent_key。CLI 请使用 --agent-key <key> 或 --agent-key-file <path>;stdio MCP 请使用 --agent-id <id> + --agent-key-file <path>;HTTP MCP 请发送 X-Agent-Key 请求头。若该 agent 尚未绑定,请执行 yotta-memory key bind ' + (owner || ident.id) + '。';
1352
+ }
1353
+ const context = IDENTITY_CONTEXT.getStore();
1354
+ if (context) {
1355
+ context.id = ident.id;
1356
+ context.agentKey = agentKey;
1357
+ } else {
1358
+ setRuntimeAgent(ident.id, agentKey);
1359
+ }
1360
+ try {
1361
+ if (!getOwnerKeyFor(root, owner || ident.id, { id: ident.id, agentKey: agentKey })) {
1362
+ return 'agent_key 校验失败:未找到 ' + (owner || ident.id) + ' 的 agent binding(可能已吊销)。请由用户重新授权,或由用户执行 yotta-memory key bind ' + (owner || ident.id) + '。';
1363
+ }
1364
+ } catch (e) {
1365
+ return 'agent_key 无效或 binding 损坏:' + e.message;
1366
+ }
1367
+ return '';
1368
+ }
1369
+ function legacyCacheOwners(root) {
1370
+ if (!isEncrypted(root)) return [];
1371
+ const dir = encCacheDir(root);
1372
+ if (!fs.existsSync(dir)) return [];
1373
+ const found = [];
1374
+ let names = [];
1375
+ try { names = fs.readdirSync(dir); } catch (e) { return found; }
1376
+ for (const f of names) {
1377
+ if (!/\.key$/.test(f)) continue;
1378
+ const owner = f.slice(0, -4);
1379
+ if (!isSafeAgentId(owner)) continue;
1380
+ if (found.indexOf(owner) === -1) found.push(owner);
1381
+ }
1382
+ return found;
1383
+ }
1384
+ function migrationRequiredInfo(root) {
1385
+ if (!isEncrypted(root)) return null;
1386
+ const owners = new Set();
1387
+ for (const owner of collectOwners(root)) owners.add(owner);
1388
+ for (const owner of legacyCacheOwners(root)) owners.add(owner);
1389
+ const actionable = [];
1390
+ const legacyOnly = [];
1391
+ for (const owner of owners) {
1392
+ const hasPrivate = fs.existsSync(path.join(root, PRIVATE_DIR, owner)) || fs.existsSync(path.join(root, PRIVATE_DIR, owner, PROFILE_FILE)) || fs.existsSync(path.join(root, PRIVATE_DIR, owner, PROFILE_FILE + ENC_SUFFIX));
1393
+ const hasLegacy = fs.existsSync(encCachePath(root, owner));
1394
+ const bound = fs.existsSync(encAgentBindingPath(root, owner));
1395
+ if (!hasPrivate) {
1396
+ if (hasLegacy) legacyOnly.push(owner);
1397
+ continue;
1398
+ }
1399
+ if (!bound) actionable.push(owner);
1400
+ }
1401
+ if (!actionable.length) return null;
1402
+ const reasons = [];
1403
+ if (actionable.some(function (owner) { return fs.existsSync(encCachePath(root, owner)); })) reasons.push('legacy_cache');
1404
+ reasons.push('no_agent_binding');
1405
+ return {
1406
+ marker: '[YTM_MIGRATION_REQUIRED] agent=' + actionable.join(',') + ' reason=' + reasons.join(','),
1407
+ owners: actionable,
1408
+ reasons: reasons,
1409
+ legacyOnly: legacyOnly,
1410
+ };
1411
+ }
1412
+ function migrationReminderText(root) {
1413
+ const info = migrationRequiredInfo(root);
1414
+ if (!info) return '';
1415
+ let text = info.marker + '\nAI 提醒(升级后首次调用元忆 / 重启会话时请主动转达):旧版授权缓存不再加载,需要重新授权。请让用户自己执行 `yotta-memory view` → 浏览器打开平台 → 输入主口令 → 点「授权」并保存只展示一次的 `agent_key`;这是用户侧操作,AI 不得代替用户执行 `migrate` / `key bind` 迁移,只负责提醒和讲解步骤。';
1416
+ if (info.legacyOnly && info.legacyOnly.length) {
1417
+ text += '\n另有 legacy 明文 cache 缺少可迁移私密数据(不属于迁移清单): ' + info.legacyOnly.join(', ') + ';如需清理请单独确认。';
1418
+ }
1419
+ return text;
660
1420
  }
661
1421
  function encryptMemoryText(text, ownerKey) {
662
1422
  const fileKey = crypto.randomBytes(32);
@@ -1080,7 +1840,7 @@ function hasGrant(userAgent, ownerAgent) {
1080
1840
  return false;
1081
1841
  }
1082
1842
  // 三态读取判定:'read' | 'denied'
1083
- // selfAgent 为可信身份(env 声明 / 调用方上下文);--agent 仅供授权与展示,不得授予跨智能体私密读取。
1843
+ // selfAgent 为调用方显式声明的可信身份(CLI 参数 / stdio 参数 / HTTP 请求头);--owner 仅供授权与展示,不得授予跨智能体私密读取。
1084
1844
  function classifyRead(entry, agent, ownerFilter, unsafe, selfAgent) {
1085
1845
  if (entry.scope === 'public') return 'read';
1086
1846
  const owner = entry.owner || '';
@@ -1272,7 +2032,7 @@ function backupWindowsTaskXml(opts) {
1272
2032
  opts = opts || {};
1273
2033
  const time = backupTimeParts(opts.time || '03:30');
1274
2034
  const nodePath = opts.nodePath || process.execPath;
1275
- const scriptPath = opts.scriptPath || __filename;
2035
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1276
2036
  const userId = opts.userId || '';
1277
2037
  const principal = userId
1278
2038
  ? '<Principal id="Author"><UserId>' + xmlEscape(userId) + '</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal>'
@@ -1305,7 +2065,7 @@ function backupWindowsTaskXml(opts) {
1305
2065
  function backupSystemdServiceContent(opts) {
1306
2066
  opts = opts || {};
1307
2067
  const nodePath = opts.nodePath || process.execPath;
1308
- const scriptPath = opts.scriptPath || __filename;
2068
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1309
2069
  return [
1310
2070
  '[Unit]',
1311
2071
  'Description=Yotta Memory daily backup',
@@ -1338,7 +2098,7 @@ function backupCronLine(opts) {
1338
2098
  opts = opts || {};
1339
2099
  const time = backupTimeParts(opts.time || '03:30');
1340
2100
  const nodePath = opts.nodePath || process.execPath;
1341
- const scriptPath = opts.scriptPath || __filename;
2101
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1342
2102
  const logPath = opts.logPath || '';
1343
2103
  const redirect = logPath ? ' >> ' + shQuote(logPath) + ' 2>&1' : '';
1344
2104
  return time.minute + ' ' + time.hour + ' * * * ' + shQuote(nodePath) + ' ' + shQuote(scriptPath) + ' backup ensure-daily' + redirect + ' ' + BACKUP_CRON_MARKER;
@@ -1347,7 +2107,7 @@ function backupLaunchdPlist(opts) {
1347
2107
  opts = opts || {};
1348
2108
  const time = backupTimeParts(opts.time || '03:30');
1349
2109
  const nodePath = opts.nodePath || process.execPath;
1350
- const scriptPath = opts.scriptPath || __filename;
2110
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1351
2111
  const logPath = opts.logPath || '';
1352
2112
  const lines = [
1353
2113
  '<?xml version="1.0" encoding="UTF-8"?>',
@@ -1398,7 +2158,7 @@ function backupScheduleEnableCore(opts) {
1398
2158
  const time = opts.time || '03:30';
1399
2159
  try { backupTimeParts(time); } catch (error) { return { error: true, text: error.message }; }
1400
2160
  const nodePath = opts.nodePath || process.execPath;
1401
- const scriptPath = opts.scriptPath || __filename;
2161
+ const scriptPath = opts.scriptPath || runtimeManagedScript();
1402
2162
  try {
1403
2163
  if (platform === 'win32') {
1404
2164
  const userId = opts.userId || (process.env.USERDOMAIN && process.env.USERNAME ? process.env.USERDOMAIN + '\\' + process.env.USERNAME : '');
@@ -1517,7 +2277,7 @@ function backupScheduleStatusCore(opts) {
1517
2277
  return { error: false, registered: false, scheduler: '', text: '当前平台不支持自动备份调度: ' + platform };
1518
2278
  }
1519
2279
  function backupFallbackCommand() {
1520
- return { command: process.execPath, args: [__filename, 'backup', 'ensure-daily'] };
2280
+ return { command: process.execPath, args: [runtimeManagedScript(), 'backup', 'ensure-daily'] };
1521
2281
  }
1522
2282
  function startBackupFallback() {
1523
2283
  const cfg = loadConfig();
@@ -1798,6 +2558,348 @@ function backupHealthCore(opts) {
1798
2558
  }
1799
2559
  return { level: 'none', needsSetup: false, text: '' };
1800
2560
  }
2561
+ // ---- v0.16.0 M3: runtime drift diagnosis ----
2562
+ function runtimePathList(value) {
2563
+ const pattern = path.delimiter === ':' ? /[;:\n]/ : /[;\n]/;
2564
+ return String(value || '').split(pattern).map(function (item) { return item.trim(); }).filter(Boolean);
2565
+ }
2566
+ function runtimeVersionFromDir(dir) {
2567
+ let current = path.resolve(String(dir || ''));
2568
+ for (let i = 0; i < 8; i++) {
2569
+ const pkgPath = path.join(current, 'package.json');
2570
+ if (fs.existsSync(pkgPath)) {
2571
+ try {
2572
+ const version = String(JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '').trim();
2573
+ if (version) return version;
2574
+ } catch (e) {}
2575
+ }
2576
+ const skillPath = path.join(current, 'SKILL.md');
2577
+ if (fs.existsSync(skillPath)) {
2578
+ try {
2579
+ const text = fs.readFileSync(skillPath, 'utf8');
2580
+ const match = text.match(/^---\s*\n([\s\S]*?)\n---/);
2581
+ if (match) {
2582
+ const versionLine = match[1].split(/\r?\n/).find(function (line) { return /^version\s*:/.test(line); });
2583
+ if (versionLine) {
2584
+ const version = versionLine.slice(versionLine.indexOf(':') + 1).trim().replace(/^["']|["']$/g, '');
2585
+ if (version) return version;
2586
+ }
2587
+ }
2588
+ } catch (e) {}
2589
+ }
2590
+ const parent = path.dirname(current);
2591
+ if (parent === current) break;
2592
+ current = parent;
2593
+ }
2594
+ return '';
2595
+ }
2596
+ function runtimeVersionFromPath(target) {
2597
+ const value = String(target || '').trim();
2598
+ if (!value) return '';
2599
+ const normalized = value.replace(/\\/g, '/');
2600
+ const versionMatch = normalized.match(/\/versions\/([^/]+)\/bin\/yotta-memory\.js$/i);
2601
+ if (versionMatch) return versionMatch[1];
2602
+ const abs = path.resolve(value);
2603
+ if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) return runtimeVersionFromDir(abs);
2604
+ const base = path.basename(abs).toLowerCase();
2605
+ const dir = path.dirname(abs);
2606
+ return runtimeVersionFromDir(base === 'yotta-memory.js' ? path.dirname(dir) : dir);
2607
+ }
2608
+ function runtimeExtractLauncherPaths(text) {
2609
+ const found = new Set();
2610
+ const add = function (value) {
2611
+ const raw = String(value || '').trim().replace(/^['"]|['"]$/g, '');
2612
+ if (!raw) return;
2613
+ const parts = splitCommandArgv(raw);
2614
+ for (const part of parts) {
2615
+ const token = part.replace(/^['"]|['"]$/g, '');
2616
+ if (/yotta-memory\.js$/i.test(token.replace(/[;,]$/, ''))) found.add(token.replace(/[;,]$/, ''));
2617
+ }
2618
+ };
2619
+ const parsed = [];
2620
+ try {
2621
+ const value = JSON.parse(String(text || ''));
2622
+ const walk = function (node) {
2623
+ if (typeof node === 'string') parsed.push(node);
2624
+ else if (Array.isArray(node)) node.forEach(walk);
2625
+ else if (node && typeof node === 'object') Object.keys(node).forEach(function (key) { walk(node[key]); });
2626
+ };
2627
+ walk(value);
2628
+ } catch (e) {}
2629
+ for (const value of parsed) if (/yotta-memory\.js/i.test(value)) add(value);
2630
+ const raw = String(text || '').replace(/\\\\/g, '\\');
2631
+ const patterns = [
2632
+ /[A-Za-z]:[\\/][^"'\r\n|;]+?yotta-memory\.js/gi,
2633
+ /(?:\.{0,2}[\\/])?[^"'\s|;]+?yotta-memory\.js/gi,
2634
+ ];
2635
+ for (const pattern of patterns) {
2636
+ let match;
2637
+ while ((match = pattern.exec(raw)) !== null) add(match[0]);
2638
+ }
2639
+ return Array.from(found);
2640
+ }
2641
+ function runtimeConfigIdentityMode(text) {
2642
+ const raw = String(text || '');
2643
+ if (/YOTTA_AGENT_ID|YOTTA_MEMORY_AGENT_KEY|YOTTA_MEMORY_TRUST_ENV_AGENT/.test(raw)) return 'legacy-env';
2644
+ if (/--agent-id/.test(raw) && /--agent-key-file/.test(raw)) return 'stdio-args';
2645
+ if (/X-Agent-Id|Authorization/i.test(raw)) return 'headers';
2646
+ return 'unknown';
2647
+ }
2648
+ function runtimeDiscoverProcesses() {
2649
+ try {
2650
+ if (process.platform === 'win32') {
2651
+ const script = "$p=Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*yotta-memory*' }; if ($p) { $p | Select-Object ProcessId,CommandLine | ConvertTo-Json -Compress }";
2652
+ const result = child_process.spawnSync('powershell.exe', ['-NoProfile', '-Command', script], {
2653
+ encoding: 'utf8',
2654
+ timeout: 15000,
2655
+ windowsHide: true,
2656
+ });
2657
+ if (result.status !== 0 || !String(result.stdout || '').trim()) return [];
2658
+ const parsed = JSON.parse(String(result.stdout).trim());
2659
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
2660
+ return rows.map(function (row) {
2661
+ return { pid: row.ProcessId, commandLine: row.CommandLine || '' };
2662
+ });
2663
+ }
2664
+ const result = child_process.spawnSync('ps', ['-eo', 'pid=,args='], { encoding: 'utf8', timeout: 10000 });
2665
+ if (result.status !== 0) return [];
2666
+ return String(result.stdout || '').split(/\r?\n/).filter(Boolean).map(function (line) {
2667
+ const match = line.trim().match(/^(\d+)\s+(.*)$/);
2668
+ return match ? { pid: parseInt(match[1], 10), commandLine: match[2] } : null;
2669
+ }).filter(Boolean);
2670
+ } catch (e) {
2671
+ return [];
2672
+ }
2673
+ }
2674
+ function runtimeDiscoverSkillDirs() {
2675
+ const explicit = runtimePathList(process.env.YOTTA_MEMORY_SKILL_DIRS);
2676
+ if (explicit.length) return explicit;
2677
+ const candidates = [];
2678
+ const add = function (base) {
2679
+ if (!base) return;
2680
+ candidates.push(path.join(base, 'skills', 'yotta-memory'));
2681
+ candidates.push(path.join(base, 'yotta-memory'));
2682
+ };
2683
+ add(process.env.CODEX_HOME);
2684
+ if (process.env.XDG_CONFIG_HOME) candidates.push(path.join(process.env.XDG_CONFIG_HOME, 'opencode', 'skills', 'yotta-memory'));
2685
+ add(process.env.HERMES_HOME);
2686
+ add(path.join(os.homedir(), '.codex'));
2687
+ candidates.push(path.join(os.homedir(), '.config', 'opencode', 'skills', 'yotta-memory'));
2688
+ return Array.from(new Set(candidates)).filter(function (dir) { return fs.existsSync(dir); });
2689
+ }
2690
+ function runtimeDoctorCore(opts) {
2691
+ opts = opts || {};
2692
+ const drifts = [];
2693
+ const warnings = [];
2694
+ const mcpConfigPaths = opts.mcpConfigPaths !== undefined
2695
+ ? opts.mcpConfigPaths
2696
+ : runtimePathList(process.env.YOTTA_MEMORY_MCP_CONFIGS);
2697
+ const skillDirs = opts.skillDirs !== undefined ? opts.skillDirs : runtimeDiscoverSkillDirs();
2698
+ const processes = opts.processes !== undefined ? opts.processes : runtimeDiscoverProcesses();
2699
+ const manifest = readRuntimeManifest();
2700
+ const currentDir = runtimeCurrentDir();
2701
+ const currentBin = runtimeCurrentBin();
2702
+ const manifestVersion = manifest && manifest.current ? String(manifest.current) : '';
2703
+ const installedCurrentVersion = fs.existsSync(path.join(currentDir, 'package.json'))
2704
+ ? runtimeVersionFromDir(currentDir)
2705
+ : '';
2706
+
2707
+ const checks = {
2708
+ cli: { path: runtimeExecutionPath(), version: VERSION },
2709
+ current: {
2710
+ path: currentDir,
2711
+ version: manifestVersion || installedCurrentVersion || '',
2712
+ installedVersion: installedCurrentVersion,
2713
+ manifestVersion: manifestVersion,
2714
+ },
2715
+ runtimeRoot: runtimeRoot(),
2716
+ mcpConfigs: [],
2717
+ runningServers: [],
2718
+ skillCopies: [],
2719
+ identityModes: [],
2720
+ drifts: drifts,
2721
+ };
2722
+
2723
+ if (!manifest || !manifest.current) {
2724
+ drifts.push({
2725
+ kind: 'current-runtime',
2726
+ source: runtimeManifestPath(),
2727
+ actual: 'missing',
2728
+ expected: VERSION,
2729
+ fix: 'yotta-memory runtime install --from-current',
2730
+ blocking: true,
2731
+ });
2732
+ } else {
2733
+ const versionDir = runtimeVersionDir(manifest.current);
2734
+ if (!fs.existsSync(versionDir)) {
2735
+ drifts.push({
2736
+ kind: 'current-runtime',
2737
+ source: runtimeManifestPath(),
2738
+ actual: manifest.current,
2739
+ expected: VERSION,
2740
+ fix: 'yotta-memory runtime install --from-current && yotta-memory runtime use ' + VERSION + ' --restart',
2741
+ blocking: true,
2742
+ });
2743
+ } else if (manifest.current !== VERSION) {
2744
+ drifts.push({
2745
+ kind: 'current-runtime',
2746
+ source: runtimeManifestPath(),
2747
+ actual: manifest.current,
2748
+ expected: VERSION,
2749
+ fix: 'yotta-memory runtime install --from-current && yotta-memory runtime use ' + VERSION + ' --restart',
2750
+ blocking: true,
2751
+ });
2752
+ }
2753
+ if (fs.existsSync(versionDir)) {
2754
+ if (!fs.existsSync(currentBin)) {
2755
+ drifts.push({
2756
+ kind: 'current-runtime',
2757
+ source: currentDir,
2758
+ actual: 'launcher-missing',
2759
+ expected: VERSION,
2760
+ fix: 'yotta-memory runtime use ' + manifest.current + ' --force',
2761
+ blocking: true,
2762
+ });
2763
+ } else {
2764
+ try {
2765
+ const real = fs.realpathSync(currentDir);
2766
+ if (path.resolve(real) !== path.resolve(versionDir)) {
2767
+ drifts.push({
2768
+ kind: 'current-runtime',
2769
+ source: currentDir,
2770
+ actual: 'pointer-mismatch',
2771
+ expected: manifest.current,
2772
+ fix: 'yotta-memory runtime use ' + manifest.current + ' --restart',
2773
+ blocking: true,
2774
+ });
2775
+ }
2776
+ } catch (e) {
2777
+ drifts.push({
2778
+ kind: 'current-runtime',
2779
+ source: currentDir,
2780
+ actual: 'pointer-unreadable',
2781
+ expected: manifest.current,
2782
+ fix: 'yotta-memory runtime use ' + manifest.current + ' --restart',
2783
+ blocking: true,
2784
+ });
2785
+ }
2786
+ }
2787
+ }
2788
+ }
2789
+
2790
+ const legacyEnv = legacyIdentityEnvNames();
2791
+ if (legacyEnv.length) {
2792
+ drifts.push({
2793
+ kind: 'identity-mode',
2794
+ source: 'process-env',
2795
+ actual: 'legacy-env',
2796
+ expected: 'headers|stdio-args',
2797
+ fix: '移除 ' + legacyEnv.join(', ') + ',HTTP 改用请求头,stdio 改用 --agent-id + --agent-key-file',
2798
+ blocking: true,
2799
+ });
2800
+ }
2801
+
2802
+ for (const configPath of mcpConfigPaths) {
2803
+ const abs = path.resolve(String(configPath));
2804
+ let text = '';
2805
+ try {
2806
+ text = fs.readFileSync(abs, 'utf8');
2807
+ } catch (e) {
2808
+ warnings.push('MCP 配置无法读取: ' + abs);
2809
+ checks.mcpConfigs.push({ path: abs, readable: false, version: 'unknown', identityMode: 'unknown' });
2810
+ continue;
2811
+ }
2812
+ const launchers = runtimeExtractLauncherPaths(text);
2813
+ const identityMode = runtimeConfigIdentityMode(text);
2814
+ const version = launchers.length ? (runtimeVersionFromPath(launchers[0]) || 'unknown') : 'unknown';
2815
+ checks.mcpConfigs.push({ path: abs, readable: true, launcher: launchers[0] || '', version: version, identityMode: identityMode });
2816
+ checks.identityModes.push({ source: abs, mode: identityMode });
2817
+ if (identityMode === 'legacy-env') {
2818
+ drifts.push({
2819
+ kind: 'identity-mode',
2820
+ source: abs,
2821
+ actual: 'legacy-env',
2822
+ expected: 'headers|stdio-args',
2823
+ fix: '从 MCP 配置移除身份 env,HTTP 改用请求头,stdio 改用 --agent-id + --agent-key-file',
2824
+ blocking: true,
2825
+ });
2826
+ }
2827
+ if (version !== 'unknown' && version !== VERSION) {
2828
+ drifts.push({
2829
+ kind: 'mcp-config',
2830
+ source: abs,
2831
+ actual: version,
2832
+ expected: VERSION,
2833
+ fix: '把 MCP 配置中的运行时路径改为 ' + runtimeCurrentBin() + ' 后重启该 MCP',
2834
+ blocking: true,
2835
+ });
2836
+ }
2837
+ }
2838
+
2839
+ for (const processInfo of processes || []) {
2840
+ const row = typeof processInfo === 'string' ? { pid: '', commandLine: processInfo } : (processInfo || {});
2841
+ const commandLine = String(row.commandLine || '');
2842
+ if (!commandLine || commandLine.indexOf('yotta-memory') === -1 || commandLine.indexOf('serve') === -1) continue;
2843
+ const launchers = runtimeExtractLauncherPaths(commandLine);
2844
+ const launcher = launchers[0] || '';
2845
+ const version = launcher ? (runtimeVersionFromPath(launcher) || 'unknown') : 'unknown';
2846
+ checks.runningServers.push({ pid: row.pid || '', launcher: launcher, version: version });
2847
+ if (version !== 'unknown' && version !== VERSION) {
2848
+ drifts.push({
2849
+ kind: 'running-server',
2850
+ source: 'pid=' + String(row.pid || 'unknown'),
2851
+ actual: version,
2852
+ expected: VERSION,
2853
+ fix: 'yotta-memory runtime use ' + VERSION + ' --restart 或重启对应 MCP 客户端',
2854
+ blocking: false,
2855
+ });
2856
+ }
2857
+ }
2858
+
2859
+ for (const skillDir of skillDirs) {
2860
+ const abs = path.resolve(String(skillDir));
2861
+ if (!fs.existsSync(path.join(abs, 'SKILL.md'))) continue;
2862
+ const version = runtimeVersionFromDir(abs) || 'unknown';
2863
+ checks.skillCopies.push({ path: abs, version: version });
2864
+ if (version !== 'unknown' && version !== VERSION) {
2865
+ drifts.push({
2866
+ kind: 'skill-copy',
2867
+ source: abs,
2868
+ actual: version,
2869
+ expected: VERSION,
2870
+ fix: '用官方安装器更新该 yotta-memory 技能副本',
2871
+ blocking: false,
2872
+ });
2873
+ }
2874
+ }
2875
+
2876
+ const lines = [
2877
+ '## 运行时一致性',
2878
+ '- CLI: ' + VERSION + ' (' + runtimeExecutionPath() + ')',
2879
+ '- current: ' + (checks.current.version || '(missing)') + ' (' + currentDir + ')',
2880
+ '- runtime.json: ' + runtimeManifestPath(),
2881
+ ];
2882
+ if (checks.mcpConfigs.length) {
2883
+ for (const item of checks.mcpConfigs) lines.push('- MCP 配置: ' + item.path + ' -> ' + item.version + ' / ' + item.identityMode);
2884
+ } else lines.push('- MCP 配置: (未提供或未发现)');
2885
+ if (checks.runningServers.length) {
2886
+ for (const item of checks.runningServers) lines.push('- 运行中 server: pid=' + item.pid + ' -> ' + item.version);
2887
+ } else lines.push('- 运行中 server: (未发现)');
2888
+ if (checks.skillCopies.length) {
2889
+ for (const item of checks.skillCopies) lines.push('- 技能副本: ' + item.path + ' -> ' + item.version);
2890
+ } else lines.push('- 技能副本: (未提供或未发现)');
2891
+ if (!drifts.length) lines.push('- 漂移: 无');
2892
+ for (const drift of drifts) {
2893
+ lines.push('- [漂移] ' + drift.kind + ': actual=' + drift.actual + ' expected=' + drift.expected + ' fix=' + drift.fix + ' blocking=' + (drift.blocking ? 'yes' : 'no'));
2894
+ }
2895
+ return {
2896
+ ok: !drifts.some(function (drift) { return drift.blocking; }),
2897
+ checks: checks,
2898
+ drifts: drifts,
2899
+ warnings: warnings,
2900
+ text: lines.join('\n'),
2901
+ };
2902
+ }
1801
2903
  // 开工可靠性检查:只读检查根目录、密钥库、索引、身份登记与最近备份。
1802
2904
  function doctorCore(opts) {
1803
2905
  opts = opts || {};
@@ -1849,6 +2951,14 @@ function doctorCore(opts) {
1849
2951
  }
1850
2952
  }
1851
2953
 
2954
+ const migration = migrationRequiredInfo(root);
2955
+ if (migration) {
2956
+ checks.agentBindings = { ok: false, pending: migration.owners };
2957
+ warnings.push(migrationReminderText(root));
2958
+ } else if (encrypted) {
2959
+ checks.agentBindings = { ok: true, pending: [] };
2960
+ }
2961
+
1852
2962
  const configuredDir = opts.dir || opts.backupDir || cfg.backup_dir;
1853
2963
  if (!configuredDir) {
1854
2964
  if (cfg.backup_setup_choice === 'manual') {
@@ -1888,6 +2998,18 @@ function doctorCore(opts) {
1888
2998
  if (cfg.backup_scheduler_error) warnings.push('每日备份调度异常: ' + cfg.backup_scheduler_error);
1889
2999
  }
1890
3000
 
3001
+ let runtimeReport = null;
3002
+ if (opts.runtime) {
3003
+ runtimeReport = runtimeDoctorCore(opts);
3004
+ checks.runtime = runtimeReport.checks;
3005
+ for (const drift of runtimeReport.drifts) {
3006
+ const message = '运行时漂移 [' + drift.kind + ']: actual=' + drift.actual + ' expected=' + drift.expected + ';修复: ' + drift.fix + ';阻断: ' + (drift.blocking ? '是' : '否');
3007
+ if (drift.blocking) critical.push(message);
3008
+ else warnings.push(message);
3009
+ }
3010
+ for (const warning of runtimeReport.warnings || []) warnings.push(warning);
3011
+ }
3012
+
1891
3013
  const level = critical.length ? 'critical' : (warnings.length ? 'warning' : 'ok');
1892
3014
  const lines = [
1893
3015
  '# yotta-memory doctor(开工可靠性检查)',
@@ -1900,6 +3022,10 @@ function doctorCore(opts) {
1900
3022
  for (const message of critical) lines.push('- [严重] ' + message);
1901
3023
  for (const message of warnings) lines.push('- [警告] ' + message);
1902
3024
  if (!critical.length && !warnings.length) lines.push('- 检查项: 全部通过');
3025
+ if (runtimeReport) {
3026
+ lines.push('');
3027
+ lines.push(runtimeReport.text);
3028
+ }
1903
3029
  if (level === 'critical') {
1904
3030
  lines.push('');
1905
3031
  lines.push('- 破坏性写入已锁定:先运行 yotta-memory doctor 修复严重问题。');
@@ -2009,7 +3135,10 @@ function isAllowedBackupPath(rel) {
2009
3135
  const top = normalized.split('/')[0];
2010
3136
  if (top === 'facts' || top === 'private' || top === '.archive') return true;
2011
3137
  if (top === 'agents.json' || top === 'index.json' || /^index-\d{4}\.json$/.test(top)) return true;
2012
- if (top === 'keys') return normalized !== 'keys/cache' && normalized.indexOf('keys/cache/') !== 0;
3138
+ if (top === 'keys') {
3139
+ return normalized !== 'keys/cache' && normalized.indexOf('keys/cache/') !== 0 &&
3140
+ normalized !== 'keys/pending' && normalized.indexOf('keys/pending/') !== 0;
3141
+ }
2013
3142
  return false;
2014
3143
  }
2015
3144
  function backupCreateCore(opts) {
@@ -2196,18 +3325,12 @@ function backupDrillCore(opts) {
2196
3325
  if (!salt) throw new Error('恢复副本缺少 keys/salt');
2197
3326
  ownerKey = unwrapOwnerKey(target, privateFile.owner, deriveUmk(opts.password, salt));
2198
3327
  source = 'password';
2199
- } else {
2200
- const ownerKeyFile = opts.ownerKeyFile || path.join(opts.sourceRoot || userRoot(), 'keys', 'cache', privateFile.owner + '.key');
2201
- if (fs.existsSync(ownerKeyFile)) {
2202
- ownerKey = fs.readFileSync(ownerKeyFile);
2203
- source = 'owner-cache';
2204
- }
2205
3328
  }
2206
3329
  } catch (error) {
2207
3330
  ownerKey = null;
2208
3331
  }
2209
3332
  if (!ownerKey) {
2210
- return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password,或确认本机 keys/cache 中有 ' + privateFile.owner + ' 的授权缓存。', {
3333
+ return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password;legacy keys/cache 明文缓存不再作为恢复凭证。', {
2211
3334
  manifest: true,
2212
3335
  index: true,
2213
3336
  private: { checked: true, decrypted: false, source: '', file: privateFile.rel },
@@ -2253,14 +3376,22 @@ function rememberCore(type, subject, statement, opts) {
2253
3376
  const subj = String(subject || '').trim();
2254
3377
  if (!stmt) return { error: true, text: 'statement 不能为空' };
2255
3378
  if (!subj) return { error: true, text: 'subject 不能为空' };
2256
- const selfAgent = opts.selfAgent || currentAgent();
3379
+ const ident = resolveIdentity(opts);
3380
+ const selfAgent = ident.id;
2257
3381
  const owner = opts.owner || selfAgent;
2258
3382
  const scope = opts.scope || defaultScope(t);
3383
+ if (scope === 'private' && ident.error) {
3384
+ return { error: true, text: ident.error };
3385
+ }
3386
+ if (scope === 'private') {
3387
+ const keyError = validatePrivateIdentity(root, ident, owner);
3388
+ if (keyError) return { error: true, text: keyError };
3389
+ }
2259
3390
  if (scope === 'private' && !owner) {
2260
- return { error: true, text: '私密记忆必须声明归属智能体:请设环境变量 YOTTA_AGENT_ID(或 AGENT_ID)、传 --owner <id>,或先 yotta-memory whoami / iam 登记唯一身份。公共记忆(FACT)不受影响。' };
3391
+ return { error: true, text: '私密记忆必须显式声明归属智能体:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。公共记忆(FACT)不受影响。' };
2261
3392
  }
2262
3393
  if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
2263
- return { error: true, text: '拒绝: 当前声明身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
3394
+ return { error: true, text: '拒绝: 当前显式身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请传正确的 --agent <id>,或加 --unsafe(用户显式授权)。' };
2264
3395
  }
2265
3396
  const encrypted = isEncrypted(root);
2266
3397
  if (scope === 'private' && encrypted && !getOwnerKeyFor(root, owner)) {
@@ -2366,12 +3497,31 @@ function recallCore(query, opts) {
2366
3497
  const limit = opts.limit || 50;
2367
3498
  const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
2368
3499
  const q = query ? String(query).toLowerCase() : '';
2369
- const selfAgent = currentAgent();
2370
- const agent = opts.agent || selfAgent;
3500
+ const ident = resolveIdentity(opts);
3501
+ if (ident.error) return { error: true, exitCode: 3, text: ident.error };
3502
+ const selfAgent = ident.id;
3503
+ const agent = selfAgent;
2371
3504
  const ownerFilter = opts.owner || '';
2372
3505
  const allSafe = !!opts.unsafe;
2373
- const impersonation = !!(opts.agent && selfAgent && opts.agent !== selfAgent);
2374
- const explicitCross = !!opts.all || impersonation || (!!ownerFilter && ownerFilter.toLowerCase() !== 'user' && ownerFilter !== agent);
3506
+ if (ownerFilter && !isSafeAgentId(ownerFilter)) {
3507
+ return { error: true, exitCode: 3, text: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
3508
+ }
3509
+ if (!selfAgent && (opts.all || ownerFilter)) {
3510
+ return {
3511
+ error: true,
3512
+ exitCode: 3,
3513
+ text: '显式跨智能体读取必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
3514
+ };
3515
+ }
3516
+ if (selfAgent && !allSafe) {
3517
+ for (const root of roots) {
3518
+ const keyError = validatePrivateIdentity(root, ident, ownerFilter || selfAgent);
3519
+ if (keyError && isEncrypted(root)) {
3520
+ return { error: true, exitCode: 3, text: keyError };
3521
+ }
3522
+ }
3523
+ }
3524
+ const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter !== agent);
2375
3525
  const wantExplain = !!opts.explain;
2376
3526
  const useSemantic = opts.semantic !== false;
2377
3527
  const prefilter = (q && useSemantic) ? recallPrefilter(q) : null;
@@ -2483,6 +3633,7 @@ function recallCore(query, opts) {
2483
3633
  }
2484
3634
  }
2485
3635
  const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
3636
+ if (!selfAgent) lines.push('[注意] 未声明身份,本次仅读取公共 FACT;私密 PREF / BOUND / COMMIT 已跳过。');
2486
3637
  for (const h of shown) {
2487
3638
  lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
2488
3639
  lines.push(' ' + path.join(h.root, h.entry.file));
@@ -2532,7 +3683,7 @@ function appendTrashAudit(root, record) {
2532
3683
  }
2533
3684
  function forgetCore(fileRef, opts) {
2534
3685
  opts = opts || {};
2535
- const selfAgent = opts.selfAgent || currentAgent();
3686
+ const selfAgent = resolveIdentity(opts).id;
2536
3687
  const roots = memoryRoots();
2537
3688
  const ref = String(fileRef || '').replace(/\\/g, '/');
2538
3689
  let target = null, targetRoot = null, targetRel = null;
@@ -2545,7 +3696,7 @@ function forgetCore(fileRef, opts) {
2545
3696
  if (seg[0] === 'private') {
2546
3697
  const owner = seg[1] || '';
2547
3698
  if (!opts.unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
2548
- return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。' };
3699
+ return { error: true, text: '拒绝: 不能删除其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。' };
2549
3700
  }
2550
3701
  }
2551
3702
  const trashFile = path.join(
@@ -2642,7 +3793,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
2642
3793
  if (seg[0] === PRIVATE_DIR) {
2643
3794
  const owner = seg[1] || '';
2644
3795
  if (!unsafe && (owner && (selfAgent ? owner !== selfAgent : true))) {
2645
- return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 YOTTA_AGENT_ID 声明自己的身份,或加 --unsafe(用户显式授权)。';
3796
+ return '拒绝: 不能操作其它智能体 ' + owner + ' 的私密记忆(当前身份 ' + (selfAgent || '未声明') + ')。请用 --agent / --agent-id 声明自己的身份,或加 --unsafe(用户显式授权)。';
2646
3797
  }
2647
3798
  }
2648
3799
  return null;
@@ -2650,7 +3801,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
2650
3801
  // 显式使用反馈:useful/useless → weight/confidence/feedback_net 演化(自我学习闭环)
2651
3802
  function feedbackCore(ref, opts) {
2652
3803
  opts = opts || {};
2653
- const selfAgent = opts.selfAgent || currentAgent();
3804
+ const selfAgent = resolveIdentity(opts).id;
2654
3805
  const target = resolveMemoryTarget(ref);
2655
3806
  if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
2656
3807
  const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
@@ -2709,7 +3860,7 @@ function feedbackUndoCore(target, owner, meta, selfAgent, opts) {
2709
3860
  // 解释单条记忆效用分项(为什么靠前/归档/遗忘)
2710
3861
  function explainCore(ref, opts) {
2711
3862
  opts = opts || {};
2712
- const selfAgent = opts.selfAgent || currentAgent();
3863
+ const selfAgent = resolveIdentity(opts).id;
2713
3864
  const target = resolveMemoryTarget(ref);
2714
3865
  if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
2715
3866
  const owner = ownerFromPrivatePath(target.root, target.fp);
@@ -2864,7 +4015,7 @@ function maintainCore(opts) {
2864
4015
  // 手动合并两条相似记忆:保留高 confidence,低 confidence 移入 .archive
2865
4016
  function mergeCore(refA, refB, opts) {
2866
4017
  opts = opts || {};
2867
- const selfAgent = opts.selfAgent || currentAgent();
4018
+ const selfAgent = resolveIdentity(opts).id;
2868
4019
  const ta = resolveMemoryTarget(refA);
2869
4020
  const tb = resolveMemoryTarget(refB);
2870
4021
  if (!ta || !tb) return { error: true, text: '未找到记忆文件: ' + refA + ' / ' + refB };
@@ -2908,7 +4059,7 @@ function distillCore(opts) {
2908
4059
  opts = opts || {};
2909
4060
  const root = userRoot();
2910
4061
  if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
2911
- const selfAgent = opts.selfAgent || currentAgent();
4062
+ const selfAgent = resolveIdentity(opts).id;
2912
4063
  const owner = opts.owner || selfAgent;
2913
4064
  const entries = [];
2914
4065
  for (const r of memoryRoots()) {
@@ -3059,6 +4210,8 @@ function promptPassword(promptText) {
3059
4210
  }
3060
4211
 
3061
4212
  function migrateCore(root, password, recoveryKeyIn) {
4213
+ // 第三个参数历史上是 recovery key;兼容旧调用,同时支持 { agent, recoveryKey } 选项对象。
4214
+ const opts = recoveryKeyIn && typeof recoveryKeyIn === 'object' ? recoveryKeyIn : {};
3062
4215
  if (!fs.existsSync(root)) return { error: true, text: '记忆库不存在,请先 yotta-memory init。' };
3063
4216
  if (isEncrypted(root)) return { error: true, text: '记忆库已启用加密(keys/ 存在),无需迁移。' };
3064
4217
  if (!hasPlaintextPrivate(root)) return { error: false, text: '无私密明文可迁移(private/ 下无 .md 文件);如需新建加密库请 init --encrypt。' };
@@ -3070,10 +4223,9 @@ function migrateCore(root, password, recoveryKeyIn) {
3070
4223
  fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
3071
4224
  let moved = 0;
3072
4225
  const owners = collectOwners(root);
3073
- const self = currentAgent();
4226
+ const self = resolveIdentity(opts).id;
3074
4227
  for (const owner of owners) {
3075
4228
  const ok = wrapOwnerKey(root, umk, rk, owner);
3076
- if (self && owner === self) writeOwnerKeyCache(root, owner, ok);
3077
4229
  for (const t of PRIVATE_LEAF) {
3078
4230
  const d = path.join(root, PRIVATE_DIR, owner, t);
3079
4231
  if (!fs.existsSync(d)) continue;
@@ -3097,8 +4249,8 @@ function migrateCore(root, password, recoveryKeyIn) {
3097
4249
  buildIndex(root);
3098
4250
  let tail = '已迁移 ' + moved + ' 个私密文件(' + owners.length + ' 个 owner)到密文。';
3099
4251
  tail += '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + rk.toString('base64');
3100
- if (self && owners.indexOf(self) !== -1) tail += '\n已为本智能体 ' + self + ' 写入授权缓存,可继续读写自己的私密。';
3101
- else tail += '\n各智能体需在用户平台授权(yotta-memory view → 授权本智能体)后才能读写自己的私密。';
4252
+ tail += '\n迁移不会写明文 owner key cache;请为每个 agent 执行一次 yotta-memory key bind <id>,再注入 agent_key。';
4253
+ if (self && owners.indexOf(self) !== -1) tail += '\n你的身份:' + self + ',请先执行 yotta-memory key bind ' + self + '。';
3102
4254
  return { error: false, text: tail };
3103
4255
  }
3104
4256
 
@@ -3111,7 +4263,7 @@ async function cmdMigrate(opts) {
3111
4263
  password = await promptPassword('主口令(迁移后私密区加密,勿忘;忘口令可用恢复钥匙重设): ') || '';
3112
4264
  if (!password) { console.error('已取消。'); process.exit(2); }
3113
4265
  }
3114
- const r = migrateCore(root, password, null);
4266
+ const r = migrateCore(root, password, { agent: opts.agent, recoveryKey: null });
3115
4267
  console.log(r.text);
3116
4268
  if (r.error) process.exit(2);
3117
4269
  }
@@ -3177,51 +4329,176 @@ async function cmdResetPassword(opts) {
3177
4329
  if (r.error) process.exit(2);
3178
4330
  }
3179
4331
 
3180
- function keyAuthorizeCore(root, owner, password) {
4332
+ function keyBindCore(root, owner, opts) {
4333
+ opts = opts || {};
4334
+ if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
3181
4335
  if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
3182
4336
  const salt = loadSalt(root);
3183
4337
  if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
3184
- const umk = deriveUmk(password, salt);
3185
- let ok = null;
3186
- try { ok = unwrapOwnerKey(root, owner, umk); } catch (e) { ok = null; }
3187
- if (!ok) {
3188
- let rk = null;
3189
- try { rk = unwrapRecoveryEnc(root, umk); } catch (e) { rk = null; }
3190
- if (!rk) return { error: true, text: '口令错误。' };
3191
- ok = wrapOwnerKey(root, umk, rk, owner);
4338
+ let ownerKey = null;
4339
+ if (opts.recoveryKey) {
4340
+ const rk = Buffer.from(String(opts.recoveryKey), 'base64');
4341
+ if (rk.length !== 32) return { error: true, text: '恢复钥匙须为 32 字节(base64)。' };
4342
+ try { ownerKey = unwrapOwnerKeyRecovery(root, owner, rk); } catch (e) { ownerKey = null; }
4343
+ if (!ownerKey) return { error: true, text: '恢复钥匙无法解开 ' + owner + ' 的 owner key。' };
4344
+ } else {
4345
+ const password = String(opts.password || '');
4346
+ if (!password) return { error: true, text: '请提供主口令(--password 或交互输入),或用 --recovery-key。' };
4347
+ const umk = deriveUmk(password, salt);
4348
+ try { ownerKey = unwrapOwnerKey(root, owner, umk); } catch (e) { ownerKey = null; }
4349
+ if (!ownerKey) {
4350
+ let rk = null;
4351
+ try { rk = unwrapRecoveryEnc(root, umk); } catch (e) { rk = null; }
4352
+ if (!rk) return { error: true, text: '口令错误,或该 owner 的密钥不存在。' };
4353
+ ownerKey = recoverOwnerKeyFromRecovery(root, owner, umk, rk);
4354
+ if (!ownerKey) {
4355
+ if (privateOwnerHasData(root, owner)) {
4356
+ return { error: true, text: '检测到 private/' + owner + ' 仍有密文,但 keys/' + owner + '.key.enc 与恢复文件均不可用;拒绝新建 owner key(会导致旧数据无法解密)。请从备份恢复原 owner key,或使用恢复钥匙修复后重试。' };
4357
+ }
4358
+ ownerKey = wrapOwnerKey(root, umk, rk, owner);
4359
+ }
4360
+ }
3192
4361
  }
3193
- writeOwnerKeyCache(root, owner, ok);
3194
- return { error: false, text: '已授权 ' + owner + ' 读取其私密(写入授权缓存 keys/cache/' + owner + '.key)。' };
4362
+ const agentKey = crypto.randomBytes(32);
4363
+ const pendingFile = writeAgentBindingWithPending(root, owner, ownerKey, agentKey);
4364
+ revokeOwnerKeyCache(root, owner);
4365
+ return {
4366
+ error: false,
4367
+ agentKey: agentKey.toString('base64'),
4368
+ pendingFile: pendingFile,
4369
+ text: '已绑定 ' + owner + ' 的 agent_key,并删除旧明文 cache。\nagent_key: ' + agentKey.toString('base64') + '\n待领取文件: ' + pendingFile + '\nAI 在新会话执行 yotta-memory key status ' + owner + ',有 pending 再执行 yotta-memory key claim ' + owner + ';用户请另外保存此 key;此 key 只显示一次。',
4370
+ };
3195
4371
  }
3196
4372
 
3197
4373
  function keyRevokeCore(root, owner) {
3198
- if (!revokeOwnerKeyCache(root, owner)) return { error: false, text: owner + ' 无授权缓存(已是未授权状态)。' };
3199
- return { error: false, text: '已吊销 ' + owner + ' 的授权缓存(该智能体将无法解密其私密)。' };
4374
+ if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
4375
+ let removed = false;
4376
+ const binding = encAgentBindingPath(root, owner);
4377
+ if (fs.existsSync(binding)) { fs.unlinkSync(binding); removed = true; }
4378
+ if (removePendingAgentKey(root, owner)) removed = true;
4379
+ if (revokeOwnerKeyCache(root, owner)) removed = true;
4380
+ if (!removed) return { error: false, text: owner + ' 无 agent binding(已是未绑定状态)。' };
4381
+ return { error: false, text: '已吊销 ' + owner + ' 的 agent_key binding;该 agent 将无法解密其私密。' };
4382
+ }
4383
+
4384
+ function keyClaimCore(root, owner, opts) {
4385
+ if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
4386
+ const location = resolveAgentKeyLocation(owner, opts);
4387
+ if (!location.ok) return { error: true, text: location.error };
4388
+ const pending = encPendingPath(root, owner);
4389
+ if (!fs.existsSync(pending)) {
4390
+ return { error: true, text: '未找到待领取 agent_key:' + pending + '。请先在 yotta-memory view 中授权,或执行 yotta-memory key bind ' + owner + '。' };
4391
+ }
4392
+ let agentKey;
4393
+ try {
4394
+ agentKey = readPendingAgentKey(root, owner);
4395
+ if (!agentKey) throw new Error('pending agent_key 不存在');
4396
+ if (!unwrapAgentBinding(root, owner, agentKey)) {
4397
+ throw new Error('agent binding 不存在或已失效');
4398
+ }
4399
+ } catch (e) {
4400
+ return { error: true, text: 'agent_key 校验失败:' + e.message + '。未写入宿主目录,pending 文件保留。' };
4401
+ }
4402
+ let targetDir;
4403
+ let targetFile;
4404
+ let tempFile;
4405
+ try {
4406
+ targetFile = location.file;
4407
+ targetDir = path.dirname(targetFile);
4408
+ fs.mkdirSync(targetDir, { recursive: true });
4409
+ tempFile = path.join(targetDir, AGENT_KEY_FILENAME + '.tmp-' + process.pid + '-' + crypto.randomBytes(4).toString('hex'));
4410
+ fs.writeFileSync(tempFile, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
4411
+ try { fs.chmodSync(tempFile, 0o600); } catch (e) {}
4412
+ try {
4413
+ fs.renameSync(tempFile, targetFile);
4414
+ } catch (e) {
4415
+ fs.rmSync(targetFile, { force: true });
4416
+ fs.renameSync(tempFile, targetFile);
4417
+ }
4418
+ const readBack = fs.readFileSync(targetFile, 'utf8').trim();
4419
+ if (readBack !== agentKey.toString('base64')) throw new Error('宿主 key 文件回读不一致');
4420
+ } catch (e) {
4421
+ try { if (tempFile) fs.rmSync(tempFile, { force: true }); } catch (ignore) {}
4422
+ return { error: true, text: '写入宿主 key 文件失败:' + e.message + '。pending 文件保留,可重试。' };
4423
+ }
4424
+ removePendingAgentKey(root, owner);
4425
+ return {
4426
+ error: false,
4427
+ targetFile: targetFile,
4428
+ source: location.source,
4429
+ text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\n发现规则: ' + location.source + '\npending 文件已删除。',
4430
+ };
4431
+ }
4432
+
4433
+ function keyStatusCore(root, owner, opts) {
4434
+ if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
4435
+ const location = resolveAgentKeyLocation(owner, opts);
4436
+ if (!location.ok) return { error: true, text: location.error };
4437
+ const binding = fs.existsSync(encAgentBindingPath(root, owner));
4438
+ const pending = fs.existsSync(encPendingPath(root, owner));
4439
+ let hostKey = 'missing';
4440
+ if (fs.existsSync(location.file)) {
4441
+ hostKey = 'invalid';
4442
+ try {
4443
+ const key = readAgentKeyFile(location.file);
4444
+ if (binding && unwrapAgentBinding(root, owner, key)) hostKey = 'valid';
4445
+ } catch (e) {}
4446
+ }
4447
+ return {
4448
+ error: false,
4449
+ binding: binding,
4450
+ pending: pending,
4451
+ hostKey: hostKey,
4452
+ targetFile: location.file,
4453
+ source: location.source,
4454
+ text: 'agent: ' + owner + '\nbinding: ' + (binding ? 'yes' : 'no') + '\npending: ' + (pending ? 'yes' : 'no') + '\nhost_key: ' + hostKey + '\nchecked: ' + location.file + '\ndiscovery: ' + location.source,
4455
+ };
4456
+ }
4457
+
4458
+ function keyListCore(root) {
4459
+ root = root || userRoot();
4460
+ if (!isEncrypted(root)) return { text: '记忆库未启用加密。' };
4461
+ const owners = collectOwners(root);
4462
+ const bound = owners.filter(function (o) { return fs.existsSync(encAgentBindingPath(root, o)); });
4463
+ const pending = owners.filter(function (o) { return fs.existsSync(encPendingPath(root, o)); });
4464
+ const legacy = owners.filter(function (o) { return fs.existsSync(encCachePath(root, o)); });
4465
+ const lines = [
4466
+ 'owner: ' + (owners.length ? owners.join(', ') : '(无)'),
4467
+ '已绑定 agent_key: ' + (bound.length ? bound.join(', ') : '(无)'),
4468
+ '待领取 agent_key: ' + (pending.length ? pending.join(', ') : '(无)'),
4469
+ 'legacy 明文 cache: ' + (legacy.length ? legacy.join(', ') + '(升级后不再加载)' : '(无)'),
4470
+ ];
4471
+ return { text: lines.join('\n'), owners: owners, bound: bound, pending: pending, legacy: legacy };
3200
4472
  }
3201
4473
 
3202
4474
  function cmdKeyList() {
3203
4475
  const root = userRoot();
3204
- if (!isEncrypted(root)) { console.log('记忆库未启用加密。'); return; }
3205
- const owners = collectOwners(root);
3206
- const authorized = owners.filter(function (o) { return fs.existsSync(encCachePath(root, o)); });
3207
- console.log('owner: ' + (owners.length ? owners.join(', ') : '(无)'));
3208
- console.log('已授权缓存: ' + (authorized.length ? authorized.join(', ') : '(无)'));
4476
+ const r = keyListCore(root);
4477
+ console.log(r.text);
4478
+ if (!r.owners) return;
4479
+ const reminder = migrationReminderText(root);
4480
+ if (reminder) console.log('\n' + reminder);
3209
4481
  }
3210
4482
 
3211
- async function cmdKeyAuthorize(id, opts) {
4483
+ async function cmdKeyBind(id, opts) {
3212
4484
  const root = userRoot();
3213
4485
  if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
3214
- if (!id) { console.error('请指定 <id>:yotta-memory key authorize <id>'); process.exit(2); }
4486
+ if (!id) { console.error('请指定 <id>:yotta-memory key bind <id>'); process.exit(2); }
3215
4487
  let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
3216
- if (!password) {
4488
+ if (!password && !opts.recoveryKey) {
3217
4489
  password = await promptPassword('主口令: ') || '';
3218
4490
  if (!password) { console.error('已取消。'); process.exit(2); }
3219
4491
  }
3220
- const r = keyAuthorizeCore(root, id, password);
4492
+ const r = keyBindCore(root, id, { password: password, recoveryKey: opts.recoveryKey });
3221
4493
  console.log(r.text);
3222
4494
  if (r.error) process.exit(2);
3223
4495
  }
3224
4496
 
4497
+ async function cmdKeyAuthorize(id, opts) {
4498
+ console.error('提示: key authorize 已由 key bind 替代,本次按 bind 执行。');
4499
+ return cmdKeyBind(id, opts);
4500
+ }
4501
+
3225
4502
  function cmdKeyRevoke(id) {
3226
4503
  const root = userRoot();
3227
4504
  if (!id) { console.error('请指定 <id>:yotta-memory key revoke <id>'); process.exit(2); }
@@ -3230,6 +4507,22 @@ function cmdKeyRevoke(id) {
3230
4507
  if (r.error) process.exit(2);
3231
4508
  }
3232
4509
 
4510
+ function cmdKeyClaim(id, opts) {
4511
+ const root = userRoot();
4512
+ if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
4513
+ const r = keyClaimCore(root, id, opts);
4514
+ console.log(r.text);
4515
+ if (r.error) process.exit(2);
4516
+ }
4517
+
4518
+ function cmdKeyStatus(id, opts) {
4519
+ const root = userRoot();
4520
+ if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME> | --agent-key-file <文件>]'); process.exit(2); }
4521
+ const r = keyStatusCore(root, id, opts);
4522
+ console.log(r.text);
4523
+ if (r.error) process.exit(2);
4524
+ }
4525
+
3233
4526
  // v0.8.1: 查看平台分页——服务端只返回当前页(offset/limit),避免记忆多了一次全量加载/渲染/传输
3234
4527
  function viewEntriesCore(root, session, query, offset, limit) {
3235
4528
  limit = Math.max(1, parseInt(limit, 10) || 50);
@@ -3250,13 +4543,50 @@ function viewEntriesCore(root, session, query, offset, limit) {
3250
4543
  const page = all.slice(offset, offset + limit);
3251
4544
  return { count: count, offset: offset, limit: limit, hasMore: offset + limit < count, entries: page };
3252
4545
  }
4546
+ function viewHostName(req) {
4547
+ const raw = String(req.headers.host || '').trim();
4548
+ if (!raw) return '';
4549
+ try {
4550
+ return new URL('http://' + raw).hostname.toLowerCase().replace(/^\[|\]$/g, '');
4551
+ } catch (e) {
4552
+ return '';
4553
+ }
4554
+ }
4555
+ function viewHostAllowed(req, bindHost) {
4556
+ const name = viewHostName(req);
4557
+ if (!name) return false;
4558
+ const bind = String(bindHost || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
4559
+ if (bind !== '127.0.0.1' && bind !== 'localhost' && bind !== '::1') return true;
4560
+ return name === '127.0.0.1' || name === 'localhost' || name === '::1';
4561
+ }
4562
+ function viewOriginAllowed(req) {
4563
+ const origin = req.headers.origin;
4564
+ if (!origin) return true;
4565
+ let originHost = '';
4566
+ try {
4567
+ originHost = new URL(String(origin)).host.toLowerCase();
4568
+ } catch (e) {
4569
+ return false;
4570
+ }
4571
+ const requestHost = String(req.headers.host || '').trim().toLowerCase();
4572
+ return !!requestHost && originHost === requestHost;
4573
+ }
3253
4574
  function viewServerCore(root, port, host) {
3254
4575
  let session = { umk: null, ownerKeys: {} };
3255
4576
  const server = http.createServer(function (req, res) {
4577
+ if (!viewHostAllowed(req, host) || !viewOriginAllowed(req)) {
4578
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });
4579
+ res.end('forbidden');
4580
+ return;
4581
+ }
3256
4582
  let pathname = '/';
3257
4583
  try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
3258
4584
  function json(code, obj) {
3259
- res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
4585
+ res.writeHead(code, {
4586
+ 'Content-Type': 'application/json; charset=utf-8',
4587
+ 'Cache-Control': 'no-store',
4588
+ 'X-Content-Type-Options': 'nosniff',
4589
+ });
3260
4590
  res.end(JSON.stringify(obj));
3261
4591
  }
3262
4592
  function readBody(cb) {
@@ -3269,8 +4599,15 @@ function viewServerCore(root, port, host) {
3269
4599
  });
3270
4600
  }
3271
4601
  if (req.method === 'GET' && pathname === '/') {
3272
- res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
3273
- res.end(VIEW_HTML);
4602
+ res.writeHead(200, {
4603
+ 'Content-Type': 'text/html; charset=utf-8',
4604
+ 'Cache-Control': 'no-store',
4605
+ 'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; frame-ancestors 'none'; base-uri 'none'",
4606
+ 'X-Content-Type-Options': 'nosniff',
4607
+ 'X-Frame-Options': 'DENY',
4608
+ 'Referrer-Policy': 'no-referrer',
4609
+ });
4610
+ res.end(viewHtml());
3274
4611
  return;
3275
4612
  }
3276
4613
  if (req.method === 'POST' && pathname === '/api/status') {
@@ -3298,7 +4635,7 @@ function viewServerCore(root, port, host) {
3298
4635
  const agents = loadAgents(root).agents || {};
3299
4636
  const owners = keyOwners(root);
3300
4637
  const all = Array.from(new Set(owners.concat(Object.keys(agents))));
3301
- return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized: !!session.ownerKeys[o] && fs.existsSync(encCachePath(root, o)) }; }) });
4638
+ return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized: fs.existsSync(encAgentBindingPath(root, o)) }; }) });
3302
4639
  }
3303
4640
  if (req.method === 'POST' && pathname === '/api/entries') {
3304
4641
  if (!session.umk) return json(401, { error: '请先解锁。' });
@@ -3320,22 +4657,39 @@ function viewServerCore(root, port, host) {
3320
4657
  return readBody(function (d) {
3321
4658
  const owner = String(d.owner || '');
3322
4659
  if (!owner) return json(400, { error: '缺少 owner。' });
4660
+ if (!isSafeAgentId(owner)) return json(400, { error: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' });
4661
+ if (fs.existsSync(encAgentBindingPath(root, owner))) {
4662
+ return json(409, { error: '该智能体已有 agent binding。为避免打断在用的 agent_key,请先“吊销”,再重新授权生成新 key。' });
4663
+ }
3323
4664
  let ok = session.ownerKeys[owner];
3324
4665
  let rk = null;
3325
4666
  try { rk = unwrapRecoveryEnc(root, session.umk); } catch (e) { rk = null; }
3326
4667
  if (!ok) {
3327
4668
  if (!rk) return json(400, { error: '密钥库缺少恢复钥匙(recovery.key.enc),无法为新 owner 建钥。' });
3328
- ok = wrapOwnerKey(root, session.umk, rk, owner);
4669
+ ok = recoverOwnerKeyFromRecovery(root, owner, session.umk, rk);
4670
+ if (!ok) {
4671
+ if (privateOwnerHasData(root, owner)) {
4672
+ return json(400, { error: '检测到 private/' + owner + ' 仍有密文,但 keys/' + owner + '.key.enc 与恢复文件均不可用;拒绝新建 owner key(会导致旧数据无法解密)。请从备份恢复原 owner key,或使用 CLI 恢复钥匙修复后重试。' });
4673
+ }
4674
+ ok = wrapOwnerKey(root, session.umk, rk, owner);
4675
+ }
3329
4676
  session.ownerKeys[owner] = ok;
3330
4677
  }
3331
- writeOwnerKeyCache(root, owner, ok);
3332
- json(200, { ok: true });
4678
+ const agentKey = crypto.randomBytes(32);
4679
+ const pendingFile = writeAgentBindingWithPending(root, owner, ok, agentKey);
4680
+ revokeOwnerKeyCache(root, owner);
4681
+ json(200, { ok: true, agentKey: agentKey.toString('base64'), pendingFile: pendingFile, text: 'agent_key 只显示一次,请用户自行保存;AI 可由 pending 文件领取并写入宿主目录。' });
3333
4682
  });
3334
4683
  }
3335
4684
  if (req.method === 'POST' && pathname === '/api/revoke') {
3336
4685
  if (!session.umk) return json(401, { error: '请先解锁。' });
3337
4686
  return readBody(function (d) {
3338
- revokeOwnerKeyCache(root, String(d.owner || ''));
4687
+ const owner = String(d.owner || '');
4688
+ if (!isSafeAgentId(owner)) return json(400, { error: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' });
4689
+ const binding = encAgentBindingPath(root, owner);
4690
+ if (fs.existsSync(binding)) fs.unlinkSync(binding);
4691
+ removePendingAgentKey(root, owner);
4692
+ revokeOwnerKeyCache(root, owner);
3339
4693
  json(200, { ok: true });
3340
4694
  });
3341
4695
  }
@@ -3380,7 +4734,9 @@ function cmdView(opts) {
3380
4734
  viewServerCore(root, port, host);
3381
4735
  }
3382
4736
 
3383
- const VIEW_HTML = "<!doctype html><html lang=\"zh\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>元忆 · 用户查看平台</title><style>\nbody{font-family:system-ui,-apple-system,\"Microsoft YaHei\",sans-serif;max-width:1000px;margin:24px auto;padding:0 16px;color:#1f2328;background:#fafafa}\nh1{font-size:22px} .card{background:#fff;border:1px solid #e2e2e2;border-radius:10px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.04)}\nbutton{background:#2563eb;color:#fff;border:0;border-radius:6px;padding:7px 14px;cursor:pointer;margin:2px;font-size:14px}\nbutton.danger{background:#dc2626} button.ghost{background:#e5e7eb;color:#1f2328}\ninput,select{padding:8px;border:1px solid #c9c9c9;border-radius:6px;margin:2px;font-size:14px;box-sizing:border-box}\ntable{border-collapse:collapse;width:100%;font-size:13px} td,th{border:1px solid #ececec;padding:6px 8px;text-align:left;vertical-align:top}\n.owner{display:inline-flex;align-items:center;gap:6px;border:1px solid #ddd;border-radius:8px;padding:5px 10px;margin:4px 6px 4px 0;background:#f6f8fa}\n.entry{border-bottom:1px solid #eee;padding:8px 0} .meta{color:#8a8a8a;font-size:12px}\n.err{color:#dc2626;margin-top:8px} .ok{color:#16a34a;margin-top:8px}\n#app{display:none} code{background:#f0f0f0;padding:1px 5px;border-radius:4px;font-size:12px}\n</style></head><body>\n<h1>元忆 · 用户查看平台 <span id=\"ver\" style=\"font-size:14px;color:#888\"></span></h1>\n<div id=\"lock\" class=\"card\">\n <p><b>输入主口令解锁</b>(口令只在本地内存派生,不落盘、不发送远端)。忘口令可在 CLI 用恢复钥匙重设:<code>yotta-memory reset-password --recovery-key &lt;钥匙&gt;</code></p>\n <input type=\"password\" id=\"pw\" placeholder=\"主口令\" style=\"width:260px\">\n <button onclick=\"unlock()\">解锁</button>\n <div class=\"err\" id=\"lockerr\"></div>\n</div>\n<div id=\"app\">\n <div class=\"card\">\n <b>AI 列表</b>(✅=已授权可读自己私密,🔒=未授权)\n <div id=\"owners\" style=\"margin-top:8px\"></div>\n </div>\n <div class=\"card\">\n <b>记忆</b>\n <input id=\"q\" placeholder=\"搜索关键词\" style=\"width:220px\" onkeydown=\"if(event.key==='Enter'){off=0;load()}\">\n <button onclick=\"off=0;load()\">搜索</button>\n <button class=\"ghost\" onclick=\"doExport()\">导出 JSON</button>\n <button class=\"ghost\" onclick=\"showRk()\">显示恢复钥匙</button>\n <span id=\"rkout\" style=\"font-size:12px;color:#888;margin-left:8px\"></span>\n <div id=\"meta\" style=\"margin-top:10px;font-size:12px;color:#666\"></div>\n <div id=\"entries\" style=\"margin-top:6px\"></div>\n <div id=\"pager\" style=\"margin-top:10px\">\n <button class=\"ghost\" id=\"prevb\" onclick=\"prevPage()\">上一页</button>\n <span id=\"pageinfo\" style=\"font-size:12px;color:#888;margin:0 8px\"></span>\n <button class=\"ghost\" id=\"nextb\" onclick=\"nextPage()\">下一页</button>\n </div>\n </div>\n <div class=\"card\">\n <b>重设口令</b><br>\n <input type=\"password\" id=\"cur\" placeholder=\"当前口令\">\n <input type=\"password\" id=\"np1\" placeholder=\"新口令\">\n <input type=\"password\" id=\"np2\" placeholder=\"确认新口令\">\n <button onclick=\"resetPw()\">重设</button>\n <span id=\"pwout\"></span>\n </div>\n</div>\n<script>\nfunction esc(s){return String(s==null?'':s).replace(/[&<>\"']/g,function(c){return{'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c];});}\nasync function api(p,b){try{const r=await fetch(p,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b||{})});return await r.json();}catch(e){return{error:String(e)};}}\nasync function boot(){const s=await api('/api/status');document.getElementById('ver').textContent='v'+(s.version||'');if(s.unlocked){showApp();}}\nfunction showApp(){document.getElementById('lock').style.display='none';document.getElementById('app').style.display='block';loadOwners();load();}\nasync function unlock(){const d=await api('/api/unlock',{password:document.getElementById('pw').value});if(d.error){document.getElementById('lockerr').textContent=d.error;return;}showApp();}\nasync function loadOwners(){const d=await api('/api/owners');const box=document.getElementById('owners');box.innerHTML='';if(!d.owners||!d.owners.length){box.innerHTML='(无 owner)';return;}\n for(const o of d.owners){const c=document.createElement('span');c.className='owner';c.innerHTML=esc(o.owner)+(o.authorized?' ✅':' 🔒')+' <button class=\"ghost\" data-a=\"'+esc(o.owner)+'\">授权</button><button class=\"danger\" data-r=\"'+esc(o.owner)+'\">吊销</button>';box.appendChild(c);}\n box.querySelectorAll('[data-a]').forEach(function(b){b.onclick=function(){api('/api/authorize',{owner:b.getAttribute('data-a')}).then(function(){loadOwners();});};});\n box.querySelectorAll('[data-r]').forEach(function(b){b.onclick=function(){api('/api/revoke',{owner:b.getAttribute('data-r')}).then(function(){loadOwners();});};});\n}\nlet off=0,PS=50;\nasync function load(){const d=await api('/api/entries',{query:document.getElementById('q').value,offset:off,limit:PS});const meta=document.getElementById('meta');const pg=document.getElementById('pageinfo');if(meta)meta.textContent='共 '+d.count+' 条';const lim=d.limit||PS;const totalPg=Math.max(1,Math.ceil(d.count/lim));const curPg=Math.floor((d.offset||0)/lim)+1;if(pg)pg.textContent='第 '+curPg+' / '+totalPg+' 页';const box=document.getElementById('entries');box.innerHTML='';if(d.entries)for(const e of d.entries){const div=document.createElement('div');div.className='entry';div.innerHTML='<b>['+esc(e.type)+'] '+esc(e.subject)+'</b><div>'+esc(e.statement)+'</div><div class=\"meta\">'+esc(e.file)+' · owner='+esc(e.owner||'-')+' · '+esc(e.updated||e.created||'')+'</div>';box.appendChild(div);}const pb=document.getElementById('prevb'),nb=document.getElementById('nextb');if(pb)pb.disabled=(d.offset||0)<=0;if(nb)nb.disabled=!d.hasMore;}\nfunction prevPage(){if(off>=PS){off-=PS;load();}}\nfunction nextPage(){off+=PS;load();}\nasync function doExport(){const d=await api('/api/export');if(d.error){alert(d.error);return;}const blob=new Blob([JSON.stringify(d,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='yottamemory-view-export.json';a.click();}\nasync function showRk(){const d=await api('/api/recovery-key');document.getElementById('rkout').textContent=d.recoveryKey?('恢复钥匙: '+d.recoveryKey):(d.error||'');}\nasync function resetPw(){const np1=document.getElementById('np1').value,np2=document.getElementById('np2').value;if(np1!==np2){document.getElementById('pwout').innerHTML='<span class=\"err\">两次新口令不一致</span>';return;}\n const d=await api('/api/reset-password',{currentPassword:document.getElementById('cur').value,newPassword:np1});document.getElementById('pwout').innerHTML=d.error?('<span class=\"err\">'+esc(d.error)+'</span>'):('<span class=\"ok\">'+esc(d.text||'ok')+'</span>');}\nboot();\n</script></body></html>";
4737
+ function viewHtml() {
4738
+ return VIEW_HTML;
4739
+ }
3384
4740
 
3385
4741
  // ---- 命令包装(CLI 入口)----
3386
4742
  async function cmdInit(opts) {
@@ -3448,8 +4804,13 @@ function cmdDistill(opts) {
3448
4804
  if (r.error) process.exit(2);
3449
4805
  }
3450
4806
  function cmdForget(fileRef, opts) {
4807
+ const ident = resolveIdentity(opts);
4808
+ if (ident.error) {
4809
+ console.log(ident.error);
4810
+ process.exit(3);
4811
+ }
3451
4812
  const r = forgetCore(fileRef, {
3452
- selfAgent: currentAgent(),
4813
+ selfAgent: ident.id,
3453
4814
  unsafe: !!(opts && opts.unsafe),
3454
4815
  });
3455
4816
  console.log(r.text);
@@ -3488,7 +4849,15 @@ function cmdBackupEnsureDaily(opts) {
3488
4849
  }
3489
4850
  function cmdBackupSchedule(action, opts) {
3490
4851
  let r;
3491
- if (action === 'enable') r = backupScheduleEnableCore(opts);
4852
+ if (action === 'enable') {
4853
+ const runtime = runtimeEnsureForManagedTask();
4854
+ if (runtime.error) {
4855
+ console.error('runtime 稳定入口未就绪,拒绝注册备份任务: ' + runtime.text);
4856
+ process.exit(1);
4857
+ }
4858
+ opts = Object.assign({}, opts, { scriptPath: runtimeManagedScript() });
4859
+ r = backupScheduleEnableCore(opts);
4860
+ }
3492
4861
  else if (action === 'disable') r = backupScheduleDisableCore(opts);
3493
4862
  else if (action === 'status') r = backupScheduleStatusCore(opts);
3494
4863
  else {
@@ -3564,6 +4933,16 @@ function importCore(root, src) {
3564
4933
  try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); }
3565
4934
  catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
3566
4935
  if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
4936
+ for (const item of data.memories) {
4937
+ const meta = item.meta || {};
4938
+ const t = (meta.type || 'FACT').toUpperCase();
4939
+ if (!TYPE_DIRS[t]) continue;
4940
+ const owner = String(meta.owner || '');
4941
+ const scope = meta.scope || defaultScope(t);
4942
+ if (scope === 'private' && !isSafeAgentId(owner)) {
4943
+ return { error: true, text: '拒绝: 导入的私密条目 owner 非法(只允许单段名称,禁止 /、\\、.. 和控制字符)。' };
4944
+ }
4945
+ }
3567
4946
  ensureInit(root);
3568
4947
  const encrypted = isEncrypted(root);
3569
4948
  let m = 0, skippedPriv = 0;
@@ -3627,6 +5006,7 @@ function cmdTokenNew(agentId, opts) {
3627
5006
  const root = userRoot();
3628
5007
  ensureInit(root);
3629
5008
  if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
5009
+ if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
3630
5010
  const data = loadTokens(root);
3631
5011
  data.version = 1;
3632
5012
  data.tokens = data.tokens || {};
@@ -3652,6 +5032,7 @@ function cmdTokenList() {
3652
5032
  function cmdTokenRevoke(agentId) {
3653
5033
  const root = userRoot();
3654
5034
  if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
5035
+ if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
3655
5036
  const data = loadTokens(root);
3656
5037
  data.tokens = data.tokens || {};
3657
5038
  if (data.tokens[agentId]) { delete data.tokens[agentId]; saveTokens(root, data); console.log('已吊销: ' + agentId); }
@@ -3758,6 +5139,7 @@ function cmdIam(agentId, opts) {
3758
5139
  const root = userRoot();
3759
5140
  ensureInit(root);
3760
5141
  if (!agentId) { console.error('请指定 <id>:yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]'); process.exit(2); }
5142
+ if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
3761
5143
  const conflict = identityConflict(root, agentId);
3762
5144
  if (conflict && !opts.force) {
3763
5145
  console.error("错误: ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一。请换一个唯一 ID,或确认是同一智能体后加 --force 覆盖。');
@@ -3778,13 +5160,18 @@ function cmdIam(agentId, opts) {
3778
5160
  console.log('已写入自我档案: ' + file);
3779
5161
  console.log('本机免 token:以后用 whoami 确认身份;远端接入需 token new --agent ' + agentId);
3780
5162
  }
3781
- function cmdWhoami() {
5163
+ function cmdWhoami(opts) {
3782
5164
  const root = userRoot();
3783
- const id = currentAgent();
5165
+ const ident = resolveIdentity(opts);
5166
+ if (ident.error) {
5167
+ console.log(ident.error);
5168
+ process.exit(2);
5169
+ }
5170
+ const id = ident.id;
3784
5171
  if (!id) {
3785
- console.log('当前未声明智能体身份(YOTTA_AGENT_ID / AGENT_ID 未设置,也未传 --agent)。');
3786
- console.log('本机:请在该智能体的 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID>,然后 yotta-memory iam <id> 登记;或 CLI 每次带 --agent <id> / --owner <id>。');
3787
- console.log('远端:X-Agent-Id 请求头 + token(token new --agent <id>)。');
5172
+ console.log('当前未声明显式智能体身份(身份不再从环境变量读取)。');
5173
+ console.log('CLI:请传 --agent <唯一ID>。stdio MCP:使用 --agent-id <唯一ID> + --agent-key-file <path>。HTTP MCP:发送 X-Agent-Id + X-Agent-Key。');
5174
+ console.log('远端:token + X-Agent-Id + X-Agent-Key(token new --agent <id>;agent_key 由 view 授权生成)。');
3788
5175
  return;
3789
5176
  }
3790
5177
  const agents = loadAgents(root).agents || {};
@@ -3834,9 +5221,20 @@ function profileGroups(root, owner) {
3834
5221
  function profileCore(opts) {
3835
5222
  opts = opts || {};
3836
5223
  const root = userRoot();
3837
- const selfAgent = opts.selfAgent !== undefined ? opts.selfAgent : currentAgent();
5224
+ const ident = resolveIdentity(opts);
5225
+ if (ident.error) return { error: true, exitCode: 3, text: ident.error };
5226
+ if (!ident.id) {
5227
+ return {
5228
+ error: true,
5229
+ exitCode: 3,
5230
+ text: '生成私密画像必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
5231
+ };
5232
+ }
5233
+ const selfAgent = ident.id;
3838
5234
  const owner = opts.owner || selfAgent;
3839
- if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(YOTTA_AGENT_ID / AGENT_ID)或传 --owner <id>,再生成画像。' };
5235
+ const keyError = validatePrivateIdentity(root, ident, owner);
5236
+ if (keyError) return { error: true, exitCode: 3, text: keyError };
5237
+ if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(--agent / --agent-id)或传 --owner <id>,再生成画像。' };
3840
5238
  if (selfAgent && owner !== selfAgent && owner !== 'user' && !opts.unsafe && !hasGrant(selfAgent, owner)) {
3841
5239
  return { error: true, exitCode: 3, text: '拒绝: 不能生成其它智能体 ' + owner + ' 的画像(private/' + owner + '/ 为私密区)。如需读取请 --owner user 或 --unsafe(用户显式授权)。' };
3842
5240
  }
@@ -3882,14 +5280,41 @@ function selfProfileKv(root, id) {
3882
5280
  try { text = readMemoryText(root, path.join(selfPrefsDir(root, id), profile), id); } catch (e) { return {}; }
3883
5281
  return parseKvBody(parseFrontmatter(text).body);
3884
5282
  }
5283
+ // v0.14.0:长期摘要优先 / 近期走廊 / 会话闭环上下文编排。
5284
+ // 摘要只认 consolidate 产物,不新增第二套摘要格式。
5285
+ function isConsolidatedSummary(entry) {
5286
+ if (!entry) return false;
5287
+ if (entry.source === 'consolidate') return true;
5288
+ const tags = Array.isArray(entry.tags) ? entry.tags : [];
5289
+ return tags.indexOf('consolidate') !== -1 && tags.indexOf('summary') !== -1;
5290
+ }
5291
+ function contextRecentKey(entry) {
5292
+ return String((entry && (entry.updated || entry.created)) || '');
5293
+ }
5294
+ function compareContextRecent(a, b) {
5295
+ const byTime = contextRecentKey(b).localeCompare(contextRecentKey(a));
5296
+ if (byTime !== 0) return byTime;
5297
+ return (b.access_count || 0) - (a.access_count || 0);
5298
+ }
3885
5299
  function contextCore(opts) {
3886
5300
  opts = opts || {};
3887
5301
  const roots = memoryRoots();
3888
5302
  if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
3889
5303
  const root = userRoot();
3890
- const limit = opts.limit || 10;
3891
- const selfAgent = opts.selfAgent !== undefined ? opts.selfAgent : currentAgent();
5304
+ const limit = parseInt(opts.limit || '10', 10) || 10;
5305
+ const ident = resolveIdentity(opts);
5306
+ if (ident.error) return { error: true, exitCode: 3, text: ident.error };
5307
+ if (!ident.id) {
5308
+ return {
5309
+ error: true,
5310
+ exitCode: 3,
5311
+ text: '开工上下文包含私密画像 / 边界 / 承诺,必须先声明身份:CLI 传 --agent <id>;stdio MCP 传 --agent-id <id> + --agent-key-file <path>;HTTP MCP 发送 X-Agent-Id + X-Agent-Key。',
5312
+ };
5313
+ }
5314
+ const selfAgent = ident.id;
3892
5315
  const owner = opts.owner || selfAgent;
5316
+ const keyError = validatePrivateIdentity(root, ident, owner);
5317
+ if (keyError) return { error: true, exitCode: 3, text: keyError };
3893
5318
  const unsafe = !!opts.unsafe;
3894
5319
  const budget = opts.budget ? parseInt(opts.budget, 10) : 0;
3895
5320
  const focus = opts.focus ? String(opts.focus) : '';
@@ -3898,14 +5323,32 @@ function contextCore(opts) {
3898
5323
  const embeddingTimeout = effectiveEmbeddingTimeout(opts);
3899
5324
  const trace = [];
3900
5325
  const lines = [];
5326
+ const shownFiles = new Set();
3901
5327
  const FENCE = String.fromCharCode(96, 96, 96);
3902
5328
  function usedChars() { return lines.reduce(function (s, x) { return s + String(x).length + 1; }, 0); }
5329
+ function appendContextEntry(entry, reason, options) {
5330
+ options = options || {};
5331
+ if (!entry) return false;
5332
+ if (shownFiles.has(entry.file)) {
5333
+ trace.push('[dropped] ' + entry.file + ' reason: duplicate');
5334
+ return false;
5335
+ }
5336
+ const line = '- [' + entry.type + '] ' + entry.subject + ': ' + entry.statement;
5337
+ if (options.budget !== false && budget > 0 && usedChars() + line.length > budget) {
5338
+ trace.push('[dropped] ' + entry.file + ' reason: budget_exceeded');
5339
+ return false;
5340
+ }
5341
+ lines.push(line);
5342
+ shownFiles.add(entry.file);
5343
+ trace.push('[included] ' + entry.file + ' reason: ' + reason);
5344
+ return true;
5345
+ }
3903
5346
  lines.push('# 开工上下文包(yotta-memory context)');
3904
5347
  lines.push('');
3905
5348
  lines.push('## 1. 身份');
3906
5349
  lines.push('');
3907
5350
  if (!owner) {
3908
- lines.push('- 未声明智能体身份(无 YOTTA_AGENT_ID / --owner)。私密记忆与画像不可用;请先 whoami / iam。');
5351
+ lines.push('- 未声明智能体身份(无 --agent / --agent-id / X-Agent-Id)。私密记忆与画像不可用;请先 whoami / iam。');
3909
5352
  } else {
3910
5353
  lines.push('- agent_id: ' + owner);
3911
5354
  const kv = selfProfileKv(root, owner);
@@ -3950,8 +5393,26 @@ function contextCore(opts) {
3950
5393
  }
3951
5394
  lines.push('');
3952
5395
 
5396
+ const readableEntries = [];
5397
+ for (const r of roots) {
5398
+ for (const e of ensureIndex(r)) {
5399
+ if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
5400
+ readableEntries.push(e);
5401
+ }
5402
+ }
5403
+
5404
+ lines.push('## 2.5 长期理解摘要');
5405
+ lines.push('');
5406
+ const summaries = readableEntries.filter(isConsolidatedSummary).sort(compareContextRecent);
5407
+ const summaryLimit = Math.min(3, Math.max(0, limit));
5408
+ if (!summaries.length) lines.push('(暂无周期摘要;可用 yotta-memory consolidate --apply 生成)');
5409
+ for (const e of summaries.slice(0, summaryLimit)) {
5410
+ appendContextEntry(e, 'summary_priority', { budget: false });
5411
+ }
5412
+ lines.push('');
5413
+
3953
5414
  if (focus) {
3954
- lines.push('## 2.5 任务相关记忆(--focus)');
5415
+ lines.push('## 2.6 任务相关记忆(--focus)');
3955
5416
  lines.push('');
3956
5417
  const focused = recallCore(focus, {
3957
5418
  limit: limit,
@@ -3964,70 +5425,51 @@ function contextCore(opts) {
3964
5425
  const focusedEntries = focused.entries || [];
3965
5426
  if (!focusedEntries.length) lines.push('(无匹配记忆)');
3966
5427
  for (const e of focusedEntries) {
3967
- const line = '- [' + e.type + '] ' + e.subject + ': ' + e.statement;
3968
- if (budget > 0 && usedChars() + line.length > budget) {
3969
- trace.push('[dropped] ' + e.file + ' reason: budget_exceeded');
3970
- continue;
3971
- }
3972
- lines.push(line);
3973
- trace.push('[included] ' + e.file + ' reason: focus_match score: ' + round3(e.score));
5428
+ appendContextEntry(e, 'focus_match score: ' + round3(e.score));
3974
5429
  }
3975
5430
  lines.push('');
3976
5431
  }
3977
5432
 
3978
- lines.push('## 3. 近期记忆(按活跃度前 ' + limit + ' 条)');
5433
+ lines.push('## 3. 近期走廊(按时间)');
3979
5434
  lines.push('');
3980
- const recent = [];
3981
- for (const r of roots) {
3982
- for (const e of ensureIndex(r)) {
3983
- if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
3984
- // v0.8.0 排序融合:0.5×importance(旧) + 0.5×utility(新盖棺分)
3985
- recent.push({ e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) });
3986
- }
3987
- }
3988
- recent.sort(function (a, b) { return b.s - a.s; });
3989
- const recentShown = recent.slice(0, limit);
3990
- if (!recentShown.length) lines.push('(暂无记忆)');
3991
- for (const h of recentShown) {
3992
- const line = '- [' + h.e.type + '] ' + h.e.subject + ': ' + h.e.statement;
3993
- if (budget > 0 && usedChars() + line.length > budget) {
3994
- trace.push('[dropped] ' + h.e.file + ' reason: budget_exceeded');
3995
- break;
3996
- }
3997
- lines.push(line);
3998
- trace.push('[included] ' + h.e.file + ' reason: recent_match');
3999
- }
5435
+ const corridor = readableEntries
5436
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT'; })
5437
+ .sort(compareContextRecent)
5438
+ .filter(function (e) { return !shownFiles.has(e.file); })
5439
+ .slice(0, limit);
5440
+ if (!corridor.length) lines.push('(暂无近期记忆)');
5441
+ for (const e of corridor) appendContextEntry(e, 'recent_corridor');
5442
+ lines.push('');
5443
+
5444
+ lines.push('## 4. 近期高价值记忆(补位)');
4000
5445
  lines.push('');
4001
- lines.push('## 4. 边界提醒(BOUND)');
5446
+ const highValue = readableEntries
5447
+ .filter(function (e) { return !isConsolidatedSummary(e) && e.type !== 'BOUND' && e.type !== 'COMMIT' && !shownFiles.has(e.file); })
5448
+ .map(function (e) { return { e: e, s: 0.5 * importanceScore(e) + 0.5 * utilityScore(e) }; })
5449
+ .sort(function (a, b) { return b.s - a.s; })
5450
+ .slice(0, limit);
5451
+ if (!highValue.length) lines.push('(暂无补位条目)');
5452
+ for (const h of highValue) appendContextEntry(h.e, 'high_value_backfill');
4002
5453
  lines.push('');
4003
- const bounds = [];
4004
- for (const r of roots) {
4005
- for (const e of ensureIndex(r)) {
4006
- if (e.type !== 'BOUND') continue;
4007
- if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
4008
- bounds.push(e);
4009
- }
4010
- }
5454
+ lines.push('## 5. 边界提醒(BOUND)');
5455
+ lines.push('');
5456
+ const bounds = readableEntries.filter(function (e) { return e.type === 'BOUND'; }).sort(compareContextRecent);
4011
5457
  if (!bounds.length) lines.push('(无)');
4012
5458
  for (const e of bounds) lines.push('- ' + (e.subject || '边界') + ': ' + e.statement);
4013
5459
  lines.push('');
4014
- lines.push('## 5. 承诺 / 锚点(COMMIT)');
5460
+ lines.push('## 6. 承诺 / 锚点(COMMIT)');
4015
5461
  lines.push('');
4016
- const commits = [];
4017
- for (const r of roots) {
4018
- for (const e of ensureIndex(r)) {
4019
- if (e.type !== 'COMMIT') continue;
4020
- if (classifyRead(e, owner, '', unsafe, selfAgent) === 'denied') continue;
4021
- commits.push(e);
4022
- }
4023
- }
5462
+ const commits = readableEntries.filter(function (e) { return e.type === 'COMMIT'; }).sort(compareContextRecent);
4024
5463
  if (!commits.length) lines.push('(无)');
4025
5464
  for (const e of commits) lines.push('- ' + (e.subject || '承诺') + ': ' + e.statement);
4026
5465
  lines.push('');
4027
- lines.push('## 6. 收工纪律');
5466
+ lines.push('## 7. 本会话闭环契约');
5467
+ lines.push('');
5468
+ lines.push('- 开工:本上下文包已加载;身份、长期摘要、边界与承诺以本包为准。');
5469
+ lines.push('- 进行中:出现事实 / 偏好 / 边界 / 纠正 / 承诺信号时立即 `remember <type> <subject> <statement> --verify`,不攒到收工。');
5470
+ lines.push('- 收工前复盘:检查本轮是否留下 COMMIT / 会话小结;有关键结论但未落盘时补写并 `recall` 回读,无长期价值不硬凑。');
4028
5471
  lines.push('');
4029
- const allEntries = [];
4030
- for (const r of roots) for (const e of ensureIndex(r)) allEntries.push(e);
5472
+ const allEntries = readableEntries;
4031
5473
  const myCommits = allEntries.filter(function (e) { return e.type === 'COMMIT' && classifyRead(e, owner, '', unsafe, selfAgent) !== 'denied'; });
4032
5474
  if (!myCommits.length) {
4033
5475
  lines.push('- 最近承诺: 无(收工请补 COMMIT / 交接锚点)');
@@ -4040,7 +5482,7 @@ function contextCore(opts) {
4040
5482
  if (oldCount > 0) lines.push('- 归档提醒: ' + oldCount + ' 条超 180 天,建议 archive');
4041
5483
  if (explain) {
4042
5484
  lines.push('');
4043
- lines.push('## 7. 选择解释(--explain)');
5485
+ lines.push('## 8. 选择解释(--explain)');
4044
5486
  lines.push('');
4045
5487
  if (!trace.length) lines.push('(无选择记录)');
4046
5488
  for (const t of trace) lines.push(t);
@@ -4059,6 +5501,10 @@ function contextCore(opts) {
4059
5501
  function cmdContext(opts) {
4060
5502
  const r = contextCore(opts);
4061
5503
  console.log(r.text);
5504
+ if (r.error) {
5505
+ const reminder = migrationReminderText(userRoot());
5506
+ if (reminder) console.log('\n' + reminder);
5507
+ }
4062
5508
  if (r.exitCode) process.exit(r.exitCode);
4063
5509
  }
4064
5510
 
@@ -4103,8 +5549,12 @@ function cmdConfigGet() {
4103
5549
  }
4104
5550
 
4105
5551
  // ---- MCP serve(局域网 streamable HTTP 记忆引擎,零依赖)----
4106
- function mcpTools() {
4107
- return [
5552
+ const MCP_CORE_TOOL_NAMES = ['context', 'recall', 'search', 'remember'];
5553
+ function normalizeMcpToolProfile(profile) {
5554
+ return String(profile || 'full').trim().toLowerCase() === 'core' ? 'core' : 'full';
5555
+ }
5556
+ function mcpTools(profile) {
5557
+ const tools = [
4108
5558
  { name: 'remember', description: '写入一条记忆。参数 type(FACT/PREF/BOUND/COMMIT)、subject、statement 必填;owner 可选,默认当前智能体', inputSchema: { type: 'object', properties: { type: { type: 'string', description: 'FACT/PREF/BOUND/COMMIT' }, subject: { type: 'string' }, statement: { type: 'string' }, owner: { type: 'string' } }, required: ['type', 'subject', 'statement'] } },
4109
5559
  { name: 'recall', description: '检索记忆。query 可选;type 可选;limit 可选(默认 20);explain 可选。只返回当前智能体可读记忆;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
4110
5560
  { name: 'search', description: '检索记忆(同 recall)。query 可选;type 可选;limit 可选(默认 20);explain 可选;embedding 插件只能由本机 config 配置,远端不可传命令', inputSchema: { type: 'object', properties: { query: { type: 'string' }, type: { type: 'string' }, limit: { type: 'number' }, embeddingTimeout: { type: 'number' }, explain: { type: 'boolean' } } } },
@@ -4115,16 +5565,33 @@ function mcpTools() {
4115
5565
  { name: 'reindex', description: '重建索引(手动改 .md 后校正;扫描 facts/prefs/bounds/commits 四目录)', inputSchema: { type: 'object', properties: {} } },
4116
5566
  { name: 'export', description: '导出全部记忆到记忆库内的 JSON 文件。out 可选(默认 <记忆库>/yottamemory-export-<日期>.json;仅限记忆库内路径)', inputSchema: { type: 'object', properties: { out: { type: 'string' } } } },
4117
5567
  { name: 'import', description: '从记忆库内的 JSON 文件导入记忆。src 为文件路径(相对记忆库目录,或记忆库内绝对路径;仅限记忆库内)', inputSchema: { type: 'object', properties: { src: { type: 'string' } }, required: ['src'] } },
4118
- { name: 'agent_info', description: '查看当前智能体身份与登记状态(远端读经 token 校验的 X-Agent-Id;本机读 YOTTA_AGENT_ID)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
5568
+ { name: 'agent_info', description: '查看当前智能体身份与登记状态(HTTP 读经 token 校验的 X-Agent-Id;stdio 读 --agent-id 显式参数)。开工先确认「我是谁」,禁止从记忆里抄别人的 ID', inputSchema: { type: 'object', properties: {} } },
4119
5569
  { name: 'profile', description: '生成当前智能体的用户画像(只读聚合 private/<owner>/ 下 PREF/BOUND/COMMIT 原文,零推断,写入 private/<owner>/profile.md)。owner 默认当前智能体', inputSchema: { type: 'object', properties: { owner: { type: 'string' } } } },
4120
5570
  { name: 'feedback', description: '显式使用反馈(自我学习):useful/useless 调整记忆 weight/confidence/feedback_net。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' }, useful: { type: 'boolean' }, useless: { type: 'boolean' }, reason: { type: 'string' } }, required: ['file'] } },
4121
5571
  { name: 'maintain', description: '记忆自组织(自我进化):规则层归档/遗忘/去重预览。默认 dry-run;apply 才执行,purge 才真删', inputSchema: { type: 'object', properties: { apply: { type: 'boolean' }, purge: { type: 'boolean' }, dedup: { type: 'boolean' }, threshold: { type: 'number' }, age: { type: 'number' } } } },
4122
5572
  { name: 'distill', description: '心理日志蒸馏(自我提升):统计摘要/主题画像/知识地图。owner 默认当前智能体(MCP 不支持 --model 外部命令)', inputSchema: { type: 'object', properties: { owner: { type: 'string' }, subject: { type: 'string' } } } },
4123
5573
  { name: 'explain', description: '解释单条记忆效用分项(为什么靠前/归档/遗忘)。file 为记忆文件路径或文件名', inputSchema: { type: 'object', properties: { file: { type: 'string' } }, required: ['file'] } },
4124
5574
  ];
5575
+ if (normalizeMcpToolProfile(profile) !== 'core') return tools;
5576
+ return MCP_CORE_TOOL_NAMES
5577
+ .map((name) => tools.find((tool) => tool.name === name))
5578
+ .filter(Boolean);
4125
5579
  }
4126
5580
  function callTool(name, args, ctx) {
4127
- const agent = ctx.agent || '';
5581
+ return runWithIdentity({
5582
+ id: ctx && ctx.agent,
5583
+ agentKey: ctx && ctx.agentKey,
5584
+ }, function () {
5585
+ return callToolInner(name, args, ctx);
5586
+ });
5587
+ }
5588
+ function callToolInner(name, args, ctx) {
5589
+ const agent = (ctx && ctx.agent) || '';
5590
+ const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
5591
+ if (toolProfile === 'core' && MCP_CORE_TOOL_NAMES.indexOf(name) === -1) {
5592
+ return { text: '工具 ' + name + ' 属于 full 分组,当前 MCP 以 core 模式启动。需要完整工具集时请使用 --tools full 重新启动。', error: true };
5593
+ }
5594
+ if (agent && !isSafeAgentId(agent)) return { text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。', error: true };
4128
5595
  try {
4129
5596
  if (name === 'remember') {
4130
5597
  const ownerArg = args.owner ? String(args.owner) : '';
@@ -4193,7 +5660,7 @@ function callTool(name, args, ctx) {
4193
5660
  if (name === 'agent_info') {
4194
5661
  const root = userRoot();
4195
5662
  const id = agent || '';
4196
- if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / YOTTA_AGENT_ID)。本机请在 MCP 配置 env 设 YOTTA_AGENT_ID=<唯一ID> 并 iam 登记。', error: false };
5663
+ if (!id) return { text: '当前未声明智能体身份(无 X-Agent-Id / --agent-id)。stdio 请使用 --agent-id <唯一ID> + --agent-key-file <path>;HTTP 请发送 X-Agent-Id + X-Agent-Key。', error: false };
4197
5664
  const agents = loadAgents(root).agents || {};
4198
5665
  const tokens = loadTokens(root).tokens || {};
4199
5666
  let reg = '未登记';
@@ -4240,11 +5707,26 @@ function reqVersion(params) {
4240
5707
  const meta = (params && params._meta) || {};
4241
5708
  return meta['io.modelcontextprotocol/protocolVersion'] || null;
4242
5709
  }
4243
- function modernOk(payload, ttl) {
5710
+ function mcpIdentityMode(ctx) {
5711
+ const mode = String((ctx && ctx.identityMode) || '').trim().toLowerCase();
5712
+ if (mode === 'headers' || mode === 'stdio-args') return mode;
5713
+ if (ctx && ctx.agent) return 'stdio-args';
5714
+ return 'unknown';
5715
+ }
5716
+ function mcpServerInfo(ctx) {
5717
+ return {
5718
+ name: 'yotta-memory',
5719
+ version: VERSION,
5720
+ runtimePath: runtimeExecutionPath(),
5721
+ identityMode: mcpIdentityMode(ctx),
5722
+ toolProfile: normalizeMcpToolProfile(ctx && ctx.toolProfile),
5723
+ };
5724
+ }
5725
+ function modernOk(payload, ttl, ctx) {
4244
5726
  const out = { resultType: 'complete' };
4245
5727
  Object.assign(out, payload);
4246
5728
  if (ttl) { out.ttlMs = ttl[0]; out.cacheScope = ttl[1]; }
4247
- out._meta = { 'io.modelcontextprotocol/serverInfo': { name: 'yotta-memory', version: VERSION } };
5729
+ out._meta = { 'io.modelcontextprotocol/serverInfo': mcpServerInfo(ctx) };
4248
5730
  return out;
4249
5731
  }
4250
5732
  function unsupportedVersion(id, pv) {
@@ -4260,17 +5742,18 @@ function handleMessage(msg, ctx) {
4260
5742
  if (pv !== null) {
4261
5743
  // ---- modern(2026-07-28 无状态)----
4262
5744
  if (pv !== MCP_PROTOCOL_MODERN) return unsupportedVersion(id, pv);
5745
+ const toolProfile = normalizeMcpToolProfile(ctx && ctx.toolProfile);
4263
5746
  if (method === 'server/discover') {
4264
5747
  return { jsonrpc: '2.0', id: id, result: modernOk({
4265
5748
  supportedVersions: [MCP_PROTOCOL_MODERN],
4266
5749
  capabilities: { tools: {} },
4267
- instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):文件式智能体记忆 remember/recall/search/context/forget/archive/maintain 等读写检索与维护;私密按 owner 物理隔离,数据不出本机。'
4268
- }, [3600000, 'public']) };
5750
+ instructions: '元忆 MCP(基于 MCP 最新协议 2026-07-28,向后兼容 2025-11-25 及更早握手):当前工具分组 ' + toolProfile + ';文件式智能体记忆 remember/recall/search/context/forget/archive/maintain 等读写检索与维护;私密按 owner 物理隔离,数据不出本机。'
5751
+ }, [3600000, 'public'], ctx) };
4269
5752
  }
4270
- if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: modernOk({ tools: mcpTools() }, [300000, 'public']) };
5753
+ if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: modernOk({ tools: mcpTools(toolProfile) }, [300000, 'public'], ctx) };
4271
5754
  if (method === 'tools/call') {
4272
5755
  const out = callTool(params.name || '', params.arguments || {}, ctx);
4273
- return { jsonrpc: '2.0', id: id, result: modernOk({ content: [{ type: 'text', text: out.text }], isError: !!out.error }) };
5756
+ return { jsonrpc: '2.0', id: id, result: modernOk({ content: [{ type: 'text', text: out.text }], isError: !!out.error }, null, ctx) };
4274
5757
  }
4275
5758
  if (method === 'initialize') {
4276
5759
  return { jsonrpc: '2.0', id: id, error: { code: -32601, message: "initialize removed in MCP 2026-07-28; use server/discover. supported: ['2026-07-28']" } };
@@ -4279,10 +5762,10 @@ function handleMessage(msg, ctx) {
4279
5762
  }
4280
5763
  // ---- legacy(<=2025-11-25,initialize 握手;响应保持旧形状)----
4281
5764
  if (method === 'initialize') {
4282
- return { jsonrpc: '2.0', id: id, result: { protocolVersion: MCP_PROTOCOL_LEGACY, capabilities: { tools: {} }, serverInfo: { name: 'yotta-memory', version: VERSION } } };
5765
+ return { jsonrpc: '2.0', id: id, result: { protocolVersion: MCP_PROTOCOL_LEGACY, capabilities: { tools: {} }, serverInfo: mcpServerInfo(ctx) } };
4283
5766
  }
4284
5767
  if (method === 'ping') return { jsonrpc: '2.0', id: id, result: {} };
4285
- if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools() } };
5768
+ if (method === 'tools/list') return { jsonrpc: '2.0', id: id, result: { tools: mcpTools(ctx && ctx.toolProfile) } };
4286
5769
  if (method === 'tools/call') {
4287
5770
  const out = callTool(params.name || '', params.arguments || {}, ctx);
4288
5771
  return { jsonrpc: '2.0', id: id, result: { content: [{ type: 'text', text: out.text }], isError: !!out.error } };
@@ -4290,21 +5773,34 @@ function handleMessage(msg, ctx) {
4290
5773
  return { jsonrpc: '2.0', id: id, error: { code: -32601, message: 'Method not found: ' + method } };
4291
5774
  }
4292
5775
  function cmdServe(opts) {
4293
- if (opts.stdio) { cmdServeStdio(); return; }
5776
+ if (opts.stdio) { cmdServeStdio(opts); return; }
5777
+ const legacyEnvError = legacyIdentityEnvError();
5778
+ if (legacyEnvError) {
5779
+ console.error(legacyEnvError);
5780
+ process.exit(2);
5781
+ }
5782
+ if (opts.agent || opts.agentId) {
5783
+ console.error('HTTP MCP 不接受 --agent/--agent-id 启动参数:身份只从请求头 Authorization + X-Agent-Id + X-Agent-Key 读取。');
5784
+ process.exit(2);
5785
+ }
4294
5786
  const host = opts.host || '0.0.0.0';
4295
5787
  const port = opts.port || 8787;
4296
5788
  const noAuth = !!opts.noAuth;
4297
5789
  const root = userRoot();
4298
5790
  ensureInit(root);
4299
5791
  function authorize(req) {
4300
- if (noAuth) return { agent: String(req.headers['x-agent-id'] || '') };
5792
+ const agentId = String(req.headers['x-agent-id'] || '').trim();
5793
+ const agentKey = String(req.headers['x-agent-key'] || '').trim();
5794
+ const toolProfile = normalizeMcpToolProfile(opts.toolProfile);
5795
+ if (noAuth) return { agent: agentId, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
5796
+ if (!agentId || !agentKey) return null;
4301
5797
  const auth = req.headers['authorization'] || '';
4302
5798
  const m = /^Bearer\s+(.+)$/i.exec(auth);
4303
5799
  if (!m) return null;
4304
5800
  const token = m[1].trim();
4305
- const agent = String(req.headers['x-agent-id'] || '');
5801
+ const agent = agentId;
4306
5802
  const tokenMap = (loadTokens(root).tokens) || {};
4307
- if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent };
5803
+ if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey, toolProfile: toolProfile, identityMode: 'headers' };
4308
5804
  return null;
4309
5805
  }
4310
5806
  function originAllowed(req) {
@@ -4330,7 +5826,7 @@ function cmdServe(opts) {
4330
5826
  }
4331
5827
  const auth = authorize(req);
4332
5828
  if (!auth) {
4333
- sendJson(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id' } });
5829
+ sendJson(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id;加密私密操作还需 X-Agent-Key' } });
4334
5830
  return;
4335
5831
  }
4336
5832
  if (req.method === 'GET') {
@@ -4374,17 +5870,41 @@ function cmdServe(opts) {
4374
5870
  console.log('URL: http://' + host + ':' + port + '/mcp');
4375
5871
  console.log('记忆库: ' + root);
4376
5872
  console.log('MCP 协议: 2026-07-28(modern,server/discover)/ 2025-11-25(legacy 握手兼容)');
5873
+ console.log('工具分组: ' + normalizeMcpToolProfile(opts.toolProfile));
4377
5874
  if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
4378
- else console.log('鉴权: Bearer token + X-Agent-Id(yotta-memory token new --agent <id> 生成)');
5875
+ else console.log('鉴权: Bearer token + X-Agent-Id + X-Agent-Key(token 由 token new 生成,agent_key 由 view 授权生成)');
4379
5876
  console.log('按 Ctrl+C 停止');
4380
5877
  startBackupFallback();
4381
5878
  });
4382
5879
  }
4383
5880
  // ---- stdio 本地零进程模式(客户端按需拉起 CLI)----
4384
- function cmdServeStdio() {
5881
+ function cmdServeStdio(opts) {
5882
+ const legacyEnvError = legacyIdentityEnvError();
5883
+ if (legacyEnvError) {
5884
+ console.error(legacyEnvError);
5885
+ process.exit(2);
5886
+ }
5887
+ if (opts.agent) {
5888
+ console.error('stdio MCP 不接受 --agent:请使用显式参数 --agent-id <id> + --agent-key-file <path>。');
5889
+ process.exit(2);
5890
+ }
5891
+ if (opts.agentKey) {
5892
+ console.error('stdio MCP 不接受 --agent-key:命令行会暴露 key;请使用 --agent-key-file <path>。');
5893
+ process.exit(2);
5894
+ }
5895
+ const ident = resolveIdentity(opts);
5896
+ if (ident.error) {
5897
+ console.error(ident.error);
5898
+ process.exit(2);
5899
+ }
4385
5900
  const root = userRoot();
4386
5901
  ensureInit(root);
4387
- const ctx = { agent: currentAgent() };
5902
+ const ctx = {
5903
+ agent: ident.id,
5904
+ agentKey: ident.agentKey,
5905
+ toolProfile: normalizeMcpToolProfile(opts && opts.toolProfile),
5906
+ identityMode: 'stdio-args',
5907
+ };
4388
5908
  let buf = '';
4389
5909
  process.stdin.setEncoding('utf8');
4390
5910
  process.stdin.on('data', function (chunk) {
@@ -4419,7 +5939,7 @@ function lanTaskRunCmd(opts) {
4419
5939
  const port = opts.port || 8787;
4420
5940
  // schtasks /tr 不接受多余内嵌引号:路径无空格不加引号,含空格/引号用 \" 转义(0.5.2 修复)
4421
5941
  const q = (p) => /[\s"]/.test(p) ? '\\"' + p.replace(/"/g, '\\"') + '\\"' : p;
4422
- return q(process.execPath) + ' ' + q(__filename) + ' serve --host ' + host + ' --port ' + port;
5942
+ return q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port;
4423
5943
  }
4424
5944
 
4425
5945
  function lanServeArgs(opts) {
@@ -4473,7 +5993,7 @@ function lanAutostartCmdContent(opts) {
4473
5993
  return '@echo off\r\n'
4474
5994
  + 'chcp 65001 >nul\r\n'
4475
5995
  + 'rem ' + LAN_GEN_MARKER + '; remove with: yotta-memory lan disable (English only, keep ASCII)\r\n'
4476
- + q(process.execPath) + ' ' + q(__filename) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
5996
+ + q(process.execPath) + ' ' + q(runtimeManagedScript()) + ' serve --host ' + host + ' --port ' + port + ' >> ' + q(lanLogPath(opts)) + ' 2>&1\r\n';
4477
5997
  }
4478
5998
  function lanVbsContent(opts) {
4479
5999
  // v0.6.3 自愈:VBS 内联 autostart.cmd 内容,启动时若 .cmd 缺失/被清理即就地重建,
@@ -4550,7 +6070,7 @@ function lanLinuxUnitPath() { return path.join(lanLinuxSystemdUserDir(), LAN_UNI
4550
6070
  function lanLinuxSystemctlBin() { return process.env.YOTTA_LAN_SYSTEMCTL_BIN || 'systemctl'; }
4551
6071
  function lanLinuxLoginctlBin() { return process.env.YOTTA_LAN_LOGINCTL_BIN || 'loginctl'; }
4552
6072
  function lanLinuxExecStart(opts) {
4553
- return [systemdEscapeArg(process.execPath), systemdEscapeArg(__filename)].concat(lanServeArgs(opts)).join(' ');
6073
+ return [systemdEscapeArg(process.execPath), systemdEscapeArg(runtimeManagedScript())].concat(lanServeArgs(opts)).join(' ');
4554
6074
  }
4555
6075
  function lanLinuxUnitContent(opts) {
4556
6076
  // keep comments ASCII (avoid locale/encoding issues); ExecStart uses systemd's own quote rules
@@ -4573,7 +6093,7 @@ function lanLinuxUnitContent(opts) {
4573
6093
  }
4574
6094
  function lanCrontabBin() { return process.env.YOTTA_LAN_CRONTAB_BIN || 'crontab'; }
4575
6095
  function lanCrontabLine(opts) {
4576
- const parts = ['@reboot', shQuote(process.execPath), shQuote(__filename)].concat(lanServeArgs(opts));
6096
+ const parts = ['@reboot', shQuote(process.execPath), shQuote(runtimeManagedScript())].concat(lanServeArgs(opts));
4577
6097
  return parts.join(' ') + ' >> ' + shQuote(lanLogPath(opts)) + ' 2>&1 ' + LAN_CRONTAB_MARKER;
4578
6098
  }
4579
6099
  function lanCrontabRead() {
@@ -4814,6 +6334,11 @@ function cmdLanWinStatus() {
4814
6334
  }
4815
6335
 
4816
6336
  function cmdLanEnable(opts) {
6337
+ const runtime = runtimeEnsureForManagedTask();
6338
+ if (runtime.error) {
6339
+ console.error('runtime 稳定入口未就绪,拒绝注册开机自启: ' + runtime.text);
6340
+ process.exit(1);
6341
+ }
4817
6342
  const p = lanPlatform();
4818
6343
  if (p === 'win32') return cmdLanWinEnable(opts);
4819
6344
  if (p === 'linux') return cmdLanLinuxEnable(opts);
@@ -5110,7 +6635,7 @@ function consolidateCore(opts) {
5110
6635
  const root = userRoot();
5111
6636
  if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
5112
6637
  const apply = !!opts.apply;
5113
- const selfAgent = opts.selfAgent || currentAgent();
6638
+ const selfAgent = resolveIdentity(opts).id;
5114
6639
  const unsafe = !!opts.unsafe;
5115
6640
  const cfg = loadConfig();
5116
6641
  const minAge = (opts.minAge !== undefined && opts.minAge !== null) ? opts.minAge : (parseInt(cfg.consolidate_min_age || '180', 10) || 180);
@@ -5275,7 +6800,7 @@ function autoMergePairCore(root, keepE, dropE, batch, selfAgent, unsafe) {
5275
6800
 
5276
6801
  function appendDedupBlock(lines, root, opts) {
5277
6802
  const apply = !!opts.apply;
5278
- const selfAgent = opts.selfAgent || currentAgent();
6803
+ const selfAgent = resolveIdentity(opts).id;
5279
6804
  const unsafe = !!opts.unsafe;
5280
6805
  lines.push('');
5281
6806
  lines.push('### 重复候选与置信度(--dedup' + (apply ? ' --apply 自动合并' : '') + ')');
@@ -5335,17 +6860,47 @@ function appendDedupBlock(lines, root, opts) {
5335
6860
 
5336
6861
 
5337
6862
  // ---- usage / main ----
6863
+ function cmdRuntimeInstall(arg, opts) {
6864
+ let result;
6865
+ if (opts.fromCurrent) result = runtimeInstallFromCurrent(opts);
6866
+ else if (!arg) {
6867
+ console.error('runtime install 需要 <tarball|版本>,或使用 --from-current。');
6868
+ process.exit(2);
6869
+ } else if (/\.tgz$/i.test(arg) || fs.existsSync(path.resolve(arg))) {
6870
+ result = runtimeInstallTarball(arg, '', opts);
6871
+ } else {
6872
+ result = runtimeInstallVersion(arg, opts);
6873
+ }
6874
+ console.log(result.text);
6875
+ if (result.error) process.exit(2);
6876
+ }
6877
+ function cmdRuntimeUse(version, opts) {
6878
+ const result = runtimeUseCore(version, opts);
6879
+ console.log(result.text);
6880
+ if (result.error) process.exit(2);
6881
+ }
6882
+ function cmdRuntimeRollback(opts) {
6883
+ const result = runtimeRollbackCore(opts);
6884
+ console.log(result.text);
6885
+ if (result.error) process.exit(2);
6886
+ }
6887
+ function cmdRuntimeStatus() {
6888
+ console.log(runtimeStatusCore().text);
6889
+ }
6890
+ function cmdRuntimeList() {
6891
+ console.log(runtimeListCore().text);
6892
+ }
5338
6893
  function usage() {
5339
6894
  const banner = 'yotta-memory v' + VERSION + ' — 元忆:有权限边界的文件式智能体记忆';
5340
6895
  const sections = [
5341
6896
  ['核心记忆', [
5342
6897
  ['init', '初始化记忆库(新建默认加密:需主口令 + 恢复钥匙;--attach 接入已有库;--no-encrypt 降级明文)'],
5343
- ['remember', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
5344
- ['recall', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
6898
+ ['remember <type> <subject> <statement>', '写入记忆(--source 来源;--weight 权重;--verify 写后回读校验;--no-hint 关启发)'],
6899
+ ['recall [关键词]', '检索记忆(--type/--limit/--agent/--owner/--all/--unsafe)'],
5345
6900
  ['forget', '删除一条记忆'],
5346
6901
  ['archive', '归档(--days/--threshold 盖棺分+年龄)'],
5347
6902
  ['backup', '备份记忆库(volumes / setup / status / ensure-daily / schedule / create / list / doctor / restore <id> --to <目录> / drill)'],
5348
- ['doctor', '开工可靠性检查(根目录/密钥库/索引/身份/最近备份;严重异常时锁定破坏性写入)'],
6903
+ ['doctor', '开工可靠性检查(根目录/密钥库/索引/身份/最近备份;--runtime 加查 CLI/current/MCP/进程/技能副本漂移)'],
5349
6904
  ['maintain', '记忆自组织(归档/遗忘候选/去重/合并;默认 dry-run;--dedup 查重+置信度,--dedup --apply 自动合并高置信组)'],
5350
6905
  ['consolidate', '周期摘要压缩(默认 dry-run;--apply 执行;--undo <batch> 回滚;--batches 查批次;候选=超龄+闲置+低效用,immutable/BOUND 豁免)'],
5351
6906
  ['distill', '心理日志蒸馏(统计摘要/主题画像/知识地图;--model 可选外部模型)'],
@@ -5366,11 +6921,12 @@ function usage() {
5366
6921
  ['migrate', '把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)'],
5367
6922
  ['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
5368
6923
  ['reset-password', '重设主口令(忘口令用恢复钥匙)'],
5369
- ['key', '管理 AI 私密读取授权缓存(list / authorize <id> / revoke <id>)'],
6924
+ ['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> [--to <AI_HOME> | --agent-key-file <文件>] / status <id> [--to <AI_HOME> | --agent-key-file <文件>] / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
5370
6925
  ['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
5371
6926
  ]],
5372
6927
  ['平台与服务', [
5373
- ['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程)'],
6928
+ ['serve', '启动 MCP 记忆引擎(streamable HTTP;--stdio 本地零进程;--tools core|full 控制工具分组)'],
6929
+ ['runtime', '运行时稳定入口(list / install <tarball|版本> [--from-current] [--force] / use <版本> [--restart] / rollback [--restart] / status)'],
5374
6930
  ['lan', '开机自启管理(enable/disable/status;Windows 计划任务 / Linux systemd/crontab)'],
5375
6931
  ['--version', '版本']
5376
6932
  ]]
@@ -5385,9 +6941,9 @@ function usage() {
5385
6941
  lines.push('');
5386
6942
  }
5387
6943
  lines.push('类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)');
5388
- lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID/AGENT_ID 当前 agent 标识(本机声明身份用;私密记忆必须有 owner)');
6944
+ lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; 身份不再读取 env;CLI 用 --agent + --agent-key/--agent-key-file,stdio MCP 用 --agent-id + --agent-key-file,HTTP MCP 用请求头');
5389
6945
  lines.push('隔离: 公共 FACT 在 facts/;私密 PREF/BOUND/COMMIT 物理分目录 private/<agent_id>/<type>/,禁止 shell 直读写记忆库,一律走本命令');
5390
- lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id>');
6946
+ lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id> + X-Agent-Key: <agent_key>');
5391
6947
  console.log(lines.join('\n'));
5392
6948
  }
5393
6949
  async function main() {
@@ -5395,7 +6951,7 @@ async function main() {
5395
6951
  if (!args.length) { usage(); return; }
5396
6952
  const opts = {};
5397
6953
  const positional = [];
5398
- const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time']);
6954
+ const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--agent-id', '--agent-key', '--agent-key-file', '--threshold', '--scope', '--host', '--port', '--dir', '--name', '--user', '--relationship', '--source', '--weight', '--budget', '--password', '--new-password', '--recovery-key', '--reason', '--merge', '--model', '--subject', '--embedding', '--focus', '--embedding-timeout', '--min-age', '--min-idle', '--max-utility', '--min-group', '--period', '--to', '--id', '--time', '--tools', '--mcp-config', '--skill-dir']);
5399
6955
  for (let i = 0; i < args.length; i++) {
5400
6956
  const a = args[i];
5401
6957
  if (a === '--version' || a === '-v') { console.log(VERSION); return; }
@@ -5406,6 +6962,8 @@ async function main() {
5406
6962
  else if (a === '--no-auth') opts.noAuth = true;
5407
6963
  else if (a === '--stdio') opts.stdio = true;
5408
6964
  else if (a === '--onstart') opts.onstart = true;
6965
+ else if (a === '--from-current') opts.fromCurrent = true;
6966
+ else if (a === '--restart') opts.restart = true;
5409
6967
  else if (a === '--force') opts.force = true;
5410
6968
  else if (a === '--attach') opts.attach = true;
5411
6969
  else if (a === '--allow-same-volume') opts.allowSameVolume = true;
@@ -5426,6 +6984,7 @@ async function main() {
5426
6984
  else if (a === '--batches') opts.batches = true;
5427
6985
  else if (a === '--explain') opts.explain = true;
5428
6986
  else if (a === '--semantic') opts.semantic = true;
6987
+ else if (a === '--runtime') opts.runtime = true;
5429
6988
  else if (valueOpts.has(a)) {
5430
6989
  const v = args[++i];
5431
6990
  if (a === '--type') opts.type = v;
@@ -5435,6 +6994,9 @@ async function main() {
5435
6994
  else if (a === '--out') opts.out = v;
5436
6995
  else if (a === '--owner') opts.owner = v;
5437
6996
  else if (a === '--agent') opts.agent = v;
6997
+ else if (a === '--agent-id') opts.agentId = v;
6998
+ else if (a === '--agent-key') opts.agentKey = v;
6999
+ else if (a === '--agent-key-file') opts.agentKeyFile = v;
5438
7000
  else if (a === '--scope') opts.scope = v;
5439
7001
  else if (a === '--host') opts.host = v;
5440
7002
  else if (a === '--port') opts.port = parseInt(v, 10) || 8787;
@@ -5463,18 +7025,42 @@ async function main() {
5463
7025
  else if (a === '--to') opts.to = v;
5464
7026
  else if (a === '--id') opts.id = v;
5465
7027
  else if (a === '--time') opts.time = v;
7028
+ else if (a === '--tools') opts.toolProfile = v;
7029
+ else if (a === '--mcp-config') opts.mcpConfigPaths = (opts.mcpConfigPaths || []).concat(v);
7030
+ else if (a === '--skill-dir') opts.skillDirs = (opts.skillDirs || []).concat(v);
5466
7031
  } else if (a.startsWith('--')) {
7032
+ if (a === '--query') {
7033
+ console.error('未知选项: --query。recall/search 的关键词是位置参数:yotta-memory recall [关键词]');
7034
+ process.exit(2);
7035
+ }
5467
7036
  console.error('未知选项: ' + a);
5468
7037
  process.exit(2);
5469
7038
  } else {
5470
7039
  positional.push(a);
5471
7040
  }
5472
7041
  }
7042
+ if (opts.toolProfile !== undefined) {
7043
+ opts.toolProfile = String(opts.toolProfile).trim().toLowerCase();
7044
+ if (opts.toolProfile !== 'core' && opts.toolProfile !== 'full') {
7045
+ console.error('工具分组必须是 core 或 full(例如:yotta-memory serve --stdio --tools core)。');
7046
+ process.exit(2);
7047
+ }
7048
+ }
5473
7049
  const first = positional[0];
5474
7050
  const rest = positional.slice(1);
7051
+ if (first === 'remember' && opts.type) {
7052
+ console.error('位置参数用法:yotta-memory remember <type> <subject> <statement>。--type 仅用于 recall/maintain/consolidate 等筛选,不是 remember 的类型参数。');
7053
+ process.exit(2);
7054
+ }
7055
+ if (first === 'remember' && rest.length < 3) {
7056
+ console.error('位置参数不足:yotta-memory remember <type> <subject> <statement>');
7057
+ process.exit(2);
7058
+ }
7059
+ const runtimeIdentity = resolveIdentity(opts);
7060
+ setRuntimeAgent(runtimeIdentity.error ? '' : runtimeIdentity.id, runtimeIdentity.error ? '' : runtimeIdentity.agentKey);
5475
7061
  switch (first) {
5476
7062
  case 'init': await cmdInit(opts); break;
5477
- case 'whoami': cmdWhoami(); break;
7063
+ case 'whoami': cmdWhoami(opts); break;
5478
7064
  case 'iam': cmdIam(rest[0], opts); break;
5479
7065
  case 'config': {
5480
7066
  const sub = rest[0];
@@ -5528,9 +7114,22 @@ async function main() {
5528
7114
  case 'key': {
5529
7115
  const sub = rest[0];
5530
7116
  if (sub === 'list') cmdKeyList();
7117
+ else if (sub === 'bind' || sub === 'rotate') await cmdKeyBind(rest[1], opts);
5531
7118
  else if (sub === 'authorize') await cmdKeyAuthorize(rest[1], opts);
7119
+ else if (sub === 'claim') cmdKeyClaim(rest[1], opts);
7120
+ else if (sub === 'status') cmdKeyStatus(rest[1], opts);
5532
7121
  else if (sub === 'revoke') cmdKeyRevoke(rest[1]);
5533
- else { console.error('key 子命令: list / authorize <id> / revoke <id>'); process.exit(2); }
7122
+ else { console.error('key 子命令: list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>'); process.exit(2); }
7123
+ break;
7124
+ }
7125
+ case 'runtime': {
7126
+ const sub = rest[0];
7127
+ if (sub === 'install') cmdRuntimeInstall(rest[1], opts);
7128
+ else if (sub === 'use') cmdRuntimeUse(rest[1], opts);
7129
+ else if (sub === 'rollback') cmdRuntimeRollback(opts);
7130
+ else if (sub === 'status') cmdRuntimeStatus();
7131
+ else if (sub === 'list') cmdRuntimeList();
7132
+ else { console.error('runtime 子命令: list / install <tarball|版本> [--from-current] [--force] / use <版本> [--restart] / rollback [--restart] / status'); process.exit(2); }
5534
7133
  break;
5535
7134
  }
5536
7135
  case 'serve': cmdServe(opts); break;
@@ -5580,6 +7179,28 @@ module.exports = {
5580
7179
  collectEntryFiles: collectEntryFiles,
5581
7180
  migrateLayout: migrateLayout,
5582
7181
  typeSubdir: typeSubdir,
7182
+ runtimeRoot: runtimeRoot,
7183
+ runtimeManifestPath: runtimeManifestPath,
7184
+ runtimeVersionsDir: runtimeVersionsDir,
7185
+ runtimeVersionDir: runtimeVersionDir,
7186
+ runtimeCurrentDir: runtimeCurrentDir,
7187
+ runtimeCurrentBin: runtimeCurrentBin,
7188
+ runtimeExecutionPath: runtimeExecutionPath,
7189
+ runtimeManagedScript: runtimeManagedScript,
7190
+ runtimeTreeHash: runtimeTreeHash,
7191
+ runtimeListVersions: runtimeListVersions,
7192
+ runtimeSwitchCurrent: runtimeSwitchCurrent,
7193
+ runtimeInstallFromCurrent: runtimeInstallFromCurrent,
7194
+ runtimeInstallTarball: runtimeInstallTarball,
7195
+ runtimeInstallVersion: runtimeInstallVersion,
7196
+ runtimeValidateTarballEntries: runtimeValidateTarballEntries,
7197
+ runtimeUseCore: runtimeUseCore,
7198
+ runtimeRollbackCore: runtimeRollbackCore,
7199
+ runtimeStatusCore: runtimeStatusCore,
7200
+ runtimeListCore: runtimeListCore,
7201
+ runtimeDoctorCore: runtimeDoctorCore,
7202
+ runtimeEnsureForManagedTask: runtimeEnsureForManagedTask,
7203
+ runtimeRestartManagedServer: runtimeRestartManagedServer,
5583
7204
  lanTaskRunCmd: lanTaskRunCmd,
5584
7205
  lanStartupDir: lanStartupDir,
5585
7206
  lanAutostartDir: lanAutostartDir,
@@ -5643,10 +7264,11 @@ module.exports = {
5643
7264
  unwrapOwnerKey: unwrapOwnerKey,
5644
7265
  unwrapOwnerKeyRecovery: unwrapOwnerKeyRecovery,
5645
7266
  unwrapRecoveryEnc: unwrapRecoveryEnc,
5646
- writeOwnerKeyCache: writeOwnerKeyCache,
5647
7267
  revokeOwnerKeyCache: revokeOwnerKeyCache,
5648
- loadOwnerKeyCache: loadOwnerKeyCache,
5649
7268
  getOwnerKeyFor: getOwnerKeyFor,
7269
+ keyListCore: keyListCore,
7270
+ migrationRequiredInfo: migrationRequiredInfo,
7271
+ migrationReminderText: migrationReminderText,
5650
7272
  encryptMemoryText: encryptMemoryText,
5651
7273
  decryptMemoryText: decryptMemoryText,
5652
7274
  readMemoryText: readMemoryText,
@@ -5654,6 +7276,7 @@ module.exports = {
5654
7276
  loadOwnerIndex: loadOwnerIndex,
5655
7277
  saveOwnerIndex: saveOwnerIndex,
5656
7278
  viewEntriesCore: viewEntriesCore,
7279
+ viewHtml: viewHtml,
5657
7280
  ensureIndex: ensureIndex,
5658
7281
  recallPrefilter: recallPrefilter,
5659
7282
  tokenize: tokenize,
@@ -5674,8 +7297,17 @@ module.exports = {
5674
7297
  writeProfileText: writeProfileText,
5675
7298
  migrateCore: migrateCore,
5676
7299
  resetPasswordCore: resetPasswordCore,
5677
- keyAuthorizeCore: keyAuthorizeCore,
7300
+ keyBindCore: keyBindCore,
5678
7301
  keyRevokeCore: keyRevokeCore,
7302
+ keyClaimCore: keyClaimCore,
7303
+ keyStatusCore: keyStatusCore,
7304
+ writeAgentBinding: writeAgentBinding,
7305
+ unwrapAgentBinding: unwrapAgentBinding,
7306
+ writePendingAgentKey: writePendingAgentKey,
7307
+ removePendingAgentKey: removePendingAgentKey,
7308
+ readPendingAgentKey: readPendingAgentKey,
7309
+ agentKeyFilePath: agentKeyFilePath,
7310
+ setRuntimeAgent: setRuntimeAgent,
5679
7311
  promptPassword: promptPassword,
5680
7312
  initCore: initCore,
5681
7313
  backupCreateCore: backupCreateCore,