@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/server.js CHANGED
@@ -1092,18 +1092,43 @@ function hardenExistingPaths() {
1092
1092
  function chainDetailMode() {
1093
1093
  return (env("AGENT_MOSS_CHAIN_DETAIL", "redacted") || "redacted").toLowerCase();
1094
1094
  }
1095
- function redactChain(chain, currentSessionId) {
1095
+ function redactChain(chain, currentInstanceId = "", currentSessionId = "") {
1096
1096
  if (!chain || chain.length === 0) return [];
1097
1097
  if (chainDetailMode() === "full") return [...chain];
1098
1098
  return chain.map((step) => {
1099
1099
  if (Array.isArray(step) && step.length >= 3) {
1100
1100
  const [sid, seq, , ts] = step;
1101
- if (String(sid) === String(currentSessionId)) return [...step];
1101
+ const inst = step.length > 5 ? String(step[5] ?? "") : "";
1102
+ const sameInstance = !!(inst && currentInstanceId && inst === String(currentInstanceId));
1103
+ const sameSessionFallback = !inst && !!currentSessionId && String(sid) === String(currentSessionId);
1104
+ if (sameInstance || sameSessionFallback) return [...step];
1102
1105
  return ["<other-session>", seq, "<\u5DF2\u9690\u85CF>", ts ?? ""];
1103
1106
  }
1104
1107
  return step;
1105
1108
  });
1106
1109
  }
1110
+ function redactGraph(graph, currentInstanceId = "", currentSessionId = "") {
1111
+ if (!graph) return null;
1112
+ if (chainDetailMode() === "full") return graph;
1113
+ const nodes = (graph.nodes || []).map((n) => {
1114
+ const m = { ...n };
1115
+ const inst = String(m.instance_id ?? "") || "";
1116
+ const sid = String(m.session_id ?? "") || "";
1117
+ const sameInstance = !!(inst && currentInstanceId && inst === String(currentInstanceId));
1118
+ const sameSessionFallback = !inst && !!currentSessionId && sid === String(currentSessionId);
1119
+ if (!(sameInstance || sameSessionFallback)) {
1120
+ m.session_id = "<other-session>";
1121
+ m.action_detail = "<\u5DF2\u9690\u85CF>";
1122
+ m.resources = [];
1123
+ }
1124
+ return m;
1125
+ });
1126
+ const out = { nodes, edges: graph.edges || [] };
1127
+ for (const key of ["property_id", "property_short", "property_name", "property_expression"]) {
1128
+ if (graph[key] !== void 0) out[key] = graph[key];
1129
+ }
1130
+ return out;
1131
+ }
1107
1132
  function sessionSweepThreshold() {
1108
1133
  return envInt("AGENT_MOSS_SESSION_SWEEP_THRESHOLD", 50);
1109
1134
  }
@@ -8870,7 +8895,7 @@ function markIntegrity(actionDetail) {
8870
8895
  }
8871
8896
  return { integrity: "medium", isLowSource: false };
8872
8897
  }
8873
- function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "") {
8898
+ function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId = "", instanceId = "") {
8874
8899
  const { integrity, isLowSource } = markIntegrity(actionDetail);
8875
8900
  return {
8876
8901
  sessionId,
@@ -8879,13 +8904,14 @@ function initNode(sessionId, seq, actionType, actionDetail, timestamp, agentId =
8879
8904
  actionDetail,
8880
8905
  stage: markStage(actionDetail),
8881
8906
  agentId: agentId || void 0,
8907
+ instanceId: instanceId || void 0,
8882
8908
  resources: extractResources(actionDetail),
8883
8909
  integrity,
8884
8910
  isLowSource,
8885
8911
  timestamp: timestamp || void 0
8886
8912
  };
8887
8913
  }
8888
- function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "") {
8914
+ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId = "", instanceId = "") {
8889
8915
  const nodes = [];
8890
8916
  const edges = [];
8891
8917
  const stageFlags = /* @__PURE__ */ new Set();
@@ -8896,7 +8922,8 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId
8896
8922
  actionType: String(a.actionType || ""),
8897
8923
  actionDetail: ad,
8898
8924
  calledAt: a.calledAt || void 0,
8899
- agentId: a.agentId || agentId || void 0
8925
+ agentId: a.agentId || agentId || void 0,
8926
+ instanceId: a.instanceId || instanceId || void 0
8900
8927
  });
8901
8928
  }
8902
8929
  if (aNext) {
@@ -8904,11 +8931,12 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId
8904
8931
  actionType: String(aNext.actionType || ""),
8905
8932
  actionDetail: String(aNext.actionDetail || ""),
8906
8933
  calledAt: aNext.calledAt || void 0,
8907
- agentId: aNext.agentId || agentId || void 0
8934
+ agentId: aNext.agentId || agentId || void 0,
8935
+ instanceId: aNext.instanceId || instanceId || void 0
8908
8936
  });
8909
8937
  }
8910
8938
  details.forEach((d, seq) => {
8911
- const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "");
8939
+ const node = initNode(sessionId, seq, d.actionType, d.actionDetail, d.calledAt, d.agentId || "", d.instanceId || "");
8912
8940
  nodes.push(node);
8913
8941
  if (node.stage) stageFlags.add(node.stage);
8914
8942
  });
