@codyswann/lisa 4.84.5 → 4.84.7

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 (100) hide show
  1. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  2. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  3. package/dist/core/upstream-evidence-manifest.js +21 -4
  4. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  5. package/package.json +4 -4
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa/hooks/block-host-name-leak.mjs +130 -40
  9. package/plugins/lisa/hooks/block-host-name-leak.sh +5 -0
  10. package/plugins/lisa/hooks/host-name-command-context.mjs +409 -0
  11. package/plugins/lisa-agy/hooks/block-host-name-leak.mjs +130 -40
  12. package/plugins/lisa-agy/hooks/block-host-name-leak.sh +5 -0
  13. package/plugins/lisa-agy/hooks/host-name-command-context.mjs +409 -0
  14. package/plugins/lisa-agy/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  17. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  18. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cdk-devin/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk-devin/.devin-plugin/plugin.json +1 -1
  22. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-copilot/hooks/block-host-name-leak.mjs +130 -40
  24. package/plugins/lisa-copilot/hooks/block-host-name-leak.sh +5 -0
  25. package/plugins/lisa-copilot/hooks/host-name-command-context.mjs +409 -0
  26. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cursor/hooks/block-host-name-leak.mjs +130 -40
  28. package/plugins/lisa-cursor/hooks/block-host-name-leak.sh +5 -0
  29. package/plugins/lisa-cursor/hooks/host-name-command-context.mjs +409 -0
  30. package/plugins/lisa-devin/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-devin/.devin-plugin/plugin.json +1 -1
  32. package/plugins/lisa-devin/hooks/block-host-name-leak.mjs +130 -40
  33. package/plugins/lisa-devin/hooks/block-host-name-leak.sh +5 -0
  34. package/plugins/lisa-devin/hooks/host-name-command-context.mjs +409 -0
  35. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-agy/plugin.json +1 -1
  38. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-expo-devin/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-expo-devin/.devin-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  45. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-harper-fabric-devin/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-harper-fabric-devin/.devin-plugin/plugin.json +1 -1
  49. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  52. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-nestjs-devin/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-nestjs-devin/.devin-plugin/plugin.json +1 -1
  56. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  59. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-openclaw-devin/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-openclaw-devin/.devin-plugin/plugin.json +1 -1
  63. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  66. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-phaser-devin/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-phaser-devin/.devin-plugin/plugin.json +1 -1
  70. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  72. package/plugins/lisa-rails-agy/plugin.json +1 -1
  73. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  74. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-rails-devin/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-rails-devin/.devin-plugin/plugin.json +1 -1
  77. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  78. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  79. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  80. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-typescript-devin/.claude-plugin/plugin.json +1 -1
  83. package/plugins/lisa-typescript-devin/.devin-plugin/plugin.json +1 -1
  84. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  86. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  87. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  88. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  89. package/plugins/lisa-wiki-devin/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-wiki-devin/.devin-plugin/plugin.json +1 -1
  91. package/plugins/src/base/hooks/block-host-name-leak.mjs +130 -40
  92. package/plugins/src/base/hooks/block-host-name-leak.sh +5 -0
  93. package/plugins/src/base/hooks/host-name-command-context.mjs +409 -0
  94. package/scripts/generate-agy-plugin-artifacts.mjs +7 -3
  95. package/scripts/lib/host-name-expanded-command-cases.mjs +286 -0
  96. package/scripts/lib/host-name-field-occurrence-cases.mjs +60 -0
  97. package/scripts/lib/host-name-review-cases.mjs +354 -0
  98. package/scripts/lib/host-name-target-cases.mjs +298 -0
  99. package/scripts/two-channel-couplings.json +29 -6
  100. package/scripts/verify-host-name-target-scope.mjs +373 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "4.84.5",
