@windyroad/risk-scorer 0.13.2 → 0.13.4
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.
|
@@ -21,3 +21,10 @@ EXTERNAL_COMMS_POLICY_FILE=RISK-POLICY.md
|
|
|
21
21
|
# Whether to run the leak-pattern pre-filter (lib/leak-detect.sh). Risk evaluator
|
|
22
22
|
# checks confidential-information leaks; voice-tone evaluator does not.
|
|
23
23
|
EXTERNAL_COMMS_LEAK_PREFILTER=yes
|
|
24
|
+
|
|
25
|
+
# Surfaces this evaluator's policy disclaims (P360). EMPTY for the risk evaluator:
|
|
26
|
+
# leak detection on commit-message bodies is meaningful (a credential committed to
|
|
27
|
+
# git history is still a leak), so risk-scorer gates every detected surface. The
|
|
28
|
+
# voice-tone evaluator sets this to `git-commit-message` because its policy doc
|
|
29
|
+
# excludes commit messages; the divergence lives in each package's .conf.
|
|
30
|
+
EXTERNAL_COMMS_SKIP_SURFACES=
|
|
@@ -78,6 +78,10 @@ source "$SCRIPT_DIR/lib/external-comms-key.sh"
|
|
|
78
78
|
# EXTERNAL_COMMS_ASSESS_SKILL — on-demand skill path for manual delegation
|
|
79
79
|
# EXTERNAL_COMMS_POLICY_FILE — policy doc whose absence triggers advisory-only
|
|
80
80
|
# EXTERNAL_COMMS_LEAK_PREFILTER — yes|no — whether to run leak-detect pre-filter
|
|
81
|
+
# EXTERNAL_COMMS_SKIP_SURFACES — space-separated surface list this evaluator's
|
|
82
|
+
# policy disclaims; the marker-review delegation
|
|
83
|
+
# silent-passes on those surfaces (P360). Default
|
|
84
|
+
# empty (gate every detected surface).
|
|
81
85
|
# Fail-closed if absent: this hook cannot operate without a configured evaluator.
|
|
82
86
|
CONF_FILE="$SCRIPT_DIR/external-comms-evaluator.conf"
|
|
83
87
|
if [ ! -f "$CONF_FILE" ]; then
|
|
@@ -91,6 +95,7 @@ source "$CONF_FILE"
|
|
|
91
95
|
: "${EXTERNAL_COMMS_ASSESS_SKILL:?assess-skill missing from $CONF_FILE}"
|
|
92
96
|
EXTERNAL_COMMS_POLICY_FILE="${EXTERNAL_COMMS_POLICY_FILE:-RISK-POLICY.md}"
|
|
93
97
|
EXTERNAL_COMMS_LEAK_PREFILTER="${EXTERNAL_COMMS_LEAK_PREFILTER:-yes}"
|
|
98
|
+
EXTERNAL_COMMS_SKIP_SURFACES="${EXTERNAL_COMMS_SKIP_SURFACES:-}"
|
|
94
99
|
|
|
95
100
|
# ---------- Bypass ----------
|
|
96
101
|
if [ "${BYPASS_RISK_GATE:-0}" = "1" ]; then
|
|
@@ -318,6 +323,48 @@ if [ "$EXTERNAL_COMMS_LEAK_PREFILTER" = "yes" ]; then
|
|
|
318
323
|
fi
|
|
319
324
|
fi
|
|
320
325
|
|
|
326
|
+
# ---------- Per-evaluator surface skip (P360) ----------
|
|
327
|
+
# Some surfaces are explicitly disclaimed by THIS evaluator's policy doc, so the
|
|
328
|
+
# marker-review delegation below would be a guaranteed-PASS no-op (the subagent
|
|
329
|
+
# reads the policy, declares the surface out of scope, emits PASS — ~19K tokens
|
|
330
|
+
# per round-trip). EXTERNAL_COMMS_SKIP_SURFACES (per-package .conf) lists those
|
|
331
|
+
# surfaces; the gate silent-passes the prose-review delegation when the detected
|
|
332
|
+
# surface is on the list. Voice-tone sets this to `git-commit-message` because
|
|
333
|
+
# docs/VOICE-AND-TONE.md § Scope excludes commit messages ("covered by ADR-014 +
|
|
334
|
+
# ADR-018"); risk-scorer leaves it empty (its leak check on commit messages is
|
|
335
|
+
# meaningful). Placed AFTER the leak pre-filter so a skipped surface still gets
|
|
336
|
+
# credential/prod-URL scanning — this silences ONLY the prose-review deny, the
|
|
337
|
+
# same conservative shape as the P365 repo-visibility precondition below.
|
|
338
|
+
if [ -n "$EXTERNAL_COMMS_SKIP_SURFACES" ]; then
|
|
339
|
+
case " $EXTERNAL_COMMS_SKIP_SURFACES " in
|
|
340
|
+
*" $SURFACE "*)
|
|
341
|
+
exit 0
|
|
342
|
+
;;
|
|
343
|
+
esac
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
# ---------- Repo-visibility precondition: git-commit-message surface (P365) ----------
|
|
347
|
+
# A commit message only becomes external-facing prose when it lands in a PUBLIC
|
|
348
|
+
# GitHub repo (git log / PR commits tab / release-page auto-notes / CHANGELOG).
|
|
349
|
+
# In private or internal repos the marker-review delegation deny below is a pure
|
|
350
|
+
# false-positive (P365 — user direction 2026-06-11: "this MUST NOT fire for
|
|
351
|
+
# private repos"). Confirm visibility authoritatively via gh and silent-pass the
|
|
352
|
+
# marker gate on any non-PUBLIC result. Any INDETERMINATE result (gh absent,
|
|
353
|
+
# unauthenticated, no remote, API error → empty $REPO_VISIBILITY) is treated as
|
|
354
|
+
# non-public: a commit message is only demonstrably external when the repo is
|
|
355
|
+
# confirmably PUBLIC, so the conservative direction for THIS surface is to not
|
|
356
|
+
# fire. This is a fail-open on the voice/tone-and-prose review ONLY — the
|
|
357
|
+
# leak-pattern pre-filter above (credentials / prod-URLs) has already run for
|
|
358
|
+
# every surface in every repo, so the high-stakes secrecy net is unaffected.
|
|
359
|
+
# Scoped to git-commit-message only; the gh-issue/pr/api, npm-publish, and
|
|
360
|
+
# changeset-author surfaces are inherently external and stay gated regardless.
|
|
361
|
+
if [ "$SURFACE" = "git-commit-message" ]; then
|
|
362
|
+
REPO_VISIBILITY=$(gh repo view --json visibility -q .visibility 2>/dev/null || echo "")
|
|
363
|
+
if [ "$REPO_VISIBILITY" != "PUBLIC" ]; then
|
|
364
|
+
exit 0
|
|
365
|
+
fi
|
|
366
|
+
fi
|
|
367
|
+
|
|
321
368
|
# ---------- Marker-based gate (per-evaluator marker per ADR-028 amended 2026-05-14) ----------
|
|
322
369
|
SESSION_DIR="${TMPDIR:-/tmp}/claude-risk-${SESSION_ID}"
|
|
323
370
|
mkdir -p "$SESSION_DIR"
|
|
@@ -65,10 +65,28 @@ print(json.dumps({
|
|
|
65
65
|
" "$file_path" "$content"
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
# Mock `gh repo view --json visibility` for the git-commit-message surface
|
|
69
|
+
# repo-visibility precondition (P365). vis ∈ {PUBLIC,PRIVATE,INTERNAL}; pass the
|
|
70
|
+
# literal "FAIL" to simulate gh absent / unauthenticated (non-zero exit). The
|
|
71
|
+
# mock ignores args and prints the chosen visibility so the hook's
|
|
72
|
+
# `gh repo view --json visibility -q .visibility` resolves deterministically.
|
|
73
|
+
mock_gh_visibility() {
|
|
74
|
+
local vis="$1"
|
|
75
|
+
mkdir -p "$TEST_PROJECT_DIR/mockbin"
|
|
76
|
+
if [ "$vis" = "FAIL" ]; then
|
|
77
|
+
printf '#!/usr/bin/env bash\nexit 1\n' > "$TEST_PROJECT_DIR/mockbin/gh"
|
|
78
|
+
else
|
|
79
|
+
printf '#!/usr/bin/env bash\necho %s\n' "$vis" > "$TEST_PROJECT_DIR/mockbin/gh"
|
|
80
|
+
fi
|
|
81
|
+
chmod +x "$TEST_PROJECT_DIR/mockbin/gh"
|
|
82
|
+
}
|
|
83
|
+
|
|
68
84
|
# Run the hook in a project dir with RISK-POLICY.md present, piping JSON via stdin.
|
|
85
|
+
# mockbin (if populated by mock_gh_visibility) is prepended to PATH so the
|
|
86
|
+
# git-commit-message visibility precondition resolves against the mock.
|
|
69
87
|
run_hook() {
|
|
70
88
|
local input="$1"
|
|
71
|
-
run bash -c "cd '$TEST_PROJECT_DIR' && printf '%s' \"\$1\" | '$HOOK'" _ "$input"
|
|
89
|
+
run bash -c "cd '$TEST_PROJECT_DIR' && export PATH='$TEST_PROJECT_DIR/mockbin':\$PATH && printf '%s' \"\$1\" | '$HOOK'" _ "$input"
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
# ---------- Tests ----------
|
|
@@ -244,6 +262,7 @@ run_hook() {
|
|
|
244
262
|
}
|
|
245
263
|
|
|
246
264
|
@test "P082: git commit -m with leak-free body denies and delegates to risk evaluator" {
|
|
265
|
+
mock_gh_visibility PUBLIC
|
|
247
266
|
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
248
267
|
run_hook "$INPUT"
|
|
249
268
|
[ "$status" -eq 0 ]
|
|
@@ -253,6 +272,7 @@ run_hook() {
|
|
|
253
272
|
}
|
|
254
273
|
|
|
255
274
|
@test "P082: git commit --amend -m is intercepted (P082 SC2)" {
|
|
275
|
+
mock_gh_visibility PUBLIC
|
|
256
276
|
INPUT=$(build_bash_input "git commit --amend -m \"rewritten subject\"")
|
|
257
277
|
run_hook "$INPUT"
|
|
258
278
|
[ "$status" -eq 0 ]
|
|
@@ -264,6 +284,7 @@ run_hook() {
|
|
|
264
284
|
# Build a HEREDOC-shaped command. The hook regex pulls the body BETWEEN
|
|
265
285
|
# the <<'EOF' opener and the closing EOF marker — the extracted DRAFT is
|
|
266
286
|
# the inner text, NOT the literal `$(cat <<'EOF' ... EOF)` wrapper.
|
|
287
|
+
mock_gh_visibility PUBLIC
|
|
267
288
|
BODY=$'feat(foo): add bar\n\nWe observed a build failure on Node 20.'
|
|
268
289
|
CMD=$'git commit -m "$(cat <<\'EOF\'\n'"$BODY"$'\nEOF\n)"'
|
|
269
290
|
INPUT=$(build_bash_input "$CMD")
|
|
@@ -306,6 +327,7 @@ run_hook() {
|
|
|
306
327
|
}
|
|
307
328
|
|
|
308
329
|
@test "P082: per-evaluator marker keyed on (body, git-commit-message) permits the call" {
|
|
330
|
+
mock_gh_visibility PUBLIC
|
|
309
331
|
BODY="docs(retro): close iter 3 ask-hygiene trail"
|
|
310
332
|
SURFACE="git-commit-message"
|
|
311
333
|
KEY=$(printf '%s\n%s' "$BODY" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
|
|
@@ -317,6 +339,85 @@ run_hook() {
|
|
|
317
339
|
[ -z "$output" ]
|
|
318
340
|
}
|
|
319
341
|
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# P365 — repo-visibility precondition on the git-commit-message surface.
|
|
344
|
+
# A commit message is external-facing prose ONLY when it lands in a PUBLIC
|
|
345
|
+
# GitHub repo (git log / PR commits tab / release notes / CHANGELOG). In
|
|
346
|
+
# private/internal repos — or any repo whose visibility cannot be confirmed
|
|
347
|
+
# PUBLIC — the marker-review delegation deny is a pure false-positive
|
|
348
|
+
# (user direction 2026-06-11: "this MUST NOT fire for private repos"). The
|
|
349
|
+
# precondition silent-passes the marker gate for the git-commit-message
|
|
350
|
+
# surface on any non-PUBLIC / indeterminate gh result. It is scoped to that
|
|
351
|
+
# surface only and runs AFTER the leak pre-filter, so the credential/prod-URL
|
|
352
|
+
# security net survives the short-circuit.
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
@test "P365: git commit -m in a PRIVATE repo silent-passes (no external-comms deny)" {
|
|
356
|
+
mock_gh_visibility PRIVATE
|
|
357
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
358
|
+
run_hook "$INPUT"
|
|
359
|
+
[ "$status" -eq 0 ]
|
|
360
|
+
[ -z "$output" ]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@test "P365: git commit -m in an INTERNAL repo silent-passes" {
|
|
364
|
+
mock_gh_visibility INTERNAL
|
|
365
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
366
|
+
run_hook "$INPUT"
|
|
367
|
+
[ "$status" -eq 0 ]
|
|
368
|
+
[ -z "$output" ]
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@test "P365: git commit -m when gh is unavailable/indeterminate silent-passes (fail-non-public)" {
|
|
372
|
+
mock_gh_visibility FAIL
|
|
373
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
374
|
+
run_hook "$INPUT"
|
|
375
|
+
[ "$status" -eq 0 ]
|
|
376
|
+
[ -z "$output" ]
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
@test "P365: git commit -m in a PUBLIC repo still denies+delegates (gate intact, precondition surface-scoped)" {
|
|
380
|
+
mock_gh_visibility PUBLIC
|
|
381
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
382
|
+
run_hook "$INPUT"
|
|
383
|
+
[ "$status" -eq 0 ]
|
|
384
|
+
[[ "$output" == *"deny"* ]]
|
|
385
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@test "P360: empty EXTERNAL_COMMS_SKIP_SURFACES leaves the risk evaluator gating commit messages (divergence guard)" {
|
|
389
|
+
# The voice-tone evaluator skips git-commit-message (its policy disclaims it);
|
|
390
|
+
# the risk evaluator's .conf leaves EXTERNAL_COMMS_SKIP_SURFACES empty, so its
|
|
391
|
+
# leak check on commit bodies stays meaningful. Guards against a regression
|
|
392
|
+
# that accidentally skips this surface for risk-scorer (e.g. defaulting the
|
|
393
|
+
# knob non-empty, or syncing voice-tone's value into the shared gate body).
|
|
394
|
+
grep -qE '^EXTERNAL_COMMS_SKIP_SURFACES=$' "$HOOKS_DIR/external-comms-evaluator.conf"
|
|
395
|
+
mock_gh_visibility PUBLIC
|
|
396
|
+
INPUT=$(build_bash_input "git commit -m \"fix(foo): handle null input\"")
|
|
397
|
+
run_hook "$INPUT"
|
|
398
|
+
[ "$status" -eq 0 ]
|
|
399
|
+
[[ "$output" == *"deny"* ]]
|
|
400
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@test "P365: leak-shaped credential in a PRIVATE-repo commit body still hard-fails (security net survives)" {
|
|
404
|
+
mock_gh_visibility PRIVATE
|
|
405
|
+
INPUT=$(build_bash_input "git commit -m \"docs: token=${GH_TOKEN_LIKE}\"")
|
|
406
|
+
run_hook "$INPUT"
|
|
407
|
+
[ "$status" -eq 0 ]
|
|
408
|
+
[[ "$output" == *"deny"* ]]
|
|
409
|
+
[[ "$output" == *"git-commit-message"* ]]
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@test "P365: PRIVATE visibility does NOT short-circuit the gh-issue surface (still denies+delegates)" {
|
|
413
|
+
mock_gh_visibility PRIVATE
|
|
414
|
+
INPUT=$(build_bash_input "gh issue create --title x --body 'a clean issue body'")
|
|
415
|
+
run_hook "$INPUT"
|
|
416
|
+
[ "$status" -eq 0 ]
|
|
417
|
+
[[ "$output" == *"deny"* ]]
|
|
418
|
+
[[ "$output" == *"gh-issue-create"* ]]
|
|
419
|
+
}
|
|
420
|
+
|
|
320
421
|
# ---------------------------------------------------------------------------
|
|
321
422
|
# P364 — backtick-bearing double-quoted --body marker-key mismatch.
|
|
322
423
|
# When an outbound body contains markdown backticks (a code span), the
|