agent-sanitizer 2.65.0 → 2.66.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
|
@@ -227,6 +227,11 @@ guessing if the marketplace was never added. To pull a release by hand instead:
|
|
|
227
227
|
|
|
228
228
|
`plugin/README.md` has the managed-settings form for enabling it fleet-wide.
|
|
229
229
|
|
|
230
|
+
The plugin installs from this repository's `plugin-dist` branch, which CI
|
|
231
|
+
builds from each release tag; `main` carries the sources only, so a checkout
|
|
232
|
+
runs `pnpm install` (and `pnpm build:redactor` for the zipapp) to get a
|
|
233
|
+
runnable `plugin/`.
|
|
234
|
+
|
|
230
235
|
To wire them yourself instead, one entry dispatches every mode on `--hook=`:
|
|
231
236
|
|
|
232
237
|
```jsonc
|
package/THREAT-MODEL.md
CHANGED
|
@@ -853,31 +853,49 @@ sanitizes (see `plugin/scripts/provision-hook-binary.sh`). That is the
|
|
|
853
853
|
product's only path that fetches an executable over the network and later runs
|
|
854
854
|
it, so what anchors its trust is worth stating exactly.
|
|
855
855
|
|
|
856
|
-
- **The anchor
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
the
|
|
856
|
+
- **The anchor travels with the plugin, not on the wire.**
|
|
857
|
+
`plugin/dist/hooks/hook-binaries.sha256` ships in the installed plugin, and the
|
|
858
|
+
SessionStart provisioner refuses to install any download that does not hash
|
|
859
|
+
to the digest it pins for that platform. A compromised or substituted release
|
|
860
|
+
asset alone therefore cannot be installed: it fails the comparison, is
|
|
861
|
+
deleted, and the launcher keeps degrading loudly through node.
|
|
862
|
+
- **The installed plugin is the `plugin-dist` branch, and no human reviews it.**
|
|
863
|
+
`.claude-plugin/marketplace.json` installs from `plugin-dist`, whose commits
|
|
864
|
+
`.github/workflows/plugin-dist.yaml` writes: it checks out a release tag (a
|
|
865
|
+
`main` commit that went through review and the required checks), builds the
|
|
866
|
+
bundle, the zipapp, the generated shell libs and the manifest, compiles the
|
|
867
|
+
binaries, uploads them only after `sha256sum --check` against that manifest
|
|
868
|
+
passes, and then pushes the build. The manifest and the binaries come from the
|
|
869
|
+
same run, so the digest proves the download is what that run built — not that
|
|
870
|
+
an independent rebuild agrees. What the user runs is therefore exactly as
|
|
871
|
+
trustworthy as whatever can write `plugin-dist`.
|
|
872
|
+
- **Who can write `plugin-dist`.** The workflow pushes with
|
|
873
|
+
`TEMPLATE_SYNC_TOKEN_ORG` (the stock `GITHUB_TOKEN` where that secret is
|
|
874
|
+
unset), and the push is fast-forward only. Pushes are restricted to that
|
|
875
|
+
identity by a branch ruleset on `refs/heads/plugin-dist` blocking creation,
|
|
876
|
+
update, deletion and non-fast-forward for everyone but the repository's admin
|
|
877
|
+
bypass actors. That ruleset is repository configuration, not code in this
|
|
878
|
+
tree, so nothing here creates it; `plugin-dist-ruleset.yaml` only detects
|
|
879
|
+
its absence, failing daily and on any marketplace change while no active
|
|
880
|
+
`update` rule covers the branch. Without it, any collaborator with write
|
|
881
|
+
access can push the branch and ship code to every installed user. With it, the residual writers are the
|
|
882
|
+
admins and anything that can run a workflow holding that secret — a workflow
|
|
883
|
+
edited onto a branch of this repository can read it, so write access to the
|
|
884
|
+
repository still reaches `plugin-dist` through Actions, as it reaches `main`'s
|
|
885
|
+
release path.
|
|
886
|
+
- **A manifest that disagrees with its bundle installs nothing.** The bake
|
|
887
|
+
refuses to stage a manifest whose `# bundle=` pin is not the bundle it built,
|
|
888
|
+
and the provisioner compares its own bundle against that pin and refuses on a
|
|
889
|
+
mismatch, so a hand-rebuilt bundle beside an older manifest degrades to the
|
|
890
|
+
node path rather than running a binary that is not the code the install ships.
|
|
891
|
+
A binary provisioned under an earlier release stays installed but loses its
|
|
892
|
+
install stamp, and the launcher runs a binary whose stamp does not name the
|
|
893
|
+
shipped manifest only on a host where no node is found — there an older
|
|
894
|
+
sanitizer still beats an unsanitized call.
|
|
895
|
+
- **Not covered: an attacker who can write `plugin-dist`, or the workflow that
|
|
896
|
+
writes it.** There is no signature; the manifest is trusted because it arrives
|
|
897
|
+
in the same commit as the rest of the plugin. Rewriting it is rewriting the
|
|
898
|
+
plugin, which is outside this boundary.
|
|
881
899
|
- **Not covered: re-verification at exec time.** The digest is checked once, at
|
|
882
900
|
install. The launcher then execs
|
|
883
901
|
`${CLAUDE_PLUGIN_DATA}/hook-binary/agent-sanitizer-hooks` on every hook
|
|
@@ -901,12 +919,12 @@ it, so what anchors its trust is worth stating exactly.
|
|
|
901
919
|
location.** The provisioned redactor venv (`${CLAUDE_PLUGIN_DATA}/venv` and its
|
|
902
920
|
`bin/`, whose daemon receives every secret-bearing payload) is handed to the
|
|
903
921
|
client only when `trusted_dir` vouches for both directories; otherwise the
|
|
904
|
-
client redacts through the
|
|
922
|
+
client redacts through the shipped zipapp. The degraded-warning marker root
|
|
905
923
|
under `TMPDIR` is read only when this user owns it with owner-only write, so a
|
|
906
924
|
squatted shared-temp entry cannot pre-suppress the warning that a call ran
|
|
907
925
|
unsanitized.
|
|
908
926
|
- **Opting out.** `AGENT_SANITIZER_HOOK_BINARY=0` never downloads and never runs
|
|
909
|
-
a binary. That leaves the node path running the
|
|
927
|
+
a binary. That leaves the node path running the shipped bundle and the
|
|
910
928
|
compile cache above; `AGENT_SANITIZER_COMPILE_CACHE=0` turns the cache off as
|
|
911
929
|
well, and an operator's own `NODE_COMPILE_CACHE` is used unchanged.
|
|
912
930
|
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
* cannot reach (it runs its no-node arm on shell builtins alone).
|
|
26
26
|
*
|
|
27
27
|
* THE single source of truth for the closed set. The shell shims cannot import
|
|
28
|
-
* it, so `plugin/scripts/lib/fail-open.sh` is GENERATED from it
|
|
29
|
-
* `scripts/gen-fail-open-lib.mjs
|
|
30
|
-
*
|
|
28
|
+
* it, so `plugin/scripts/lib/fail-open.sh` is GENERATED from it at build time
|
|
29
|
+
* by `scripts/gen-fail-open-lib.mjs`; plugin/test/fail-open-parity.test.mjs
|
|
30
|
+
* asserts the generated lib decides as failOpenEnabled(). Everything else that spells the set
|
|
31
31
|
* out by hand is an implementation that must appear in the parity table in
|
|
32
32
|
* tests/test_safe_launch.py.
|
|
33
33
|
*/
|
|
@@ -115,7 +115,7 @@ export const DEFAULT_SOCKET_PATH = resolveSocketPath(process.env, currentUid());
|
|
|
115
115
|
* sets it to the provisioned venv's console script when one exists, and
|
|
116
116
|
* tests point it at dead paths to drive the fail-closed arm; a fallback
|
|
117
117
|
* from an explicit setting would turn those arms into silent passes).
|
|
118
|
-
* 2. The
|
|
118
|
+
* 2. The shipped self-contained zipapp (`plugin/dist/redactor/daemon.pyz`) —
|
|
119
119
|
* the guaranteed floor: it needs only a python3 on PATH, so a cold start
|
|
120
120
|
* with no venv and no network still redacts instead of failing closed on
|
|
121
121
|
* every tool call. At `_AGENT_SANITIZER_REDACTOR_PYZ` when the launcher
|
|
@@ -129,7 +129,7 @@ function daemonCommand() {
|
|
|
129
129
|
const configured = readKnob("_AGENT_SANITIZER_REDACTOR_DAEMON");
|
|
130
130
|
if (configured) return [configured];
|
|
131
131
|
// Inside the built bundle import.meta.url is .../plugin/dist/hooks/<bundle>,
|
|
132
|
-
// so the
|
|
132
|
+
// so the shipped zipapp sits one level up; from the raw sources no such
|
|
133
133
|
// sibling exists and the check falls through.
|
|
134
134
|
const pyz =
|
|
135
135
|
readKnob("_AGENT_SANITIZER_REDACTOR_PYZ") ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-sanitizer",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.66.0",
|
|
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": {
|
|
@@ -249,12 +249,13 @@
|
|
|
249
249
|
"check": "tsc --noEmit && tsc -p tsconfig.hooks.json --noEmit",
|
|
250
250
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.hooks.json --noEmit",
|
|
251
251
|
"build:types": "tsc -p tsconfig.build.json && tsc -p tsconfig.build-hooks.json",
|
|
252
|
+
"build": "pnpm build:plugin && node plugin/scripts/build-redactor-pyz.mjs --if-missing",
|
|
253
|
+
"build:plugin": "node plugin/scripts/build-plugin.mjs",
|
|
254
|
+
"build:redactor": "node plugin/scripts/build-redactor-pyz.mjs",
|
|
252
255
|
"gen:joining-type": "node scripts/gen-joining-type.mjs",
|
|
253
256
|
"gen:unicode": "node scripts/gen-unicode.mjs",
|
|
254
257
|
"gen:gfm-autolink": "node scripts/gen-gfm-autolink.mjs",
|
|
255
258
|
"gen:unicode-properties": "node scripts/gen-unicode-properties.mjs",
|
|
256
|
-
"gen:fail-open-lib": "node scripts/gen-fail-open-lib.mjs",
|
|
257
|
-
"gen:node-floor-lib": "node scripts/gen-node-floor-lib.mjs",
|
|
258
259
|
"gen:hook-binaries": "node plugin/scripts/build-hook-binaries.mjs",
|
|
259
260
|
"lint": "eslint .",
|
|
260
261
|
"test:mutation": "node scripts/mutate.mjs",
|
|
@@ -45,9 +45,9 @@ export function readKnob<K extends keyof Knobs>(name: K, env?: NodeJS.ProcessEnv
|
|
|
45
45
|
* cannot reach (it runs its no-node arm on shell builtins alone).
|
|
46
46
|
*
|
|
47
47
|
* THE single source of truth for the closed set. The shell shims cannot import
|
|
48
|
-
* it, so `plugin/scripts/lib/fail-open.sh` is GENERATED from it
|
|
49
|
-
* `scripts/gen-fail-open-lib.mjs
|
|
50
|
-
*
|
|
48
|
+
* it, so `plugin/scripts/lib/fail-open.sh` is GENERATED from it at build time
|
|
49
|
+
* by `scripts/gen-fail-open-lib.mjs`; plugin/test/fail-open-parity.test.mjs
|
|
50
|
+
* asserts the generated lib decides as failOpenEnabled(). Everything else that spells the set
|
|
51
51
|
* out by hand is an implementation that must appear in the parity table in
|
|
52
52
|
* tests/test_safe_launch.py.
|
|
53
53
|
*/
|