3
+ "version": "4.84.7",
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.84.5",
3
+ "version": "4.84.7",
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.84.5",
3
+ "version": "4.84.7",
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.84.5",
3
+ "version": "4.84.7",
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.84.5",
3
+ "version": "4.84.7",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -53,6 +53,24 @@
53
53
  * tempted to add it to `plugins/materialized-artifacts.json` should have a
54
54
  * reason why a host project should be refused its own name.
55
55
  *
56
+ * WHERE IT APPLIES: THE TARGET, NOT THE CHECKOUT (#4462)
57
+ *
58
+ * Not materializing it was not enough. The guard is registered by the PLUGIN,
59
+ * so it runs in every project the plugin is enabled in, and every host has the
60
+ * denylist at `node_modules/@codyswann/lisa/dist/`. Inside a host it refused
61
+ * that host's own tracker writes — including the `Work-Item: <owner>/<repo>#<n>`
62
+ * line the work-item gate REQUIRES in every pull-request body, so the two Lisa
63
+ * guards contradicted each other and every agent-opened host PR failed
64
+ * traceability.
65
+ *
66
+ * So the guard resolves the active gh call's explicit repository or API
67
+ * endpoint, then GH_REPO, the locally selected gh default, or one unambiguous
68
+ * Git remote. Literal cd and environment assignments use the command's actual
69
+ * directory; the project root still locates the detector. Body text is never
70
+ * routing. Unsupported shell syntax, unresolved expansions and ambiguous
71
+ * repositories earn no host exemption. This is bounded command inspection,
72
+ * not shell execution or a claim to close every shell/identity leak class.
73
+ *
56
74
  * FAILING OPEN, LOUDLY
57
75
  *
58
76
  * When the denylist cannot be loaded this permits the write and says so on
@@ -65,6 +83,13 @@
65
83
  import { existsSync, readFileSync, realpathSync } from "node:fs";
66
84
  import path from "node:path";
67
85
  import { fileURLToPath, pathToFileURL } from "node:url";
86
+ import {
87
+ commandRepository,
88
+ ghCommands,
89
+ repositoryFromSpec,
90
+ } from "./host-name-command-context.mjs";
91
+
92
+ export { repositoryFromSpec };
68
93
 
69
94
  /** Claude Code's refusal code. Every other non-zero exit is a hook error. */
70
95
  const EXIT_BLOCKED = 2;
@@ -110,10 +135,17 @@ const TEXT_FLAGS = new Set([
110
135
  "-m",
111
136
  ]);
112
137
  /** `gh api` field flags: `key=value`, or `key=@path` to read a file. */
113
- const FIELD_FLAGS = new Set(["-f", "--field", "--raw-field"]);
138
+ const FIELD_FLAGS = new Set(["-f", "-F", "--field", "--raw-field"]);
114
139
  /** Field names whose value is published prose rather than metadata. */
115
140
  const PROSE_FIELDS = new Set(["body", "title", "text", "message"]);
116
141
 
142
+ /**
143
+ * The repositories this guard protects, lowercased `owner/repo`.
144
+ *
145
+ * Only this one: it is public and ships `dist/` to npm. A host's own tracker
146
+ * is not on the list, which is the point (#4462).
147
+ */
148
+ const GUARDED_REPOSITORIES = new Set(["codyswanngt/lisa"]);
117
149
  /**
118
150
  * Vendor endpoints quoted from guard source, which are not host identities.
119
151
  *
@@ -173,30 +205,10 @@ export function tokenize(command) {
173
205
  * @returns True when the command publishes text.
174
206
  */
175
207
  export function isOutboundTrackerWrite(tokens) {
176
- const gh = tokens.indexOf("gh");
177
- if (gh === -1) return false;
178
- const rest = tokens.slice(gh + 1).filter(token => !token.startsWith("-"));
179
- const [noun, verb] = rest;
180
- if (noun === "api") return isMutatingApiCall(tokens);
181
- return WRITE_NOUNS.has(noun ?? "") && WRITE_VERBS.has(verb ?? "");
182
- }
183
-
184
- /**
185
- * Whether a `gh api` call mutates.
186
- *
187
- * `gh api` defaults to GET, and to POST when any field flag is present — so a
188
- * field flag alone makes it a write even with no explicit method.
189
- * @param tokens - Command tokens.
190
- * @returns True when the call changes state.
191
- */
192
- function isMutatingApiCall(tokens) {
193
- const method = tokens.findIndex(
194
- token => token === "-X" || token === "--method"
195
- );
196
- if (method !== -1) {
197
- return MUTATING_METHODS.has((tokens[method + 1] ?? "").toUpperCase());
198
- }
199
- return tokens.some(token => FIELD_FLAGS.has(token));
208
+ return ghCommands(
209
+ tokens.map(token => JSON.stringify(token)).join(" "),
210
+ process.cwd()
211
+ ).some(context => context.writes);
200
212
  }
201
213
 
202
214
  /**
@@ -212,6 +224,32 @@ function valueAt(tokens, index) {
212
224
  return tokens[index + 1] ?? "";
213
225
  }
214
226
 
227
+ /**
228
+ * Resolve the target of one tokenized command without treating prose as routing.
229
+ * @param tokens Command tokens.
230
+ * @param cwd Actual command working directory.
231
+ * @returns Verified owner/repository, or unknown.
232
+ */
233
+ export function targetRepository(tokens, cwd) {
234
+ const context = ghCommands(
235
+ tokens.map(token => JSON.stringify(token)).join(" "),
236
+ cwd
237
+ )[0];
238
+ return context === undefined ? undefined : commandRepository(context);
239
+ }
240
+
241
+ /**
242
+ * Whether the guard applies to a write landing in this repository.
243
+ *
244
+ * An undetermined target is guarded: not knowing where text is going is no
245
+ * reason to stop protecting the one place it must not go.
246
+ * @param target - `owner/repo`, or undefined.
247
+ * @returns True when the write must be checked.
248
+ */
249
+ export function isGuardedTarget(target) {
250
+ return target === undefined || GUARDED_REPOSITORIES.has(target);
251
+ }
252
+
215
253
  /**
216
254
  * Read a file the command names, if it is readable.
217
255
  * @param candidate - Path as written on the command line.
@@ -236,16 +274,30 @@ function readIfPresent(candidate, cwd) {
236
274
  * from so the refusal can point at the thing to edit.
237
275
  * @param tokens - Command tokens.
238
276
  * @param cwd - Directory the command runs in.
277
+ * @param noun - The active gh noun, so api fields retain their own meaning.
239
278
  * @returns Sources of publishable text.
240
279
  */
241
- export function publishableText(tokens, cwd) {
280
+ export function publishableText(tokens, cwd, noun) {
242
281
  return tokens.flatMap((token, index) => {
243
282
  const bare = token.startsWith("-") ? token.split("=")[0] : token;
244
- if (TEXT_FLAGS.has(bare)) {
283
+ if (noun !== "api" && TEXT_FLAGS.has(bare)) {
245
284
  return [{ origin: bare, text: valueAt(tokens, index) }];
246
285
  }
247
286
  if (FILE_FLAGS.has(bare)) {
248
- const file = valueAt(tokens, index).replace(/^@/u, "");
287
+ const value = valueAt(tokens, index);
288
+ // On `gh api`, `-F` is `--field`: `key=@path` or `key=value`. Read as a
289
+ // bare path, `body=@file` named no file and published unchecked (#4462).
290
+ if (
291
+ (noun === "api" || (noun === undefined && bare === "-F")) &&
292
+ /^[\w.[\]-]+=/u.test(value)
293
+ ) {
294
+ const field = fieldSource(value, cwd);
295
+ if (noun === "api") return [field];
296
+ // An unresolved noun may become api or issue/pr. Inspect both possible
297
+ // meanings of -F without evaluating the expansion or certifying routing.
298
+ return [field, { origin: value, text: readIfPresent(value, cwd) }];
299
+ }
300
+ const file = value.replace(/^@/u, "");
249
301
  return [{ origin: file, text: readIfPresent(file, cwd) }];
250
302
  }
251
303
  if (FIELD_FLAGS.has(bare))
@@ -353,19 +405,57 @@ means "no known name appeared", not "this text is safe".`;
353
405
  * @returns The exit code and any message.
354
406
  */
355
407
  export async function evaluate(command, projectDir) {
356
- const tokens = tokenize(command);
357
- if (!isOutboundTrackerWrite(tokens)) return { code: EXIT_ALLOWED };
358
- const detector = await loadDetector(projectDir);
359
- if (detector === undefined) {
360
- return {
361
- code: EXIT_ALLOWED,
362
- warning:
363
- "block-host-name-leak: host-name denylist not found (no built dist/ or installed package); host-identity enforcement is NOT active",
364
- };
408
+ let detector;
409
+ const findings = [];
410
+ for (const context of ghCommands(command, process.cwd())) {
411
+ if (!context.writes || !isGuardedTarget(commandRepository(context)))
412
+ continue;
413
+ detector ??= await loadDetector(projectDir);
414
+ if (detector === undefined) {
415
+ return {
416
+ code: EXIT_ALLOWED,
417
+ warning:
418
+ "block-host-name-leak: host-name denylist not found (no built dist/ or installed package); host-identity enforcement is NOT active",
419
+ };
420
+ }
421
+ // Without a known cwd a relative file cannot be inspected honestly. Keep
422
+ // protection, and name the unread input instead of claiming the text clean.
423
+ if (
424
+ context.cwd === undefined &&
425
+ [
426
+ ...context.flags,
427
+ ...(context.noun === "api" || context.noun === undefined
428
+ ? context.fieldOccurrences
429
+ : []),
430
+ ].some(([flag, item]) => {
431
+ const field =
432
+ (context.noun === "api" || context.noun === undefined) &&
433
+ FIELD_FLAGS.has(flag)
434
+ ? /^([^=]+)=@(.+)$/u.exec(item.value)
435
+ : null;
436
+ const files = [
437
+ field !== null && PROSE_FIELDS.has(field[1]) ? field[2] : undefined,
438
+ FILE_FLAGS.has(flag) && (context.noun !== "api" || field === null)
439
+ ? item.value.replace(/^@/, "")
440
+ : undefined,
441
+ ];
442
+ return files.some(file => file !== undefined && !path.isAbsolute(file));
443
+ })
444
+ ) {
445
+ return {
446
+ code: EXIT_BLOCKED,
447
+ message:
448
+ "BLOCKED: the working directory for this tracker write's body file could not be resolved. Use a literal directory and readable body file so the host-name guard can inspect it.",
449
+ };
450
+ }
451
+ findings.push(
452
+ ...publishableText(
453
+ context.tokens,
454
+ context.cwd ?? projectDir,
455
+ context.noun
456
+ ).flatMap(source => findingsIn(source, detector))
457
+ );
365
458
  }
366
- const findings = publishableText(tokens, projectDir).flatMap(source =>
367
- findingsIn(source, detector)
368
- );
369
459
  if (findings.length === 0) return { code: EXIT_ALLOWED };
370
460
  return { code: EXIT_BLOCKED, message: formatRefusal(findings) };
371
461
  }
@@ -56,6 +56,11 @@ if [ ! -r "$classifier" ]; then
56
56
  exit 0
57
57
  fi
58
58
 
59
+ if [ ! -r "$hook_dir/host-name-command-context.mjs" ]; then
60
+ printf 'block-host-name-leak: command-context helper missing; host-identity enforcement is NOT active\n' >&2
61
+ exit 0
62
+ fi
63
+
59
64
  status=0
60
65
  node "$classifier" "$command_str" "$project_dir" || status=$?
61
66
  exit "$status"