@codyswann/lisa 4.70.3 → 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/history-secret-evidence-shape.mjs +188 -0
- package/all/copy-overwrite/scripts/lib/history-secret-evidence.mjs +271 -0
- package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +6 -296
- 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-history-secrets.mjs +8 -1
- 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 +620 -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 +165 -13
- 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
|
@@ -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
|
]
|
|
@@ -149,6 +149,7 @@ fi
|
|
|
149
149
|
|
|
150
150
|
if ! BLOCK_NO_VERIFY_COMMAND="$command_str" python3 - <<'PY'
|
|
151
151
|
import os
|
|
152
|
+
import io
|
|
152
153
|
import re
|
|
153
154
|
import shlex
|
|
154
155
|
import sys
|
|
@@ -1281,6 +1282,57 @@ def git_skips_verification(text, depth=0):
|
|
|
1281
1282
|
return False
|
|
1282
1283
|
|
|
1283
1284
|
|
|
1285
|
+
def git_config_reads_key(tokens, index):
|
|
1286
|
+
"""Whether this key is a read operand of this exact Git config invocation.
|
|
1287
|
+
|
|
1288
|
+
Keep the command boundary: a query cannot authorize the next command or
|
|
1289
|
+
turn a later setting into a read. Unknown options remain conservative.
|
|
1290
|
+
"""
|
|
1291
|
+
start = index
|
|
1292
|
+
while start > 0 and tokens[start - 1] not in COMMAND_SEPARATORS:
|
|
1293
|
+
start -= 1
|
|
1294
|
+
end = index + 1
|
|
1295
|
+
while end < len(tokens) and tokens[end] not in COMMAND_SEPARATORS:
|
|
1296
|
+
end += 1
|
|
1297
|
+
program, args, opaque = command_word(tokens[start:end])
|
|
1298
|
+
if opaque or program != "git":
|
|
1299
|
+
return False
|
|
1300
|
+
found = subcommand_after_git(args, 0)
|
|
1301
|
+
if not found or found[0] != "config":
|
|
1302
|
+
return False
|
|
1303
|
+
argv = found[1]
|
|
1304
|
+
key_offset = index - (end - len(argv))
|
|
1305
|
+
read = bool(argv and argv[0] == "get")
|
|
1306
|
+
cursor = 1 if read else 0
|
|
1307
|
+
operands = []
|
|
1308
|
+
read_actions = {"--get", "--get-all", "--get-regexp", "--get-urlmatch"}
|
|
1309
|
+
modifiers = {
|
|
1310
|
+
"--local", "--global", "--system", "--worktree", "--includes",
|
|
1311
|
+
"--no-includes", "--show-origin", "--show-scope", "--null", "-z",
|
|
1312
|
+
"--name-only", "--all", "--regexp",
|
|
1313
|
+
}
|
|
1314
|
+
value_options = {"--file", "-f", "--blob", "--type", "--default", "--value"}
|
|
1315
|
+
while cursor < len(argv):
|
|
1316
|
+
token = argv[cursor]
|
|
1317
|
+
if token in read_actions:
|
|
1318
|
+
if read:
|
|
1319
|
+
return False
|
|
1320
|
+
read = True
|
|
1321
|
+
elif token in value_options:
|
|
1322
|
+
if cursor + 1 >= len(argv):
|
|
1323
|
+
return False
|
|
1324
|
+
cursor += 1
|
|
1325
|
+
elif token in modifiers:
|
|
1326
|
+
pass
|
|
1327
|
+
elif token.startswith("-"):
|
|
1328
|
+
if not any(token.startswith(option + "=") for option in value_options):
|
|
1329
|
+
return False
|
|
1330
|
+
else:
|
|
1331
|
+
operands.append(cursor)
|
|
1332
|
+
cursor += 1
|
|
1333
|
+
return bool(operands and operands[0] == key_offset and (read or len(operands) == 1))
|
|
1334
|
+
|
|
1335
|
+
|
|
1284
1336
|
def token_bypass(tokens):
|
|
1285
1337
|
"""Whether any token disables hooks by environment or by git config.
|
|
1286
1338
|
|
|
@@ -1292,7 +1344,7 @@ def token_bypass(tokens):
|
|
|
1292
1344
|
option, and an option belongs to an argv.
|
|
1293
1345
|
|
|
1294
1346
|
Args:
|
|
1295
|
-
tokens:
|
|
1347
|
+
tokens: Operator-aware tokens of one command or script.
|
|
1296
1348
|
|
|
1297
1349
|
Returns:
|
|
1298
1350
|
True when a token disables verification.
|
|
@@ -1318,7 +1370,9 @@ def token_bypass(tokens):
|
|
|
1318
1370
|
if lowered.startswith("core.hookspath="):
|
|
1319
1371
|
if not is_permitted_hooks_path(token.split("=", 1)[1]):
|
|
1320
1372
|
return True
|
|
1321
|
-
if lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1373
|
+
if (lowered == "core.hookspath" and i + 1 < len(tokens)
|
|
1374
|
+
and tokens[i + 1] not in COMMAND_SEPARATORS
|
|
1375
|
+
and not git_config_reads_key(tokens, i)):
|
|
1322
1376
|
if not is_permitted_hooks_path(tokens[i + 1]):
|
|
1323
1377
|
return True
|
|
1324
1378
|
# `git --config-env=core.hooksPath=SOMEVAR` sets the same config, reading
|
|
@@ -1385,8 +1439,22 @@ def token_bypass(tokens):
|
|
|
1385
1439
|
return False
|
|
1386
1440
|
|
|
1387
1441
|
|
|
1442
|
+
class OperatorStream(io.StringIO):
|
|
1443
|
+
"""Remember punctuation read while shlex is inside a quote or escape."""
|
|
1444
|
+
|
|
1445
|
+
lexer = None
|
|
1446
|
+
literal_operator = False
|
|
1447
|
+
|
|
1448
|
+
def read(self, size=-1):
|
|
1449
|
+
value = super().read(size)
|
|
1450
|
+
if (self.lexer is not None and self.lexer.state in {"'", '"', "\\"}
|
|
1451
|
+
and any(char in "();|&<>" for char in value)):
|
|
1452
|
+
self.literal_operator = True
|
|
1453
|
+
return value
|
|
1454
|
+
|
|
1455
|
+
|
|
1388
1456
|
def flat_tokens(text):
|
|
1389
|
-
"""
|
|
1457
|
+
"""Operator-aware tokens without printed data, or None on a lex failure.
|
|
1390
1458
|
|
|
1391
1459
|
Args:
|
|
1392
1460
|
text: A command line or a script's contents.
|
|
@@ -1395,9 +1463,25 @@ def flat_tokens(text):
|
|
|
1395
1463
|
The token list, or None.
|
|
1396
1464
|
"""
|
|
1397
1465
|
try:
|
|
1398
|
-
|
|
1466
|
+
stream = OperatorStream(line_boundaries_as_separators(text))
|
|
1467
|
+
lexer = shlex.shlex(stream, posix=True, punctuation_chars=True)
|
|
1468
|
+
lexer.whitespace_split = True
|
|
1469
|
+
lexer.commenters = ""
|
|
1470
|
+
stream.lexer = lexer
|
|
1471
|
+
tokens = []
|
|
1472
|
+
while True:
|
|
1473
|
+
stream.literal_operator = False
|
|
1474
|
+
token = lexer.get_token()
|
|
1475
|
+
if token is None:
|
|
1476
|
+
break
|
|
1477
|
+
# shlex otherwise makes quoted ";" indistinguishable from a real
|
|
1478
|
+
# command boundary. Keep literal destinations as data, so a setter
|
|
1479
|
+
# cannot masquerade as a one-operand configuration read.
|
|
1480
|
+
if token in COMMAND_SEPARATORS and stream.literal_operator:
|
|
1481
|
+
token = "\x00" + token
|
|
1482
|
+
tokens.append(token)
|
|
1399
1483
|
return [
|
|
1400
|
-
token
|
|
1484
|
+
token
|
|
1401
1485
|
for index, token in enumerate(tokens)
|
|
1402
1486
|
if not printed_argument(tokens, index)
|
|
1403
1487
|
]
|
|
@@ -289,13 +289,14 @@ const RELEASE_TAG = /^v\d+\.\d+\.\d+$/u;
|
|
|
289
289
|
*
|
|
290
290
|
* @param {string} root - repository root.
|
|
291
291
|
* @param {string} rev - the revision to test.
|
|
292
|
+
* @param {string} [head] - pinned scan commit, defaulting to HEAD.
|
|
292
293
|
* @returns {boolean} true when `rev` is reachable from HEAD.
|
|
293
294
|
*/
|
|
294
|
-
function isAncestorOfHead(root, rev) {
|
|
295
|
+
function isAncestorOfHead(root, rev, head = "HEAD") {
|
|
295
296
|
try {
|
|
296
297
|
boundedExecFileSync(
|
|
297
298
|
"git",
|
|
298
|
-
["-C", root, "merge-base", "--is-ancestor", rev,
|
|
299
|
+
["-C", root, "merge-base", "--is-ancestor", rev, head],
|
|
299
300
|
{ stdio: "ignore" }
|
|
300
301
|
);
|
|
301
302
|
return true;
|
|
@@ -321,11 +322,12 @@ function isAncestorOfHead(root, rev) {
|
|
|
321
322
|
*
|
|
322
323
|
* @param {string} root - repository root.
|
|
323
324
|
* @returns {string | null} the tag name, or null when no release tag is reachable.
|
|
325
|
+
* @param {string} [head] - pinned scan commit, defaulting to HEAD.
|
|
324
326
|
*/
|
|
325
|
-
export function oldestReachableReleaseTag(root) {
|
|
327
|
+
export function oldestReachableReleaseTag(root, head = "HEAD") {
|
|
326
328
|
const listed = git(
|
|
327
329
|
root,
|
|
328
|
-
["tag", "--merged",
|
|
330
|
+
["tag", "--merged", head, "--sort=creatordate"],
|
|
329
331
|
"could not list tags reachable from HEAD"
|
|
330
332
|
);
|
|
331
333
|
const tags = listed
|
|
@@ -386,12 +388,13 @@ export function oldestReachableReleaseTag(root) {
|
|
|
386
388
|
* @param {string} root - repository root.
|
|
387
389
|
* @param {string} ref - the baseline as the caller named it, for the message.
|
|
388
390
|
* @param {string} sha - the commit that baseline resolved to.
|
|
391
|
+
* @param {string} [head] - pinned scan commit, defaulting to HEAD.
|
|
389
392
|
* @throws {UsageError} When `sha..HEAD` contains no commits.
|
|
390
393
|
*/
|
|
391
|
-
function assertNonEmptyWindow(root, ref, sha) {
|
|
394
|
+
function assertNonEmptyWindow(root, ref, sha, head = "HEAD") {
|
|
392
395
|
const span = git(
|
|
393
396
|
root,
|
|
394
|
-
["rev-list", "--count", `${sha}
|
|
397
|
+
["rev-list", "--count", `${sha}..${head}`],
|
|
395
398
|
`could not measure the window ${ref}..HEAD`
|
|
396
399
|
).trim();
|
|
397
400
|
if (span === "0") {
|
|
@@ -402,13 +405,21 @@ function assertNonEmptyWindow(root, ref, sha) {
|
|
|
402
405
|
}
|
|
403
406
|
}
|
|
404
407
|
|
|
405
|
-
|
|
408
|
+
/**
|
|
409
|
+
* Resolve the configured or explicit baseline against the same scan commit.
|
|
410
|
+
* @param {string} root - repository root.
|
|
411
|
+
* @param {string} ref - requested baseline.
|
|
412
|
+
* @param {boolean} [mayFallBack] - whether the ledger pin may fall back.
|
|
413
|
+
* @param {string} [head] - pinned scan commit, defaulting to HEAD.
|
|
414
|
+
* @returns {{ sha: string, ref: string, requested: string, substituted: boolean }} baseline.
|
|
415
|
+
*/
|
|
416
|
+
export function resolveBaseline(root, ref, mayFallBack = false, head = "HEAD") {
|
|
406
417
|
const sha = git(
|
|
407
418
|
root,
|
|
408
419
|
["rev-parse", "--verify", `${ref}^{commit}`],
|
|
409
420
|
`baseline ${ref} does not resolve to a commit`
|
|
410
421
|
).trim();
|
|
411
|
-
if (isAncestorOfHead(root, sha)) {
|
|
422
|
+
if (isAncestorOfHead(root, sha, head)) {
|
|
412
423
|
// A reachable baseline can still be HEAD itself, and then the window holds
|
|
413
424
|
// no commits at all. The scan reads nothing and reports every removal
|
|
414
425
|
// governed, which is a confident pass over nothing examined — the failure
|
|
@@ -416,7 +427,7 @@ export function resolveBaseline(root, ref, mayFallBack = false) {
|
|
|
416
427
|
// below already refuses that; an explicit `--since` reached the same state
|
|
417
428
|
// by a different route and did not, so the check belongs on every path out
|
|
418
429
|
// of here rather than on one of them.
|
|
419
|
-
assertNonEmptyWindow(root, ref, sha);
|
|
430
|
+
assertNonEmptyWindow(root, ref, sha, head);
|
|
420
431
|
return { ref, requested: ref, sha, substituted: false };
|
|
421
432
|
}
|
|
422
433
|
if (!mayFallBack) {
|
|
@@ -425,7 +436,7 @@ export function resolveBaseline(root, ref, mayFallBack = false) {
|
|
|
425
436
|
`release history this gate can read`
|
|
426
437
|
);
|
|
427
438
|
}
|
|
428
|
-
const fallback = oldestReachableReleaseTag(root);
|
|
439
|
+
const fallback = oldestReachableReleaseTag(root, head);
|
|
429
440
|
if (fallback === null) {
|
|
430
441
|
throw new UsageError(
|
|
431
442
|
`baseline ${ref} is not an ancestor of HEAD, and no release tag is ` +
|
|
@@ -440,7 +451,7 @@ export function resolveBaseline(root, ref, mayFallBack = false) {
|
|
|
440
451
|
).trim();
|
|
441
452
|
const span = git(
|
|
442
453
|
root,
|
|
443
|
-
["rev-list", "--count", `${fallbackSha}
|
|
454
|
+
["rev-list", "--count", `${fallbackSha}..${head}`],
|
|
444
455
|
`could not measure the window ${fallback}..HEAD`
|
|
445
456
|
).trim();
|
|
446
457
|
if (span === "0") {
|
|
@@ -463,13 +474,22 @@ export function resolveBaseline(root, ref, mayFallBack = false) {
|
|
|
463
474
|
* @param {string} [symbol] - the removed export; when given, the commit is
|
|
464
475
|
* found by pickaxe on that symbol rather than by last touch of the file, so
|
|
465
476
|
* an unrelated later edit does not get the blame.
|
|
477
|
+
* @param {string} [head] - pinned scan commit, defaulting to HEAD.
|
|
466
478
|
* @returns {string} a tag name, or a description of why there is none.
|
|
467
479
|
*/
|
|
468
|
-
export function removalRelease(root, baseline, file, symbol) {
|
|
480
|
+
export function removalRelease(root, baseline, file, symbol, head = "HEAD") {
|
|
469
481
|
const pickaxe = symbol === undefined ? [] : [`-S${symbol}`];
|
|
470
482
|
const sha = git(
|
|
471
483
|
root,
|
|
472
|
-
[
|
|
484
|
+
[
|
|
485
|
+
"log",
|
|
486
|
+
"-1",
|
|
487
|
+
"--format=%H",
|
|
488
|
+
...pickaxe,
|
|
489
|
+
`${baseline}..${head}`,
|
|
490
|
+
"--",
|
|
491
|
+
file,
|
|
492
|
+
],
|
|
473
493
|
`could not date the removal of ${file}`
|
|
474
494
|
).trim();
|
|
475
495
|
if (sha === "") return "an unidentified commit";
|
|
@@ -508,7 +528,7 @@ export function findGoverningDeletion(stack, destination, manifests) {
|
|
|
508
528
|
/**
|
|
509
529
|
* Findings for shipped FILES that existed at the baseline and are gone at HEAD.
|
|
510
530
|
*
|
|
511
|
-
* @param {{ root: string, baseline: string, before: Map<string, object>, after: Map<string, object>, manifests: Map<string, object>, ledger: Map<string, object> }} input
|
|
531
|
+
* @param {{ root: string, baseline: string, head?: string, before: Map<string, object>, after: Map<string, object>, manifests: Map<string, object>, ledger: Map<string, object> }} input
|
|
512
532
|
* the resolved scan inputs.
|
|
513
533
|
* @returns {Array<Record<string, unknown>>} one row per ungoverned removal.
|
|
514
534
|
*/
|
|
@@ -529,7 +549,13 @@ export function findRemovedPaths(input) {
|
|
|
529
549
|
kind: verdict.kind,
|
|
530
550
|
path: file,
|
|
531
551
|
reason: verdict.reason,
|
|
532
|
-
release: removalRelease(
|
|
552
|
+
release: removalRelease(
|
|
553
|
+
input.root,
|
|
554
|
+
input.baseline,
|
|
555
|
+
file,
|
|
556
|
+
undefined,
|
|
557
|
+
input.head
|
|
558
|
+
),
|
|
533
559
|
});
|
|
534
560
|
}
|
|
535
561
|
return rows.sort((left, right) => left.path.localeCompare(right.path));
|
|
@@ -579,16 +605,20 @@ export function classifyRemovedPath(destination, deletion, noted) {
|
|
|
579
605
|
/**
|
|
580
606
|
* Findings for named EXPORTS present at the baseline and gone at HEAD.
|
|
581
607
|
*
|
|
582
|
-
* @param {{ root: string, baseline: string, before: Map<string, object>, after: Map<string, object>, ledger: Map<string, object>, importers: Map<string, number> }} input
|
|
608
|
+
* @param {{ root: string, baseline: string, head?: string, read?: (file: string) => string, before: Map<string, object>, after: Map<string, object>, ledger: Map<string, object>, importers: Map<string, number> }} input
|
|
583
609
|
* the resolved scan inputs.
|
|
584
610
|
* @returns {Array<Record<string, unknown>>} one row per unrecorded export removal.
|
|
585
611
|
*/
|
|
586
612
|
export function findRemovedExports(input) {
|
|
587
613
|
const rows = [];
|
|
588
|
-
for (const file of changedModules(input.root, input.baseline)) {
|
|
614
|
+
for (const file of changedModules(input.root, input.baseline, input.head)) {
|
|
589
615
|
if (!input.before.has(file) || !input.after.has(file)) continue;
|
|
590
616
|
const was = parseNamedExports(showFile(input.root, input.baseline, file));
|
|
591
|
-
const now = parseNamedExports(
|
|
617
|
+
const now = parseNamedExports(
|
|
618
|
+
input.read
|
|
619
|
+
? input.read(file)
|
|
620
|
+
: showFile(input.root, input.head ?? "HEAD", file)
|
|
621
|
+
);
|
|
592
622
|
for (const name of [...was.names].sort()) {
|
|
593
623
|
if (now.names.has(name)) continue;
|
|
594
624
|
if (hasUsableNote(input.ledger.get(removalKey(file, name)))) continue;
|
|
@@ -597,7 +627,13 @@ export function findRemovedExports(input) {
|
|
|
597
627
|
importers: input.importers.get(file) ?? 0,
|
|
598
628
|
kind: "unrecorded-export",
|
|
599
629
|
path: file,
|
|
600
|
-
release: removalRelease(
|
|
630
|
+
release: removalRelease(
|
|
631
|
+
input.root,
|
|
632
|
+
input.baseline,
|
|
633
|
+
file,
|
|
634
|
+
name,
|
|
635
|
+
input.head
|
|
636
|
+
),
|
|
601
637
|
});
|
|
602
638
|
}
|
|
603
639
|
}
|
|
@@ -612,11 +648,12 @@ export function findRemovedExports(input) {
|
|
|
612
648
|
* @param {string} root - repository root.
|
|
613
649
|
* @param {string} baseline - the baseline sha.
|
|
614
650
|
* @returns {string[]} repo-relative paths.
|
|
651
|
+
* @param {string} [head] - pinned scan commit, defaulting to HEAD.
|
|
615
652
|
*/
|
|
616
|
-
function changedModules(root, baseline) {
|
|
653
|
+
function changedModules(root, baseline, head = "HEAD") {
|
|
617
654
|
const stdout = git(
|
|
618
655
|
root,
|
|
619
|
-
["diff", "--name-only", "-z", `${baseline}
|
|
656
|
+
["diff", "--name-only", "-z", `${baseline}..${head}`],
|
|
620
657
|
"could not diff the baseline against HEAD"
|
|
621
658
|
);
|
|
622
659
|
return stdout
|
|
@@ -641,6 +678,37 @@ function showFile(root, ref, file) {
|
|
|
641
678
|
return git(root, ["show", `${ref}:${file}`], `could not read ${file}@${ref}`);
|
|
642
679
|
}
|
|
643
680
|
|
|
681
|
+
/**
|
|
682
|
+
* One complete committed snapshot with an invocation-local blob cache.
|
|
683
|
+
* Resolve mutable refs once; never cache live files, failures or other roots.
|
|
684
|
+
* The existing output bound also caps retained UTF-8 bytes. Once full, reads
|
|
685
|
+
* continue uncached so cache capacity cannot truncate the scan.
|
|
686
|
+
* @param {string} root - repository root.
|
|
687
|
+
* @param {string} ref - revision to resolve to an immutable commit.
|
|
688
|
+
* @returns {{ commit: string, read: (file: string) => string }} snapshot reader.
|
|
689
|
+
*/
|
|
690
|
+
export function createBlobSnapshot(root, ref) {
|
|
691
|
+
const commit = git(
|
|
692
|
+
root,
|
|
693
|
+
["rev-parse", "--verify", `${ref}^{commit}`],
|
|
694
|
+
`could not resolve snapshot ${ref}`
|
|
695
|
+
).trim();
|
|
696
|
+
const cache = new Map();
|
|
697
|
+
let retainedBytes = 0;
|
|
698
|
+
/** @param {string} file - repo path. @returns {string} Complete blob text. */
|
|
699
|
+
const read = file => {
|
|
700
|
+
if (cache.has(file)) return cache.get(file);
|
|
701
|
+
const source = showFile(root, commit, file);
|
|
702
|
+
const bytes = Buffer.byteLength(source, "utf8");
|
|
703
|
+
if (bytes <= MAX_GIT_OUTPUT_BYTES - retainedBytes) {
|
|
704
|
+
cache.set(file, source);
|
|
705
|
+
retainedBytes += bytes;
|
|
706
|
+
}
|
|
707
|
+
return source;
|
|
708
|
+
};
|
|
709
|
+
return { commit, read };
|
|
710
|
+
}
|
|
711
|
+
|
|
644
712
|
/**
|
|
645
713
|
* The delivery view a consumer of `stack` sees: destination path to repo path,
|
|
646
714
|
* more specific stacks winning over their ancestors.
|
|
@@ -932,21 +1000,22 @@ export function countImporters(shipped, read) {
|
|
|
932
1000
|
*/
|
|
933
1001
|
export function scan(opts) {
|
|
934
1002
|
const ledger = loadLedger(opts.root);
|
|
1003
|
+
const { commit: head, read } = createBlobSnapshot(opts.root, "HEAD");
|
|
935
1004
|
const resolved = resolveBaseline(
|
|
936
1005
|
opts.root,
|
|
937
1006
|
opts.since ?? ledger.baseline,
|
|
938
|
-
opts.since === null || opts.since === undefined
|
|
1007
|
+
opts.since === null || opts.since === undefined,
|
|
1008
|
+
head
|
|
939
1009
|
);
|
|
940
1010
|
const baseline = resolved.sha;
|
|
941
1011
|
const before = shippedFilesAt(opts.root, baseline);
|
|
942
|
-
const after = shippedFilesAt(opts.root,
|
|
1012
|
+
const after = shippedFilesAt(opts.root, head);
|
|
943
1013
|
if (after.size === 0) {
|
|
944
1014
|
throw new UsageError(
|
|
945
1015
|
`no shipped files found under ${opts.root} - refusing to report a clean ` +
|
|
946
1016
|
`scan of a tree this gate could not read`
|
|
947
1017
|
);
|
|
948
1018
|
}
|
|
949
|
-
const read = file => showFile(opts.root, "HEAD", file);
|
|
950
1019
|
const index = indexRemovals(ledger.removals);
|
|
951
1020
|
const findings = {
|
|
952
1021
|
exports: findRemovedExports({
|
|
@@ -954,7 +1023,9 @@ export function scan(opts) {
|
|
|
954
1023
|
baseline,
|
|
955
1024
|
before,
|
|
956
1025
|
importers: countImporters(after, read),
|
|
1026
|
+
head,
|
|
957
1027
|
ledger: index,
|
|
1028
|
+
read,
|
|
958
1029
|
root: opts.root,
|
|
959
1030
|
}),
|
|
960
1031
|
imports: findUnresolvedImports({ read, shipped: after }),
|
|
@@ -967,6 +1038,7 @@ export function scan(opts) {
|
|
|
967
1038
|
after,
|
|
968
1039
|
baseline,
|
|
969
1040
|
before,
|
|
1041
|
+
head,
|
|
970
1042
|
ledger: index,
|
|
971
1043
|
manifests: readDeletionManifests(opts.root),
|
|
972
1044
|
root: opts.root,
|