@kenhkl/agent-moss 0.17.7 → 0.17.10

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
@@ -1246,18 +1246,43 @@ function hardenExistingPaths() {
1246
1246
  function chainDetailMode() {
1247
1247
  return (env("AGENT_MOSS_CHAIN_DETAIL", "redacted") || "redacted").toLowerCase();
1248
1248
  }
1249
- function redactChain(chain, currentSessionId) {
1249
+ function redactChain(chain, currentInstanceId = "", currentSessionId = "") {
1250
1250
  if (!chain || chain.length === 0) return [];
1251
1251
  if (chainDetailMode() === "full") return [...chain];
1252
1252
  return chain.map((step) => {
1253
1253
  if (Array.isArray(step) && step.length >= 3) {
1254
1254
  const [sid, seq, , ts] = step;
1255
- if (String(sid) === String(currentSessionId)) return [...step];
1255
+ const inst = step.length > 5 ? String(step[5] ?? "") : "";
1256
+ const sameInstance = !!(inst && currentInstanceId && inst === String(currentInstanceId));
1257
+ const sameSessionFallback = !inst && !!currentSessionId && String(sid) === String(currentSessionId);
1258
+ if (sameInstance || sameSessionFallback) return [...step];
1256
1259
  return ["<other-session>", seq, "<\u5DF2\u9690\u85CF>", ts ?? ""];
1257
1260
  }
1258
1261
  return step;
1259
1262
  });
1260
1263
  }
1264
+ function redactGraph(graph, currentInstanceId = "", currentSessionId = "") {
1265
+ if (!graph) return null;
1266
+ if (chainDetailMode() === "full") return graph;
1267
+ const nodes = (graph.nodes || []).map((n) => {
1268
+ const m = { ...n };
1269
+ const inst = String(m.instance_id ?? "") || "";
1270
+ const sid = String(m.session_id ?? "") || "";
1271
+ const sameInstance = !!(inst && currentInstanceId && inst === String(currentInstanceId));
1272
+ const sameSessionFallback = !inst && !!currentSessionId && sid === String(currentSessionId);
1273
+ if (!(sameInstance || sameSessionFallback)) {
1274
+ m.session_id = "<other-session>";
1275
+ m.action_detail = "<\u5DF2\u9690\u85CF>";
1276
+ m.resources = [];
1277
+ }
1278
+ return m;
1279
+ });
1280
+ const out = { nodes, edges: graph.edges || [] };
1281
+ for (const key of ["property_id", "property_short", "property_name", "property_expression"]) {
1282
+ if (graph[key] !== void 0) out[key] = graph[key];
1283
+ }
1284
+ return out;
1285
+ }
1261
1286
  function sessionSweepThreshold() {
1262
1287
  return envInt("AGENT_MOSS_SESSION_SWEEP_THRESHOLD", 50);
1263
1288
  }
@@ -1419,6 +1444,8 @@ var init_models = __esm({
1419
1444
  session_id: z.string(),
1420
1445
  /** 来源 agent(AgentGate 逐会话带上):记录在链路径里供告警/取证,不参与链匹配。 */
1421
1446
  agent_id: z.string().optional(),
1447
+ /** 来源实例:仅用于展示层脱敏(同实例不隐藏),不参与链匹配。 */
1448
+ instance_id: z.string().optional(),
1422
1449
  action_history: z.array(ActionItemSchema).default([])
1423
1450
  })).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)."),
1424
1451
  a_next: ActionItemSchema.describe("Next action to be executed"),
@@ -1426,6 +1453,7 @@ var init_models = __esm({
1426
1453
  os_type: z.string().default("").describe("OS type: 'linux', 'windows', or empty for auto-detect"),
1427
1454
  cwd: z.string().default("").describe("Current working directory"),
1428
1455
  agent_id: z.string().default("").describe("Caller agent identifier, e.g. 'xiaoo' / 'opencode' / 'opendesk'. Empty = generic mode (no agent-specific rules)."),
1456
+ instance_id: z.string().default("").describe("Agent installation/instance identifier. Used only for display-layer redaction: nodes from the same instance are returned unredacted. Does not affect detection."),
1429
1457
  metadata: z.record(z.unknown()).optional().describe("Extended metadata")
1430
1458
  });
