@kenhkl/agent-moss 0.17.6 → 0.17.8

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
@@ -1258,6 +1258,20 @@ function redactChain(chain, currentSessionId) {
1258
1258
  return step;
1259
1259
  });
1260
1260
  }
1261
+ function redactGraph(graph, currentSessionId) {
1262
+ if (!graph) return null;
1263
+ if (chainDetailMode() === "full") return graph;
1264
+ const nodes = (graph.nodes || []).map((n) => {
1265
+ const m = { ...n };
1266
+ if (String(m.session_id ?? "") !== String(currentSessionId)) {
1267
+ m.session_id = "<other-session>";
1268
+ m.action_detail = "<\u5DF2\u9690\u85CF>";
1269
+ m.resources = [];
1270
+ }
1271
+ return m;
1272
+ });
1273
+ return { nodes, edges: graph.edges || [] };
1274
+ }
1261
1275
  function sessionSweepThreshold() {
1262
1276
  return envInt("AGENT_MOSS_SESSION_SWEEP_THRESHOLD", 50);
1263
1277
  }
@@ -1340,6 +1354,12 @@ function agentInflight() {
1340
1354
  function boundAgentOf(principal) {
1341
1355
  return principal.startsWith("agent:") ? principal.slice("agent:".length) : "";
1342
1356
  }
1357
+ function chainScope() {
1358
+ return (env("AGENT_MOSS_CHAIN_SCOPE", "system") || "system").toLowerCase();
1359
+ }
1360
+ function chainBucket(agentId = "") {
1361
+ return chainScope() === "agent" ? agentId || "default" : "__system__";
1362
+ }
1343
1363
  function rateLimitQps() {
1344
1364
  return envInt("AGENT_MOSS_RATE_LIMIT_QPS", 100);
1345
1365
  }
@@ -1411,6 +1431,8 @@ var init_models = __esm({
1411
1431
  action_history: z.array(ActionItemSchema).default([]).describe("Historical action sequence"),
1412
1432
  session_history: z.array(z.object({
1413
1433
  session_id: z.string(),
1434
+ /** 来源 agent(AgentGate 逐会话带上):记录在链路径里供告警/取证,不参与链匹配。 */
1435
+ agent_id: z.string().optional(),
1414
1436
  action_history: z.array(ActionItemSchema).default([])
1415
1437
  })).default([]).describe("Multi-session historical actions (optional): for L2.2 formal engine cross-session combo-chain / sensitive-data-flow verification. Provided by caller (incl. future agentgate)."),
1416
1438
  a_next: ActionItemSchema.describe("Next action to be executed"),
@@ -1431,7 +1453,42 @@ var init_models = __esm({
1431
1453
  confidence: z.number().int().min(0).max(100).default(100),
1432
1454
  analysis_duration_ms: z.number().default(0),
1433
1455
  // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
1434
- violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
1456
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([]),
1457
+ // 结构化攻击链图(前端可视化):节点 + 边(next 时序 / data 数据流)
1458
+ violation_graph: z.object({
1459
+ // 性质元信息(前端展示"违反了哪条性质")
1460
+ property_id: z.string().optional(),
1461
+ property_short: z.string().optional(),
1462
+ property_name: z.string().optional(),
1463
+ // 性质表达式 token → 节点位置(前端展示"表达式对应哪个节点")
1464
+ property_expression: z.array(
1465
+ z.object({
1466
+ token: z.string(),
1467
+ node_index: z.union([z.number(), z.array(z.number())])
1468
+ })
1469
+ ).optional(),
1470
+ nodes: z.array(
1471
+ z.object({
1472
+ session_id: z.string().default(""),
1473
+ seq: z.number().default(0),
1474
+ stage: z.string().default(""),
1475
+ action_detail: z.string().default(""),
1476
+ resources: z.array(z.string()).default([]),
1477
+ // 节点在性质中的角色:source/carrier/sink(P2)或阶段名(P1)
1478
+ role: z.string().optional(),
1479
+ // 是否为违例见证节点(当前动作补全链 / 触发外传)
1480
+ witness: z.boolean().optional()
1481
+ })
1482
+ ),
1483
+ edges: z.array(
1484
+ z.object({
1485
+ from: z.number(),
1486
+ to: z.number(),
1487
+ type: z.enum(["next", "data"]).default("next"),
1488
+ label: z.string().optional()
1489
+ })
1490
+ )
1491
+ }).nullable().default(null)
1435
1492
  });
1436
1493
  HealthResponseSchema = z.object({
1437
1494
  status: z.string().default("healthy"),
@@ -2999,7 +3056,7 @@ function _scenarioRulesToDefaults() {
2999
3056
  if (!existsSync7(scenDir)) return out;
3000
3057
  let entries;
3001
3058
  try {
3002
- entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
3059
+ entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("__")).map((d) => d.name);
3003
3060
  } catch {
3004
3061
  return out;
3005
3062
  }
@@ -9379,7 +9436,7 @@ function markIntegrity(actionDetail) {
9379
9436
  }
9380
9437
  return { integrity: "medium", isLowSource: false };
9381
9438
  }
9382
- function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
9439
+ function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "") {
9383
9440
  const { integrity, isLowSource } = markIntegrity(actionDetail);
9384
9441
  return {
9385
9442
  sessionId,
@@ -9387,13 +9444,14 @@ function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
9387
9444
  actionType,
9388
9445
  actionDetail,
9389
9446
  stage: markStage(actionDetail),
9447
+ agentId: agentId || void 0,
9390
9448
  resources: extractResources(actionDetail),
9391
9449
  integrity,
9392
9450
  isLowSource,
9393
9451
  timestamp: timestamp || void 0
9394
9452
  };
9395
9453
  }
9396
- function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
9454
+ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "") {
9397
9455
  const nodes = [];
9398
9456
  const edges = [];
9399
9457
  const stageFlags = /* @__PURE__ */ new Set();
@@ -9403,18 +9461,20 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
9403
9461
  if (ad) details.push({
9404
9462
  actionType: String(a.actionType || ""),
9405
9463
  actionDetail: ad,
9406
- calledAt: a.calledAt || void 0
9464
+ calledAt: a.calledAt || void 0,
9465
+ agentId: a.agentId || agentId || void 0
9407
9466
  });
9408
9467
  }
9409
9468
  if (aNext) {
9410
9469
  details.push({
9411
9470
  actionType: String(aNext.actionType || ""),
9412
9471
  actionDetail: String(aNext.actionDetail || ""),
9413
- calledAt: aNext.calledAt || void 0
9472
+ calledAt: aNext.calledAt || void 0,
9473
+ agentId: aNext.agentId || agentId || void 0
9414
9474
  });
9415
9475
  }
9416
9476
  details.forEach((d, seq) => {
9417
- const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt);
9477
+ const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "");
9418
9478
  nodes.push(node);
9419
9479
  if (node.stage) stageFlags.add(node.stage);
9420
9480
  });
