@kenhkl/agent-moss 0.12.6 → 0.12.7

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/dist/cli.js CHANGED
@@ -137,6 +137,7 @@ __export(config_exports, {
137
137
  getProviderHeaders: () => getProviderHeaders,
138
138
  getRetryConfig: () => getRetryConfig,
139
139
  getTimeoutConfig: () => getTimeoutConfig,
140
+ isBrainEnabled: () => isBrainEnabled,
140
141
  isHeuristicEnabled: () => isHeuristicEnabled,
141
142
  isLLMAvailable: () => isLLMAvailable,
142
143
  isLogicRulesEnabled: () => isLogicRulesEnabled,
@@ -256,11 +257,11 @@ function isLLMAvailable() {
256
257
  }
257
258
  async function probeLLMConnection(timeoutMs = 8e3) {
258
259
  if (!isLLMAvailable()) return false;
259
- const OpenAI2 = (await import("openai")).default;
260
+ const OpenAI3 = (await import("openai")).default;
260
261
  const config = getLLMConfig();
261
262
  const provider = detectProvider(config.baseUrl, config.provider);
262
263
  const extraHeaders = getProviderHeaders(provider);
263
- const client = new OpenAI2({
264
+ const client = new OpenAI3({
264
265
  baseURL: config.baseUrl,
265
266
  apiKey: config.apiKey,
266
267
  timeout: timeoutMs,
@@ -318,6 +319,9 @@ function getProviderHeaders(provider) {
318
319
  function isHeuristicEnabled() {
319
320
  return process.env.AGENT_MOSS_DISABLE_HEURISTIC !== "1";
320
321
  }
322
+ function isBrainEnabled() {
323
+ return process.env.AGENT_MOSS_BRAIN_ENABLED === "1";
324
+ }
321
325
  function isLogicRulesEnabled() {
322
326
  return process.env.AGENT_MOSS_DISABLE_LOGIC_RULES !== "1";
323
327
  }
@@ -3957,10 +3961,561 @@ var init_llm_analyzer = __esm({
3957
3961
  }
3958
3962
  });
3959
3963
 
3964
+ // src/brain/rule-store.ts
3965
+ import { existsSync as existsSync9, readFileSync as readFileSync10, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "fs";
3966
+ import { join as join5 } from "path";
3967
+ import { homedir as homedir3 } from "os";
3968
+ function getRulesPath() {
3969
+ const envPath = process.env[ENV_RULES_PATH];
3970
+ if (envPath) return envPath;
3971
+ return DEFAULT_RULES_PATH;
3972
+ }
3973
+ function uuid() {
3974
+ try {
3975
+ return crypto.randomUUID();
3976
+ } catch {
3977
+ return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
3978
+ }
3979
+ }
3980
+ var DEFAULT_RULES_PATH, ENV_RULES_PATH, STATUS_DRAFT, STATUS_APPROVED, STATUS_ACTIVE, STATUS_DISPUTED, STATUS_STALE, STATUS_ARCHIVED, STATUS_REJECTED, BrainRuleStore;
3981
+ var init_rule_store = __esm({
3982
+ "src/brain/rule-store.ts"() {
3983
+ "use strict";
3984
+ init_esm_shims();
3985
+ DEFAULT_RULES_PATH = join5(homedir3(), ".agentmoss", "brain", "rules.json");
3986
+ ENV_RULES_PATH = "AGENT_MOSS_BRAIN_RULES_PATH";
3987
+ STATUS_DRAFT = "draft";
3988
+ STATUS_APPROVED = "approved";
3989
+ STATUS_ACTIVE = "active";
3990
+ STATUS_DISPUTED = "disputed";
3991
+ STATUS_STALE = "stale";
3992
+ STATUS_ARCHIVED = "archived";
3993
+ STATUS_REJECTED = "rejected";
3994
+ BrainRuleStore = class {
3995
+ path;
3996
+ data;
3997
+ constructor(path3) {
3998
+ this.path = path3 || getRulesPath();
3999
+ this.data = this.load();
4000
+ }
4001
+ load() {
4002
+ if (!existsSync9(this.path)) {
4003
+ return { version: 1, rules: [], pattern_candidates: [] };
4004
+ }
4005
+ try {
4006
+ const raw = JSON.parse(readFileSync10(this.path, "utf-8"));
4007
+ return {
4008
+ version: raw.version || 1,
4009
+ rules: raw.rules || [],
4010
+ pattern_candidates: raw.pattern_candidates || []
4011
+ };
4012
+ } catch {
4013
+ return { version: 1, rules: [], pattern_candidates: [] };
4014
+ }
4015
+ }
4016
+ save() {
4017
+ const dir = this.path.substring(0, this.path.lastIndexOf("/"));
4018
+ if (dir) mkdirSync3(dir, { recursive: true });
4019
+ writeFileSync3(this.path, JSON.stringify(this.data, null, 2), "utf-8");
4020
+ }
4021
+ addRule(rule) {
4022
+ const now = Date.now();
4023
+ const full = {
4024
+ id: rule.id || uuid(),
4025
+ layer: rule.layer,
4026
+ ruleType: rule.ruleType || "deny_pattern",
4027
+ pattern: rule.pattern,
4028
+ scope: rule.scope || "all",
4029
+ source: rule.source || "llm_extracted",
4030
+ sourceEventIds: rule.sourceEventIds || [],
4031
+ confidence: rule.confidence || 0,
4032
+ status: rule.status || STATUS_DRAFT,
4033
+ createdAt: rule.createdAt || now,
4034
+ lastTriggeredAt: rule.lastTriggeredAt || 0,
4035
+ triggerCount: rule.triggerCount || 0,
4036
+ disputedCount: rule.disputedCount || 0,
4037
+ explanation: rule.explanation || "",
4038
+ originNode: rule.originNode || "local"
4039
+ };
4040
+ this.data.rules.push(full);
4041
+ this.save();
4042
+ return full.id;
4043
+ }
4044
+ approveRule(ruleId) {
4045
+ const rule = this.getRule(ruleId);
4046
+ if (!rule) throw new Error(`\u89C4\u5219\u4E0D\u5B58\u5728: ${ruleId}`);
4047
+ if (rule.status !== STATUS_DRAFT) throw new Error(`\u53EA\u6709 draft \u72B6\u6001\u53EF\u5BA1\u6838\uFF0C\u5F53\u524D: ${rule.status}`);
4048
+ rule.status = STATUS_ACTIVE;
4049
+ this.save();
4050
+ }
4051
+ rejectRule(ruleId) {
4052
+ const rule = this.getRule(ruleId);
4053
+ if (!rule) throw new Error(`\u89C4\u5219\u4E0D\u5B58\u5728: ${ruleId}`);
4054
+ if (rule.status !== STATUS_DRAFT) throw new Error(`\u53EA\u6709 draft \u72B6\u6001\u53EF\u62D2\u7EDD\uFF0C\u5F53\u524D: ${rule.status}`);
4055
+ rule.status = STATUS_REJECTED;
4056
+ this.save();
4057
+ }
4058
+ getRule(ruleId) {
4059
+ return this.data.rules.find((r) => r.id === ruleId);
4060
+ }
4061
+ getActiveRules(layer) {
4062
+ return this.data.rules.filter((r) => r.status === STATUS_ACTIVE && (layer === void 0 || r.layer === layer));
4063
+ }
4064
+ getDraftRules() {
4065
+ return this.data.rules.filter((r) => r.status === STATUS_DRAFT).sort((a, b) => b.createdAt - a.createdAt);
4066
+ }
4067
+ recordTrigger(ruleId) {
4068
+ const rule = this.getRule(ruleId);
4069
+ if (!rule) return;
4070
+ rule.triggerCount += 1;
4071
+ rule.lastTriggeredAt = Date.now();
4072
+ this.save();
4073
+ }
4074
+ // --- pattern_candidates ---
4075
+ addPatternCandidate(pattern, layer, scope, sourceEventId, confidence) {
4076
+ const now = Date.now();
4077
+ const existing = this.data.pattern_candidates.find(
4078
+ (c) => c.pattern === pattern && c.layer === layer && c.scope === scope
4079
+ );
4080
+ if (existing) {
4081
+ if (existing.reviewStatus === "rejected") return 0;
4082
+ existing.occurrenceCount += 1;
4083
+ existing.lastSeenAt = now;
4084
+ existing.sourceEventIds.unshift(sourceEventId);
4085
+ this.save();
4086
+ return existing.occurrenceCount;
4087
+ }
4088
+ this.data.pattern_candidates.push({
4089
+ pattern,
4090
+ layer,
4091
+ scope,
4092
+ sourceEventIds: [sourceEventId],
4093
+ confidence,
4094
+ occurrenceCount: 1,
4095
+ firstSeenAt: now,
4096
+ lastSeenAt: now,
4097
+ reviewStatus: "pending",
4098
+ reviewReason: "",
4099
+ reviewedAt: 0
4100
+ });
4101
+ this.save();
4102
+ return 1;
4103
+ }
4104
+ getPatternCandidates(minOccurrences = 3) {
4105
+ return this.data.pattern_candidates.filter((c) => c.occurrenceCount >= minOccurrences && c.reviewStatus !== "rejected").sort((a, b) => b.occurrenceCount - a.occurrenceCount);
4106
+ }
4107
+ getPendingReviewCandidates(minOccurrences = 3) {
4108
+ return this.data.pattern_candidates.filter((c) => c.occurrenceCount >= minOccurrences && c.reviewStatus === "pending").sort((a, b) => b.occurrenceCount - a.occurrenceCount);
4109
+ }
4110
+ isExcludedPattern(pattern, layer, scope) {
4111
+ const c = this.data.pattern_candidates.find(
4112
+ (c2) => c2.pattern === pattern && c2.layer === layer && c2.scope === scope
4113
+ );
4114
+ return !!c && c.reviewStatus === "rejected";
4115
+ }
4116
+ setPatternReviewStatus(pattern, layer, scope, status, reason = "") {
4117
+ const c = this.data.pattern_candidates.find(
4118
+ (c2) => c2.pattern === pattern && c2.layer === layer && c2.scope === scope
4119
+ );
4120
+ if (c) {
4121
+ c.reviewStatus = status;
4122
+ c.reviewReason = reason;
4123
+ c.reviewedAt = Date.now();
4124
+ this.save();
4125
+ }
4126
+ }
4127
+ removePatternCandidate(pattern, layer, scope) {
4128
+ this.data.pattern_candidates = this.data.pattern_candidates.filter(
4129
+ (c) => !(c.pattern === pattern && c.layer === layer && c.scope === scope)
4130
+ );
4131
+ this.save();
4132
+ }
4133
+ getStats() {
4134
+ const count = (status) => this.data.rules.filter((r) => r.status === status).length;
4135
+ return {
4136
+ draft: count(STATUS_DRAFT),
4137
+ approved: count(STATUS_APPROVED),
4138
+ active: count(STATUS_ACTIVE),
4139
+ disputed: count(STATUS_DISPUTED),
4140
+ stale: count(STATUS_STALE),
4141
+ archived: count(STATUS_ARCHIVED),
4142
+ rejected: count(STATUS_REJECTED),
4143
+ total: this.data.rules.length,
4144
+ by_layer: {
4145
+ 1: this.data.rules.filter((r) => r.layer === 1 && r.status === STATUS_ACTIVE).length,
4146
+ 2: this.data.rules.filter((r) => r.layer === 2 && r.status === STATUS_ACTIVE).length
4147
+ },
4148
+ pattern_candidates: this.data.pattern_candidates.length
4149
+ };
4150
+ }
4151
+ };
4152
+ }
4153
+ });
4154
+
4155
+ // src/brain/pattern-extractor.ts
4156
+ var PatternExtractor;
4157
+ var init_pattern_extractor = __esm({
4158
+ "src/brain/pattern-extractor.ts"() {
4159
+ "use strict";
4160
+ init_esm_shims();
4161
+ init_rule_store();
4162
+ PatternExtractor = class {
4163
+ ruleStore;
4164
+ minConfidence;
4165
+ minOccurrences;
4166
+ builtinPatterns;
4167
+ reviewFn;
4168
+ constructor(ruleStore, opts) {
4169
+ this.ruleStore = ruleStore;
4170
+ this.minConfidence = opts?.minConfidence ?? 0.9;
4171
+ this.minOccurrences = opts?.minOccurrences ?? 3;
4172
+ this.builtinPatterns = opts?.builtinPatterns ?? [];
4173
+ this.reviewFn = opts?.review ?? null;
4174
+ }
4175
+ async extract(event, llmResult) {
4176
+ const result = { rulesCreated: 0, patternsRecorded: 0, skippedReason: "", deduped: [] };
4177
+ const confidence = llmResult.confidence ?? 0;
4178
+ const reason = llmResult.reason ?? "";
4179
+ const riskType = llmResult.riskType ?? "";
4180
+ const actionDetail = llmResult.actionDetail ?? event.actionDetail ?? "";
4181
+ const eventId = event.eventId ?? "";
4182
+ if (confidence < this.minConfidence) {
4183
+ result.skippedReason = `confidence ${confidence} < ${this.minConfidence}`;
4184
+ return result;
4185
+ }
4186
+ const patterns = this.extractPatterns(actionDetail, reason, riskType);
4187
+ if (patterns.length === 0) {
4188
+ result.skippedReason = "no extractable pattern";
4189
+ return result;
4190
+ }
4191
+ for (const p of patterns) {
4192
+ if (this.isCoveredByBuiltin(actionDetail)) {
4193
+ result.deduped.push(p.pattern);
4194
+ continue;
4195
+ }
4196
+ const count = this.ruleStore.addPatternCandidate(
4197
+ p.pattern,
4198
+ p.layer,
4199
+ p.scope,
4200
+ eventId,
4201
+ confidence
4202
+ );
4203
+ if (count === 0) {
4204
+ result.deduped.push(p.pattern);
4205
+ continue;
4206
+ }
4207
+ result.patternsRecorded += 1;
4208
+ if (count >= this.minOccurrences) {
4209
+ const candidates = this.ruleStore.getPatternCandidates(this.minOccurrences);
4210
+ const matched = candidates.find((c) => c.pattern === p.pattern && c.layer === p.layer);
4211
+ if (!matched) continue;
4212
+ if (this.reviewFn) {
4213
+ let review;
4214
+ try {
4215
+ review = await this.reviewFn({
4216
+ pattern: p.pattern,
4217
+ layer: p.layer,
4218
+ ruleType: p.ruleType,
4219
+ actionDetail,
4220
+ reason
4221
+ });
4222
+ } catch {
4223
+ review = null;
4224
+ }
4225
+ if (review === null) {
4226
+ continue;
4227
+ }
4228
+ if (!review.shouldKeep) {
4229
+ this.ruleStore.setPatternReviewStatus(
4230
+ p.pattern,
4231
+ p.layer,
4232
+ p.scope,
4233
+ "rejected",
4234
+ review.reason || "LLM \u5BA1\u6838\u62D2\u7EDD"
4235
+ );
4236
+ result.deduped.push(p.pattern);
4237
+ continue;
4238
+ }
4239
+ this.ruleStore.setPatternReviewStatus(
4240
+ p.pattern,
4241
+ p.layer,
4242
+ p.scope,
4243
+ "approved",
4244
+ review.reason || "LLM \u5BA1\u6838\u901A\u8FC7"
4245
+ );
4246
+ }
4247
+ this.ruleStore.addRule({
4248
+ layer: p.layer,
4249
+ ruleType: p.ruleType,
4250
+ pattern: p.pattern,
4251
+ scope: p.scope,
4252
+ source: "llm_extracted",
4253
+ sourceEventIds: eventId ? [eventId] : [],
4254
+ confidence: matched.confidence,
4255
+ status: STATUS_DRAFT,
4256
+ explanation: `\u4ECE ${count} \u6B21 LLM Deny \u4E2D\u63D0\u53D6(\u7ECF\u5BA1\u6838): ${reason.slice(0, 200)}`
4257
+ });
4258
+ this.ruleStore.removePatternCandidate(p.pattern, p.layer, p.scope);
4259
+ result.rulesCreated += 1;
4260
+ }
4261
+ }
4262
+ return result;
4263
+ }
4264
+ extractPatterns(actionDetail, reason, riskType) {
4265
+ const patterns = [];
4266
+ const detailLower = actionDetail.toLowerCase();
4267
+ const seen = /* @__PURE__ */ new Set();
4268
+ const sensitivePathRe = /(\/(?:etc|root)(?:\/[^\s;|&"'<>`$]*)?|(?:[^\s;|&"'<>`$]*)?\/\.ssh\/[^\s;|&"'<>`$]*|(?:[^\s;|&"'<>`$]*)?\/\.aws\/[^\s;|&"'<>`$]*|\S+\.(?:pem|key|p12|pfx|id_rsa|authorized_keys)\b)/;
4269
+ const m = detailLower.match(sensitivePathRe);
4270
+ if (m) {
4271
+ const gen = this.generalizePath(m[1]);
4272
+ if (!seen.has(gen)) {
4273
+ seen.add(gen);
4274
+ patterns.push({ pattern: gen, layer: 1, scope: "openat", ruleType: "deny_path" });
4275
+ }
4276
+ }
4277
+ const dangerousCmdRe = /\b(rm\s+-[a-zA-Z]*r[a-zA-Z]*f|rm\s+--recursive|chmod\s+777|curl\s+[^|]*\|\s*(?:ba)?sh|wget\s+[^|]*\|\s*(?:ba)?sh|mkfs\.|dd\s+if=|>\s*\/dev\/sd)\b/;
4278
+ const cmdM = detailLower.match(dangerousCmdRe);
4279
+ if (cmdM) {
4280
+ const gen = this.generalizeCommand(cmdM[1]);
4281
+ const seenKey = `cmd:${gen}`;
4282
+ if (!seen.has(seenKey)) {
4283
+ seen.add(seenKey);
4284
+ patterns.push({ pattern: gen, layer: 1, scope: "execve", ruleType: "deny_command" });
4285
+ }
4286
+ }
4287
+ const reasonLower = (reason || "").toLowerCase();
4288
+ if (reasonLower.includes("inject") || reasonLower.includes("\u6570\u636E\u6CE8\u5165")) {
4289
+ patterns.push({
4290
+ pattern: "BEHAVIOR_CHAIN:external_read\u2192sensitive_write",
4291
+ layer: 2,
4292
+ scope: "all",
4293
+ ruleType: "deny_pattern"
4294
+ });
4295
+ }
4296
+ if (reasonLower.includes("reconnaissance") || reasonLower.includes("\u4FA6\u5BDF") || reasonLower.includes("\u63D0\u6743")) {
4297
+ patterns.push({
4298
+ pattern: "BEHAVIOR_CHAIN:multi_config_read\u2192privilege_action",
4299
+ layer: 2,
4300
+ scope: "all",
4301
+ ruleType: "deny_pattern"
4302
+ });
4303
+ }
4304
+ const pathPrefix = `(?:^|[\\s"'(=])`;
4305
+ const pathBody = "(\\/(?:[a-zA-Z0-9_.-]+\\/)+[a-zA-Z0-9_.-]+)";
4306
+ const pathSuffix = `(?=[\\s"')\uFF0C\u3002\uFF09\uFF1B;]|$)`;
4307
+ for (const src of [detailLower, reasonLower]) {
4308
+ const pathRe = new RegExp(pathPrefix + pathBody + pathSuffix, "g");
4309
+ let mm;
4310
+ while ((mm = pathRe.exec(src)) !== null) {
4311
+ const path3 = mm[1];
4312
+ if (path3.split("/").length < 3) continue;
4313
+ if (["/dev/null", "/dev/zero", "/dev/random", "/dev/urandom"].includes(path3)) continue;
4314
+ const gen = this.generalizePath(path3);
4315
+ if (seen.has(gen)) continue;
4316
+ seen.add(gen);
4317
+ patterns.push({ pattern: gen, layer: 1, scope: "openat", ruleType: "deny_path" });
4318
+ }
4319
+ }
4320
+ return patterns;
4321
+ }
4322
+ isCoveredByBuiltin(actionDetail) {
4323
+ if (this.builtinPatterns.length === 0 || !actionDetail) return false;
4324
+ for (const pat of this.builtinPatterns) {
4325
+ try {
4326
+ if (new RegExp(pat, "i").test(actionDetail)) return true;
4327
+ } catch {
4328
+ continue;
4329
+ }
4330
+ }
4331
+ return false;
4332
+ }
4333
+ generalizePath(path3) {
4334
+ const p = path3.toLowerCase();
4335
+ if (/\/etc\/(shadow|passwd|sudoers|security|ssh)/.test(p)) {
4336
+ return "(?:^|[^a-z0-9_-])etc/(?:shadow|passwd|sudoers|security/|ssh/)";
4337
+ }
4338
+ if (p.includes("/etc/")) {
4339
+ return "(?:^|[^a-z0-9_-])etc/";
4340
+ }
4341
+ if (p.includes("/root/")) {
4342
+ return "(?:^|[^a-z0-9_-])root/\\.(?:ssh|gnupg|aws|config)/";
4343
+ }
4344
+ if (p.includes(".ssh/")) {
4345
+ return "(?:^|[^a-z0-9_-])[\\w./-]*/\\.ssh/";
4346
+ }
4347
+ if (p.includes(".aws/")) {
4348
+ return "(?:^|[^a-z0-9_-])[\\w./-]*/\\.aws/";
4349
+ }
4350
+ if (/\.(pem|key|p12|pfx|id_rsa|authorized_keys)\b/.test(p)) {
4351
+ return "[\\w./-]*\\.(?:pem|key|p12|pfx|id_rsa|authorized_keys)(?:$|[^a-z0-9])";
4352
+ }
4353
+ const escaped = p.replace(/^\//, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4354
+ return `(?:^|[^a-z0-9_-])${escaped}(?:/|\\b)`;
4355
+ }
4356
+ generalizeCommand(cmd) {
4357
+ if (cmd.includes("rm") && cmd.includes("r") && cmd.includes("f")) {
4358
+ return "rm\\s+(?:-[a-zA-Z]*r[a-zA-Z]*f|--recursive)\\s+/";
4359
+ }
4360
+ if (cmd.includes("chmod")) return "chmod\\s+777";
4361
+ if (cmd.includes("curl") || cmd.includes("wget")) {
4362
+ return "(?:curl|wget)\\s+[^|]*\\|\\s*(?:ba)?sh";
4363
+ }
4364
+ if (cmd.includes("mkfs")) return "mkfs\\.";
4365
+ if (cmd.includes("dd")) return "dd\\s+if=";
4366
+ return cmd.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4367
+ }
4368
+ };
4369
+ }
4370
+ });
4371
+
4372
+ // src/brain/reviewer.ts
4373
+ import OpenAI2 from "openai";
4374
+ function createBrainReviewer(timeoutMs = 1e4) {
4375
+ return async (opts) => {
4376
+ if (process.env.AGENT_MOSS_BRAIN_REVIEW === "0") return null;
4377
+ if (!isLLMAvailable()) return null;
4378
+ const config = getLLMConfig();
4379
+ const provider = detectProvider(config.baseUrl, config.provider);
4380
+ const extraHeaders = getProviderHeaders(provider);
4381
+ const client = new OpenAI2({
4382
+ baseURL: config.baseUrl,
4383
+ apiKey: config.apiKey,
4384
+ timeout: timeoutMs,
4385
+ maxRetries: 1,
4386
+ defaultHeaders: Object.keys(extraHeaders).length > 0 ? extraHeaders : void 0
4387
+ });
4388
+ const prompt = REVIEW_PROMPT.replace("{pattern}", opts.pattern).replace("{layer}", String(opts.layer)).replace("{ruleType}", opts.ruleType).replace("{actionDetail}", (opts.actionDetail || "").slice(0, 400)).replace("{reason}", (opts.reason || "").slice(0, 400));
4389
+ try {
4390
+ const response = await client.chat.completions.create({
4391
+ model: config.model,
4392
+ temperature: config.temperature || 0.1,
4393
+ max_tokens: 256,
4394
+ messages: [
4395
+ { role: "system", content: "\u53EA\u8F93\u51FA\u7EAF JSON\uFF0C\u4E0D\u8981\u5305\u542B markdown \u6807\u8BB0\u3001\u4EE3\u7801\u5757\u6216\u4EFB\u4F55\u5176\u4ED6\u6587\u5B57\u3002" },
4396
+ { role: "user", content: prompt }
4397
+ ]
4398
+ });
4399
+ const content = response.choices[0]?.message?.content || "";
4400
+ const parsed = parseReview(content);
4401
+ return { shouldKeep: parsed.shouldKeep, reason: parsed.reason };
4402
+ } catch (e) {
4403
+ console.warn(`[AgentMoss] Brain \u5BA1\u6838 LLM \u8C03\u7528\u5931\u8D25\uFF08\u964D\u7EA7\u4FDD\u6301\u5019\u9009\uFF09: ${e.message}`);
4404
+ return null;
4405
+ }
4406
+ };
4407
+ }
4408
+ function parseReview(content) {
4409
+ let text = content.trim();
4410
+ if (text.startsWith("```")) {
4411
+ text = text.replace(/^```[a-zA-Z]*\n?/, "").replace(/\n?```$/, "");
4412
+ }
4413
+ const match = text.match(/\{[\s\S]*\}/);
4414
+ if (!match) throw new Error(`\u5BA1\u6838\u54CD\u5E94\u65E0 JSON \u5BF9\u8C61: ${text.slice(0, 200)}`);
4415
+ const data = JSON.parse(match[0]);
4416
+ if (!("should_keep" in data) && !("shouldKeep" in data)) {
4417
+ throw new Error(`\u5BA1\u6838\u54CD\u5E94\u7F3A should_keep: ${text.slice(0, 200)}`);
4418
+ }
4419
+ let shouldKeep = data.shouldKeep ?? data.should_keep;
4420
+ if (typeof shouldKeep === "string") {
4421
+ shouldKeep = ["true", "1", "\u662F", "yes"].includes(shouldKeep.trim().toLowerCase());
4422
+ }
4423
+ return {
4424
+ shouldKeep: !!shouldKeep,
4425
+ confidence: Number(data.confidence ?? 0) || 0,
4426
+ reason: String(data.reason ?? data.suggested_rule ?? "") || ""
4427
+ };
4428
+ }
4429
+ var REVIEW_PROMPT;
4430
+ var init_reviewer = __esm({
4431
+ "src/brain/reviewer.ts"() {
4432
+ "use strict";
4433
+ init_esm_shims();
4434
+ init_config();
4435
+ REVIEW_PROMPT = `\u4F60\u662F AgentMoss \u5B89\u5168\u89C4\u5219\u5BA1\u6838\u5668\u3002\u5224\u65AD\u4E0B\u5217"\u4ECE LLM \u62D2\u7EDD\u7ED3\u679C\u4E2D\u63D0\u53D6\u7684\u5B89\u5168\u6A21\u5F0F"\u662F\u5426\u503C\u5F97\u6C89\u6DC0\u4E3A\u672C\u5730\u89C4\u5219\u3002
4436
+
4437
+ ## \u63D0\u53D6\u7684\u6A21\u5F0F\uFF08\u5019\u9009\uFF09
4438
+ - \u6A21\u5F0F: {pattern}
4439
+ - \u6240\u5C5E\u5C42: Layer {layer}\uFF081=\u7279\u5F81\u5339\u914D\uFF0C2=\u884C\u4E3A\u94FE\uFF09
4440
+ - \u7C7B\u578B: {ruleType}
4441
+
4442
+ ## \u539F\u59CB\u88AB\u62D2\u7EDD\u7684\u64CD\u4F5C
4443
+ {actionDetail}
4444
+
4445
+ ## LLM \u5F53\u65F6\u7684\u62D2\u7EDD\u7406\u7531
4446
+ {reason}
4447
+
4448
+ ## \u5224\u65AD\u51C6\u5219
4449
+ 1. **\u662F\u5426\u8FC7\u9AD8\u6CDB\u5316**\uFF1A\u8FD9\u6761\u6A21\u5F0F\u662F\u5426\u4F1A\u8BEF\u62E6\u6B63\u5E38\u7684\u5408\u6CD5\u64CD\u4F5C\uFF1F
4450
+ 2. **\u662F\u5426\u7B26\u5408\u654F\u611F\u7279\u5F81**\uFF1A\u6307\u5411\u7684\u8DEF\u5F84/\u547D\u4EE4\u662F\u5426\u786E\u5B9E\u654F\u611F\uFF08\u51ED\u636E/\u7CFB\u7EDF\u6587\u4EF6/\u6076\u610F\u547D\u4EE4/\u5916\u4F20\uFF09\uFF1F
4451
+ 3. **\u4E0E\u4E09\u5C42\u89C4\u5219\u91CD\u590D**\uFF1A\u662F\u5426\u5DF2\u88AB\u73B0\u6709 L1/L2 \u89C4\u5219\u8986\u76D6\uFF1F
4452
+ 4. **\u662F\u4E0D\u662F\u771F\u5B9E\u653B\u51FB\u6A21\u5F0F**\uFF1A\u662F\u5426\u4EE3\u8868\u4E00\u79CD\u8FDE\u7EED\u7684\u653B\u51FB\u884C\u4E3A\u6A21\u5F0F\uFF1F
4453
+
4454
+ ## \u8F93\u51FA JSON\uFF08\u53EA\u8F93\u51FA JSON\uFF0C\u65E0\u5176\u4ED6\u6587\u5B57\uFF09
4455
+ {"should_keep": true/false, "confidence": 0-100, "reason": "\u7B80\u77ED\u7406\u7531\uFF08\u4E2D\u6587\uFF09", "suggested_rule": "\u82E5\u4FDD\u7559\u5219\u8F93\u51FA\u5EFA\u8BAE\u6B63\u5219\uFF0C\u5426\u5219\u7A7A\u4E32", "duplicate_of": "\u82E5\u91CD\u590D\u5219\u8BF4\u660E\u662F\u54EA\u4E2A\uFF0C\u5426\u5219\u7A7A\u4E32"}`;
4456
+ }
4457
+ });
4458
+
4459
+ // src/brain/index.ts
4460
+ function checkBrainRules(store, opts) {
4461
+ if (!store) return null;
4462
+ const rules = store.getActiveRules(opts.layer);
4463
+ if (rules.length === 0) return null;
4464
+ const combined = `${opts.actionType} ${opts.actionDetail}`;
4465
+ for (const rule of rules) {
4466
+ try {
4467
+ if (new RegExp(rule.pattern, "i").test(combined)) {
4468
+ if (opts.layer === 2 && opts.actionHistory) {
4469
+ if (!checkLayer2Context(rule.pattern, opts.actionHistory, opts.actionDetail)) {
4470
+ continue;
4471
+ }
4472
+ }
4473
+ store.recordTrigger(rule.id);
4474
+ return {
4475
+ ruleId: rule.id,
4476
+ pattern: rule.pattern,
4477
+ explanation: rule.explanation,
4478
+ confidence: rule.confidence
4479
+ };
4480
+ }
4481
+ } catch {
4482
+ continue;
4483
+ }
4484
+ }
4485
+ return null;
4486
+ }
4487
+ function checkLayer2Context(pattern, actionHistory, actionDetail) {
4488
+ if (pattern.startsWith("BEHAVIOR_CHAIN:")) {
4489
+ const chainType = pattern.replace("BEHAVIOR_CHAIN:", "");
4490
+ if (chainType === "external_read\u2192sensitive_write") {
4491
+ const hasExternalRead = actionHistory.slice(-5).some((h) => /curl|wget|http/i.test(h.actionDetail ?? ""));
4492
+ const isSensitiveWrite = /\.ssh|\.gnupg|authorized_keys|bashrc|profile/i.test(actionDetail);
4493
+ return hasExternalRead && isSensitiveWrite;
4494
+ }
4495
+ if (chainType === "multi_config_read\u2192privilege_action") {
4496
+ const configReads = actionHistory.slice(-5).filter((h) => /\/etc\/|config|\.conf|\.yaml|\.json/i.test(h.actionDetail ?? "")).length;
4497
+ const isPrivilege = /sudo|su |chmod|chown/i.test(actionDetail);
4498
+ return configReads >= 2 && isPrivilege;
4499
+ }
4500
+ return false;
4501
+ }
4502
+ return true;
4503
+ }
4504
+ var init_brain = __esm({
4505
+ "src/brain/index.ts"() {
4506
+ "use strict";
4507
+ init_esm_shims();
4508
+ init_rule_store();
4509
+ init_pattern_extractor();
4510
+ init_reviewer();
4511
+ }
4512
+ });
4513
+
3960
4514
  // src/engine/coordinator.ts
3961
4515
  var coordinator_exports = {};
3962
4516
  __export(coordinator_exports, {
3963
4517
  TRUSTED_SKILLS_BASE: () => TRUSTED_SKILLS_BASE,
4518
+ _resetBrainForTest: () => _resetBrainForTest,
3964
4519
  runAnalysis: () => runAnalysis
3965
4520
  });
3966
4521
  function extractFirstCommand(command) {
@@ -3981,6 +4536,20 @@ function extractRawCommand(detail) {
3981
4536
  function matchesAny(cmd, patterns) {
3982
4537
  return patterns.some((p) => p.test(cmd));
3983
4538
  }
4539
+ function getBrainStore() {
4540
+ if (!isBrainEnabled()) return null;
4541
+ if (!_brainInitialized) {
4542
+ _brainStore = new BrainRuleStore();
4543
+ _brainExtractor = new PatternExtractor(_brainStore, { review: createBrainReviewer() });
4544
+ _brainInitialized = true;
4545
+ }
4546
+ return _brainStore;
4547
+ }
4548
+ function _resetBrainForTest() {
4549
+ _brainStore = null;
4550
+ _brainExtractor = null;
4551
+ _brainInitialized = false;
4552
+ }
3984
4553
  async function runAnalysis(input) {
3985
4554
  const heuristicResult = isHeuristicEnabled() ? runHeuristicCheck(input) : null;
3986
4555
  const isInlineScriptCmd = isInlineScriptCommand(input.aNext.actionDetail.toLowerCase());
@@ -4013,6 +4582,25 @@ async function runAnalysis(input) {
4013
4582
  }
4014
4583
  }
4015
4584
  }
4585
+ const brainStore = getBrainStore();
4586
+ if (brainStore) {
4587
+ const brainL1 = checkBrainRules(brainStore, {
4588
+ layer: 1,
4589
+ actionType: input.aNext.actionType,
4590
+ actionDetail: input.aNext.actionDetail
4591
+ });
4592
+ if (brainL1) {
4593
+ return {
4594
+ allowed: false,
4595
+ reason: `[Brain L1] ${brainL1.explanation}`,
4596
+ riskLevel: "high",
4597
+ riskType: "brain_rule",
4598
+ confidence: Math.round(brainL1.confidence * 100),
4599
+ source: "brain_rule",
4600
+ violatedLayers: ["brain_L1"]
4601
+ };
4602
+ }
4603
+ }
4016
4604
  const actionType = input.aNext.actionType.toLowerCase();
4017
4605
  const rawDetail = extractRawCommand(input.aNext.actionDetail);
4018
4606
  const lowerDetail = rawDetail.toLowerCase();
@@ -4138,6 +4726,25 @@ async function runAnalysis(input) {
4138
4726
  }
4139
4727
  }
4140
4728
  }
4729
+ if (brainStore) {
4730
+ const brainL2 = checkBrainRules(brainStore, {
4731
+ layer: 2,
4732
+ actionType: input.aNext.actionType,
4733
+ actionDetail: input.aNext.actionDetail,
4734
+ actionHistory: input.actionHistory
4735
+ });
4736
+ if (brainL2) {
4737
+ return {
4738
+ allowed: false,
4739
+ reason: `[Brain L2] ${brainL2.explanation}`,
4740
+ riskLevel: "high",
4741
+ riskType: "brain_rule",
4742
+ confidence: Math.round(brainL2.confidence * 100),
4743
+ source: "brain_rule",
4744
+ violatedLayers: ["brain_L2"]
4745
+ };
4746
+ }
4747
+ }
4141
4748
  if (skipLLM || !isLLMAvailable()) {
4142
4749
  return {
4143
4750
  allowed: true,
@@ -4204,9 +4811,23 @@ async function runAnalysis(input) {
4204
4811
  violatedLayers: ["3"]
4205
4812
  };
4206
4813
  }
4814
+ if (!llmResult.allowed && llmResult.confidence && llmResult.confidence >= 90 && _brainExtractor) {
4815
+ try {
4816
+ await _brainExtractor.extract(
4817
+ { actionDetail: input.aNext.actionDetail, eventId: "" },
4818
+ {
4819
+ confidence: llmResult.confidence / 100,
4820
+ reason: llmResult.reason || "",
4821
+ riskType: llmResult.riskType || "",
4822
+ actionDetail: input.aNext.actionDetail
4823
+ }
4824
+ );
4825
+ } catch {
4826
+ }
4827
+ }
4207
4828
  return llmResult;
4208
4829
  }
4209
- var FULLY_SAFE_TOOLS, TRUSTED_SKILLS_BASE, READONLY_SENSITIVE_TOOLS, FULLY_SAFE_BASH_PATTERNS, READONLY_SENSITIVE_BASH_PATTERNS;
4830
+ var FULLY_SAFE_TOOLS, TRUSTED_SKILLS_BASE, READONLY_SENSITIVE_TOOLS, FULLY_SAFE_BASH_PATTERNS, READONLY_SENSITIVE_BASH_PATTERNS, _brainStore, _brainExtractor, _brainInitialized;
4210
4831
  var init_coordinator = __esm({
4211
4832
  "src/engine/coordinator.ts"() {
4212
4833
  "use strict";
@@ -4218,6 +4839,8 @@ var init_coordinator = __esm({
4218
4839
  init_script_content_analyzer();
4219
4840
  init_config();
4220
4841
  init_agents();
4842
+ init_brain();
4843
+ init_reviewer();
4221
4844
  FULLY_SAFE_TOOLS = /* @__PURE__ */ new Set([
4222
4845
  "ask_user_question",
4223
4846
  "glob",
@@ -4284,6 +4907,9 @@ var init_coordinator = __esm({
4284
4907
  /^sort(?:\s|$)/i,
4285
4908
  /^uniq(?:\s|$)/i
4286
4909
  ];
4910
+ _brainStore = null;
4911
+ _brainExtractor = null;
4912
+ _brainInitialized = false;
4287
4913
  }
4288
4914
  });
4289
4915
 
@@ -4370,7 +4996,7 @@ var require_bindings = __commonJS({
4370
4996
  var fs4 = __require("fs");
4371
4997
  var path3 = __require("path");
4372
4998
  var fileURLToPath10 = require_file_uri_to_path();
4373
- var join6 = path3.join;
4999
+ var join7 = path3.join;
4374
5000
  var dirname8 = path3.dirname;
4375
5001
  var exists = fs4.accessSync && function(path4) {
4376
5002
  try {
@@ -4430,7 +5056,7 @@ var require_bindings = __commonJS({
4430
5056
  var requireFunc = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
4431
5057
  var tries = [], i = 0, l = opts.try.length, n, b, err;
4432
5058
  for (; i < l; i++) {
4433
- n = join6.apply(
5059
+ n = join7.apply(
4434
5060
  null,
4435
5061
  opts.try[i].map(function(p) {
4436
5062
  return opts[p] || p;
@@ -4491,7 +5117,7 @@ var require_bindings = __commonJS({
4491
5117
  if (dir === ".") {
4492
5118
  dir = process.cwd();
4493
5119
  }
4494
- if (exists(join6(dir, "package.json")) || exists(join6(dir, "node_modules"))) {
5120
+ if (exists(join7(dir, "package.json")) || exists(join7(dir, "node_modules"))) {
4495
5121
  return dir;
4496
5122
  }
4497
5123
  if (prev === dir) {
@@ -4500,7 +5126,7 @@ var require_bindings = __commonJS({
4500
5126
  );
4501
5127
  }
4502
5128
  prev = dir;
4503
- dir = join6(dir, "..");
5129
+ dir = join7(dir, "..");
4504
5130
  }
4505
5131
  };
4506
5132
  }
@@ -5332,11 +5958,11 @@ init_esm_shims();
5332
5958
  init_runtime_config();
5333
5959
  init_token_stats();
5334
5960
  import { Hono } from "hono";
5335
- import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
5336
- import { join as join5, dirname as dirname7 } from "path";
5961
+ import { readFileSync as readFileSync11, existsSync as existsSync11 } from "fs";
5962
+ import { join as join6, dirname as dirname7 } from "path";
5337
5963
  import { fileURLToPath as fileURLToPath8 } from "url";
5338
5964
  var __dirname7 = dirname7(fileURLToPath8(import.meta.url));
5339
- var STATIC_DIR = join5(__dirname7, "static");
5965
+ var STATIC_DIR = join6(__dirname7, "static");
5340
5966
  var CONSOLE_TOKEN_ENV = "AGENT_MOSS_CONSOLE_TOKEN";
5341
5967
  function getConsoleToken() {
5342
5968
  return process.env[CONSOLE_TOKEN_ENV] || "";
@@ -5360,9 +5986,9 @@ function createConsoleRouter() {
5360
5986
  const console2 = new Hono();
5361
5987
  console2.use("/api/*", authMiddleware());
5362
5988
  const serveIndex = (c) => {
5363
- const indexFile = join5(STATIC_DIR, "index.html");
5364
- if (existsSync10(indexFile)) {
5365
- return c.html(readFileSync10(indexFile, "utf-8"));
5989
+ const indexFile = join6(STATIC_DIR, "index.html");
5990
+ if (existsSync11(indexFile)) {
5991
+ return c.html(readFileSync11(indexFile, "utf-8"));
5366
5992
  }
5367
5993
  return c.html("<h1>AgentMoss Policy Console</h1><p>Frontend not built. Use /console/api/* endpoints.</p>");
5368
5994
  };
@@ -5453,13 +6079,13 @@ function createConsoleRouter() {
5453
6079
  if (!/^[A-Za-z0-9_-]+$/.test(skillId)) {
5454
6080
  return c.json({ error: "invalid skill_id" }, 400);
5455
6081
  }
5456
- const skillPath = join5(dirname7(getRuntimeConfigPath()), "agent_moss_skills", `${skillId}.md`);
5457
- if (!existsSync10(skillPath)) {
5458
- const builtinPath = join5(__dirname7, "..", "..", "agent_moss", "skills", `${skillId}.md`);
5459
- if (existsSync10(builtinPath)) return c.text(readFileSync10(builtinPath, "utf-8"));
6082
+ const skillPath = join6(dirname7(getRuntimeConfigPath()), "agent_moss_skills", `${skillId}.md`);
6083
+ if (!existsSync11(skillPath)) {
6084
+ const builtinPath = join6(__dirname7, "..", "..", "agent_moss", "skills", `${skillId}.md`);
6085
+ if (existsSync11(builtinPath)) return c.text(readFileSync11(builtinPath, "utf-8"));
5460
6086
  return c.json({ error: "skill not found" }, 404);
5461
6087
  }
5462
- return c.text(readFileSync10(skillPath, "utf-8"));
6088
+ return c.text(readFileSync11(skillPath, "utf-8"));
5463
6089
  });
5464
6090
  console2.get("/api/agents", (c) => {
5465
6091
  const rc = loadRuntimeConfig();