@yottameta/yotta-memory 0.13.1 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +21 -10
- package/README.zh-CN.md +25 -14
- package/SKILL.md +79 -21
- package/USER_GUIDE.md +27 -14
- package/assets/view.html +71 -0
- package/bin/yotta-memory.js +631 -93
- package/package.json +1 -1
- package/references/faq.md +4 -4
- package/references/protocol.md +7 -7
- package/skill-manifest.json +1 -1
package/bin/yotta-memory.js
CHANGED
|
@@ -23,7 +23,10 @@ const crypto = require('crypto');
|
|
|
23
23
|
const http = require('http');
|
|
24
24
|
const child_process = require('child_process');
|
|
25
25
|
|
|
26
|
-
const VERSION = '0.13.
|
|
26
|
+
const VERSION = '0.13.2';
|
|
27
|
+
// @generated view-html:start
|
|
28
|
+
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 <钥匙></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 class=\"meta\" style=\"margin-top:6px\">「授权」由你(用户)操作:确认后生成只显示一次的 agent_key,请立即单独保存;服务端同时写临时待领取文件 <code>keys/pending/<agent_id>.key</code>,供该 AI 新会话领取,领取成功后自动删除。</div>\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{'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[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(){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();});};});\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();});};});\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 claim <agent_id> --to <AI_HOME> 领取,写入 <AI_HOME>/.yotta-memory-agent-key 后删除待领取文件。若 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();}\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>\n";
|
|
29
|
+
// @generated view-html:end
|
|
27
30
|
// MCP 协议(2026-07-28 无状态 + 2025-11-25 legacy 握手,dual-era)
|
|
28
31
|
const MCP_PROTOCOL_MODERN = '2026-07-28';
|
|
29
32
|
const MCP_PROTOCOL_LEGACY = '2025-11-25';
|
|
@@ -91,9 +94,77 @@ function today() {
|
|
|
91
94
|
const p = (n) => String(n).padStart(2, '0');
|
|
92
95
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
93
96
|
}
|
|
94
|
-
function
|
|
97
|
+
function envAgentTrusted() {
|
|
98
|
+
const v = String(process.env.YOTTA_MEMORY_TRUST_ENV_AGENT || '').trim().toLowerCase();
|
|
99
|
+
return v === '1' || v === 'true' || v === 'yes';
|
|
100
|
+
}
|
|
101
|
+
function rawEnvAgent() {
|
|
95
102
|
return process.env.YOTTA_AGENT_ID || process.env.AGENT_ID || '';
|
|
96
103
|
}
|
|
104
|
+
let RUNTIME_AGENT = { id: '', agentKey: '' };
|
|
105
|
+
const RUNTIME_OWNER_KEYS = new Map();
|
|
106
|
+
function setRuntimeAgent(id, agentKey) {
|
|
107
|
+
RUNTIME_AGENT = { id: String(id || '').trim(), agentKey: String(agentKey || '').trim() };
|
|
108
|
+
RUNTIME_OWNER_KEYS.clear();
|
|
109
|
+
}
|
|
110
|
+
function trustedEnvAgentKey() {
|
|
111
|
+
return envAgentTrusted() ? String(process.env.YOTTA_MEMORY_AGENT_KEY || '').trim() : '';
|
|
112
|
+
}
|
|
113
|
+
function currentAgent() {
|
|
114
|
+
// Ambient environment identity is only trusted for host-managed MCP processes.
|
|
115
|
+
if (RUNTIME_AGENT.id) return RUNTIME_AGENT.id;
|
|
116
|
+
return envAgentTrusted() ? rawEnvAgent() : '';
|
|
117
|
+
}
|
|
118
|
+
function resolveIdentity(opts) {
|
|
119
|
+
opts = opts || {};
|
|
120
|
+
const explicit = String(opts.agent || opts.selfAgent || '').trim();
|
|
121
|
+
let explicitKey = String(opts.agentKey || '').trim();
|
|
122
|
+
const keyFile = String(opts.agentKeyFile || '').trim();
|
|
123
|
+
const env = String(rawEnvAgent() || '').trim();
|
|
124
|
+
const trusted = envAgentTrusted();
|
|
125
|
+
if (keyFile) {
|
|
126
|
+
let fileKey = '';
|
|
127
|
+
try { fileKey = fs.readFileSync(path.resolve(keyFile), 'utf8').trim(); } catch (e) {
|
|
128
|
+
return { id: '', agentKey: '', source: 'invalid-key-file', error: '无法读取 agent-key 文件: ' + keyFile };
|
|
129
|
+
}
|
|
130
|
+
if (!fileKey) return { id: '', agentKey: '', source: 'invalid-key-file', error: 'agent-key 文件为空: ' + keyFile };
|
|
131
|
+
if (explicitKey && explicitKey !== fileKey) return { id: '', agentKey: '', source: 'key-conflict', error: '--agent-key 与 --agent-key-file 内容不一致。' };
|
|
132
|
+
explicitKey = fileKey;
|
|
133
|
+
}
|
|
134
|
+
if (explicit && !isSafeAgentId(explicit)) {
|
|
135
|
+
return { id: '', agentKey: '', source: 'invalid', error: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
136
|
+
}
|
|
137
|
+
if (env && !isSafeAgentId(env)) {
|
|
138
|
+
return { id: '', agentKey: '', source: 'invalid-env', error: '环境中的 agent ID 非法:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
139
|
+
}
|
|
140
|
+
if (explicit && env && explicit !== env) {
|
|
141
|
+
return {
|
|
142
|
+
id: '',
|
|
143
|
+
agentKey: '',
|
|
144
|
+
source: 'conflict',
|
|
145
|
+
error: '身份冲突:显式身份 ' + explicit + ' 与环境身份 ' + env + ' 不一致。请只保留一个明确身份。',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (explicit) return { id: explicit, agentKey: explicitKey, source: 'explicit', error: '' };
|
|
149
|
+
if (env && trusted) return { id: env, agentKey: trustedEnvAgentKey(), source: 'trusted-env', error: '' };
|
|
150
|
+
if (env && !trusted) {
|
|
151
|
+
return {
|
|
152
|
+
id: '',
|
|
153
|
+
agentKey: '',
|
|
154
|
+
source: 'untrusted-env',
|
|
155
|
+
error: '检测到未受信任的环境身份 ' + env + '。请显式传 --agent <id>,或在 MCP 配置中设置 YOTTA_MEMORY_TRUST_ENV_AGENT=1 后重试。',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return { id: '', agentKey: '', source: 'none', error: '' };
|
|
159
|
+
}
|
|
160
|
+
function isSafeAgentId(id) {
|
|
161
|
+
const value = String(id || '');
|
|
162
|
+
if (!value || value.length > 128) return false;
|
|
163
|
+
if (value === '.' || value === '..') return false;
|
|
164
|
+
if (/[\\/]/.test(value)) return false;
|
|
165
|
+
if (/[\u0000-\u001f]/.test(value)) return false;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
97
168
|
function typeDir(type) {
|
|
98
169
|
const t = String(type).toUpperCase();
|
|
99
170
|
if (!TYPE_DIRS[t]) {
|
|
@@ -528,8 +599,12 @@ function nextSeq(dir) {
|
|
|
528
599
|
const ENC_MAGIC = 'YTMENC1';
|
|
529
600
|
const IDX_MAGIC = 'YTMIDX1';
|
|
530
601
|
const KEY_MAGIC = 'YTMKEY1';
|
|
602
|
+
const AGENT_MAGIC = 'YTMAGENT1';
|
|
531
603
|
const KEYS_DIR = 'keys';
|
|
532
604
|
const KEY_CACHE_DIR = 'cache';
|
|
605
|
+
const AGENT_BINDINGS_DIR = 'bindings';
|
|
606
|
+
const AGENT_PENDING_DIR = 'pending';
|
|
607
|
+
const AGENT_KEY_FILENAME = '.yotta-memory-agent-key';
|
|
533
608
|
const ENC_INDEX_FILE = 'index.enc';
|
|
534
609
|
const ENC_SUFFIX = '.enc';
|
|
535
610
|
const PBKDF2_ITER = 600000;
|
|
@@ -544,6 +619,11 @@ function encOwnerRecPath(root, id) { return path.join(encKeysDir(root), id + '.k
|
|
|
544
619
|
function encRecoveryEncPath(root) { return path.join(encKeysDir(root), 'recovery.key.enc'); }
|
|
545
620
|
function encCacheDir(root) { return path.join(encKeysDir(root), KEY_CACHE_DIR); }
|
|
546
621
|
function encCachePath(root, id) { return path.join(encCacheDir(root), id + '.key'); }
|
|
622
|
+
function encBindingsDir(root) { return path.join(encKeysDir(root), AGENT_BINDINGS_DIR); }
|
|
623
|
+
function encAgentBindingPath(root, id) { return path.join(encBindingsDir(root), id + '.key.agent'); }
|
|
624
|
+
function encPendingDir(root) { return path.join(encKeysDir(root), AGENT_PENDING_DIR); }
|
|
625
|
+
function encPendingPath(root, id) { return path.join(encPendingDir(root), id + '.key'); }
|
|
626
|
+
function agentKeyFilePath(agentHome) { return path.join(path.resolve(agentHome), AGENT_KEY_FILENAME); }
|
|
547
627
|
function isEncrypted(root) { return fs.existsSync(encKeysDir(root)); }
|
|
548
628
|
function isEncFile(fp) { return String(fp).slice(-ENC_SUFFIX.length) === ENC_SUFFIX; }
|
|
549
629
|
function ownerFromPrivatePath(root, fp) {
|
|
@@ -637,26 +717,181 @@ function unwrapOwnerKeyRecovery(root, owner, rk) {
|
|
|
637
717
|
if (buf.slice(0, KEY_MAGIC.length).toString('utf8') !== KEY_MAGIC) throw new Error('恢复密钥文件格式错误: ' + p);
|
|
638
718
|
return aesDecryptBytes(rk, buf.slice(KEY_MAGIC.length), Buffer.from('owner:' + owner, 'utf8'));
|
|
639
719
|
}
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
720
|
+
function recoverOwnerKeyFromRecovery(root, owner, umk, rk) {
|
|
721
|
+
if (!isSafeAgentId(owner) || !Buffer.isBuffer(rk)) return null;
|
|
722
|
+
try {
|
|
723
|
+
const ownerKey = unwrapOwnerKeyRecovery(root, owner, rk);
|
|
724
|
+
if (!ownerKey) return null;
|
|
725
|
+
fs.writeFileSync(encOwnerKeyPath(root, owner), Buffer.concat([
|
|
726
|
+
Buffer.from(KEY_MAGIC, 'utf8'),
|
|
727
|
+
aesEncryptBytes(umk, ownerKey, Buffer.from('owner:' + owner, 'utf8')),
|
|
728
|
+
]));
|
|
729
|
+
return ownerKey;
|
|
730
|
+
} catch (e) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
645
733
|
}
|
|
646
|
-
function
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
734
|
+
function privateOwnerHasData(root, owner) {
|
|
735
|
+
if (!isSafeAgentId(owner)) return false;
|
|
736
|
+
const dir = path.join(root, PRIVATE_DIR, owner);
|
|
737
|
+
if (!fs.existsSync(dir)) return false;
|
|
738
|
+
const stack = [dir];
|
|
739
|
+
while (stack.length) {
|
|
740
|
+
const current = stack.pop();
|
|
741
|
+
let entries = [];
|
|
742
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch (e) { continue; }
|
|
743
|
+
for (const entry of entries) {
|
|
744
|
+
if (entry.isDirectory()) {
|
|
745
|
+
stack.push(path.join(current, entry.name));
|
|
746
|
+
} else if (entry.isFile() && entry.name !== ENC_INDEX_FILE) {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
650
752
|
}
|
|
651
753
|
function revokeOwnerKeyCache(root, owner) {
|
|
652
754
|
const p = encCachePath(root, owner);
|
|
653
755
|
if (fs.existsSync(p)) { fs.unlinkSync(p); return true; }
|
|
654
756
|
return false;
|
|
655
757
|
}
|
|
758
|
+
function writeAgentBinding(root, owner, ownerKey, agentKey) {
|
|
759
|
+
if (!isSafeAgentId(owner)) throw new Error('非法 agent ID');
|
|
760
|
+
if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) throw new Error('agent_key 必须是 32 字节');
|
|
761
|
+
fs.mkdirSync(encBindingsDir(root), { recursive: true });
|
|
762
|
+
const aad = Buffer.from('agent:' + owner, 'utf8');
|
|
763
|
+
const env = aesEncryptBytes(agentKey, ownerKey, aad);
|
|
764
|
+
fs.writeFileSync(encAgentBindingPath(root, owner), Buffer.concat([Buffer.from(AGENT_MAGIC, 'utf8'), env]));
|
|
765
|
+
try { fs.chmodSync(encAgentBindingPath(root, owner), 0o600); } catch (e) {}
|
|
766
|
+
}
|
|
767
|
+
function writePendingAgentKey(root, owner, agentKey) {
|
|
768
|
+
if (!isSafeAgentId(owner)) throw new Error('非法 agent ID');
|
|
769
|
+
if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) throw new Error('agent_key 必须是 32 字节');
|
|
770
|
+
fs.mkdirSync(encPendingDir(root), { recursive: true });
|
|
771
|
+
const fp = encPendingPath(root, owner);
|
|
772
|
+
fs.writeFileSync(fp, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
773
|
+
try { fs.chmodSync(fp, 0o600); } catch (e) {}
|
|
774
|
+
return fp;
|
|
775
|
+
}
|
|
776
|
+
function writeAgentBindingWithPending(root, owner, ownerKey, agentKey) {
|
|
777
|
+
writeAgentBinding(root, owner, ownerKey, agentKey);
|
|
778
|
+
try {
|
|
779
|
+
return writePendingAgentKey(root, owner, agentKey);
|
|
780
|
+
} catch (e) {
|
|
781
|
+
try { fs.unlinkSync(encAgentBindingPath(root, owner)); } catch (ignore) {}
|
|
782
|
+
try { fs.unlinkSync(encPendingPath(root, owner)); } catch (ignore) {}
|
|
783
|
+
throw e;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function removePendingAgentKey(root, owner) {
|
|
787
|
+
if (!isSafeAgentId(owner)) return false;
|
|
788
|
+
const fp = encPendingPath(root, owner);
|
|
789
|
+
if (!fs.existsSync(fp)) return false;
|
|
790
|
+
fs.unlinkSync(fp);
|
|
791
|
+
return true;
|
|
792
|
+
}
|
|
793
|
+
function readPendingAgentKey(root, owner) {
|
|
794
|
+
if (!isSafeAgentId(owner)) return null;
|
|
795
|
+
const fp = encPendingPath(root, owner);
|
|
796
|
+
if (!fs.existsSync(fp)) return null;
|
|
797
|
+
const raw = fs.readFileSync(fp, 'utf8').trim();
|
|
798
|
+
if (!raw) throw new Error('pending agent_key 文件为空');
|
|
799
|
+
const key = Buffer.from(raw, 'base64');
|
|
800
|
+
if (key.length !== 32) throw new Error('pending agent_key 格式错误');
|
|
801
|
+
return key;
|
|
802
|
+
}
|
|
803
|
+
function readAgentKeyFile(fp) {
|
|
804
|
+
const raw = fs.readFileSync(path.resolve(String(fp)), 'utf8').trim();
|
|
805
|
+
if (!raw) throw new Error('agent-key 文件为空');
|
|
806
|
+
const key = Buffer.from(raw, 'base64');
|
|
807
|
+
if (key.length !== 32) throw new Error('agent-key 文件格式错误');
|
|
808
|
+
return key;
|
|
809
|
+
}
|
|
810
|
+
function unwrapAgentBinding(root, owner, agentKey) {
|
|
811
|
+
if (!isSafeAgentId(owner)) return null;
|
|
812
|
+
if (!Buffer.isBuffer(agentKey) || agentKey.length !== 32) return null;
|
|
813
|
+
const p = encAgentBindingPath(root, owner);
|
|
814
|
+
if (!fs.existsSync(p)) return null;
|
|
815
|
+
const buf = fs.readFileSync(p);
|
|
816
|
+
if (buf.slice(0, AGENT_MAGIC.length).toString('utf8') !== AGENT_MAGIC) throw new Error('agent binding 文件格式错误: ' + p);
|
|
817
|
+
return aesDecryptBytes(agentKey, buf.slice(AGENT_MAGIC.length), Buffer.from('agent:' + owner, 'utf8'));
|
|
818
|
+
}
|
|
656
819
|
function getOwnerKeyFor(root, owner) {
|
|
657
820
|
if (!owner) return null;
|
|
658
821
|
if (!isEncrypted(root)) return null;
|
|
659
|
-
|
|
822
|
+
if (!RUNTIME_AGENT.id || RUNTIME_AGENT.id !== owner || !RUNTIME_AGENT.agentKey) return null;
|
|
823
|
+
const agentKey = Buffer.from(RUNTIME_AGENT.agentKey, 'base64');
|
|
824
|
+
return unwrapAgentBinding(root, owner, agentKey);
|
|
825
|
+
}
|
|
826
|
+
function validatePrivateIdentity(root, ident, owner) {
|
|
827
|
+
if (!ident || !ident.id) return '私密操作必须先声明显式身份。';
|
|
828
|
+
if (!isSafeAgentId(owner || ident.id)) return '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。';
|
|
829
|
+
if (!isEncrypted(root)) return '';
|
|
830
|
+
const agentKey = ident.agentKey || (RUNTIME_AGENT.id === ident.id ? RUNTIME_AGENT.agentKey : '');
|
|
831
|
+
if (!agentKey) {
|
|
832
|
+
return '记忆库已加密,但缺少 agent_key。请使用 --agent-key <key>,或在 MCP 配置中设置 YOTTA_MEMORY_AGENT_KEY + YOTTA_MEMORY_TRUST_ENV_AGENT=1。若该 agent 尚未绑定,请执行 yotta-memory key bind ' + (owner || ident.id) + '。';
|
|
833
|
+
}
|
|
834
|
+
setRuntimeAgent(ident.id, agentKey);
|
|
835
|
+
try {
|
|
836
|
+
if (!getOwnerKeyFor(root, owner || ident.id)) {
|
|
837
|
+
return 'agent_key 校验失败:未找到 ' + (owner || ident.id) + ' 的 agent binding(可能已吊销)。请由用户重新授权,或由用户执行 yotta-memory key bind ' + (owner || ident.id) + '。';
|
|
838
|
+
}
|
|
839
|
+
} catch (e) {
|
|
840
|
+
return 'agent_key 无效或 binding 损坏:' + e.message;
|
|
841
|
+
}
|
|
842
|
+
return '';
|
|
843
|
+
}
|
|
844
|
+
function legacyCacheOwners(root) {
|
|
845
|
+
if (!isEncrypted(root)) return [];
|
|
846
|
+
const dir = encCacheDir(root);
|
|
847
|
+
if (!fs.existsSync(dir)) return [];
|
|
848
|
+
const found = [];
|
|
849
|
+
let names = [];
|
|
850
|
+
try { names = fs.readdirSync(dir); } catch (e) { return found; }
|
|
851
|
+
for (const f of names) {
|
|
852
|
+
if (!/\.key$/.test(f)) continue;
|
|
853
|
+
const owner = f.slice(0, -4);
|
|
854
|
+
if (!isSafeAgentId(owner)) continue;
|
|
855
|
+
if (found.indexOf(owner) === -1) found.push(owner);
|
|
856
|
+
}
|
|
857
|
+
return found;
|
|
858
|
+
}
|
|
859
|
+
function migrationRequiredInfo(root) {
|
|
860
|
+
if (!isEncrypted(root)) return null;
|
|
861
|
+
const owners = new Set();
|
|
862
|
+
for (const owner of collectOwners(root)) owners.add(owner);
|
|
863
|
+
for (const owner of legacyCacheOwners(root)) owners.add(owner);
|
|
864
|
+
const actionable = [];
|
|
865
|
+
const legacyOnly = [];
|
|
866
|
+
for (const owner of owners) {
|
|
867
|
+
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));
|
|
868
|
+
const hasLegacy = fs.existsSync(encCachePath(root, owner));
|
|
869
|
+
const bound = fs.existsSync(encAgentBindingPath(root, owner));
|
|
870
|
+
if (!hasPrivate) {
|
|
871
|
+
if (hasLegacy) legacyOnly.push(owner);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (!bound) actionable.push(owner);
|
|
875
|
+
}
|
|
876
|
+
if (!actionable.length) return null;
|
|
877
|
+
const reasons = [];
|
|
878
|
+
if (actionable.some(function (owner) { return fs.existsSync(encCachePath(root, owner)); })) reasons.push('legacy_cache');
|
|
879
|
+
reasons.push('no_agent_binding');
|
|
880
|
+
return {
|
|
881
|
+
marker: '[YTM_MIGRATION_REQUIRED] agent=' + actionable.join(',') + ' reason=' + reasons.join(','),
|
|
882
|
+
owners: actionable,
|
|
883
|
+
reasons: reasons,
|
|
884
|
+
legacyOnly: legacyOnly,
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
function migrationReminderText(root) {
|
|
888
|
+
const info = migrationRequiredInfo(root);
|
|
889
|
+
if (!info) return '';
|
|
890
|
+
let text = info.marker + '\nAI 提醒(升级后首次调用元忆 / 重启会话时请主动转达):旧版授权缓存不再加载,需要重新授权。请让用户自己执行 `yotta-memory view` → 浏览器打开平台 → 输入主口令 → 点「授权」并保存只展示一次的 `agent_key`;这是用户侧操作,AI 不得代替用户执行 `migrate` / `key bind` 迁移,只负责提醒和讲解步骤。';
|
|
891
|
+
if (info.legacyOnly && info.legacyOnly.length) {
|
|
892
|
+
text += '\n另有 legacy 明文 cache 缺少可迁移私密数据(不属于迁移清单): ' + info.legacyOnly.join(', ') + ';如需清理请单独确认。';
|
|
893
|
+
}
|
|
894
|
+
return text;
|
|
660
895
|
}
|
|
661
896
|
function encryptMemoryText(text, ownerKey) {
|
|
662
897
|
const fileKey = crypto.randomBytes(32);
|
|
@@ -1849,6 +2084,14 @@ function doctorCore(opts) {
|
|
|
1849
2084
|
}
|
|
1850
2085
|
}
|
|
1851
2086
|
|
|
2087
|
+
const migration = migrationRequiredInfo(root);
|
|
2088
|
+
if (migration) {
|
|
2089
|
+
checks.agentBindings = { ok: false, pending: migration.owners };
|
|
2090
|
+
warnings.push(migrationReminderText(root));
|
|
2091
|
+
} else if (encrypted) {
|
|
2092
|
+
checks.agentBindings = { ok: true, pending: [] };
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1852
2095
|
const configuredDir = opts.dir || opts.backupDir || cfg.backup_dir;
|
|
1853
2096
|
if (!configuredDir) {
|
|
1854
2097
|
if (cfg.backup_setup_choice === 'manual') {
|
|
@@ -2009,7 +2252,10 @@ function isAllowedBackupPath(rel) {
|
|
|
2009
2252
|
const top = normalized.split('/')[0];
|
|
2010
2253
|
if (top === 'facts' || top === 'private' || top === '.archive') return true;
|
|
2011
2254
|
if (top === 'agents.json' || top === 'index.json' || /^index-\d{4}\.json$/.test(top)) return true;
|
|
2012
|
-
if (top === 'keys')
|
|
2255
|
+
if (top === 'keys') {
|
|
2256
|
+
return normalized !== 'keys/cache' && normalized.indexOf('keys/cache/') !== 0 &&
|
|
2257
|
+
normalized !== 'keys/pending' && normalized.indexOf('keys/pending/') !== 0;
|
|
2258
|
+
}
|
|
2013
2259
|
return false;
|
|
2014
2260
|
}
|
|
2015
2261
|
function backupCreateCore(opts) {
|
|
@@ -2196,18 +2442,12 @@ function backupDrillCore(opts) {
|
|
|
2196
2442
|
if (!salt) throw new Error('恢复副本缺少 keys/salt');
|
|
2197
2443
|
ownerKey = unwrapOwnerKey(target, privateFile.owner, deriveUmk(opts.password, salt));
|
|
2198
2444
|
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
2445
|
}
|
|
2206
2446
|
} catch (error) {
|
|
2207
2447
|
ownerKey = null;
|
|
2208
2448
|
}
|
|
2209
2449
|
if (!ownerKey) {
|
|
2210
|
-
return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password
|
|
2450
|
+
return fail('恢复演练失败: 已恢复文件结构,但无法解密测试私密记忆。请提供 --recovery-key 或 --password;legacy keys/cache 明文缓存不再作为恢复凭证。', {
|
|
2211
2451
|
manifest: true,
|
|
2212
2452
|
index: true,
|
|
2213
2453
|
private: { checked: true, decrypted: false, source: '', file: privateFile.rel },
|
|
@@ -2253,14 +2493,22 @@ function rememberCore(type, subject, statement, opts) {
|
|
|
2253
2493
|
const subj = String(subject || '').trim();
|
|
2254
2494
|
if (!stmt) return { error: true, text: 'statement 不能为空' };
|
|
2255
2495
|
if (!subj) return { error: true, text: 'subject 不能为空' };
|
|
2256
|
-
const
|
|
2496
|
+
const ident = resolveIdentity(opts);
|
|
2497
|
+
const selfAgent = ident.id;
|
|
2257
2498
|
const owner = opts.owner || selfAgent;
|
|
2258
2499
|
const scope = opts.scope || defaultScope(t);
|
|
2500
|
+
if (scope === 'private' && ident.error) {
|
|
2501
|
+
return { error: true, text: ident.error };
|
|
2502
|
+
}
|
|
2503
|
+
if (scope === 'private') {
|
|
2504
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
2505
|
+
if (keyError) return { error: true, text: keyError };
|
|
2506
|
+
}
|
|
2259
2507
|
if (scope === 'private' && !owner) {
|
|
2260
|
-
return { error: true, text: '
|
|
2508
|
+
return { error: true, text: '私密记忆必须显式声明归属智能体:请传 --agent <id>;MCP 场景需由宿主在配置中设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。公共记忆(FACT)不受影响。' };
|
|
2261
2509
|
}
|
|
2262
2510
|
if (scope === 'private' && owner && selfAgent && owner !== selfAgent && !opts.unsafe) {
|
|
2263
|
-
return { error: true, text: '拒绝:
|
|
2511
|
+
return { error: true, text: '拒绝: 当前显式身份 ' + selfAgent + ' 不能写入其它智能体 ' + owner + ' 的私密区。请传正确的 --agent <id>,或加 --unsafe(用户显式授权)。' };
|
|
2264
2512
|
}
|
|
2265
2513
|
const encrypted = isEncrypted(root);
|
|
2266
2514
|
if (scope === 'private' && encrypted && !getOwnerKeyFor(root, owner)) {
|
|
@@ -2366,12 +2614,31 @@ function recallCore(query, opts) {
|
|
|
2366
2614
|
const limit = opts.limit || 50;
|
|
2367
2615
|
const onlyType = opts.type ? String(opts.type).toUpperCase() : null;
|
|
2368
2616
|
const q = query ? String(query).toLowerCase() : '';
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2617
|
+
const ident = resolveIdentity(opts);
|
|
2618
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
2619
|
+
const selfAgent = ident.id;
|
|
2620
|
+
const agent = selfAgent;
|
|
2371
2621
|
const ownerFilter = opts.owner || '';
|
|
2372
2622
|
const allSafe = !!opts.unsafe;
|
|
2373
|
-
|
|
2374
|
-
|
|
2623
|
+
if (ownerFilter && !isSafeAgentId(ownerFilter)) {
|
|
2624
|
+
return { error: true, exitCode: 3, text: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
2625
|
+
}
|
|
2626
|
+
if (!selfAgent && (opts.all || ownerFilter)) {
|
|
2627
|
+
return {
|
|
2628
|
+
error: true,
|
|
2629
|
+
exitCode: 3,
|
|
2630
|
+
text: '显式跨智能体读取必须先声明身份:请传 --agent <id>;MCP 场景需设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。',
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
if (selfAgent && !allSafe) {
|
|
2634
|
+
for (const root of roots) {
|
|
2635
|
+
const keyError = validatePrivateIdentity(root, ident, ownerFilter || selfAgent);
|
|
2636
|
+
if (keyError && isEncrypted(root)) {
|
|
2637
|
+
return { error: true, exitCode: 3, text: keyError };
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
const explicitCross = !!opts.all || (!!ownerFilter && ownerFilter !== agent);
|
|
2375
2642
|
const wantExplain = !!opts.explain;
|
|
2376
2643
|
const useSemantic = opts.semantic !== false;
|
|
2377
2644
|
const prefilter = (q && useSemantic) ? recallPrefilter(q) : null;
|
|
@@ -2483,6 +2750,7 @@ function recallCore(query, opts) {
|
|
|
2483
2750
|
}
|
|
2484
2751
|
}
|
|
2485
2752
|
const lines = ['共 ' + shown.length + ' 条记忆(' + (hits.length > limit ? '前 ' + limit + ' 条' : '全部') + '):'];
|
|
2753
|
+
if (!selfAgent) lines.push('[注意] 未声明身份,本次仅读取公共 FACT;私密 PREF / BOUND / COMMIT 已跳过。');
|
|
2486
2754
|
for (const h of shown) {
|
|
2487
2755
|
lines.push('[' + h.entry.type + '] ' + h.entry.subject + ': ' + h.entry.statement);
|
|
2488
2756
|
lines.push(' ' + path.join(h.root, h.entry.file));
|
|
@@ -2532,7 +2800,7 @@ function appendTrashAudit(root, record) {
|
|
|
2532
2800
|
}
|
|
2533
2801
|
function forgetCore(fileRef, opts) {
|
|
2534
2802
|
opts = opts || {};
|
|
2535
|
-
const selfAgent = opts.
|
|
2803
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2536
2804
|
const roots = memoryRoots();
|
|
2537
2805
|
const ref = String(fileRef || '').replace(/\\/g, '/');
|
|
2538
2806
|
let target = null, targetRoot = null, targetRel = null;
|
|
@@ -2650,7 +2918,7 @@ function checkOwnerWritable(targetRoot, targetRel, selfAgent, unsafe) {
|
|
|
2650
2918
|
// 显式使用反馈:useful/useless → weight/confidence/feedback_net 演化(自我学习闭环)
|
|
2651
2919
|
function feedbackCore(ref, opts) {
|
|
2652
2920
|
opts = opts || {};
|
|
2653
|
-
const selfAgent = opts.
|
|
2921
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2654
2922
|
const target = resolveMemoryTarget(ref);
|
|
2655
2923
|
if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
|
|
2656
2924
|
const deny = checkOwnerWritable(target.root, target.rel, selfAgent, opts.unsafe);
|
|
@@ -2709,7 +2977,7 @@ function feedbackUndoCore(target, owner, meta, selfAgent, opts) {
|
|
|
2709
2977
|
// 解释单条记忆效用分项(为什么靠前/归档/遗忘)
|
|
2710
2978
|
function explainCore(ref, opts) {
|
|
2711
2979
|
opts = opts || {};
|
|
2712
|
-
const selfAgent = opts.
|
|
2980
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2713
2981
|
const target = resolveMemoryTarget(ref);
|
|
2714
2982
|
if (!target) return { error: true, text: '未找到记忆文件: ' + ref };
|
|
2715
2983
|
const owner = ownerFromPrivatePath(target.root, target.fp);
|
|
@@ -2864,7 +3132,7 @@ function maintainCore(opts) {
|
|
|
2864
3132
|
// 手动合并两条相似记忆:保留高 confidence,低 confidence 移入 .archive
|
|
2865
3133
|
function mergeCore(refA, refB, opts) {
|
|
2866
3134
|
opts = opts || {};
|
|
2867
|
-
const selfAgent = opts.
|
|
3135
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2868
3136
|
const ta = resolveMemoryTarget(refA);
|
|
2869
3137
|
const tb = resolveMemoryTarget(refB);
|
|
2870
3138
|
if (!ta || !tb) return { error: true, text: '未找到记忆文件: ' + refA + ' / ' + refB };
|
|
@@ -2908,7 +3176,7 @@ function distillCore(opts) {
|
|
|
2908
3176
|
opts = opts || {};
|
|
2909
3177
|
const root = userRoot();
|
|
2910
3178
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
2911
|
-
const selfAgent = opts.
|
|
3179
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
2912
3180
|
const owner = opts.owner || selfAgent;
|
|
2913
3181
|
const entries = [];
|
|
2914
3182
|
for (const r of memoryRoots()) {
|
|
@@ -3059,6 +3327,8 @@ function promptPassword(promptText) {
|
|
|
3059
3327
|
}
|
|
3060
3328
|
|
|
3061
3329
|
function migrateCore(root, password, recoveryKeyIn) {
|
|
3330
|
+
// 第三个参数历史上是 recovery key;兼容旧调用,同时支持 { agent, recoveryKey } 选项对象。
|
|
3331
|
+
const opts = recoveryKeyIn && typeof recoveryKeyIn === 'object' ? recoveryKeyIn : {};
|
|
3062
3332
|
if (!fs.existsSync(root)) return { error: true, text: '记忆库不存在,请先 yotta-memory init。' };
|
|
3063
3333
|
if (isEncrypted(root)) return { error: true, text: '记忆库已启用加密(keys/ 存在),无需迁移。' };
|
|
3064
3334
|
if (!hasPlaintextPrivate(root)) return { error: false, text: '无私密明文可迁移(private/ 下无 .md 文件);如需新建加密库请 init --encrypt。' };
|
|
@@ -3070,10 +3340,9 @@ function migrateCore(root, password, recoveryKeyIn) {
|
|
|
3070
3340
|
fs.writeFileSync(encRecoveryEncPath(root), Buffer.concat([Buffer.from(KEY_MAGIC, 'utf8'), aesEncryptBytes(umk, rk, Buffer.from('recovery', 'utf8'))]));
|
|
3071
3341
|
let moved = 0;
|
|
3072
3342
|
const owners = collectOwners(root);
|
|
3073
|
-
const self =
|
|
3343
|
+
const self = resolveIdentity(opts).id;
|
|
3074
3344
|
for (const owner of owners) {
|
|
3075
3345
|
const ok = wrapOwnerKey(root, umk, rk, owner);
|
|
3076
|
-
if (self && owner === self) writeOwnerKeyCache(root, owner, ok);
|
|
3077
3346
|
for (const t of PRIVATE_LEAF) {
|
|
3078
3347
|
const d = path.join(root, PRIVATE_DIR, owner, t);
|
|
3079
3348
|
if (!fs.existsSync(d)) continue;
|
|
@@ -3097,8 +3366,8 @@ function migrateCore(root, password, recoveryKeyIn) {
|
|
|
3097
3366
|
buildIndex(root);
|
|
3098
3367
|
let tail = '已迁移 ' + moved + ' 个私密文件(' + owners.length + ' 个 owner)到密文。';
|
|
3099
3368
|
tail += '\n[恢复钥匙](务必离线保存,仅此一次,泄露=可解全部私密): ' + rk.toString('base64');
|
|
3100
|
-
|
|
3101
|
-
|
|
3369
|
+
tail += '\n迁移不会写明文 owner key cache;请为每个 agent 执行一次 yotta-memory key bind <id>,再注入 agent_key。';
|
|
3370
|
+
if (self && owners.indexOf(self) !== -1) tail += '\n你的身份:' + self + ',请先执行 yotta-memory key bind ' + self + '。';
|
|
3102
3371
|
return { error: false, text: tail };
|
|
3103
3372
|
}
|
|
3104
3373
|
|
|
@@ -3111,7 +3380,7 @@ async function cmdMigrate(opts) {
|
|
|
3111
3380
|
password = await promptPassword('主口令(迁移后私密区加密,勿忘;忘口令可用恢复钥匙重设): ') || '';
|
|
3112
3381
|
if (!password) { console.error('已取消。'); process.exit(2); }
|
|
3113
3382
|
}
|
|
3114
|
-
const r = migrateCore(root, password, null);
|
|
3383
|
+
const r = migrateCore(root, password, { agent: opts.agent, recoveryKey: null });
|
|
3115
3384
|
console.log(r.text);
|
|
3116
3385
|
if (r.error) process.exit(2);
|
|
3117
3386
|
}
|
|
@@ -3177,51 +3446,170 @@ async function cmdResetPassword(opts) {
|
|
|
3177
3446
|
if (r.error) process.exit(2);
|
|
3178
3447
|
}
|
|
3179
3448
|
|
|
3180
|
-
function
|
|
3449
|
+
function keyBindCore(root, owner, opts) {
|
|
3450
|
+
opts = opts || {};
|
|
3451
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3181
3452
|
if (!isEncrypted(root)) return { error: true, text: '记忆库未启用加密。' };
|
|
3182
3453
|
const salt = loadSalt(root);
|
|
3183
3454
|
if (!salt) return { error: true, text: '密钥库损坏(缺 keys/salt)。' };
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3455
|
+
let ownerKey = null;
|
|
3456
|
+
if (opts.recoveryKey) {
|
|
3457
|
+
const rk = Buffer.from(String(opts.recoveryKey), 'base64');
|
|
3458
|
+
if (rk.length !== 32) return { error: true, text: '恢复钥匙须为 32 字节(base64)。' };
|
|
3459
|
+
try { ownerKey = unwrapOwnerKeyRecovery(root, owner, rk); } catch (e) { ownerKey = null; }
|
|
3460
|
+
if (!ownerKey) return { error: true, text: '恢复钥匙无法解开 ' + owner + ' 的 owner key。' };
|
|
3461
|
+
} else {
|
|
3462
|
+
const password = String(opts.password || '');
|
|
3463
|
+
if (!password) return { error: true, text: '请提供主口令(--password 或交互输入),或用 --recovery-key。' };
|
|
3464
|
+
const umk = deriveUmk(password, salt);
|
|
3465
|
+
try { ownerKey = unwrapOwnerKey(root, owner, umk); } catch (e) { ownerKey = null; }
|
|
3466
|
+
if (!ownerKey) {
|
|
3467
|
+
let rk = null;
|
|
3468
|
+
try { rk = unwrapRecoveryEnc(root, umk); } catch (e) { rk = null; }
|
|
3469
|
+
if (!rk) return { error: true, text: '口令错误,或该 owner 的密钥不存在。' };
|
|
3470
|
+
ownerKey = recoverOwnerKeyFromRecovery(root, owner, umk, rk);
|
|
3471
|
+
if (!ownerKey) {
|
|
3472
|
+
if (privateOwnerHasData(root, owner)) {
|
|
3473
|
+
return { error: true, text: '检测到 private/' + owner + ' 仍有密文,但 keys/' + owner + '.key.enc 与恢复文件均不可用;拒绝新建 owner key(会导致旧数据无法解密)。请从备份恢复原 owner key,或使用恢复钥匙修复后重试。' };
|
|
3474
|
+
}
|
|
3475
|
+
ownerKey = wrapOwnerKey(root, umk, rk, owner);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3192
3478
|
}
|
|
3193
|
-
|
|
3194
|
-
|
|
3479
|
+
const agentKey = crypto.randomBytes(32);
|
|
3480
|
+
const pendingFile = writeAgentBindingWithPending(root, owner, ownerKey, agentKey);
|
|
3481
|
+
revokeOwnerKeyCache(root, owner);
|
|
3482
|
+
return {
|
|
3483
|
+
error: false,
|
|
3484
|
+
agentKey: agentKey.toString('base64'),
|
|
3485
|
+
pendingFile: pendingFile,
|
|
3486
|
+
text: '已绑定 ' + owner + ' 的 agent_key,并删除旧明文 cache。\nagent_key: ' + agentKey.toString('base64') + '\n待领取文件: ' + pendingFile + '\nAI 在新会话执行 yotta-memory key claim ' + owner + ' --to <AI_HOME> 领取;用户请另外保存此 key;此 key 只显示一次。',
|
|
3487
|
+
};
|
|
3195
3488
|
}
|
|
3196
3489
|
|
|
3197
3490
|
function keyRevokeCore(root, owner) {
|
|
3198
|
-
if (!
|
|
3199
|
-
|
|
3491
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3492
|
+
let removed = false;
|
|
3493
|
+
const binding = encAgentBindingPath(root, owner);
|
|
3494
|
+
if (fs.existsSync(binding)) { fs.unlinkSync(binding); removed = true; }
|
|
3495
|
+
if (removePendingAgentKey(root, owner)) removed = true;
|
|
3496
|
+
if (revokeOwnerKeyCache(root, owner)) removed = true;
|
|
3497
|
+
if (!removed) return { error: false, text: owner + ' 无 agent binding(已是未绑定状态)。' };
|
|
3498
|
+
return { error: false, text: '已吊销 ' + owner + ' 的 agent_key binding;该 agent 将无法解密其私密。' };
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
function keyClaimCore(root, owner, targetHome) {
|
|
3502
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3503
|
+
if (!targetHome) return { error: true, text: '请用 --to <AI_HOME> 指定 AI 宿主目录。' };
|
|
3504
|
+
const pending = encPendingPath(root, owner);
|
|
3505
|
+
if (!fs.existsSync(pending)) {
|
|
3506
|
+
return { error: true, text: '未找到待领取 agent_key:' + pending + '。请先在 yotta-memory view 中授权,或执行 yotta-memory key bind ' + owner + '。' };
|
|
3507
|
+
}
|
|
3508
|
+
let agentKey;
|
|
3509
|
+
try {
|
|
3510
|
+
agentKey = readPendingAgentKey(root, owner);
|
|
3511
|
+
if (!agentKey) throw new Error('pending agent_key 不存在');
|
|
3512
|
+
if (!unwrapAgentBinding(root, owner, agentKey)) {
|
|
3513
|
+
throw new Error('agent binding 不存在或已失效');
|
|
3514
|
+
}
|
|
3515
|
+
} catch (e) {
|
|
3516
|
+
return { error: true, text: 'agent_key 校验失败:' + e.message + '。未写入宿主目录,pending 文件保留。' };
|
|
3517
|
+
}
|
|
3518
|
+
let targetDir;
|
|
3519
|
+
let targetFile;
|
|
3520
|
+
let tempFile;
|
|
3521
|
+
try {
|
|
3522
|
+
targetDir = path.resolve(String(targetHome));
|
|
3523
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
3524
|
+
targetFile = agentKeyFilePath(targetDir);
|
|
3525
|
+
tempFile = path.join(targetDir, AGENT_KEY_FILENAME + '.tmp-' + process.pid + '-' + crypto.randomBytes(4).toString('hex'));
|
|
3526
|
+
fs.writeFileSync(tempFile, agentKey.toString('base64') + '\n', { encoding: 'utf8', mode: 0o600 });
|
|
3527
|
+
try { fs.chmodSync(tempFile, 0o600); } catch (e) {}
|
|
3528
|
+
try {
|
|
3529
|
+
fs.renameSync(tempFile, targetFile);
|
|
3530
|
+
} catch (e) {
|
|
3531
|
+
fs.rmSync(targetFile, { force: true });
|
|
3532
|
+
fs.renameSync(tempFile, targetFile);
|
|
3533
|
+
}
|
|
3534
|
+
const readBack = fs.readFileSync(targetFile, 'utf8').trim();
|
|
3535
|
+
if (readBack !== agentKey.toString('base64')) throw new Error('宿主 key 文件回读不一致');
|
|
3536
|
+
} catch (e) {
|
|
3537
|
+
try { if (tempFile) fs.rmSync(tempFile, { force: true }); } catch (ignore) {}
|
|
3538
|
+
return { error: true, text: '写入宿主 key 文件失败:' + e.message + '。pending 文件保留,可重试。' };
|
|
3539
|
+
}
|
|
3540
|
+
removePendingAgentKey(root, owner);
|
|
3541
|
+
return { error: false, targetFile: targetFile, text: '已领取 ' + owner + ' 的 agent_key。\n目标文件: ' + targetFile + '\npending 文件已删除。' };
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
function keyStatusCore(root, owner, targetHome) {
|
|
3545
|
+
if (!isSafeAgentId(owner)) return { error: true, text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' };
|
|
3546
|
+
const binding = fs.existsSync(encAgentBindingPath(root, owner));
|
|
3547
|
+
const pending = fs.existsSync(encPendingPath(root, owner));
|
|
3548
|
+
let hostKey = 'missing';
|
|
3549
|
+
if (targetHome) {
|
|
3550
|
+
const fp = agentKeyFilePath(targetHome);
|
|
3551
|
+
if (fs.existsSync(fp)) {
|
|
3552
|
+
hostKey = 'invalid';
|
|
3553
|
+
try {
|
|
3554
|
+
const key = readAgentKeyFile(fp);
|
|
3555
|
+
if (binding && unwrapAgentBinding(root, owner, key)) hostKey = 'valid';
|
|
3556
|
+
} catch (e) {}
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
return {
|
|
3560
|
+
error: false,
|
|
3561
|
+
binding: binding,
|
|
3562
|
+
pending: pending,
|
|
3563
|
+
hostKey: hostKey,
|
|
3564
|
+
targetFile: targetHome ? agentKeyFilePath(targetHome) : '',
|
|
3565
|
+
text: 'agent: ' + owner + '\nbinding: ' + (binding ? 'yes' : 'no') + '\npending: ' + (pending ? 'yes' : 'no') + '\nhost_key: ' + hostKey + (targetHome ? '\ntarget: ' + agentKeyFilePath(targetHome) : ''),
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
function keyListCore(root) {
|
|
3570
|
+
root = root || userRoot();
|
|
3571
|
+
if (!isEncrypted(root)) return { text: '记忆库未启用加密。' };
|
|
3572
|
+
const owners = collectOwners(root);
|
|
3573
|
+
const bound = owners.filter(function (o) { return fs.existsSync(encAgentBindingPath(root, o)); });
|
|
3574
|
+
const pending = owners.filter(function (o) { return fs.existsSync(encPendingPath(root, o)); });
|
|
3575
|
+
const legacy = owners.filter(function (o) { return fs.existsSync(encCachePath(root, o)); });
|
|
3576
|
+
const lines = [
|
|
3577
|
+
'owner: ' + (owners.length ? owners.join(', ') : '(无)'),
|
|
3578
|
+
'已绑定 agent_key: ' + (bound.length ? bound.join(', ') : '(无)'),
|
|
3579
|
+
'待领取 agent_key: ' + (pending.length ? pending.join(', ') : '(无)'),
|
|
3580
|
+
'legacy 明文 cache: ' + (legacy.length ? legacy.join(', ') + '(升级后不再加载)' : '(无)'),
|
|
3581
|
+
];
|
|
3582
|
+
return { text: lines.join('\n'), owners: owners, bound: bound, pending: pending, legacy: legacy };
|
|
3200
3583
|
}
|
|
3201
3584
|
|
|
3202
3585
|
function cmdKeyList() {
|
|
3203
3586
|
const root = userRoot();
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
console.log('
|
|
3587
|
+
const r = keyListCore(root);
|
|
3588
|
+
console.log(r.text);
|
|
3589
|
+
if (!r.owners) return;
|
|
3590
|
+
const reminder = migrationReminderText(root);
|
|
3591
|
+
if (reminder) console.log('\n' + reminder);
|
|
3209
3592
|
}
|
|
3210
3593
|
|
|
3211
|
-
async function
|
|
3594
|
+
async function cmdKeyBind(id, opts) {
|
|
3212
3595
|
const root = userRoot();
|
|
3213
3596
|
if (!isEncrypted(root)) { console.error('记忆库未启用加密。'); process.exit(2); }
|
|
3214
|
-
if (!id) { console.error('请指定 <id>:yotta-memory key
|
|
3597
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key bind <id>'); process.exit(2); }
|
|
3215
3598
|
let password = opts.password || process.env.YOTTA_MEMORY_PASS || '';
|
|
3216
|
-
if (!password) {
|
|
3599
|
+
if (!password && !opts.recoveryKey) {
|
|
3217
3600
|
password = await promptPassword('主口令: ') || '';
|
|
3218
3601
|
if (!password) { console.error('已取消。'); process.exit(2); }
|
|
3219
3602
|
}
|
|
3220
|
-
const r =
|
|
3603
|
+
const r = keyBindCore(root, id, { password: password, recoveryKey: opts.recoveryKey });
|
|
3221
3604
|
console.log(r.text);
|
|
3222
3605
|
if (r.error) process.exit(2);
|
|
3223
3606
|
}
|
|
3224
3607
|
|
|
3608
|
+
async function cmdKeyAuthorize(id, opts) {
|
|
3609
|
+
console.error('提示: key authorize 已由 key bind 替代,本次按 bind 执行。');
|
|
3610
|
+
return cmdKeyBind(id, opts);
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3225
3613
|
function cmdKeyRevoke(id) {
|
|
3226
3614
|
const root = userRoot();
|
|
3227
3615
|
if (!id) { console.error('请指定 <id>:yotta-memory key revoke <id>'); process.exit(2); }
|
|
@@ -3230,6 +3618,22 @@ function cmdKeyRevoke(id) {
|
|
|
3230
3618
|
if (r.error) process.exit(2);
|
|
3231
3619
|
}
|
|
3232
3620
|
|
|
3621
|
+
function cmdKeyClaim(id, opts) {
|
|
3622
|
+
const root = userRoot();
|
|
3623
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key claim <id> --to <AI_HOME>'); process.exit(2); }
|
|
3624
|
+
const r = keyClaimCore(root, id, opts.to);
|
|
3625
|
+
console.log(r.text);
|
|
3626
|
+
if (r.error) process.exit(2);
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
function cmdKeyStatus(id, opts) {
|
|
3630
|
+
const root = userRoot();
|
|
3631
|
+
if (!id) { console.error('请指定 <id>:yotta-memory key status <id> [--to <AI_HOME>]'); process.exit(2); }
|
|
3632
|
+
const r = keyStatusCore(root, id, opts.to);
|
|
3633
|
+
console.log(r.text);
|
|
3634
|
+
if (r.error) process.exit(2);
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3233
3637
|
// v0.8.1: 查看平台分页——服务端只返回当前页(offset/limit),避免记忆多了一次全量加载/渲染/传输
|
|
3234
3638
|
function viewEntriesCore(root, session, query, offset, limit) {
|
|
3235
3639
|
limit = Math.max(1, parseInt(limit, 10) || 50);
|
|
@@ -3250,13 +3654,50 @@ function viewEntriesCore(root, session, query, offset, limit) {
|
|
|
3250
3654
|
const page = all.slice(offset, offset + limit);
|
|
3251
3655
|
return { count: count, offset: offset, limit: limit, hasMore: offset + limit < count, entries: page };
|
|
3252
3656
|
}
|
|
3657
|
+
function viewHostName(req) {
|
|
3658
|
+
const raw = String(req.headers.host || '').trim();
|
|
3659
|
+
if (!raw) return '';
|
|
3660
|
+
try {
|
|
3661
|
+
return new URL('http://' + raw).hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
3662
|
+
} catch (e) {
|
|
3663
|
+
return '';
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
function viewHostAllowed(req, bindHost) {
|
|
3667
|
+
const name = viewHostName(req);
|
|
3668
|
+
if (!name) return false;
|
|
3669
|
+
const bind = String(bindHost || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
3670
|
+
if (bind !== '127.0.0.1' && bind !== 'localhost' && bind !== '::1') return true;
|
|
3671
|
+
return name === '127.0.0.1' || name === 'localhost' || name === '::1';
|
|
3672
|
+
}
|
|
3673
|
+
function viewOriginAllowed(req) {
|
|
3674
|
+
const origin = req.headers.origin;
|
|
3675
|
+
if (!origin) return true;
|
|
3676
|
+
let originHost = '';
|
|
3677
|
+
try {
|
|
3678
|
+
originHost = new URL(String(origin)).host.toLowerCase();
|
|
3679
|
+
} catch (e) {
|
|
3680
|
+
return false;
|
|
3681
|
+
}
|
|
3682
|
+
const requestHost = String(req.headers.host || '').trim().toLowerCase();
|
|
3683
|
+
return !!requestHost && originHost === requestHost;
|
|
3684
|
+
}
|
|
3253
3685
|
function viewServerCore(root, port, host) {
|
|
3254
3686
|
let session = { umk: null, ownerKeys: {} };
|
|
3255
3687
|
const server = http.createServer(function (req, res) {
|
|
3688
|
+
if (!viewHostAllowed(req, host) || !viewOriginAllowed(req)) {
|
|
3689
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });
|
|
3690
|
+
res.end('forbidden');
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3256
3693
|
let pathname = '/';
|
|
3257
3694
|
try { pathname = new URL(req.url, 'http://' + (req.headers.host || 'localhost')).pathname; } catch (e) {}
|
|
3258
3695
|
function json(code, obj) {
|
|
3259
|
-
res.writeHead(code, {
|
|
3696
|
+
res.writeHead(code, {
|
|
3697
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
3698
|
+
'Cache-Control': 'no-store',
|
|
3699
|
+
'X-Content-Type-Options': 'nosniff',
|
|
3700
|
+
});
|
|
3260
3701
|
res.end(JSON.stringify(obj));
|
|
3261
3702
|
}
|
|
3262
3703
|
function readBody(cb) {
|
|
@@ -3269,8 +3710,15 @@ function viewServerCore(root, port, host) {
|
|
|
3269
3710
|
});
|
|
3270
3711
|
}
|
|
3271
3712
|
if (req.method === 'GET' && pathname === '/') {
|
|
3272
|
-
res.writeHead(200, {
|
|
3273
|
-
|
|
3713
|
+
res.writeHead(200, {
|
|
3714
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
3715
|
+
'Cache-Control': 'no-store',
|
|
3716
|
+
'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'",
|
|
3717
|
+
'X-Content-Type-Options': 'nosniff',
|
|
3718
|
+
'X-Frame-Options': 'DENY',
|
|
3719
|
+
'Referrer-Policy': 'no-referrer',
|
|
3720
|
+
});
|
|
3721
|
+
res.end(viewHtml());
|
|
3274
3722
|
return;
|
|
3275
3723
|
}
|
|
3276
3724
|
if (req.method === 'POST' && pathname === '/api/status') {
|
|
@@ -3298,7 +3746,7 @@ function viewServerCore(root, port, host) {
|
|
|
3298
3746
|
const agents = loadAgents(root).agents || {};
|
|
3299
3747
|
const owners = keyOwners(root);
|
|
3300
3748
|
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:
|
|
3749
|
+
return json(200, { owners: all.map(function (o) { return { owner: o, registered: !!agents[o], authorized: fs.existsSync(encAgentBindingPath(root, o)) }; }) });
|
|
3302
3750
|
}
|
|
3303
3751
|
if (req.method === 'POST' && pathname === '/api/entries') {
|
|
3304
3752
|
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
@@ -3320,22 +3768,39 @@ function viewServerCore(root, port, host) {
|
|
|
3320
3768
|
return readBody(function (d) {
|
|
3321
3769
|
const owner = String(d.owner || '');
|
|
3322
3770
|
if (!owner) return json(400, { error: '缺少 owner。' });
|
|
3771
|
+
if (!isSafeAgentId(owner)) return json(400, { error: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' });
|
|
3772
|
+
if (fs.existsSync(encAgentBindingPath(root, owner))) {
|
|
3773
|
+
return json(409, { error: '该智能体已有 agent binding。为避免打断在用的 agent_key,请先“吊销”,再重新授权生成新 key。' });
|
|
3774
|
+
}
|
|
3323
3775
|
let ok = session.ownerKeys[owner];
|
|
3324
3776
|
let rk = null;
|
|
3325
3777
|
try { rk = unwrapRecoveryEnc(root, session.umk); } catch (e) { rk = null; }
|
|
3326
3778
|
if (!ok) {
|
|
3327
3779
|
if (!rk) return json(400, { error: '密钥库缺少恢复钥匙(recovery.key.enc),无法为新 owner 建钥。' });
|
|
3328
|
-
ok =
|
|
3780
|
+
ok = recoverOwnerKeyFromRecovery(root, owner, session.umk, rk);
|
|
3781
|
+
if (!ok) {
|
|
3782
|
+
if (privateOwnerHasData(root, owner)) {
|
|
3783
|
+
return json(400, { error: '检测到 private/' + owner + ' 仍有密文,但 keys/' + owner + '.key.enc 与恢复文件均不可用;拒绝新建 owner key(会导致旧数据无法解密)。请从备份恢复原 owner key,或使用 CLI 恢复钥匙修复后重试。' });
|
|
3784
|
+
}
|
|
3785
|
+
ok = wrapOwnerKey(root, session.umk, rk, owner);
|
|
3786
|
+
}
|
|
3329
3787
|
session.ownerKeys[owner] = ok;
|
|
3330
3788
|
}
|
|
3331
|
-
|
|
3332
|
-
|
|
3789
|
+
const agentKey = crypto.randomBytes(32);
|
|
3790
|
+
const pendingFile = writeAgentBindingWithPending(root, owner, ok, agentKey);
|
|
3791
|
+
revokeOwnerKeyCache(root, owner);
|
|
3792
|
+
json(200, { ok: true, agentKey: agentKey.toString('base64'), pendingFile: pendingFile, text: 'agent_key 只显示一次,请用户自行保存;AI 可由 pending 文件领取并写入宿主目录。' });
|
|
3333
3793
|
});
|
|
3334
3794
|
}
|
|
3335
3795
|
if (req.method === 'POST' && pathname === '/api/revoke') {
|
|
3336
3796
|
if (!session.umk) return json(401, { error: '请先解锁。' });
|
|
3337
3797
|
return readBody(function (d) {
|
|
3338
|
-
|
|
3798
|
+
const owner = String(d.owner || '');
|
|
3799
|
+
if (!isSafeAgentId(owner)) return json(400, { error: '非法 owner ID:只允许单段名称,禁止 /、\\、.. 和控制字符。' });
|
|
3800
|
+
const binding = encAgentBindingPath(root, owner);
|
|
3801
|
+
if (fs.existsSync(binding)) fs.unlinkSync(binding);
|
|
3802
|
+
removePendingAgentKey(root, owner);
|
|
3803
|
+
revokeOwnerKeyCache(root, owner);
|
|
3339
3804
|
json(200, { ok: true });
|
|
3340
3805
|
});
|
|
3341
3806
|
}
|
|
@@ -3380,7 +3845,9 @@ function cmdView(opts) {
|
|
|
3380
3845
|
viewServerCore(root, port, host);
|
|
3381
3846
|
}
|
|
3382
3847
|
|
|
3383
|
-
|
|
3848
|
+
function viewHtml() {
|
|
3849
|
+
return VIEW_HTML;
|
|
3850
|
+
}
|
|
3384
3851
|
|
|
3385
3852
|
// ---- 命令包装(CLI 入口)----
|
|
3386
3853
|
async function cmdInit(opts) {
|
|
@@ -3448,8 +3915,13 @@ function cmdDistill(opts) {
|
|
|
3448
3915
|
if (r.error) process.exit(2);
|
|
3449
3916
|
}
|
|
3450
3917
|
function cmdForget(fileRef, opts) {
|
|
3918
|
+
const ident = resolveIdentity(opts);
|
|
3919
|
+
if (ident.error) {
|
|
3920
|
+
console.log(ident.error);
|
|
3921
|
+
process.exit(3);
|
|
3922
|
+
}
|
|
3451
3923
|
const r = forgetCore(fileRef, {
|
|
3452
|
-
selfAgent:
|
|
3924
|
+
selfAgent: ident.id,
|
|
3453
3925
|
unsafe: !!(opts && opts.unsafe),
|
|
3454
3926
|
});
|
|
3455
3927
|
console.log(r.text);
|
|
@@ -3564,6 +4036,16 @@ function importCore(root, src) {
|
|
|
3564
4036
|
try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
3565
4037
|
catch (e) { return { error: true, text: 'JSON 解析失败: ' + (e && e.message ? e.message : String(e)) }; }
|
|
3566
4038
|
if (!data.memories || !Array.isArray(data.memories)) return { error: true, text: 'JSON 格式不正确(缺少 memories 数组)' };
|
|
4039
|
+
for (const item of data.memories) {
|
|
4040
|
+
const meta = item.meta || {};
|
|
4041
|
+
const t = (meta.type || 'FACT').toUpperCase();
|
|
4042
|
+
if (!TYPE_DIRS[t]) continue;
|
|
4043
|
+
const owner = String(meta.owner || '');
|
|
4044
|
+
const scope = meta.scope || defaultScope(t);
|
|
4045
|
+
if (scope === 'private' && !isSafeAgentId(owner)) {
|
|
4046
|
+
return { error: true, text: '拒绝: 导入的私密条目 owner 非法(只允许单段名称,禁止 /、\\、.. 和控制字符)。' };
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
3567
4049
|
ensureInit(root);
|
|
3568
4050
|
const encrypted = isEncrypted(root);
|
|
3569
4051
|
let m = 0, skippedPriv = 0;
|
|
@@ -3627,6 +4109,7 @@ function cmdTokenNew(agentId, opts) {
|
|
|
3627
4109
|
const root = userRoot();
|
|
3628
4110
|
ensureInit(root);
|
|
3629
4111
|
if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
|
|
4112
|
+
if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
|
|
3630
4113
|
const data = loadTokens(root);
|
|
3631
4114
|
data.version = 1;
|
|
3632
4115
|
data.tokens = data.tokens || {};
|
|
@@ -3652,6 +4135,7 @@ function cmdTokenList() {
|
|
|
3652
4135
|
function cmdTokenRevoke(agentId) {
|
|
3653
4136
|
const root = userRoot();
|
|
3654
4137
|
if (!agentId) { console.error('请指定 --agent <id>'); process.exit(2); }
|
|
4138
|
+
if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
|
|
3655
4139
|
const data = loadTokens(root);
|
|
3656
4140
|
data.tokens = data.tokens || {};
|
|
3657
4141
|
if (data.tokens[agentId]) { delete data.tokens[agentId]; saveTokens(root, data); console.log('已吊销: ' + agentId); }
|
|
@@ -3758,6 +4242,7 @@ function cmdIam(agentId, opts) {
|
|
|
3758
4242
|
const root = userRoot();
|
|
3759
4243
|
ensureInit(root);
|
|
3760
4244
|
if (!agentId) { console.error('请指定 <id>:yotta-memory iam <id> [--name <显示名>] [--user <用户名>] [--relationship <关系>] [--force]'); process.exit(2); }
|
|
4245
|
+
if (!isSafeAgentId(agentId)) { console.error('非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。'); process.exit(2); }
|
|
3761
4246
|
const conflict = identityConflict(root, agentId);
|
|
3762
4247
|
if (conflict && !opts.force) {
|
|
3763
4248
|
console.error("错误: ID '" + agentId + "' " + conflict + '。每个 AI 智能体的 ID 必须唯一。请换一个唯一 ID,或确认是同一智能体后加 --force 覆盖。');
|
|
@@ -3778,13 +4263,18 @@ function cmdIam(agentId, opts) {
|
|
|
3778
4263
|
console.log('已写入自我档案: ' + file);
|
|
3779
4264
|
console.log('本机免 token:以后用 whoami 确认身份;远端接入需 token new --agent ' + agentId);
|
|
3780
4265
|
}
|
|
3781
|
-
function cmdWhoami() {
|
|
4266
|
+
function cmdWhoami(opts) {
|
|
3782
4267
|
const root = userRoot();
|
|
3783
|
-
const
|
|
4268
|
+
const ident = resolveIdentity(opts);
|
|
4269
|
+
if (ident.error) {
|
|
4270
|
+
console.log(ident.error);
|
|
4271
|
+
process.exit(2);
|
|
4272
|
+
}
|
|
4273
|
+
const id = ident.id;
|
|
3784
4274
|
if (!id) {
|
|
3785
|
-
console.log('
|
|
3786
|
-
console.log('
|
|
3787
|
-
console.log('远端:X-Agent-Id
|
|
4275
|
+
console.log('当前未声明可信智能体身份(未传 --agent,且没有受信任的 MCP 环境身份)。');
|
|
4276
|
+
console.log('CLI:请显式传 --agent <唯一ID>。MCP:在客户端配置中同时设置 YOTTA_AGENT_ID=<唯一ID> 与 YOTTA_MEMORY_TRUST_ENV_AGENT=1。');
|
|
4277
|
+
console.log('远端:token + X-Agent-Id + X-Agent-Key(token new --agent <id>;agent_key 由 view 授权生成)。');
|
|
3788
4278
|
return;
|
|
3789
4279
|
}
|
|
3790
4280
|
const agents = loadAgents(root).agents || {};
|
|
@@ -3834,8 +4324,19 @@ function profileGroups(root, owner) {
|
|
|
3834
4324
|
function profileCore(opts) {
|
|
3835
4325
|
opts = opts || {};
|
|
3836
4326
|
const root = userRoot();
|
|
3837
|
-
const
|
|
4327
|
+
const ident = resolveIdentity(opts);
|
|
4328
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
4329
|
+
if (!ident.id) {
|
|
4330
|
+
return {
|
|
4331
|
+
error: true,
|
|
4332
|
+
exitCode: 3,
|
|
4333
|
+
text: '生成私密画像必须先声明身份:请传 --agent <id>;MCP 场景需设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。',
|
|
4334
|
+
};
|
|
4335
|
+
}
|
|
4336
|
+
const selfAgent = ident.id;
|
|
3838
4337
|
const owner = opts.owner || selfAgent;
|
|
4338
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
4339
|
+
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
3839
4340
|
if (!owner) return { error: true, exitCode: 2, text: '请先声明身份(YOTTA_AGENT_ID / AGENT_ID)或传 --owner <id>,再生成画像。' };
|
|
3840
4341
|
if (selfAgent && owner !== selfAgent && owner !== 'user' && !opts.unsafe && !hasGrant(selfAgent, owner)) {
|
|
3841
4342
|
return { error: true, exitCode: 3, text: '拒绝: 不能生成其它智能体 ' + owner + ' 的画像(private/' + owner + '/ 为私密区)。如需读取请 --owner user 或 --unsafe(用户显式授权)。' };
|
|
@@ -3888,8 +4389,19 @@ function contextCore(opts) {
|
|
|
3888
4389
|
if (!roots.length) return { error: false, exitCode: 0, text: '记忆库不存在,请先运行: yotta-memory init' };
|
|
3889
4390
|
const root = userRoot();
|
|
3890
4391
|
const limit = opts.limit || 10;
|
|
3891
|
-
const
|
|
4392
|
+
const ident = resolveIdentity(opts);
|
|
4393
|
+
if (ident.error) return { error: true, exitCode: 3, text: ident.error };
|
|
4394
|
+
if (!ident.id) {
|
|
4395
|
+
return {
|
|
4396
|
+
error: true,
|
|
4397
|
+
exitCode: 3,
|
|
4398
|
+
text: '开工上下文包含私密画像 / 边界 / 承诺,必须先声明身份:请传 --agent <id>;MCP 场景需设置 YOTTA_AGENT_ID=<id> + YOTTA_MEMORY_TRUST_ENV_AGENT=1。',
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
const selfAgent = ident.id;
|
|
3892
4402
|
const owner = opts.owner || selfAgent;
|
|
4403
|
+
const keyError = validatePrivateIdentity(root, ident, owner);
|
|
4404
|
+
if (keyError) return { error: true, exitCode: 3, text: keyError };
|
|
3893
4405
|
const unsafe = !!opts.unsafe;
|
|
3894
4406
|
const budget = opts.budget ? parseInt(opts.budget, 10) : 0;
|
|
3895
4407
|
const focus = opts.focus ? String(opts.focus) : '';
|
|
@@ -4059,6 +4571,10 @@ function contextCore(opts) {
|
|
|
4059
4571
|
function cmdContext(opts) {
|
|
4060
4572
|
const r = contextCore(opts);
|
|
4061
4573
|
console.log(r.text);
|
|
4574
|
+
if (r.error) {
|
|
4575
|
+
const reminder = migrationReminderText(userRoot());
|
|
4576
|
+
if (reminder) console.log('\n' + reminder);
|
|
4577
|
+
}
|
|
4062
4578
|
if (r.exitCode) process.exit(r.exitCode);
|
|
4063
4579
|
}
|
|
4064
4580
|
|
|
@@ -4125,6 +4641,7 @@ function mcpTools() {
|
|
|
4125
4641
|
}
|
|
4126
4642
|
function callTool(name, args, ctx) {
|
|
4127
4643
|
const agent = ctx.agent || '';
|
|
4644
|
+
if (agent && !isSafeAgentId(agent)) return { text: '非法 agent ID:只允许单段名称,禁止 /、\\、.. 和控制字符。', error: true };
|
|
4128
4645
|
try {
|
|
4129
4646
|
if (name === 'remember') {
|
|
4130
4647
|
const ownerArg = args.owner ? String(args.owner) : '';
|
|
@@ -4251,6 +4768,7 @@ function unsupportedVersion(id, pv) {
|
|
|
4251
4768
|
return { jsonrpc: '2.0', id: id, error: { code: -32022, message: 'Unsupported protocol version', data: { supported: [MCP_PROTOCOL_MODERN], requested: pv } } };
|
|
4252
4769
|
}
|
|
4253
4770
|
function handleMessage(msg, ctx) {
|
|
4771
|
+
setRuntimeAgent(ctx && ctx.agent, ctx && ctx.agentKey);
|
|
4254
4772
|
if (!msg || msg.jsonrpc !== '2.0') return { jsonrpc: '2.0', id: msg && msg.id, error: { code: -32600, message: 'invalid request' } };
|
|
4255
4773
|
const id = msg.id;
|
|
4256
4774
|
if (id === undefined || id === null) return null;
|
|
@@ -4297,14 +4815,16 @@ function cmdServe(opts) {
|
|
|
4297
4815
|
const root = userRoot();
|
|
4298
4816
|
ensureInit(root);
|
|
4299
4817
|
function authorize(req) {
|
|
4300
|
-
|
|
4818
|
+
const agentId = String(req.headers['x-agent-id'] || '');
|
|
4819
|
+
const agentKey = String(req.headers['x-agent-key'] || '').trim();
|
|
4820
|
+
if (noAuth) return { agent: agentId, agentKey: agentKey };
|
|
4301
4821
|
const auth = req.headers['authorization'] || '';
|
|
4302
4822
|
const m = /^Bearer\s+(.+)$/i.exec(auth);
|
|
4303
4823
|
if (!m) return null;
|
|
4304
4824
|
const token = m[1].trim();
|
|
4305
|
-
const agent =
|
|
4825
|
+
const agent = agentId;
|
|
4306
4826
|
const tokenMap = (loadTokens(root).tokens) || {};
|
|
4307
|
-
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent };
|
|
4827
|
+
if (tokenMap[agent] && tokenMap[agent].token === token) return { agent: agent, agentKey: agentKey };
|
|
4308
4828
|
return null;
|
|
4309
4829
|
}
|
|
4310
4830
|
function originAllowed(req) {
|
|
@@ -4330,7 +4850,7 @@ function cmdServe(opts) {
|
|
|
4330
4850
|
}
|
|
4331
4851
|
const auth = authorize(req);
|
|
4332
4852
|
if (!auth) {
|
|
4333
|
-
sendJson(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id' } });
|
|
4853
|
+
sendJson(res, 401, { jsonrpc: '2.0', error: { code: -32001, message: 'unauthorized: 需要有效 Bearer token 与 X-Agent-Id;加密私密操作还需 X-Agent-Key' } });
|
|
4334
4854
|
return;
|
|
4335
4855
|
}
|
|
4336
4856
|
if (req.method === 'GET') {
|
|
@@ -4375,7 +4895,7 @@ function cmdServe(opts) {
|
|
|
4375
4895
|
console.log('记忆库: ' + root);
|
|
4376
4896
|
console.log('MCP 协议: 2026-07-28(modern,server/discover)/ 2025-11-25(legacy 握手兼容)');
|
|
4377
4897
|
if (noAuth) console.log('鉴权: 已关闭(--no-auth,仅限可信内网)');
|
|
4378
|
-
else console.log('鉴权: Bearer token + X-Agent-Id(
|
|
4898
|
+
else console.log('鉴权: Bearer token + X-Agent-Id + X-Agent-Key(token 由 token new 生成,agent_key 由 view 授权生成)');
|
|
4379
4899
|
console.log('按 Ctrl+C 停止');
|
|
4380
4900
|
startBackupFallback();
|
|
4381
4901
|
});
|
|
@@ -4384,7 +4904,7 @@ function cmdServe(opts) {
|
|
|
4384
4904
|
function cmdServeStdio() {
|
|
4385
4905
|
const root = userRoot();
|
|
4386
4906
|
ensureInit(root);
|
|
4387
|
-
const ctx = { agent: currentAgent() };
|
|
4907
|
+
const ctx = { agent: currentAgent(), agentKey: trustedEnvAgentKey() };
|
|
4388
4908
|
let buf = '';
|
|
4389
4909
|
process.stdin.setEncoding('utf8');
|
|
4390
4910
|
process.stdin.on('data', function (chunk) {
|
|
@@ -5110,7 +5630,7 @@ function consolidateCore(opts) {
|
|
|
5110
5630
|
const root = userRoot();
|
|
5111
5631
|
if (!fs.existsSync(root)) return { error: false, text: '记忆库不存在。' };
|
|
5112
5632
|
const apply = !!opts.apply;
|
|
5113
|
-
const selfAgent = opts.
|
|
5633
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
5114
5634
|
const unsafe = !!opts.unsafe;
|
|
5115
5635
|
const cfg = loadConfig();
|
|
5116
5636
|
const minAge = (opts.minAge !== undefined && opts.minAge !== null) ? opts.minAge : (parseInt(cfg.consolidate_min_age || '180', 10) || 180);
|
|
@@ -5275,7 +5795,7 @@ function autoMergePairCore(root, keepE, dropE, batch, selfAgent, unsafe) {
|
|
|
5275
5795
|
|
|
5276
5796
|
function appendDedupBlock(lines, root, opts) {
|
|
5277
5797
|
const apply = !!opts.apply;
|
|
5278
|
-
const selfAgent = opts.
|
|
5798
|
+
const selfAgent = resolveIdentity(opts).id;
|
|
5279
5799
|
const unsafe = !!opts.unsafe;
|
|
5280
5800
|
lines.push('');
|
|
5281
5801
|
lines.push('### 重复候选与置信度(--dedup' + (apply ? ' --apply 自动合并' : '') + ')');
|
|
@@ -5366,7 +5886,7 @@ function usage() {
|
|
|
5366
5886
|
['migrate', '把明文私密区迁移为密文(需主口令;迁移后打印恢复钥匙)'],
|
|
5367
5887
|
['view', '启动用户查看平台(--port/--host;口令解锁浏览/授权/吊销 AI)'],
|
|
5368
5888
|
['reset-password', '重设主口令(忘口令用恢复钥匙)'],
|
|
5369
|
-
['key', '管理
|
|
5889
|
+
['key', '管理 agent_key binding(list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>;bind/rotate 需主口令或恢复钥匙)'],
|
|
5370
5890
|
['config', '查看/设置配置(get;set memory_home <目录> / backup_dir <目录> / embedding_cmd <命令> / embedding_timeout <毫秒> / maintain_* 阈值与半衰 / consolidate_* 参数)']
|
|
5371
5891
|
]],
|
|
5372
5892
|
['平台与服务', [
|
|
@@ -5385,9 +5905,9 @@ function usage() {
|
|
|
5385
5905
|
lines.push('');
|
|
5386
5906
|
}
|
|
5387
5907
|
lines.push('类型: FACT(公共共享) / PREF(偏好) / BOUND(边界) / COMMIT(承诺)');
|
|
5388
|
-
lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; YOTTA_AGENT_ID
|
|
5908
|
+
lines.push('环境变量: YOTTA_MEMORY_HOME 临时覆盖用户级位置; MCP 身份必须同时设置 YOTTA_AGENT_ID + YOTTA_MEMORY_AGENT_KEY + YOTTA_MEMORY_TRUST_ENV_AGENT=1;CLI 用 --agent + --agent-key/--agent-key-file');
|
|
5389
5909
|
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>');
|
|
5910
|
+
lines.push('远端接入: MCP url http://<主机IP>:8787/mcp;请求头 Authorization: Bearer <token> + X-Agent-Id: <id> + X-Agent-Key: <agent_key>');
|
|
5391
5911
|
console.log(lines.join('\n'));
|
|
5392
5912
|
}
|
|
5393
5913
|
async function main() {
|
|
@@ -5395,7 +5915,7 @@ async function main() {
|
|
|
5395
5915
|
if (!args.length) { usage(); return; }
|
|
5396
5916
|
const opts = {};
|
|
5397
5917
|
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']);
|
|
5918
|
+
const valueOpts = new Set(['--type', '--limit', '--days', '--out', '--owner', '--agent', '--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']);
|
|
5399
5919
|
for (let i = 0; i < args.length; i++) {
|
|
5400
5920
|
const a = args[i];
|
|
5401
5921
|
if (a === '--version' || a === '-v') { console.log(VERSION); return; }
|
|
@@ -5435,6 +5955,8 @@ async function main() {
|
|
|
5435
5955
|
else if (a === '--out') opts.out = v;
|
|
5436
5956
|
else if (a === '--owner') opts.owner = v;
|
|
5437
5957
|
else if (a === '--agent') opts.agent = v;
|
|
5958
|
+
else if (a === '--agent-key') opts.agentKey = v;
|
|
5959
|
+
else if (a === '--agent-key-file') opts.agentKeyFile = v;
|
|
5438
5960
|
else if (a === '--scope') opts.scope = v;
|
|
5439
5961
|
else if (a === '--host') opts.host = v;
|
|
5440
5962
|
else if (a === '--port') opts.port = parseInt(v, 10) || 8787;
|
|
@@ -5472,9 +5994,11 @@ async function main() {
|
|
|
5472
5994
|
}
|
|
5473
5995
|
const first = positional[0];
|
|
5474
5996
|
const rest = positional.slice(1);
|
|
5997
|
+
const runtimeIdentity = resolveIdentity(opts);
|
|
5998
|
+
setRuntimeAgent(runtimeIdentity.error ? '' : runtimeIdentity.id, runtimeIdentity.error ? '' : runtimeIdentity.agentKey);
|
|
5475
5999
|
switch (first) {
|
|
5476
6000
|
case 'init': await cmdInit(opts); break;
|
|
5477
|
-
case 'whoami': cmdWhoami(); break;
|
|
6001
|
+
case 'whoami': cmdWhoami(opts); break;
|
|
5478
6002
|
case 'iam': cmdIam(rest[0], opts); break;
|
|
5479
6003
|
case 'config': {
|
|
5480
6004
|
const sub = rest[0];
|
|
@@ -5528,9 +6052,12 @@ async function main() {
|
|
|
5528
6052
|
case 'key': {
|
|
5529
6053
|
const sub = rest[0];
|
|
5530
6054
|
if (sub === 'list') cmdKeyList();
|
|
6055
|
+
else if (sub === 'bind' || sub === 'rotate') await cmdKeyBind(rest[1], opts);
|
|
5531
6056
|
else if (sub === 'authorize') await cmdKeyAuthorize(rest[1], opts);
|
|
6057
|
+
else if (sub === 'claim') cmdKeyClaim(rest[1], opts);
|
|
6058
|
+
else if (sub === 'status') cmdKeyStatus(rest[1], opts);
|
|
5532
6059
|
else if (sub === 'revoke') cmdKeyRevoke(rest[1]);
|
|
5533
|
-
else { console.error('key 子命令: list /
|
|
6060
|
+
else { console.error('key 子命令: list / bind <id> / rotate <id> / claim <id> --to <AI_HOME> / status <id> / revoke <id>'); process.exit(2); }
|
|
5534
6061
|
break;
|
|
5535
6062
|
}
|
|
5536
6063
|
case 'serve': cmdServe(opts); break;
|
|
@@ -5643,10 +6170,11 @@ module.exports = {
|
|
|
5643
6170
|
unwrapOwnerKey: unwrapOwnerKey,
|
|
5644
6171
|
unwrapOwnerKeyRecovery: unwrapOwnerKeyRecovery,
|
|
5645
6172
|
unwrapRecoveryEnc: unwrapRecoveryEnc,
|
|
5646
|
-
writeOwnerKeyCache: writeOwnerKeyCache,
|
|
5647
6173
|
revokeOwnerKeyCache: revokeOwnerKeyCache,
|
|
5648
|
-
loadOwnerKeyCache: loadOwnerKeyCache,
|
|
5649
6174
|
getOwnerKeyFor: getOwnerKeyFor,
|
|
6175
|
+
keyListCore: keyListCore,
|
|
6176
|
+
migrationRequiredInfo: migrationRequiredInfo,
|
|
6177
|
+
migrationReminderText: migrationReminderText,
|
|
5650
6178
|
encryptMemoryText: encryptMemoryText,
|
|
5651
6179
|
decryptMemoryText: decryptMemoryText,
|
|
5652
6180
|
readMemoryText: readMemoryText,
|
|
@@ -5654,6 +6182,7 @@ module.exports = {
|
|
|
5654
6182
|
loadOwnerIndex: loadOwnerIndex,
|
|
5655
6183
|
saveOwnerIndex: saveOwnerIndex,
|
|
5656
6184
|
viewEntriesCore: viewEntriesCore,
|
|
6185
|
+
viewHtml: viewHtml,
|
|
5657
6186
|
ensureIndex: ensureIndex,
|
|
5658
6187
|
recallPrefilter: recallPrefilter,
|
|
5659
6188
|
tokenize: tokenize,
|
|
@@ -5674,8 +6203,17 @@ module.exports = {
|
|
|
5674
6203
|
writeProfileText: writeProfileText,
|
|
5675
6204
|
migrateCore: migrateCore,
|
|
5676
6205
|
resetPasswordCore: resetPasswordCore,
|
|
5677
|
-
|
|
6206
|
+
keyBindCore: keyBindCore,
|
|
5678
6207
|
keyRevokeCore: keyRevokeCore,
|
|
6208
|
+
keyClaimCore: keyClaimCore,
|
|
6209
|
+
keyStatusCore: keyStatusCore,
|
|
6210
|
+
writeAgentBinding: writeAgentBinding,
|
|
6211
|
+
unwrapAgentBinding: unwrapAgentBinding,
|
|
6212
|
+
writePendingAgentKey: writePendingAgentKey,
|
|
6213
|
+
removePendingAgentKey: removePendingAgentKey,
|
|
6214
|
+
readPendingAgentKey: readPendingAgentKey,
|
|
6215
|
+
agentKeyFilePath: agentKeyFilePath,
|
|
6216
|
+
setRuntimeAgent: setRuntimeAgent,
|
|
5679
6217
|
promptPassword: promptPassword,
|
|
5680
6218
|
initCore: initCore,
|
|
5681
6219
|
backupCreateCore: backupCreateCore,
|