@@ -9431,7 +9491,7 @@ function buildMultiSessionGraph(sessions, aNext) {
9431
9491
  }
9432
9492
  for (const s of sessions) {
9433
9493
  const sid = String(s.sessionId || "s");
9434
- mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid));
9494
+ mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid, s.agentId || ""));
9435
9495
  }
9436
9496
  if (aNext) {
9437
9497
  const last = mg.sessions[mg.sessions.length - 1];
@@ -9441,7 +9501,8 @@ function buildMultiSessionGraph(sessions, aNext) {
9441
9501
  last.nodes.length,
9442
9502
  String(aNext.actionType || ""),
9443
9503
  String(aNext.actionDetail || ""),
9444
- aNext.calledAt || void 0
9504
+ aNext.calledAt || void 0,
9505
+ aNext.agentId || ""
9445
9506
  ));
9446
9507
  if (last.nodes.length > 1) {
9447
9508
  last.edges.push({ src: last.nodes.length - 2, dst: last.nodes.length - 1, kind: "next" });
@@ -9499,6 +9560,33 @@ var init_builder = __esm({
9499
9560
  });
9500
9561
 
9501
9562
  // src/engine/formal-engine/properties.ts
9563
+ function graphFromPath(nodes, path6, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
9564
+ const byKey = /* @__PURE__ */ new Map();
9565
+ for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
9566
+ const gNodes = [];
9567
+ const gEdges = [];
9568
+ for (const item of path6) {
9569
+ const n = byKey.get(`${item.sessionId}#${item.seq}`);
9570
+ gNodes.push({
9571
+ session_id: item.sessionId,
9572
+ seq: item.seq,
9573
+ stage: n ? n.stage : "",
9574
+ action_detail: n ? n.actionDetail : item.detail,
9575
+ resources: n ? [...n.resources].sort() : []
9576
+ });
9577
+ if (gNodes.length > 1) {
9578
+ gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
9579
+ }
9580
+ }
9581
+ (roles || []).forEach((role, i) => {
9582
+ if (i < gNodes.length && role) gNodes[i].role = role;
9583
+ });
9584
+ if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9585
+ const out = { nodes: gNodes, edges: gEdges };
9586
+ if (propertyMeta) Object.assign(out, propertyMeta);
9587
+ if (propertyExpression) out.property_expression = propertyExpression;
9588
+ return out;
9589
+ }
9502
9590
  function parseTs(ts) {
9503
9591
  if (!ts) return null;
9504
9592
  const t = Date.parse(String(ts).trim());
@@ -9556,7 +9644,12 @@ function checkCrossSessionChain(graph) {
9556
9644
  propertyId: "cross-" + chain.id,
9557
9645
  reason: chain.reason,
9558
9646
  riskLevel: chain.riskLevel,
9559
- path: path6
9647
+ path: path6,
9648
+ graph: graphFromPath(nodes, path6, "next", {
9649
+ property_id: "cross-" + chain.id,
9650
+ property_short: "P1",
9651
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9652
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9560
9653
  });
9561
9654
  return violations;
9562
9655
  } else {
@@ -9576,7 +9669,12 @@ function checkCrossSessionChain(graph) {
9576
9669
  propertyId: "cross-" + chain.id,
9577
9670
  reason: chain.reason,
9578
9671
  riskLevel: chain.riskLevel,
9579
- path: path6
9672
+ path: path6,
9673
+ graph: graphFromPath(nodes, path6, "next", {
9674
+ property_id: "cross-" + chain.id,
9675
+ property_short: "P1",
9676
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9677
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9580
9678
  });
9581
9679
  }
9582
9680
  }
@@ -9591,10 +9689,15 @@ function checkSensitiveDataFlow(graph) {
9591
9689
  for (let i = 0; i < nodes.length; i++) {
9592
9690
  const n = nodes[i];
9593
9691
  if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
9594
- tainted.set(i, `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`);
9692
+ tainted.set(i, {
9693
+ source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
9694
+ parent: null
9695
+ });
9595
9696
  }
9596
9697
  }
9597
9698
  if (tainted.size === 0) return violations;
9699
+ const taintEdges = /* @__PURE__ */ new Map();
9700
+ const edgeKey = (p, c) => `${p}:${c}`;
9598
9701
  const edges = allEdges(graph);
9599
9702
  let changed = true;
9600
9703
  while (changed) {
@@ -9605,7 +9708,8 @@ function checkSensitiveDataFlow(graph) {
9605
9708
  const dstRes = nodes[e.dst].resources;
9606
9709
  for (const r of srcRes) {
9607
9710
  if (dstRes.has(r)) {
9608
- tainted.set(e.dst, tainted.get(e.src));
9711
+ tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
9712
+ taintEdges.set(edgeKey(e.src, e.dst), r);
9609
9713
  changed = true;
9610
9714
  break;
9611
9715
  }
@@ -9613,16 +9717,75 @@ function checkSensitiveDataFlow(graph) {
9613
9717
  }
9614
9718
  }
9615
9719
  }
9720
+ const tracePath = (idx) => {
9721
+ const chain = [idx];
9722
+ const seen = /* @__PURE__ */ new Set([idx]);
9723
+ while (true) {
9724
+ const parent = tainted.get(chain[chain.length - 1])?.parent;
9725
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
9726
+ chain.push(parent);
9727
+ seen.add(parent);
9728
+ }
9729
+ return chain.reverse();
9730
+ };
9731
+ const taintGraph = (chain) => {
9732
+ const gNodes = [];
9733
+ const gEdges = [];
9734
+ chain.forEach((ni, k) => {
9735
+ const n = nodes[ni];
9736
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
9737
+ gNodes.push({
9738
+ session_id: n.sessionId,
9739
+ seq: n.seq,
9740
+ stage: n.stage,
9741
+ action_detail: n.actionDetail,
9742
+ resources: [...n.resources].sort(),
9743
+ role
9744
+ });
9745
+ if (k > 0) {
9746
+ const prev = chain[k - 1];
9747
+ gEdges.push({
9748
+ from: k - 1,
9749
+ to: k,
9750
+ type: "data",
9751
+ label: taintEdges.get(edgeKey(prev, ni)) || ""
9752
+ });
9753
+ }
9754
+ });
9755
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9756
+ const expr = [{ token: "CRED", node_index: 0 }];
9757
+ if (chain.length > 2) {
9758
+ expr.push({
9759
+ token: "\u5171\u4EAB\u8F7D\u4F53",
9760
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
9761
+ });
9762
+ }
9763
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
9764
+ return {
9765
+ property_id: "sensitive-data-flow",
9766
+ property_short: "P2",
9767
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
9768
+ property_expression: expr,
9769
+ nodes: gNodes,
9770
+ edges: gEdges
9771
+ };
9772
+ };
9616
9773
  const witness = graph.witnessIdx;
9617
9774
  const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
9618
9775
  for (const i of candidates) {
9619
9776
  const n = nodes[i];
9620
9777
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
9778
+ const chain = tracePath(i);
9621
9779
  violations.push({
9622
9780
  propertyId: "sensitive-data-flow",
9623
- reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${tainted.get(i)} \u2192 ${n.sessionId}#${n.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
9781
+ reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${tainted.get(i).source} \u2192 ${n.sessionId}#${n.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
9624
9782
  riskLevel: "critical",
9625
- path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
9783
+ path: chain.map((j) => ({
9784
+ sessionId: nodes[j].sessionId,
9785
+ seq: nodes[j].seq,
9786
+ detail: nodes[j].actionDetail.slice(0, 60)
9787
+ })),
9788
+ graph: taintGraph(chain)
9626
9789
  });
9627
9790
  if (witness !== void 0) return violations;
9628
9791
  }
@@ -10164,6 +10327,12 @@ var init_stateful = __esm({
10164
10327
  nodes_ = [];
10165
10328
  ptr = {};
10166
10329
  tainted = /* @__PURE__ */ new Set();
10330
+ /** taint 传播路径:nodeIdx -> 使该节点染毒的前驱 nodeIdx(CRED 源为 null) */
10331
+ taintParent = /* @__PURE__ */ new Map();
10332
+ /** "parent:child" -> 触发传播的共享资源(图上的 data 边标签) */
10333
+ taintEdgeLabel = /* @__PURE__ */ new Map();
10334
+ /** 资源 -> 首次染毒该资源的节点 seq */
10335
+ taintOrigin = /* @__PURE__ */ new Map();
10167
10336
  /** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
10168
10337
  taintedResources = /* @__PURE__ */ new Set();
10169
10338
  aliveResources = {};
@@ -10228,8 +10397,8 @@ var init_stateful = __esm({
10228
10397
  }
10229
10398
  /** 增量推进:把当前动作追加到时间线 → 链指针推进 → 判定。 */
10230
10399
  advance(args) {
10231
- const { aNext, sessionId = "current", isError = false } = args;
10232
- const node = this.appendNode(aNext, sessionId, isError);
10400
+ const { aNext, sessionId = "current", isError = false, agentId = "" } = args;
10401
+ const node = this.appendNode(aNext, sessionId, isError, agentId);
10233
10402
  if (!node) {
10234
10403
  return { seq: this.seqNext, node: null, matched: null, pendingPrefixes: this.pendingPrefixes, exfil: [], shouldBlock: false };
10235
10404
  }
@@ -10249,7 +10418,7 @@ var init_stateful = __esm({
10249
10418
  }
10250
10419
  };
10251
10420
  }
10252
- appendNode(action, sessionId, isError = false) {
10421
+ appendNode(action, sessionId, isError = false, agentId = "") {
10253
10422
  const ad = String(action.actionDetail || "");
10254
10423
  const at = String(action.actionType || "");
10255
10424
  if (!ad) return null;
@@ -10266,6 +10435,7 @@ var init_stateful = __esm({
10266
10435
  resources: extractResources2(ad),
10267
10436
  instances: extractInstances(ad, stage),
10268
10437
  isError,
10438
+ agentId: String(action.agentId || agentId || ""),
10269
10439
  lifecycle: lifecycleFor(ad, stage, isError),
10270
10440
  timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
10271
10441
  };
@@ -10373,7 +10543,8 @@ var init_stateful = __esm({
10373
10543
  stages,
10374
10544
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
10375
10545
  reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
10376
- path: this.chainPath(stages)
10546
+ path: this.chainPath(stages),
10547
+ graph: this.buildChainGraph(stages, cid)
10377
10548
  };
10378
10549
  }
10379
10550
  }
@@ -10403,7 +10574,13 @@ var init_stateful = __esm({
10403
10574
  let want = stages[0];
10404
10575
  for (const n of this.nodes_) {
10405
10576
  if (n.stage === want) {
10406
- path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage, timestamp: n.timestamp });
10577
+ path6.push({
10578
+ sessionId: n.sessionId,
10579
+ seq: n.seq,
10580
+ stage: n.stage,
10581
+ timestamp: n.timestamp,
10582
+ agentId: n.agentId
10583
+ });
10407
10584
  const idx = stages.indexOf(n.stage);
10408
10585
  if (idx + 1 < stages.length) {
10409
10586
  want = stages[idx + 1];
@@ -10414,21 +10591,69 @@ var init_stateful = __esm({
10414
10591
  }
10415
10592
  return path6;
10416
10593
  }
10594
+ /** 把链路径转成前端可画的结构化图(相邻节点 next 边)。 */
10595
+ buildChainGraph(stages, chainId) {
10596
+ const path6 = this.chainPath(stages);
10597
+ const gNodes = path6.map((p, i) => {
10598
+ const n = this.nodes_.find((x) => x.sessionId === p.sessionId && x.seq === p.seq);
10599
+ return {
10600
+ session_id: p.sessionId,
10601
+ seq: p.seq,
10602
+ stage: p.stage,
10603
+ action_detail: n ? n.actionDetail : "",
10604
+ resources: n ? [...n.resources].sort() : [],
10605
+ role: stages[i] || ""
10606
+ };
10607
+ });
10608
+ const gEdges = [];
10609
+ for (let i = 1; i < gNodes.length; i++) {
10610
+ gEdges.push({ from: i - 1, to: i, type: "next" });
10611
+ }
10612
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10613
+ return {
10614
+ property_id: "cross-" + chainId,
10615
+ property_short: "P1",
10616
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE",
10617
+ property_expression: stages.map((s, i) => ({ token: s, node_index: i })),
10618
+ nodes: gNodes,
10619
+ edges: gEdges
10620
+ };
10621
+ }
10417
10622
  // ---------------- 敏感数据 taint(P2) ----------------
10418
10623
  advanceTaint(node) {
10419
10624
  if (node.lifecycle === "dead") return;
10420
10625
  const idx = node.seq;
10421
10626
  const taintedRes = /* @__PURE__ */ new Set();
10627
+ let parent = null;
10628
+ let label = "";
10629
+ let foundParent = false;
10422
10630
  for (const r of node.resources) {
10423
- if (this.resourceTainted(r)) taintedRes.add(r);
10631
+ if (this.resourceTainted(r)) {
10632
+ taintedRes.add(r);
10633
+ if (!foundParent) {
10634
+ const origin = this.taintOrigin.get(r);
10635
+ if (origin !== void 0) {
10636
+ parent = origin;
10637
+ label = r;
10638
+ foundParent = true;
10639
+ }
10640
+ }
10641
+ }
10424
10642
  }
10425
10643
  if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
10426
10644
  this.tainted.add(idx);
10645
+ parent = null;
10646
+ label = "";
10427
10647
  for (const r of node.resources) taintedRes.add(r);
10428
10648
  }
10429
10649
  if (taintedRes.size > 0) {
10430
10650
  this.tainted.add(idx);
10431
- for (const r of taintedRes) this.taintedResources.add(r);
10651
+ this.taintParent.set(idx, parent);
10652
+ if (parent !== null) this.taintEdgeLabel.set(`${parent}:${idx}`, label);
10653
+ for (const r of node.resources) {
10654
+ this.taintedResources.add(r);
10655
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, idx);
10656
+ }
10432
10657
  }
10433
10658
  }
10434
10659
  resourceTainted(resource) {
@@ -10438,16 +10663,84 @@ var init_stateful = __esm({
10438
10663
  checkExfilAfterTaint(node) {
10439
10664
  if (!this.tainted.has(node.seq)) return [];
10440
10665
  if (EXFIL_RE2.test(node.actionDetail)) {
10666
+ const chain = this.traceTaintChain(node.seq);
10441
10667
  return [{
10442
10668
  property_id: "sensitive-data-flow",
10443
- reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${this.taintSource(node)} \u2192 ${node.sessionId}#${node.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
10669
+ reason: `\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20\uFF1A${this.taintSource(node, chain)} \u2192 ${node.sessionId}#${node.seq} \u7ECF\u5171\u4EAB\u8F7D\u4F53\u5916\u53D1`,
10444
10670
  risk_level: "critical",
10445
- path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
10671
+ path: chain.map((j) => {
10672
+ const n = this.nodes_[j];
10673
+ return [n.sessionId, n.seq, n.actionDetail.slice(0, 60), n.timestamp];
10674
+ }),
10675
+ graph: this.taintGraph(chain)
10446
10676
  }];
10447
10677
  }
10448
10678
  return [];
10449
10679
  }
10450
- taintSource(node) {
10680
+ /** 沿 taint parent 回溯出 CRED 源 → 当前节点的完整路径。 */
10681
+ traceTaintChain(idx) {
10682
+ const chain = [idx];
10683
+ const seen = /* @__PURE__ */ new Set([idx]);
10684
+ while (true) {
10685
+ const parent = this.taintParent.get(chain[chain.length - 1]);
10686
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
10687
+ chain.push(parent);
10688
+ seen.add(parent);
10689
+ }
10690
+ return chain.reverse();
10691
+ }
10692
+ /** 把 taint 路径转成前端可画的结构化图(data 边带共享资源标签)。 */
10693
+ taintGraph(chain) {
10694
+ const gNodes = [];
10695
+ const gEdges = [];
10696
+ chain.forEach((ni, k) => {
10697
+ const n = this.nodes_[ni];
10698
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
10699
+ gNodes.push({
10700
+ session_id: n.sessionId,
10701
+ seq: n.seq,
10702
+ stage: n.stage,
10703
+ action_detail: n.actionDetail,
10704
+ resources: [...n.resources].sort(),
10705
+ role
10706
+ });
10707
+ if (k > 0) {
10708
+ const prev = chain[k - 1];
10709
+ gEdges.push({
10710
+ from: k - 1,
10711
+ to: k,
10712
+ type: "data",
10713
+ label: this.taintEdgeLabel.get(`${prev}:${ni}`) || ""
10714
+ });
10715
+ }
10716
+ });
10717
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10718
+ const expr = [{ token: "CRED", node_index: 0 }];
10719
+ if (chain.length > 2) {
10720
+ expr.push({
10721
+ token: "\u5171\u4EAB\u8F7D\u4F53",
10722
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
10723
+ });
10724
+ }
10725
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
10726
+ return {
10727
+ property_id: "sensitive-data-flow",
10728
+ property_short: "P2",
10729
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
10730
+ property_expression: expr,
10731
+ nodes: gNodes,
10732
+ edges: gEdges
10733
+ };
10734
+ }
10735
+ taintSource(node, chain = []) {
10736
+ if (chain.length > 0) {
10737
+ const src = this.nodes_[chain[0]];
10738
+ if (src) {
10739
+ for (const r of node.resources) {
10740
+ if (src.resources.includes(r)) return `${src.sessionId}#${src.seq} \u8BBF\u95EE ${r}`;
10741
+ }
10742
+ }
10743
+ }
10451
10744
  const sorted = [...this.tainted].sort((a, b) => a - b);
10452
10745
  for (const i of sorted) {
10453
10746
  if (i === node.seq) continue;
@@ -10466,6 +10759,9 @@ var init_stateful = __esm({
10466
10759
  nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
10467
10760
  ptrs: { ...this.ptr },
10468
10761
  tainted: [...this.tainted].sort((a, b) => a - b),
10762
+ // taint 传播路径(前端攻击链图);边标签不入快照,重启后可从资源交集重建
10763
+ taint_parent: Object.fromEntries([...this.taintParent.entries()].map(([k, v]) => [String(k), v])),
10764
+ taint_origin: Object.fromEntries([...this.taintOrigin.entries()]),
10469
10765
  alive: { ...this.aliveResources },
10470
10766
  seq_next: this.seqNext
10471
10767
  };
@@ -10477,6 +10773,7 @@ var init_stateful = __esm({
10477
10773
  this.nodes_.push({
10478
10774
  seq: Number(raw.seq || 0),
10479
10775
  sessionId: String(raw.sessionId || ""),
10776
+ agentId: String(raw.agentId || ""),
10480
10777
  actionType: String(raw.actionType || ""),
10481
10778
  actionDetail: String(raw.actionDetail || ""),
10482
10779
  stage: String(raw.stage || ""),
@@ -10492,15 +10789,53 @@ var init_stateful = __esm({
10492
10789
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
10493
10790
  this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
10494
10791
  this.tainted = new Set((snapshot.tainted || []).map(Number));
10792
+ this.taintParent = new Map(
10793
+ Object.entries(snapshot.taint_parent || {}).map(
10794
+ ([k, v]) => [Number(k), v === null || v === void 0 ? null : Number(v)]
10795
+ )
10796
+ );
10797
+ this.taintOrigin = new Map(
10798
+ Object.entries(snapshot.taint_origin || {}).map(
10799
+ ([k, v]) => [String(k), Number(v)]
10800
+ )
10801
+ );
10802
+ this.taintEdgeLabel = /* @__PURE__ */ new Map();
10495
10803
  this.taintedResources = /* @__PURE__ */ new Set();
10496
10804
  for (const i of this.tainted) {
10497
10805
  const n = this.nodes_[i];
10498
10806
  if (n) for (const r of n.resources) this.taintedResources.add(r);
10499
10807
  }
10808
+ if (this.tainted.size > 0 && this.taintParent.size === 0) this.rebuildTaintParent();
10809
+ if (this.taintOrigin.size === 0) {
10810
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10811
+ const n = this.nodes_[i];
10812
+ if (!n) continue;
10813
+ for (const r of n.resources) {
10814
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, i);
10815
+ }
10816
+ }
10817
+ }
10500
10818
  this.aliveResources = Object.fromEntries(
10501
10819
  Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
10502
10820
  );
10503
10821
  }
10822
+ /** 旧快照无 parent 信息时,按 seq 顺序找最近的资源相交染毒前驱。 */
10823
+ rebuildTaintParent() {
10824
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10825
+ const n = this.nodes_[i];
10826
+ if (!n) continue;
10827
+ let parent = null;
10828
+ for (let j = i - 1; j >= 0; j--) {
10829
+ if (!this.tainted.has(j)) continue;
10830
+ const m = this.nodes_[j];
10831
+ if (m && m.resources.some((r) => n.resources.includes(r))) {
10832
+ parent = j;
10833
+ break;
10834
+ }
10835
+ }
10836
+ this.taintParent.set(i, parent);
10837
+ }
10838
+ }
10504
10839
  };
10505
10840
  }
10506
10841
  });
@@ -10583,9 +10918,9 @@ var init_chain_state = __esm({
10583
10918
  delete this.data[agentId];
10584
10919
  this._scheduleWrite();
10585
10920
  }
10586
- applyAction(agentId, aNext, sessionId, isError = false) {
10921
+ applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "") {
10587
10922
  const t = this.load(agentId) || new StatefulChainTracker(agentId);
10588
- const r = t.advance({ aNext, sessionId, isError });
10923
+ const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent });
10589
10924
  this.save(t);
10590
10925
  return r;
10591
10926
  }
@@ -10900,10 +11235,14 @@ async function runAnalysis(input) {
10900
11235
  const formalSessions = [];
10901
11236
  if (input.sessionHistory) {
10902
11237
  for (const s of input.sessionHistory) {
10903
- formalSessions.push({ sessionId: s.sessionId || "s", actionHistory: s.actionHistory || [] });
11238
+ formalSessions.push({
11239
+ sessionId: s.sessionId || "s",
11240
+ ...s.agentId ? { agentId: s.agentId } : {},
11241
+ actionHistory: s.actionHistory || []
11242
+ });
10904
11243
  }
10905
11244
  }
10906
- formalSessions.push({ sessionId: "current", actionHistory: input.actionHistory });
11245
+ formalSessions.push({ sessionId: "current", agentId: input.agentId, actionHistory: input.actionHistory });
10907
11246
  const formalResult = formalEngine.verify(formalSessions, input.aNext);
10908
11247
  if (!formalResult.verified) {
10909
11248
  const v = formalResult.violations[0];
@@ -10920,7 +11259,9 @@ async function runAnalysis(input) {
10920
11259
  riskType: "formal_verification",
10921
11260
  confidence: 90,
10922
11261
  source: "formal_engine",
10923
- violatedLayers: ["L2.2"]
11262
+ violatedLayers: ["L2.2"],
11263
+ violationGraph: v?.graph ?? null,
11264
+ violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
10924
11265
  };
10925
11266
  }
10926
11267
  }
@@ -11465,7 +11806,9 @@ async function analyzeInner(input, mossLog) {
11465
11806
  violated_layers: judgment.violatedLayers || [],
11466
11807
  policy: "",
11467
11808
  violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
11468
- confidence: judgment.confidence
11809
+ confidence: judgment.confidence,
11810
+ violation_graph: judgment.violationGraph ?? null,
11811
+ violation_chain: judgment.violationChain ?? []
11469
11812
  };
11470
11813
  mossLog.logResult("Deny", denied.reason);
11471
11814
  return denied;
@@ -11999,6 +12342,8 @@ async function analyzeRoute(c) {
11999
12342
  })),
12000
12343
  sessionHistory: (req.session_history || []).map((s) => ({
12001
12344
  sessionId: s.session_id,
12345
+ // 来源 agent(AgentGate 逐会话带上,用于链路径取证展示;不参与链匹配)
12346
+ ...s.agent_id ? { agentId: s.agent_id } : {},
12002
12347
  actionHistory: (s.action_history || []).map((h) => ({
12003
12348
  actionType: h.action_type,
12004
12349
  actionDetail: h.action_detail,
@@ -12057,10 +12402,14 @@ async function analyzeRoute(c) {
12057
12402
  if (incEnabled === "1" || incEnabled === "true" || incEnabled === "yes" || incEnabled === "on") {
12058
12403
  try {
12059
12404
  const store = getChainStateStoreFor(agentId);
12060
- const inc = store.applyAction(agentId, {
12061
- actionType: req.a_next.action_type,
12062
- actionDetail: req.a_next.action_detail
12063
- }, req.session_id, Boolean(meta.is_error));
12405
+ const inc = store.applyAction(
12406
+ chainBucket(agentId),
12407
+ { actionType: req.a_next.action_type, actionDetail: req.a_next.action_detail },
12408
+ req.session_id,
12409
+ Boolean(meta.is_error),
12410
+ agentId
12411
+ // 来源:真实 agent(桶 key 可能是 __system__)
12412
+ );
12064
12413
  const judgment = incMatchedToDeny(inc);
12065
12414
  if (judgment) {
12066
12415
  const layers = [...result.violated_layers];
@@ -12080,7 +12429,8 @@ async function analyzeRoute(c) {
12080
12429
  confidence: 90,
12081
12430
  analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
12082
12431
  // 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
12083
- violation_chain: redactChain(judgment.violation_chain, req.session_id)
12432
+ violation_chain: redactChain(judgment.violation_chain, req.session_id),
12433
+ violation_graph: redactGraph(judgment.violation_graph, req.session_id)
12084
12434
  })
12085
12435
  );
12086
12436
  }
@@ -12100,7 +12450,9 @@ async function analyzeRoute(c) {
12100
12450
  policy: result.policy,
12101
12451
  violated_policy: result.violated_policy,
12102
12452
  confidence: result.confidence,
12103
- analysis_duration_ms: Math.round(durationMs * 10) / 10
12453
+ analysis_duration_ms: Math.round(durationMs * 10) / 10,
12454
+ violation_chain: result.violation_chain ?? [],
12455
+ violation_graph: redactGraph(result.violation_graph, req.session_id)
12104
12456
  })
12105
12457
  );
12106
12458
  } catch (err) {
@@ -12125,12 +12477,13 @@ function getChainStateStoreFor(_agentId) {
12125
12477
  }
12126
12478
  function incMatchedToDeny(inc) {
12127
12479
  if (inc.matched) {
12128
- const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
12480
+ const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp, p.agentId ?? null]);
12129
12481
  return {
12130
12482
  property_id: "cross-" + inc.matched.chainId,
12131
12483
  reason: inc.matched.reason,
12132
12484
  risk_level: inc.matched.riskLevel,
12133
- violation_chain: chain
12485
+ violation_chain: chain,
12486
+ violation_graph: inc.matched.graph ?? null
12134
12487
  };
12135
12488
  }
12136
12489
  if (inc.exfil && inc.exfil.length > 0) {
@@ -12138,7 +12491,9 @@ function incMatchedToDeny(inc) {
12138
12491
  property_id: "sensitive-data-flow",
12139
12492
  reason: inc.exfil[0].reason,
12140
12493
  risk_level: inc.exfil[0].risk_level,
12141
- violation_chain: []
12494
+ // P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
12495
+ violation_chain: inc.exfil[0].path ?? [],
12496
+ violation_graph: inc.exfil[0].graph ?? null
12142
12497
  };
12143
12498
  }
12144
12499
  return null;
@@ -12150,9 +12505,10 @@ async function reviewRoute(c) {
12150
12505
  const req = ReviewRequestSchema.parse(body);
12151
12506
  const agentId = req.agent_id || "default";
12152
12507
  const store = getChainStateStoreFor(agentId);
12153
- let tracker = store.load(agentId);
12508
+ const bucket = chainBucket(agentId);
12509
+ let tracker = store.load(bucket);
12154
12510
  if (!tracker) {
12155
- tracker = new StatefulChainTracker(agentId);
12511
+ tracker = new StatefulChainTracker(bucket);
12156
12512
  }
12157
12513
  if (req.db_path && !pathWithin(req.db_path, reviewRoots())) {
12158
12514
  return c.json({