@grwnd/pi-governance 1.4.2 → 1.5.1

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.
@@ -1,3 +1,6 @@
1
+ // src/extensions/index.ts
2
+ import { existsSync as existsSync2 } from "fs";
3
+
1
4
  // src/lib/config/loader.ts
2
5
  import { existsSync, readFileSync } from "fs";
3
6
  import { parse as parseYaml } from "yaml";
@@ -76,12 +79,78 @@ var OrgUnitOverride = Type.Object({
76
79
  })
77
80
  )
78
81
  });
82
+ var DlpMaskingConfig = Type.Object({
83
+ strategy: Type.Union([Type.Literal("partial"), Type.Literal("full"), Type.Literal("hash")], {
84
+ default: "partial"
85
+ }),
86
+ show_chars: Type.Optional(Type.Number({ default: 4, minimum: 0 })),
87
+ placeholder: Type.Optional(Type.String({ default: "***" }))
88
+ });
89
+ var DlpCustomPatternConfig = Type.Object({
90
+ name: Type.String(),
91
+ pattern: Type.String(),
92
+ severity: Type.Union([
93
+ Type.Literal("low"),
94
+ Type.Literal("medium"),
95
+ Type.Literal("high"),
96
+ Type.Literal("critical")
97
+ ]),
98
+ action: Type.Optional(
99
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
100
+ )
101
+ });
102
+ var DlpAllowlistEntryConfig = Type.Object({
103
+ pattern: Type.String()
104
+ });
105
+ var DlpRoleOverrideConfig = Type.Object({
106
+ enabled: Type.Optional(Type.Boolean()),
107
+ mode: Type.Optional(
108
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
109
+ ),
110
+ on_input: Type.Optional(
111
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
112
+ ),
113
+ on_output: Type.Optional(
114
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
115
+ )
116
+ });
117
+ var DlpConfig = Type.Object({
118
+ enabled: Type.Boolean({ default: false }),
119
+ mode: Type.Optional(
120
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")], {
121
+ default: "audit"
122
+ })
123
+ ),
124
+ on_input: Type.Optional(
125
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
126
+ ),
127
+ on_output: Type.Optional(
128
+ Type.Union([Type.Literal("audit"), Type.Literal("mask"), Type.Literal("block")])
129
+ ),
130
+ masking: Type.Optional(DlpMaskingConfig),
131
+ severity_threshold: Type.Optional(
132
+ Type.Union(
133
+ [Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("critical")],
134
+ { default: "low" }
135
+ )
136
+ ),
137
+ built_in: Type.Optional(
138
+ Type.Object({
139
+ secrets: Type.Boolean({ default: true }),
140
+ pii: Type.Boolean({ default: true })
141
+ })
142
+ ),
143
+ custom_patterns: Type.Optional(Type.Array(DlpCustomPatternConfig)),
144
+ allowlist: Type.Optional(Type.Array(DlpAllowlistEntryConfig)),
145
+ role_overrides: Type.Optional(Type.Record(Type.String(), DlpRoleOverrideConfig))
146
+ });
79
147
  var GovernanceConfigSchema = Type.Object({
80
148
  auth: Type.Optional(AuthConfig),
81
149
  policy: Type.Optional(PolicyConfig),
82
150
  templates: Type.Optional(TemplatesConfig),
83
151
  hitl: Type.Optional(HitlConfig),
84
152
  audit: Type.Optional(AuditConfig),
153
+ dlp: Type.Optional(DlpConfig),
85
154
  org_units: Type.Optional(Type.Record(Type.String(), OrgUnitOverride))
86
155
  });
87
156
 
@@ -112,6 +181,9 @@ var DEFAULTS = {
112
181
  },
113
182
  audit: {
114
183
  sinks: [{ type: "jsonl", path: "~/.pi/agent/audit.jsonl" }]
184
+ },
185
+ dlp: {
186
+ enabled: false
115
187
  }
116
188
  };
117
189
 
@@ -821,6 +893,297 @@ var ConfigWatcher = class {
821
893
  }
822
894
  };
823
895
 
