driftproof 0.11.0 → 0.11.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.
package/README.md CHANGED
@@ -305,7 +305,7 @@ A receipt is the unit of evidence — one JSON document conforming to
305
305
  "model_release_date": "2025-10-01",
306
306
  "provider": "anthropic",
307
307
  "surface": "claude-cli",
308
- "runner_version": "0.11.0",
308
+ "runner_version": "0.11.1",
309
309
  "date_utc": "2026-07-27T…Z",
310
310
  "registry": "registered",
311
311
  "transcripts": "hashes-only",
@@ -384,7 +384,7 @@ jobs:
384
384
  runs-on: ubuntu-latest
385
385
  steps:
386
386
  - uses: actions/checkout@v4
387
- - uses: driftproofhq/driftproof@v0.11.0
387
+ - uses: driftproofhq/driftproof@v0.11.1
388
388
  with:
389
389
  skill-dir: skills/my-skill
390
390
  models: claude-haiku-4-5
package/config.js CHANGED
@@ -9,7 +9,7 @@ const PROJECT_NAME = 'driftproof';
9
9
  // Bumped whenever the runner's behaviour or receipt-generation semantics change
10
10
  // in a way that could affect results. Recorded into every receipt as
11
11
  // run.runner_version so a receipt is reproducible against a known engine.
12
- const RUNNER_VERSION = '0.11.0';
12
+ const RUNNER_VERSION = '0.11.1';
13
13
 
14
14
  // The eval format we CONSUME (we deliberately do not invent our own).
15
15
  const SUITE_FORMAT = 'agentskills.io/evals';
package/lib/hygiene.js CHANGED
@@ -23,6 +23,8 @@
23
23
  // Patterns are written so a regex literal cannot match its own text: a
24
24
  // metacharacter or a character class follows each fixed prefix.
25
25
 
26
+ const zlib = require('zlib');
27
+
26
28
  const dec = (b64) => JSON.parse(Buffer.from(b64, 'base64').toString('utf8'));
27
29
 
28
30
  // Build-box host + private IP, base64 so the plaintext never lands in a file.
@@ -54,7 +56,70 @@ function scanPath(rel) {
54
56
  return /(^|\/)\.env(\.|$)/.test(rel) ? [{ file: rel, kind: 'env-file' }] : [];
55
57
  }
56
58
 
59
+ // A PNG'S TEXT-BEARING REGIONS (spec 038 A-038-5, the classification spec 027 already made for the
60
+ // repository gate's confidentiality scan, applied here).
61
+ //
62
+ // THE THREAT MODEL IS ACCIDENTAL PLAINTEXT DISCLOSURE by a person or an agent writing the tree
63
+ // (CONSTITUTION § Threat model). A PNG carries text in its tEXt, iTXt and zTXt chunks, and carries
64
+ // pixels in IDAT, which is deflate output: nobody types into it, and a run of bytes that happens to
65
+ // decode as an address there is a coincidence of compression, not a disclosure. Scanned as one
66
+ // string, a 2 MB screenshot is two million chances of that coincidence, and it has fired twice on
67
+ // this repository's own committed captures.
68
+ //
69
+ // So: every chunk but IDAT is scanned, chunk type names included, and IDAT is not. A name RENDERED
70
+ // INTO a screenshot is pixels, which no byte scan sees either way - that is the gap this scan has
71
+ // always had, and it is unchanged. Everything that is not a PNG is scanned whole, as before.
72
+ //
73
+ // FAIL CLOSED WHERE THE FILE STOPS BEING A PNG (spec 038 approval F-3). Bytes after IEND are no chunk
74
+ // at all, and a chunk whose length runs past the end of the file is a truncated file: either way the
75
+ // remainder, from where the walk lost the structure, is scanned whole, as every file was before this
76
+ // classification. And a text chunk is read in the encoding it carries text in: a zTXt chunk's text
77
+ // and a compressed iTXt chunk's text are inflated and scanned as well as their raw bytes. A chunk
78
+ // that does not inflate is scanned raw, which it already was.
79
+ const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
80
+ const INFLATE_CAP = 16 * 1024 * 1024;
81
+ function inflated(data) {
82
+ try { return zlib.inflateSync(data, { maxOutputLength: INFLATE_CAP }).toString('latin1'); } catch (_e) { return null; }
83
+ }
84
+ function pngTextBearing(buf) {
85
+ const parts = [];
86
+ const rest = (from) => { parts.push(buf.slice(from).toString('latin1')); return parts.join('\n'); };
87
+ let off = 8;
88
+ while (off + 8 <= buf.length) {
89
+ const len = buf.readUInt32BE(off);
90
+ const type = buf.slice(off + 4, off + 8).toString('latin1');
91
+ const start = off + 8;
92
+ const end = start + len;
93
+ if (end + 4 > buf.length) return rest(off); // a truncated chunk: it and all after it, whole
94
+ const data = buf.slice(start, end);
95
+ if (type !== 'IDAT') parts.push(type, data.toString('latin1'));
96
+ if (type === 'zTXt') parts.push(zTXtText(data));
97
+ if (type === 'iTXt') parts.push(iTXtText(data));
98
+ off = end + 4;
99
+ if (type === 'IEND') return rest(off); // bytes after IEND, whole
100
+ }
101
+ return rest(off); // fewer than a chunk header's bytes, no IEND
102
+ }
103
+ // keyword \0 method data
104
+ function zTXtText(data) {
105
+ const z = data.indexOf(0);
106
+ return (z >= 0 && inflated(data.slice(z + 2))) || '';
107
+ }
108
+ // keyword \0 flag method language \0 translated keyword \0 text; the text is deflated when flag is 1
109
+ function iTXtText(data) {
110
+ const z = data.indexOf(0);
111
+ if (z < 0 || data[z + 1] !== 1) return '';
112
+ const lang = data.indexOf(0, z + 3);
113
+ const tr = lang >= 0 ? data.indexOf(0, lang + 1) : -1;
114
+ return (tr >= 0 && inflated(data.slice(tr + 1))) || '';
115
+ }
116
+
57
117
  function scanContent(rel, content) {
118
+ // A reader may hand this bytes rather than text. A PNG is then read as its text-bearing regions;
119
+ // anything else is decoded and scanned whole.
120
+ if (Buffer.isBuffer(content)) {
121
+ return scanContent(rel, content.slice(0, 8).equals(PNG_SIG) ? pngTextBearing(content) : content.toString('utf8'));
122
+ }
58
123
  const hits = [];
59
124
  for (const p of PATTERNS) {
60
125
  const m = content.match(p.re);
@@ -104,10 +169,10 @@ function scanFiles(files, read, readLink) {
104
169
  }
105
170
  let c;
106
171
  try { c = read(rel); } catch (_e) { continue; }
107
- if (typeof c !== 'string') continue;
172
+ if (typeof c !== 'string' && !Buffer.isBuffer(c)) continue;
108
173
  hits.push(...scanContent(rel, c));
109
174
  }
110
175
  return hits;
111
176
  }
112
177
 
113
- module.exports = { PATTERNS, EMAIL_ALLOW, HOSTIP, scanPath, scanContent, scanFiles };
178
+ module.exports = { PATTERNS, EMAIL_ALLOW, HOSTIP, PNG_SIG, pngTextBearing, scanPath, scanContent, scanFiles };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "driftproof",
3
- "version": "0.11.0",
3
+ "version": "0.11.1",
4
4
  "description": "A dated proof that this skill, this hash, this model, still helps: run a skill's eval suite with and without the skill across model versions, emit hash-verified dated receipts, and diff receipts into drift reports.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [