@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/server.js CHANGED
@@ -1104,6 +1104,20 @@ function redactChain(chain, currentSessionId) {
1104
1104
  return step;
1105
1105
  });
1106
1106
  }
1107
+ function redactGraph(graph, currentSessionId) {
1108
+ if (!graph) return null;
1109
+ if (chainDetailMode() === "full") return graph;
1110
+ const nodes = (graph.nodes || []).map((n) => {
1111
+ const m = { ...n };
1112
+ if (String(m.session_id ?? "") !== String(currentSessionId)) {
1113
+ m.session_id = "<other-session>";
1114
+ m.action_detail = "<\u5DF2\u9690\u85CF>";
1115
+ m.resources = [];
1116
+ }
1117
+ return m;
1118
+ });
1119
+ return { nodes, edges: graph.edges || [] };
1120
+ }
1107
1121
  function sessionSweepThreshold() {
1108
1122
  return envInt("AGENT_MOSS_SESSION_SWEEP_THRESHOLD", 50);
1109
1123
  }
@@ -1186,6 +1200,12 @@ function agentInflight() {
1186
1200
  function boundAgentOf(principal) {
1187
1201
  return principal.startsWith("agent:") ? principal.slice("agent:".length) : "";
1188
1202
  }
1203
+ function chainScope() {
1204
+ return (env("AGENT_MOSS_CHAIN_SCOPE", "system") || "system").toLowerCase();
1205
+ }
1206
+ function chainBucket(agentId = "") {
1207
+ return chainScope() === "agent" ? agentId || "default" : "__system__";
1208
+ }
1189
1209
  function rateLimitQps() {
1190
1210
  return envInt("AGENT_MOSS_RATE_LIMIT_QPS", 100);
1191
1211
  }
@@ -2749,7 +2769,7 @@ function _scenarioRulesToDefaults() {
2749
2769
  if (!existsSync7(scenDir)) return out;
2750
2770
  let entries;
2751
2771
  try {
2752
- entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
2772
+ entries = readdirSync4(scenDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("__")).map((d) => d.name);
2753
2773
  } catch {
2754
2774
  return out;
2755
2775
  }
@@ -8864,7 +8884,7 @@ function markIntegrity(actionDetail) {
8864
8884
  }
8865
8885
  return { integrity: "medium", isLowSource: false };
8866
8886
  }
8867
- function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
8887
+ function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "") {
8868
8888
  const { integrity, isLowSource } = markIntegrity(actionDetail);
8869
8889
  return {
8870
8890
  sessionId,
@@ -8872,13 +8892,14 @@ function initNode(sessionId, seq, actionType, actionDetail, timestamp) {
8872
8892
  actionType,
8873
8893
  actionDetail,
8874
8894
  stage: markStage(actionDetail),
8895
+ agentId: agentId || void 0,
8875
8896
  resources: extractResources(actionDetail),
8876
8897
  integrity,
8877
8898
  isLowSource,
8878
8899
  timestamp: timestamp || void 0
8879
8900
  };
8880
8901
  }
8881
- function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
8902
+ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "") {
8882
8903
  const nodes = [];
8883
8904
  const edges = [];
8884
8905
  const stageFlags = /* @__PURE__ */ new Set();
@@ -8888,18 +8909,20 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current") {
8888
8909
  if (ad) details.push({
8889
8910
  actionType: String(a.actionType || ""),
8890
8911
  actionDetail: ad,
8891
- calledAt: a.calledAt || void 0
8912
+ calledAt: a.calledAt || void 0,
8913
+ agentId: a.agentId || agentId || void 0
8892
8914
  });
8893
8915
  }
8894
8916
  if (aNext) {
8895
8917
  details.push({
8896
8918
  actionType: String(aNext.actionType || ""),
8897
8919
  actionDetail: String(aNext.actionDetail || ""),
8898
- calledAt: aNext.calledAt || void 0
8920
+ calledAt: aNext.calledAt || void 0,
8921
+ agentId: aNext.agentId || agentId || void 0
8899
8922
  });
8900
8923
  }
8901
8924
  details.forEach((d, seq) => {
8902
- const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt);
8925
+ const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "");
8903
8926
  nodes.push(node);
8904
8927
  if (node.stage) stageFlags.add(node.stage);
8905
8928
  });
@@ -8916,7 +8939,7 @@ function buildMultiSessionGraph(sessions, aNext) {
8916
8939
  }
