@codyswann/lisa 4.70.4 → 4.71.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.
Files changed (139) hide show
  1. package/all/copy-overwrite/scripts/lib/automation-provenance-contract.mjs +174 -0
  2. package/all/copy-overwrite/scripts/lib/automation-provenance-local.mjs +279 -0
  3. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +17 -7
  4. package/all/copy-overwrite/scripts/lib/github-attestation-provider.mjs +241 -0
  5. package/all/copy-overwrite/scripts/lib/github-attestation-recovery.mjs +283 -0
  6. package/all/copy-overwrite/scripts/lib/github-attestation-verifier.mjs +244 -0
  7. package/all/copy-overwrite/scripts/lib/npm-update-allocate.mjs +289 -0
  8. package/all/copy-overwrite/scripts/lib/npm-update-authorization.mjs +235 -0
  9. package/all/copy-overwrite/scripts/lib/npm-update-broker-client.mjs +92 -0
  10. package/all/copy-overwrite/scripts/lib/npm-update-checkpoint.mjs +241 -0
  11. package/all/copy-overwrite/scripts/lib/npm-update-contract.mjs +278 -0
  12. package/all/copy-overwrite/scripts/lib/npm-update-controller-broker.mjs +234 -0
  13. package/all/copy-overwrite/scripts/lib/npm-update-controller-factory.mjs +278 -0
  14. package/all/copy-overwrite/scripts/lib/npm-update-controller-recipe.mjs +135 -0
  15. package/all/copy-overwrite/scripts/lib/npm-update-execution-adapter.mjs +292 -0
  16. package/all/copy-overwrite/scripts/lib/npm-update-gate-hooks.mjs +214 -0
  17. package/all/copy-overwrite/scripts/lib/npm-update-gate-install.mjs +267 -0
  18. package/all/copy-overwrite/scripts/lib/npm-update-gate-proof.mjs +99 -0
  19. package/all/copy-overwrite/scripts/lib/npm-update-gate.mjs +300 -0
  20. package/all/copy-overwrite/scripts/lib/npm-update-gh-dispatch.mjs +182 -0
  21. package/all/copy-overwrite/scripts/lib/npm-update-gh-grants.mjs +156 -0
  22. package/all/copy-overwrite/scripts/lib/npm-update-gh-requests.mjs +221 -0
  23. package/all/copy-overwrite/scripts/lib/npm-update-github.mjs +214 -0
  24. package/all/copy-overwrite/scripts/lib/npm-update-helper-graph.mjs +238 -0
  25. package/all/copy-overwrite/scripts/lib/npm-update-helper-inventory.mjs +126 -0
  26. package/all/copy-overwrite/scripts/lib/npm-update-helper.mjs +292 -0
  27. package/all/copy-overwrite/scripts/lib/npm-update-hook-installation.mjs +116 -0
  28. package/all/copy-overwrite/scripts/lib/npm-update-hook-preload.mjs +153 -0
  29. package/all/copy-overwrite/scripts/lib/npm-update-hook-provider.mjs +207 -0
  30. package/all/copy-overwrite/scripts/lib/npm-update-hook-read-client.mjs +164 -0
  31. package/all/copy-overwrite/scripts/lib/npm-update-hosted-gate.mjs +300 -0
  32. package/all/copy-overwrite/scripts/lib/npm-update-hosted-hook.mjs +157 -0
  33. package/all/copy-overwrite/scripts/lib/npm-update-invariants.mjs +28 -0
  34. package/all/copy-overwrite/scripts/lib/npm-update-isolation.mjs +11 -0
  35. package/all/copy-overwrite/scripts/lib/npm-update-leaf-contract.mjs +171 -0
  36. package/all/copy-overwrite/scripts/lib/npm-update-leaf.mjs +194 -0
  37. package/all/copy-overwrite/scripts/lib/npm-update-native-process.mjs +155 -0
  38. package/all/copy-overwrite/scripts/lib/npm-update-npm.mjs +190 -0
  39. package/all/copy-overwrite/scripts/lib/npm-update-object.mjs +112 -0
  40. package/all/copy-overwrite/scripts/lib/npm-update-orchestrator.mjs +273 -0
  41. package/all/copy-overwrite/scripts/lib/npm-update-owner.mjs +292 -0
  42. package/all/copy-overwrite/scripts/lib/npm-update-prepare.mjs +263 -0
  43. package/all/copy-overwrite/scripts/lib/npm-update-process-core.mjs +179 -0
  44. package/all/copy-overwrite/scripts/lib/npm-update-process.mjs +16 -0
  45. package/all/copy-overwrite/scripts/lib/npm-update-publication.mjs +258 -0
  46. package/all/copy-overwrite/scripts/lib/npm-update-publish.mjs +254 -0
  47. package/all/copy-overwrite/scripts/lib/npm-update-quality.mjs +153 -0
  48. package/all/copy-overwrite/scripts/lib/npm-update-recovery.mjs +264 -0
  49. package/all/copy-overwrite/scripts/lib/npm-update-runtime-archive.mjs +204 -0
  50. package/all/copy-overwrite/scripts/lib/npm-update-runtime-graph.mjs +185 -0
  51. package/all/copy-overwrite/scripts/lib/npm-update-runtime-transport.mjs +221 -0
  52. package/all/copy-overwrite/scripts/lib/npm-update-runtime.mjs +284 -0
  53. package/all/copy-overwrite/scripts/lib/npm-update-tool-launcher.mjs +287 -0
  54. package/all/copy-overwrite/scripts/lib/npm-update-worker-environment.mjs +161 -0
  55. package/all/copy-overwrite/scripts/lib/npm-update-worker-inspection.mjs +142 -0
  56. package/all/copy-overwrite/scripts/lib/npm-update-worker-lifecycle.mjs +256 -0
  57. package/all/copy-overwrite/scripts/lib/npm-update-worker-policy.mjs +266 -0
  58. package/all/copy-overwrite/scripts/lib/process-tree-runner.mjs +230 -36
  59. package/all/copy-overwrite/scripts/lisa-automation-provenance.mjs +161 -0
  60. package/all/copy-overwrite/scripts/lisa-hooks/block-no-verify.sh +89 -5
  61. package/all/copy-overwrite/scripts/lisa-npm-updater.mjs +204 -0
  62. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +48 -123
  63. package/all/copy-overwrite/scripts/lisa-work-item.mjs +107 -41
  64. package/all/copy-overwrite/scripts/npm-updater-gate-runtime.json +1 -0
  65. package/all/copy-overwrite/scripts/npm-updater-gate-supervisor.c +121 -0
  66. package/all/copy-overwrite/scripts/npm-updater-gate.Dockerfile +49 -0
  67. package/all/copy-overwrite/scripts/npm-updater-helper-graph.json +1085 -0
  68. package/dist/codex/scripts/block-no-verify.sh +89 -5
  69. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  70. package/dist/core/lisa-owned-hash-ledger.js +604 -0
  71. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  72. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  73. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  74. package/dist/core/upstream-evidence-manifest.js +159 -11
  75. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  76. package/dist/opencode/plugin-templates/block-no-verify.sh +89 -5
  77. package/package.json +4 -4
  78. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  79. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  80. package/plugins/lisa/hooks/block-no-verify.agy.sh +89 -5
  81. package/plugins/lisa/hooks/block-no-verify.sh +89 -5
  82. package/plugins/lisa-agy/hooks/block-no-verify.agy.sh +89 -5
  83. package/plugins/lisa-agy/plugin.json +1 -1
  84. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  87. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-copilot/hooks/block-no-verify.sh +89 -5
  91. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-cursor/hooks/block-no-verify.sh +89 -5
  93. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  94. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  95. package/plugins/lisa-expo-agy/plugin.json +1 -1
  96. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  98. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  99. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  100. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  101. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  104. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  105. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  106. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  109. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  110. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  111. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  112. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  113. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  114. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  115. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  116. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  117. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  118. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  119. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  120. package/plugins/lisa-rails-agy/plugin.json +1 -1
  121. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  122. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  123. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  124. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  125. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  126. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  127. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  128. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  129. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  130. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  131. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  132. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  133. package/plugins/src/base/hooks/block-no-verify.agy.sh +89 -5
  134. package/plugins/src/base/hooks/block-no-verify.sh +89 -5
  135. package/scripts/check-shipped-surface-removals.mjs +96 -24
  136. package/scripts/generate-npm-updater-helper-graph.mjs +184 -0
  137. package/scripts/two-channel-couplings.json +31 -7
  138. package/typescript/copy-contents/.husky/commit-msg +18 -0
  139. 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: Punctuation-stripped tokens of one command or script.
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
- """Punctuation-stripped tokens, or None when the text does not lex.
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
- tokens = shell_tokens(text)
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.strip("();|&")
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.70.4",
192
+ "version": "4.71.1",
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": "d839499d0fa795604c1dac28224eb960d7af1a55",
349
- "gitHead": "d839499d0fa795604c1dac28224eb960d7af1a55",
350
- "lisaReleaseTag": "v4.70.4"
348
+ "lisaReleaseCommit": "d0b0b904116419d726e950ab45111598b41c764e",
349
+ "gitHead": "d0b0b904116419d726e950ab45111598b41c764e",
350
+ "lisaReleaseTag": "v4.71.1"
351
351
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -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: Punctuation-stripped tokens of one command or script.
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
- """Punctuation-stripped tokens, or None when the text does not lex.
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
- tokens = shell_tokens(text)
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.strip("();|&")
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: Punctuation-stripped tokens of one command or script.
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
- """Punctuation-stripped tokens, or None when the text does not lex.
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
- tokens = shell_tokens(text)
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.strip("();|&")
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: Punctuation-stripped tokens of one command or script.
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
- """Punctuation-stripped tokens, or None when the text does not lex.
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
- tokens = shell_tokens(text)
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.strip("();|&")
1386
+ token
1303
1387
  for index, token in enumerate(tokens)
1304
1388
  if not printed_argument(tokens, index)
1305
1389
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "4.70.4",
3
+ "version": "4.71.1",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"