@@ -8920,12 +8948,18 @@ function buildSessionGraph(actionHistory, aNext, sessionId = "current", agentId
8920
8948
  function buildMultiSessionGraph(sessions, aNext) {
8921
8949
  const mg = { sessions: [], crossEdges: [] };
8922
8950
  if (!sessions || sessions.length === 0) {
8923
- mg.sessions.push(buildSessionGraph([], aNext, "current"));
8951
+ mg.sessions.push(buildSessionGraph([], aNext, "current", "", aNext?.instanceId || ""));
8924
8952
  return mg;
8925
8953
  }
8926
8954
  for (const s of sessions) {
8927
8955
  const sid = String(s.sessionId || "s");
8928
- mg.sessions.push(buildSessionGraph(s.actionHistory || [], void 0, sid, s.agentId || ""));
8956
+ mg.sessions.push(buildSessionGraph(
8957
+ s.actionHistory || [],
8958
+ void 0,
8959
+ sid,
8960
+ s.agentId || "",
8961
+ s.instanceId || ""
8962
+ ));
8929
8963
  }
8930
8964
  if (aNext) {
8931
8965
  const last = mg.sessions[mg.sessions.length - 1];
@@ -8936,13 +8970,14 @@ function buildMultiSessionGraph(sessions, aNext) {
8936
8970
  String(aNext.actionType || ""),
8937
8971
  String(aNext.actionDetail || ""),
8938
8972
  aNext.calledAt || void 0,
8939
- aNext.agentId || ""
8973
+ aNext.agentId || "",
8974
+ aNext.instanceId || ""
8940
8975
  ));
8941
8976
  if (last.nodes.length > 1) {
8942
8977
  last.edges.push({ src: last.nodes.length - 2, dst: last.nodes.length - 1, kind: "next" });
8943
8978
  }
8944
8979
  } else {
8945
- mg.sessions.push(buildSessionGraph([], aNext, "current"));
8980
+ mg.sessions.push(buildSessionGraph([], aNext, "current", "", aNext.instanceId || ""));
8946
8981
  }
8947
8982
  }
8948
8983
  addCrossDataEdges(mg);
@@ -8994,6 +9029,34 @@ var init_builder = __esm({
8994
9029
  });
8995
9030
 
8996
9031
  // src/engine/formal-engine/properties.ts
9032
+ function graphFromPath(nodes, path6, edgeType = "next", propertyMeta, roles, witnessLast = true, propertyExpression) {
9033
+ const byKey = /* @__PURE__ */ new Map();
9034
+ for (const n of nodes) byKey.set(`${n.sessionId}#${n.seq}`, n);
9035
+ const gNodes = [];
9036
+ const gEdges = [];
9037
+ for (const item of path6) {
9038
+ const n = byKey.get(`${item.sessionId}#${item.seq}`);
9039
+ gNodes.push({
9040
+ session_id: item.sessionId,
9041
+ seq: item.seq,
9042
+ stage: n ? n.stage : "",
9043
+ action_detail: n ? n.actionDetail : item.detail,
9044
+ resources: n ? [...n.resources].sort() : [],
9045
+ instance_id: n ? n.instanceId || "" : ""
9046
+ });
9047
+ if (gNodes.length > 1) {
9048
+ gEdges.push({ from: gNodes.length - 2, to: gNodes.length - 1, type: edgeType });
9049
+ }
9050
+ }
9051
+ (roles || []).forEach((role, i) => {
9052
+ if (i < gNodes.length && role) gNodes[i].role = role;
9053
+ });
9054
+ if (witnessLast && gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9055
+ const out = { nodes: gNodes, edges: gEdges };
9056
+ if (propertyMeta) Object.assign(out, propertyMeta);
9057
+ if (propertyExpression) out.property_expression = propertyExpression;
9058
+ return out;
9059
+ }
8997
9060
  function parseTs(ts) {
8998
9061
  if (!ts) return null;
8999
9062
  const t = Date.parse(String(ts).trim());
@@ -9051,7 +9114,12 @@ function checkCrossSessionChain(graph) {
9051
9114
  propertyId: "cross-" + chain.id,
9052
9115
  reason: chain.reason,
9053
9116
  riskLevel: chain.riskLevel,
9054
- path: path6
9117
+ path: path6,
9118
+ graph: graphFromPath(nodes, path6, "next", {
9119
+ property_id: "cross-" + chain.id,
9120
+ property_short: "P1",
9121
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9122
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9055
9123
  });
9056
9124
  return violations;
9057
9125
  } else {
@@ -9071,7 +9139,12 @@ function checkCrossSessionChain(graph) {
9071
9139
  propertyId: "cross-" + chain.id,
9072
9140
  reason: chain.reason,
9073
9141
  riskLevel: chain.riskLevel,
9074
- path: path6
9142
+ path: path6,
9143
+ graph: graphFromPath(nodes, path6, "next", {
9144
+ property_id: "cross-" + chain.id,
9145
+ property_short: "P1",
9146
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE"
9147
+ }, stages, true, stages.map((s, i) => ({ token: s, node_index: i })))
9075
9148
  });
9076
9149
  }
9077
9150
  }
