@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.
@@ -102,12 +102,78 @@ var OrgUnitOverride = import_typebox.Type.Object({
102
102
  })
103
103
  )
104
104
  });
105
+ var DlpMaskingConfig = import_typebox.Type.Object({
106
+ strategy: import_typebox.Type.Union([import_typebox.Type.Literal("partial"), import_typebox.Type.Literal("full"), import_typebox.Type.Literal("hash")], {
107
+ default: "partial"
108
+ }),
109
+ show_chars: import_typebox.Type.Optional(import_typebox.Type.Number({ default: 4, minimum: 0 })),
110
+ placeholder: import_typebox.Type.Optional(import_typebox.Type.String({ default: "***" }))
111
+ });
112
+ var DlpCustomPatternConfig = import_typebox.Type.Object({
113
+ name: import_typebox.Type.String(),
114
+ pattern: import_typebox.Type.String(),
115
+ severity: import_typebox.Type.Union([
116
+ import_typebox.Type.Literal("low"),
117
+ import_typebox.Type.Literal("medium"),
118
+ import_typebox.Type.Literal("high"),
119
+ import_typebox.Type.Literal("critical")
120
+ ]),
121
+ action: import_typebox.Type.Optional(
122
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")])
123
+ )
124
+ });
125
+ var DlpAllowlistEntryConfig = import_typebox.Type.Object({
126
+ pattern: import_typebox.Type.String()
127
+ });
128
+ var DlpRoleOverrideConfig = import_typebox.Type.Object({
129
+ enabled: import_typebox.Type.Optional(import_typebox.Type.Boolean()),
130
+ mode: import_typebox.Type.Optional(
131
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")])
132
+ ),
133
+ on_input: import_typebox.Type.Optional(
134
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")])
135
+ ),
136
+ on_output: import_typebox.Type.Optional(
137
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")])
138
+ )
139
+ });
140
+ var DlpConfig = import_typebox.Type.Object({
141
+ enabled: import_typebox.Type.Boolean({ default: false }),
142
+ mode: import_typebox.Type.Optional(
143
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")], {
144
+ default: "audit"
145
+ })
146
+ ),
147
+ on_input: import_typebox.Type.Optional(
148
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")])
149
+ ),
150
+ on_output: import_typebox.Type.Optional(
151
+ import_typebox.Type.Union([import_typebox.Type.Literal("audit"), import_typebox.Type.Literal("mask"), import_typebox.Type.Literal("block")])
152
+ ),
153
+ masking: import_typebox.Type.Optional(DlpMaskingConfig),
154
+ severity_threshold: import_typebox.Type.Optional(
155
+ import_typebox.Type.Union(
156
+ [import_typebox.Type.Literal("low"), import_typebox.Type.Literal("medium"), import_typebox.Type.Literal("high"), import_typebox.Type.Literal("critical")],
157
+ { default: "low" }
158
+ )
159
+ ),
160
+ built_in: import_typebox.Type.Optional(
161
+ import_typebox.Type.Object({
162
+ secrets: import_typebox.Type.Boolean({ default: true }),
163
+ pii: import_typebox.Type.Boolean({ default: true })
164
+ })
165
+ ),
166
+ custom_patterns: import_typebox.Type.Optional(import_typebox.Type.Array(DlpCustomPatternConfig)),
167
+ allowlist: import_typebox.Type.Optional(import_typebox.Type.Array(DlpAllowlistEntryConfig)),
168
+ role_overrides: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), DlpRoleOverrideConfig))
169
+ });
105
170
  var GovernanceConfigSchema = import_typebox.Type.Object({
106
171
  auth: import_typebox.Type.Optional(AuthConfig),
107
172
  policy: import_typebox.Type.Optional(PolicyConfig),
108
173
  templates: import_typebox.Type.Optional(TemplatesConfig),
109
174
  hitl: import_typebox.Type.Optional(HitlConfig),
110
175
  audit: import_typebox.Type.Optional(AuditConfig),
176
+ dlp: import_typebox.Type.Optional(DlpConfig),
111
177
  org_units: import_typebox.Type.Optional(import_typebox.Type.Record(import_typebox.Type.String(), OrgUnitOverride))
112
178
  });
113
179
 
@@ -138,6 +204,9 @@ var DEFAULTS = {
138
204
  },
139
205
  audit: {
140
206
  sinks: [{ type: "jsonl", path: "~/.pi/agent/audit.jsonl" }]
207
+ },
208
+ dlp: {
209
+ enabled: false
141
210
  }