1431
1459
  AnalyzeResponseSchema = z.object({
@@ -1439,7 +1467,44 @@ var init_models = __esm({
1439
1467
  confidence: z.number().int().min(0).max(100).default(100),
1440
1468
  analysis_duration_ms: z.number().default(0),
1441
1469
  // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
1442
- violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
1470
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([]),
1471
+ // 结构化攻击链图(前端可视化):节点 + 边(next 时序 / data 数据流)
1472
+ violation_graph: z.object({
1473
+ // 性质元信息(前端展示"违反了哪条性质")
1474
+ property_id: z.string().optional(),
1475
+ property_short: z.string().optional(),
1476
+ property_name: z.string().optional(),
1477
+ // 性质表达式 token → 节点位置(前端展示"表达式对应哪个节点")
1478
+ property_expression: z.array(
1479
+ z.object({
1480
+ token: z.string(),
1481
+ node_index: z.union([z.number(), z.array(z.number())])
1482
+ })
1483
+ ).optional(),
1484
+ nodes: z.array(
1485
+ z.object({
1486
+ session_id: z.string().default(""),
1487
+ seq: z.number().default(0),
1488
+ stage: z.string().default(""),
1489
+ action_detail: z.string().default(""),
1490
+ resources: z.array(z.string()).default([]),
1491
+ // 节点在性质中的角色:source/carrier/sink(P2)或阶段名(P1)
1492
+ role: z.string().optional(),
1493
+ // 是否为违例见证节点(当前动作补全链 / 触发外传)
1494
+ witness: z.boolean().optional(),
1495
+ // 来源实例(仅展示层脱敏:同实例不隐藏)
1496
+ instance_id: z.string().optional()
1497
+ })
1498
+ ),
1499
+ edges: z.array(
1500
+ z.object({
1501
+ from: z.number(),
1502
+ to: z.number(),
1503
+ type: z.enum(["next", "data"]).default("next"),
1504
+ label: z.string().optional()
1505
+ })
1506
+ )
1507
+ }).nullable().default(null)
1443
1508
  });
1444
1509
  HealthResponseSchema = z.object({
1445
1510
  status: z.string().default("healthy"),
@@ -9387,7 +9452,7 @@ function markIntegrity(actionDetail) {
9387
9452
  }
9388
9453
  return { integrity: "medium", isLowSource: false };
9389
9454
  }
9390
- function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "") {
9455
+ function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "", instanceId = "") {
9391
9456
  const { integrity, isLowSource } = markIntegrity(actionDetail);
9392
9457
  return {
9393
9458
  sessionId,
@@ -9396,13 +9461,14 @@ function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId =
9396
9461
  actionDetail,
9397
9462
  stage: markStage(actionDetail),
9398
9463
  agentId: agentId || void 0,
9464
+ instanceId: instanceId || void 0,
9399
9465
  resources: extractResources(actionDetail),
9400
9466
  integrity,
9401
9467
  isLowSource,
9402
9468
  timestamp: timestamp || void 0
9403
9469
  };
9404
9470
  }
9405
- function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "") {
9471
+ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "", instanceId = "") {
9406
9472
  const nodes = [];
9407
9473
  const edges = [];
9408
9474
  const stageFlags = /* @__PURE__ */ new Set();
@@ -9413,7 +9479,8 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId
9413
9479
  actionType: String(a.actionType || ""),
9414
9480
  actionDetail: ad,
9415
9481
  calledAt: a.calledAt || void 0,
9416
- agentId: a.agentId || agentId || void 0
9482
+ agentId: a.agentId || agentId || void 0,
9483
+ instanceId: a.instanceId || instanceId || void 0
9417
9484
  });
9418
9485
  }
9419
9486
  if (aNext) {
@@ -9421,11 +9488,12 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId
9421
9488
  actionType: String(aNext.actionType || ""),
9422
9489
  actionDetail: String(aNext.actionDetail || ""),
9423
9490
  calledAt: aNext.calledAt || void 0,
9424
- agentId: aNext.agentId || agentId || void 0
9491
+ agentId: aNext.agentId || agentId || void 0,
9492
+ instanceId: aNext.instanceId || instanceId || void 0
9425
9493
  });
9426
9494
  }
9427
9495
  details.forEach((d, seq) => {
9428
- const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "");
9496
+ const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "", d.instanceId || "");
9429
9497
  nodes.push(node);
9430
9498
  if (node.stage) stageFlags.add(node.stage);
9431
9499
  });
@@ -9437,12 +9505,18 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId
9437
9505
  function buildMultiSessionGraph(sessions, aNext) {
9438
9506
  const mg = { sessions: [], crossEdges: [] };
9439
9507
  if (!sessions || sessions.length === 0) {
9440
- mg.sessions.push(buildSessionGraph([], aNext, "current"));
9508
+ mg.sessions.push(buildSessionGraph([], aNext, "current", "", aNext?.instanceId || ""));
9441
9509
  return mg;
9442
9510
  }
9443
9511
  for (const s of sessions) {
9444
9512
  const sid = String(s.sessionId || "s");
9445
- mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid, s.agentId || ""));
9513
+ mg.sessions.push(buildSessionGraph(
9514
+ s.actionHistory || [],
9515
+ void 0,
9516
+ sid,
9517
+ s.agentId || "",
9518
+ s.instanceId || ""
9519
+ ));
9446
9520
  }