@@ -9086,10 +9159,15 @@ function checkSensitiveDataFlow(graph) {
9086
9159
  for (let i = 0; i < nodes.length; i++) {
9087
9160
  const n = nodes[i];
9088
9161
  if (n.stage === "CRED" && [...n.resources].some((r) => CRED_FILE_RE.test(r))) {
9089
- tainted.set(i, `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`);
9162
+ tainted.set(i, {
9163
+ source: `${n.sessionId}#${n.seq} \u8BBF\u95EE ${[...n.resources][0]}`,
9164
+ parent: null
9165
+ });
9090
9166
  }
9091
9167
  }
9092
9168
  if (tainted.size === 0) return violations;
9169
+ const taintEdges = /* @__PURE__ */ new Map();
9170
+ const edgeKey = (p, c) => `${p}:${c}`;
9093
9171
  const edges = allEdges(graph);
9094
9172
  let changed = true;
9095
9173
  while (changed) {
@@ -9100,7 +9178,8 @@ function checkSensitiveDataFlow(graph) {
9100
9178
  const dstRes = nodes[e.dst].resources;
9101
9179
  for (const r of srcRes) {
9102
9180
  if (dstRes.has(r)) {
9103
- tainted.set(e.dst, tainted.get(e.src));
9181
+ tainted.set(e.dst, { source: tainted.get(e.src).source, parent: e.src });
9182
+ taintEdges.set(edgeKey(e.src, e.dst), r);
9104
9183
  changed = true;
9105
9184
  break;
9106
9185
  }
@@ -9108,16 +9187,76 @@ function checkSensitiveDataFlow(graph) {
9108
9187
  }
9109
9188
  }
9110
9189
  }
9190
+ const tracePath = (idx) => {
9191
+ const chain = [idx];
9192
+ const seen = /* @__PURE__ */ new Set([idx]);
9193
+ while (true) {
9194
+ const parent = tainted.get(chain[chain.length - 1])?.parent;
9195
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
9196
+ chain.push(parent);
9197
+ seen.add(parent);
9198
+ }
9199
+ return chain.reverse();
9200
+ };
9201
+ const taintGraph = (chain) => {
9202
+ const gNodes = [];
9203
+ const gEdges = [];
9204
+ chain.forEach((ni, k) => {
9205
+ const n = nodes[ni];
9206
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
9207
+ gNodes.push({
9208
+ session_id: n.sessionId,
9209
+ seq: n.seq,
9210
+ stage: n.stage,
9211
+ action_detail: n.actionDetail,
9212
+ resources: [...n.resources].sort(),
9213
+ role,
9214
+ instance_id: n.instanceId || ""
9215
+ });
9216
+ if (k > 0) {
9217
+ const prev = chain[k - 1];
9218
+ gEdges.push({
9219
+ from: k - 1,
9220
+ to: k,
9221
+ type: "data",
9222
+ label: taintEdges.get(edgeKey(prev, ni)) || ""
9223
+ });
9224
+ }
9225
+ });
9226
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
9227
+ const expr = [{ token: "CRED", node_index: 0 }];
9228
+ if (chain.length > 2) {
9229
+ expr.push({
9230
+ token: "\u5171\u4EAB\u8F7D\u4F53",
9231
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
9232
+ });
9233
+ }
9234
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
9235
+ return {
9236
+ property_id: "sensitive-data-flow",
9237
+ property_short: "P2",
9238
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
9239
+ property_expression: expr,
9240
+ nodes: gNodes,
9241
+ edges: gEdges
9242
+ };
9243
+ };
9111
9244
  const witness = graph.witnessIdx;
9112
9245
  const candidates = witness !== void 0 ? [witness] : nodes.map((_, i) => i);
9113
9246
  for (const i of candidates) {
9114
9247
  const n = nodes[i];
9115
9248
  if (tainted.has(i) && EXFIL_RE.test(n.actionDetail)) {
9249
+ const chain = tracePath(i);
9116
9250
  violations.push({
9117
9251
  propertyId: "sensitive-data-flow",
9118
- 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`,
9252
+ 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`,
9119
9253
  riskLevel: "critical",
9120
- path: [{ sessionId: n.sessionId, seq: n.seq, detail: n.actionDetail.slice(0, 60) }]
9254
+ path: chain.map((j) => ({
9255
+ sessionId: nodes[j].sessionId,
9256
+ seq: nodes[j].seq,
9257
+ detail: nodes[j].actionDetail.slice(0, 60)
9258
+ })),
9259
+ graph: taintGraph(chain)
9121
9260
  });
9122
9261
  if (witness !== void 0) return violations;
9123
9262
  }
@@ -9659,6 +9798,12 @@ var init_stateful = __esm({
9659
9798
  nodes_ = [];
9660
9799
  ptr = {};
9661
9800
  tainted = /* @__PURE__ */ new Set();
9801
+ /** taint 传播路径:nodeIdx -> 使该节点染毒的前驱 nodeIdx(CRED 源为 null) */
9802
+ taintParent = /* @__PURE__ */ new Map();
9803
+ /** "parent:child" -> 触发传播的共享资源(图上的 data 边标签) */
9804
+ taintEdgeLabel = /* @__PURE__ */ new Map();
9805
+ /** 资源 -> 首次染毒该资源的节点 seq */
9806
+ taintOrigin = /* @__PURE__ */ new Map();
9662
9807
  /** taint 反索引:资源路径 → 已染毒(O(1) 命中)。 */
9663
9808
  taintedResources = /* @__PURE__ */ new Set();
9664
9809
  aliveResources = {};
@@ -9709,7 +9854,7 @@ var init_stateful = __esm({
9709
9854
  for (const s of sessions || []) {
9710
9855
  const sid = String(s.sessionId || "s");
9711
9856
  for (const a of s.actionHistory || []) {
9712
- const node = this.appendNode(a, sid);
9857
+ const node = this.appendNode(a, sid, false, s.agentId || "", s.instanceId || "");
9713
9858
  if (!node) {
9714
9859
  continue;
9715
9860
  }
@@ -9723,8 +9868,8 @@ var init_stateful = __esm({
9723
9868
  }
9724
9869
  /** 增量推进:把当前动作追加到时间线 → 链指针推进 → 判定。 */
9725
9870
  advance(args) {
9726
- const { aNext, sessionId = "current", isError = false, agentId = "" } = args;
9727
- const node = this.appendNode(aNext, sessionId, isError, agentId);
9871
+ const { aNext, sessionId = "current", isError = false, agentId = "", instanceId = "" } = args;
9872
+ const node = this.appendNode(aNext, sessionId, isError, agentId, instanceId);
9728
9873
  if (!node) {
9729
9874
  return { seq: this.seqNext, node: null, matched: null, pendingPrefixes: this.pendingPrefixes, exfil: [], shouldBlock: false };
9730
9875
  }
@@ -9744,7 +9889,7 @@ var init_stateful = __esm({
9744
9889
  }
9745
9890
  };
9746
9891
  }
9747
- appendNode(action, sessionId, isError = false, agentId = "") {
9892
+ appendNode(action, sessionId, isError = false, agentId = "", instanceId = "") {
9748
9893
  const ad = String(action.actionDetail || "");
9749
9894
  const at = String(action.actionType || "");
9750
9895
  if (!ad) return null;
@@ -9762,6 +9907,7 @@ var init_stateful = __esm({
9762
9907
  instances: extractInstances(ad, stage),
9763
9908
  isError,
9764
9909
  agentId: String(action.agentId || agentId || ""),
9910
+ instanceId: String(action.instanceId || instanceId || ""),
9765
9911
  lifecycle: lifecycleFor(ad, stage, isError),
9766
9912
  timestamp: typeof action.calledAt === "string" ? action.calledAt : _StatefulChainTracker.now()
9767
9913
  };
@@ -9869,7 +10015,8 @@ var init_stateful = __esm({
9869
10015
  stages,
9870
10016
  riskLevel: String(chain.riskLevel || chain.risk_level || "high"),
9871
10017
  reason: String(chain.reason || "\u68C0\u6D4B\u5230\u7EC4\u5408\u653B\u51FB\u94FE"),
9872
- path: this.chainPath(stages)
10018
+ path: this.chainPath(stages),
10019
+ graph: this.buildChainGraph(stages, cid)
9873
10020
  };
9874
10021
  }
9875
10022
  }
@@ -9904,7 +10051,8 @@ var init_stateful = __esm({
9904
10051
  seq: n.seq,
9905
10052
  stage: n.stage,
9906
10053
  timestamp: n.timestamp,
9907
- agentId: n.agentId
10054
+ agentId: n.agentId,
10055
+ instanceId: n.instanceId
9908
10056
  });
9909
10057
  const idx = stages.indexOf(n.stage);
9910
10058
  if (idx + 1 < stages.length) {
@@ -9916,21 +10064,70 @@ var init_stateful = __esm({
9916
10064
  }
9917
10065
  return path6;
9918
10066
  }
10067
+ /** 把链路径转成前端可画的结构化图(相邻节点 next 边)。 */
10068
+ buildChainGraph(stages, chainId) {
10069
+ const path6 = this.chainPath(stages);
10070
+ const gNodes = path6.map((p, i) => {
10071
+ const n = this.nodes_.find((x) => x.sessionId === p.sessionId && x.seq === p.seq);
10072
+ return {
10073
+ session_id: p.sessionId,
10074
+ seq: p.seq,
10075
+ stage: p.stage,
10076
+ action_detail: n ? n.actionDetail : "",
10077
+ resources: n ? [...n.resources].sort() : [],
10078
+ role: stages[i] || "",
10079
+ instance_id: p.instanceId || ""
10080
+ };
10081
+ });
10082
+ const gEdges = [];
10083
+ for (let i = 1; i < gNodes.length; i++) {
10084
+ gEdges.push({ from: i - 1, to: i, type: "next" });
10085
+ }
10086
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10087
+ return {
10088
+ property_id: "cross-" + chainId,
10089
+ property_short: "P1",
10090
+ property_name: "\u8DE8\u4F1A\u8BDD\u7EC4\u5408\u94FE",
10091
+ property_expression: stages.map((s, i) => ({ token: s, node_index: i })),
10092
+ nodes: gNodes,
10093
+ edges: gEdges
10094
+ };
10095
+ }
9919
10096
  // ---------------- 敏感数据 taint(P2) ----------------
9920
10097
  advanceTaint(node) {
9921
10098
  if (node.lifecycle === "dead") return;
9922
10099
  const idx = node.seq;
9923
10100
  const taintedRes = /* @__PURE__ */ new Set();
10101
+ let parent = null;
10102
+ let label = "";
10103
+ let foundParent = false;
9924
10104
  for (const r of node.resources) {
9925
- if (this.resourceTainted(r)) taintedRes.add(r);
10105
+ if (this.resourceTainted(r)) {
10106
+ taintedRes.add(r);
10107
+ if (!foundParent) {
10108
+ const origin = this.taintOrigin.get(r);
10109
+ if (origin !== void 0) {
10110
+ parent = origin;
10111
+ label = r;
10112
+ foundParent = true;
10113
+ }
10114
+ }
10115
+ }
9926
10116
  }
9927
10117
  if (node.stage === "CRED" && node.resources.some((r) => CRED_FILE_RE2.test(r))) {
9928
10118
  this.tainted.add(idx);
10119
+ parent = null;
10120
+ label = "";
9929
10121
  for (const r of node.resources) taintedRes.add(r);
9930
10122
  }
9931
10123
  if (taintedRes.size > 0) {
9932
10124
  this.tainted.add(idx);
9933
- for (const r of node.resources) this.taintedResources.add(r);
10125
+ this.taintParent.set(idx, parent);
10126
+ if (parent !== null) this.taintEdgeLabel.set(`${parent}:${idx}`, label);
10127
+ for (const r of node.resources) {
10128
+ this.taintedResources.add(r);
10129
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, idx);
10130
+ }
9934
10131
  }
9935
10132
  }
9936
10133
  resourceTainted(resource) {
@@ -9940,16 +10137,85 @@ var init_stateful = __esm({
9940
10137
  checkExfilAfterTaint(node) {
9941
10138
  if (!this.tainted.has(node.seq)) return [];
9942
10139
  if (EXFIL_RE2.test(node.actionDetail)) {
10140
+ const chain = this.traceTaintChain(node.seq);
9943
10141
  return [{
9944
10142
  property_id: "sensitive-data-flow",
9945
- 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`,
10143
+ 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`,
9946
10144
  risk_level: "critical",
9947
- path: [[node.sessionId, node.seq, node.actionDetail.slice(0, 60)]]
10145
+ path: chain.map((j) => {
10146
+ const n = this.nodes_[j];
10147
+ return [n.sessionId, n.seq, n.actionDetail.slice(0, 60), n.timestamp, null, n.instanceId];
10148
+ }),
10149
+ graph: this.taintGraph(chain)
9948
10150
  }];
9949
10151
  }
9950
10152
  return [];
9951
10153
  }
9952
- taintSource(node) {
10154
+ /** 沿 taint parent 回溯出 CRED 源 → 当前节点的完整路径。 */
10155
+ traceTaintChain(idx) {
10156
+ const chain = [idx];
10157
+ const seen = /* @__PURE__ */ new Set([idx]);
10158
+ while (true) {
10159
+ const parent = this.taintParent.get(chain[chain.length - 1]);
10160
+ if (parent === void 0 || parent === null || seen.has(parent)) break;
10161
+ chain.push(parent);
10162
+ seen.add(parent);
10163
+ }
10164
+ return chain.reverse();
10165
+ }
10166
+ /** 把 taint 路径转成前端可画的结构化图(data 边带共享资源标签)。 */
10167
+ taintGraph(chain) {
10168
+ const gNodes = [];
10169
+ const gEdges = [];
10170
+ chain.forEach((ni, k) => {
10171
+ const n = this.nodes_[ni];
10172
+ const role = k === 0 ? "source" : k === chain.length - 1 ? "sink" : "carrier";
10173
+ gNodes.push({
10174
+ session_id: n.sessionId,
10175
+ seq: n.seq,
10176
+ stage: n.stage,
10177
+ action_detail: n.actionDetail,
10178
+ resources: [...n.resources].sort(),
10179
+ role,
10180
+ instance_id: n.instanceId
10181
+ });
10182
+ if (k > 0) {
10183
+ const prev = chain[k - 1];
10184
+ gEdges.push({
10185
+ from: k - 1,
10186
+ to: k,
10187
+ type: "data",
10188
+ label: this.taintEdgeLabel.get(`${prev}:${ni}`) || ""
10189
+ });
10190
+ }
10191
+ });
10192
+ if (gNodes.length > 0) gNodes[gNodes.length - 1].witness = true;
10193
+ const expr = [{ token: "CRED", node_index: 0 }];
10194
+ if (chain.length > 2) {
10195
+ expr.push({
10196
+ token: "\u5171\u4EAB\u8F7D\u4F53",
10197
+ node_index: Array.from({ length: chain.length - 2 }, (_, i) => i + 1)
10198
+ });
10199
+ }
10200
+ expr.push({ token: "EXFIL", node_index: chain.length - 1 });
10201
+ return {
10202
+ property_id: "sensitive-data-flow",
10203
+ property_short: "P2",
10204
+ property_name: "\u654F\u611F\u6570\u636E\u4F20\u64AD\u5916\u4F20",
10205
+ property_expression: expr,
10206
+ nodes: gNodes,
10207
+ edges: gEdges
10208
+ };
10209
+ }
10210
+ taintSource(node, chain = []) {
10211
+ if (chain.length > 0) {
10212
+ const src = this.nodes_[chain[0]];
10213
+ if (src) {
10214
+ for (const r of node.resources) {
10215
+ if (src.resources.includes(r)) return `${src.sessionId}#${src.seq} \u8BBF\u95EE ${r}`;
10216
+ }
10217
+ }
10218
+ }
9953
10219
  const sorted = [...this.tainted].sort((a, b) => a - b);
9954
10220
  for (const i of sorted) {
9955
10221
  if (i === node.seq) continue;
@@ -9968,6 +10234,9 @@ var init_stateful = __esm({
9968
10234
  nodes: this.nodes_.map((n) => ({ ...n, resources: [...n.resources] })),
9969
10235
  ptrs: { ...this.ptr },
9970
10236
  tainted: [...this.tainted].sort((a, b) => a - b),
10237
+ // taint 传播路径(前端攻击链图);边标签不入快照,重启后可从资源交集重建
10238
+ taint_parent: Object.fromEntries([...this.taintParent.entries()].map(([k, v]) => [String(k), v])),
10239
+ taint_origin: Object.fromEntries([...this.taintOrigin.entries()]),
9971
10240
  alive: { ...this.aliveResources },
9972
10241
  seq_next: this.seqNext
9973
10242
  };
@@ -9980,6 +10249,8 @@ var init_stateful = __esm({
9980
10249
  seq: Number(raw.seq || 0),
9981
10250
  sessionId: String(raw.sessionId || ""),
9982
10251
  agentId: String(raw.agentId || ""),
10252
+ instanceId: String(raw.instanceId || ""),
10253
+ // 旧快照无此字段 → 空
9983
10254
  actionType: String(raw.actionType || ""),
9984
10255
  actionDetail: String(raw.actionDetail || ""),
9985
10256
  stage: String(raw.stage || ""),
@@ -9995,15 +10266,53 @@ var init_stateful = __esm({
9995
10266
  this.seqNext = Number(snapshot.seq_next || this.nodes_.length);
9996
10267
  this.ptr = Object.fromEntries(Object.entries(snapshot.ptrs || {}).map(([k, v]) => [k, Number(v)]));
9997
10268
  this.tainted = new Set((snapshot.tainted || []).map(Number));
10269
+ this.taintParent = new Map(
10270
+ Object.entries(snapshot.taint_parent || {}).map(
10271
+ ([k, v]) => [Number(k), v === null || v === void 0 ? null : Number(v)]
10272
+ )
10273
+ );
10274
+ this.taintOrigin = new Map(
10275
+ Object.entries(snapshot.taint_origin || {}).map(
10276
+ ([k, v]) => [String(k), Number(v)]
10277
+ )
10278
+ );
10279
+ this.taintEdgeLabel = /* @__PURE__ */ new Map();
9998
10280
  this.taintedResources = /* @__PURE__ */ new Set();
9999
10281
  for (const i of this.tainted) {
10000
10282
  const n = this.nodes_[i];
10001
10283
  if (n) for (const r of n.resources) this.taintedResources.add(r);
10002
10284
  }
10285
+ if (this.tainted.size > 0 && this.taintParent.size === 0) this.rebuildTaintParent();
10286
+ if (this.taintOrigin.size === 0) {
10287
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10288
+ const n = this.nodes_[i];
10289
+ if (!n) continue;
10290
+ for (const r of n.resources) {
10291
+ if (!this.taintOrigin.has(r)) this.taintOrigin.set(r, i);
10292
+ }
10293
+ }
10294
+ }
10003
10295
  this.aliveResources = Object.fromEntries(
10004
10296
  Object.entries(snapshot.alive || {}).map(([k, v]) => [k, Number(v)])
10005
10297
  );
10006
10298
  }
10299
+ /** 旧快照无 parent 信息时,按 seq 顺序找最近的资源相交染毒前驱。 */
10300
+ rebuildTaintParent() {
10301
+ for (const i of [...this.tainted].sort((a, b) => a - b)) {
10302
+ const n = this.nodes_[i];
10303
+ if (!n) continue;
10304
+ let parent = null;
10305
+ for (let j = i - 1; j >= 0; j--) {
10306
+ if (!this.tainted.has(j)) continue;
10307
+ const m = this.nodes_[j];
10308
+ if (m && m.resources.some((r) => n.resources.includes(r))) {
10309
+ parent = j;
10310
+ break;
10311
+ }
10312
+ }
10313
+ this.taintParent.set(i, parent);
10314
+ }
10315
+ }
10007
10316
  };
10008
10317
  }
10009
10318
  });
@@ -10086,9 +10395,9 @@ var init_chain_state = __esm({
10086
10395
  delete this.data[agentId];
10087
10396
  this._scheduleWrite();
10088
10397
  }
10089
- applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "") {
10398
+ applyAction(agentId, aNext, sessionId, isError = false, sourceAgent = "", instanceId = "") {
10090
10399
  const t = this.load(agentId) || new StatefulChainTracker(agentId);
10091
- const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent });
10400
+ const r = t.advance({ aNext, sessionId, isError, agentId: sourceAgent, instanceId });
10092
10401
  this.save(t);
10093
10402
  return r;
10094
10403
  }
@@ -10406,12 +10715,21 @@ async function runAnalysis(input) {
10406
10715
  formalSessions.push({
10407
10716
  sessionId: s.sessionId || "s",
10408
10717
  ...s.agentId ? { agentId: s.agentId } : {},
10718
+ ...s.instanceId ? { instanceId: s.instanceId } : {},
10409
10719
  actionHistory: s.actionHistory || []
10410
10720
  });
10411
10721
  }
10412
10722
  }
10413
- formalSessions.push({ sessionId: "current", agentId: input.agentId, actionHistory: input.actionHistory });
10414
- const formalResult = formalEngine.verify(formalSessions, input.aNext);
10723
+ formalSessions.push({
10724
+ sessionId: "current",
10725
+ agentId: input.agentId,
10726
+ instanceId: input.instanceId || "",
10727
+ actionHistory: input.actionHistory
10728
+ });
10729
+ const formalResult = formalEngine.verify(formalSessions, {
10730
+ ...input.aNext,
10731
+ instanceId: input.instanceId || ""
10732
+ });
10415
10733
  if (!formalResult.verified) {
10416
10734
  const v = formalResult.violations[0];
10417
10735
  if (isFormalObserveMode()) {
@@ -10427,7 +10745,9 @@ async function runAnalysis(input) {
10427
10745
  riskType: "formal_verification",
10428
10746
  confidence: 90,
10429
10747
  source: "formal_engine",
10430
- violatedLayers: ["L2.2"]
10748
+ violatedLayers: ["L2.2"],
10749
+ violationGraph: v?.graph ?? null,
10750
+ violationChain: (v?.path ?? []).map((p) => [p.sessionId, p.seq, p.detail])
10431
10751
  };
10432
10752
  }
10433
10753
  }
@@ -10780,6 +11100,8 @@ var AnalyzeRequestSchema = z.object({
10780
11100
  session_id: z.string(),
10781
11101
  /** 来源 agent(AgentGate 逐会话带上):记录在链路径里供告警/取证,不参与链匹配。 */
10782
11102
  agent_id: z.string().optional(),
11103
+ /** 来源实例:仅用于展示层脱敏(同实例不隐藏),不参与链匹配。 */
11104
+ instance_id: z.string().optional(),
10783
11105
  action_history: z.array(ActionItemSchema).default([])
10784
11106
  })).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)."),
10785
11107
  a_next: ActionItemSchema.describe("Next action to be executed"),
@@ -10787,6 +11109,7 @@ var AnalyzeRequestSchema = z.object({
10787
11109
  os_type: z.string().default("").describe("OS type: 'linux', 'windows', or empty for auto-detect"),
10788
11110
  cwd: z.string().default("").describe("Current working directory"),
10789
11111
  agent_id: z.string().default("").describe("Caller agent identifier, e.g. 'xiaoo' / 'opencode' / 'opendesk'. Empty = generic mode (no agent-specific rules)."),
11112
+ 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."),
10790
11113
  metadata: z.record(z.unknown()).optional().describe("Extended metadata")
10791
11114
  });
10792
11115
  var AnalyzeResponseSchema = z.object({
@@ -10800,7 +11123,44 @@ var AnalyzeResponseSchema = z.object({
10800
11123
  confidence: z.number().int().min(0).max(100).default(100),
10801
11124
  analysis_duration_ms: z.number().default(0),
10802
11125
  // 组合攻击链反例路径:每步 [session_id, seq, stage, timestamp],含真实发生时间
10803
- violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([])
11126
+ violation_chain: z.array(z.array(z.union([z.string(), z.number()]).nullable())).default([]),
11127
+ // 结构化攻击链图(前端可视化):节点 + 边(next 时序 / data 数据流)
11128
+ violation_graph: z.object({
11129
+ // 性质元信息(前端展示"违反了哪条性质")
11130
+ property_id: z.string().optional(),
11131
+ property_short: z.string().optional(),
11132
+ property_name: z.string().optional(),
11133
+ // 性质表达式 token → 节点位置(前端展示"表达式对应哪个节点")
11134
+ property_expression: z.array(
11135
+ z.object({
11136
+ token: z.string(),
11137
+ node_index: z.union([z.number(), z.array(z.number())])
11138
+ })
11139
+ ).optional(),
11140
+ nodes: z.array(
11141
+ z.object({
11142
+ session_id: z.string().default(""),
11143
+ seq: z.number().default(0),
11144
+ stage: z.string().default(""),
11145
+ action_detail: z.string().default(""),
11146
+ resources: z.array(z.string()).default([]),
11147
+ // 节点在性质中的角色:source/carrier/sink(P2)或阶段名(P1)
11148
+ role: z.string().optional(),
11149
+ // 是否为违例见证节点(当前动作补全链 / 触发外传)
11150
+ witness: z.boolean().optional(),
11151
+ // 来源实例(仅展示层脱敏:同实例不隐藏)
11152
+ instance_id: z.string().optional()
11153
+ })
11154
+ ),
11155
+ edges: z.array(
11156
+ z.object({
11157
+ from: z.number(),
11158
+ to: z.number(),
11159
+ type: z.enum(["next", "data"]).default("next"),
11160
+ label: z.string().optional()
11161
+ })
11162
+ )
11163
+ }).nullable().default(null)
10804
11164
  });
10805
11165
  var HealthResponseSchema = z.object({
10806
11166
  status: z.string().default("healthy"),
@@ -11224,7 +11584,9 @@ async function analyzeInner(input, mossLog) {
11224
11584
  violated_layers: judgment.violatedLayers || [],
11225
11585
  policy: "",
11226
11586
  violated_policy: `[${judgment.riskType}] ${judgment.reason}`,
11227
- confidence: judgment.confidence
11587
+ confidence: judgment.confidence,
11588
+ violation_graph: judgment.violationGraph ?? null,
11589
+ violation_chain: judgment.violationChain ?? []
11228
11590
  };
11229
11591
  mossLog.logResult("Deny", denied.reason);
11230
11592
  return denied;
@@ -11679,6 +12041,9 @@ async function analyzeRoute(c) {
11679
12041
  `[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`
11680
12042
  );
11681
12043
  }
12044
+ const instanceId = String(
12045
+ req.instance_id || req.metadata?.instance_id || ""
12046
+ );
11682
12047
  const result = await analyze({
11683
12048
  sessionId: req.session_id,
11684
12049
  promptSession: req.prompt_session,
@@ -11694,8 +12059,9 @@ async function analyzeRoute(c) {
11694
12059
  })),
11695
12060
  sessionHistory: (req.session_history || []).map((s) => ({
11696
12061
  sessionId: s.session_id,
11697
- // 来源 agent(AgentGate 逐会话带上,用于链路径取证展示;不参与链匹配)
12062
+ // 来源 agent/实例(AgentGate 逐会话带上,用于链路径取证/展示脱敏;不参与链匹配)
11698
12063
  ...s.agent_id ? { agentId: s.agent_id } : {},
12064
+ ...s.instance_id ? { instanceId: s.instance_id } : {},
11699
12065
  actionHistory: (s.action_history || []).map((h) => ({
11700
12066
  actionType: h.action_type,
11701
12067
  actionDetail: h.action_detail,
@@ -11719,11 +12085,13 @@ async function analyzeRoute(c) {
11719
12085
  })),
11720
12086
  aNext: {
11721
12087
  actionType: req.a_next.action_type,
11722
- actionDetail: req.a_next.action_detail
12088
+ actionDetail: req.a_next.action_detail,
12089
+ instanceId
11723
12090
  },
11724
12091
  reason: req.reason,
11725
12092
  osType,
11726
12093
  cwd: req.cwd,
12094
+ instanceId,
11727
12095
  // agent_id:AgentGate V1 normalizer 历史上把 agent_type 塞 metadata,
11728
12096
  // 顶层为空时兜底(OpenDesk 的 agent-moss 需 agent_id 加载专属规则)。
11729
12097
  agentId,
@@ -11759,8 +12127,9 @@ async function analyzeRoute(c) {
11759
12127
  { actionType: req.a_next.action_type, actionDetail: req.a_next.action_detail },
11760
12128
  req.session_id,
11761
12129
  Boolean(meta.is_error),
11762
- agentId
12130
+ agentId,
11763
12131
  // 来源:真实 agent(桶 key 可能是 __system__)
12132
+ instanceId
11764
12133
  );
11765
12134
  const judgment = incMatchedToDeny(inc);
11766
12135
  if (judgment) {
@@ -11781,7 +12150,8 @@ async function analyzeRoute(c) {
11781
12150
  confidence: 90,
11782
12151
  analysis_duration_ms: Math.round((performance.now() - t0) * 10) / 10,
11783
12152
  // 跨会话节点脱敏(时间线按 agent_id 共享,原样返回等于泄露别人的命令原文)
11784
- violation_chain: redactChain(judgment.violation_chain, req.session_id)
12153
+ violation_chain: redactChain(judgment.violation_chain, instanceId, req.session_id),
12154
+ violation_graph: redactGraph(judgment.violation_graph, instanceId, req.session_id)
11785
12155
  })
11786
12156
  );
11787
12157
  }
@@ -11801,7 +12171,9 @@ async function analyzeRoute(c) {
11801
12171
  policy: result.policy,
11802
12172
  violated_policy: result.violated_policy,
11803
12173
  confidence: result.confidence,
11804
- analysis_duration_ms: Math.round(durationMs * 10) / 10
12174
+ analysis_duration_ms: Math.round(durationMs * 10) / 10,
12175
+ violation_chain: result.violation_chain ?? [],
12176
+ violation_graph: redactGraph(result.violation_graph, instanceId, req.session_id)
11805
12177
  })
11806
12178
  );
11807
12179
  } catch (err) {
@@ -11832,7 +12204,8 @@ function incMatchedToDeny(inc) {
11832
12204
  property_id: "cross-" + inc.matched.chainId,
11833
12205
  reason: inc.matched.reason,
11834
12206
  risk_level: inc.matched.riskLevel,
11835
- violation_chain: chain
12207
+ violation_chain: chain,
12208
+ violation_graph: inc.matched.graph ?? null
11836
12209
  };
11837
12210
  }
11838
12211
  if (inc.exfil && inc.exfil.length > 0) {
@@ -11840,7 +12213,9 @@ function incMatchedToDeny(inc) {
11840
12213
  property_id: "sensitive-data-flow",
11841
12214
  reason: inc.exfil[0].reason,
11842
12215
  risk_level: inc.exfil[0].risk_level,
11843
- violation_chain: []
12216
+ // P2 的 path 现在包含 CRED 源 → 中间载体 → 外传点的完整链
12217
+ violation_chain: inc.exfil[0].path ?? [],
12218
+ violation_graph: inc.exfil[0].graph ?? null
11844
12219
  };
11845
12220
  }
11846
12221
  return null;