agentlas 0.9.4 → 0.9.6
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 +16 -0
- package/engine/agentlas-capabilities.cjs +94 -2
- package/engine/agentlas-evolution.cjs +299 -0
- package/engine/agentlas-experience-exchange.cjs +66 -11
- package/engine/agentlas-i18n.cjs +4 -0
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-memory-governance.cjs +1 -11
- package/engine/agentlas-memory-import.cjs +310 -0
- package/engine/agentlas-repl.cjs +31 -10
- package/engine/agentlas-secret-patterns.cjs +71 -0
- package/engine/agentlas.cjs +191 -17
- package/package.json +1 -1
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// `agentlas memory import <path> [--apply]` — Phase 1b.
|
|
4
|
+
//
|
|
5
|
+
// Promote legacy markdown memory into the shared agentlas.sqlite the desktop
|
|
6
|
+
// uses (same userData/agentlas.sqlite). Maps each substantive markdown section
|
|
7
|
+
// to a durable memory_entries row owned by the right layer of a team (member
|
|
8
|
+
// cell / orchestrator / shared team_memory) or a single agent. Mirrors the app's
|
|
9
|
+
// electron/memory/import.ts mapping so app and terminal agree. Dry-run by
|
|
10
|
+
// default (prints the preview table); --apply writes. Idempotent via a stable
|
|
11
|
+
// per-section source-hash sentinel embedded in evidence. Secrets are dropped.
|
|
12
|
+
|
|
13
|
+
const fs = require("node:fs");
|
|
14
|
+
const path = require("node:path");
|
|
15
|
+
const { createHash, randomUUID } = require("node:crypto");
|
|
16
|
+
|
|
17
|
+
const SOURCE_TOKEN_PREFIX = "mem-import:v1";
|
|
18
|
+
const MAX_FILES = 400;
|
|
19
|
+
const MAX_CONTENT = 4000;
|
|
20
|
+
|
|
21
|
+
// ── Section extraction (parity with electron/memory/import.ts) ───────────────
|
|
22
|
+
const TEMPLATE_LINE =
|
|
23
|
+
/^\s*[-*]?\s*(Add\b|Fill in\b|Example:|Record\b|Link\b|Prefer\b|Note\b|Which\b|Date:\s*$|Topic:\s*$|Decision:\s*$|Why:\s*$|Risk accepted:\s*$)/i;
|
|
24
|
+
const META_HEADING =
|
|
25
|
+
/^(How To Use|사용 규칙|사용법|구조|형식|사전 참조|누가 업데이트|Entries|Read First|Memory Rules|Recent Activity|Recently Touched|CROSS-REFERENCES|LEARNINGS LOG|ANTIPATTERNS|GOTCHAS|Repeated Failures|성공 패턴|발견 사항|안티패턴|에이전트 구성)/i;
|
|
26
|
+
const SHARED_HINT =
|
|
27
|
+
/(team[-_ ]?memory|team_memory|glossary|handoff|scope[-_ ]?ownership|common[-_ ]?safety|safety|tone|language|dossier|operating[-_ ]?architecture|memory[-_ ]?architecture|용어|공통|안전|인계|톤)/i;
|
|
28
|
+
const RISK_HINT = /(security|attack|vuln|bug|gotcha|incident|보안|취약|버그|사고)/i;
|
|
29
|
+
const ALWAYS_KEEP_HINT = /(team[-_ ]?memory|glossary|dossier|handoff|safety|scope[-_ ]?ownership|tone)/i;
|
|
30
|
+
|
|
31
|
+
// Minimal secret guard (a subset of the shared secret-patterns chokepoint) so a
|
|
32
|
+
// stray key in legacy notes never becomes a durable memory row.
|
|
33
|
+
const { looksSecret } = require("./agentlas-secret-patterns.cjs");
|
|
34
|
+
|
|
35
|
+
function substantiveBody(body) {
|
|
36
|
+
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
37
|
+
const real = lines.filter((l) => !TEMPLATE_LINE.test(l) && l.replace(/^#+\s*/, "").length >= 12);
|
|
38
|
+
return real.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function kindForHeading(heading, fallback) {
|
|
42
|
+
const m = /\[([A-Z_]+)\]/.exec(heading);
|
|
43
|
+
const tag = m ? m[1] : "";
|
|
44
|
+
if (["SUCCESS", "DISCOVERY"].includes(tag)) return "procedure";
|
|
45
|
+
if (["ANTIPATTERN", "GOTCHA", "SECURITY", "CONFIRMED", "REGRESSION", "FALSE_POSITIVE", "BLOCKED_BY_GUARD", "FAILURE"].includes(tag)) {
|
|
46
|
+
return "risk";
|
|
47
|
+
}
|
|
48
|
+
return fallback;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function keepSection(heading, body, alwaysKeep) {
|
|
52
|
+
if (META_HEADING.test(heading.replace(/\[[A-Z_]+\]\s*/, "").trim())) return false;
|
|
53
|
+
const hasDate = /\(20\d\d-\d\d-\d\d\)|Date:\s*20\d\d-\d\d-\d\d/.test(heading + "\n" + body);
|
|
54
|
+
const hasTag = /\[[A-Z_]+\]/.test(heading);
|
|
55
|
+
const real = substantiveBody(body);
|
|
56
|
+
if (alwaysKeep) return real.length >= 60;
|
|
57
|
+
if (hasDate || hasTag) return real.length >= 40;
|
|
58
|
+
return real.length >= 160;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function splitSections(md) {
|
|
62
|
+
const lines = md.split("\n");
|
|
63
|
+
const sections = [];
|
|
64
|
+
let cur = null;
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (/^#{2,3}\s+\S/.test(line)) {
|
|
67
|
+
if (cur) sections.push(cur);
|
|
68
|
+
cur = { heading: line.replace(/^#{2,3}\s+/, "").trim(), body: "" };
|
|
69
|
+
} else if (cur) {
|
|
70
|
+
cur.body += line + "\n";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (cur) sections.push(cur);
|
|
74
|
+
return sections;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function splitDatedBullets(md) {
|
|
78
|
+
const idx = md.indexOf("- Date:");
|
|
79
|
+
if (idx === -1) return [];
|
|
80
|
+
return md
|
|
81
|
+
.slice(idx)
|
|
82
|
+
.split(/\n(?=- Date:)/)
|
|
83
|
+
.map((p) => p.trim())
|
|
84
|
+
.filter((p) => /Date:\s*20\d\d-\d\d-\d\d/.test(p))
|
|
85
|
+
.map((p) => {
|
|
86
|
+
const topic = /Topic:\s*(.+)/.exec(p);
|
|
87
|
+
const date = /Date:\s*(20\d\d-\d\d-\d\d)/.exec(p);
|
|
88
|
+
return { heading: `Decision ${date ? date[1] : ""}: ${topic ? topic[1].trim() : ""}`.trim(), body: p };
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeToken(value) {
|
|
93
|
+
return String(value || "").toLowerCase().replace(/[^a-z0-9가-힣]+/g, " ").trim();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveTarget(db, agentId) {
|
|
97
|
+
let firms = [];
|
|
98
|
+
try {
|
|
99
|
+
firms = db.prepare("SELECT id, ceo_agent_id, org_chart_json FROM firms").all();
|
|
100
|
+
} catch {
|
|
101
|
+
firms = [];
|
|
102
|
+
}
|
|
103
|
+
const firm =
|
|
104
|
+
firms.find((f) => f.id === agentId) ||
|
|
105
|
+
firms.find((f) => f.ceo_agent_id === agentId) ||
|
|
106
|
+
null;
|
|
107
|
+
if (!firm) return { agentId, kind: "agent", members: [] };
|
|
108
|
+
let chart = [];
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(firm.org_chart_json);
|
|
111
|
+
if (Array.isArray(parsed)) chart = parsed;
|
|
112
|
+
} catch {
|
|
113
|
+
chart = [];
|
|
114
|
+
}
|
|
115
|
+
const members = chart
|
|
116
|
+
.filter((node) => node && node.agentId && node.agentId !== firm.ceo_agent_id)
|
|
117
|
+
.map((node) => ({ agentId: node.agentId, role: node.role || node.agentSlug, slug: node.agentSlug }));
|
|
118
|
+
return { agentId: firm.ceo_agent_id, kind: "team", members };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function matchMember(fileTokens, target) {
|
|
122
|
+
if (!target.members.length) return null;
|
|
123
|
+
let best = null;
|
|
124
|
+
for (const member of target.members) {
|
|
125
|
+
const roleTokens = normalizeToken(member.role).split(" ").filter((t) => t.length >= 3);
|
|
126
|
+
const slugTokens = normalizeToken(member.slug).split(" ").filter((t) => t.length >= 3);
|
|
127
|
+
const tokens = [...new Set([...roleTokens, ...slugTokens])];
|
|
128
|
+
let score = 0;
|
|
129
|
+
for (const token of tokens) if (fileTokens.includes(token)) score += token.length;
|
|
130
|
+
if (score > 0 && (!best || score > best.score)) best = { agentId: member.agentId, role: member.role, score };
|
|
131
|
+
}
|
|
132
|
+
return best ? { agentId: best.agentId, role: best.role } : null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function decideOwner(relFile, target) {
|
|
136
|
+
const lower = relFile.toLowerCase();
|
|
137
|
+
const fileTokens = normalizeToken(relFile);
|
|
138
|
+
const alwaysKeep = ALWAYS_KEEP_HINT.test(lower);
|
|
139
|
+
if (SHARED_HINT.test(lower)) {
|
|
140
|
+
return {
|
|
141
|
+
scope: "team_memory",
|
|
142
|
+
ownerAgentId: null,
|
|
143
|
+
ownerLabel: "team_memory",
|
|
144
|
+
fallbackKind: /glossary|dossier|용어/i.test(lower) ? "fact" : "procedure",
|
|
145
|
+
alwaysKeep,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (target.kind === "team") {
|
|
149
|
+
const member = matchMember(fileTokens, target);
|
|
150
|
+
if (member) {
|
|
151
|
+
return { scope: "agent_repo", ownerAgentId: member.agentId, ownerLabel: member.role, fallbackKind: RISK_HINT.test(lower) ? "risk" : "procedure", alwaysKeep };
|
|
152
|
+
}
|
|
153
|
+
return { scope: "agent_repo", ownerAgentId: target.agentId, ownerLabel: "orchestrator", fallbackKind: RISK_HINT.test(lower) ? "risk" : "decision", alwaysKeep };
|
|
154
|
+
}
|
|
155
|
+
return { scope: "agent_repo", ownerAgentId: target.agentId, ownerLabel: "agent", fallbackKind: RISK_HINT.test(lower) ? "risk" : "procedure", alwaysKeep };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function collectMarkdown(root) {
|
|
159
|
+
const stat = fs.statSync(root);
|
|
160
|
+
if (stat.isFile()) {
|
|
161
|
+
return /\.(md|markdown|mdx|txt)$/i.test(root) ? [{ abs: root, rel: path.basename(root) }] : [];
|
|
162
|
+
}
|
|
163
|
+
const out = [];
|
|
164
|
+
const walk = (dir) => {
|
|
165
|
+
if (out.length >= MAX_FILES) return;
|
|
166
|
+
let entries;
|
|
167
|
+
try {
|
|
168
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
if (out.length >= MAX_FILES) return;
|
|
174
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
175
|
+
const abs = path.join(dir, entry.name);
|
|
176
|
+
if (entry.isDirectory()) walk(abs);
|
|
177
|
+
else if (entry.isFile() && /\.(md|markdown|mdx|txt)$/i.test(entry.name)) out.push({ abs, rel: path.relative(root, abs) });
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
walk(root);
|
|
181
|
+
return out.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function stableToken(relFile, heading) {
|
|
185
|
+
const hash = createHash("sha256").update(`${SOURCE_TOKEN_PREFIX}|${relFile}|${heading}`).digest("hex").slice(0, 16);
|
|
186
|
+
return `${SOURCE_TOKEN_PREFIX}:${hash}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildEntries(sourcePath, target) {
|
|
190
|
+
const built = [];
|
|
191
|
+
for (const { abs, rel } of collectMarkdown(sourcePath)) {
|
|
192
|
+
let md;
|
|
193
|
+
try {
|
|
194
|
+
md = fs.readFileSync(abs, "utf8");
|
|
195
|
+
} catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const owner = decideOwner(rel, target);
|
|
199
|
+
const sections = /decisions\.md$/i.test(rel) ? splitDatedBullets(md) : splitSections(md);
|
|
200
|
+
for (const sec of sections) {
|
|
201
|
+
if (!keepSection(sec.heading, sec.body, owner.alwaysKeep)) continue;
|
|
202
|
+
const bodyText = sec.body.replace(/\n{3,}/g, "\n\n").trim();
|
|
203
|
+
const content = `${sec.heading}\n${bodyText}`.trim().slice(0, MAX_CONTENT);
|
|
204
|
+
if (content.length < 40) continue;
|
|
205
|
+
built.push({
|
|
206
|
+
token: stableToken(rel, sec.heading),
|
|
207
|
+
relFile: rel,
|
|
208
|
+
heading: sec.heading,
|
|
209
|
+
content,
|
|
210
|
+
scope: owner.scope,
|
|
211
|
+
kind: kindForHeading(sec.heading, owner.fallbackKind),
|
|
212
|
+
ownerAgentId: owner.ownerAgentId,
|
|
213
|
+
ownerLabel: owner.ownerLabel,
|
|
214
|
+
redacted: looksSecret(content),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return built;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function existsByToken(db, token) {
|
|
222
|
+
try {
|
|
223
|
+
return Boolean(db.prepare("SELECT 1 FROM memory_entries WHERE evidence_json LIKE ? LIMIT 1").get(`%${token}%`));
|
|
224
|
+
} catch {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* cmdMemory — `agentlas memory <sub> ...`. Currently: import.
|
|
231
|
+
* @param {{db:any,args:string[],out:(s:string)=>void,fail:(s:string)=>void}} ctx
|
|
232
|
+
*/
|
|
233
|
+
function cmdMemory(ctx) {
|
|
234
|
+
const { db, out, fail } = ctx;
|
|
235
|
+
const args = Array.isArray(ctx.args) ? ctx.args : [];
|
|
236
|
+
const sub = args[0] || "help";
|
|
237
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
238
|
+
out("usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]");
|
|
239
|
+
out(" dry-run by default (prints the preview table); --apply writes to the shared DB.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (sub !== "import") return fail(`Unknown memory subcommand: ${sub} (import)`);
|
|
243
|
+
|
|
244
|
+
const apply = args.includes("--apply");
|
|
245
|
+
const agentIdx = args.indexOf("--agent");
|
|
246
|
+
const agentId = agentIdx >= 0 ? String(args[agentIdx + 1] || "").trim() : "";
|
|
247
|
+
const positional = args.slice(1).filter((a, i, arr) => a !== "--apply" && a !== "--agent" && arr[i - 1] !== "--agent");
|
|
248
|
+
const rawPath = positional[0];
|
|
249
|
+
if (!rawPath) return fail('usage: agentlas memory import <folder-or-file> --agent <agentId> [--apply]');
|
|
250
|
+
if (!agentId) return fail("memory import requires --agent <agentId> (the single agent or team to import into).");
|
|
251
|
+
const sourcePath = path.resolve(rawPath);
|
|
252
|
+
if (!fs.existsSync(sourcePath)) return fail(`Import source not found: ${sourcePath}`);
|
|
253
|
+
|
|
254
|
+
const target = resolveTarget(db, agentId);
|
|
255
|
+
const entries = buildEntries(sourcePath, target);
|
|
256
|
+
|
|
257
|
+
out(`== memory import (${apply ? "APPLY" : "DRY-RUN"}) ==`);
|
|
258
|
+
out(`source: ${sourcePath}`);
|
|
259
|
+
out(`target: ${agentId} (${target.kind})`);
|
|
260
|
+
out("");
|
|
261
|
+
out(pad("OWNER", 26) + pad("KIND", 10) + pad("STATUS", 8) + "SECTION");
|
|
262
|
+
const byOwner = {};
|
|
263
|
+
let newCount = 0;
|
|
264
|
+
let dupCount = 0;
|
|
265
|
+
let redacted = 0;
|
|
266
|
+
for (const e of entries) {
|
|
267
|
+
const status = e.redacted ? "skip" : existsByToken(db, e.token) ? "dup" : "new";
|
|
268
|
+
if (status === "new") {
|
|
269
|
+
newCount += 1;
|
|
270
|
+
byOwner[e.ownerLabel] = (byOwner[e.ownerLabel] || 0) + 1;
|
|
271
|
+
} else if (status === "dup") dupCount += 1;
|
|
272
|
+
else redacted += 1;
|
|
273
|
+
out(pad(e.ownerLabel, 26) + pad(e.kind, 10) + pad(status, 8) + e.heading.slice(0, 70));
|
|
274
|
+
}
|
|
275
|
+
out("");
|
|
276
|
+
out(`total ${entries.length} · new ${newCount} · duplicate ${dupCount} · redacted ${redacted}`);
|
|
277
|
+
|
|
278
|
+
if (!apply) {
|
|
279
|
+
out("");
|
|
280
|
+
out("dry-run — nothing written. Re-run with --apply to write to the shared agentlas.sqlite.");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const now = new Date().toISOString();
|
|
285
|
+
let imported = 0;
|
|
286
|
+
const insert = db.prepare(
|
|
287
|
+
"INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)",
|
|
288
|
+
);
|
|
289
|
+
const write = db.transaction((list) => {
|
|
290
|
+
for (const e of list) {
|
|
291
|
+
if (e.redacted || existsByToken(db, e.token)) continue;
|
|
292
|
+
const confidence = /\(20\d\d-\d\d-\d\d\)|Date:\s*20\d\d/.test(e.content) ? "high" : "medium";
|
|
293
|
+
const context = JSON.stringify({ userIntent: `Imported memory: ${e.heading}`.slice(0, 200), outcome: "imported-from-existing-memory" });
|
|
294
|
+
const evidence = JSON.stringify([e.token, `source:memory-import/${e.relFile}`]);
|
|
295
|
+
insert.run(randomUUID(), e.scope, e.kind, e.content, null, null, e.ownerAgentId, null, confidence, "internal", evidence, context, now);
|
|
296
|
+
imported += 1;
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
write(entries);
|
|
300
|
+
|
|
301
|
+
out("");
|
|
302
|
+
out(`imported ${imported} memory entries into the shared DB. (Embedding runs in the desktop app on next open.)`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function pad(value, width) {
|
|
306
|
+
const s = String(value == null ? "" : value);
|
|
307
|
+
return s.length >= width ? s.slice(0, width - 1) + " " : s + " ".repeat(width - s.length);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
module.exports = { cmdMemory, buildEntries, resolveTarget, decideOwner };
|
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -643,12 +643,26 @@ function startRepl(opts) {
|
|
|
643
643
|
state.modelPinned = false;
|
|
644
644
|
state.native = {};
|
|
645
645
|
}
|
|
646
|
+
// 이미지 능력 판정 warm-cache — 동기 호출자(applyRuntimeFor/배지)가 읽기 전에 비동기
|
|
647
|
+
// 경로에서 상주 판정 서비스를 먼저 데운다. 러너 없음/실패는 어휘 폴백(라벨은 routingNote가 찍음).
|
|
648
|
+
async function warmImageJudgment(agentRow) {
|
|
649
|
+
if (!agentRow || typeof caps.resolveNeedsImage !== "function") return;
|
|
650
|
+
try {
|
|
651
|
+
await caps.resolveNeedsImage(agentRow);
|
|
652
|
+
} catch {
|
|
653
|
+
/* lexical fallback */
|
|
654
|
+
}
|
|
655
|
+
}
|
|
646
656
|
// Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
|
|
647
657
|
function routingNote(subject) {
|
|
648
658
|
if (!subject || !caps.needsImage(subject.capAgent)) return;
|
|
649
659
|
const spec = caps.specOf(state.runtime);
|
|
650
660
|
if (caps.capsFor(spec).image) {
|
|
651
|
-
if (spec !== caps.specOf(baseRuntime))
|
|
661
|
+
if (spec !== caps.specOf(baseRuntime)) {
|
|
662
|
+
// 판정 주체 라벨 — 모델 판정인지 결정적 폴백인지 반드시 밝힌다(조용한 폴백 금지).
|
|
663
|
+
const judged = typeof caps.imageJudgmentSource === "function" && caps.imageJudgmentSource(subject.capAgent) === "llm";
|
|
664
|
+
ui.info(ui.t("routedImage", spec) + " — " + ui.t(judged ? "judge.source.llm" : "judge.source.fallback"));
|
|
665
|
+
}
|
|
652
666
|
} else {
|
|
653
667
|
ui.warn(ui.t("guard.imageWarn", caps.capsFor(spec).label || spec));
|
|
654
668
|
}
|
|
@@ -701,10 +715,11 @@ function startRepl(opts) {
|
|
|
701
715
|
state.routePreambleOnce = null;
|
|
702
716
|
applyRuntimeFor(state.subject);
|
|
703
717
|
}
|
|
704
|
-
function switchSubject(kind, query) {
|
|
718
|
+
async function switchSubject(kind, query) {
|
|
705
719
|
if (kind === "agent") {
|
|
706
720
|
const agent = H.resolveAgent(db, query);
|
|
707
721
|
if (!agent) return ui.error(ui.t("noAgent", query));
|
|
722
|
+
await warmImageJudgment(agent); // 런타임 자동 배정 전에 모델 판정을 데운다
|
|
708
723
|
setSubjectAgent(agent);
|
|
709
724
|
} else {
|
|
710
725
|
const firm = H.resolveFirm(db, query);
|
|
@@ -933,11 +948,11 @@ function startRepl(opts) {
|
|
|
933
948
|
}
|
|
934
949
|
case "agent":
|
|
935
950
|
if (!arg) return ui.warn(ui.t("agentUsage")), true;
|
|
936
|
-
switchSubject("agent", arg);
|
|
951
|
+
await switchSubject("agent", arg);
|
|
937
952
|
return true;
|
|
938
953
|
case "firm":
|
|
939
954
|
if (!arg) return ui.warn(ui.t("firmUsage")), true;
|
|
940
|
-
switchSubject("firm", arg);
|
|
955
|
+
await switchSubject("firm", arg);
|
|
941
956
|
return true;
|
|
942
957
|
case "runtime":
|
|
943
958
|
setRuntime(arg);
|
|
@@ -1364,7 +1379,8 @@ function startRepl(opts) {
|
|
|
1364
1379
|
}
|
|
1365
1380
|
|
|
1366
1381
|
// ── interactive picker (when no agent was given) ──
|
|
1367
|
-
function chooseAndStart(setter, row) {
|
|
1382
|
+
async function chooseAndStart(setter, row) {
|
|
1383
|
+
if (setter === setSubjectAgent) await warmImageJudgment(row); // firm은 팀 베토라 판정 대상 아님
|
|
1368
1384
|
setter(row);
|
|
1369
1385
|
ui.ok(ui.t("switched", state.subject.label));
|
|
1370
1386
|
routingNote(state.subject);
|
|
@@ -1402,12 +1418,14 @@ function startRepl(opts) {
|
|
|
1402
1418
|
if (a) return chooseAndStart(setSubjectAgent, a);
|
|
1403
1419
|
const f = H.resolveFirm(db, t);
|
|
1404
1420
|
if (f) return chooseAndStart(setSubjectFirm, f);
|
|
1405
|
-
if (H.autoRouteAgent) {
|
|
1406
|
-
|
|
1421
|
+
if (H.autoRouteAgent || H.resolveAutoRoute) {
|
|
1422
|
+
// 연결 모델이 라우트를 최종 판정한다(resolveAutoRoute) — 없으면 어휘 폴백.
|
|
1423
|
+
const choice = H.resolveAutoRoute ? await H.resolveAutoRoute(db, t, ui.lang) : H.autoRouteAgent(db, t, ui.lang);
|
|
1407
1424
|
if (choice) {
|
|
1408
1425
|
if (choice.direct) {
|
|
1409
1426
|
setSubjectDirect();
|
|
1410
1427
|
} else {
|
|
1428
|
+
await warmImageJudgment(choice.agent);
|
|
1411
1429
|
setSubjectAgent(choice.agent);
|
|
1412
1430
|
}
|
|
1413
1431
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
@@ -1442,6 +1460,7 @@ function startRepl(opts) {
|
|
|
1442
1460
|
if (/^\d+$/.test(t)) {
|
|
1443
1461
|
const n = parseInt(t, 10);
|
|
1444
1462
|
if (n >= 1 && n <= ags.length) {
|
|
1463
|
+
await warmImageJudgment(ags[n - 1]);
|
|
1445
1464
|
setSubjectAgent(ags[n - 1]);
|
|
1446
1465
|
ui.ok(ui.t("switched", state.subject.label));
|
|
1447
1466
|
routingNote(state.subject);
|
|
@@ -1450,13 +1469,14 @@ function startRepl(opts) {
|
|
|
1450
1469
|
}
|
|
1451
1470
|
if (single) {
|
|
1452
1471
|
const a = H.resolveAgent(db, t);
|
|
1453
|
-
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1472
|
+
if (a) { await warmImageJudgment(a); setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1454
1473
|
const f = H.resolveFirm(db, t);
|
|
1455
1474
|
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1456
1475
|
}
|
|
1457
1476
|
}
|
|
1458
|
-
if (H.autoRouteAgent) {
|
|
1459
|
-
|
|
1477
|
+
if (H.autoRouteAgent || H.resolveAutoRoute) {
|
|
1478
|
+
// 연결 모델이 라우트를 최종 판정한다(resolveAutoRoute) — 없으면 어휘 폴백.
|
|
1479
|
+
const choice = H.resolveAutoRoute ? await H.resolveAutoRoute(db, t, ui.lang) : H.autoRouteAgent(db, t, ui.lang);
|
|
1460
1480
|
if (choice) {
|
|
1461
1481
|
if (choice.direct) {
|
|
1462
1482
|
setSubjectDirect();
|
|
@@ -1466,6 +1486,7 @@ function startRepl(opts) {
|
|
|
1466
1486
|
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
|
|
1467
1487
|
}
|
|
1468
1488
|
} else {
|
|
1489
|
+
await warmImageJudgment(choice.agent);
|
|
1469
1490
|
setSubjectAgent(choice.agent);
|
|
1470
1491
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1471
1492
|
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Credential/secret detection for every terminal write boundary that must never persist
|
|
3
|
+
// or transmit a live key. Three inline copies had drifted apart (memory import missed
|
|
4
|
+
// JWTs, generic `key=` assignments and bearer headers; governance missed sk_live_/
|
|
5
|
+
// github_pat_/glpat-; experience exchange missed Google AIza and JWTs), so the same
|
|
6
|
+
// secret was caught at one boundary and stored in plain text at another. One list, one
|
|
7
|
+
// behaviour: extend HERE, not at a call site. Mirrors the desktop's
|
|
8
|
+
// shared/secret-patterns.ts.
|
|
9
|
+
//
|
|
10
|
+
// Scope rule: match *credential shapes*, not the words around them. Ordinary prose that
|
|
11
|
+
// mentions "token", or a hyphenated phrase like "risk-management-notes", must not trip
|
|
12
|
+
// this — a false positive silently drops a user's memory, which is its own data loss.
|
|
13
|
+
|
|
14
|
+
/** Live-credential shapes across the providers this product actually touches. */
|
|
15
|
+
const SECRET_SHAPES = [
|
|
16
|
+
// GitHub: classic PAT, OAuth/user/server/refresh tokens, fine-grained PAT.
|
|
17
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/,
|
|
18
|
+
/github_pat_[A-Za-z0-9_]{20,}/,
|
|
19
|
+
// Slack bot/user/app tokens.
|
|
20
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/,
|
|
21
|
+
// AWS access key ids (long-lived and STS).
|
|
22
|
+
/(?:AKIA|ASIA)[0-9A-Z]{16}/,
|
|
23
|
+
// Google / Firebase API keys.
|
|
24
|
+
/AIza[0-9A-Za-z_-]{30,}/,
|
|
25
|
+
// Stripe and similar: secret/restricted/publishable, live or test.
|
|
26
|
+
/(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/,
|
|
27
|
+
// OpenAI / Anthropic, including provider-segmented forms. The \b prevents an ordinary
|
|
28
|
+
// hyphenated phrase ("ask-forgiveness-not-permission") from matching.
|
|
29
|
+
/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{12,}/,
|
|
30
|
+
// HuggingFace, GitLab, npm.
|
|
31
|
+
/hf_[A-Za-z0-9]{20,}/,
|
|
32
|
+
/glpat-[A-Za-z0-9_-]{20,}/,
|
|
33
|
+
/npm_[A-Za-z0-9]{20,}/,
|
|
34
|
+
// JWTs (three base64url segments) — bearer tokens frequently land in pasted logs.
|
|
35
|
+
/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
|
|
36
|
+
// Telegram bot tokens.
|
|
37
|
+
/\b[0-9]{8,}:[A-Za-z0-9_-]{25,}\b/,
|
|
38
|
+
// Private key blocks.
|
|
39
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** `password: hunter2` style assignments, where the value shape alone proves nothing. */
|
|
43
|
+
const SECRET_ASSIGNMENT_RE =
|
|
44
|
+
/\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|cookie|bearer)\b\s*[:=]\s*['"]?[^\s,;'"]{6,}/i;
|
|
45
|
+
|
|
46
|
+
/** `Authorization: Bearer …` / `Basic …` headers. */
|
|
47
|
+
const AUTH_HEADER_RE = /\bauthorization\b\s*[:=]\s*['"]?(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/i;
|
|
48
|
+
|
|
49
|
+
/** Single source of truth. Case-insensitive: providers are inconsistent about casing. */
|
|
50
|
+
const SECRET_PATTERNS = [
|
|
51
|
+
...SECRET_SHAPES.map((re) => new RegExp(re.source, "i")),
|
|
52
|
+
SECRET_ASSIGNMENT_RE,
|
|
53
|
+
AUTH_HEADER_RE,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** True when the text contains something that looks like a live credential. */
|
|
57
|
+
function looksSecret(content) {
|
|
58
|
+
const text = String(content || "");
|
|
59
|
+
return SECRET_PATTERNS.some((re) => re.test(text));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Replace credential-shaped substrings with a marker, preserving surrounding text. */
|
|
63
|
+
function redactSecrets(content, marker = "[redacted-secret]") {
|
|
64
|
+
let out = String(content || "");
|
|
65
|
+
for (const re of SECRET_PATTERNS) {
|
|
66
|
+
out = out.replace(new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`), marker);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { SECRET_PATTERNS, looksSecret, redactSecrets };
|