896
+ // src/lib/dlp/patterns.ts
897
+ var SECRET_PATTERNS = [
898
+ // AWS
899
+ {
900
+ name: "aws_access_key",
901
+ pattern: /\b(AKIA[0-9A-Z]{16})\b/g,
902
+ severity: "critical",
903
+ category: "secret"
904
+ },
905
+ {
906
+ name: "aws_secret_key",
907
+ pattern: /\b([A-Za-z0-9/+=]{40})(?=\s|$|"|')/g,
908
+ severity: "critical",
909
+ category: "secret"
910
+ },
911
+ // GitHub
912
+ {
913
+ name: "github_pat",
914
+ pattern: /\b(ghp_[A-Za-z0-9]{36,})\b/g,
915
+ severity: "critical",
916
+ category: "secret"
917
+ },
918
+ {
919
+ name: "github_oauth",
920
+ pattern: /\b(gho_[A-Za-z0-9]{36,})\b/g,
921
+ severity: "high",
922
+ category: "secret"
923
+ },
924
+ {
925
+ name: "github_app_token",
926
+ pattern: /\b(ghu_[A-Za-z0-9]{36,})\b/g,
927
+ severity: "high",
928
+ category: "secret"
929
+ },
930
+ // Anthropic
931
+ {
932
+ name: "anthropic_api_key",
933
+ pattern: /\b(sk-ant-api03-[A-Za-z0-9_-]{90,})\b/g,
934
+ severity: "critical",
935
+ category: "secret"
936
+ },
937
+ // OpenAI
938
+ {
939
+ name: "openai_api_key",
940
+ pattern: /\b(sk-[A-Za-z0-9]{20,}T3BlbkFJ[A-Za-z0-9]{20,})\b/g,
941
+ severity: "critical",
942
+ category: "secret"
943
+ },
944
+ // JWT
945
+ {
946
+ name: "jwt_token",
947
+ pattern: /\b(eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g,
948
+ severity: "high",
949
+ category: "secret"
950
+ },
951
+ // Private key headers
952
+ {
953
+ name: "private_key",
954
+ pattern: /-----BEGIN\s+(RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g,
955
+ severity: "critical",
956
+ category: "secret"
957
+ },
958
+ // Database connection strings
959
+ {
960
+ name: "database_url",
961
+ pattern: /\b((?:postgres|mysql|mongodb|redis):\/\/[^\s'"]{10,})\b/g,
962
+ severity: "high",
963
+ category: "secret"
964
+ },
965
+ // Slack
966
+ {
967
+ name: "slack_token",
968
+ pattern: /\b(xox[bpras]-[A-Za-z0-9-]{10,})\b/g,
969
+ severity: "high",
970
+ category: "secret"
971
+ },
972
+ // Stripe
973
+ {
974
+ name: "stripe_key",
975
+ pattern: /\b([rs]k_(?:live|test)_[A-Za-z0-9]{20,})\b/g,
976
+ severity: "critical",
977
+ category: "secret"
978
+ },
979
+ // npm
980
+ {
981
+ name: "npm_token",
982
+ pattern: /\b(npm_[A-Za-z0-9]{36,})\b/g,
983
+ severity: "high",
984
+ category: "secret"
985
+ },
986
+ // SendGrid
987
+ {
988
+ name: "sendgrid_key",
989
+ pattern: /\b(SG\.[A-Za-z0-9_-]{22,}\.[A-Za-z0-9_-]{22,})\b/g,
990
+ severity: "high",
991
+ category: "secret"
992
+ },
993
+ // Generic API key patterns (env-var style assignments)
994
+ {
995
+ name: "generic_api_key",
996
+ pattern: /\b(?:API_KEY|API_SECRET|ACCESS_TOKEN|AUTH_TOKEN|SECRET_KEY)\s*[=:]\s*['"]?([A-Za-z0-9_-]{16,})['"]?/gi,
997
+ severity: "medium",
998
+ category: "secret"
999
+ },
1000
+ // High-entropy string near keyword context
1001
+ {
1002
+ name: "generic_secret_assignment",
1003
+ pattern: /\b(?:password|passwd|secret|token|credential)\s*[=:]\s*['"]([^'"]{8,})['"]?/gi,
1004
+ severity: "medium",
1005
+ category: "secret"
1006
+ }
1007
+ ];
1008
+ var PII_PATTERNS = [
1009
+ // SSN (US)
1010
+ {
1011
+ name: "ssn",
1012
+ pattern: /\b(\d{3}-\d{2}-\d{4})\b/g,
1013
+ severity: "critical",
1014
+ category: "pii"
1015
+ },
1016
+ // Credit card numbers
1017
+ {
1018
+ name: "credit_card",
1019
+ 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,
1020
+ severity: "critical",
1021
+ category: "pii"
1022
+ },
1023
+ // Email address
1024
+ {
1025
+ name: "email",
1026
+ pattern: /\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
1027
+ severity: "low",
1028
+ category: "pii"
1029
+ },
1030
+ // US phone number
1031
+ {
1032
+ name: "phone_us",
1033
+ pattern: /\b(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4})\b/g,
1034
+ severity: "medium",
1035
+ category: "pii"
1036
+ },
1037
+ // IPv4 address
1038
+ {
1039
+ name: "ipv4",
1040
+ 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,
1041
+ severity: "low",
1042
+ category: "pii"
1043
+ }
1044
+ ];
1045
+
1046
+ // src/lib/dlp/scanner.ts
1047
+ var SEVERITY_ORDER = {
1048
+ low: 0,
1049
+ medium: 1,
1050
+ high: 2,
1051
+ critical: 3
1052
+ };
1053
+ var DlpScanner = class {
1054
+ patterns;
1055
+ allowlistRegexps;
1056
+ severityThreshold;
1057
+ config;
1058
+ constructor(config) {
1059
+ this.config = config;
1060
+ this.severityThreshold = SEVERITY_ORDER[config.severity_threshold];
1061
+ this.patterns = [];
1062
+ this.allowlistRegexps = [];
1063
+ if (config.built_in.secrets) {
1064
+ for (const def of SECRET_PATTERNS) {
1065
+ this.patterns.push({ def });
1066
+ }
1067
+ }
1068
+ if (config.built_in.pii) {
1069
+ for (const def of PII_PATTERNS) {
1070
+ this.patterns.push({ def });
1071
+ }
1072
+ }
1073
+ for (const cp of config.custom_patterns) {
1074
+ const def = {
1075
+ name: cp.name,
1076
+ pattern: new RegExp(cp.pattern, "g"),
1077
+ severity: cp.severity,
1078
+ category: "custom"
1079
+ };
1080
+ this.patterns.push({ def, action: cp.action });
1081
+ }
1082
+ for (const compiled of this.patterns) {
1083
+ const override = config.pattern_overrides.get(compiled.def.name);
1084
+ if (override) {
1085
+ compiled.action = override;
1086
+ }
1087
+ }
1088
+ for (const entry of config.allowlist) {
1089
+ this.allowlistRegexps.push(new RegExp(entry.pattern));
1090
+ }
1091
+ }
1092
+ scan(text) {
1093
+ if (!this.config.enabled || text.length === 0) {
1094
+ return { hasMatches: false, matches: [] };
1095
+ }
1096
+ const matches = [];
1097
+ for (const compiled of this.patterns) {
1098
+ if (SEVERITY_ORDER[compiled.def.severity] < this.severityThreshold) {
1099
+ continue;
1100
+ }
1101
+ const regex = new RegExp(compiled.def.pattern.source, compiled.def.pattern.flags);
1102
+ let match;
1103
+ while ((match = regex.exec(text)) !== null) {
1104
+ const matched = match[1] ?? match[0];
1105
+ const start = match[1] ? match.index + match[0].indexOf(match[1]) : match.index;
1106
+ const end = start + matched.length;
1107
+ if (this.isAllowlisted(matched)) {
1108
+ continue;
1109
+ }
1110
+ matches.push({
1111
+ patternName: compiled.def.name,
1112
+ category: compiled.def.category,
1113
+ severity: compiled.def.severity,
1114
+ start,
1115
+ end,
1116
+ matched
1117
+ });
1118
+ }
1119
+ }
1120
+ return { hasMatches: matches.length > 0, matches };
1121
+ }
1122
+ getAction(direction) {
1123
+ if (direction === "input" && this.config.on_input) {
1124
+ return this.config.on_input;
1125
+ }
1126
+ if (direction === "output" && this.config.on_output) {
1127
+ return this.config.on_output;
1128
+ }
1129
+ return this.config.mode;
1130
+ }
1131
+ getPatternAction(match, direction) {
1132
+ const compiled = this.patterns.find((p) => p.def.name === match.patternName);
1133
+ if (compiled?.action) {
1134
+ return compiled.action;
1135
+ }
1136
+ return this.getAction(direction);
1137
+ }
1138
+ isAllowlisted(value) {
1139
+ for (const re of this.allowlistRegexps) {
1140
+ if (re.test(value)) return true;
1141
+ }
1142
+ return false;
1143
+ }
1144
+ };
1145
+
1146
+ // src/lib/dlp/masker.ts
1147
+ import { createHash } from "crypto";
1148
+ var DEFAULT_CONFIG = {
1149
+ strategy: "partial",
1150
+ show_chars: 4,
1151
+ placeholder: "***"
1152
+ };
1153
+ var DlpMasker = class {
1154
+ config;
1155
+ constructor(config) {
1156
+ this.config = { ...DEFAULT_CONFIG, ...config };
1157
+ }
1158
+ maskValue(value) {
1159
+ switch (this.config.strategy) {
1160
+ case "full":
1161
+ return this.config.placeholder;
1162
+ case "hash": {
1163
+ const hash = createHash("sha256").update(value).digest("hex").slice(0, 8);
1164
+ return `[REDACTED:${hash}]`;
1165
+ }
1166
+ case "partial":
1167
+ default: {
1168
+ if (value.length <= this.config.show_chars) {
1169
+ return this.config.placeholder;
1170
+ }
1171
+ return this.config.placeholder + value.slice(-this.config.show_chars);
1172
+ }
1173
+ }
1174
+ }
1175
+ maskText(text, matches) {
1176
+ if (matches.length === 0) return text;
1177
+ const sorted = [...matches].sort((a, b) => b.start - a.start);
1178
+ let result = text;
1179
+ for (const match of sorted) {
1180
+ const masked = this.maskValue(match.matched);
1181
+ result = result.slice(0, match.start) + masked + result.slice(match.end);
1182
+ }
1183
+ return result;
1184
+ }
1185
+ };
1186
+
824
1187
  // src/extensions/index.ts
825
1188
  var PATH_TOOLS = {
826
1189
  read: "path",
@@ -858,6 +1221,72 @@ function extractPath(toolName, input) {
858
1221
  const val = input[key];
859
1222
  return typeof val === "string" ? val : void 0;
860
1223
  }
1224
+ var ACTION_PRIORITY = { audit: 0, mask: 1, block: 2 };
1225
+ function extractDlpFields(toolName, input) {
1226
+ const fields = /* @__PURE__ */ new Map();
1227
+ switch (toolName) {
1228
+ case "bash": {
1229
+ const cmd = input["command"];
1230
+ if (typeof cmd === "string") fields.set("command", cmd);
1231
+ break;
1232
+ }
1233
+ case "write": {
1234
+ const content = input["content"];
1235
+ if (typeof content === "string") fields.set("content", content);
1236
+ const path = input["path"];
1237
+ if (typeof path === "string") fields.set("path", path);
1238
+ break;
1239
+ }
1240
+ case "edit": {
1241
+ const newStr = input["new_string"];
1242
+ if (typeof newStr === "string") fields.set("new_string", newStr);
1243
+ const oldStr = input["old_string"];
1244
+ if (typeof oldStr === "string") fields.set("old_string", oldStr);
1245
+ break;
1246
+ }
1247
+ default: {
1248
+ for (const [key, val] of Object.entries(input)) {
1249
+ if (typeof val === "string") fields.set(key, val);
1250
+ }
1251
+ }
1252
+ }
1253
+ return fields;
1254
+ }
1255
+ function resolveHighestAction(scanner, matches, direction) {
1256
+ let highest = "audit";
1257
+ for (const match of matches) {
1258
+ const action = scanner.getPatternAction(match, direction);
1259
+ if (ACTION_PRIORITY[action] > ACTION_PRIORITY[highest]) {
1260
+ highest = action;
1261
+ }
1262
+ }
1263
+ return highest;
1264
+ }
1265
+ function resolveDlpConfig(dlpConfig, role) {
1266
+ if (!dlpConfig?.enabled) return void 0;
1267
+ const roleOverride = dlpConfig.role_overrides?.[role];
1268
+ if (roleOverride?.enabled === false) return void 0;
1269
+ const patternOverrides = /* @__PURE__ */ new Map();
1270
+ return {
1271
+ enabled: true,
1272
+ mode: roleOverride?.mode ?? dlpConfig.mode ?? "audit",
1273
+ on_input: roleOverride?.on_input ?? dlpConfig.on_input,
1274
+ on_output: roleOverride?.on_output ?? dlpConfig.on_output,
1275
+ severity_threshold: dlpConfig.severity_threshold ?? "low",
1276
+ built_in: {
1277
+ secrets: dlpConfig.built_in?.secrets ?? true,
1278
+ pii: dlpConfig.built_in?.pii ?? true
1279
+ },
1280
+ custom_patterns: (dlpConfig.custom_patterns ?? []).map((cp) => ({
1281
+ name: cp.name,
1282
+ pattern: cp.pattern,
1283
+ severity: cp.severity,
1284
+ action: cp.action
1285
+ })),
1286
+ allowlist: dlpConfig.allowlist ?? [],
1287
+ pattern_overrides: patternOverrides
1288
+ };
1289
+ }
861
1290
  var piGovernance = (pi) => {
862
1291
  let config;
863
1292
  let policyEngine;
@@ -869,7 +1298,18 @@ var piGovernance = (pi) => {
869
1298
  let sessionId;
870
1299
  let budgetTracker;
871
1300
  let configWatcher;
872
- const stats = { allowed: 0, denied: 0, approvals: 0, dryRun: 0, budgetExceeded: 0 };
1301
+ let dlpScanner;
1302
+ let dlpMasker;
1303
+ const stats = {
1304
+ allowed: 0,
1305
+ denied: 0,
1306
+ approvals: 0,
1307
+ dryRun: 0,
1308
+ budgetExceeded: 0,
1309
+ dlpBlocked: 0,
1310
+ dlpDetected: 0,
1311
+ dlpMasked: 0
1312
+ };
873
1313
  pi.on("session_start", async (_event, ctx) => {
874
1314
  sessionId = ctx.sessionId;
875
1315
  const loaded = loadConfig();
@@ -877,7 +1317,45 @@ var piGovernance = (pi) => {
877
1317
  const chain = createIdentityChain(config.auth);
878
1318
  identity = await chain.resolve();
879
1319
  const rulesFile = config.policy?.yaml?.rules_file ?? "./governance-rules.yaml";
880
- policyEngine = new YamlPolicyEngine(rulesFile);
1320
+ if (existsSync2(rulesFile)) {
1321
+ policyEngine = new YamlPolicyEngine(rulesFile);
1322
+ } else {
1323
+ policyEngine = new YamlPolicyEngine({
1324
+ roles: {
1325
+ admin: {
1326
+ allowed_tools: ["all"],
1327
+ blocked_tools: [],
1328
+ prompt_template: "admin",
1329
+ execution_mode: "autonomous",
1330
+ human_approval: { required_for: [] },
1331
+ token_budget_daily: -1,
1332
+ allowed_paths: ["**"],
1333
+ blocked_paths: []
1334
+ },
1335
+ project_lead: {
1336
+ allowed_tools: ["all"],
1337
+ blocked_tools: [],
1338
+ prompt_template: "project-lead",
1339
+ execution_mode: "supervised",
1340
+ human_approval: { required_for: ["bash", "write"] },
1341
+ token_budget_daily: -1,
1342
+ allowed_paths: ["**"],
1343
+ blocked_paths: []
1344
+ },
1345
+ analyst: {
1346
+ allowed_tools: ["read", "grep", "find", "ls"],
1347
+ blocked_tools: ["write", "edit", "bash"],
1348
+ prompt_template: "analyst",
1349
+ execution_mode: "supervised",
1350
+ human_approval: { required_for: ["all"] },
1351
+ token_budget_daily: -1,
1352
+ allowed_paths: ["**"],
1353
+ blocked_paths: []
1354
+ }
1355
+ }
1356
+ });
1357
+ ctx.ui.notify(`Rules file not found: ${rulesFile} \u2014 using built-in defaults`, "warning");
1358
+ }
881
1359
  executionMode = policyEngine.getExecutionMode(identity.role);
882
1360
  const bashOverrides = policyEngine.getBashOverrides(identity.role);
883
1361
  bashClassifier = new BashClassifier(bashOverrides);
@@ -898,15 +1376,30 @@ var piGovernance = (pi) => {
898
1376
  }
899
1377
  const budget = policyEngine.getTokenBudget(identity.role);
900
1378
  budgetTracker = new BudgetTracker(budget);
1379
+ const dlpCfg = resolveDlpConfig(config.dlp, identity.role);
1380
+ if (dlpCfg) {
1381
+ dlpScanner = new DlpScanner(dlpCfg);
1382
+ dlpMasker = new DlpMasker(config.dlp?.masking);
1383
+ }
901
1384
  if (loaded.source !== "built-in") {
902
1385
  configWatcher = new ConfigWatcher(
903
1386
  loaded.source,
904
1387
  (newConfig) => {
905
1388
  config = newConfig;
906
1389
  const newRulesFile = newConfig.policy?.yaml?.rules_file ?? "./governance-rules.yaml";
907
- policyEngine = new YamlPolicyEngine(newRulesFile);
1390
+ if (existsSync2(newRulesFile)) {
1391
+ policyEngine = new YamlPolicyEngine(newRulesFile);
1392
+ }
908
1393
  const newOverrides = policyEngine.getBashOverrides(identity.role);
909
1394
  bashClassifier = new BashClassifier(newOverrides);
1395
+ const newDlpCfg = resolveDlpConfig(newConfig.dlp, identity.role);
1396
+ if (newDlpCfg) {
1397
+ dlpScanner = new DlpScanner(newDlpCfg);
1398
+ dlpMasker = new DlpMasker(newConfig.dlp?.masking);
1399
+ } else {
1400
+ dlpScanner = void 0;
1401
+ dlpMasker = void 0;
1402
+ }
910
1403
  audit.log({
911
1404
  sessionId,
912
1405
  event: "config_reloaded",
@@ -1054,6 +1547,63 @@ var piGovernance = (pi) => {
1054
1547
  return { block: true, reason: `Access denied to path: ${path}` };
1055
1548
  }
1056
1549
  }
1550
+ if (dlpScanner && dlpMasker) {
1551
+ const fields = extractDlpFields(toolName, input);
1552
+ const allMatches = [];
1553
+ for (const [, fieldValue] of fields) {
1554
+ const result = dlpScanner.scan(fieldValue);
1555
+ allMatches.push(...result.matches);
1556
+ }
1557
+ if (allMatches.length > 0) {
1558
+ const action = resolveHighestAction(dlpScanner, allMatches, "input");
1559
+ const patternNames = [...new Set(allMatches.map((m) => m.patternName))];
1560
+ const severities = [...new Set(allMatches.map((m) => m.severity))];
1561
+ const dlpMeta = {
1562
+ patterns: patternNames,
1563
+ severities,
1564
+ direction: "input",
1565
+ count: allMatches.length
1566
+ };
1567
+ if (action === "block") {
1568
+ stats.dlpBlocked++;
1569
+ await audit.log({
1570
+ ...baseRecord,
1571
+ event: "dlp_blocked",
1572
+ decision: "denied",
1573
+ reason: `DLP: ${patternNames.join(", ")} detected in input`,
1574
+ metadata: dlpMeta
1575
+ });
1576
+ return {
1577
+ block: true,
1578
+ reason: `DLP blocked: sensitive data detected (${patternNames.join(", ")})`
1579
+ };
1580
+ }
1581
+ if (action === "mask") {
1582
+ stats.dlpMasked++;
1583
+ for (const [fieldKey, fieldValue] of fields) {
1584
+ const fieldResult = dlpScanner.scan(fieldValue);
1585
+ if (fieldResult.hasMatches) {
1586
+ input[fieldKey] = dlpMasker.maskText(
1587
+ fieldValue,
1588
+ fieldResult.matches
1589
+ );
1590
+ }
1591
+ }
1592
+ await audit.log({
1593
+ ...baseRecord,
1594
+ event: "dlp_masked",
1595
+ metadata: { ...dlpMeta, strategy: dlpMasker["config"].strategy }
1596
+ });
1597
+ } else {
1598
+ stats.dlpDetected++;
1599
+ await audit.log({
1600
+ ...baseRecord,
1601
+ event: "dlp_detected",
1602
+ metadata: dlpMeta
1603
+ });
1604
+ }
1605
+ }
1606
+ }
1057
1607
  if (toolName !== "bash" && policyEngine.requiresApproval(identity.role, toolName)) {
1058
1608
  if (approvalFlow) {
1059
1609
  stats.approvals++;
@@ -1081,6 +1631,44 @@ var piGovernance = (pi) => {
1081
1631
  return void 0;
1082
1632
  });
1083
1633
  pi.on("tool_result", async (event, _ctx) => {
1634
+ if (dlpScanner && dlpMasker && event.output) {
1635
+ const result = dlpScanner.scan(event.output);
1636
+ if (result.hasMatches) {
1637
+ const action = resolveHighestAction(dlpScanner, result.matches, "output");
1638
+ const patternNames = [...new Set(result.matches.map((m) => m.patternName))];
1639
+ const severities = [...new Set(result.matches.map((m) => m.severity))];
1640
+ const dlpMeta = {
1641
+ patterns: patternNames,
1642
+ severities,
1643
+ direction: "output",
1644
+ count: result.matches.length
1645
+ };
1646
+ if (action === "mask" || action === "block") {
1647
+ stats.dlpMasked++;
1648
+ event.output = dlpMasker.maskText(event.output, result.matches);
1649
+ await audit.log({
1650
+ sessionId,
1651
+ event: "dlp_masked",
1652
+ userId: identity.userId,
1653
+ role: identity.role,
1654
+ orgUnit: identity.orgUnit,
1655
+ tool: event.toolName,
1656
+ metadata: { ...dlpMeta, strategy: dlpMasker["config"].strategy }
1657
+ });
1658
+ } else {
1659
+ stats.dlpDetected++;
1660
+ await audit.log({
1661
+ sessionId,
1662
+ event: "dlp_detected",
1663
+ userId: identity.userId,
1664
+ role: identity.role,
1665
+ orgUnit: identity.orgUnit,
1666
+ tool: event.toolName,
1667
+ metadata: dlpMeta
1668
+ });
1669
+ }
1670
+ }
1671
+ }
1084
1672
  await audit.log({
1085
1673
  sessionId,
1086
1674
  event: "tool_result",
@@ -1128,6 +1716,9 @@ var piGovernance = (pi) => {
1128
1716
  ` Approvals: ${stats.approvals}`,
1129
1717
  ` Dry-run blocks: ${stats.dryRun}`,
1130
1718
  ` Budget exceeded: ${stats.budgetExceeded}`,
1719
+ ` DLP blocked: ${stats.dlpBlocked}`,
1720
+ ` DLP detected: ${stats.dlpDetected}`,
1721
+ ` DLP masked: ${stats.dlpMasked}`,
1131
1722
  "",
1132
1723
  "Audit Events:",
1133
1724
  ...[...summary.entries()].map(([k, v]) => ` ${k}: ${v}`)