@codyswann/lisa 4.39.0 → 4.40.1
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-overwrite/scripts/lisa-cross-worktree-guard.mjs +333 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +17 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-managed-file-edits.sh +14 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +28 -0
- package/dist/codex/scripts/block-no-verify.sh +21 -0
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +22 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +19 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-managed-file-edits.sh +14 -0
- package/dist/opencode/plugin-templates/block-no-verify.sh +28 -0
- package/npm-package/create-only/.github/workflows/publish-to-npm.yml +48 -19
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/block-direct-issue-create.sh +17 -0
- package/plugins/lisa/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa/hooks/block-no-verify.agy.sh +21 -0
- package/plugins/lisa/hooks/block-no-verify.sh +28 -0
- package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +17 -0
- package/plugins/lisa-agy/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +21 -0
- package/plugins/lisa-agy/plugin.json +1 -1
- 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/block-direct-issue-create.sh +17 -0
- package/plugins/lisa-copilot/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa-copilot/hooks/block-no-verify.sh +28 -0
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +17 -0
- package/plugins/lisa-cursor/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +28 -0
- 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/block-direct-issue-create.sh +17 -0
- package/plugins/src/base/hooks/block-managed-file-edits.sh +14 -0
- package/plugins/src/base/hooks/block-no-verify.agy.sh +21 -0
- package/plugins/src/base/hooks/block-no-verify.sh +28 -0
- package/rails/copy-contents/scripts/lisa-mutation.sh +8 -1
- package/scripts/two-channel-couplings.json +1 -1
- package/typescript/copy-contents/.husky/pre-commit +40 -0
- package/typescript/copy-overwrite/.prettierignore +1 -0
- package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +20 -14
|
@@ -719,6 +719,20 @@ def shell_script_operand(args):
|
|
|
719
719
|
# not a script file.
|
|
720
720
|
if not token.startswith("--") and "c" in token[1:]:
|
|
721
721
|
return None
|
|
722
|
+
# `-n` is noexec: the shell READS and parses the file, then exits
|
|
723
|
+
# without executing a line of it. `bash -n <file>` is the one shape
|
|
724
|
+
# that puts a path at a command position while provably running
|
|
725
|
+
# nothing — the exact seam between reading and running, and this
|
|
726
|
+
# walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
|
|
727
|
+
# syntax check of any script mentioning a managed path was refused
|
|
728
|
+
# as though it were about to write one.
|
|
729
|
+
#
|
|
730
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
731
|
+
# distinction, not the program.
|
|
732
|
+
if token == "--noexec" or (
|
|
733
|
+
not token.startswith("--") and "n" in token[1:]
|
|
734
|
+
):
|
|
735
|
+
return None
|
|
722
736
|
continue
|
|
723
737
|
return token
|
|
724
738
|
return None
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
# `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
|
|
25
25
|
# `xargs` puts at a command position.
|
|
26
26
|
#
|
|
27
|
+
# NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
|
|
28
|
+
# the file, then exits without running a line — so a syntax check is the one
|
|
29
|
+
# shape that puts a path at a command position while executing nothing. It used
|
|
30
|
+
# to be refused, which made `bash -n` unusable on this file, on every husky
|
|
31
|
+
# hook, and on most of this repository (#3781). The distinction is noexec, not
|
|
32
|
+
# the program: `bash nv.sh` still executes and is still refused.
|
|
33
|
+
#
|
|
27
34
|
# The line below is what lets `lisa apply` tell a downstream copy of this guard
|
|
28
35
|
# that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
|
|
29
36
|
# "differs from mine", and guessing "behind" is how a fork's stronger guard gets
|
|
@@ -1031,6 +1038,10 @@ def resolve_script(token):
|
|
|
1031
1038
|
|
|
1032
1039
|
SHELL_COMMAND_STRING = object()
|
|
1033
1040
|
SHELL_STDIN = object()
|
|
1041
|
+
# `-n` / `--noexec`: the shell READS and parses its operand, then exits without
|
|
1042
|
+
# executing a line of it. Distinct from the two above because nothing runs at
|
|
1043
|
+
# all — there is no payload to scan and no stdin script to wait for.
|
|
1044
|
+
SHELL_NOEXEC = object()
|
|
1034
1045
|
|
|
1035
1046
|
|
|
1036
1047
|
def shell_execution_operand(args):
|
|
@@ -1061,12 +1072,25 @@ def shell_execution_operand(args):
|
|
|
1061
1072
|
index += 1
|
|
1062
1073
|
continue
|
|
1063
1074
|
if token.startswith("--"):
|
|
1075
|
+
if token == "--noexec":
|
|
1076
|
+
return SHELL_NOEXEC
|
|
1064
1077
|
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
1065
1078
|
opaque_option_seen = True
|
|
1066
1079
|
index += 1
|
|
1067
1080
|
continue
|
|
1068
1081
|
if "c" in token[1:]:
|
|
1069
1082
|
return SHELL_COMMAND_STRING
|
|
1083
|
+
# `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
|
|
1084
|
+
# a command position while provably executing nothing, so it is the
|
|
1085
|
+
# exact seam between reading and running — and reach got it wrong in
|
|
1086
|
+
# BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
|
|
1087
|
+
# of any file merely DISCUSSING the bypass was refused, which is most of
|
|
1088
|
+
# this repository's own guards and every husky hook.
|
|
1089
|
+
#
|
|
1090
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
1091
|
+
# distinction, not the program: this is not a read-only exemption.
|
|
1092
|
+
if "n" in token[1:]:
|
|
1093
|
+
return SHELL_NOEXEC
|
|
1070
1094
|
index += 1
|
|
1071
1095
|
return args[index] if index < len(args) else SHELL_STDIN
|
|
1072
1096
|
|
|
@@ -1111,6 +1135,10 @@ def executed_scripts(tokens):
|
|
|
1111
1135
|
continue
|
|
1112
1136
|
if operand is SHELL_COMMAND_STRING:
|
|
1113
1137
|
continue
|
|
1138
|
+
if operand is SHELL_NOEXEC:
|
|
1139
|
+
# Nothing runs, so there is nothing to follow and nothing
|
|
1140
|
+
# to fail closed about.
|
|
1141
|
+
continue
|
|
1114
1142
|
else:
|
|
1115
1143
|
continue
|
|
1116
1144
|
if operand is None:
|
|
@@ -48,19 +48,47 @@ jobs:
|
|
|
48
48
|
publish:
|
|
49
49
|
name: Publish to npm
|
|
50
50
|
runs-on: ubuntu-latest
|
|
51
|
+
# Job-level so every step below reads these from the environment instead of
|
|
52
|
+
# having them substituted into its script. A `${{ }}` expression is
|
|
53
|
+
# substituted by Actions BEFORE the shell parses the script, so an
|
|
54
|
+
# interpolated value is shell SOURCE TEXT rather than an argument - and
|
|
55
|
+
# `tag`, `version` and `package_manager` are all free-text `type: string`
|
|
56
|
+
# inputs that reach `git checkout`, `npm version` and a bare command name.
|
|
57
|
+
# Paired with the shape gates in Validate inputs below (#3717).
|
|
58
|
+
env:
|
|
59
|
+
RELEASE_TAG: ${{ inputs.tag }}
|
|
60
|
+
RELEASE_VERSION: ${{ inputs.version }}
|
|
61
|
+
PACKAGE_MANAGER: ${{ inputs.package_manager }}
|
|
51
62
|
steps:
|
|
52
63
|
- name: Validate inputs
|
|
53
64
|
run: |
|
|
54
|
-
if [ -z "$
|
|
65
|
+
if [ -z "$RELEASE_TAG" ]; then
|
|
55
66
|
echo "::error::Tag input is empty - cannot publish without a version tag"
|
|
56
67
|
echo "This usually means the version job was skipped (e.g., promotion merge detected)"
|
|
57
68
|
exit 1
|
|
58
69
|
fi
|
|
59
|
-
if [ -z "$
|
|
70
|
+
if [ -z "$RELEASE_VERSION" ]; then
|
|
60
71
|
echo "::error::Version input is empty - cannot publish without a version"
|
|
61
72
|
exit 1
|
|
62
73
|
fi
|
|
63
|
-
|
|
74
|
+
# Shape gates, so a value carrying shell metacharacters is refused
|
|
75
|
+
# before it reaches `git checkout`, `npm version`, or a command name.
|
|
76
|
+
if ! printf '%s' "$RELEASE_TAG" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
|
|
77
|
+
echo "::error::Tag input is not a valid version tag"
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
if ! printf '%s' "$RELEASE_VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'; then
|
|
81
|
+
echo "::error::Version input is not a valid semantic version"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
case "$PACKAGE_MANAGER" in
|
|
85
|
+
npm|yarn|pnpm|bun) ;;
|
|
86
|
+
*)
|
|
87
|
+
echo "::error::Unsupported package manager: must be npm, yarn, pnpm or bun"
|
|
88
|
+
exit 1
|
|
89
|
+
;;
|
|
90
|
+
esac
|
|
91
|
+
echo "✅ Inputs validated: tag=$RELEASE_TAG, version=$RELEASE_VERSION"
|
|
64
92
|
|
|
65
93
|
- name: Checkout
|
|
66
94
|
uses: actions/checkout@v6
|
|
@@ -69,12 +97,12 @@ jobs:
|
|
|
69
97
|
|
|
70
98
|
- name: Wait for tag to be available
|
|
71
99
|
run: |
|
|
72
|
-
echo "Waiting for tag $
|
|
100
|
+
echo "Waiting for tag $RELEASE_TAG to be available..."
|
|
73
101
|
for i in {1..30}; do
|
|
74
102
|
git fetch --tags
|
|
75
|
-
if git tag --list | grep -
|
|
76
|
-
echo "Tag $
|
|
77
|
-
git checkout $
|
|
103
|
+
if git tag --list | grep -qx "$RELEASE_TAG"; then
|
|
104
|
+
echo "Tag $RELEASE_TAG is now available"
|
|
105
|
+
git checkout "$RELEASE_TAG"
|
|
78
106
|
break
|
|
79
107
|
fi
|
|
80
108
|
echo "Attempt $i/30: Tag not found yet, waiting 10 seconds..."
|
|
@@ -82,8 +110,8 @@ jobs:
|
|
|
82
110
|
done
|
|
83
111
|
|
|
84
112
|
# Final check
|
|
85
|
-
if [ "$(git describe --exact-match --tags 2>/dev/null)" != "$
|
|
86
|
-
echo "Error: Tag $
|
|
113
|
+
if [ "$(git describe --exact-match --tags 2>/dev/null)" != "$RELEASE_TAG" ]; then
|
|
114
|
+
echo "Error: Tag $RELEASE_TAG is still not available after 5 minutes"
|
|
87
115
|
exit 1
|
|
88
116
|
fi
|
|
89
117
|
|
|
@@ -107,24 +135,25 @@ jobs:
|
|
|
107
135
|
|
|
108
136
|
- name: Install dependencies
|
|
109
137
|
run: |
|
|
110
|
-
if [ "$
|
|
138
|
+
if [ "$PACKAGE_MANAGER" = "npm" ]; then
|
|
111
139
|
npm ci
|
|
112
|
-
elif [ "$
|
|
140
|
+
elif [ "$PACKAGE_MANAGER" = "yarn" ]; then
|
|
113
141
|
yarn install --frozen-lockfile
|
|
114
|
-
elif [ "$
|
|
142
|
+
elif [ "$PACKAGE_MANAGER" = "bun" ]; then
|
|
115
143
|
bun install --frozen-lockfile
|
|
116
144
|
fi
|
|
117
145
|
|
|
118
146
|
- name: Build package
|
|
119
|
-
run:
|
|
147
|
+
run: |
|
|
148
|
+
"$PACKAGE_MANAGER" run build
|
|
120
149
|
|
|
121
150
|
- name: Update package version
|
|
122
151
|
run: |
|
|
123
152
|
# Update package.json to match the release version
|
|
124
|
-
VERSION="$
|
|
153
|
+
VERSION="$RELEASE_VERSION"
|
|
125
154
|
if [ -n "$VERSION" ]; then
|
|
126
155
|
echo "Updating package.json to version $VERSION"
|
|
127
|
-
npm version $VERSION --no-git-tag-version --allow-same-version
|
|
156
|
+
npm version "$VERSION" --no-git-tag-version --allow-same-version
|
|
128
157
|
fi
|
|
129
158
|
|
|
130
159
|
- name: Publish to npm with OIDC
|
|
@@ -136,22 +165,22 @@ jobs:
|
|
|
136
165
|
if: ${{ hashFiles('eslint-plugin-code-organization/package.json') != '' }}
|
|
137
166
|
run: |
|
|
138
167
|
cd eslint-plugin-code-organization
|
|
139
|
-
npm version $
|
|
168
|
+
npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version
|
|
140
169
|
npm publish --access public --provenance
|
|
141
170
|
|
|
142
171
|
- name: Publish @codyswann/eslint-plugin-component-structure
|
|
143
172
|
if: ${{ hashFiles('eslint-plugin-component-structure/package.json') != '' }}
|
|
144
173
|
run: |
|
|
145
174
|
cd eslint-plugin-component-structure
|
|
146
|
-
npm version $
|
|
175
|
+
npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version
|
|
147
176
|
npm publish --access public --provenance
|
|
148
177
|
|
|
149
178
|
- name: Publish @codyswann/eslint-plugin-ui-standards
|
|
150
179
|
if: ${{ hashFiles('eslint-plugin-ui-standards/package.json') != '' }}
|
|
151
180
|
run: |
|
|
152
181
|
cd eslint-plugin-ui-standards
|
|
153
|
-
npm version $
|
|
182
|
+
npm version "$RELEASE_VERSION" --no-git-tag-version --allow-same-version
|
|
154
183
|
npm publish --access public --provenance
|
|
155
184
|
|
|
156
185
|
- name: Notify on success
|
|
157
|
-
run: echo "Successfully published version $
|
|
186
|
+
run: echo "Successfully published version $RELEASE_VERSION to npm with provenance"
|
package/package.json
CHANGED
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"zod-validation-error": "^4.0.0"
|
|
172
172
|
},
|
|
173
173
|
"name": "@codyswann/lisa",
|
|
174
|
-
"version": "4.
|
|
174
|
+
"version": "4.40.1",
|
|
175
175
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
176
176
|
"main": "dist/index.js",
|
|
177
177
|
"exports": {
|
|
@@ -319,7 +319,7 @@
|
|
|
319
319
|
"test": "tests"
|
|
320
320
|
},
|
|
321
321
|
"types": "./dist/index.d.ts",
|
|
322
|
-
"lisaReleaseCommit": "
|
|
323
|
-
"gitHead": "
|
|
324
|
-
"lisaReleaseTag": "v4.
|
|
322
|
+
"lisaReleaseCommit": "ec7fc892e6ac49b4162920b7aecc37157877d4eb",
|
|
323
|
+
"gitHead": "ec7fc892e6ac49b4162920b7aecc37157877d4eb",
|
|
324
|
+
"lisaReleaseTag": "v4.40.1"
|
|
325
325
|
}
|
|
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
|
|
|
1708
1708
|
# ordinary shell options and do not consume the script path.
|
|
1709
1709
|
if not head.startswith("--") and "c" in head[1:]:
|
|
1710
1710
|
return None
|
|
1711
|
+
# `-n` is noexec: the shell READS and parses the file and then
|
|
1712
|
+
# exits without running a line of it. `bash -n <file>` is the
|
|
1713
|
+
# one shape that puts a path at a command position while
|
|
1714
|
+
# provably executing nothing, so it is the exact seam between
|
|
1715
|
+
# reading and running — and this walk got it wrong in BOTH
|
|
1716
|
+
# directions (CodySwannGT/lisa#3781). A creation-shaped file
|
|
1717
|
+
# earned a false refusal; a file carrying a human-gate marker
|
|
1718
|
+
# earned a false ALLOW, adjudicating as "declared" a command
|
|
1719
|
+
# that filed nothing. The silent direction is the worse one.
|
|
1720
|
+
#
|
|
1721
|
+
# Not a read-only-command exemption: `bash <file>` still runs
|
|
1722
|
+
# the file and is still refused. The distinction is noexec, not
|
|
1723
|
+
# the program.
|
|
1724
|
+
if head == "--noexec" or (
|
|
1725
|
+
not head.startswith("--") and "n" in head[1:]
|
|
1726
|
+
):
|
|
1727
|
+
return None
|
|
1711
1728
|
elif head in NO_SCRIPT_OPTIONS.get(program, set()):
|
|
1712
1729
|
return None
|
|
1713
1730
|
takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
|
|
@@ -719,6 +719,20 @@ def shell_script_operand(args):
|
|
|
719
719
|
# not a script file.
|
|
720
720
|
if not token.startswith("--") and "c" in token[1:]:
|
|
721
721
|
return None
|
|
722
|
+
# `-n` is noexec: the shell READS and parses the file, then exits
|
|
723
|
+
# without executing a line of it. `bash -n <file>` is the one shape
|
|
724
|
+
# that puts a path at a command position while provably running
|
|
725
|
+
# nothing — the exact seam between reading and running, and this
|
|
726
|
+
# walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
|
|
727
|
+
# syntax check of any script mentioning a managed path was refused
|
|
728
|
+
# as though it were about to write one.
|
|
729
|
+
#
|
|
730
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
731
|
+
# distinction, not the program.
|
|
732
|
+
if token == "--noexec" or (
|
|
733
|
+
not token.startswith("--") and "n" in token[1:]
|
|
734
|
+
):
|
|
735
|
+
return None
|
|
722
736
|
continue
|
|
723
737
|
return token
|
|
724
738
|
return None
|
|
@@ -961,6 +961,10 @@ def resolve_script(token):
|
|
|
961
961
|
|
|
962
962
|
SHELL_COMMAND_STRING = object()
|
|
963
963
|
SHELL_STDIN = object()
|
|
964
|
+
# `-n` / `--noexec`: the shell READS and parses its operand, then exits without
|
|
965
|
+
# executing a line of it. Distinct from the two above because nothing runs at
|
|
966
|
+
# all — there is no payload to scan and no stdin script to wait for.
|
|
967
|
+
SHELL_NOEXEC = object()
|
|
964
968
|
|
|
965
969
|
|
|
966
970
|
def shell_execution_operand(args):
|
|
@@ -991,12 +995,25 @@ def shell_execution_operand(args):
|
|
|
991
995
|
index += 1
|
|
992
996
|
continue
|
|
993
997
|
if token.startswith("--"):
|
|
998
|
+
if token == "--noexec":
|
|
999
|
+
return SHELL_NOEXEC
|
|
994
1000
|
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
995
1001
|
opaque_option_seen = True
|
|
996
1002
|
index += 1
|
|
997
1003
|
continue
|
|
998
1004
|
if "c" in token[1:]:
|
|
999
1005
|
return SHELL_COMMAND_STRING
|
|
1006
|
+
# `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
|
|
1007
|
+
# a command position while provably executing nothing, so it is the
|
|
1008
|
+
# exact seam between reading and running — and reach got it wrong in
|
|
1009
|
+
# BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
|
|
1010
|
+
# of any file merely DISCUSSING the bypass was refused, which is most of
|
|
1011
|
+
# this repository's own guards and every husky hook.
|
|
1012
|
+
#
|
|
1013
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
1014
|
+
# distinction, not the program: this is not a read-only exemption.
|
|
1015
|
+
if "n" in token[1:]:
|
|
1016
|
+
return SHELL_NOEXEC
|
|
1000
1017
|
index += 1
|
|
1001
1018
|
return args[index] if index < len(args) else SHELL_STDIN
|
|
1002
1019
|
|
|
@@ -1041,6 +1058,10 @@ def executed_scripts(tokens):
|
|
|
1041
1058
|
continue
|
|
1042
1059
|
if operand is SHELL_COMMAND_STRING:
|
|
1043
1060
|
continue
|
|
1061
|
+
if operand is SHELL_NOEXEC:
|
|
1062
|
+
# Nothing runs, so there is nothing to follow and nothing
|
|
1063
|
+
# to fail closed about.
|
|
1064
|
+
continue
|
|
1044
1065
|
else:
|
|
1045
1066
|
continue
|
|
1046
1067
|
if operand is None:
|
|
@@ -24,6 +24,13 @@
|
|
|
24
24
|
# `source`, `.`, `bash < nv.sh`, and an interpreter a `find -exec` or
|
|
25
25
|
# `xargs` puts at a command position.
|
|
26
26
|
#
|
|
27
|
+
# NOT vector 11: `bash -n nv.sh`. `-n` is noexec — the shell reads and parses
|
|
28
|
+
# the file, then exits without running a line — so a syntax check is the one
|
|
29
|
+
# shape that puts a path at a command position while executing nothing. It used
|
|
30
|
+
# to be refused, which made `bash -n` unusable on this file, on every husky
|
|
31
|
+
# hook, and on most of this repository (#3781). The distinction is noexec, not
|
|
32
|
+
# the program: `bash nv.sh` still executes and is still refused.
|
|
33
|
+
#
|
|
27
34
|
# The line below is what lets `lisa apply` tell a downstream copy of this guard
|
|
28
35
|
# that is BEHIND from one that is AHEAD. Byte comparison cannot: both look like
|
|
29
36
|
# "differs from mine", and guessing "behind" is how a fork's stronger guard gets
|
|
@@ -1031,6 +1038,10 @@ def resolve_script(token):
|
|
|
1031
1038
|
|
|
1032
1039
|
SHELL_COMMAND_STRING = object()
|
|
1033
1040
|
SHELL_STDIN = object()
|
|
1041
|
+
# `-n` / `--noexec`: the shell READS and parses its operand, then exits without
|
|
1042
|
+
# executing a line of it. Distinct from the two above because nothing runs at
|
|
1043
|
+
# all — there is no payload to scan and no stdin script to wait for.
|
|
1044
|
+
SHELL_NOEXEC = object()
|
|
1034
1045
|
|
|
1035
1046
|
|
|
1036
1047
|
def shell_execution_operand(args):
|
|
@@ -1061,12 +1072,25 @@ def shell_execution_operand(args):
|
|
|
1061
1072
|
index += 1
|
|
1062
1073
|
continue
|
|
1063
1074
|
if token.startswith("--"):
|
|
1075
|
+
if token == "--noexec":
|
|
1076
|
+
return SHELL_NOEXEC
|
|
1064
1077
|
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
1065
1078
|
opaque_option_seen = True
|
|
1066
1079
|
index += 1
|
|
1067
1080
|
continue
|
|
1068
1081
|
if "c" in token[1:]:
|
|
1069
1082
|
return SHELL_COMMAND_STRING
|
|
1083
|
+
# `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
|
|
1084
|
+
# a command position while provably executing nothing, so it is the
|
|
1085
|
+
# exact seam between reading and running — and reach got it wrong in
|
|
1086
|
+
# BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
|
|
1087
|
+
# of any file merely DISCUSSING the bypass was refused, which is most of
|
|
1088
|
+
# this repository's own guards and every husky hook.
|
|
1089
|
+
#
|
|
1090
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
1091
|
+
# distinction, not the program: this is not a read-only exemption.
|
|
1092
|
+
if "n" in token[1:]:
|
|
1093
|
+
return SHELL_NOEXEC
|
|
1070
1094
|
index += 1
|
|
1071
1095
|
return args[index] if index < len(args) else SHELL_STDIN
|
|
1072
1096
|
|
|
@@ -1111,6 +1135,10 @@ def executed_scripts(tokens):
|
|
|
1111
1135
|
continue
|
|
1112
1136
|
if operand is SHELL_COMMAND_STRING:
|
|
1113
1137
|
continue
|
|
1138
|
+
if operand is SHELL_NOEXEC:
|
|
1139
|
+
# Nothing runs, so there is nothing to follow and nothing
|
|
1140
|
+
# to fail closed about.
|
|
1141
|
+
continue
|
|
1114
1142
|
else:
|
|
1115
1143
|
continue
|
|
1116
1144
|
if operand is None:
|
|
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
|
|
|
1708
1708
|
# ordinary shell options and do not consume the script path.
|
|
1709
1709
|
if not head.startswith("--") and "c" in head[1:]:
|
|
1710
1710
|
return None
|
|
1711
|
+
# `-n` is noexec: the shell READS and parses the file and then
|
|
1712
|
+
# exits without running a line of it. `bash -n <file>` is the
|
|
1713
|
+
# one shape that puts a path at a command position while
|
|
1714
|
+
# provably executing nothing, so it is the exact seam between
|
|
1715
|
+
# reading and running — and this walk got it wrong in BOTH
|
|
1716
|
+
# directions (CodySwannGT/lisa#3781). A creation-shaped file
|
|
1717
|
+
# earned a false refusal; a file carrying a human-gate marker
|
|
1718
|
+
# earned a false ALLOW, adjudicating as "declared" a command
|
|
1719
|
+
# that filed nothing. The silent direction is the worse one.
|
|
1720
|
+
#
|
|
1721
|
+
# Not a read-only-command exemption: `bash <file>` still runs
|
|
1722
|
+
# the file and is still refused. The distinction is noexec, not
|
|
1723
|
+
# the program.
|
|
1724
|
+
if head == "--noexec" or (
|
|
1725
|
+
not head.startswith("--") and "n" in head[1:]
|
|
1726
|
+
):
|
|
1727
|
+
return None
|
|
1711
1728
|
elif head in NO_SCRIPT_OPTIONS.get(program, set()):
|
|
1712
1729
|
return None
|
|
1713
1730
|
takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
|
|
@@ -719,6 +719,20 @@ def shell_script_operand(args):
|
|
|
719
719
|
# not a script file.
|
|
720
720
|
if not token.startswith("--") and "c" in token[1:]:
|
|
721
721
|
return None
|
|
722
|
+
# `-n` is noexec: the shell READS and parses the file, then exits
|
|
723
|
+
# without executing a line of it. `bash -n <file>` is the one shape
|
|
724
|
+
# that puts a path at a command position while provably running
|
|
725
|
+
# nothing — the exact seam between reading and running, and this
|
|
726
|
+
# walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
|
|
727
|
+
# syntax check of any script mentioning a managed path was refused
|
|
728
|
+
# as though it were about to write one.
|
|
729
|
+
#
|
|
730
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
731
|
+
# distinction, not the program.
|
|
732
|
+
if token == "--noexec" or (
|
|
733
|
+
not token.startswith("--") and "n" in token[1:]
|
|
734
|
+
):
|
|
735
|
+
return None
|
|
722
736
|
continue
|
|
723
737
|
return token
|
|
724
738
|
return None
|
|
@@ -961,6 +961,10 @@ def resolve_script(token):
|
|
|
961
961
|
|
|
962
962
|
SHELL_COMMAND_STRING = object()
|
|
963
963
|
SHELL_STDIN = object()
|
|
964
|
+
# `-n` / `--noexec`: the shell READS and parses its operand, then exits without
|
|
965
|
+
# executing a line of it. Distinct from the two above because nothing runs at
|
|
966
|
+
# all — there is no payload to scan and no stdin script to wait for.
|
|
967
|
+
SHELL_NOEXEC = object()
|
|
964
968
|
|
|
965
969
|
|
|
966
970
|
def shell_execution_operand(args):
|
|
@@ -991,12 +995,25 @@ def shell_execution_operand(args):
|
|
|
991
995
|
index += 1
|
|
992
996
|
continue
|
|
993
997
|
if token.startswith("--"):
|
|
998
|
+
if token == "--noexec":
|
|
999
|
+
return SHELL_NOEXEC
|
|
994
1000
|
if token not in SHELL_LONG_OPTIONS_NO_VALUE:
|
|
995
1001
|
opaque_option_seen = True
|
|
996
1002
|
index += 1
|
|
997
1003
|
continue
|
|
998
1004
|
if "c" in token[1:]:
|
|
999
1005
|
return SHELL_COMMAND_STRING
|
|
1006
|
+
# `-n` is noexec. `bash -n <file>` is the one shape that puts a path at
|
|
1007
|
+
# a command position while provably executing nothing, so it is the
|
|
1008
|
+
# exact seam between reading and running — and reach got it wrong in
|
|
1009
|
+
# BOTH directions (CodySwannGT/lisa#3781). Here it meant a syntax check
|
|
1010
|
+
# of any file merely DISCUSSING the bypass was refused, which is most of
|
|
1011
|
+
# this repository's own guards and every husky hook.
|
|
1012
|
+
#
|
|
1013
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
1014
|
+
# distinction, not the program: this is not a read-only exemption.
|
|
1015
|
+
if "n" in token[1:]:
|
|
1016
|
+
return SHELL_NOEXEC
|
|
1000
1017
|
index += 1
|
|
1001
1018
|
return args[index] if index < len(args) else SHELL_STDIN
|
|
1002
1019
|
|
|
@@ -1041,6 +1058,10 @@ def executed_scripts(tokens):
|
|
|
1041
1058
|
continue
|
|
1042
1059
|
if operand is SHELL_COMMAND_STRING:
|
|
1043
1060
|
continue
|
|
1061
|
+
if operand is SHELL_NOEXEC:
|
|
1062
|
+
# Nothing runs, so there is nothing to follow and nothing
|
|
1063
|
+
# to fail closed about.
|
|
1064
|
+
continue
|
|
1044
1065
|
else:
|
|
1045
1066
|
continue
|
|
1046
1067
|
if operand is None:
|
|
@@ -1708,6 +1708,23 @@ def executed_operand(argv):
|
|
|
1708
1708
|
# ordinary shell options and do not consume the script path.
|
|
1709
1709
|
if not head.startswith("--") and "c" in head[1:]:
|
|
1710
1710
|
return None
|
|
1711
|
+
# `-n` is noexec: the shell READS and parses the file and then
|
|
1712
|
+
# exits without running a line of it. `bash -n <file>` is the
|
|
1713
|
+
# one shape that puts a path at a command position while
|
|
1714
|
+
# provably executing nothing, so it is the exact seam between
|
|
1715
|
+
# reading and running — and this walk got it wrong in BOTH
|
|
1716
|
+
# directions (CodySwannGT/lisa#3781). A creation-shaped file
|
|
1717
|
+
# earned a false refusal; a file carrying a human-gate marker
|
|
1718
|
+
# earned a false ALLOW, adjudicating as "declared" a command
|
|
1719
|
+
# that filed nothing. The silent direction is the worse one.
|
|
1720
|
+
#
|
|
1721
|
+
# Not a read-only-command exemption: `bash <file>` still runs
|
|
1722
|
+
# the file and is still refused. The distinction is noexec, not
|
|
1723
|
+
# the program.
|
|
1724
|
+
if head == "--noexec" or (
|
|
1725
|
+
not head.startswith("--") and "n" in head[1:]
|
|
1726
|
+
):
|
|
1727
|
+
return None
|
|
1711
1728
|
elif head in NO_SCRIPT_OPTIONS.get(program, set()):
|
|
1712
1729
|
return None
|
|
1713
1730
|
takes_value = head in INTERPRETER_VALUE_OPTIONS.get(program, set())
|
|
@@ -719,6 +719,20 @@ def shell_script_operand(args):
|
|
|
719
719
|
# not a script file.
|
|
720
720
|
if not token.startswith("--") and "c" in token[1:]:
|
|
721
721
|
return None
|
|
722
|
+
# `-n` is noexec: the shell READS and parses the file, then exits
|
|
723
|
+
# without executing a line of it. `bash -n <file>` is the one shape
|
|
724
|
+
# that puts a path at a command position while provably running
|
|
725
|
+
# nothing — the exact seam between reading and running, and this
|
|
726
|
+
# walk got it wrong (CodySwannGT/lisa#3781). Here that meant a
|
|
727
|
+
# syntax check of any script mentioning a managed path was refused
|
|
728
|
+
# as though it were about to write one.
|
|
729
|
+
#
|
|
730
|
+
# `bash <file>` still executes and is still refused. Noexec is the
|
|
731
|
+
# distinction, not the program.
|
|
732
|
+
if token == "--noexec" or (
|
|
733
|
+
not token.startswith("--") and "n" in token[1:]
|
|
734
|
+
):
|
|
735
|
+
return None
|
|
722
736
|
continue
|
|
723
737
|
return token
|
|
724
738
|
return None
|