142
211
  };
143
212
 
@@ -847,6 +916,297 @@ var ConfigWatcher = class {
847
916
  }
848
917
  };
849
918
 
919
+ // src/lib/dlp/patterns.ts
920
+ var SECRET_PATTERNS = [
921
+ // AWS
922
+ {
923
+ name: "aws_access_key",
924
+ pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
925
+ severity: "critical",
926
+ category: "secret"
927
+ },
928
+ {
929
+ name: "aws_secret_key",
930
+ pattern: /\b([A-Za-z0-9/+=]{40})(?=\s|$|"|')/g,
931
+ severity: "critical",
932
+ category: "secret"
933
+ },
934
+ // GitHub
935
+ {
936
+ name: "github_pat",
937
+ pattern: /\b(ghp_[A-Za-z0-9]{36,})\b/g,
938
+ severity: "critical",
939
+ category: "secret"
940
+ },
941
+ {
942
+ name: "github_oauth",
943
+ pattern: /\b(gho_[A-Za-z0-9]{36,})\b/g,
944
+ severity: "high",
945
+ category: "secret"
946
+ },
947
+ {
948
+ name: "github_app_token",
949
+ pattern: /\b(ghu_[A-Za-z0-9]{36,})\b/g,
950
+ severity: "high",
951
+ category: "secret"
952
+ },
953
+ // Anthropic
954
+ {
955
+ name: "anthropic_api_key",
956
+ pattern: /\b(sk-ant-api03-[A-Za-z0-9_-]{90,})\b/g,
957
+ severity: "critical",
958
+ category: "secret"
959
+ },
960
+ // OpenAI
961
+ {
962
+ name: "openai_api_key",
963
+ pattern: /\b(sk-[A-Za-z0-9]{20,}T3BlbkFJ[A-Za-z0-9]{20,})\b/g,
964
+ severity: "critical",
965
+ category: "secret"
966
+ },
967
+ // JWT
968
+ {
969
+ name: "jwt_token",
970
+ pattern: /\b(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
971
+ severity: "high",
972
+ category: "secret"
973
+ },
974
+ // Private key headers
975
+ {
976
+ name: "private_key",
977
+ pattern: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
978
+ severity: "critical",
979
+ category: "secret"
980
+ },
981
+ // Database connection strings
982
+ {
983
+ name: "database_url",
984
+ pattern: /\b((?:postgres|mysql|mongodb|redis):\/\/[^\s'"]{10,})\b/g,
985
+ severity: "high",
986
+ category: "secret"
987
+ },
988
+ // Slack
989
+ {
990
+ name: "slack_token",
991
+ pattern: /\b(xox[bpras]-[A-Za-z0-9-]{10,})\b/g,
992
+ severity: "high",
993
+ category: "secret"
994
+ },
995
+ // Stripe
996
+ {
997
+ name: "stripe_key",
998
+ pattern: /\b([rs]k_(?:live|test)_[A-Za-z0-9]{20,})\b/g,
999
+ severity: "critical",
1000
+ category: "secret"
1001
+ },
1002
+ // npm
1003
+ {
1004
+ name: "npm_token",
1005
+ pattern: /\b(npm_[A-Za-z0-9]{36,})\b/g,
1006
+ severity: "high",
1007
+ category: "secret"
1008
+ },
1009
+ // SendGrid
1010
+ {
1011
+ name: "sendgrid_key",
1012
+ pattern: /\b(SG\.[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{22,})\b/g,
1013
+ severity: "high",
1014
+ category: "secret"
1015
+ },
1016
+ // Generic API key patterns (env-var style assignments)
1017
+ {
1018
+ name: "generic_api_key",
1019
+ pattern: /\b(?:API_KEY|API_SECRET|ACCESS_TOKEN|AUTH_TOKEN|SECRET_KEY)\s*[=:]\s*['"]?([A-Za-z0-9_-]{16,})['"]?/gi,
1020
+ severity: "medium",
1021
+ category: "secret"
1022
+ },
1023
+ // High-entropy string near keyword context
1024
+ {
1025
+ name: "generic_secret_assignment",
1026
+ pattern: /\b(?:password|passwd|secret|token|credential)\s*[=:]\s*['"]([^'"]{8,})['"]?/gi,
1027
+ severity: "medium",
1028
+ category: "secret"
1029
+ }
1030
+ ];
1031
+ var PII_PATTERNS = [
1032
+ // SSN (US)
1033
+ {
1034
+ name: "ssn",
1035
+ pattern: /\b(\d{3}-\d{2}-\d{4})\b/g,
1036
+ severity: "critical",
1037
+ category: "pii"
1038
+ },
1039
+ // Credit card numbers
1040
+ {
1041
+ name: "credit_card",
1042
+ 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,
1043
+ severity: "critical",
1044
+ category: "pii"
1045
+ },
1046
+ // Email address
1047
+ {
1048
+ name: "email",
1049
+ pattern: /\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
1050
+ severity: "low",
1051
+ category: "pii"
1052
+ },
1053
+ // US phone number
1054
+ {
1055
+ name: "phone_us",
1056
+ pattern: /\b(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4})\b/g,
1057
+ severity: "medium",
1058
+ category: "pii"
1059
+ },
1060
+ // IPv4 address
1061
+ {
1062
+ name: "ipv4",
1063
+ 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,
1064
+ severity: "low",
1065
+ category: "pii"
1066
+ }
1067
+ ];
1068
+
1069
+ // src/lib/dlp/scanner.ts
1070
+ var SEVERITY_ORDER = {
1071
+ low: 0,
1072
+ medium: 1,
1073
+ high: 2,
1074
+ critical: 3
1075
+ };
1076
+ var DlpScanner = class {
1077
+ patterns;
1078
+ allowlistRegexps;
1079
+ severityThreshold;
1080
+ config;
1081
+ constructor(config) {
1082
+ this.config = config;
1083
+ this.severityThreshold = SEVERITY_ORDER[config.severity_threshold];
1084
+ this.patterns = [];
1085
+ this.allowlistRegexps = [];
1086
+ if (config.built_in.secrets) {
1087
+ for (const def of SECRET_PATTERNS) {
1088
+ this.patterns.push({ def });
1089
+ }
1090
+ }
1091
+ if (config.built_in.pii) {
1092
+ for (const def of PII_PATTERNS) {
1093
+ this.patterns.push({ def });
1094
+ }
1095
+ }
1096
+ for (const cp of config.custom_patterns) {
1097
+ const def = {
1098
+ name: cp.name,
1099
+ pattern: new RegExp(cp.pattern, "g"),
1100
+ severity: cp.severity,
1101
+ category: "custom"
1102
+ };
1103
+ this.patterns.push({ def, action: cp.action });
1104
+ }
1105
+ for (const compiled of this.patterns) {
1106
+ const override = config.pattern_overrides.get(compiled.def.name);
1107
+ if (override) {
1108
+ compiled.action = override;
1109
+ }
1110
+ }
1111
+ for (const entry of config.allowlist) {
1112
+ this.allowlistRegexps.push(new RegExp(entry.pattern));
1113
+ }
1114
+ }
1115
+ scan(text) {
1116
+ if (!this.config.enabled || text.length === 0) {
1117
+ return { hasMatches: false, matches: [] };
1118
+ }
1119
+ const matches = [];
1120
+ for (const compiled of this.patterns) {
1121
+ if (SEVERITY_ORDER[compiled.def.severity] < this.severityThreshold) {
1122
+ continue;
1123
+ }
1124
+ const regex = new RegExp(compiled.def.pattern.source, compiled.def.pattern.flags);
1125
+ let match;
1126
+ while ((match = regex.exec(text)) !== null) {
1127
+ const matched = match[1] ?? match[0];
1128
+ const start = match[1] ? match.index + match[0].indexOf(match[1]) : match.index;
1129
+ const end = start + matched.length;
1130
+ if (this.isAllowlisted(matched)) {
1131
+ continue;
1132
+ }
1133
+ matches.push({
1134
+ patternName: compiled.def.name,
1135
+ category: compiled.def.category,
1136
+ severity: compiled.def.severity,
1137
+ start,
1138
+ end,
1139
+ matched
1140
+ });
1141
+ }
1142
+ }
1143
+ return { hasMatches: matches.length > 0, matches };
1144
+ }
1145
+ getAction(direction) {
1146
+ if (direction === "input" && this.config.on_input) {
1147
+ return this.config.on_input;
1148
+ }
1149
+ if (direction === "output" && this.config.on_output) {
1150
+ return this.config.on_output;
1151
+ }
1152
+ return this.config.mode;
1153
+ }
1154
+ getPatternAction(match, direction) {
1155
+ const compiled = this.patterns.find((p) => p.def.name === match.patternName);
1156
+ if (compiled?.action) {
1157
+ return compiled.action;
1158
+ }
1159
+ return this.getAction(direction);
1160
+ }
1161
+ isAllowlisted(value) {
1162
+ for (const re of this.allowlistRegexps) {
1163
+ if (re.test(value)) return true;
1164
+ }
1165
+ return false;
1166
+ }
1167
+ };
1168
+
1169
+ // src/lib/dlp/masker.ts
1170
+ var import_node_crypto2 = require("crypto");
1171
+ var DEFAULT_CONFIG = {
1172
+ strategy: "partial",
1173
+ show_chars: 4,
1174
+ placeholder: "***"
1175
+ };
1176
+ var DlpMasker = class {
1177
+ config;
1178
+ constructor(config) {
1179
+ this.config = { ...DEFAULT_CONFIG, ...config };
1180
+ }
1181
+ maskValue(value) {
1182
+ switch (this.config.strategy) {
1183
+ case "full":
1184
+ return this.config.placeholder;
1185
+ case "hash": {
1186
+ const hash = (0, import_node_crypto2.createHash)("sha256").update(value).digest("hex").slice(0, 8);
1187
+ return `[REDACTED:${hash}]`;
1188
+ }
1189
+ case "partial":
1190
+ default: {
1191
+ if (value.length <= this.config.show_chars) {
1192
+ return this.config.placeholder;
1193
+ }
1194
+ return this.config.placeholder + value.slice(-this.config.show_chars);
1195
+ }
1196
+ }
1197
+ }
1198
+ maskText(text, matches) {
1199
+ if (matches.length === 0) return text;
1200
+ const sorted = [...matches].sort((a, b) => b.start - a.start);
1201
+ let result = text;
1202
+ for (const match of sorted) {
1203
+ const masked = this.maskValue(match.matched);
1204
+ result = result.slice(0, match.start) + masked + result.slice(match.end);
1205
+ }
1206
+ return result;
1207
+ }
1208
+ };
1209
+
850
1210
  // src/extensions/index.ts
851
1211
  var PATH_TOOLS = {
852
1212
  read: "path",
@@ -884,6 +1244,72 @@ function extractPath(toolName, input) {
884
1244
  const val = input[key];
885
1245
  return typeof val === "string" ? val : void 0;
886
1246
  }
1247
+ var ACTION_PRIORITY = { audit: 0, mask: 1, block: 2 };
1248
+ function extractDlpFields(toolName, input) {
1249
+ const fields = /* @__PURE__ */ new Map();
1250
+ switch (toolName) {
1251
+ case "bash": {
1252
+ const cmd = input["command"];
1253
+ if (typeof cmd === "string") fields.set("command", cmd);
1254
+ break;
1255
+ }
1256
+ case "write": {
1257
+ const content = input["content"];
1258
+ if (typeof content === "string") fields.set("content", content);
1259
+ const path = input["path"];
1260
+ if (typeof path === "string") fields.set("path", path);
1261
+ break;
1262
+ }
1263
+ case "edit": {
1264
+ const newStr = input["new_string"];
1265
+ if (typeof newStr === "string") fields.set("new_string", newStr);
1266
+ const oldStr = input["old_string"];
1267
+ if (typeof oldStr === "string") fields.set("old_string", oldStr);
1268
+ break;
1269
+ }
1270
+ default: {
1271
+ for (const [key, val] of Object.entries(input)) {
1272
+ if (typeof val === "string") fields.set(key, val);
1273
+ }
1274
+ }
1275
+ }
1276
+ return fields;
1277
+ }
1278
+ function resolveHighestAction(scanner, matches, direction) {
1279
+ let highest = "audit";
1280
+ for (const match of matches) {
1281
+ const action = scanner.getPatternAction(match, direction);
1282
+ if (ACTION_PRIORITY[action] > ACTION_PRIORITY[highest]) {
1283
+ highest = action;
1284
+ }
1285
+ }
1286
+ return highest;
1287
+ }
1288
+ function resolveDlpConfig(dlpConfig, role) {
1289
+ if (!dlpConfig?.enabled) return void 0;
1290
+ const roleOverride = dlpConfig.role_overrides?.[role];
1291
+ if (roleOverride?.enabled === false) return void 0;
1292
+ const patternOverrides = /* @__PURE__ */ new Map();
1293
+ return {
1294
+ enabled: true,
1295
+ mode: roleOverride?.mode ?? dlpConfig.mode ?? "audit",
1296
+ on_input: roleOverride?.on_input ?? dlpConfig.on_input,
1297
+ on_output: roleOverride?.on_output ?? dlpConfig.on_output,
1298
+ severity_threshold: dlpConfig.severity_threshold ?? "low",
1299
+ built_in: {
1300
+ secrets: dlpConfig.built_in?.secrets ?? true,
1301
+ pii: dlpConfig.built_in?.pii ?? true
1302
+ },
1303
+ custom_patterns: (dlpConfig.custom_patterns ?? []).map((cp) => ({
1304
+ name: cp.name,
1305
+ pattern: cp.pattern,
1306
+ severity: cp.severity,
1307
+ action: cp.action
1308
+ })),
1309
+ allowlist: dlpConfig.allowlist ?? [],
1310
+ pattern_overrides: patternOverrides
1311
+ };
1312
+ }
887
1313
  var piGovernance = (pi) => {
888
1314
  let config;
889
1315
  let policyEngine;
@@ -895,7 +1321,18 @@ var piGovernance = (pi) => {
895
1321
  let sessionId;
896
1322
  let budgetTracker;
897
1323
  let configWatcher;
898
- const stats = { allowed: 0, denied: 0, approvals: 0, dryRun: 0, budgetExceeded: 0 };
1324
+ let dlpScanner;
1325
+ let dlpMasker;
1326
+ const stats = {
1327
+ allowed: 0,
1328
+ denied: 0,
1329
+ approvals: 0,
1330
+ dryRun: 0,
1331
+ budgetExceeded: 0,
1332
+ dlpBlocked: 0,
1333
+ dlpDetected: 0,
1334
+ dlpMasked: 0
1335
+ };
899
1336
  pi.on("session_start", async (_event, ctx) => {
900
1337
  sessionId = ctx.sessionId;
901
1338
  const loaded = loadConfig();
@@ -924,6 +1361,11 @@ var piGovernance = (pi) => {
924
1361
  }
925
1362
  const budget = policyEngine.getTokenBudget(identity.role);
926
1363
  budgetTracker = new BudgetTracker(budget);
1364
+ const dlpCfg = resolveDlpConfig(config.dlp, identity.role);
1365
+ if (dlpCfg) {
1366
+ dlpScanner = new DlpScanner(dlpCfg);
1367
+ dlpMasker = new DlpMasker(config.dlp?.masking);
1368
+ }
927
1369
  if (loaded.source !== "built-in") {
928
1370
  configWatcher = new ConfigWatcher(
929
1371
  loaded.source,
@@ -933,6 +1375,14 @@ var piGovernance = (pi) => {
933
1375
  policyEngine = new YamlPolicyEngine(newRulesFile);
934
1376
  const newOverrides = policyEngine.getBashOverrides(identity.role);
935
1377
  bashClassifier = new BashClassifier(newOverrides);
1378
+ const newDlpCfg = resolveDlpConfig(newConfig.dlp, identity.role);
1379
+ if (newDlpCfg) {
1380
+ dlpScanner = new DlpScanner(newDlpCfg);
1381
+ dlpMasker = new DlpMasker(newConfig.dlp?.masking);
1382
+ } else {
1383
+ dlpScanner = void 0;
1384
+ dlpMasker = void 0;
1385
+ }
936
1386
  audit.log({
937
1387
  sessionId,
938
1388
  event: "config_reloaded",
@@ -1080,6 +1530,63 @@ var piGovernance = (pi) => {
1080
1530
  return { block: true, reason: `Access denied to path: ${path}` };
1081
1531
  }
1082
1532
  }
1533
+ if (dlpScanner && dlpMasker) {
1534
+ const fields = extractDlpFields(toolName, input);
1535
+ const allMatches = [];
1536
+ for (const [, fieldValue] of fields) {
1537
+ const result = dlpScanner.scan(fieldValue);
1538
+ allMatches.push(...result.matches);
1539
+ }
1540
+ if (allMatches.length > 0) {
1541
+ const action = resolveHighestAction(dlpScanner, allMatches, "input");
1542
+ const patternNames = [...new Set(allMatches.map((m) => m.patternName))];
1543
+ const severities = [...new Set(allMatches.map((m) => m.severity))];
1544
+ const dlpMeta = {
1545
+ patterns: patternNames,
1546
+ severities,
1547
+ direction: "input",
1548
+ count: allMatches.length
1549
+ };
1550
+ if (action === "block") {
1551
+ stats.dlpBlocked++;
1552
+ await audit.log({
1553
+ ...baseRecord,
1554
+ event: "dlp_blocked",
1555
+ decision: "denied",
1556
+ reason: `DLP: ${patternNames.join(", ")} detected in input`,
1557
+ metadata: dlpMeta
1558
+ });
1559
+ return {
1560
+ block: true,
1561
+ reason: `DLP blocked: sensitive data detected (${patternNames.join(", ")})`
1562
+ };
1563
+ }
1564
+ if (action === "mask") {
1565
+ stats.dlpMasked++;
1566
+ for (const [fieldKey, fieldValue] of fields) {
1567
+ const fieldResult = dlpScanner.scan(fieldValue);
1568
+ if (fieldResult.hasMatches) {
1569
+ input[fieldKey] = dlpMasker.maskText(
1570
+ fieldValue,
1571
+ fieldResult.matches
1572
+ );
1573
+ }
1574
+ }
1575
+ await audit.log({
1576
+ ...baseRecord,
1577
+ event: "dlp_masked",
1578
+ metadata: { ...dlpMeta, strategy: dlpMasker["config"].strategy }
1579
+ });
1580
+ } else {
1581
+ stats.dlpDetected++;
1582
+ await audit.log({
1583
+ ...baseRecord,
1584
+ event: "dlp_detected",
1585
+ metadata: dlpMeta
1586
+ });
1587
+ }
1588
+ }
1589
+ }
1083
1590
  if (toolName !== "bash" && policyEngine.requiresApproval(identity.role, toolName)) {
1084
1591
  if (approvalFlow) {
1085
1592
  stats.approvals++;
@@ -1107,6 +1614,44 @@ var piGovernance = (pi) => {
1107
1614
  return void 0;
1108
1615
  });
1109
1616
  pi.on("tool_result", async (event, _ctx) => {
1617
+ if (dlpScanner && dlpMasker && event.output) {
1618
+ const result = dlpScanner.scan(event.output);
1619
+ if (result.hasMatches) {
1620
+ const action = resolveHighestAction(dlpScanner, result.matches, "output");
1621
+ const patternNames = [...new Set(result.matches.map((m) => m.patternName))];
1622
+ const severities = [...new Set(result.matches.map((m) => m.severity))];
1623
+ const dlpMeta = {
1624
+ patterns: patternNames,
1625
+ severities,
1626
+ direction: "output",
1627
+ count: result.matches.length
1628
+ };
1629
+ if (action === "mask" || action === "block") {
1630
+ stats.dlpMasked++;
1631
+ event.output = dlpMasker.maskText(event.output, result.matches);
1632
+ await audit.log({
1633
+ sessionId,
1634
+ event: "dlp_masked",
1635
+ userId: identity.userId,
1636
+ role: identity.role,
1637
+ orgUnit: identity.orgUnit,
1638
+ tool: event.toolName,
1639
+ metadata: { ...dlpMeta, strategy: dlpMasker["config"].strategy }
1640
+ });
1641
+ } else {
1642
+ stats.dlpDetected++;
1643
+ await audit.log({
1644
+ sessionId,
1645
+ event: "dlp_detected",
1646
+ userId: identity.userId,
1647
+ role: identity.role,
1648
+ orgUnit: identity.orgUnit,
1649
+ tool: event.toolName,
1650
+ metadata: dlpMeta
1651
+ });
1652
+ }
1653
+ }
1654
+ }
1110
1655
  await audit.log({
1111
1656
  sessionId,
1112
1657
  event: "tool_result",
@@ -1154,6 +1699,9 @@ var piGovernance = (pi) => {
1154
1699
  ` Approvals: ${stats.approvals}`,
1155
1700
  ` Dry-run blocks: ${stats.dryRun}`,
1156
1701
  ` Budget exceeded: ${stats.budgetExceeded}`,
1702
+ ` DLP blocked: ${stats.dlpBlocked}`,
1703
+ ` DLP detected: ${stats.dlpDetected}`,
1704
+ ` DLP masked: ${stats.dlpMasked}`,
1157
1705
  "",
1158
1706
  "Audit Events:",
1159
1707
  ...[...summary.entries()].map(([k, v]) => ` ${k}: ${v}`)