@grwnd/pi-governance 1.4.1 → 1.5.0

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.
@@ -76,12 +76,78 @@ var OrgUnitOverride = Type.Object({
76
76
  })
77
77
  )
78
78
  });
79
+ var DlpMaskingConfig = Type.Object({
80
+ strategy: Type.Union([Type.Literal("partial"), Type.Literal("full"), Type.Literal("hash")], {
81
+ default: "partial"
82
+ }),
83
+ show_chars: Type.Optional(Type.Number({ default: 4, minimum: 0 })),
84
+ placeholder: Type.Optional(Type.String({ default: "***" }))
85
+ });
86
+ var DlpCustomPatternConfig = Type.Object({
87
+ name: Type.String(),
88
+ pattern: Type.String(),
89
+ severity: Type.Union([
90
+ Type.Literal("low"),
91
+ Type.Literal("medium"),
92
+ Type.Literal("high"),
93
+ Type.Literal("critical")
94
+ ]),
95
+ action: Type.Optional(
96
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
97
+ )
98
+ });
99
+ var DlpAllowlistEntryConfig = Type.Object({
100
+ pattern: Type.String()
101
+ });
102
+ var DlpRoleOverrideConfig = Type.Object({
103
+ enabled: Type.Optional(Type.Boolean()),
104
+ mode: Type.Optional(
105
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
106
+ ),
107
+ on_input: Type.Optional(
108
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
109
+ ),
110
+ on_output: Type.Optional(
111
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
112
+ )
113
+ });
114
+ var DlpConfig = Type.Object({
115
+ enabled: Type.Boolean({ default: false }),
116
+ mode: Type.Optional(
117
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")], {
118
+ default: "audit"
119
+ })
120
+ ),
121
+ on_input: Type.Optional(
122
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
123
+ ),
124
+ on_output: Type.Optional(
125
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
126
+ ),
127
+ masking: Type.Optional(DlpMaskingConfig),
128
+ severity_threshold: Type.Optional(
129
+ Type.Union(
130
+ [Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("critical")],
131
+ { default: "low" }
132
+ )
133
+ ),
134
+ built_in: Type.Optional(
135
+ Type.Object({
136
+ secrets: Type.Boolean({ default: true }),
137
+ pii: Type.Boolean({ default: true })
138
+ })
139
+ ),
140
+ custom_patterns: Type.Optional(Type.Array(DlpCustomPatternConfig)),
141
+ allowlist: Type.Optional(Type.Array(DlpAllowlistEntryConfig)),
142
+ role_overrides: Type.Optional(Type.Record(Type.String(), DlpRoleOverrideConfig))
143
+ });
79
144
  var GovernanceConfigSchema = Type.Object({
80
145
  auth: Type.Optional(AuthConfig),
81
146
  policy: Type.Optional(PolicyConfig),
82
147
  templates: Type.Optional(TemplatesConfig),
83
148
  hitl: Type.Optional(HitlConfig),
84
149
  audit: Type.Optional(AuditConfig),
150
+ dlp: Type.Optional(DlpConfig),
85
151
  org_units: Type.Optional(Type.Record(Type.String(), OrgUnitOverride))
86
152
  });
87
153
 
@@ -112,6 +178,9 @@ var DEFAULTS = {
112
178
  },
113
179
  audit: {
114
180
  sinks: [{ type: "jsonl", path: "~/.pi/agent/audit.jsonl" }]
181
+ },
182
+ dlp: {
183
+ enabled: false
115
184
  }
116
185
  };
117
186
 
@@ -821,6 +890,297 @@ var ConfigWatcher = class {
821
890
  }
822
891
  };
823
892
 