9447
9521
  if (aNext) {
9448
9522
  const last = mg.sessions[mg.sessions.length - 1];
@@ -9453,13 +9527,14 @@ function buildMultiSessionGraph(sessions, aNext) {
9453
9527
  String(aNext.actionType || ""),
9454
9528
  String(aNext.actionDetail || ""),
9455
9529
  aNext.calledAt || void 0,
9456
- aNext.agentId || ""
9530
+ aNext.agentId || "",
9531
+ aNext.instanceId || ""
9457
9532
  ));
9458
9533
  if (last.nodes.length > 1) {
9459
9534
  last.edges.push({ src: last.nodes.length - 2, dst: last.nodes.length - 1, kind: "next" });
9460
9535
  }
9461
9536
  } else {
9462
- mg.sessions.push(buildSessionGraph([], aNext, "current"));
9537
+ mg.sessions.push(buildSessionGraph([], aNext, "current", "", aNext.instanceId || ""));
9463
9538
  }
9464
9539
  }
9465
9540
  addCrossDataEdges(mg);
@@ -9511,6 +9586,34 @@ var init_builder = __esm({
9511
9586
  });
9512
9587
 
9513
9588
  // src/engine/formal-engine/properties.ts
9589
+ function graphFromPath(nodes, path6, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
9590
+ const byKey = /* @__PURE__ */ new Map();
9591
+ for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
9592
+ const gNodes = [];
9593
+ const gEdges = [];
9594
+ for (const item of path6) {
9595
+ const n = byKey.get(`${item.sessionId}#${item.seq}`);
9596
+ gNodes.push({
9597
+ session_id: item.sessionId,
9598
+ seq: item.seq,
9599
+ stage: n ? n.stage : "",
9600
+ action_detail: n ? n.actionDetail : item.detail,
9601
+ resources: n ? [...n.resources].sort() : [],
9602
+ instance_id: n ? n.instanceId || "" : ""
9603
+ });
9604
+ if (gNodes.length > 1) {
9605
+ gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
9606
+ }
9607
+ }
9608
+ (roles || []).forEach((role, i) => {
9609
+ if (i < gNodes.length && role) gNodes[i].role = role;
9610
+ });
9611
+ if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9612
+ const out = { nodes: gNodes, edges: gEdges };
9613
+ if (propertyMeta) Object.assign(out, propertyMeta);
9614
+ if (propertyExpression) out.property_expression = propertyExpression;
9615
+ return out;
9616
+ }
9514
9617
  function parseTs(ts) {
9515
9618
  if (!ts) return null;
9516
9619
  const t = Date.parse(String(ts).trim());
@@ -9568,7 +9671,12 @@ function checkCrossSessionChain(graph) {
9568
9671
  propertyId: "cross-" + chain.id,
9569
9672
  reason: chain.reason,
9570
9673
  riskLevel: chain.riskLevel,
9571
- path: path6
9674
+ path: path6,
9675
+ graph: graphFromPath(nodes, path6, "next", {
9676
+ property_id: "cross-" + chain.id,
9677
+ property_short: "P1",
9678
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9679
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9572
9680
  });
9573
9681
  return violations;
9574
9682
  } else {
@@ -9588,7 +9696,12 @@ function checkCrossSessionChain(graph) {
9588
9696
  propertyId: "cross-" + chain.id,
9589
9697
  reason: chain.reason,
9590
9698
  riskLevel: chain.riskLevel,
9591
- path: path6
9699
+ path: path6,
9700
+ graph: graphFromPath(nodes, path6, "next", {
9701
+ property_id: "cross-" + chain.id,
9702
+ property_short: "P1",
9703
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9704
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9592
9705
  });
9593
9706
  }
9594
9707
  }
@@ -9603,10 +9716,15 @@ function checkSensitiveDataFlow(graph) {
9603
9716
  for (let i = 0; i < nodes.length; i++) {
9604
9717
  const n = nodes[i];
9605
9718
  if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
9606
- tainted.set(i, `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`);
9719
+ tainted.set(i, {
9720
+ source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
9721
+ parent: null
9722
+ });
9607
9723
  }
9608
9724
  }
9609
9725
  if (tainted.size === 0) return violations;
9726
+ const taintEdges = /* @__PURE__ */ new Map();
9727
+ const edgeKey = (p, c) => `${p}:${c}`;
9610
9728
  const edges = allEdges(graph);
9611
9729
  let changed = true;
9612
9730
  while (changed) {
@@ -9617,7 +9735,8 @@ function checkSensitiveDataFlow(graph) {
9617
9735
  const dstRes = nodes[e.dst].resources;
9618
9736
  for (const r of srcRes) {
9619
9737
  if (dstRes.has(r)) {
9620
- tainted.set(e.dst, tainted.get(e.src));
9738
+ tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
9739
+ taintEdges.set(edgeKey(e.src, e.dst), r);
9621
9740
  changed = true;
9622
9741
  break;
9623
9742
  }
@@ -9625,16 +9744,76 @@ function checkSensitiveDataFlow(graph) {
9625
9744
  }
9626
9745
  }
9627
9746
  }
9747
+ const tracePath = (idx) => {
9748
+ const chain = [idx];
9749
+ const seen = /* @__PURE__ */ new Set([idx]);
9750
+ while (true) {
9751
+ const parent = tainted.get(chain[chain.length - 1])?.parent;
9752
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
9753
+ chain.push(parent);
9754
+ seen.add(parent);
9755
+ }
9756
+ return chain.reverse();
9757
+ };
9758
+ const taintGraph = (chain) => {
9759
+ const gNodes = [];
9760
+ const gEdges = [];
9761
+ chain.forEach((ni, k) => {
9762
+ const n = nodes[ni];
9763
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
9764
+ gNodes.push({
9765
+ session_id: n.sessionId,
9766
+ seq: n.seq,
9767
+ stage: n.stage,
9768
+ action_detail: n.actionDetail,
9769
+ resources: [...n.resources].sort(),
9770
+ role,
9771
+ instance_id: n.instanceId || ""
9772
+ });
9773
+ if (k > 0) {
9774
+ const prev = chain[k - 1];
9775
+ gEdges.push({
9776
+ from: k - 1,
9777
+ to: k,
9778
+ type: "data",
9779
+ label: taintEdges.get(edgeKey(prev, ni)) || ""
9780
+ });
9781
+ }
9782
+ });
9783
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9784
+ const expr = [{ token: "CRED", node_index: 0 }];
9785
+ if (chain.length > 2) {
9786
+ expr.push({
9787
+ token: "\u5171\u4EAB\u8F7D\u4F53",
9788
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
9789
+ });
9790
+ }
9791
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
9792
+ return {
9793
+ property_id: "sensitive-data-flow",
9794
+ property_short: "P2",
9795
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
9796
+ property_expression: expr,
9797
+ nodes: gNodes,
9798
+ edges: gEdges
9799
+ };
9800
+ };
9628
9801
  const witness = graph.witnessIdx;
9629
9802
  const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
9630
9803
  for (const i of candidates) {
9631
9804
  const n = nodes[i];
9632
9805
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
9806
+ const chain = tracePath(i);
9633
9807
  violations.push({
9634
9808
  propertyId: "sensitive-data-flow",
9635
- 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`,
9809
+ 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`,
9636
9810
  riskLevel: "critical",
9637
- path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
9811
+ path: chain.map((j) => ({
9812
+ sessionId: nodes[j].sessionId,
9813
+ seq: nodes[j].seq,
9814
+ detail: nodes[j].actionDetail.slice(0, 60)
9815
+ })),
9816
+ graph: taintGraph(chain)
9638
9817
  });
9639
9818
  if (witness !== void 0) return violations;
9640
9819
  }
@@ -10176,6 +10355,12 @@ var init_stateful = __esm({
10176
10355
  nodes_ = [];
10177
10356
  ptr = {};
10178
10357
  tainted = /* @__PURE__ */ new Set();
10358
+ /** taint 传播路径:nodeIdx -> 使该节点染毒的前驱 nodeIdx(CRED 源为 null) */
10359
+ taintParent = /* @__PURE__ */ new Map();
10360
+ /** "parent:child" -> 触发传播的共享资源(图上的 data 边标签) */
10361
+ taintEdgeLabel = /* @__PURE__ */ new Map();
10362
+ /** 资源 -> 首次染毒该资源的节点 seq */
10363
+ taintOrigin = /* @__PURE__ */ new Map();
10179
10364
  /** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
10180
10365
  taintedResources = /* @__PURE__ */ new Set();
10181
10366
  aliveResources = {};
@@ -10226,7 +10411,7 @@ var init_stateful = __esm({
10226
10411
  for (const s of sessions || []) {
10227
10412
  const sid = String(s.sessionId || "s");
10228
10413
  for (const a of s.actionHistory || []) {
10229
- const node = this.appendNode(a, sid);
10414
+ const node = this.appendNode(a, sid, false, s.agentId || "", s.instanceId || "");
10230
10415
  if (!node) {
10231
10416
  continue;
10232
10417
  }
@@ -10240,8 +10425,8 @@ var init_stateful = __esm({
10240
10425
  }
10241
10426
  /** 增量推进:把当前动作追加到时间线 → 链指针推进 → 判定。 */
10242
10427
  advance(args) {
10243
- const { aNext, sessionId = "current", isError = false, agentId = "" } = args;
10244
- const node = this.appendNode(aNext, sessionId, isError, agentId);
10428
+ const { aNext, sessionId = "current", isError = false, agentId = "", instanceId = "" } = args;
10429
+ const node = this.appendNode(aNext, sessionId, isError, agentId, instanceId);
10245
10430
  if (!node) {
10246
10431
  return { seq: this.seqNext, node: null, matched: null, pendingPrefixes: this.pendingPrefixes, exfil: [], shouldBlock: false };
10247
10432
  }
@@ -10261,7 +10446,7 @@ var init_stateful = __esm({
10261
10446
  }
10262
10447
  };
10263
10448
  }
10264
- appendNode(action, sessionId, isError = false, agentId = "") {
10449
+ appendNode(action, sessionId, isError = false, agentId = "", instanceId = "") {
10265
10450
  const ad = String(action.actionDetail || "");
10266
10451
  const at = String(action.actionType || "");
10267
10452
  if (!ad) return null;
@@ -10279,6 +10464,7 @@ var init_stateful = __esm({
10279
10464
  instances: extractInstances(ad, stage),
10280
10465
  isError,
10281
10466
  agentId: String(action.agentId || agentId || ""),
10467
+ instanceId: String(action.instanceId || instanceId || ""),
10282
10468
  lifecycle: lifecycleFor(ad, stage, isError),
10283
10469
  timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
10284
10470
  };
@@ -10386,7 +10572,8 @@ var init_stateful = __esm({
10386
10572
  stages,
10387
10573
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
10388
10574
  reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
10389
- path: this.chainPath(stages)
10575
+ path: this.chainPath(stages),
10576
+ graph: this.buildChainGraph(stages, cid)
10390
10577
  };
10391
10578
  }
10392
10579
  }
@@ -10421,7 +10608,8 @@ var init_stateful = __esm({
10421
10608
  seq: n.seq,
10422
10609
  stage: n.stage,
10423
10610
  timestamp: n.timestamp,
10424
- agentId: n.agentId
10611
+ agentId: n.agentId,
10612
+ instanceId: n.instanceId
10425
10613
  });
10426
10614
  const idx = stages.indexOf(n.stage);
10427
10615
  if (idx + 1 < stages.length) {
@@ -10433,21 +10621,70 @@ var init_stateful = __esm({
10433
10621
  }
10434
10622
  return path6;
10435
10623
  }
10624
+ /** 把链路径转成前端可画的结构化图(相邻节点 next 边)。 */
10625
+ buildChainGraph(stages, chainId) {
10626
+ const path6 = this.chainPath(stages);
10627
+ const gNodes = path6.map((p, i) => {
10628
+ const n = this.nodes_.find((x) => x.sessionId === p.sessionId && x.seq === p.seq);
10629
+ return {
10630
+ session_id: p.sessionId,
10631
+ seq: p.seq,
10632
+ stage: p.stage,
10633
+ action_detail: n ? n.actionDetail : "",
10634
+ resources: n ? [...n.resources].sort() : [],
10635
+ role: stages[i] || "",
10636
+ instance_id: p.instanceId || ""
10637
+ };
10638
+ });
10639
+ const gEdges = [];
10640
+ for (let i = 1; i < gNodes.length; i++) {
10641
+ gEdges.push({ from: i - 1, to: i, type: "next" });
10642
+ }
10643
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10644
+ return {
10645
+ property_id: "cross-" + chainId,
10646
+ property_short: "P1",
10647
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE",
10648
+ property_expression: stages.map((s, i) => ({ token: s, node_index: i })),
10649
+ nodes: gNodes,
10650
+ edges: gEdges
10651
+ };
10652
+ }
10436
10653
  // ---------------- 敏感数据 taint(P2) ----------------
10437
10654
  advanceTaint(node) {
10438
10655
  if (node.lifecycle === "dead") return;
10439
10656
  const idx = node.seq;
10440
10657
  const taintedRes = /* @__PURE__ */ new Set();
10658
+ let parent = null;
10659
+ let label = "";
10660
+ let foundParent = false;
10441
10661
  for (const r of node.resources) {
10442
- if (this.resourceTainted(r)) taintedRes.add(r);
10662
+ if (this.resourceTainted(r)) {
10663
+ taintedRes.add(r);
10664
+ if (!foundParent) {
10665
+ const origin = this.taintOrigin.get(r);
10666
+ if (origin !== void 0) {
10667
+ parent = origin;
10668
+ label = r;
10669
+ foundParent = true;
10670
+ }
10671
+ }
10672
+ }
10443
10673
  }
10444
10674
  if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
10445
10675
  this.tainted.add(idx);
10676
+ parent = null;
10677
+ label = "";
10446
10678
  for (const r of node.resources) taintedRes.add(r);
10447
10679
  }
10448
10680
  if (taintedRes.size > 0) {
10449
10681
  this.tainted.add(idx);
10450
- for (const r of node.resources) this.taintedResources.add(r);
10682
+ this.taintParent.set(idx, parent);
10683
+ if (parent !== null) this.taintEdgeLabel.set(`${parent}:${idx}`, label);
10684
+ for (const r of node.resources) {
10685
+ this.taintedResources.add(r);
10686
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, idx);
10687
+ }
10451
10688
  }
10452
10689
  }
10453
10690
  resourceTainted(resource) {
@@ -10457,16 +10694,85 @@ var init_stateful = __esm({
10457
10694
  checkExfilAfterTaint(node) {
10458
10695
  if (!this.tainted.has(node.seq)) return [];
10459
10696
  if (EXFIL_RE2.test(node.actionDetail)) {
10697
+ const chain = this.traceTaintChain(node.seq);
10460
10698
  return [{
10461
10699
  property_id: "sensitive-data-flow",
10462
- 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`,
10700
+ 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`,
10463
10701
  risk_level: "critical",
10464
- path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
10702
+ path: chain.map((j) => {
10703
+ const n = this.nodes_[j];
10704
+ return [n.sessionId, n.seq, n.actionDetail.slice(0, 60), n.timestamp, null, n.instanceId];
10705
+ }),
10706
+ graph: this.taintGraph(chain)
10465
10707
  }];
10466
10708
  }
10467
10709
  return [];
10468
10710
  }
10469
- taintSource(node) {
10711
+ /** 沿 taint parent 回溯出 CRED 源 → 当前节点的完整路径。 */
10712
+ traceTaintChain(idx) {
10713
+ const chain = [idx];
10714
+ const seen = /* @__PURE__ */ new Set([idx]);
10715
+ while (true) {
10716
+ const parent = this.taintParent.get(chain[chain.length - 1]);
10717
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
10718
+ chain.push(parent);
10719
+ seen.add(parent);
10720
+ }
10721
+ return chain.reverse();
10722
+ }
10723
+ /** 把 taint 路径转成前端可画的结构化图(data 边带共享资源标签)。 */
10724
+ taintGraph(chain) {
10725
+ const gNodes = [];
10726
+ const gEdges = [];
10727
+ chain.forEach((ni, k) => {
10728
+ const n = this.nodes_[ni];
10729
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
10730
+ gNodes.push({
10731
+ session_id: n.sessionId,
10732
+ seq: n.seq,
10733
+ stage: n.stage,
10734
+ action_detail: n.actionDetail,
10735
+ resources: [...n.resources].sort(),
10736
+ role,
10737
+ instance_id: n.instanceId
10738
+ });
10739
+ if (k > 0) {
10740
+ const prev = chain[k - 1];
10741
+ gEdges.push({
10742
+ from: k - 1,
10743
+ to: k,
10744
+ type: "data",
10745
+ label: this.taintEdgeLabel.get(`${prev}:${ni}`) || ""
10746
+ });
10747
+ }
10748
+ });
10749
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10750
+ const expr = [{ token: "CRED", node_index: 0 }];
10751
+ if (chain.length > 2) {
10752
+ expr.push({
10753
+ token: "\u5171\u4EAB\u8F7D\u4F53",
10754
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
10755
+ });
10756
+ }
10757
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
10758
+ return {
10759
+ property_id: "sensitive-data-flow",
10760
+ property_short: "P2",
10761
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
10762
+ property_expression: expr,
10763
+ nodes: gNodes,
10764
+ edges: gEdges
10765
+ };
10766
+ }
10767
+ taintSource(node, chain = []) {
10768
+ if (chain.length > 0) {
10769
+ const src = this.nodes_[chain[0]];
10770
+ if (src) {
10771
+ for (const r of node.resources) {
10772
+ if (src.resources.includes(r)) return `${src.sessionId}#${src.seq} \u8BBF\u95EE ${r}`;
10773
+ }
10774
+ }
10775
+ }
10470
10776
  const sorted = [...this.tainted].sort((a, b) => a - b);
10471
10777
  for (const i of sorted) {
10472
10778
  if (i === node.seq) continue;
@@ -10485,6 +10791,9 @@ var init_stateful = __esm({
10485
10791
  nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
10486
10792
  ptrs: { ...this.ptr },
10487
10793
  tainted: [...this.tainted].sort((a, b) => a - b),
10794
+ // taint 传播路径(前端攻击链图);边标签不入快照,重启后可从资源交集重建
10795
+ taint_parent: Object.fromEntries([...this.taintParent.entries()].map(([k, v]) => [String(k), v])),
10796
+ taint_origin: Object.fromEntries([...this.taintOrigin.entries()]),
10488
10797
  alive: { ...this.aliveResources },
10489
10798
  seq_next: this.seqNext
10490
10799
  };
@@ -10497,6 +10806,8 @@ var init_stateful = __esm({
10497
10806
  seq: Number(raw.seq || 0),
10498
10807
  sessionId: String(raw.sessionId || ""),
10499
10808
  agentId: String(raw.agentId || ""),
10809
+ instanceId: String(raw.instanceId || ""),
10810
+ // 旧快照无此字段 → 空
10500
10811
  actionType: String(raw.actionType || ""),
10501
10812
  actionDetail: String(raw.actionDetail || ""),
10502
10813
  stage: String(raw.stage || ""),
@@ -10512,15 +10823,53 @@ var init_stateful = __esm({
10512
10823
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
10513
10824
  this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
10514
10825
  this.tainted = new Set((snapshot.tainted || []).map(Number));
10826
+ this.taintParent = new Map(
10827
+ Object.entries(snapshot.taint_parent || {}).map(
10828
+ ([k, v]) => [Number(k), v === null || v === void 0 ? null : Number(v)]
10829
+ )
10830
+ );
10831
+ this.taintOrigin = new Map(
10832
+ Object.entries(snapshot.taint_origin || {}).map(
10833
+ ([k, v]) => [String(k), Number(v)]
10834
+ )
10835
+ );
10836
+ this.taintEdgeLabel = /* @__PURE__ */ new Map();
10515
10837
  this.taintedResources = /* @__PURE__ */ new Set();
10516
10838
  for (const i of this.tainted) {
10517
10839
  const n = this.nodes_[i];
10518
10840
  if (n) for (const r of n.resources) this.taintedResources.add(r);
10519
10841
  }
10842
+ if (this.tainted.size > 0 && this.taintParent.size === 0) this.rebuildTaintParent();
10843
+ if (this.taintOrigin.size === 0) {
10844
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10845
+ const n = this.nodes_[i];
10846
+ if (!n) continue;
10847
+ for (const r of n.resources) {
10848
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, i);
10849
+ }
10850
+ }
10851
+ }
10520
10852
  this.aliveResources = Object.fromEntries(
10521
10853
  Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
10522
10854
  );
10523
10855
  }
10856
+ /** 旧快照无 parent 信息时,按 seq 顺序找最近的资源相交染毒前驱。 */
10857
+ rebuildTaintParent() {
10858
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10859
+ const n = this.nodes_[i];
10860
+ if (!n) continue;
10861
+ let parent = null;
10862
+ for (let j = i - 1; j >= 0; j--) {
10863
+ if (!this.tainted.has(j)) continue;
10864
+ const m = this.nodes_[j];
10865
+ if (m && m.resources.some((r) => n.resources.includes(r))) {
10866
+ parent = j;
10867
+ break;
10868
+ }
10869
+ }
10870
+ this.taintParent.set(i, parent);
10871
+ }
10872
+ }
10524
10873
  };
10525
10874
  }
10526
10875
  });
@@ -10603,9 +10952,9 @@ var init_chain_state = __esm({
10603
10952
  delete this.data[agentId];
10604
10953
  this._scheduleWrite();
10605
10954
  }
10606
- applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "") {
10955
+ applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "", instanceId = "") {
10607
10956
  const t = this.load(agentId) || new StatefulChainTracker(agentId);
10608
- const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent });
10957
+ const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent, instanceId });
10609
10958
  this.save(t);
10610
10959
  return r;
10611
10960
  }
@@ -10923,12 +11272,21 @@ async function runAnalysis(input) {
10923
11272
  formalSessions.push({
10924
11273
  sessionId: s.sessionId || "s",
10925
11274
  ...s.agentId ? { agentId: s.agentId } : {},
11275
+ ...s.instanceId ? { instanceId: s.instanceId } : {},
10926
11276
  actionHistory: s.actionHistory || []
10927
11277
  });
10928
11278
  }
10929
11279
  }
10930
- formalSessions.push({ sessionId: "current", agentId: input.agentId, actionHistory: input.actionHistory });
10931
- const formalResult = formalEngine.verify(formalSessions, input.aNext);
11280
+ formalSessions.push({
11281
+ sessionId: "current",
11282
+ agentId: input.agentId,
11283
+ instanceId: input.instanceId || "",
11284
+ actionHistory: input.actionHistory
11285
+ });
11286
+ const formalResult = formalEngine.verify(formalSessions, {
11287
+ ...input.aNext,
11288
+ instanceId: input.instanceId || ""
11289
+ });
10932
11290
  if (!formalResult.verified) {
10933
11291
  const v = formalResult.violations[0];
10934
11292
  if (isFormalObserveMode()) {
@@ -10944,7 +11302,9 @@ async function runAnalysis(input) {
10944
11302
  riskType: "formal_verification",
10945
11303
  confidence: 90,
10946
11304
  source: "formal_engine",
10947
- violatedLayers: ["L2.2"]
11305
+ violatedLayers: ["L2.2"],
11306
+ violationGraph: v?.graph ?? null,
11307
+ violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
10948
11308
  };
10949
11309
  }
10950
11310
  }
@@ -11489,7 +11849,9 @@ async function analyzeInner(input, mossLog) {
11489
11849
  violated_layers: judgment.violatedLayers || [],
11490
11850
  policy: "",
11491
11851
  violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
11492
- confidence: judgment.confidence
11852
+ confidence: judgment.confidence,
11853
+ violation_graph: judgment.violationGraph ?? null,
11854
+ violation_chain: judgment.violationChain ?? []
11493
11855
  };
11494
11856
  mossLog.logResult("Deny", denied.reason);
11495
11857
  return denied;
@@ -12008,6 +12370,9 @@ async function analyzeRoute(c) {
12008
12370
  `[AgentMoss] \u26A0\uFE0F \u8DE8\u4F1A\u8BDD\u6279\u91CF\u8BBF\u95EE: key=${_sweepKey} \u7A97\u53E3\u5185 ${_sweepN} \u4E2A\u4E0D\u540C session\uFF08\u9608\u503C ${sessionSweepThreshold()}\uFF09`
12009
12371
  );
12010
12372
  }
12373
+ const instanceId = String(
12374
+ req.instance_id || req.metadata?.instance_id || ""
12375
+ );
12011
12376
  const result = await analyze({
12012
12377
  sessionId: req.session_id,
12013
12378
  promptSession: req.prompt_session,
@@ -12023,8 +12388,9 @@ async function analyzeRoute(c) {
12023
12388
  })),
12024
12389
  sessionHistory: (req.session_history || []).map((s) => ({
12025
12390
  sessionId: s.session_id,
12026
- // 来源 agent(AgentGate 逐会话带上,用于链路径取证展示;不参与链匹配)
12391
+ // 来源 agent/实例(AgentGate 逐会话带上,用于链路径取证/展示脱敏;不参与链匹配)
12027
12392
  ...s.agent_id ? { agentId: s.agent_id } : {},
12393
+ ...s.instance_id ? { instanceId: s.instance_id } : {},
12028
12394
  actionHistory: (s.action_history || []).map((h) => ({
12029
12395
  actionType: h.action_type,
12030
12396
  actionDetail: h.action_detail,
@@ -12048,11 +12414,13 @@ async function analyzeRoute(c) {
12048
12414
  })),
12049
12415
  aNext: {
12050
12416
  actionType: req.a_next.action_type,
12051
- actionDetail: req.a_next.action_detail
12417
+ actionDetail: req.a_next.action_detail,
12418
+ instanceId
12052
12419
  },
12053
12420
  reason: req.reason,
12054
12421
  osType,
12055
12422
  cwd: req.cwd,
12423
+ instanceId,
12056
12424
  // agent_id:AgentGate V1 normalizer 历史上把 agent_type 塞 metadata,
12057
12425
  // 顶层为空时兜底(OpenDesk 的 agent-moss 需 agent_id 加载专属规则)。
12058
12426
  agentId,
@@ -12088,8 +12456,9 @@ async function analyzeRoute(c) {
12088
12456
  { actionType: req.a_next.action_type, actionDetail: req.a_next.action_detail },
12089
12457
  req.session_id,
12090
12458
  Boolean(meta.is_error),
12091
- agentId
12459
+ agentId,
12092
12460
  // 来源:真实 agent(桶 key 可能是 __system__)
12461
+ instanceId
12093
12462
  );
12094
12463
  const judgment = incMatchedToDeny(inc);
12095
12464
  if (judgment) {
@@ -12110,7 +12479,8 @@ async function analyzeRoute(c) {
12110
12479
  confidence: 90,
12111
12480
  analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
12112
12481
  // 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
12113
- violation_chain: redactChain(judgment.violation_chain, req.session_id)
12482
+ violation_chain: redactChain(judgment.violation_chain, instanceId, req.session_id),
12483
+ violation_graph: redactGraph(judgment.violation_graph, instanceId, req.session_id)
12114
12484
  })
12115
12485
  );
12116
12486
  }
@@ -12130,7 +12500,9 @@ async function analyzeRoute(c) {
12130
12500
  policy: result.policy,
12131
12501
  violated_policy: result.violated_policy,
12132
12502
  confidence: result.confidence,
12133
- analysis_duration_ms: Math.round(durationMs * 10) / 10
12503
+ analysis_duration_ms: Math.round(durationMs * 10) / 10,
12504
+ violation_chain: result.violation_chain ?? [],
12505
+ violation_graph: redactGraph(result.violation_graph, instanceId, req.session_id)
12134
12506
  })
12135
12507
  );
12136
12508
  } catch (err) {
@@ -12160,7 +12532,8 @@ function incMatchedToDeny(inc) {
12160
12532
  property_id: "cross-" + inc.matched.chainId,
12161
12533
  reason: inc.matched.reason,
12162
12534
  risk_level: inc.matched.riskLevel,
12163
- violation_chain: chain
12535
+ violation_chain: chain,
12536
+ violation_graph: inc.matched.graph ?? null
12164
12537
  };
12165
12538
  }
12166
12539
  if (inc.exfil && inc.exfil.length > 0) {
@@ -12168,7 +12541,9 @@ function incMatchedToDeny(inc) {
12168
12541
  property_id: "sensitive-data-flow",
12169
12542
  reason: inc.exfil[0].reason,
12170
12543
  risk_level: inc.exfil[0].risk_level,
12171
- violation_chain: []
12544
+ // P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
12545
+ violation_chain: inc.exfil[0].path ?? [],
12546
+ violation_graph: inc.exfil[0].graph ?? null
12172
12547
  };
12173
12548
  }
12174
12549
  return null;