@windyroad/risk-scorer 0.13.1 → 0.13.2

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.1"
313
+ "version": "0.13.2"
314
314
  }
@@ -185,28 +185,59 @@ except Exception:
185
185
  DRAFT=$(printf '%s' "$COMMAND" | python3 -c "
186
186
  import sys, re
187
187
  cmd = sys.stdin.read()
188
- # (pattern, flags) first match wins.
188
+ # P364: bash double-quote unescape. The double-quoted body capture groups
189
+ # carry RAW shell-escaped command text — an orchestrator must backslash-escape
190
+ # backticks (and \$, \", \\) inside \"...\" to survive bash parsing, e.g.
191
+ # --body \"Fixed in \\\`code\\\` ...\". The PostToolUse mark hook hashes the
192
+ # LOGICAL <draft> body (plain backticks), so the gate must undo those escapes
193
+ # or the two marker keys diverge → permanent deny-after-PASS. Inside double
194
+ # quotes a backslash is special ONLY before \$ \` \" \\ or a newline (line
195
+ # continuation); single-quoted and <<'EOF' forms are literal and need none.
196
+ # Single left-to-right pass so an escaped backslash adjacent to another escape
197
+ # (\\\\\` -> backslash + backtick) is NOT mis-collapsed. chr() literals keep
198
+ # this source free of the very metacharacters the surrounding shell double
199
+ # quotes would otherwise eat.
200
+ def unescape_dq(s):
201
+ out = []
202
+ i = 0
203
+ n = len(s)
204
+ special = set([chr(36), chr(96), chr(34), chr(92), chr(10)])
205
+ while i < n:
206
+ if s[i] == chr(92) and i + 1 < n and s[i + 1] in special:
207
+ if s[i + 1] != chr(10):
208
+ out.append(s[i + 1])
209
+ i += 2
210
+ else:
211
+ out.append(s[i])
212
+ i += 1
213
+ return ''.join(out)
214
+ # (pattern, flags, unescape) — first match wins. unescape=True for the
215
+ # double-quoted forms only (P364).
189
216
  patterns = [
190
217
  # HEREDOC body — matches a here-doc with EOF delimiter (quoted or
191
218
  # unquoted). The literal '<<' is written as the char-class pair
192
219
  # [<][<] so bash's command-substitution parser does NOT mis-parse
193
220
  # this regex as a real here-doc operator (P082 implementation note).
194
- # DOTALL so the body can span newlines.
195
- (r\"[<][<]\s*['\\\"]?EOF['\\\"]?\s*\n(.*?)\nEOF\", re.DOTALL),
221
+ # DOTALL so the body can span newlines. Left literal: the AI-canonical
222
+ # form is the quoted <<'EOF' heredoc, whose body bash does not unescape.
223
+ (r\"[<][<]\s*['\\\"]?EOF['\\\"]?\s*\n(.*?)\nEOF\", re.DOTALL, False),
196
224
  # gh issue/pr + npm publish --body 'TEXT' / --body \"TEXT\" (existing).
197
- (r\"--body[= ]'([^']*)'\", 0),
198
- (r'--body[= ]\"([^\"]*)\"', 0),
225
+ (r\"--body[= ]'([^']*)'\", 0, False),
226
+ (r'--body[= ]\"([^\"]*)\"', 0, True),
199
227
  # gh api --field summary='TEXT' / --field summary=\"TEXT\" (existing).
200
- (r\"--field [a-zA-Z_]+='([^']*)'\", 0),
201
- (r'--field [a-zA-Z_]+=\"([^\"]*)\"', 0),
228
+ (r\"--field [a-zA-Z_]+='([^']*)'\", 0, False),
229
+ (r'--field [a-zA-Z_]+=\"([^\"]*)\"', 0, True),
202
230
  # git commit -m / --message single-line literal forms (P082 Phase 1).
203
- (r\"(?:-m|--message)[= ]'([^']*)'\", 0),
204
- (r'(?:-m|--message)[= ]\"([^\"]*)\"', 0),
231
+ (r\"(?:-m|--message)[= ]'([^']*)'\", 0, False),
232
+ (r'(?:-m|--message)[= ]\"([^\"]*)\"', 0, True),
205
233
  ]
206
- for pat, flags in patterns:
234
+ for pat, flags, unescape in patterns:
207
235
  m = re.search(pat, cmd, flags)
208
236
  if m:
209
- print(m.group(1))
237
+ body = m.group(1)
238
+ if unescape:
239
+ body = unescape_dq(body)
240
+ print(body)
210
241
  break
211
242
  " 2>/dev/null || echo "")
212
243
  ;;
@@ -316,3 +316,68 @@ run_hook() {
316
316
  [ "$status" -eq 0 ]
317
317
  [ -z "$output" ]
318
318
  }
319
+
320
+ # ---------------------------------------------------------------------------
321
+ # P364 — backtick-bearing double-quoted --body marker-key mismatch.
322
+ # When an outbound body contains markdown backticks (a code span), the
323
+ # orchestrator must backslash-escape them to survive bash double quotes:
324
+ # `gh issue comment 42 --body "Fixed in \`foo\` ..."`. Before the fix the
325
+ # gate extracted the RAW shell-escaped body (literal `\`` with backslash)
326
+ # from the command text, while the PostToolUse mark hook hashed the LOGICAL
327
+ # <draft> body (plain backticks). The two sha256 keys diverged → the PASS
328
+ # marker landed at a key the gate never re-read → permanent deny-after-PASS.
329
+ # After the fix the gate unescapes bash double-quote backslash-escapes on
330
+ # the captured double-quoted body so its DRAFT is byte-equal to the logical
331
+ # reviewed draft and the body-keyed marker permits. DISTINCT from P276 /
332
+ # P010 (whitespace / frontmatter) — this is a shell-escaping-layer fix.
333
+ # ---------------------------------------------------------------------------
334
+
335
+ @test "P364: backtick-bearing double-quoted --body permits when marker keyed on the unescaped logical body" {
336
+ # Logical draft body (plain backticks) — what the agent wraps in <draft>
337
+ # and what the mark hook hashes via compute_external_comms_key.
338
+ LOGICAL='Fixed in `compute_external_comms_key` per the patch on Node 20.'
339
+ SURFACE="gh-issue-comment"
340
+ KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
341
+ touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
342
+
343
+ # The orchestrator backslash-escapes the backticks to survive bash double
344
+ # quotes — this is the byte-different command text the gate actually sees.
345
+ CMD='gh issue comment 42 --body "Fixed in \`compute_external_comms_key\` per the patch on Node 20."'
346
+ INPUT=$(build_bash_input "$CMD")
347
+ run_hook "$INPUT"
348
+ [ "$status" -eq 0 ]
349
+ [ -z "$output" ]
350
+ }
351
+
352
+ @test "P364: escaped backslash adjacent to escaped backtick (\\\\\\\`) unescapes to literal backslash + backtick (single left-to-right pass)" {
353
+ # Adjacency edge case (architect note): \\\` is escaped-backslash THEN
354
+ # escaped-backtick and must unescape to a literal backslash followed by a
355
+ # literal backtick — NOT mis-collapsed. The logical body therefore carries
356
+ # one backslash then one backtick.
357
+ LOGICAL=$'edge: \\`token here'
358
+ SURFACE="gh-issue-comment"
359
+ KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
360
+ touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
361
+
362
+ # Command text carries \\\` (escaped backslash + escaped backtick).
363
+ CMD='gh issue comment 42 --body "edge: \\\`token here"'
364
+ INPUT=$(build_bash_input "$CMD")
365
+ run_hook "$INPUT"
366
+ [ "$status" -eq 0 ]
367
+ [ -z "$output" ]
368
+ }
369
+
370
+ @test "P364: single-quoted --body with literal backticks stays literal (no unescaping applied)" {
371
+ # Inside single quotes bash does NOT process backslashes or backticks, so
372
+ # the orchestrator writes plain backticks and the captured body already
373
+ # equals the logical draft. The fix must NOT alter the single-quote path.
374
+ LOGICAL='Fixed in `plain_span` here.'
375
+ SURFACE="gh-issue-comment"
376
+ KEY=$(printf '%s\n%s' "$LOGICAL" "$SURFACE" | shasum -a 256 | cut -d' ' -f1)
377
+ touch "${RDIR}/external-comms-risk-reviewed-${KEY}"
378
+
379
+ INPUT=$(build_bash_input "gh issue comment 42 --body 'Fixed in \`plain_span\` here.'")
380
+ run_hook "$INPUT"
381
+ [ "$status" -eq 0 ]
382
+ [ -z "$output" ]
383
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/risk-scorer",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "Pipeline risk scoring, commit/push gates, and secret leak detection",
5
5
  "bin": {
6
6
  "windyroad-risk-scorer": "./bin/install.mjs"