893
+ // src/lib/dlp/patterns.ts
894
+ var SECRET_PATTERNS = [
895
+ // AWS
896
+ {
897
+ name: "aws_access_key",
898
+ pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
899
+ severity: "critical",
900
+ category: "secret"
901
+ },
902
+ {
903
+ name: "aws_secret_key",
904
+ pattern: /\b([A-Za-z0-9/+=]{40})(?=\s|$|"|')/g,
905
+ severity: "critical",
906
+ category: "secret"
907
+ },
908
+ // GitHub
909
+ {
910
+ name: "github_pat",
911
+ pattern: /\b(ghp_[A-Za-z0-9]{36,})\b/g,
912
+ severity: "critical",
913
+ category: "secret"
914
+ },
915
+ {
916
+ name: "github_oauth",
917
+ pattern: /\b(gho_[A-Za-z0-9]{36,})\b/g,
918
+ severity: "high",
919
+ category: "secret"
920
+ },
921
+ {
922
+ name: "github_app_token",
923
+ pattern: /\b(ghu_[A-Za-z0-9]{36,})\b/g,
924
+ severity: "high",
925
+ category: "secret"
926
+ },
927
+ // Anthropic
928
+ {
929
+ name: "anthropic_api_key",
930
+ pattern: /\b(sk-ant-api03-[A-Za-z0-9_-]{90,})\b/g,
931
+ severity: "critical",
932
+ category: "secret"
933
+ },
934
+ // OpenAI
935
+ {
936
+ name: "openai_api_key",
937
+ pattern: /\b(sk-[A-Za-z0-9]{20,}T3BlbkFJ[A-Za-z0-9]{20,})\b/g,
938
+ severity: "critical",
939
+ category: "secret"
940
+ },
941
+ // JWT
942
+ {
943
+ name: "jwt_token",
944
+ pattern: /\b(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
945
+ severity: "high",
946
+ category: "secret"
947
+ },
948
+ // Private key headers
949
+ {
950
+ name: "private_key",
951
+ pattern: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
952
+ severity: "critical",
953
+ category: "secret"
954
+ },
955
+ // Database connection strings
956
+ {
957
+ name: "database_url",
958
+ pattern: /\b((?:postgres|mysql|mongodb|redis):\/\/[^\s'"]{10,})\b/g,
959
+ severity: "high",
960
+ category: "secret"
961
+ },
962
+ // Slack
963
+ {
964
+ name: "slack_token",
965
+ pattern: /\b(xox[bpras]-[A-Za-z0-9-]{10,})\b/g,
966
+ severity: "high",
967
+ category: "secret"
968
+ },
969
+ // Stripe
970
+ {
971
+ name: "stripe_key",
972
+ pattern: /\b([rs]k_(?:live|test)_[A-Za-z0-9]{20,})\b/g,
973
+ severity: "critical",
974
+ category: "secret"
975
+ },
976
+ // npm
977
+ {
978
+ name: "npm_token",
979
+ pattern: /\b(npm_[A-Za-z0-9]{36,})\b/g,
980
+ severity: "high",
981
+ category: "secret"
982
+ },
983
+ // SendGrid
984
+ {
985
+ name: "sendgrid_key",
986
+ pattern: /\b(SG\.[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{22,})\b/g,
987
+ severity: "high",
988
+ category: "secret"
989
+ },
990
+ // Generic API key patterns (env-var style assignments)
991
+ {
992
+ name: "generic_api_key",
993
+ pattern: /\b(?:API_KEY|API_SECRET|ACCESS_TOKEN|AUTH_TOKEN|SECRET_KEY)\s*[=:]\s*['"]?([A-Za-z0-9_-]{16,})['"]?/gi,
994
+ severity: "medium",
995
+ category: "secret"
996
+ },
997
+ // High-entropy string near keyword context
998
+ {
999
+ name: "generic_secret_assignment",
1000
+ pattern: /\b(?:password|passwd|secret|token|credential)\s*[=:]\s*['"]([^'"]{8,})['"]?/gi,
1001
+ severity: "medium",
1002
+ category: "secret"
1003
+ }
1004
+ ];
1005
+ var PII_PATTERNS = [
1006
+ // SSN (US)
1007
+ {
1008
+ name: "ssn",
1009
+ pattern: /\b(\d{3}-\d{2}-\d{4})\b/g,
1010
+ severity: "critical",
1011
+ category: "pii"
1012
+ },
1013
+ // Credit card numbers
1014
+ {
1015
+ name: "credit_card",
1016
+ pattern: /\b(4\d{3}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}|5[1-5]\d{2}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}|3[47]\d{2}[\s-]?\d{6}[\s-]?\d{5}|6(?:011|5\d{2})[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4})\b/g,
1017
+ severity: "critical",
1018
+ category: "pii"
1019
+ },
1020
+ // Email address
1021
+ {
1022
+ name: "email",
1023
+ pattern: /\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
1024
+ severity: "low",
1025
+ category: "pii"
1026
+ },
1027
+ // US phone number
1028
+ {
1029
+ name: "phone_us",
1030
+ pattern: /\b(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4})\b/g,
1031
+ severity: "medium",
1032
+ category: "pii"
1033
+ },
1034
+ // IPv4 address
1035
+ {
1036
+ name: "ipv4",
1037
+ pattern: /\b((?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.(?:25[0-5]|2[0-4]\d|[01]?\d\d?))\b/g,
1038
+ severity: "low",
1039
+ category: "pii"
1040
+ }
1041
+ ];
1042
+
1043
+ // src/lib/dlp/scanner.ts
1044
+ var SEVERITY_ORDER = {
1045
+ low: 0,
1046
+ medium: 1,
1047
+ high: 2,
1048
+ critical: 3
1049
+ };
1050
+ var DlpScanner = class {
1051
+ patterns;
1052
+ allowlistRegexps;
1053
+ severityThreshold;
1054
+ config;
1055
+ constructor(config) {
1056
+ this.config = config;
1057
+ this.severityThreshold = SEVERITY_ORDER[config.severity_threshold];
1058
+ this.patterns = [];
1059
+ this.allowlistRegexps = [];
1060
+ if (config.built_in.secrets) {
1061
+ for (const def of SECRET_PATTERNS) {
1062
+ this.patterns.push({ def });
1063
+ }
1064
+ }
1065
+ if (config.built_in.pii) {
1066
+ for (const def of PII_PATTERNS) {
1067
+ this.patterns.push({ def });
1068
+ }
1069
+ }
1070
+ for (const cp of config.custom_patterns) {
1071
+ const def = {
1072
+ name: cp.name,
1073
+ pattern: new RegExp(cp.pattern, "g"),
1074
+ severity: cp.severity,
1075
+ category: "custom"
1076
+ };
1077
+ this.patterns.push({ def, action: cp.action });
1078
+ }
1079
+ for (const compiled of this.patterns) {
1080
+ const override = config.pattern_overrides.get(compiled.def.name);
1081
+ if (override) {
1082
+ compiled.action = override;
1083
+ }
1084
+ }
1085
+ for (const entry of config.allowlist) {
1086
+ this.allowlistRegexps.push(new RegExp(entry.pattern));
1087
+ }
1088
+ }
1089
+ scan(text) {
1090
+ if (!this.config.enabled || text.length === 0) {
1091
+ return { hasMatches: false, matches: [] };
1092
+ }
1093
+ const matches = [];
1094
+ for (const compiled of this.patterns) {
1095
+ if (SEVERITY_ORDER[compiled.def.severity] < this.severityThreshold) {
1096
+ continue;
1097
+ }
1098
+ const regex = new RegExp(compiled.def.pattern.source, compiled.def.pattern.flags);
1099
+ let match;
1100
+ while ((match = regex.exec(text)) !== null) {
1101
+ const matched = match[1] ?? match[0];
1102
+ const start = match[1] ? match.index + match[0].indexOf(match[1]) : match.index;
1103
+ const end = start + matched.length;
1104
+ if (this.isAllowlisted(matched)) {
1105
+ continue;
1106
+ }
1107
+ matches.push({
1108
+ patternName: compiled.def.name,
1109
+ category: compiled.def.category,
1110
+ severity: compiled.def.severity,
1111
+ start,
1112
+ end,
1113
+ matched
1114
+ });
1115
+ }
1116
+ }
1117
+ return { hasMatches: matches.length > 0, matches };
1118
+ }
1119
+ getAction(direction) {
1120
+ if (direction === "input" && this.config.on_input) {
1121
+ return this.config.on_input;
1122
+ }
1123
+ if (direction === "output" && this.config.on_output) {
1124
+ return this.config.on_output;
1125
+ }
1126
+ return this.config.mode;
1127
+ }
1128
+ getPatternAction(match, direction) {
1129
+ const compiled = this.patterns.find((p) => p.def.name === match.patternName);
1130
+ if (compiled?.action) {
1131
+ return compiled.action;
1132
+ }
1133
+ return this.getAction(direction);
1134
+ }
1135
+ isAllowlisted(value) {
1136
+ for (const re of this.allowlistRegexps) {
1137
+ if (re.test(value)) return true;
1138
+ }
1139
+ return false;
1140
+ }
1141
+ };
1142
+
1143
+ // src/lib/dlp/masker.ts
1144
+ import { createHash } from "crypto";
1145
+ var DEFAULT_CONFIG = {
1146
+ strategy: "partial",
1147
+ show_chars: 4,
1148
+ placeholder: "***"
1149
+ };
1150
+ var DlpMasker = class {
1151
+ config;
1152
+ constructor(config) {
1153
+ this.config = { ...DEFAULT_CONFIG, ...config };
1154
+ }
1155
+ maskValue(value) {
1156
+ switch (this.config.strategy) {
1157
+ case "full":
1158
+ return this.config.placeholder;
1159
+ case "hash": {
1160
+ const hash = createHash("sha256").update(value).digest("hex").slice(0, 8);
1161
+ return `[REDACTED:${hash}]`;
1162
+ }
1163
+ case "partial":
1164
+ default: {
1165
+ if (value.length <= this.config.show_chars) {
1166
+ return this.config.placeholder;
1167
+ }
1168
+ return this.config.placeholder + value.slice(-this.config.show_chars);
1169
+ }
1170
+ }
1171
+ }
1172
+ maskText(text, matches) {
1173
+ if (matches.length === 0) return text;
1174
+ const sorted = [...matches].sort((a, b) => b.start - a.start);
1175
+ let result = text;
1176
+ for (const match of sorted) {
1177
+ const masked = this.maskValue(match.matched);
1178
+ result = result.slice(0, match.start) + masked + result.slice(match.end);
1179
+ }
1180
+ return result;
1181
+ }
1182
+ };
1183
+
824
1184
  // src/extensions/index.ts
825
1185
  var PATH_TOOLS = {
826
1186
  read: "path",
@@ -858,6 +1218,72 @@ function extractPath(toolName, input) {
858
1218
  const val = input[key];
859
1219
  return typeof val === "string" ? val : void 0;
860
1220
  }
1221
+ var ACTION_PRIORITY = { audit: 0, mask: 1, block: 2 };
1222
+ function extractDlpFields(toolName, input) {
1223
+ const fields = /* @__PURE__ */ new Map();
1224
+ switch (toolName) {
1225
+ case "bash": {
1226
+ const cmd = input["command"];
1227
+ if (typeof cmd === "string") fields.set("command", cmd);
1228
+ break;
1229
+ }
1230
+ case "write": {
1231
+ const content = input["content"];
1232
+ if (typeof content === "string") fields.set("content", content);
1233
+ const path = input["path"];
1234
+ if (typeof path === "string") fields.set("path", path);
1235
+ break;
1236
+ }
1237
+ case "edit": {
1238
+ const newStr = input["new_string"];
1239
+ if (typeof newStr === "string") fields.set("new_string", newStr);
1240
+ const oldStr = input["old_string"];
1241
+ if (typeof oldStr === "string") fields.set("old_string", oldStr);
1242
+ break;
1243
+ }
1244
+ default: {
1245
+ for (const [key, val] of Object.entries(input)) {
1246
+ if (typeof val === "string") fields.set(key, val);
1247
+ }
1248
+ }
1249
+ }
1250
+ return fields;
1251
+ }
1252
+ function resolveHighestAction(scanner, matches, direction) {
1253
+ let highest = "audit";
1254
+ for (const match of matches) {
1255
+ const action = scanner.getPatternAction(match, direction);
1256
+ if (ACTION_PRIORITY[action] > ACTION_PRIORITY[highest]) {
1257
+ highest = action;
1258
+ }
1259
+ }
1260
+ return highest;
1261
+ }
1262
+ function resolveDlpConfig(dlpConfig, role) {
1263
+ if (!dlpConfig?.enabled) return void 0;
1264
+ const roleOverride = dlpConfig.role_overrides?.[role];
1265
+ if (roleOverride?.enabled === false) return void 0;
1266
+ const patternOverrides = /* @__PURE__ */ new Map();
1267
+ return {
1268
+ enabled: true,
1269
+ mode: roleOverride?.mode ?? dlpConfig.mode ?? "audit",
1270
+ on_input: roleOverride?.on_input ?? dlpConfig.on_input,
1271
+ on_output: roleOverride?.on_output ?? dlpConfig.on_output,
1272
+ severity_threshold: dlpConfig.severity_threshold ?? "low",
1273
+ built_in: {
1274
+ secrets: dlpConfig.built_in?.secrets ?? true,
1275
+ pii: dlpConfig.built_in?.pii ?? true
1276
+ },
1277
+ custom_patterns: (dlpConfig.custom_patterns ?? []).map((cp) => ({
1278
+ name: cp.name,
1279
+ pattern: cp.pattern,
1280
+ severity: cp.severity,
1281
+ action: cp.action
1282
+ })),
1283
+ allowlist: dlpConfig.allowlist ?? [],
1284
+ pattern_overrides: patternOverrides
1285
+ };
1286
+ }
861
1287
  var piGovernance = (pi) => {
862
1288
  let config;
863
1289
  let policyEngine;
@@ -869,7 +1295,18 @@ var piGovernance = (pi) => {
869
1295
  let sessionId;
870
1296
  let budgetTracker;
871
1297
  let configWatcher;
872
- const stats = { allowed: 0, denied: 0, approvals: 0, dryRun: 0, budgetExceeded: 0 };
1298
+ let dlpScanner;
1299
+ let dlpMasker;
1300
+ const stats = {
1301
+ allowed: 0,
1302
+ denied: 0,
1303
+ approvals: 0,
1304
+ dryRun: 0,
1305
+ budgetExceeded: 0,
1306
+ dlpBlocked: 0,
1307
+ dlpDetected: 0,
1308
+ dlpMasked: 0
1309
+ };
873
1310
  pi.on("session_start", async (_event, ctx) => {
874
1311
  sessionId = ctx.sessionId;
875
1312
  const loaded = loadConfig();
@@ -898,6 +1335,11 @@ var piGovernance = (pi) => {
898
1335
  }
899
1336
  const budget = policyEngine.getTokenBudget(identity.role);
900
1337
  budgetTracker = new BudgetTracker(budget);
1338
+ const dlpCfg = resolveDlpConfig(config.dlp, identity.role);
1339
+ if (dlpCfg) {
1340
+ dlpScanner = new DlpScanner(dlpCfg);
1341
+ dlpMasker = new DlpMasker(config.dlp?.masking);
1342
+ }
901
1343
  if (loaded.source !== "built-in") {
902
1344
  configWatcher = new ConfigWatcher(
903
1345
  loaded.source,
@@ -907,6 +1349,14 @@ var piGovernance = (pi) => {
907
1349
  policyEngine = new YamlPolicyEngine(newRulesFile);
908
1350
  const newOverrides = policyEngine.getBashOverrides(identity.role);
909
1351
  bashClassifier = new BashClassifier(newOverrides);
1352
+ const newDlpCfg = resolveDlpConfig(newConfig.dlp, identity.role);
1353
+ if (newDlpCfg) {
1354
+ dlpScanner = new DlpScanner(newDlpCfg);
1355
+ dlpMasker = new DlpMasker(newConfig.dlp?.masking);
1356
+ } else {
1357
+ dlpScanner = void 0;
1358
+ dlpMasker = void 0;
1359
+ }
910
1360
  audit.log({
911
1361
  sessionId,
912
1362
  event: "config_reloaded",
@@ -1054,6 +1504,63 @@ var piGovernance = (pi) => {
1054
1504
  return { block: true, reason: `Access denied to path: ${path}` };
1055
1505
  }
1056
1506
  }
1507
+ if (dlpScanner && dlpMasker) {
1508
+ const fields = extractDlpFields(toolName, input);
1509
+ const allMatches = [];
1510
+ for (const [, fieldValue] of fields) {
1511
+ const result = dlpScanner.scan(fieldValue);
1512
+ allMatches.push(...result.matches);
1513
+ }
1514
+ if (allMatches.length > 0) {
1515
+ const action = resolveHighestAction(dlpScanner, allMatches, "input");
1516
+ const patternNames = [...new Set(allMatches.map((m) => m.patternName))];
1517
+ const severities = [...new Set(allMatches.map((m) => m.severity))];
1518
+ const dlpMeta = {
1519
+ patterns: patternNames,
1520
+ severities,
1521
+ direction: "input",
1522
+ count: allMatches.length
1523
+ };
1524
+ if (action === "block") {
1525
+ stats.dlpBlocked++;
1526
+ await audit.log({
1527
+ ...baseRecord,
1528
+ event: "dlp_blocked",
1529
+ decision: "denied",
1530
+ reason: `DLP: ${patternNames.join(", ")} detected in input`,
1531
+ metadata: dlpMeta
1532
+ });
1533
+ return {
1534
+ block: true,
1535
+ reason: `DLP blocked: sensitive data detected (${patternNames.join(", ")})`
1536
+ };
1537
+ }
1538
+ if (action === "mask") {
1539
+ stats.dlpMasked++;
1540
+ for (const [fieldKey, fieldValue] of fields) {
1541
+ const fieldResult = dlpScanner.scan(fieldValue);
1542
+ if (fieldResult.hasMatches) {
1543
+ input[fieldKey] = dlpMasker.maskText(
1544
+ fieldValue,
1545
+ fieldResult.matches
1546
+ );
1547
+ }
1548
+ }
1549
+ await audit.log({
1550
+ ...baseRecord,
1551
+ event: "dlp_masked",
1552
+ metadata: { ...dlpMeta, strategy: dlpMasker["config"].strategy }
1553
+ });
1554
+ } else {
1555
+ stats.dlpDetected++;
1556
+ await audit.log({
1557
+ ...baseRecord,
1558
+ event: "dlp_detected",
1559
+ metadata: dlpMeta
1560
+ });
1561
+ }
1562
+ }
1563
+ }
1057
1564
  if (toolName !== "bash" && policyEngine.requiresApproval(identity.role, toolName)) {
1058
1565
  if (approvalFlow) {
1059
1566
  stats.approvals++;
@@ -1081,6 +1588,44 @@ var piGovernance = (pi) => {
1081
1588
  return void 0;
1082
1589
  });
1083
1590
  pi.on("tool_result", async (event, _ctx) => {
1591
+ if (dlpScanner && dlpMasker && event.output) {
1592
+ const result = dlpScanner.scan(event.output);
1593
+ if (result.hasMatches) {
1594
+ const action = resolveHighestAction(dlpScanner, result.matches, "output");
1595
+ const patternNames = [...new Set(result.matches.map((m) => m.patternName))];
1596
+ const severities = [...new Set(result.matches.map((m) => m.severity))];
1597
+ const dlpMeta = {
1598
+ patterns: patternNames,
1599
+ severities,
1600
+ direction: "output",
1601
+ count: result.matches.length
1602
+ };
1603
+ if (action === "mask" || action === "block") {
1604
+ stats.dlpMasked++;
1605
+ event.output = dlpMasker.maskText(event.output, result.matches);
1606
+ await audit.log({
1607
+ sessionId,
1608
+ event: "dlp_masked",
1609
+ userId: identity.userId,
1610
+ role: identity.role,
1611
+ orgUnit: identity.orgUnit,
1612
+ tool: event.toolName,
1613
+ metadata: { ...dlpMeta, strategy: dlpMasker["config"].strategy }
1614
+ });
1615
+ } else {
1616
+ stats.dlpDetected++;
1617
+ await audit.log({
1618
+ sessionId,
1619
+ event: "dlp_detected",
1620
+ userId: identity.userId,
1621
+ role: identity.role,
1622
+ orgUnit: identity.orgUnit,
1623
+ tool: event.toolName,
1624
+ metadata: dlpMeta
1625
+ });
1626
+ }
1627
+ }
1628
+ }
1084
1629
  await audit.log({
1085
1630
  sessionId,
1086
1631
  event: "tool_result",
@@ -1128,6 +1673,9 @@ var piGovernance = (pi) => {
1128
1673
  ` Approvals: ${stats.approvals}`,
1129
1674
  ` Dry-run blocks: ${stats.dryRun}`,
1130
1675
  ` Budget exceeded: ${stats.budgetExceeded}`,
1676
+ ` DLP blocked: ${stats.dlpBlocked}`,
1677
+ ` DLP detected: ${stats.dlpDetected}`,
1678
+ ` DLP masked: ${stats.dlpMasked}`,
1131
1679
  "",
1132
1680
  "Audit Events:",
1133
1681
  ...[...summary.entries()].map(([k, v]) => ` ${k}: ${v}`)