agent-sanitizer 2.66.0 → 2.66.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.
Files changed (2) hide show
  1. package/THREAT-MODEL.md +36 -30
  2. package/package.json +3 -1
package/THREAT-MODEL.md CHANGED
@@ -1020,38 +1020,44 @@ up to the constant factors a parse pays, and one sort where findings or splice
1020
1020
  ranges arrive in an order the caller does not promise (`foldConfusables`,
1021
1021
  `mergeRanges`). The bound that matters is that nothing is _quadratic_ — a sort
1022
1022
  of the findings in a 1 MB paste is milliseconds, a quadratic rescan of it is
1023
- minutes — and quadratic is what the two gates below are calibrated to catch,
1023
+ minutes — and quadratic is what the two tiers below are built to catch,
1024
1024
  because neither can see what the other does.
1025
1025
 
1026
- **Static — every pattern.** `tests/test_redos_js_static_guard.py` drives
1027
- regexploit over an inventory that `scripts/extract-js-regexes.mjs` extracts by
1028
- walking the TypeScript AST of every shipped `.mjs` (not by matching regex source
1029
- with a regex), and fails on any pattern with super-linear backtracking. A
1030
- `RegExp(…)` built at runtime must be statically resolvable or carry a written
1031
- exemption, so a new dynamic pattern is a red build rather than a hole.
1032
- `tests/secrets/test_redos_static_guard.py` is the twin over the Python engine,
1033
- and `eslint-plugin-redos` reports the same class in the editor, before CI.
1034
-
1035
- **Empirical — everything the static gates cannot see.** Two shapes of quadratic
1036
- carry no backtracking at all, so no pattern analyzer will ever report them:
1037
-
1038
- - an **unanchored** pattern retried at every start offset, each attempt linear
1039
- and none of them backtracking — `(?=.*[A-Z])(?=.*[0-9])` against a long value
1040
- carrying neither class, or a `[^>]*$` tail rescanned from every `<`;
1041
- - work that is not a pattern at all — a string rebuilt once per finding, a
1042
- prefix re-sliced inside a loop, a re-walk of an already-walked node.
1043
-
1044
- `test/algorithmic-complexity.test.mjs` runs an entry point over an adversarial
1045
- input at four sizes spanning 8x and fits how its CPU cost grows — an exponent,
1046
- classified against the one pair of thresholds every growth gate shares
1047
- (`test/helpers/cpu-timing.mjs`, twin `tests/_growth.py`) — with a
1048
- known-quadratic specimen as the control that proves the harness can still see
1049
- one. Each case also asserts, at the size it measures, that
1050
- the input still reaches the code the case is about — a length gate answering
1051
- first is how such a case quietly degrades into timing nothing.
1052
- `tests/secrets/test_algorithmic_complexity.py` is the twin over the Python
1053
- engine. `test/hook-latency.test.mjs` covers the three blocking hook paths
1054
- instead, in cost per calibration pass, with one wall-clock ceiling.
1026
+ **Deterministic — every pattern, no clock.** `tests/test_redos_js_static_guard.py`
1027
+ drives regexploit over an inventory that `scripts/extract-js-regexes.mjs`
1028
+ extracts by walking the TypeScript AST of every shipped `.mjs` (not by matching
1029
+ regex source with a regex), and fails on any pattern with super-linear
1030
+ backtracking. A `RegExp(…)` built at runtime must be statically resolvable or
1031
+ carry a written exemption, so a new dynamic pattern is a red build rather than a
1032
+ hole. `tests/secrets/test_redos_static_guard.py` is the twin over the Python
1033
+ engine. In `pnpm lint`, `eslint-plugin-redos` (recheck) and
1034
+ `eslint-plugin-regexp`'s `no-super-linear-backtracking` and
1035
+ `no-super-linear-move` read every shipped regex literal — the last of them
1036
+ reports the shape that carries no backtracking at all, an **unanchored**
1037
+ pattern retried at every start offset (`(?=.*[A-Z])(?=.*[0-9])` against a long
1038
+ value carrying neither class). `test/regex-recheck.test.mjs` hands every
1039
+ pattern a shipped module actually compiles at runtime to recheck. Every recheck
1040
+ verdict runs with its wall-clock incubation and attack timeouts off, so it
1041
+ rests on step counts alone and no machine's speed can change it.
1042
+
1043
+ **Budgets — everything that is not a pattern.** Work that is not a regex at
1044
+ all — a string rebuilt once per finding, a prefix re-sliced inside a loop, a
1045
+ re-walk of an already-walked node, a parser's own look-back — no pattern
1046
+ analyzer will ever report. `test/algorithmic-complexity.test.mjs` runs each
1047
+ entry point ONCE, after a warm-up, over an adversarial input sized so the
1048
+ linear implementation costs tens to hundreds of milliseconds, against an
1049
+ absolute CPU ceiling at least ten times that reading (`assertUnderBudget` in
1050
+ `test/helpers/budget.mjs`, twin `tests/_budget.py`). A quadratic at that size
1051
+ overshoots the ceiling by another order of magnitude, so no runner is slow or
1052
+ busy enough to blur the two; a constant-factor slowdown passes, by design. Each
1053
+ case also asserts, at the size it measures, that the input still reaches the
1054
+ code the case is about — a length gate answering first is how such a case
1055
+ quietly degrades into timing nothing. `tests/secrets/test_algorithmic_complexity.py`
1056
+ is the twin over the Python engine. `test/hook-latency.test.mjs` holds the
1057
+ three blocking hook paths to absolute ceilings per 256 KB call, read
1058
+ uninstrumented in CI. Every test verdict in the repo that reads a clock goes
1059
+ through that one helper (`test/real-clock-thresholds.test.mjs` and its Python
1060
+ twin enforce it).
1055
1061
 
1056
1062
  **Idioms new code is held to.** Each is the fix for a defect one of the gates
1057
1063
  caught:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sanitizer",
3
- "version": "2.66.0",
3
+ "version": "2.66.1",
4
4
  "description": "Defend an agent against hidden-content injection: strip payload-capable invisible Unicode and ANSI, splice out human-invisible HTML, and flag data-exfil URLs in untrusted text before any model sees it.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -62,11 +62,13 @@
62
62
  "esbuild": "0.28.1",
63
63
  "eslint": "10.4.0",
64
64
  "eslint-plugin-redos": "4.5.0",
65
+ "eslint-plugin-regexp": "3.3.1",
65
66
  "fast-check": "4.8.0",
66
67
  "globals": "17.6.0",
67
68
  "lint-staged": "^17.0.5",
68
69
  "micromark-extension-gfm-autolink-literal": "2.1.0",
69
70
  "prettier": "^3.0.0",
71
+ "recheck": "4.5.0",
70
72
  "rehype-parse": "9.0.1",
71
73
  "remark-gfm": "4.0.1",
72
74
  "smol-toml": "^1.7.1",