@codyswann/lisa 4.70.4 → 4.71.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-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
- package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
- package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
- package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
- package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
- package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
- package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
- package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
- package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
- package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
- package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
- package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
- package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
- package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
- package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
- package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +272 -0
- package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
- package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
- package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
- package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
- package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
- package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
- package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
- package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
- package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
- package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
- package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
- package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
- package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +258 -0
- package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
- package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
- package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
- package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
- package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
- package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
- package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
- package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
- package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
- package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
- package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
- package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
- package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
- package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
- package/dist/codex/scripts/block-no-verify.sh +89 -5
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +598 -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 +157 -11
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
- 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-no-verify.agy.sh +89 -5
- package/plugins/lisa/hooks/block-no-verify.sh +89 -5
- package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
- 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-no-verify.sh +89 -5
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
- 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-no-verify.agy.sh +89 -5
- package/plugins/src/base/hooks/block-no-verify.sh +89 -5
- package/scripts/check-shipped-surface-removals.mjs +96 -24
- package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
- package/scripts/two-channel-couplings.json +31 -7
- package/typescript/copy-contents/.husky/commit-msg +18 -0
- package/typescript/copy-overwrite/.prettierignore +2 -0
|
@@ -152,6 +152,7 @@ fi
|
|
|
152
152
|
|
|
153
153
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
154
154
|
import os
|
|
155
|
+
import io
|
|
155
156
|
import re
|
|
156
157
|
import shlex
|
|
157
158
|
import sys
|
|
@@ -1284,6 +1285,57 @@ def git_skips_verification(text, depth=0):
|
|
|
1284
1285
|
return False
|
|
1285
1286
|
|
|
1286
1287
|
|
|
1288
|
+
def git_config_reads_key(tokens, index):
|
|
1289
|
+
"""Whether this key is a read operand of this exact Git config invocation.
|
|
1290
|
+
|
|
1291
|
+
Keep the command boundary: a query cannot authorize the next command or
|
|
1292
|
+
turn a later setting into a read. Unknown options remain conservative.
|
|
1293
|
+
"""
|
|
1294
|
+
start = index
|
|
1295
|
+
while start > 0 and tokens[start - 1] not in COMMAND_SEPARATORS:
|
|
1296
|
+
start -= 1
|
|
1297
|
+
end = index + 1
|
|
1298
|
+
while end < len(tokens) and tokens[end] not in COMMAND_SEPARATORS:
|
|
1299
|
+
end += 1
|
|
1300
|
+
program, args, opaque = command_word(tokens[start:end])
|
|
1301
|
+
if opaque or program != "git":
|
|
1302
|
+
return False
|
|
1303
|
+
found = subcommand_after_git(args, 0)
|
|
1304
|
+
if not found or found[0] != "config":
|
|
1305
|
+
return False
|
|
1306
|
+
argv = found[1]
|
|
1307
|
+
key_offset = index - (end - len(argv))
|
|
1308
|
+
read = bool(argv and argv[0] == "get")
|
|
1309
|
+
cursor = 1 if read else 0
|
|
1310
|
+
operands = []
|
|
1311
|
+
read_actions = {"--get", "--get-all", "--get-regexp", "--get-urlmatch"}
|
|
1312
|
+
modifiers = {
|
|
1313
|
+
"--local", "--global", "--system", "--worktree", "--includes",
|
|
1314
|
+
"--no-includes", "--show-origin", "--show-scope", "--null", "-z",
|
|
1315
|
+
"--name-only", "--all", "--regexp",
|
|
1316
|
+
}
|
|
1317
|
+
value_options = {"--file", "-f", "--blob", "--type", "--default", "--value"}
|
|
1318
|
+
while cursor < len(argv):
|
|
1319
|
+
token = argv[cursor]
|
|
1320
|
+
if token in read_actions:
|
|
1321
|
+
if read:
|
|
1322
|
+
return False
|
|
1323
|
+
read = True
|
|
1324
|
+
elif token in value_options:
|
|
1325
|
+
if cursor + 1 >= len(argv):
|
|
1326
|
+
return False
|
|
1327
|
+
cursor += 1
|
|
1328
|
+
elif token in modifiers:
|
|
1329
|
+
pass
|
|
1330
|
+
elif token.startswith("-"):
|
|
1331
|
+
if not any(token.startswith(option + "=") for option in value_options):
|
|
1332
|
+
return False
|
|
1333
|
+
else:
|
|
1334
|
+
operands.append(cursor)
|
|
1335
|
+
cursor += 1
|
|
1336
|
+
return bool(operands and operands[0] == key_offset and (read or len(operands) == 1))
|
|
1337
|
+
|
|
1338
|
+
|
|
1287
1339
|
def token_bypass(tokens):
|
|
1288
1340
|
"""Whether any token disables hooks by environment or by git config.
|
|
1289
1341
|
|
|
@@ -1295,7 +1347,7 @@ def token_bypass(tokens):
|
|
|
1295
1347
|
option, and an option belongs to an argv.
|
|
1296
1348
|
|
|
1297
1349
|
Args:
|
|
1298
|
-
tokens:
|
|
1350
|
+
tokens: Operator-aware tokens of one command or script.
|
|
1299
1351
|
|
|
1300
1352
|
Returns:
|
|
1301
1353
|
True when a token disables verification.
|
|
@@ -1321,7 +1373,9 @@ def token_bypass(tokens):
|
|
|
1321
1373
|
if lowered.startswith("core.hookspath="):
|
|
1322
1374
|
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
1323
1375
|
return True
|
|
1324
|
-
if lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1376
|
+
if (lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1377
|
+
and tokens[i + 1] not in COMMAND_SEPARATORS
|
|
1378
|
+
and not git_config_reads_key(tokens, i)):
|
|
1325
1379
|
if not is_permitted_hooks_path(tokens[i + 1]):
|
|
1326
1380
|
return True
|
|
1327
1381
|
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
@@ -1388,8 +1442,22 @@ def token_bypass(tokens):
|
|
|
1388
1442
|
return False
|
|
1389
1443
|
|
|
1390
1444
|
|
|
1445
|
+
class OperatorStream(io.StringIO):
|
|
1446
|
+
"""Remember punctuation read while shlex is inside a quote or escape."""
|
|
1447
|
+
|
|
1448
|
+
lexer = None
|
|
1449
|
+
literal_operator = False
|
|
1450
|
+
|
|
1451
|
+
def read(self, size=-1):
|
|
1452
|
+
value = super().read(size)
|
|
1453
|
+
if (self.lexer is not None and self.lexer.state in {"'", '"', "\\"}
|
|
1454
|
+
and any(char in "();|&<>" for char in value)):
|
|
1455
|
+
self.literal_operator = True
|
|
1456
|
+
return value
|
|
1457
|
+
|
|
1458
|
+
|
|
1391
1459
|
def flat_tokens(text):
|
|
1392
|
-
"""
|
|
1460
|
+
"""Operator-aware tokens without printed data, or None on a lex failure.
|
|
1393
1461
|
|
|
1394
1462
|
Args:
|
|
1395
1463
|
text: A command line or a script's contents.
|
|
@@ -1398,9 +1466,25 @@ def flat_tokens(text):
|
|
|
1398
1466
|
The token list, or None.
|
|
1399
1467
|
"""
|
|
1400
1468
|
try:
|
|
1401
|
-
|
|
1469
|
+
stream = OperatorStream(line_boundaries_as_separators(text))
|
|
1470
|
+
lexer = shlex.shlex(stream, posix=True, punctuation_chars=True)
|
|
1471
|
+
lexer.whitespace_split = True
|
|
1472
|
+
lexer.commenters = ""
|
|
1473
|
+
stream.lexer = lexer
|
|
1474
|
+
tokens = []
|
|
1475
|
+
while True:
|
|
1476
|
+
stream.literal_operator = False
|
|
1477
|
+
token = lexer.get_token()
|
|
1478
|
+
if token is None:
|
|
1479
|
+
break
|
|
1480
|
+
# shlex otherwise makes quoted ";" indistinguishable from a real
|
|
1481
|
+
# command boundary. Keep literal destinations as data, so a setter
|
|
1482
|
+
# cannot masquerade as a one-operand configuration read.
|
|
1483
|
+
if token in COMMAND_SEPARATORS and stream.literal_operator:
|
|
1484
|
+
token = "\x00" + token
|
|
1485
|
+
tokens.append(token)
|
|
1402
1486
|
return [
|
|
1403
|
-
token
|
|
1487
|
+
token
|
|
1404
1488
|
for index, token in enumerate(tokens)
|
|
1405
1489
|
if not printed_argument(tokens, index)
|
|
1406
1490
|
]
|
package/package.json
CHANGED
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"qs": "^6.16.0"
|
|
190
190
|
},
|
|
191
191
|
"name": "@codyswann/lisa",
|
|
192
|
-
"version": "4.
|
|
192
|
+
"version": "4.71.0",
|
|
193
193
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
194
194
|
"main": "dist/index.js",
|
|
195
195
|
"exports": {
|
|
@@ -345,7 +345,7 @@
|
|
|
345
345
|
"test": "tests"
|
|
346
346
|
},
|
|
347
347
|
"types": "./dist/index.d.ts",
|
|
348
|
-
"lisaReleaseCommit": "
|
|
349
|
-
"gitHead": "
|
|
350
|
-
"lisaReleaseTag": "v4.
|
|
348
|
+
"lisaReleaseCommit": "a1f7ee64bb77d2cb174c361ec4a6023cc24863fa",
|
|
349
|
+
"gitHead": "a1f7ee64bb77d2cb174c361ec4a6023cc24863fa",
|
|
350
|
+
"lisaReleaseTag": "v4.71.0"
|
|
351
351
|
}
|
|
@@ -51,6 +51,7 @@ command -v python3 >/dev/null 2>&1 || allow
|
|
|
51
51
|
|
|
52
52
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
53
53
|
import os
|
|
54
|
+
import io
|
|
54
55
|
import re
|
|
55
56
|
import shlex
|
|
56
57
|
import sys
|
|
@@ -1183,6 +1184,57 @@ def git_skips_verification(text, depth=0):
|
|
|
1183
1184
|
return False
|
|
1184
1185
|
|
|
1185
1186
|
|
|
1187
|
+
def git_config_reads_key(tokens, index):
|
|
1188
|
+
"""Whether this key is a read operand of this exact Git config invocation.
|
|
1189
|
+
|
|
1190
|
+
Keep the command boundary: a query cannot authorize the next command or
|
|
1191
|
+
turn a later setting into a read. Unknown options remain conservative.
|
|
1192
|
+
"""
|
|
1193
|
+
start = index
|
|
1194
|
+
while start > 0 and tokens[start - 1] not in COMMAND_SEPARATORS:
|
|
1195
|
+
start -= 1
|
|
1196
|
+
end = index + 1
|
|
1197
|
+
while end < len(tokens) and tokens[end] not in COMMAND_SEPARATORS:
|
|
1198
|
+
end += 1
|
|
1199
|
+
program, args, opaque = command_word(tokens[start:end])
|
|
1200
|
+
if opaque or program != "git":
|
|
1201
|
+
return False
|
|
1202
|
+
found = subcommand_after_git(args, 0)
|
|
1203
|
+
if not found or found[0] != "config":
|
|
1204
|
+
return False
|
|
1205
|
+
argv = found[1]
|
|
1206
|
+
key_offset = index - (end - len(argv))
|
|
1207
|
+
read = bool(argv and argv[0] == "get")
|
|
1208
|
+
cursor = 1 if read else 0
|
|
1209
|
+
operands = []
|
|
1210
|
+
read_actions = {"--get", "--get-all", "--get-regexp", "--get-urlmatch"}
|
|
1211
|
+
modifiers = {
|
|
1212
|
+
"--local", "--global", "--system", "--worktree", "--includes",
|
|
1213
|
+
"--no-includes", "--show-origin", "--show-scope", "--null", "-z",
|
|
1214
|
+
"--name-only", "--all", "--regexp",
|
|
1215
|
+
}
|
|
1216
|
+
value_options = {"--file", "-f", "--blob", "--type", "--default", "--value"}
|
|
1217
|
+
while cursor < len(argv):
|
|
1218
|
+
token = argv[cursor]
|
|
1219
|
+
if token in read_actions:
|
|
1220
|
+
if read:
|
|
1221
|
+
return False
|
|
1222
|
+
read = True
|
|
1223
|
+
elif token in value_options:
|
|
1224
|
+
if cursor + 1 >= len(argv):
|
|
1225
|
+
return False
|
|
1226
|
+
cursor += 1
|
|
1227
|
+
elif token in modifiers:
|
|
1228
|
+
pass
|
|
1229
|
+
elif token.startswith("-"):
|
|
1230
|
+
if not any(token.startswith(option + "=") for option in value_options):
|
|
1231
|
+
return False
|
|
1232
|
+
else:
|
|
1233
|
+
operands.append(cursor)
|
|
1234
|
+
cursor += 1
|
|
1235
|
+
return bool(operands and operands[0] == key_offset and (read or len(operands) == 1))
|
|
1236
|
+
|
|
1237
|
+
|
|
1186
1238
|
def token_bypass(tokens):
|
|
1187
1239
|
"""Whether any token disables hooks by environment or by git config.
|
|
1188
1240
|
|
|
@@ -1194,7 +1246,7 @@ def token_bypass(tokens):
|
|
|
1194
1246
|
option, and an option belongs to an argv.
|
|
1195
1247
|
|
|
1196
1248
|
Args:
|
|
1197
|
-
tokens:
|
|
1249
|
+
tokens: Operator-aware tokens of one command or script.
|
|
1198
1250
|
|
|
1199
1251
|
Returns:
|
|
1200
1252
|
True when a token disables verification.
|
|
@@ -1220,7 +1272,9 @@ def token_bypass(tokens):
|
|
|
1220
1272
|
if lowered.startswith("core.hookspath="):
|
|
1221
1273
|
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
1222
1274
|
return True
|
|
1223
|
-
if lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1275
|
+
if (lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1276
|
+
and tokens[i + 1] not in COMMAND_SEPARATORS
|
|
1277
|
+
and not git_config_reads_key(tokens, i)):
|
|
1224
1278
|
if not is_permitted_hooks_path(tokens[i + 1]):
|
|
1225
1279
|
return True
|
|
1226
1280
|
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
@@ -1287,8 +1341,22 @@ def token_bypass(tokens):
|
|
|
1287
1341
|
return False
|
|
1288
1342
|
|
|
1289
1343
|
|
|
1344
|
+
class OperatorStream(io.StringIO):
|
|
1345
|
+
"""Remember punctuation read while shlex is inside a quote or escape."""
|
|
1346
|
+
|
|
1347
|
+
lexer = None
|
|
1348
|
+
literal_operator = False
|
|
1349
|
+
|
|
1350
|
+
def read(self, size=-1):
|
|
1351
|
+
value = super().read(size)
|
|
1352
|
+
if (self.lexer is not None and self.lexer.state in {"'", '"', "\\"}
|
|
1353
|
+
and any(char in "();|&<>" for char in value)):
|
|
1354
|
+
self.literal_operator = True
|
|
1355
|
+
return value
|
|
1356
|
+
|
|
1357
|
+
|
|
1290
1358
|
def flat_tokens(text):
|
|
1291
|
-
"""
|
|
1359
|
+
"""Operator-aware tokens without printed data, or None on a lex failure.
|
|
1292
1360
|
|
|
1293
1361
|
Args:
|
|
1294
1362
|
text: A command line or a script's contents.
|
|
@@ -1297,9 +1365,25 @@ def flat_tokens(text):
|
|
|
1297
1365
|
The token list, or None.
|
|
1298
1366
|
"""
|
|
1299
1367
|
try:
|
|
1300
|
-
|
|
1368
|
+
stream = OperatorStream(line_boundaries_as_separators(text))
|
|
1369
|
+
lexer = shlex.shlex(stream, posix=True, punctuation_chars=True)
|
|
1370
|
+
lexer.whitespace_split = True
|
|
1371
|
+
lexer.commenters = ""
|
|
1372
|
+
stream.lexer = lexer
|
|
1373
|
+
tokens = []
|
|
1374
|
+
while True:
|
|
1375
|
+
stream.literal_operator = False
|
|
1376
|
+
token = lexer.get_token()
|
|
1377
|
+
if token is None:
|
|
1378
|
+
break
|
|
1379
|
+
# shlex otherwise makes quoted ";" indistinguishable from a real
|
|
1380
|
+
# command boundary. Keep literal destinations as data, so a setter
|
|
1381
|
+
# cannot masquerade as a one-operand configuration read.
|
|
1382
|
+
if token in COMMAND_SEPARATORS and stream.literal_operator:
|
|
1383
|
+
token = "\x00" + token
|
|
1384
|
+
tokens.append(token)
|
|
1301
1385
|
return [
|
|
1302
|
-
token
|
|
1386
|
+
token
|
|
1303
1387
|
for index, token in enumerate(tokens)
|
|
1304
1388
|
if not printed_argument(tokens, index)
|
|
1305
1389
|
]
|
|
@@ -152,6 +152,7 @@ fi
|
|
|
152
152
|
|
|
153
153
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
154
154
|
import os
|
|
155
|
+
import io
|
|
155
156
|
import re
|
|
156
157
|
import shlex
|
|
157
158
|
import sys
|
|
@@ -1284,6 +1285,57 @@ def git_skips_verification(text, depth=0):
|
|
|
1284
1285
|
return False
|
|
1285
1286
|
|
|
1286
1287
|
|
|
1288
|
+
def git_config_reads_key(tokens, index):
|
|
1289
|
+
"""Whether this key is a read operand of this exact Git config invocation.
|
|
1290
|
+
|
|
1291
|
+
Keep the command boundary: a query cannot authorize the next command or
|
|
1292
|
+
turn a later setting into a read. Unknown options remain conservative.
|
|
1293
|
+
"""
|
|
1294
|
+
start = index
|
|
1295
|
+
while start > 0 and tokens[start - 1] not in COMMAND_SEPARATORS:
|
|
1296
|
+
start -= 1
|
|
1297
|
+
end = index + 1
|
|
1298
|
+
while end < len(tokens) and tokens[end] not in COMMAND_SEPARATORS:
|
|
1299
|
+
end += 1
|
|
1300
|
+
program, args, opaque = command_word(tokens[start:end])
|
|
1301
|
+
if opaque or program != "git":
|
|
1302
|
+
return False
|
|
1303
|
+
found = subcommand_after_git(args, 0)
|
|
1304
|
+
if not found or found[0] != "config":
|
|
1305
|
+
return False
|
|
1306
|
+
argv = found[1]
|
|
1307
|
+
key_offset = index - (end - len(argv))
|
|
1308
|
+
read = bool(argv and argv[0] == "get")
|
|
1309
|
+
cursor = 1 if read else 0
|
|
1310
|
+
operands = []
|
|
1311
|
+
read_actions = {"--get", "--get-all", "--get-regexp", "--get-urlmatch"}
|
|
1312
|
+
modifiers = {
|
|
1313
|
+
"--local", "--global", "--system", "--worktree", "--includes",
|
|
1314
|
+
"--no-includes", "--show-origin", "--show-scope", "--null", "-z",
|
|
1315
|
+
"--name-only", "--all", "--regexp",
|
|
1316
|
+
}
|
|
1317
|
+
value_options = {"--file", "-f", "--blob", "--type", "--default", "--value"}
|
|
1318
|
+
while cursor < len(argv):
|
|
1319
|
+
token = argv[cursor]
|
|
1320
|
+
if token in read_actions:
|
|
1321
|
+
if read:
|
|
1322
|
+
return False
|
|
1323
|
+
read = True
|
|
1324
|
+
elif token in value_options:
|
|
1325
|
+
if cursor + 1 >= len(argv):
|
|
1326
|
+
return False
|
|
1327
|
+
cursor += 1
|
|
1328
|
+
elif token in modifiers:
|
|
1329
|
+
pass
|
|
1330
|
+
elif token.startswith("-"):
|
|
1331
|
+
if not any(token.startswith(option + "=") for option in value_options):
|
|
1332
|
+
return False
|
|
1333
|
+
else:
|
|
1334
|
+
operands.append(cursor)
|
|
1335
|
+
cursor += 1
|
|
1336
|
+
return bool(operands and operands[0] == key_offset and (read or len(operands) == 1))
|
|
1337
|
+
|
|
1338
|
+
|
|
1287
1339
|
def token_bypass(tokens):
|
|
1288
1340
|
"""Whether any token disables hooks by environment or by git config.
|
|
1289
1341
|
|
|
@@ -1295,7 +1347,7 @@ def token_bypass(tokens):
|
|
|
1295
1347
|
option, and an option belongs to an argv.
|
|
1296
1348
|
|
|
1297
1349
|
Args:
|
|
1298
|
-
tokens:
|
|
1350
|
+
tokens: Operator-aware tokens of one command or script.
|
|
1299
1351
|
|
|
1300
1352
|
Returns:
|
|
1301
1353
|
True when a token disables verification.
|
|
@@ -1321,7 +1373,9 @@ def token_bypass(tokens):
|
|
|
1321
1373
|
if lowered.startswith("core.hookspath="):
|
|
1322
1374
|
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
1323
1375
|
return True
|
|
1324
|
-
if lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1376
|
+
if (lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1377
|
+
and tokens[i + 1] not in COMMAND_SEPARATORS
|
|
1378
|
+
and not git_config_reads_key(tokens, i)):
|
|
1325
1379
|
if not is_permitted_hooks_path(tokens[i + 1]):
|
|
1326
1380
|
return True
|
|
1327
1381
|
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
@@ -1388,8 +1442,22 @@ def token_bypass(tokens):
|
|
|
1388
1442
|
return False
|
|
1389
1443
|
|
|
1390
1444
|
|
|
1445
|
+
class OperatorStream(io.StringIO):
|
|
1446
|
+
"""Remember punctuation read while shlex is inside a quote or escape."""
|
|
1447
|
+
|
|
1448
|
+
lexer = None
|
|
1449
|
+
literal_operator = False
|
|
1450
|
+
|
|
1451
|
+
def read(self, size=-1):
|
|
1452
|
+
value = super().read(size)
|
|
1453
|
+
if (self.lexer is not None and self.lexer.state in {"'", '"', "\\"}
|
|
1454
|
+
and any(char in "();|&<>" for char in value)):
|
|
1455
|
+
self.literal_operator = True
|
|
1456
|
+
return value
|
|
1457
|
+
|
|
1458
|
+
|
|
1391
1459
|
def flat_tokens(text):
|
|
1392
|
-
"""
|
|
1460
|
+
"""Operator-aware tokens without printed data, or None on a lex failure.
|
|
1393
1461
|
|
|
1394
1462
|
Args:
|
|
1395
1463
|
text: A command line or a script's contents.
|
|
@@ -1398,9 +1466,25 @@ def flat_tokens(text):
|
|
|
1398
1466
|
The token list, or None.
|
|
1399
1467
|
"""
|
|
1400
1468
|
try:
|
|
1401
|
-
|
|
1469
|
+
stream = OperatorStream(line_boundaries_as_separators(text))
|
|
1470
|
+
lexer = shlex.shlex(stream, posix=True, punctuation_chars=True)
|
|
1471
|
+
lexer.whitespace_split = True
|
|
1472
|
+
lexer.commenters = ""
|
|
1473
|
+
stream.lexer = lexer
|
|
1474
|
+
tokens = []
|
|
1475
|
+
while True:
|
|
1476
|
+
stream.literal_operator = False
|
|
1477
|
+
token = lexer.get_token()
|
|
1478
|
+
if token is None:
|
|
1479
|
+
break
|
|
1480
|
+
# shlex otherwise makes quoted ";" indistinguishable from a real
|
|
1481
|
+
# command boundary. Keep literal destinations as data, so a setter
|
|
1482
|
+
# cannot masquerade as a one-operand configuration read.
|
|
1483
|
+
if token in COMMAND_SEPARATORS and stream.literal_operator:
|
|
1484
|
+
token = "\x00" + token
|
|
1485
|
+
tokens.append(token)
|
|
1402
1486
|
return [
|
|
1403
|
-
token
|
|
1487
|
+
token
|
|
1404
1488
|
for index, token in enumerate(tokens)
|
|
1405
1489
|
if not printed_argument(tokens, index)
|
|
1406
1490
|
]
|
|
@@ -51,6 +51,7 @@ command -v python3 >/dev/null 2>&1 || allow
|
|
|
51
51
|
|
|
52
52
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
53
53
|
import os
|
|
54
|
+
import io
|
|
54
55
|
import re
|
|
55
56
|
import shlex
|
|
56
57
|
import sys
|
|
@@ -1183,6 +1184,57 @@ def git_skips_verification(text, depth=0):
|
|
|
1183
1184
|
return False
|
|
1184
1185
|
|
|
1185
1186
|
|
|
1187
|
+
def git_config_reads_key(tokens, index):
|
|
1188
|
+
"""Whether this key is a read operand of this exact Git config invocation.
|
|
1189
|
+
|
|
1190
|
+
Keep the command boundary: a query cannot authorize the next command or
|
|
1191
|
+
turn a later setting into a read. Unknown options remain conservative.
|
|
1192
|
+
"""
|
|
1193
|
+
start = index
|
|
1194
|
+
while start > 0 and tokens[start - 1] not in COMMAND_SEPARATORS:
|
|
1195
|
+
start -= 1
|
|
1196
|
+
end = index + 1
|
|
1197
|
+
while end < len(tokens) and tokens[end] not in COMMAND_SEPARATORS:
|
|
1198
|
+
end += 1
|
|
1199
|
+
program, args, opaque = command_word(tokens[start:end])
|
|
1200
|
+
if opaque or program != "git":
|
|
1201
|
+
return False
|
|
1202
|
+
found = subcommand_after_git(args, 0)
|
|
1203
|
+
if not found or found[0] != "config":
|
|
1204
|
+
return False
|
|
1205
|
+
argv = found[1]
|
|
1206
|
+
key_offset = index - (end - len(argv))
|
|
1207
|
+
read = bool(argv and argv[0] == "get")
|
|
1208
|
+
cursor = 1 if read else 0
|
|
1209
|
+
operands = []
|
|
1210
|
+
read_actions = {"--get", "--get-all", "--get-regexp", "--get-urlmatch"}
|
|
1211
|
+
modifiers = {
|
|
1212
|
+
"--local", "--global", "--system", "--worktree", "--includes",
|
|
1213
|
+
"--no-includes", "--show-origin", "--show-scope", "--null", "-z",
|
|
1214
|
+
"--name-only", "--all", "--regexp",
|
|
1215
|
+
}
|
|
1216
|
+
value_options = {"--file", "-f", "--blob", "--type", "--default", "--value"}
|
|
1217
|
+
while cursor < len(argv):
|
|
1218
|
+
token = argv[cursor]
|
|
1219
|
+
if token in read_actions:
|
|
1220
|
+
if read:
|
|
1221
|
+
return False
|
|
1222
|
+
read = True
|
|
1223
|
+
elif token in value_options:
|
|
1224
|
+
if cursor + 1 >= len(argv):
|
|
1225
|
+
return False
|
|
1226
|
+
cursor += 1
|
|
1227
|
+
elif token in modifiers:
|
|
1228
|
+
pass
|
|
1229
|
+
elif token.startswith("-"):
|
|
1230
|
+
if not any(token.startswith(option + "=") for option in value_options):
|
|
1231
|
+
return False
|
|
1232
|
+
else:
|
|
1233
|
+
operands.append(cursor)
|
|
1234
|
+
cursor += 1
|
|
1235
|
+
return bool(operands and operands[0] == key_offset and (read or len(operands) == 1))
|
|
1236
|
+
|
|
1237
|
+
|
|
1186
1238
|
def token_bypass(tokens):
|
|
1187
1239
|
"""Whether any token disables hooks by environment or by git config.
|
|
1188
1240
|
|
|
@@ -1194,7 +1246,7 @@ def token_bypass(tokens):
|
|
|
1194
1246
|
option, and an option belongs to an argv.
|
|
1195
1247
|
|
|
1196
1248
|
Args:
|
|
1197
|
-
tokens:
|
|
1249
|
+
tokens: Operator-aware tokens of one command or script.
|
|
1198
1250
|
|
|
1199
1251
|
Returns:
|
|
1200
1252
|
True when a token disables verification.
|
|
@@ -1220,7 +1272,9 @@ def token_bypass(tokens):
|
|
|
1220
1272
|
if lowered.startswith("core.hookspath="):
|
|
1221
1273
|
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
1222
1274
|
return True
|
|
1223
|
-
if lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1275
|
+
if (lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1276
|
+
and tokens[i + 1] not in COMMAND_SEPARATORS
|
|
1277
|
+
and not git_config_reads_key(tokens, i)):
|
|
1224
1278
|
if not is_permitted_hooks_path(tokens[i + 1]):
|
|
1225
1279
|
return True
|
|
1226
1280
|
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
@@ -1287,8 +1341,22 @@ def token_bypass(tokens):
|
|
|
1287
1341
|
return False
|
|
1288
1342
|
|
|
1289
1343
|
|
|
1344
|
+
class OperatorStream(io.StringIO):
|
|
1345
|
+
"""Remember punctuation read while shlex is inside a quote or escape."""
|
|
1346
|
+
|
|
1347
|
+
lexer = None
|
|
1348
|
+
literal_operator = False
|
|
1349
|
+
|
|
1350
|
+
def read(self, size=-1):
|
|
1351
|
+
value = super().read(size)
|
|
1352
|
+
if (self.lexer is not None and self.lexer.state in {"'", '"', "\\"}
|
|
1353
|
+
and any(char in "();|&<>" for char in value)):
|
|
1354
|
+
self.literal_operator = True
|
|
1355
|
+
return value
|
|
1356
|
+
|
|
1357
|
+
|
|
1290
1358
|
def flat_tokens(text):
|
|
1291
|
-
"""
|
|
1359
|
+
"""Operator-aware tokens without printed data, or None on a lex failure.
|
|
1292
1360
|
|
|
1293
1361
|
Args:
|
|
1294
1362
|
text: A command line or a script's contents.
|
|
@@ -1297,9 +1365,25 @@ def flat_tokens(text):
|
|
|
1297
1365
|
The token list, or None.
|
|
1298
1366
|
"""
|
|
1299
1367
|
try:
|
|
1300
|
-
|
|
1368
|
+
stream = OperatorStream(line_boundaries_as_separators(text))
|
|
1369
|
+
lexer = shlex.shlex(stream, posix=True, punctuation_chars=True)
|
|
1370
|
+
lexer.whitespace_split = True
|
|
1371
|
+
lexer.commenters = ""
|
|
1372
|
+
stream.lexer = lexer
|
|
1373
|
+
tokens = []
|
|
1374
|
+
while True:
|
|
1375
|
+
stream.literal_operator = False
|
|
1376
|
+
token = lexer.get_token()
|
|
1377
|
+
if token is None:
|
|
1378
|
+
break
|
|
1379
|
+
# shlex otherwise makes quoted ";" indistinguishable from a real
|
|
1380
|
+
# command boundary. Keep literal destinations as data, so a setter
|
|
1381
|
+
# cannot masquerade as a one-operand configuration read.
|
|
1382
|
+
if token in COMMAND_SEPARATORS and stream.literal_operator:
|
|
1383
|
+
token = "\x00" + token
|
|
1384
|
+
tokens.append(token)
|
|
1301
1385
|
return [
|
|
1302
|
-
token
|
|
1386
|
+
token
|
|
1303
1387
|
for index, token in enumerate(tokens)
|
|
1304
1388
|
if not printed_argument(tokens, index)
|
|
1305
1389
|
]
|