@grwnd/pi-governance 1.8.0 → 1.9.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.
package/README.md CHANGED
@@ -38,6 +38,7 @@ pi install npm:@grwnd/pi-governance
38
38
  - **Audit** — Every decision logged as structured JSON
39
39
  - **HITL** — Human approval for sensitive operations
40
40
  - **Budgets** — Per-role tool invocation limits
41
+ - **Config self-protection** — Agents cannot modify their own governance files
41
42
 
42
43
  ## Customize
43
44
 
@@ -1551,15 +1551,15 @@ function sendJson(res, status, data) {
1551
1551
  res.end(JSON.stringify(data));
1552
1552
  }
1553
1553
  function readBody(req) {
1554
- return new Promise((resolve, reject) => {
1554
+ return new Promise((resolve2, reject) => {
1555
1555
  const chunks = [];
1556
1556
  req.on("data", (chunk) => chunks.push(chunk));
1557
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1557
+ req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf-8")));
1558
1558
  req.on("error", reject);
1559
1559
  });
1560
1560
  }
1561
1561
  function startWizardServer(options) {
1562
- return new Promise((resolve, reject) => {
1562
+ return new Promise((resolve2, reject) => {
1563
1563
  let shutdownTimer;
1564
1564
  const server = (0, import_node_http.createServer)((req, res) => {
1565
1565
  setCorsHeaders(res);
@@ -1649,7 +1649,7 @@ function startWizardServer(options) {
1649
1649
  shutdownTimer = setTimeout(() => {
1650
1650
  closeServer();
1651
1651
  }, AUTO_SHUTDOWN_MS);
1652
- resolve({ port: addr.port, close: closeServer });
1652
+ resolve2({ port: addr.port, close: closeServer });
1653
1653
  });
1654
1654
  });
1655
1655
  }
@@ -1708,6 +1708,7 @@ __export(extensions_exports, {
1708
1708
  });
1709
1709
  module.exports = __toCommonJS(extensions_exports);
1710
1710
  var import_node_fs3 = require("fs");
1711
+ var import_node_path3 = require("path");
1711
1712
 
1712
1713
  // src/lib/config/loader.ts
1713
1714
  var import_fs = require("fs");
@@ -2181,7 +2182,16 @@ var DANGEROUS_PATTERNS = [
2181
2182
  // Compiler/build (can execute arbitrary code)
2182
2183
  /\bmake\s/,
2183
2184
  /\bgcc\b/,
2184
- /\bg\+\+/
2185
+ /\bg\+\+/,
2186
+ // Governance config tampering — shell-based writes to governance files
2187
+ /(cat|echo|printf)\s.*>\s*.*governance(-rules)?\.yaml/,
2188
+ /\btee\s+.*governance(-rules)?\.yaml/,
2189
+ /sed\s+-i.*governance(-rules)?\.yaml/,
2190
+ /(cp|mv|rm)\s.*governance(-rules)?\.yaml/,
2191
+ /(cat|echo|printf)\s.*>\s*.*\.pi\/governance/,
2192
+ /\btee\s+.*\.pi\/governance/,
2193
+ /sed\s+-i.*\.pi\/governance/,
2194
+ /(cp|mv|rm)\s.*\.pi\/governance/
2185
2195
  ];
2186
2196
 
2187
2197
  // src/lib/bash/classifier.ts
@@ -2976,7 +2986,9 @@ var piGovernance = (pi) => {
2976
2986
  let configWatcher;
2977
2987
  let dlpScanner;
2978
2988
  let dlpMasker;
2989
+ let protectedPaths = /* @__PURE__ */ new Set();
2979
2990
  const stats = {
2991
+ configTampered: 0,
2980
2992
  allowed: 0,
2981
2993
  denied: 0,
2982
2994
  approvals: 0,
@@ -2990,6 +3002,15 @@ var piGovernance = (pi) => {
2990
3002
  sessionId = ctx.sessionId;
2991
3003
  const loaded = loadConfig();
2992
3004
  config = loaded.config;
3005
+ const paths = /* @__PURE__ */ new Set();
3006
+ if (loaded.source !== "built-in") {
3007
+ paths.add((0, import_node_path3.resolve)(loaded.source));
3008
+ }
3009
+ const rulesFileCfg = config.policy?.yaml?.rules_file ?? "./governance-rules.yaml";
3010
+ paths.add((0, import_node_path3.resolve)(rulesFileCfg));
3011
+ paths.add((0, import_node_path3.resolve)(ctx.workingDirectory, ".pi/governance.yaml"));
3012
+ paths.add((0, import_node_path3.resolve)(ctx.workingDirectory, "governance-rules.yaml"));
3013
+ protectedPaths = paths;
2993
3014
  const chain = createIdentityChain(config.auth);
2994
3015
  identity = await chain.resolve();
2995
3016
  const rulesFile = config.policy?.yaml?.rules_file ?? "./governance-rules.yaml";
@@ -3119,6 +3140,22 @@ var piGovernance = (pi) => {
3119
3140
  tool: toolName,
3120
3141
  input: params
3121
3142
  };
3143
+ if (WRITE_TOOLS.has(toolName)) {
3144
+ const filePath = extractPath(toolName, input);
3145
+ if (filePath && protectedPaths.has((0, import_node_path3.resolve)(filePath))) {
3146
+ stats.configTampered++;
3147
+ await audit.log({
3148
+ ...baseRecord,
3149
+ event: "config_tampered",
3150
+ decision: "denied",
3151
+ reason: `Config self-protection: write to governance file blocked (${filePath})`
3152
+ });
3153
+ return {
3154
+ block: true,
3155
+ reason: `Governance config files are protected and cannot be modified by agents`
3156
+ };
3157
+ }
3158
+ }
3122
3159
  if (executionMode === "dry_run") {
3123
3160
  stats.dryRun++;
3124
3161
  await audit.log({
@@ -3394,6 +3431,7 @@ var piGovernance = (pi) => {
3394
3431
  ` Approvals: ${stats.approvals}`,
3395
3432
  ` Dry-run blocks: ${stats.dryRun}`,
3396
3433
  ` Budget exceeded: ${stats.budgetExceeded}`,
3434
+ ` Config tampered: ${stats.configTampered}`,
3397
3435
  ` DLP blocked: ${stats.dlpBlocked}`,
3398
3436
  ` DLP detected: ${stats.dlpDetected}`,
3399
3437
  ` DLP masked: ${stats.dlpMasked}`,