@codyswann/lisa 2.298.0 → 2.299.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/all/copy-contents/.gitattributes +6 -0
- package/dist/cli/gate-commands.d.ts +8 -6
- package/dist/cli/gate-commands.d.ts.map +1 -1
- package/dist/cli/gate-commands.js +21 -0
- package/dist/cli/gate-commands.js.map +1 -1
- package/dist/cli/learnings-overflow-cmd.d.ts +27 -0
- package/dist/cli/learnings-overflow-cmd.d.ts.map +1 -0
- package/dist/cli/learnings-overflow-cmd.js +57 -0
- package/dist/cli/learnings-overflow-cmd.js.map +1 -0
- package/dist/core/learnings-alias.d.ts +156 -0
- package/dist/core/learnings-alias.d.ts.map +1 -0
- package/dist/core/learnings-alias.js +199 -0
- package/dist/core/learnings-alias.js.map +1 -0
- package/dist/core/learnings-document.d.ts +18 -0
- package/dist/core/learnings-document.d.ts.map +1 -1
- package/dist/core/learnings-document.js +23 -2
- package/dist/core/learnings-document.js.map +1 -1
- package/dist/core/learnings-merge-driver.d.ts +8 -33
- package/dist/core/learnings-merge-driver.d.ts.map +1 -1
- package/dist/core/learnings-merge-driver.js +15 -0
- package/dist/core/learnings-merge-driver.js.map +1 -1
- package/dist/core/learnings-overflow.d.ts +76 -0
- package/dist/core/learnings-overflow.d.ts.map +1 -0
- package/dist/core/learnings-overflow.js +212 -0
- package/dist/core/learnings-overflow.js.map +1 -0
- package/dist/core/learnings-writer.d.ts +6 -0
- package/dist/core/learnings-writer.d.ts.map +1 -1
- package/dist/core/learnings-writer.js +57 -8
- package/dist/core/learnings-writer.js.map +1 -1
- package/dist/core/learnings.d.ts +2 -0
- package/dist/core/learnings.d.ts.map +1 -1
- package/dist/core/learnings.js +2 -0
- package/dist/core/learnings.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +15 -6
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net-heredoc.py +232 -40
- package/dist/opencode/plugin-templates/parity-safety-net.sh +21 -1
- package/package.json +3 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-learnings-audit/SKILL.md +42 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-parity-safety-net-rules/SKILL.md +38 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-persist-learning/SKILL.md +7 -2
- package/plugins/lisa/hooks/parity-safety-net-heredoc.py +232 -40
- package/plugins/lisa/hooks/parity-safety-net.sh +21 -1
- package/plugins/lisa/skills/lisa-learnings-audit/SKILL.md +42 -6
- package/plugins/lisa/skills/lisa-parity-safety-net-rules/SKILL.md +38 -1
- package/plugins/lisa/skills/lisa-persist-learning/SKILL.md +7 -2
- package/plugins/lisa-agy/hooks/parity-safety-net-heredoc.py +232 -40
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +21 -1
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-learnings-audit/SKILL.md +42 -6
- package/plugins/lisa-agy/skills/lisa-parity-safety-net-rules/SKILL.md +38 -1
- package/plugins/lisa-agy/skills/lisa-persist-learning/SKILL.md +7 -2
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net-heredoc.py +232 -40
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +21 -1
- package/plugins/lisa-copilot/skills/lisa-learnings-audit/SKILL.md +42 -6
- package/plugins/lisa-copilot/skills/lisa-parity-safety-net-rules/SKILL.md +38 -1
- package/plugins/lisa-copilot/skills/lisa-persist-learning/SKILL.md +7 -2
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net-heredoc.py +232 -40
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +21 -1
- package/plugins/lisa-cursor/skills/lisa-learnings-audit/SKILL.md +42 -6
- package/plugins/lisa-cursor/skills/lisa-parity-safety-net-rules/SKILL.md +38 -1
- package/plugins/lisa-cursor/skills/lisa-persist-learning/SKILL.md +7 -2
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net-heredoc.py +232 -40
- package/plugins/src/base/hooks/parity-safety-net.sh +21 -1
- package/plugins/src/base/skills/lisa-learnings-audit/SKILL.md +42 -6
- package/plugins/src/base/skills/lisa-parity-safety-net-rules/SKILL.md +38 -1
- package/plugins/src/base/skills/lisa-persist-learning/SKILL.md +7 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.299.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.299.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.299.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.299.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.299.0",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Classify the only heredoc forms whose payload is non-executable text.
|
|
2
|
+
"""Classify the only heredoc forms whose payload is non-executable text.
|
|
3
|
+
|
|
4
|
+
SCOPE — read this before hardening anything here (issue #1993).
|
|
5
|
+
|
|
6
|
+
This module is a HEURISTIC RE-IMPLEMENTATION of bash's here-doc lexing, not an
|
|
7
|
+
adversary-resistant control. Every divergence between this Python model and real
|
|
8
|
+
bash is, by construction, a potential mis-classification, and five rounds of
|
|
9
|
+
fix-then-re-attack on issue #1958 plus three more here (#1993) established that
|
|
10
|
+
enumerating those divergences is an open-ended arms race. Treat the class as
|
|
11
|
+
CLOSED: a newly found member is documented, not chased, unless someone first
|
|
12
|
+
demonstrates it defeats the CONTENT GUARDS, which are the layer that matters.
|
|
13
|
+
|
|
14
|
+
The one hard guarantee, and the reason a mis-classification is survivable:
|
|
15
|
+
|
|
16
|
+
Exit SAFE(0) is the ONLY path that strips payload text, and it requires the
|
|
17
|
+
narrow, fully-quoted `gh issue|pr create/edit/comment` + `<<'EOF'` grammar in
|
|
18
|
+
``classify_safe``. EVERY other path — UNSUPPORTED(10) and MALFORMED(20) —
|
|
19
|
+
hands the RAW, unmodified command to the content guards. So a here-doc the
|
|
20
|
+
classifier mis-reads degrades to the guards; it does not bypass them. Any
|
|
21
|
+
change here must preserve that property: widen ``classify_safe`` only with
|
|
22
|
+
proof, and keep every other exit raw-pass-through.
|
|
23
|
+
|
|
24
|
+
Known accepted bypass class (measured, deliberately not chased further):
|
|
25
|
+
R5a — a trailing backslash on the last body line is a bash continuation that
|
|
26
|
+
swallows the terminator; R5b — an odd apostrophe in an unquoted body poisons a
|
|
27
|
+
flat quote scanner past the terminator; R5c — `<<\\DELIM` is a quoted delimiter
|
|
28
|
+
this parser did not model. All three are FIXED below, but they are members of a
|
|
29
|
+
family, not the family itself. Each requires deliberately-crafted input; none is
|
|
30
|
+
a shape an agent emits by accident; and post-#1982 every destructive class the
|
|
31
|
+
safety net exists to stop is independently blocked by the content guards on the
|
|
32
|
+
raw text.
|
|
33
|
+
|
|
34
|
+
The two bash invariants this file relies on — both measured directly against
|
|
35
|
+
/bin/bash, not inferred — so a future round starts from a spec:
|
|
36
|
+
|
|
37
|
+
1. UNQUOTED delimiter (`<<EOF`): the body performs NO quote or comment
|
|
38
|
+
processing (`'`, `"`, `#` are literal data bytes) but STILL expands `$(...)`,
|
|
39
|
+
backticks and parameters, and bash removes `\\<newline>` continuations BEFORE
|
|
40
|
+
matching the delimiter. Measured: `cat <<EOF` / `foo\\` / `EOF` / `bar` emits
|
|
41
|
+
`fooEOF\\nbar` — the here-doc never terminated.
|
|
42
|
+
2. QUOTED delimiter in any spelling (`<<'EOF'`, `<<"EOF"`, `<<\\EOF`): the body is
|
|
43
|
+
fully literal, no expansion of any kind, and backslash is DATA rather than an
|
|
44
|
+
escape — so the delimiter IS matched per physical line there. Measured: the
|
|
45
|
+
same trailing-backslash shape under `<<'EOF'` emits `foo\\` and then fails on
|
|
46
|
+
`bar: command not found`.
|
|
47
|
+
|
|
48
|
+
That asymmetry is why the continuation handling in ``terminator_line_index`` is
|
|
49
|
+
conditioned on ``Marker.quoted`` rather than applied uniformly.
|
|
50
|
+
|
|
51
|
+
``Marker.quoted`` means "bash makes this BODY literal" and nothing more. The
|
|
52
|
+
three spellings above are interchangeable for body semantics ONLY — they are NOT
|
|
53
|
+
interchangeable on the writer path. ``classify_safe`` still demands the literal
|
|
54
|
+
``<<'DELIM'`` spelling, so `<<"EOF"` and `<<\\EOF` can never reach SAFE(0); since
|
|
55
|
+
#1993 they additionally record a Marker, which arms the writer-owns-a-real-marker
|
|
56
|
+
and multi-marker rules and lands a `gh … <<\\EOF` writer on MALFORMED(20) where it
|
|
57
|
+
used to be UNSUPPORTED(10). That is a deliberate, test-pinned over-block: the
|
|
58
|
+
documented payload-strip workaround is spelled `<<'EOF'`. Widening
|
|
59
|
+
``classify_safe`` to admit other spellings is a separate change needing its own
|
|
60
|
+
proof — do not do it as a side effect of body-semantics work.
|
|
61
|
+
"""
|
|
3
62
|
|
|
4
63
|
from __future__ import annotations
|
|
5
64
|
|
|
@@ -494,6 +553,105 @@ def collapse_body_continuations(body: str) -> str:
|
|
|
494
553
|
return "".join(result)
|
|
495
554
|
|
|
496
555
|
|
|
556
|
+
def trailing_continuation(line: str) -> str | None:
|
|
557
|
+
"""Strip a bash ``\\<newline>`` continuation backslash, or ``None``.
|
|
558
|
+
|
|
559
|
+
A here-doc body line ends in a continuation exactly when it ends in an ODD
|
|
560
|
+
run of backslashes: each pair is one escaped literal backslash, so a leftover
|
|
561
|
+
single backslash is the one that escapes the following newline. Parity of the
|
|
562
|
+
TRAILING run is sufficient — any backslash earlier in the line is separated
|
|
563
|
+
from the run by a non-backslash byte, which ends that escape.
|
|
564
|
+
"""
|
|
565
|
+
stripped = line.rstrip("\\")
|
|
566
|
+
if (len(line) - len(stripped)) % 2 == 0:
|
|
567
|
+
return None
|
|
568
|
+
return line[:-1]
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def terminator_line_index(command: str, marker: Marker) -> int | None:
|
|
572
|
+
"""Index of the physical line that closes ``marker``, or ``None``.
|
|
573
|
+
|
|
574
|
+
THE single home of "where does this here-doc body end", consulted by the
|
|
575
|
+
closure check, the body scanner, the literal-body strip and the body
|
|
576
|
+
neutraliser so no two of them can ever disagree about the window (issue
|
|
577
|
+
#1993 R5a).
|
|
578
|
+
|
|
579
|
+
Bash removes ``\\<newline>`` continuations from an UNQUOTED here-doc body
|
|
580
|
+
BEFORE it matches the delimiter, so a trailing backslash on the last body
|
|
581
|
+
line JOINS that line to the delimiter line and the here-doc is NOT closed
|
|
582
|
+
there — the real body swallows the apparent terminator and keeps consuming
|
|
583
|
+
following lines, where a ``$(...)`` the classifier believed was ordinary
|
|
584
|
+
post-here-doc text is expanded and EXECUTED. Matching the delimiter per
|
|
585
|
+
PHYSICAL line therefore under-reads the body window for unquoted delimiters.
|
|
586
|
+
Measured ground truth: ``cat <<EOF`` / ``foo\\`` / ``EOF`` / ``bar`` prints
|
|
587
|
+
``fooEOF\\nbar`` (never terminated), while the same shape with ``<<'EOF'``
|
|
588
|
+
prints ``foo\\`` and then fails on ``bar: command not found``.
|
|
589
|
+
|
|
590
|
+
A QUOTED (or backslash-quoted) delimiter makes the body fully literal —
|
|
591
|
+
backslash is DATA there, not an escape — so those markers keep the exact
|
|
592
|
+
per-physical-line match, which the same measurement proves correct.
|
|
593
|
+
"""
|
|
594
|
+
lines = bash_lines(command)
|
|
595
|
+
marker_line = command.count("\n", 0, marker.start)
|
|
596
|
+
pending: list[str] = []
|
|
597
|
+
for index in range(marker_line + 1, len(lines)):
|
|
598
|
+
line = lines[index]
|
|
599
|
+
if not marker.quoted:
|
|
600
|
+
continued = trailing_continuation(line)
|
|
601
|
+
if continued is not None:
|
|
602
|
+
pending.append(continued)
|
|
603
|
+
continue
|
|
604
|
+
candidate = "".join(pending) + line
|
|
605
|
+
pending = []
|
|
606
|
+
if marker.strip_tabs:
|
|
607
|
+
candidate = candidate.lstrip("\t")
|
|
608
|
+
if candidate == marker.delimiter:
|
|
609
|
+
return index
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def neutralize_heredoc_bodies(command: str, markers: list[Marker]) -> str:
|
|
614
|
+
"""Blank every real here-doc body window, preserving offsets.
|
|
615
|
+
|
|
616
|
+
``has_active_command_substitution`` runs ONE flat single/double-quote state
|
|
617
|
+
machine across the whole command — including here-doc bodies, where bash
|
|
618
|
+
applies NO quote processing at all. An odd apostrophe in an unquoted body
|
|
619
|
+
(``it's fine``) therefore opens a phantom single-quoted string in the flat
|
|
620
|
+
scanner that persists PAST the terminator, hiding a live ``$(...)`` on a
|
|
621
|
+
FOLLOWING command line that bash happily executes (issue #1993 R5b). The
|
|
622
|
+
trigger is precisely an odd ``'`` count: an even ``''``, an odd ``"``, a
|
|
623
|
+
``#``, an ANSI-C ``$'a`` and a lone trailing backslash all leave the flat
|
|
624
|
+
state correct, which is what pins the defect to this one branch.
|
|
625
|
+
|
|
626
|
+
``strip_provably_literal_body`` cannot fix this: it bails whenever the marker
|
|
627
|
+
is unquoted, so the poisoning body is exactly the one it never touches. So
|
|
628
|
+
blank the body window of EVERY marker bash really treats as a here-doc
|
|
629
|
+
redirection before the flat scan runs. Bytes are replaced with spaces and
|
|
630
|
+
newlines are preserved, so marker offsets and line numbering stay valid for
|
|
631
|
+
every later walker.
|
|
632
|
+
|
|
633
|
+
Blanking loses nothing: a substitution INSIDE an unquoted body is still
|
|
634
|
+
caught by ``unquoted_heredoc_body_has_substitution`` (scanned with correct
|
|
635
|
+
here-doc-body semantics), and a quoted body is genuinely inert to bash. A
|
|
636
|
+
marker that is not closed blanks to end of command — that command is
|
|
637
|
+
MALFORMED on the closure check regardless, and its trailing region is still
|
|
638
|
+
body-scanned. As everywhere else, a marker nested inside an open quoted
|
|
639
|
+
string is NOT a here-doc to bash and is skipped, so its "body" stays fully
|
|
640
|
+
visible to the flat scan (issue #1958 Finding 1).
|
|
641
|
+
"""
|
|
642
|
+
lines = bash_lines(command)
|
|
643
|
+
blanked = list(lines)
|
|
644
|
+
for marker in markers:
|
|
645
|
+
if cross_line_quote_state(command, marker.start) != "plain":
|
|
646
|
+
continue
|
|
647
|
+
marker_line = command.count("\n", 0, marker.start)
|
|
648
|
+
end = terminator_line_index(command, marker)
|
|
649
|
+
stop = len(lines) if end is None else end
|
|
650
|
+
for index in range(marker_line + 1, stop):
|
|
651
|
+
blanked[index] = " " * len(lines[index])
|
|
652
|
+
return "\n".join(blanked)
|
|
653
|
+
|
|
654
|
+
|
|
497
655
|
def body_line_has_substitution(line: str) -> bool:
|
|
498
656
|
"""True if an unquoted-heredoc-body line contains an active substitution.
|
|
499
657
|
|
|
@@ -553,22 +711,22 @@ def unquoted_heredoc_body_has_substitution(
|
|
|
553
711
|
if cross_line_quote_state(command, marker.start) != "plain":
|
|
554
712
|
continue
|
|
555
713
|
marker_line = command.count("\n", 0, marker.start)
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
lines[index].lstrip("\t") if marker.strip_tabs else lines[index]
|
|
560
|
-
)
|
|
561
|
-
if candidate == marker.delimiter:
|
|
562
|
-
break
|
|
563
|
-
body_lines.append(lines[index])
|
|
714
|
+
end = terminator_line_index(command, marker)
|
|
715
|
+
stop = len(lines) if end is None else end
|
|
716
|
+
body_lines = lines[marker_line + 1 : stop]
|
|
564
717
|
# Join the raw body window under bash's unquoted-here-doc ``\<newline>``
|
|
565
718
|
# removal BEFORE scanning, so a ``$(`` a caller split across a line
|
|
566
719
|
# continuation is seen as the one contiguous token bash executes. The
|
|
567
|
-
#
|
|
568
|
-
#
|
|
569
|
-
#
|
|
570
|
-
#
|
|
571
|
-
#
|
|
720
|
+
# window itself comes from ``terminator_line_index``, which applies that
|
|
721
|
+
# SAME removal before matching the delimiter — because bash does (issue
|
|
722
|
+
# #1993 R5a). An earlier revision matched the terminator per physical
|
|
723
|
+
# line here on the belief that bash matches the delimiter before
|
|
724
|
+
# continuation processing; that is false for an unquoted delimiter, and
|
|
725
|
+
# it let a trailing backslash on the last body line swallow the
|
|
726
|
+
# terminator so this scan stopped short of the region bash still treats
|
|
727
|
+
# as body. This does not rely on ``collapse_line_continuations`` — whose
|
|
728
|
+
# flat quote state a body apostrophe corrupts — which is the whole point
|
|
729
|
+
# of Finding R4.
|
|
572
730
|
body = collapse_body_continuations("\n".join(body_lines))
|
|
573
731
|
if body_line_has_substitution(body):
|
|
574
732
|
return True
|
|
@@ -595,16 +753,42 @@ def parse_marker(line: str, start: int, offset: int) -> Marker | None:
|
|
|
595
753
|
delimiter = line[index:end]
|
|
596
754
|
final = end + 1
|
|
597
755
|
# Deliberate POSIX divergence, fail-safe: POSIX makes the body
|
|
598
|
-
# non-expanding when ANY part of the delimiter is quoted
|
|
599
|
-
#
|
|
600
|
-
#
|
|
601
|
-
#
|
|
602
|
-
#
|
|
603
|
-
#
|
|
604
|
-
#
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
#
|
|
756
|
+
# non-expanding when ANY part of the delimiter is quoted. Partial forms
|
|
757
|
+
# like `<<EO'F'` stay unmodelled (mis-tokenized as delimiter EO, so the
|
|
758
|
+
# terminator never matches and the command fails closed as MALFORMED).
|
|
759
|
+
# Only a delimiter this parser can PROVE was one full quote pair earns
|
|
760
|
+
# quoted=True, and trailing word characters after the closing quote
|
|
761
|
+
# (`<<'EOF'X` — real bash delimiter EOFX) keep it conservative too: the
|
|
762
|
+
# next character must end the token.
|
|
763
|
+
quoted = final >= len(line) or line[final] in " \t;&|)<>#"
|
|
764
|
+
elif line[index] == "\\":
|
|
765
|
+
# `<<\DELIM` is a BACKSLASH-quoted delimiter: bash and POSIX make its
|
|
766
|
+
# body fully literal, exactly like `<<'DELIM'` (measured: `cat <<\EOF`
|
|
767
|
+
# with body `it's $(echo RAN)` prints that text verbatim, unexpanded).
|
|
768
|
+
# This spelling used to record NO Marker at all, on the rationale that an
|
|
769
|
+
# unmodelled body stays raw-visible to every content guard. That covers
|
|
770
|
+
# the body's own contents but NOT the body's ability to corrupt the flat
|
|
771
|
+
# scanner's quote state for text AFTER the terminator: an apostrophe in
|
|
772
|
+
# an invisible body still opened a phantom string that hid a live
|
|
773
|
+
# `$(...)` on a following line (issue #1993 R5c). Recording it as the
|
|
774
|
+
# quoted marker bash actually treats it as is both more faithful and
|
|
775
|
+
# what brings it under `neutralize_heredoc_bodies`. The same
|
|
776
|
+
# token-must-end discipline as the quote-pair branch applies, so exotic
|
|
777
|
+
# spellings (`<<\EOF'x'` — real bash delimiter EOFx) stay conservative.
|
|
778
|
+
# quoted=True here is a statement about BODY semantics only. Recording a
|
|
779
|
+
# Marker where there was none also arms the writer-owns-a-real-marker and
|
|
780
|
+
# multi-marker rules, so `gh issue create --body-file - <<\EOF` and
|
|
781
|
+
# `cat <<\A <<B` now fail closed at MALFORMED instead of passing through
|
|
782
|
+
# at UNSUPPORTED. Both are valid, harmless bash, so that is a real
|
|
783
|
+
# over-block — accepted and test-pinned, because `classify_safe` requires
|
|
784
|
+
# the literal `<<'DELIM'` spelling and `<<\EOF` can never reach SAFE
|
|
785
|
+
# anyway. The documented payload-strip workaround is `<<'EOF'`.
|
|
786
|
+
index += 1
|
|
787
|
+
match = re.match(r"[A-Za-z_][A-Za-z0-9_]*", line[index:])
|
|
788
|
+
if match is None:
|
|
789
|
+
return None
|
|
790
|
+
delimiter = match.group(0)
|
|
791
|
+
final = index + len(delimiter)
|
|
608
792
|
quoted = final >= len(line) or line[final] in " \t;&|)<>#"
|
|
609
793
|
else:
|
|
610
794
|
match = re.match(r"[A-Za-z_][A-Za-z0-9_]*", line[index:])
|
|
@@ -698,21 +882,22 @@ def strip_provably_literal_body(command: str, markers: list[Marker]) -> str:
|
|
|
698
882
|
return command
|
|
699
883
|
lines = bash_lines(command)
|
|
700
884
|
marker_line = command.count("\n", 0, marker.start)
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
return command
|
|
885
|
+
end = terminator_line_index(command, marker)
|
|
886
|
+
if end is None:
|
|
887
|
+
return command
|
|
888
|
+
return "\n".join(lines[: marker_line + 1] + lines[end:])
|
|
706
889
|
|
|
707
890
|
|
|
708
891
|
def marker_is_closed(command: str, marker: Marker) -> bool:
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
892
|
+
"""True when bash really terminates this here-doc inside the command.
|
|
893
|
+
|
|
894
|
+
Delegates to ``terminator_line_index`` so the closure verdict and the body
|
|
895
|
+
window are decided by one rule. For an unquoted delimiter that rule removes
|
|
896
|
+
``\\<newline>`` continuations first, so a trailing backslash on the last body
|
|
897
|
+
line correctly reads as NOT closed — which is what bash does, and what makes
|
|
898
|
+
the swallowed-terminator shape fail closed as MALFORMED (issue #1993 R5a).
|
|
899
|
+
"""
|
|
900
|
+
return terminator_line_index(command, marker) is not None
|
|
716
901
|
|
|
717
902
|
|
|
718
903
|
def main() -> int:
|
|
@@ -748,11 +933,18 @@ def main() -> int:
|
|
|
748
933
|
return MALFORMED
|
|
749
934
|
|
|
750
935
|
# Nested substitution plus a heredoc is executable shell syntax unless it
|
|
751
|
-
# matched the one exact quoted `--body "$(cat ...)"` form above.
|
|
752
|
-
#
|
|
753
|
-
#
|
|
936
|
+
# matched the one exact quoted `--body "$(cat ...)"` form above. Here-doc
|
|
937
|
+
# bodies are neutralised before the flat scan: bash applies NO quote
|
|
938
|
+
# processing inside a body, so leaving those bytes in a flat quote state
|
|
939
|
+
# machine let an odd body apostrophe poison the scanner PAST the terminator
|
|
940
|
+
# and hide a live substitution on a following command line (issue #1993
|
|
941
|
+
# R5b). Neutralising preserves offsets, so the narrower literal-body strip
|
|
942
|
+
# still applies on top; the text outside every body window is still scanned,
|
|
943
|
+
# and in-body substitutions are covered by the body-semantics scan below.
|
|
754
944
|
if has_active_command_substitution(
|
|
755
|
-
strip_provably_literal_body(
|
|
945
|
+
strip_provably_literal_body(
|
|
946
|
+
neutralize_heredoc_bodies(logical_command, markers), markers
|
|
947
|
+
)
|
|
756
948
|
) or unquoted_heredoc_body_has_substitution(logical_command, markers):
|
|
757
949
|
return MALFORMED
|
|
758
950
|
if len(markers) > 1:
|
|
@@ -54,7 +54,27 @@
|
|
|
54
54
|
# would never expand — a single-quoted `echo '$(rm -rf /)'` or an escaped
|
|
55
55
|
# `echo "\$(rm -rf /)"` — because the scan has no quote-context awareness. That
|
|
56
56
|
# over-block stays inside this accepted class. Workaround: quote-break the string
|
|
57
|
-
# or use the gh-writer heredoc form, whose payload is stripped before the guards
|
|
57
|
+
# or use the gh-writer heredoc form, whose payload is stripped before the guards
|
|
58
|
+
# run — spelled with a QUOTE-PAIR delimiter (`<<'EOF'`). That exact spelling is
|
|
59
|
+
# what the SAFE path recognises; `<<"EOF"` and `<<\EOF` do not reach it, and
|
|
60
|
+
# since #1993 a `gh … <<\EOF` writer fails closed instead (see parse_marker).
|
|
61
|
+
#
|
|
62
|
+
# Known accepted BYPASS class (the symmetric error direction, issue #1993): the
|
|
63
|
+
# heredoc classifier in parity-safety-net-heredoc.py re-implements bash's
|
|
64
|
+
# here-doc lexing in Python, and any divergence from real bash is a shape it
|
|
65
|
+
# mis-classifies. Five rounds on #1958 and three more on #1993 (R5a swallowed
|
|
66
|
+
# terminator, R5b body-apostrophe quote-state poisoning, R5c backslash-quoted
|
|
67
|
+
# delimiter) established that enumerating those divergences is an open-ended
|
|
68
|
+
# arms race, so the class is CLOSED as accepted rather than chased further. This
|
|
69
|
+
# is survivable because of where a mis-classification lands: ONLY the narrow
|
|
70
|
+
# gh-writer SAFE path strips payload text, and every other classifier exit hands
|
|
71
|
+
# the RAW command to the guards below. A missed here-doc therefore degrades to
|
|
72
|
+
# the content guards; it does not bypass them. Since #1982 taught those guards to
|
|
73
|
+
# see through substitution wrappers, every destructive class this net exists to
|
|
74
|
+
# stop is blocked on the raw text regardless of how the here-doc was read. The
|
|
75
|
+
# net catches ACCIDENTAL catastrophic commands; it is not a wall against an
|
|
76
|
+
# adversary who knows bash's lexer, and it was never the only thing standing
|
|
77
|
+
# between an agent and arbitrary execution.
|
|
58
78
|
#
|
|
59
79
|
# Operators extend the built-in rules with a project-local rule file — one
|
|
60
80
|
# extended-regex (ERE) per line, blank lines and `#` comments ignored — managed
|