@windyroad/risk-scorer 0.13.2 → 0.13.4-preview.746

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.
@@ -310,5 +310,5 @@
310
310
  }
311
311
  },
312
312
  "name": "wr-risk-scorer",
313
- "version": "0.13.2"
313
+ "version": "0.13.4"
314
314
  }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.13.2",
3
+ "version": "0.13.4-preview.746",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"