8917
8940
  for (const s of sessions) {
8918
8941
  const sid = String(s.sessionId || "s");
8919
- mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid));
8942
+ mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid, s.agentId || ""));
8920
8943
  }
8921
8944
  if (aNext) {
8922
8945
  const last = mg.sessions[mg.sessions.length - 1];
@@ -8926,7 +8949,8 @@ function buildMultiSessionGraph(sessions, aNext) {
8926
8949
  last.nodes.length,
8927
8950
  String(aNext.actionType || ""),
8928
8951
  String(aNext.actionDetail || ""),
8929
- aNext.calledAt || void 0
8952
+ aNext.calledAt || void 0,
8953
+ aNext.agentId || ""
8930
8954
  ));
8931
8955
  if (last.nodes.length > 1) {
8932
8956
  last.edges.push({ src: last.nodes.length - 2, dst: last.nodes.length - 1, kind: "next" });
@@ -8984,6 +9008,33 @@ var init_builder = __esm({
8984
9008
  });
8985
9009
 
8986
9010
  // src/engine/formal-engine/properties.ts
9011
+ function graphFromPath(nodes, path6, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
9012
+ const byKey = /* @__PURE__ */ new Map();
9013
+ for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
9014
+ const gNodes = [];
9015
+ const gEdges = [];
9016
+ for (const item of path6) {
9017
+ const n = byKey.get(`${item.sessionId}#${item.seq}`);
9018
+ gNodes.push({
9019
+ session_id: item.sessionId,
9020
+ seq: item.seq,
9021
+ stage: n ? n.stage : "",
9022
+ action_detail: n ? n.actionDetail : item.detail,
9023
+ resources: n ? [...n.resources].sort() : []
9024
+ });
9025
+ if (gNodes.length > 1) {
9026
+ gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
9027
+ }
9028
+ }
9029
+ (roles || []).forEach((role, i) => {
9030
+ if (i < gNodes.length && role) gNodes[i].role = role;
9031
+ });
9032
+ if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9033
+ const out = { nodes: gNodes, edges: gEdges };
9034
+ if (propertyMeta) Object.assign(out, propertyMeta);
9035
+ if (propertyExpression) out.property_expression = propertyExpression;
9036
+ return out;
9037
+ }
8987
9038
  function parseTs(ts) {
8988
9039
  if (!ts) return null;
8989
9040
  const t = Date.parse(String(ts).trim());
@@ -9041,7 +9092,12 @@ function checkCrossSessionChain(graph) {
9041
9092
  propertyId: "cross-" + chain.id,
9042
9093
  reason: chain.reason,
9043
9094
  riskLevel: chain.riskLevel,
9044
- path: path6
9095
+ path: path6,
9096
+ graph: graphFromPath(nodes, path6, "next", {
9097
+ property_id: "cross-" + chain.id,
9098
+ property_short: "P1",
9099
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9100
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9045
9101
  });
9046
9102
  return violations;
9047
9103
  } else {
@@ -9061,7 +9117,12 @@ function checkCrossSessionChain(graph) {
9061
9117
  propertyId: "cross-" + chain.id,
9062
9118
  reason: chain.reason,
9063
9119
  riskLevel: chain.riskLevel,
9064
- path: path6
9120
+ path: path6,
9121
+ graph: graphFromPath(nodes, path6, "next", {
9122
+ property_id: "cross-" + chain.id,
9123
+ property_short: "P1",
9124
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9125
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9065
9126
  });
9066
9127
  }
9067
9128
  }
@@ -9076,10 +9137,15 @@ function checkSensitiveDataFlow(graph) {
9076
9137
  for (let i = 0; i < nodes.length; i++) {
9077
9138
  const n = nodes[i];
9078
9139
  if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
9079
- tainted.set(i, `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`);
9140
+ tainted.set(i, {
9141
+ source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
9142
+ parent: null
9143
+ });
9080
9144
  }
9081
9145
  }
9082
9146
  if (tainted.size === 0) return violations;
9147
+ const taintEdges = /* @__PURE__ */ new Map();
9148
+ const edgeKey = (p, c) => `${p}:${c}`;
9083
9149
  const edges = allEdges(graph);
9084
9150
  let changed = true;
9085
9151
  while (changed) {
@@ -9090,7 +9156,8 @@ function checkSensitiveDataFlow(graph) {
9090
9156
  const dstRes = nodes[e.dst].resources;
9091
9157
  for (const r of srcRes) {
9092
9158
  if (dstRes.has(r)) {
9093
- tainted.set(e.dst, tainted.get(e.src));
9159
+ tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
9160
+ taintEdges.set(edgeKey(e.src, e.dst), r);
9094
9161
  changed = true;
9095
9162
  break;
9096
9163
  }
@@ -9098,16 +9165,75 @@ function checkSensitiveDataFlow(graph) {
9098
9165
  }
9099
9166
  }
9100
9167
  }
9168
+ const tracePath = (idx) => {
9169
+ const chain = [idx];
9170
+ const seen = /* @__PURE__ */ new Set([idx]);
9171
+ while (true) {
9172
+ const parent = tainted.get(chain[chain.length - 1])?.parent;
9173
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
9174
+ chain.push(parent);
9175
+ seen.add(parent);
9176
+ }
9177
+ return chain.reverse();
9178
+ };
9179
+ const taintGraph = (chain) => {
9180
+ const gNodes = [];
9181
+ const gEdges = [];
9182
+ chain.forEach((ni, k) => {
9183
+ const n = nodes[ni];
9184
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
9185
+ gNodes.push({
9186
+ session_id: n.sessionId,
9187
+ seq: n.seq,
9188
+ stage: n.stage,
9189
+ action_detail: n.actionDetail,
9190
+ resources: [...n.resources].sort(),
9191
+ role
9192
+ });
9193
+ if (k > 0) {
9194
+ const prev = chain[k - 1];
9195
+ gEdges.push({
9196
+ from: k - 1,
9197
+ to: k,
9198
+ type: "data",
9199
+ label: taintEdges.get(edgeKey(prev, ni)) || ""
9200
+ });
9201
+ }
9202
+ });
9203
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9204
+ const expr = [{ token: "CRED", node_index: 0 }];
9205
+ if (chain.length > 2) {
9206
+ expr.push({
9207
+ token: "\u5171\u4EAB\u8F7D\u4F53",
9208
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
9209
+ });
9210
+ }
9211
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
9212
+ return {
9213
+ property_id: "sensitive-data-flow",
9214
+ property_short: "P2",
9215
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
9216
+ property_expression: expr,
9217
+ nodes: gNodes,
9218
+ edges: gEdges
9219
+ };
9220
+ };
9101
9221
  const witness = graph.witnessIdx;
9102
9222
  const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
9103
9223
  for (const i of candidates) {
9104
9224
  const n = nodes[i];
9105
9225
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
9226
+ const chain = tracePath(i);
9106
9227
  violations.push({
9107
9228
  propertyId: "sensitive-data-flow",
9108
- 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`,
9229
+ 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`,
9109
9230
  riskLevel: "critical",
9110
- path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
9231
+ path: chain.map((j) => ({
9232
+ sessionId: nodes[j].sessionId,
9233
+ seq: nodes[j].seq,
9234
+ detail: nodes[j].actionDetail.slice(0, 60)
9235
+ })),
9236
+ graph: taintGraph(chain)
9111
9237
  });
9112
9238
  if (witness !== void 0) return violations;
9113
9239
  }
@@ -9649,6 +9775,12 @@ var init_stateful = __esm({
9649
9775
  nodes_ = [];
9650
9776
  ptr = {};
9651
9777
  tainted = /* @__PURE__ */ new Set();
9778
+ /** taint 传播路径:nodeIdx -> 使该节点染毒的前驱 nodeIdx(CRED 源为 null) */
9779
+ taintParent = /* @__PURE__ */ new Map();
9780
+ /** "parent:child" -> 触发传播的共享资源(图上的 data 边标签) */
9781
+ taintEdgeLabel = /* @__PURE__ */ new Map();
9782
+ /** 资源 -> 首次染毒该资源的节点 seq */
9783
+ taintOrigin = /* @__PURE__ */ new Map();
9652
9784
  /** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
9653
9785
  taintedResources = /* @__PURE__ */ new Set();
9654
9786
  aliveResources = {};
@@ -9713,8 +9845,8 @@ var init_stateful = __esm({
9713
9845
  }
9714
9846
  /** 增量推进:把当前动作追加到时间线 → 链指针推进 → 判定。 */
9715
9847
  advance(args) {
9716
- const { aNext, sessionId = "current", isError = false } = args;
9717
- const node = this.appendNode(aNext, sessionId, isError);
9848
+ const { aNext, sessionId = "current", isError = false, agentId = "" } = args;
9849
+ const node = this.appendNode(aNext, sessionId, isError, agentId);
9718
9850
  if (!node) {
9719
9851
  return { seq: this.seqNext, node: null, matched: null, pendingPrefixes: this.pendingPrefixes, exfil: [], shouldBlock: false };
9720
9852
  }
@@ -9734,7 +9866,7 @@ var init_stateful = __esm({
9734
9866
  }
9735
9867
  };
9736
9868
  }
9737
- appendNode(action, sessionId, isError = false) {
9869
+ appendNode(action, sessionId, isError = false, agentId = "") {
9738
9870
  const ad = String(action.actionDetail || "");
9739
9871
  const at = String(action.actionType || "");
9740
9872
  if (!ad) return null;
@@ -9751,6 +9883,7 @@ var init_stateful = __esm({
9751
9883
  resources: extractResources2(ad),
9752
9884
  instances: extractInstances(ad, stage),
9753
9885
  isError,
9886
+ agentId: String(action.agentId || agentId || ""),
9754
9887
  lifecycle: lifecycleFor(ad, stage, isError),
9755
9888
  timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
9756
9889
  };
@@ -9858,7 +9991,8 @@ var init_stateful = __esm({
9858
9991
  stages,
9859
9992
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
9860
9993
  reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
9861
- path: this.chainPath(stages)
9994
+ path: this.chainPath(stages),
9995
+ graph: this.buildChainGraph(stages, cid)
9862
9996
  };
9863
9997
  }
9864
9998
  }
@@ -9888,7 +10022,13 @@ var init_stateful = __esm({
9888
10022
  let want = stages[0];
9889
10023
  for (const n of this.nodes_) {
9890
10024
  if (n.stage === want) {
9891
- path6.push({ sessionId: n.sessionId, seq: n.seq, stage: n.stage, timestamp: n.timestamp });
10025
+ path6.push({
10026
+ sessionId: n.sessionId,
10027
+ seq: n.seq,
10028
+ stage: n.stage,
10029
+ timestamp: n.timestamp,
10030
+ agentId: n.agentId
10031
+ });
9892
10032
  const idx = stages.indexOf(n.stage);
9893
10033
  if (idx + 1 < stages.length) {
9894
10034
  want = stages[idx + 1];
@@ -9899,21 +10039,69 @@ var init_stateful = __esm({
9899
10039
  }
9900
10040
  return path6;
9901
10041
  }
10042
+ /** 把链路径转成前端可画的结构化图(相邻节点 next 边)。 */
10043
+ buildChainGraph(stages, chainId) {
10044
+ const path6 = this.chainPath(stages);
10045
+ const gNodes = path6.map((p, i) => {
10046
+ const n = this.nodes_.find((x) => x.sessionId === p.sessionId && x.seq === p.seq);
10047
+ return {
10048
+ session_id: p.sessionId,
10049
+ seq: p.seq,
10050
+ stage: p.stage,
10051
+ action_detail: n ? n.actionDetail : "",
10052
+ resources: n ? [...n.resources].sort() : [],
10053
+ role: stages[i] || ""
10054
+ };
10055
+ });
10056
+ const gEdges = [];
10057
+ for (let i = 1; i < gNodes.length; i++) {
10058
+ gEdges.push({ from: i - 1, to: i, type: "next" });
10059
+ }
10060
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10061
+ return {
10062
+ property_id: "cross-" + chainId,
10063
+ property_short: "P1",
10064
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE",
10065
+ property_expression: stages.map((s, i) => ({ token: s, node_index: i })),
10066
+ nodes: gNodes,
10067
+ edges: gEdges
10068
+ };
10069
+ }
9902
10070
  // ---------------- 敏感数据 taint(P2) ----------------
9903
10071
  advanceTaint(node) {
9904
10072
  if (node.lifecycle === "dead") return;
9905
10073
  const idx = node.seq;
9906
10074
  const taintedRes = /* @__PURE__ */ new Set();
10075
+ let parent = null;
10076
+ let label = "";
10077
+ let foundParent = false;
9907
10078
  for (const r of node.resources) {
9908
- if (this.resourceTainted(r)) taintedRes.add(r);
10079
+ if (this.resourceTainted(r)) {
10080
+ taintedRes.add(r);
10081
+ if (!foundParent) {
10082
+ const origin = this.taintOrigin.get(r);
10083
+ if (origin !== void 0) {
10084
+ parent = origin;
10085
+ label = r;
10086
+ foundParent = true;
10087
+ }
10088
+ }
10089
+ }
9909
10090
  }
9910
10091
  if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
9911
10092
  this.tainted.add(idx);
10093
+ parent = null;
10094
+ label = "";
9912
10095
  for (const r of node.resources) taintedRes.add(r);
9913
10096
  }
9914
10097
  if (taintedRes.size > 0) {
9915
10098
  this.tainted.add(idx);
9916
- for (const r of taintedRes) this.taintedResources.add(r);
10099
+ this.taintParent.set(idx, parent);
10100
+ if (parent !== null) this.taintEdgeLabel.set(`${parent}:${idx}`, label);
10101
+ for (const r of node.resources) {
10102
+ this.taintedResources.add(r);
10103
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, idx);
10104
+ }
9917
10105
  }
9918
10106
  }
9919
10107
  resourceTainted(resource) {
@@ -9923,16 +10111,84 @@ var init_stateful = __esm({
9923
10111
  checkExfilAfterTaint(node) {
9924
10112
  if (!this.tainted.has(node.seq)) return [];
9925
10113
  if (EXFIL_RE2.test(node.actionDetail)) {
10114
+ const chain = this.traceTaintChain(node.seq);
9926
10115
  return [{
9927
10116
  property_id: "sensitive-data-flow",
9928
- 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`,
10117
+ 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`,
9929
10118
  risk_level: "critical",
9930
- path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
10119
+ path: chain.map((j) => {
10120
+ const n = this.nodes_[j];
10121
+ return [n.sessionId, n.seq, n.actionDetail.slice(0, 60), n.timestamp];
10122
+ }),
10123
+ graph: this.taintGraph(chain)
9931
10124
  }];
9932
10125
  }
9933
10126
  return [];
9934
10127
  }
9935
- taintSource(node) {
10128
+ /** 沿 taint parent 回溯出 CRED 源 → 当前节点的完整路径。 */
10129
+ traceTaintChain(idx) {
10130
+ const chain = [idx];
10131
+ const seen = /* @__PURE__ */ new Set([idx]);
10132
+ while (true) {
10133
+ const parent = this.taintParent.get(chain[chain.length - 1]);
10134
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
10135
+ chain.push(parent);
10136
+ seen.add(parent);
10137
+ }
10138
+ return chain.reverse();
10139
+ }
10140
+ /** 把 taint 路径转成前端可画的结构化图(data 边带共享资源标签)。 */
10141
+ taintGraph(chain) {
10142
+ const gNodes = [];
10143
+ const gEdges = [];
10144
+ chain.forEach((ni, k) => {
10145
+ const n = this.nodes_[ni];
10146
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
10147
+ gNodes.push({
10148
+ session_id: n.sessionId,
10149
+ seq: n.seq,
10150
+ stage: n.stage,
10151
+ action_detail: n.actionDetail,
10152
+ resources: [...n.resources].sort(),
10153
+ role
10154
+ });
10155
+ if (k > 0) {
10156
+ const prev = chain[k - 1];
10157
+ gEdges.push({
10158
+ from: k - 1,
10159
+ to: k,
10160
+ type: "data",
10161
+ label: this.taintEdgeLabel.get(`${prev}:${ni}`) || ""
10162
+ });
10163
+ }
10164
+ });
10165
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10166
+ const expr = [{ token: "CRED", node_index: 0 }];
10167
+ if (chain.length > 2) {
10168
+ expr.push({
10169
+ token: "\u5171\u4EAB\u8F7D\u4F53",
10170
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
10171
+ });
10172
+ }
10173
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
10174
+ return {
10175
+ property_id: "sensitive-data-flow",
10176
+ property_short: "P2",
10177
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
10178
+ property_expression: expr,
10179
+ nodes: gNodes,
10180
+ edges: gEdges
10181
+ };
10182
+ }
10183
+ taintSource(node, chain = []) {
10184
+ if (chain.length > 0) {
10185
+ const src = this.nodes_[chain[0]];
10186
+ if (src) {
10187
+ for (const r of node.resources) {
10188
+ if (src.resources.includes(r)) return `${src.sessionId}#${src.seq} \u8BBF\u95EE ${r}`;
10189
+ }
10190
+ }
10191
+ }
9936
10192
  const sorted = [...this.tainted].sort((a, b) => a - b);
9937
10193
  for (const i of sorted) {
9938
10194
  if (i === node.seq) continue;
@@ -9951,6 +10207,9 @@ var init_stateful = __esm({
9951
10207
  nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
9952
10208
  ptrs: { ...this.ptr },
9953
10209
  tainted: [...this.tainted].sort((a, b) => a - b),
10210
+ // taint 传播路径(前端攻击链图);边标签不入快照,重启后可从资源交集重建
10211
+ taint_parent: Object.fromEntries([...this.taintParent.entries()].map(([k, v]) => [String(k), v])),
10212
+ taint_origin: Object.fromEntries([...this.taintOrigin.entries()]),
9954
10213
  alive: { ...this.aliveResources },
9955
10214
  seq_next: this.seqNext
9956
10215
  };
@@ -9962,6 +10221,7 @@ var init_stateful = __esm({
9962
10221
  this.nodes_.push({
9963
10222
  seq: Number(raw.seq || 0),
9964
10223
  sessionId: String(raw.sessionId || ""),
10224
+ agentId: String(raw.agentId || ""),
9965
10225
  actionType: String(raw.actionType || ""),
9966
10226
  actionDetail: String(raw.actionDetail || ""),
9967
10227
  stage: String(raw.stage || ""),
@@ -9977,15 +10237,53 @@ var init_stateful = __esm({
9977
10237
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
9978
10238
  this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
9979
10239
  this.tainted = new Set((snapshot.tainted || []).map(Number));
10240
+ this.taintParent = new Map(
10241
+ Object.entries(snapshot.taint_parent || {}).map(
10242
+ ([k, v]) => [Number(k), v === null || v === void 0 ? null : Number(v)]
10243
+ )
10244
+ );
10245
+ this.taintOrigin = new Map(
10246
+ Object.entries(snapshot.taint_origin || {}).map(
10247
+ ([k, v]) => [String(k), Number(v)]
10248
+ )
10249
+ );
10250
+ this.taintEdgeLabel = /* @__PURE__ */ new Map();
9980
10251
  this.taintedResources = /* @__PURE__ */ new Set();
9981
10252
  for (const i of this.tainted) {
9982
10253
  const n = this.nodes_[i];
9983
10254
  if (n) for (const r of n.resources) this.taintedResources.add(r);
9984
10255
  }
10256
+ if (this.tainted.size > 0 && this.taintParent.size === 0) this.rebuildTaintParent();
10257
+ if (this.taintOrigin.size === 0) {
10258
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10259
+ const n = this.nodes_[i];
10260
+ if (!n) continue;
10261
+ for (const r of n.resources) {
10262
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, i);
10263
+ }
10264
+ }
10265
+ }
9985
10266
  this.aliveResources = Object.fromEntries(
9986
10267
  Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
9987
10268
  );
9988
10269
  }
10270
+ /** 旧快照无 parent 信息时,按 seq 顺序找最近的资源相交染毒前驱。 */
10271
+ rebuildTaintParent() {
10272
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10273
+ const n = this.nodes_[i];
10274
+ if (!n) continue;
10275
+ let parent = null;
10276
+ for (let j = i - 1; j >= 0; j--) {
10277
+ if (!this.tainted.has(j)) continue;
10278
+ const m = this.nodes_[j];
10279
+ if (m && m.resources.some((r) => n.resources.includes(r))) {
10280
+ parent = j;
10281
+ break;
10282
+ }
10283
+ }
10284
+ this.taintParent.set(i, parent);
10285
+ }
10286
+ }
9989
10287
  };
9990
10288
  }
9991
10289
  });
@@ -10068,9 +10366,9 @@ var init_chain_state = __esm({
10068
10366
  delete this.data[agentId];
10069
10367
  this._scheduleWrite();
10070
10368
  }
10071
- applyAction(agentId, aNext, sessionId, isError = false) {
10369
+ applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "") {
10072
10370
  const t = this.load(agentId) || new StatefulChainTracker(agentId);
10073
- const r = t.advance({ aNext, sessionId, isError });
10371
+ const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent });
10074
10372
  this.save(t);
10075
10373
  return r;
10076
10374
  }
@@ -10385,10 +10683,14 @@ async function runAnalysis(input) {
10385
10683
  const formalSessions = [];
10386
10684
  if (input.sessionHistory) {
10387
10685
  for (const s of input.sessionHistory) {
10388
- formalSessions.push({ sessionId: s.sessionId || "s", actionHistory: s.actionHistory || [] });
10686
+ formalSessions.push({
10687
+ sessionId: s.sessionId || "s",
10688
+ ...s.agentId ? { agentId: s.agentId } : {},
10689
+ actionHistory: s.actionHistory || []
10690
+ });
10389
10691
  }
10390
10692
  }
10391
- formalSessions.push({ sessionId: "current", actionHistory: input.actionHistory });
10693
+ formalSessions.push({ sessionId: "current", agentId: input.agentId, actionHistory: input.actionHistory });
10392
10694
  const formalResult = formalEngine.verify(formalSessions, input.aNext);
10393
10695
  if (!formalResult.verified) {
10394
10696
  const v = formalResult.violations[0];
@@ -10405,7 +10707,9 @@ async function runAnalysis(input) {
10405
10707
  riskType: "formal_verification",
10406
10708
  confidence: 90,
10407
10709
  source: "formal_engine",
10408
- violatedLayers: ["L2.2"]
10710
+ violatedLayers: ["L2.2"],
10711
+ violationGraph: v?.graph ?? null,
10712
+ violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
10409
10713
  };
10410
10714
  }
10411
10715
  }
@@ -10756,6 +11060,8 @@ var AnalyzeRequestSchema = z.object({
10756
11060
  action_history: z.array(ActionItemSchema).default([]).describe("Historical action sequence"),
10757
11061
  session_history: z.array(z.object({
10758
11062
  session_id: z.string(),
11063
+ /** 来源 agent(AgentGate 逐会话带上):记录在链路径里供告警/取证,不参与链匹配。 */
11064
+ agent_id: z.string().optional(),
10759
11065
  action_history: z.array(ActionItemSchema).default([])
10760
11066
  })).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)."),
10761
11067
  a_next: ActionItemSchema.describe("Next action to be executed"),
@@ -10776,7 +11082,42 @@ var AnalyzeResponseSchema = z.object({
10776
11082
  confidence: z.number().int().min(0).max(100).default(100),
10777
11083
  analysis_duration_ms: z.number().default(0),
10778
11084
  // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
10779
- violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
11085
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([]),
11086
+ // 结构化攻击链图(前端可视化):节点 + 边(next 时序 / data 数据流)
11087
+ violation_graph: z.object({
11088
+ // 性质元信息(前端展示"违反了哪条性质")
11089
+ property_id: z.string().optional(),
11090
+ property_short: z.string().optional(),
11091
+ property_name: z.string().optional(),
11092
+ // 性质表达式 token → 节点位置(前端展示"表达式对应哪个节点")
11093
+ property_expression: z.array(
11094
+ z.object({
11095
+ token: z.string(),
11096
+ node_index: z.union([z.number(), z.array(z.number())])
11097
+ })
11098
+ ).optional(),
11099
+ nodes: z.array(
11100
+ z.object({
11101
+ session_id: z.string().default(""),
11102
+ seq: z.number().default(0),
11103
+ stage: z.string().default(""),
11104
+ action_detail: z.string().default(""),
11105
+ resources: z.array(z.string()).default([]),
11106
+ // 节点在性质中的角色:source/carrier/sink(P2)或阶段名(P1)
11107
+ role: z.string().optional(),
11108
+ // 是否为违例见证节点(当前动作补全链 / 触发外传)
11109
+ witness: z.boolean().optional()
11110
+ })
11111
+ ),
11112
+ edges: z.array(
11113
+ z.object({
11114
+ from: z.number(),
11115
+ to: z.number(),
11116
+ type: z.enum(["next", "data"]).default("next"),
11117
+ label: z.string().optional()
11118
+ })
11119
+ )
11120
+ }).nullable().default(null)
10780
11121
  });
10781
11122
  var HealthResponseSchema = z.object({
10782
11123
  status: z.string().default("healthy"),
@@ -11200,7 +11541,9 @@ async function analyzeInner(input, mossLog) {
11200
11541
  violated_layers: judgment.violatedLayers || [],
11201
11542
  policy: "",
11202
11543
  violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
11203
- confidence: judgment.confidence
11544
+ confidence: judgment.confidence,
11545
+ violation_graph: judgment.violationGraph ?? null,
11546
+ violation_chain: judgment.violationChain ?? []
11204
11547
  };
11205
11548
  mossLog.logResult("Deny", denied.reason);
11206
11549
  return denied;
@@ -11670,6 +12013,8 @@ async function analyzeRoute(c) {
11670
12013
  })),
11671
12014
  sessionHistory: (req.session_history || []).map((s) => ({
11672
12015
  sessionId: s.session_id,
12016
+ // 来源 agent(AgentGate 逐会话带上,用于链路径取证展示;不参与链匹配)
12017
+ ...s.agent_id ? { agentId: s.agent_id } : {},
11673
12018
  actionHistory: (s.action_history || []).map((h) => ({
11674
12019
  actionType: h.action_type,
11675
12020
  actionDetail: h.action_detail,
@@ -11728,10 +12073,14 @@ async function analyzeRoute(c) {
11728
12073
  if (incEnabled === "1" || incEnabled === "true" || incEnabled === "yes" || incEnabled === "on") {
11729
12074
  try {
11730
12075
  const store = getChainStateStoreFor(agentId);
11731
- const inc = store.applyAction(agentId, {
11732
- actionType: req.a_next.action_type,
11733
- actionDetail: req.a_next.action_detail
11734
- }, req.session_id, Boolean(meta.is_error));
12076
+ const inc = store.applyAction(
12077
+ chainBucket(agentId),
12078
+ { actionType: req.a_next.action_type, actionDetail: req.a_next.action_detail },
12079
+ req.session_id,
12080
+ Boolean(meta.is_error),
12081
+ agentId
12082
+ // 来源:真实 agent(桶 key 可能是 __system__)
12083
+ );
11735
12084
  const judgment = incMatchedToDeny(inc);
11736
12085
  if (judgment) {
11737
12086
  const layers = [...result.violated_layers];
@@ -11751,7 +12100,8 @@ async function analyzeRoute(c) {
11751
12100
  confidence: 90,
11752
12101
  analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
11753
12102
  // 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
11754
- violation_chain: redactChain(judgment.violation_chain, req.session_id)
12103
+ violation_chain: redactChain(judgment.violation_chain, req.session_id),
12104
+ violation_graph: redactGraph(judgment.violation_graph, req.session_id)
11755
12105
  })
11756
12106
  );
11757
12107
  }
@@ -11771,7 +12121,9 @@ async function analyzeRoute(c) {
11771
12121
  policy: result.policy,
11772
12122
  violated_policy: result.violated_policy,
11773
12123
  confidence: result.confidence,
11774
- analysis_duration_ms: Math.round(durationMs * 10) / 10
12124
+ analysis_duration_ms: Math.round(durationMs * 10) / 10,
12125
+ violation_chain: result.violation_chain ?? [],
12126
+ violation_graph: redactGraph(result.violation_graph, req.session_id)
11775
12127
  })
11776
12128
  );
11777
12129
  } catch (err) {
@@ -11797,12 +12149,13 @@ function getChainStateStoreFor(_agentId) {
11797
12149
  }
11798
12150
  function incMatchedToDeny(inc) {
11799
12151
  if (inc.matched) {
11800
- const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp]);
12152
+ const chain = (inc.matched.path || []).map((p) => [p.sessionId, p.seq, p.stage, p.timestamp, p.agentId ?? null]);
11801
12153
  return {
11802
12154
  property_id: "cross-" + inc.matched.chainId,
11803
12155
  reason: inc.matched.reason,
11804
12156
  risk_level: inc.matched.riskLevel,
11805
- violation_chain: chain
12157
+ violation_chain: chain,
12158
+ violation_graph: inc.matched.graph ?? null
11806
12159
  };
11807
12160
  }
11808
12161
  if (inc.exfil && inc.exfil.length > 0) {
@@ -11810,7 +12163,9 @@ function incMatchedToDeny(inc) {
11810
12163
  property_id: "sensitive-data-flow",
11811
12164
  reason: inc.exfil[0].reason,
11812
12165
  risk_level: inc.exfil[0].risk_level,
11813
- violation_chain: []
12166
+ // P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
12167
+ violation_chain: inc.exfil[0].path ?? [],
12168
+ violation_graph: inc.exfil[0].graph ?? null
11814
12169
  };
11815
12170
  }
11816
12171
  return null;
@@ -11822,9 +12177,10 @@ async function reviewRoute(c) {
11822
12177
  const req = ReviewRequestSchema.parse(body);
11823
12178
  const agentId = req.agent_id || "default";
11824
12179
  const store = getChainStateStoreFor(agentId);
11825
- let tracker = store.load(agentId);
12180
+ const bucket = chainBucket(agentId);
12181
+ let tracker = store.load(bucket);
11826
12182
  if (!tracker) {
11827
- tracker = new StatefulChainTracker(agentId);
12183
+ tracker = new StatefulChainTracker(bucket);
11828
12184
  }
11829
12185
  if (req.db_path && !pathWithin(req.db_path, reviewRoots())) {
11830
12186
  return c.json({